import { describe, expect, it, vi } from 'vitest'

import {
  configureDetectedTerminalKeybindings,
  configureTerminalKeybindings,
  detectVSCodeLikeTerminal,
  getVSCodeStyleConfigDir,
  shouldPromptForTerminalSetup,
  stripJsonComments
} from '../lib/terminalSetup.js'

describe('terminalSetup helpers', () => {
  it('detects VS Code family terminals from environment', () => {
    expect(detectVSCodeLikeTerminal({ CURSOR_TRACE_ID: 'x' } as NodeJS.ProcessEnv)).toBe('cursor')
    expect(detectVSCodeLikeTerminal({ VSCODE_GIT_ASKPASS_MAIN: '/tmp/windsurf' } as NodeJS.ProcessEnv)).toBe('windsurf')
    expect(detectVSCodeLikeTerminal({ TERM_PROGRAM: 'vscode' } as NodeJS.ProcessEnv)).toBe('vscode')
    expect(detectVSCodeLikeTerminal({} as NodeJS.ProcessEnv)).toBeNull()
  })

  it('computes VS Code style config dirs cross-platform', () => {
    expect(getVSCodeStyleConfigDir('Code', 'darwin', {} as NodeJS.ProcessEnv, '/home/me')).toBe(
      '/home/me/Library/Application Support/Code/User'
    )
    expect(getVSCodeStyleConfigDir('Code', 'linux', {} as NodeJS.ProcessEnv, '/home/me')).toBe(
      '/home/me/.config/Code/User'
    )
    expect(
      getVSCodeStyleConfigDir(
        'Code',
        'win32',
        { APPDATA: 'C:/Users/me/AppData/Roaming' } as NodeJS.ProcessEnv,
        '/home/me'
      )
    ).toBe('C:/Users/me/AppData/Roaming/Code/User')
  })

  it('strips line comments from keybindings JSON', () => {
    expect(stripJsonComments('// comment\n[{"key":"shift+enter"}]')).toBe('\n[{"key":"shift+enter"}]')
  })

  it('strips inline comments and block comments', () => {
    expect(stripJsonComments('[{"key":"a"} // inline\n]')).toBe('[{"key":"a"} \n]')
    expect(stripJsonComments('[/* block */{"key":"a"}]')).toBe('[{"key":"a"}]')
  })

  it('removes trailing commas before ] or }', () => {
    expect(JSON.parse(stripJsonComments('[{"key":"a"},]'))).toEqual([{ key: 'a' }])
    expect(JSON.parse(stripJsonComments('[{"key":"a",}]'))).toEqual([{ key: 'a' }])
  })

  it('preserves comment-like sequences inside strings', () => {
    const input = '[{"key":"a","args":{"text":"// not a comment"}}]'
    expect(JSON.parse(stripJsonComments(input))).toEqual([{ key: 'a', args: { text: '// not a comment' } }])
  })

  it('handles unterminated block comments gracefully', () => {
    const input = '[{"key":"a"} /* never closed'
    const stripped = stripJsonComments(input)
    // The unterminated comment is consumed to end-of-file; the remainder is parseable
    expect(stripped).toBe('[{"key":"a"} ')
  })
})

describe('configureTerminalKeybindings', () => {
  it('writes missing bindings into a VS Code style keybindings file', async () => {
    const mkdir = vi.fn().mockResolvedValue(undefined)
    const readFile = vi.fn().mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' }))
    const writeFile = vi.fn().mockResolvedValue(undefined)
    const copyFile = vi.fn().mockResolvedValue(undefined)

    const result = await configureTerminalKeybindings('vscode', {
      fileOps: { copyFile, mkdir, readFile, writeFile },
      homeDir: '/Users/me',
      platform: 'darwin'
    })

    expect(result.success).toBe(true)
    expect(result.requiresRestart).toBe(true)
    expect(writeFile).toHaveBeenCalledTimes(1)
    expect(copyFile).not.toHaveBeenCalled() // no existing file to back up
    const written = writeFile.mock.calls[0]?.[1] as string
    expect(written).toContain('cmd+c')
    expect(written).toContain('terminalTextSelected')
    expect(written).toContain('\\u001b[99;13u')
    expect(written).toContain('shift+enter')
    expect(written).toContain('cmd+enter')
    expect(written).toContain('cmd+z')
  })

  it('only adds the Cmd+C forwarding binding on macOS', async () => {
    const mkdir = vi.fn().mockResolvedValue(undefined)
    const readFile = vi.fn().mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' }))
    const writeFile = vi.fn().mockResolvedValue(undefined)
    const copyFile = vi.fn().mockResolvedValue(undefined)

    const result = await configureTerminalKeybindings('vscode', {
      fileOps: { copyFile, mkdir, readFile, writeFile },
      homeDir: '/home/me',
      platform: 'linux'
    })

    expect(result.success).toBe(true)
    const written = writeFile.mock.calls[0]?.[1] as string
    expect(written).not.toContain('cmd+c')
    expect(written).not.toContain('terminalTextSelected')
    expect(written).not.toContain('\\u001b[99;13u')
    expect(written).toContain('shift+enter')
  })

  it('reports conflicts without overwriting existing bindings', async () => {
    const mkdir = vi.fn().mockResolvedValue(undefined)

    const readFile = vi.fn().mockResolvedValue(
      JSON.stringify([
        {
          key: 'cmd+z',
          command: 'something.else',
          when: 'terminalFocus',
          args: { text: 'noop' }
        }
      ])
    )

    const writeFile = vi.fn().mockResolvedValue(undefined)
    const copyFile = vi.fn().mockResolvedValue(undefined)

    const result = await configureTerminalKeybindings('cursor', {
      fileOps: { copyFile, mkdir, readFile, writeFile },
      homeDir: '/Users/me',
      platform: 'darwin'
    })

    expect(result.success).toBe(false)
    expect(result.message).toContain('cmd+z')
    expect(writeFile).not.toHaveBeenCalled()
    expect(copyFile).not.toHaveBeenCalled() // no backup when not writing
  })

  it('flags a global (when-less) binding on the same key as a conflict', async () => {
    // A user's keybindings.json `cmd+c` with no `when` clause is global —
    // it overlaps any context, including our terminal scope. We must NOT
    // silently add a terminal-scoped cmd+c that would shadow it.
    const mkdir = vi.fn().mockResolvedValue(undefined)

    const readFile = vi.fn().mockResolvedValue(
      JSON.stringify([
        {
          key: 'cmd+c',
          command: 'myExtension.smartCopy'
        }
      ])
    )

    const writeFile = vi.fn().mockResolvedValue(undefined)
    const copyFile = vi.fn().mockResolvedValue(undefined)

    const result = await configureTerminalKeybindings('vscode', {
      fileOps: { copyFile, mkdir, readFile, writeFile },
      homeDir: '/Users/me',
      platform: 'darwin'
    })

    expect(result.success).toBe(false)
    expect(result.message).toContain('cmd+c')
    expect(writeFile).not.toHaveBeenCalled()
  })

  it('flags an overlapping terminal-context binding as a conflict', async () => {
    // Existing `cmd+c` scoped to plain `terminalFocus` overlaps with our
    // `terminalFocus && terminalTextSelected` — both fire when the
    // terminal is focused with text selected, so the existing binding
    // would shadow ours. Treat as a conflict even though the strings
    // aren't identical.
    const mkdir = vi.fn().mockResolvedValue(undefined)

    const readFile = vi.fn().mockResolvedValue(
      JSON.stringify([
        {
          key: 'cmd+c',
          command: 'workbench.action.terminal.copySelection',
          when: 'terminalFocus'
        }
      ])
    )

    const writeFile = vi.fn().mockResolvedValue(undefined)
    const copyFile = vi.fn().mockResolvedValue(undefined)

    const result = await configureTerminalKeybindings('vscode', {
      fileOps: { copyFile, mkdir, readFile, writeFile },
      homeDir: '/Users/me',
      platform: 'darwin'
    })

    expect(result.success).toBe(false)
    expect(result.message).toContain('cmd+c')
    expect(writeFile).not.toHaveBeenCalled()
  })

  it('does not flag a negated terminalTextSelected binding as a conflict', async () => {
    // A binding scoped to "terminal focused but no selected text" is
    // logically disjoint from our copy-forwarding binding, which requires
    // terminalTextSelected.
    const mkdir = vi.fn().mockResolvedValue(undefined)

    const readFile = vi.fn().mockResolvedValue(
      JSON.stringify([
        {
          key: 'cmd+c',
          command: 'workbench.action.terminal.sendSequence',
          when: 'terminalFocus && !terminalTextSelected',
          args: { text: '\u0003' }
        }
      ])
    )

    const writeFile = vi.fn().mockResolvedValue(undefined)
    const copyFile = vi.fn().mockResolvedValue(undefined)

    const result = await configureTerminalKeybindings('vscode', {
      fileOps: { copyFile, mkdir, readFile, writeFile },
      homeDir: '/Users/me',
      platform: 'darwin'
    })

    expect(result.success).toBe(true)
    expect(writeFile).toHaveBeenCalledTimes(1)
  })

  it('does not flag a disjoint-when binding on the same key as a conflict', async () => {
    // VS Code allows multiple bindings for the same key when their `when`
    // clauses don't overlap. A user's pre-existing cmd+c binding scoped to
    // editor focus should NOT block our terminal-scoped cmd+c binding.
    const mkdir = vi.fn().mockResolvedValue(undefined)

    const readFile = vi.fn().mockResolvedValue(
      JSON.stringify([
        {
          key: 'cmd+c',
          command: 'editor.action.clipboardCopyAction',
          when: 'editorFocus'
        }
      ])
    )

    const writeFile = vi.fn().mockResolvedValue(undefined)
    const copyFile = vi.fn().mockResolvedValue(undefined)

    const result = await configureTerminalKeybindings('vscode', {
      fileOps: { copyFile, mkdir, readFile, writeFile },
      homeDir: '/Users/me',
      platform: 'darwin'
    })

    expect(result.success).toBe(true)
    expect(writeFile).toHaveBeenCalledTimes(1)
  })

  it('backs up existing keybindings.json only when writing changes', async () => {
    const mkdir = vi.fn().mockResolvedValue(undefined)
    const readFile = vi.fn().mockResolvedValue(JSON.stringify([]))
    const writeFile = vi.fn().mockResolvedValue(undefined)
    const copyFile = vi.fn().mockResolvedValue(undefined)

    const result = await configureTerminalKeybindings('vscode', {
      fileOps: { copyFile, mkdir, readFile, writeFile },
      homeDir: '/Users/me',
      platform: 'darwin'
    })

    expect(result.success).toBe(true)
    expect(writeFile).toHaveBeenCalledTimes(1)
    expect(copyFile).toHaveBeenCalledTimes(1) // backup created before writing
  })

  it('reports error when keybindings.json is not readable (EACCES)', async () => {
    const mkdir = vi.fn().mockResolvedValue(undefined)
    const readFile = vi.fn().mockRejectedValue(Object.assign(new Error('permission denied'), { code: 'EACCES' }))
    const writeFile = vi.fn().mockResolvedValue(undefined)
    const copyFile = vi.fn().mockResolvedValue(undefined)

    const result = await configureTerminalKeybindings('vscode', {
      fileOps: { copyFile, mkdir, readFile, writeFile },
      homeDir: '/Users/me',
      platform: 'darwin'
    })

    expect(result.success).toBe(false)
    expect(result.message).toContain('Failed to read')
    expect(writeFile).not.toHaveBeenCalled()
  })

  it('auto-detects the current IDE terminal', async () => {
    const mkdir = vi.fn().mockResolvedValue(undefined)
    const readFile = vi.fn().mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' }))
    const writeFile = vi.fn().mockResolvedValue(undefined)
    const copyFile = vi.fn().mockResolvedValue(undefined)

    const result = await configureDetectedTerminalKeybindings({
      env: { CURSOR_TRACE_ID: 'trace' } as NodeJS.ProcessEnv,
      fileOps: { copyFile, mkdir, readFile, writeFile },
      homeDir: '/Users/me',
      platform: 'darwin'
    })

    expect(result.success).toBe(true)
    expect(writeFile).toHaveBeenCalled()
  })

  it('refuses to configure IDE bindings from an SSH session', async () => {
    const result = await configureDetectedTerminalKeybindings({
      env: { SSH_CONNECTION: '1 2 3 4', TERM_PROGRAM: 'vscode' } as NodeJS.ProcessEnv,
      homeDir: '/Users/me',
      platform: 'darwin'
    })

    expect(result.success).toBe(false)
    expect(result.message).toContain('local machine')
  })

  it('prompts for setup when bindings are missing and suppresses prompt when complete', async () => {
    const readMissing = vi.fn().mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' }))
    await expect(
      shouldPromptForTerminalSetup({
        env: { TERM_PROGRAM: 'vscode' } as NodeJS.ProcessEnv,
        fileOps: { readFile: readMissing }
      })
    ).resolves.toBe(true)

    const readComplete = vi.fn().mockResolvedValue(
      JSON.stringify([
        {
          key: 'cmd+c',
          command: 'workbench.action.terminal.sendSequence',
          when: 'terminalFocus && terminalTextSelected',
          args: { text: '\u001b[99;13u' }
        },
        {
          key: 'shift+enter',
          command: 'workbench.action.terminal.sendSequence',
          when: 'terminalFocus',
          args: { text: '\\\r\n' }
        },
        {
          key: 'ctrl+enter',
          command: 'workbench.action.terminal.sendSequence',
          when: 'terminalFocus',
          args: { text: '\\\r\n' }
        },
        {
          key: 'cmd+enter',
          command: 'workbench.action.terminal.sendSequence',
          when: 'terminalFocus',
          args: { text: '\\\r\n' }
        },
        {
          key: 'cmd+z',
          command: 'workbench.action.terminal.sendSequence',
          when: 'terminalFocus',
          args: { text: '\u001b[122;9u' }
        },
        {
          key: 'shift+cmd+z',
          command: 'workbench.action.terminal.sendSequence',
          when: 'terminalFocus',
          args: { text: '\u001b[122;10u' }
        }
      ])
    )

    await expect(
      shouldPromptForTerminalSetup({
        env: { TERM_PROGRAM: 'vscode' } as NodeJS.ProcessEnv,
        fileOps: { readFile: readComplete }
      })
    ).resolves.toBe(false)
  })

  it('suppresses terminal setup prompts inside SSH sessions', async () => {
    await expect(
      shouldPromptForTerminalSetup({
        env: { SSH_CONNECTION: '1 2 3 4', TERM_PROGRAM: 'vscode' } as NodeJS.ProcessEnv
      })
    ).resolves.toBe(false)
  })
})
