/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {
  vi,
  describe,
  it,
  expect,
  beforeEach,
  afterEach,
  type Mock,
} from 'vitest';
import {
  hasValidEditorCommand,
  hasValidEditorCommandAsync,
  getDiffCommand,
  openDiff,
  allowEditorTypeInSandbox,
  isEditorAvailable,
  isEditorAvailableAsync,
  isValidEditorType,
  getEditorWaitFlag,
  getEditorExtraArgs,
  resolveEditorAsync,
  resolveEditorTypeFromCommand,
  type EditorType,
} from './editor.js';
import { coreEvents, CoreEvent } from './events.js';
import { exec, execSync, spawn, spawnSync } from 'node:child_process';
import { debugLogger } from './debugLogger.js';

vi.mock('child_process', () => ({
  exec: vi.fn(),
  execSync: vi.fn(),
  spawn: vi.fn(),
  spawnSync: vi.fn(() => ({ error: null, status: 0 })),
}));

const originalPlatform = process.platform;

describe('editor utils', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.unstubAllEnvs();
    Object.defineProperty(process, 'platform', {
      value: originalPlatform,
      writable: true,
    });
  });

  afterEach(() => {
    vi.restoreAllMocks();
    vi.unstubAllEnvs();
    Object.defineProperty(process, 'platform', {
      value: originalPlatform,
      writable: true,
    });
  });

  describe('hasValidEditorCommand', () => {
    const testCases: Array<{
      editor: EditorType;
      commands: string[];
      win32Commands: string[];
    }> = [
      { editor: 'vscode', commands: ['code'], win32Commands: ['code.cmd'] },
      {
        editor: 'vscodium',
        commands: ['codium'],
        win32Commands: ['codium.cmd'],
      },
      {
        editor: 'windsurf',
        commands: ['windsurf'],
        win32Commands: ['windsurf'],
      },
      { editor: 'cursor', commands: ['cursor'], win32Commands: ['cursor'] },
      { editor: 'vim', commands: ['vim'], win32Commands: ['vim'] },
      { editor: 'neovim', commands: ['nvim'], win32Commands: ['nvim'] },
      { editor: 'zed', commands: ['zed', 'zeditor'], win32Commands: ['zed'] },
      { editor: 'emacs', commands: ['emacs'], win32Commands: ['emacs.exe'] },
      {
        editor: 'antigravity',
        commands: ['agy', 'antigravity'],
        win32Commands: ['agy.cmd', 'antigravity.cmd', 'antigravity'],
      },
      { editor: 'hx', commands: ['hx'], win32Commands: ['hx'] },
      {
        editor: 'sublimetext',
        commands: ['subl'],
        win32Commands: ['subl'],
      },
      { editor: 'lapce', commands: ['lapce'], win32Commands: ['lapce'] },
      { editor: 'nova', commands: ['nova'], win32Commands: ['nova'] },
      { editor: 'bbedit', commands: ['bbedit'], win32Commands: ['bbedit'] },
      {
        editor: 'emacsclient',
        commands: ['emacsclient'],
        win32Commands: ['emacsclient'],
      },
      { editor: 'micro', commands: ['micro'], win32Commands: ['micro'] },
    ];

    for (const { editor, commands, win32Commands } of testCases) {
      describe(`${editor}`, () => {
        // Non-windows tests
        it(`should return true if first command "${commands[0]}" exists on non-windows`, () => {
          Object.defineProperty(process, 'platform', { value: 'linux' });
          (execSync as Mock).mockReturnValue(
            Buffer.from(`/usr/bin/${commands[0]}`),
          );
          expect(hasValidEditorCommand(editor)).toBe(true);
          expect(execSync).toHaveBeenCalledWith(`command -v ${commands[0]}`, {
            stdio: 'ignore',
          });
        });

        if (commands.length > 1) {
          it(`should return true if first command doesn't exist but second command "${commands[1]}" exists on non-windows`, () => {
            Object.defineProperty(process, 'platform', { value: 'linux' });
            (execSync as Mock)
              .mockImplementationOnce(() => {
                throw new Error(); // first command not found
              })
              .mockReturnValueOnce(Buffer.from(`/usr/bin/${commands[1]}`)); // second command found
            expect(hasValidEditorCommand(editor)).toBe(true);
            expect(execSync).toHaveBeenCalledTimes(2);
          });
        }

        it(`should return false if none of the commands exist on non-windows`, () => {
          Object.defineProperty(process, 'platform', { value: 'linux' });
          (execSync as Mock).mockImplementation(() => {
            throw new Error(); // all commands not found
          });
          expect(hasValidEditorCommand(editor)).toBe(false);
          expect(execSync).toHaveBeenCalledTimes(commands.length);
        });

        // Windows tests
        it(`should return true if first command "${win32Commands[0]}" exists on windows`, () => {
          Object.defineProperty(process, 'platform', { value: 'win32' });
          (execSync as Mock).mockReturnValue(
            Buffer.from(`C:\\Program Files\\...\\${win32Commands[0]}`),
          );
          expect(hasValidEditorCommand(editor)).toBe(true);
          expect(execSync).toHaveBeenCalledWith(
            `where.exe ${win32Commands[0]}`,
            {
              stdio: 'ignore',
            },
          );
        });

        if (win32Commands.length > 1) {
          it(`should return true if first command doesn't exist but second command "${win32Commands[1]}" exists on windows`, () => {
            Object.defineProperty(process, 'platform', { value: 'win32' });
            (execSync as Mock)
              .mockImplementationOnce(() => {
                throw new Error(); // first command not found
              })
              .mockReturnValueOnce(
                Buffer.from(`C:\\Program Files\\...\\${win32Commands[1]}`),
              ); // second command found
            expect(hasValidEditorCommand(editor)).toBe(true);
            expect(execSync).toHaveBeenCalledTimes(2);
          });
        }

        it(`should return false if none of the commands exist on windows`, () => {
          Object.defineProperty(process, 'platform', { value: 'win32' });
          (execSync as Mock).mockImplementation(() => {
            throw new Error(); // all commands not found
          });
          expect(hasValidEditorCommand(editor)).toBe(false);
          expect(execSync).toHaveBeenCalledTimes(win32Commands.length);
        });
      });
    }
  });

  describe('getDiffCommand', () => {
    const guiEditors: Array<{
      editor: EditorType;
      commands: string[];
      win32Commands: string[];
    }> = [
      { editor: 'vscode', commands: ['code'], win32Commands: ['code.cmd'] },
      {
        editor: 'vscodium',
        commands: ['codium'],
        win32Commands: ['codium.cmd'],
      },
      {
        editor: 'windsurf',
        commands: ['windsurf'],
        win32Commands: ['windsurf'],
      },
      { editor: 'cursor', commands: ['cursor'], win32Commands: ['cursor'] },
      { editor: 'zed', commands: ['zed', 'zeditor'], win32Commands: ['zed'] },
      {
        editor: 'antigravity',
        commands: ['agy', 'antigravity'],
        win32Commands: ['agy.cmd', 'antigravity.cmd', 'antigravity'],
      },
      { editor: 'bbedit', commands: ['bbedit'], win32Commands: ['bbedit'] },
    ];

    for (const { editor, commands, win32Commands } of guiEditors) {
      // Non-windows tests
      it(`should use first command "${commands[0]}" when it exists on non-windows`, () => {
        Object.defineProperty(process, 'platform', { value: 'linux' });
        (execSync as Mock).mockReturnValue(
          Buffer.from(`/usr/bin/${commands[0]}`),
        );
        const diffCommand = getDiffCommand('old.txt', 'new.txt', editor);
        expect(diffCommand).toEqual({
          command: commands[0],
          args: ['--wait', '--diff', 'old.txt', 'new.txt'],
        });
      });

      if (commands.length > 1) {
        it(`should use second command "${commands[1]}" when first doesn't exist on non-windows`, () => {
          Object.defineProperty(process, 'platform', { value: 'linux' });
          (execSync as Mock)
            .mockImplementationOnce(() => {
              throw new Error(); // first command not found
            })
            .mockReturnValueOnce(Buffer.from(`/usr/bin/${commands[1]}`)); // second command found

          const diffCommand = getDiffCommand('old.txt', 'new.txt', editor);
          expect(diffCommand).toEqual({
            command: commands[1],
            args: ['--wait', '--diff', 'old.txt', 'new.txt'],
          });
        });
      }

      it(`should fall back to last command "${commands[commands.length - 1]}" when none exist on non-windows`, () => {
        Object.defineProperty(process, 'platform', { value: 'linux' });
        (execSync as Mock).mockImplementation(() => {
          throw new Error(); // all commands not found
        });

        const diffCommand = getDiffCommand('old.txt', 'new.txt', editor);
        expect(diffCommand).toEqual({
          command: commands[commands.length - 1],
          args: ['--wait', '--diff', 'old.txt', 'new.txt'],
        });
      });

      // Windows tests
      it(`should use first command "${win32Commands[0]}" when it exists on windows`, () => {
        Object.defineProperty(process, 'platform', { value: 'win32' });
        (execSync as Mock).mockReturnValue(
          Buffer.from(`C:\\Program Files\\...\\${win32Commands[0]}`),
        );
        const diffCommand = getDiffCommand('old.txt', 'new.txt', editor);
        expect(diffCommand).toEqual({
          command: win32Commands[0],
          args: ['--wait', '--diff', 'old.txt', 'new.txt'],
        });
      });

      if (win32Commands.length > 1) {
        it(`should use second command "${win32Commands[1]}" when first doesn't exist on windows`, () => {
          Object.defineProperty(process, 'platform', { value: 'win32' });
          (execSync as Mock)
            .mockImplementationOnce(() => {
              throw new Error(); // first command not found
            })
            .mockReturnValueOnce(
              Buffer.from(`C:\\Program Files\\...\\${win32Commands[1]}`),
            ); // second command found

          const diffCommand = getDiffCommand('old.txt', 'new.txt', editor);
          expect(diffCommand).toEqual({
            command: win32Commands[1],
            args: ['--wait', '--diff', 'old.txt', 'new.txt'],
          });
        });
      }

      it(`should fall back to last command "${win32Commands[win32Commands.length - 1]}" when none exist on windows`, () => {
        Object.defineProperty(process, 'platform', { value: 'win32' });
        (execSync as Mock).mockImplementation(() => {
          throw new Error(); // all commands not found
        });

        const diffCommand = getDiffCommand('old.txt', 'new.txt', editor);
        expect(diffCommand).toEqual({
          command: win32Commands[win32Commands.length - 1],
          args: ['--wait', '--diff', 'old.txt', 'new.txt'],
        });
      });
    }

    const terminalEditors: Array<{
      editor: EditorType;
      command: string;
    }> = [
      { editor: 'vim', command: 'vim' },
      { editor: 'neovim', command: 'nvim' },
    ];

    for (const { editor, command } of terminalEditors) {
      it(`should return the correct command for ${editor}`, () => {
        const diffCommand = getDiffCommand('old.txt', 'new.txt', editor);
        expect(diffCommand).toEqual({
          command,
          args: [
            '-d',
            '-i',
            'NONE',
            '-c',
            'wincmd h | set readonly | wincmd l',
            '-c',
            'highlight DiffAdd cterm=bold ctermbg=22 guibg=#005f00 | highlight DiffChange cterm=bold ctermbg=24 guibg=#005f87 | highlight DiffText ctermbg=21 guibg=#0000af | highlight DiffDelete ctermbg=52 guibg=#5f0000',
            '-c',
            'set showtabline=2 | set tabline=[Instructions]\\ :wqa(save\\ &\\ quit)\\ \\|\\ i/esc(toggle\\ edit\\ mode)',
            '-c',
            'wincmd h | setlocal statusline=OLD\\ FILE',
            '-c',
            'wincmd l | setlocal statusline=%#StatusBold#NEW\\ FILE\\ :wqa(save\\ &\\ quit)\\ \\|\\ i/esc(toggle\\ edit\\ mode)',
            '-c',
            'autocmd BufWritePost * wqa',
            'old.txt',
            'new.txt',
          ],
        });
      });
    }

    it('should return the correct command for emacs with escaped paths', () => {
      Object.defineProperty(process, 'platform', { value: 'linux' });
      const command = getDiffCommand(
        'old file "quote".txt',
        'new file \\back\\slash.txt',
        'emacs',
      );
      expect(command).toEqual({
        command: 'emacs',
        args: [
          '--eval',
          '(ediff "old file \\"quote\\".txt" "new file \\\\back\\\\slash.txt")',
        ],
      });
    });

    it('should return the correct command for emacsclient', () => {
      const command = getDiffCommand('old.txt', 'new.txt', 'emacsclient');
      expect(command).toEqual({
        command: 'emacsclient',
        args: ['-nw', '--eval', '(ediff "old.txt" "new.txt")'],
      });
    });

    it('should return the correct command for emacsclient with escaped paths', () => {
      const command = getDiffCommand(
        'old file "quote".txt',
        'new file \\back\\slash.txt',
        'emacsclient',
      );
      expect(command).toEqual({
        command: 'emacsclient',
        args: [
          '-nw',
          '--eval',
          '(ediff "old file \\"quote\\".txt" "new file \\\\back\\\\slash.txt")',
        ],
      });
    });

    it('should return the correct command for helix', () => {
      const command = getDiffCommand('old.txt', 'new.txt', 'hx');
      expect(command).toEqual({
        command: 'hx',
        args: ['--vsplit', '--', 'old.txt', 'new.txt'],
      });
    });

    it('should return null for sublimetext (no CLI diff support)', () => {
      expect(getDiffCommand('old.txt', 'new.txt', 'sublimetext')).toBeNull();
    });

    it('should return null for lapce (no CLI diff support)', () => {
      expect(getDiffCommand('old.txt', 'new.txt', 'lapce')).toBeNull();
    });

    it('should return null for nova (no CLI diff support)', () => {
      expect(getDiffCommand('old.txt', 'new.txt', 'nova')).toBeNull();
    });

    it('should return null for micro (no CLI diff support)', () => {
      expect(getDiffCommand('old.txt', 'new.txt', 'micro')).toBeNull();
    });

    it('should return null for an unsupported editor', () => {
      // @ts-expect-error Testing unsupported editor
      const command = getDiffCommand('old.txt', 'new.txt', 'foobar');
      expect(command).toBeNull();
    });
  });

  describe('openDiff', () => {
    const guiEditors: EditorType[] = [
      'vscode',
      'vscodium',
      'windsurf',
      'cursor',
      'zed',
      'bbedit',
    ];

    for (const editor of guiEditors) {
      it(`should call spawn for ${editor}`, async () => {
        const mockSpawnOn = vi.fn((event, cb) => {
          if (event === 'close') {
            cb(0);
          }
        });
        (spawn as Mock).mockReturnValue({ on: mockSpawnOn });

        await openDiff('old.txt', 'new.txt', editor);
        const diffCommand = getDiffCommand('old.txt', 'new.txt', editor)!;
        expect(spawn).toHaveBeenCalledWith(
          diffCommand.command,
          diffCommand.args,
          {
            stdio: 'inherit',
            shell: process.platform === 'win32',
          },
        );
        expect(mockSpawnOn).toHaveBeenCalledWith('close', expect.any(Function));
        expect(mockSpawnOn).toHaveBeenCalledWith('error', expect.any(Function));
      });

      it(`should reject if spawn for ${editor} fails`, async () => {
        const mockError = new Error('spawn error');
        const mockSpawnOn = vi.fn((event, cb) => {
          if (event === 'error') {
            cb(mockError);
          }
        });
        (spawn as Mock).mockReturnValue({ on: mockSpawnOn });

        await expect(openDiff('old.txt', 'new.txt', editor)).rejects.toThrow(
          'spawn error',
        );
      });

      it(`should resolve and log warning if ${editor} exits with non-zero code`, async () => {
        const warnSpy = vi
          .spyOn(debugLogger, 'warn')
          .mockImplementation(() => {});
        const mockSpawnOn = vi.fn((event, cb) => {
          if (event === 'close') {
            cb(1);
          }
        });
        (spawn as Mock).mockReturnValue({ on: mockSpawnOn });

        await openDiff('old.txt', 'new.txt', editor);
        expect(warnSpy).toHaveBeenCalledWith(`${editor} exited with code 1`);
      });

      it(`should emit ExternalEditorClosed when ${editor} exits successfully`, async () => {
        const emitSpy = vi.spyOn(coreEvents, 'emit');
        const mockSpawnOn = vi.fn((event, cb) => {
          if (event === 'close') {
            cb(0);
          }
        });
        (spawn as Mock).mockReturnValue({ on: mockSpawnOn });

        await openDiff('old.txt', 'new.txt', editor);
        expect(emitSpy).toHaveBeenCalledWith(CoreEvent.ExternalEditorClosed);
      });

      it(`should emit ExternalEditorClosed when ${editor} exits with non-zero code`, async () => {
        vi.spyOn(debugLogger, 'warn').mockImplementation(() => {});
        const emitSpy = vi.spyOn(coreEvents, 'emit');
        const mockSpawnOn = vi.fn((event, cb) => {
          if (event === 'close') {
            cb(1);
          }
        });
        (spawn as Mock).mockReturnValue({ on: mockSpawnOn });

        await openDiff('old.txt', 'new.txt', editor);
        expect(emitSpy).toHaveBeenCalledWith(CoreEvent.ExternalEditorClosed);
      });

      it(`should emit ExternalEditorClosed when ${editor} spawn errors`, async () => {
        const emitSpy = vi.spyOn(coreEvents, 'emit');
        const mockError = new Error('spawn error');
        const mockSpawnOn = vi.fn((event, cb) => {
          if (event === 'error') {
            cb(mockError);
          }
        });
        (spawn as Mock).mockReturnValue({ on: mockSpawnOn });

        await expect(openDiff('old.txt', 'new.txt', editor)).rejects.toThrow(
          'spawn error',
        );
        expect(emitSpy).toHaveBeenCalledWith(CoreEvent.ExternalEditorClosed);
      });

      it(`should only emit ExternalEditorClosed once when ${editor} fires both error and close`, async () => {
        const emitSpy = vi.spyOn(coreEvents, 'emit');
        const callbacks: Record<string, (arg: unknown) => void> = {};
        const mockSpawnOn = vi.fn(
          (event: string, cb: (arg: unknown) => void) => {
            callbacks[event] = cb;
          },
        );
        (spawn as Mock).mockReturnValue({ on: mockSpawnOn });

        const promise = openDiff('old.txt', 'new.txt', editor);
        // Simulate Node.js behavior: error fires first, then close.
        callbacks['error'](new Error('spawn error'));
        callbacks['close'](1);

        await expect(promise).rejects.toThrow('spawn error');
        const editorClosedEmissions = emitSpy.mock.calls.filter(
          (call) => call[0] === CoreEvent.ExternalEditorClosed,
        );
        expect(editorClosedEmissions).toHaveLength(1);
      });
    }

    // micro has no CLI diff support (getDiffCommand returns null) so is excluded here
    const terminalEditors: EditorType[] = [
      'vim',
      'neovim',
      'emacs',
      'hx',
      'emacsclient',
    ];

    for (const editor of terminalEditors) {
      it(`should call spawnSync for ${editor}`, async () => {
        await openDiff('old.txt', 'new.txt', editor);
        const diffCommand = getDiffCommand('old.txt', 'new.txt', editor)!;
        expect(spawnSync).toHaveBeenCalledWith(
          diffCommand.command,
          diffCommand.args,
          {
            stdio: 'inherit',
          },
        );
      });
    }

    it('should log an error if diff command is not available', async () => {
      const consoleErrorSpy = vi
        .spyOn(debugLogger, 'error')
        .mockImplementation(() => {});
      // @ts-expect-error Testing unsupported editor
      await openDiff('old.txt', 'new.txt', 'foobar');
      expect(consoleErrorSpy).toHaveBeenCalledWith(
        'No diff tool available. Install a supported editor.',
      );
    });
  });

  describe('allowEditorTypeInSandbox', () => {
    it('should allow vim in sandbox mode', () => {
      vi.stubEnv('SANDBOX', 'sandbox');
      expect(allowEditorTypeInSandbox('vim')).toBe(true);
    });

    it('should allow vim when not in sandbox mode', () => {
      expect(allowEditorTypeInSandbox('vim')).toBe(true);
    });

    it('should allow emacs in sandbox mode', () => {
      vi.stubEnv('SANDBOX', 'sandbox');
      expect(allowEditorTypeInSandbox('emacs')).toBe(true);
    });

    it('should allow emacs when not in sandbox mode', () => {
      expect(allowEditorTypeInSandbox('emacs')).toBe(true);
    });

    it('should allow emacsclient in sandbox mode', () => {
      vi.stubEnv('SANDBOX', 'sandbox');
      expect(allowEditorTypeInSandbox('emacsclient')).toBe(true);
    });

    it('should allow emacsclient when not in sandbox mode', () => {
      expect(allowEditorTypeInSandbox('emacsclient')).toBe(true);
    });

    it('should allow neovim in sandbox mode', () => {
      vi.stubEnv('SANDBOX', 'sandbox');
      expect(allowEditorTypeInSandbox('neovim')).toBe(true);
    });

    it('should allow neovim when not in sandbox mode', () => {
      expect(allowEditorTypeInSandbox('neovim')).toBe(true);
    });

    it('should allow hx in sandbox mode', () => {
      vi.stubEnv('SANDBOX', 'sandbox');
      expect(allowEditorTypeInSandbox('hx')).toBe(true);
    });

    it('should allow hx when not in sandbox mode', () => {
      expect(allowEditorTypeInSandbox('hx')).toBe(true);
    });

    const guiEditors: EditorType[] = [
      'vscode',
      'vscodium',
      'windsurf',
      'cursor',
      'zed',
      'sublimetext',
      'lapce',
      'nova',
      'bbedit',
    ];
    for (const editor of guiEditors) {
      it(`should not allow ${editor} in sandbox mode`, () => {
        vi.stubEnv('SANDBOX', 'sandbox');
        expect(allowEditorTypeInSandbox(editor)).toBe(false);
      });

      it(`should allow ${editor} when not in sandbox mode`, () => {
        vi.stubEnv('SANDBOX', '');
        expect(allowEditorTypeInSandbox(editor)).toBe(true);
      });
    }
  });

  describe('isEditorAvailable', () => {
    it('should return false for undefined editor', () => {
      expect(isEditorAvailable(undefined)).toBe(false);
    });

    it('should return false for empty string editor', () => {
      expect(isEditorAvailable('')).toBe(false);
    });

    it('should return false for invalid editor type', () => {
      expect(isEditorAvailable('invalid-editor')).toBe(false);
    });

    it('should return true for vscode when installed and not in sandbox mode', () => {
      (execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/code'));
      vi.stubEnv('SANDBOX', '');
      expect(isEditorAvailable('vscode')).toBe(true);
    });

    it('should return false for vscode when not installed and not in sandbox mode', () => {
      (execSync as Mock).mockImplementation(() => {
        throw new Error();
      });
      expect(isEditorAvailable('vscode')).toBe(false);
    });

    it('should return false for vscode when installed and in sandbox mode', () => {
      (execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/code'));
      vi.stubEnv('SANDBOX', 'sandbox');
      expect(isEditorAvailable('vscode')).toBe(false);
    });

    it('should return true for vim when installed and in sandbox mode', () => {
      (execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/vim'));
      vi.stubEnv('SANDBOX', 'sandbox');
      expect(isEditorAvailable('vim')).toBe(true);
    });

    it('should return true for emacs when installed and in sandbox mode', () => {
      (execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/emacs'));
      vi.stubEnv('SANDBOX', 'sandbox');
      expect(isEditorAvailable('emacs')).toBe(true);
    });

    it('should return true for hx when installed and in sandbox mode', () => {
      (execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/hx'));
      vi.stubEnv('SANDBOX', 'sandbox');
      expect(isEditorAvailable('hx')).toBe(true);
    });

    it('should return true for neovim when installed and in sandbox mode', () => {
      (execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/nvim'));
      vi.stubEnv('SANDBOX', 'sandbox');
      expect(isEditorAvailable('neovim')).toBe(true);
    });
  });

  // Helper to create a mock exec that simulates async behavior
  const mockExecAsync = (implementation: (cmd: string) => boolean): void => {
    (exec as unknown as Mock).mockImplementation(
      (
        cmd: string,
        callback: (error: Error | null, stdout: string, stderr: string) => void,
      ) => {
        if (implementation(cmd)) {
          callback(null, '/usr/bin/cmd', '');
        } else {
          callback(new Error('Command not found'), '', '');
        }
      },
    );
  };

  describe('hasValidEditorCommandAsync', () => {
    it('should return true if vim command exists', async () => {
      Object.defineProperty(process, 'platform', { value: 'linux' });
      mockExecAsync((cmd) => cmd.includes('vim'));
      expect(await hasValidEditorCommandAsync('vim')).toBe(true);
    });

    it('should return false if vim command does not exist', async () => {
      Object.defineProperty(process, 'platform', { value: 'linux' });
      mockExecAsync(() => false);
      expect(await hasValidEditorCommandAsync('vim')).toBe(false);
    });

    it('should check zed and zeditor commands in order', async () => {
      Object.defineProperty(process, 'platform', { value: 'linux' });
      mockExecAsync((cmd) => cmd.includes('zeditor'));
      expect(await hasValidEditorCommandAsync('zed')).toBe(true);
    });
  });

  describe('isEditorAvailableAsync', () => {
    it('should return false for undefined editor', async () => {
      expect(await isEditorAvailableAsync(undefined)).toBe(false);
    });

    it('should return false for empty string editor', async () => {
      expect(await isEditorAvailableAsync('')).toBe(false);
    });

    it('should return false for invalid editor type', async () => {
      expect(await isEditorAvailableAsync('invalid-editor')).toBe(false);
    });

    it('should return true for vscode when installed and not in sandbox mode', async () => {
      mockExecAsync((cmd) => cmd.includes('code'));
      vi.stubEnv('SANDBOX', '');
      expect(await isEditorAvailableAsync('vscode')).toBe(true);
    });

    it('should return false for vscode when not installed', async () => {
      mockExecAsync(() => false);
      expect(await isEditorAvailableAsync('vscode')).toBe(false);
    });

    it('should return false for vscode in sandbox mode', async () => {
      mockExecAsync((cmd) => cmd.includes('code'));
      vi.stubEnv('SANDBOX', 'sandbox');
      expect(await isEditorAvailableAsync('vscode')).toBe(false);
    });

    it('should return true for vim in sandbox mode', async () => {
      mockExecAsync((cmd) => cmd.includes('vim'));
      vi.stubEnv('SANDBOX', 'sandbox');
      expect(await isEditorAvailableAsync('vim')).toBe(true);
    });
  });

  describe('resolveEditorAsync', () => {
    it('should return the preferred editor when available', async () => {
      mockExecAsync((cmd) => cmd.includes('vim'));
      vi.stubEnv('SANDBOX', '');
      const result = await resolveEditorAsync('vim');
      expect(result).toBe('vim');
    });

    it('should request editor selection when preferred editor is not installed', async () => {
      mockExecAsync(() => false);
      vi.stubEnv('SANDBOX', '');
      const resolvePromise = resolveEditorAsync('vim');
      setTimeout(
        () => coreEvents.emit(CoreEvent.EditorSelected, { editor: 'neovim' }),
        0,
      );
      const result = await resolvePromise;
      expect(result).toBe('neovim');
    });

    it('should request editor selection when preferred GUI editor cannot be used in sandbox mode', async () => {
      mockExecAsync((cmd) => cmd.includes('code'));
      vi.stubEnv('SANDBOX', 'sandbox');
      const resolvePromise = resolveEditorAsync('vscode');
      setTimeout(
        () => coreEvents.emit(CoreEvent.EditorSelected, { editor: 'vim' }),
        0,
      );
      const result = await resolvePromise;
      expect(result).toBe('vim');
    });

    it('should request editor selection when no preference is set', async () => {
      const emitSpy = vi.spyOn(coreEvents, 'emit');
      vi.stubEnv('SANDBOX', '');

      const resolvePromise = resolveEditorAsync(undefined);

      // Simulate UI selection
      setTimeout(
        () => coreEvents.emit(CoreEvent.EditorSelected, { editor: 'vim' }),
        0,
      );

      const result = await resolvePromise;
      expect(result).toBe('vim');
      expect(emitSpy).toHaveBeenCalledWith(CoreEvent.RequestEditorSelection);
    });

    it('should return undefined when editor selection is cancelled', async () => {
      const resolvePromise = resolveEditorAsync(undefined);

      // Simulate UI cancellation (exit dialog)
      setTimeout(
        () => coreEvents.emit(CoreEvent.EditorSelected, { editor: undefined }),
        0,
      );

      const result = await resolvePromise;
      expect(result).toBeUndefined();
    });

    it('should return undefined when abort signal is triggered', async () => {
      const controller = new AbortController();
      const resolvePromise = resolveEditorAsync(undefined, controller.signal);

      setTimeout(() => controller.abort(), 0);

      const result = await resolvePromise;
      expect(result).toBeUndefined();
    });

    it('should request editor selection in sandbox mode when no preference is set', async () => {
      const emitSpy = vi.spyOn(coreEvents, 'emit');
      vi.stubEnv('SANDBOX', 'sandbox');

      const resolvePromise = resolveEditorAsync(undefined);

      // Simulate UI selection
      setTimeout(
        () => coreEvents.emit(CoreEvent.EditorSelected, { editor: 'vim' }),
        0,
      );

      const result = await resolvePromise;
      expect(result).toBe('vim');
      expect(emitSpy).toHaveBeenCalledWith(CoreEvent.RequestEditorSelection);
    });
  });

  describe('isValidEditorType', () => {
    it('should return true for known editor identifiers', () => {
      expect(isValidEditorType('vscode')).toBe(true);
      expect(isValidEditorType('vim')).toBe(true);
      expect(isValidEditorType('sublimetext')).toBe(true);
      expect(isValidEditorType('emacsclient')).toBe(true);
      expect(isValidEditorType('micro')).toBe(true);
      expect(isValidEditorType('lapce')).toBe(true);
      expect(isValidEditorType('nova')).toBe(true);
      expect(isValidEditorType('bbedit')).toBe(true);
    });

    it('should return false for unrecognized strings', () => {
      expect(isValidEditorType('emacsclient -nw')).toBe(false);
      expect(isValidEditorType('subl')).toBe(false);
      expect(isValidEditorType('code')).toBe(false);
      expect(isValidEditorType('')).toBe(false);
      expect(isValidEditorType('notepad')).toBe(false);
    });
  });

  describe('getEditorWaitFlag', () => {
    it('should return -w for sublimetext', () => {
      expect(getEditorWaitFlag('sublimetext')).toBe('-w');
    });

    it('should return --wait for all other GUI editors', () => {
      const standardGuiEditors: EditorType[] = [
        'vscode',
        'vscodium',
        'windsurf',
        'cursor',
        'zed',
        'antigravity',
        'lapce',
        'nova',
        'bbedit',
      ];
      for (const editor of standardGuiEditors) {
        expect(getEditorWaitFlag(editor)).toBe('--wait');
      }
    });
  });

  describe('resolveEditorTypeFromCommand', () => {
    it('should resolve known command names to their editor type', () => {
      expect(resolveEditorTypeFromCommand('cursor')).toBe('cursor');
      expect(resolveEditorTypeFromCommand('code')).toBe('vscode');
      expect(resolveEditorTypeFromCommand('codium')).toBe('vscodium');
      expect(resolveEditorTypeFromCommand('vim')).toBe('vim');
    });

    it('should be case-insensitive', () => {
      expect(resolveEditorTypeFromCommand('Cursor')).toBe('cursor');
      expect(resolveEditorTypeFromCommand('CODE')).toBe('vscode');
    });

    it('should return undefined for unknown commands', () => {
      expect(resolveEditorTypeFromCommand('unknowntool')).toBeUndefined();
      expect(resolveEditorTypeFromCommand('')).toBeUndefined();
    });
  });

  describe('getEditorExtraArgs', () => {
    it('should return [-nw] for emacsclient', () => {
      expect(getEditorExtraArgs('emacsclient')).toEqual(['-nw']);
    });

    it('should return [] for VS Code-family editors by default', () => {
      const vscodeEditors: EditorType[] = [
        'vscode',
        'vscodium',
        'cursor',
        'windsurf',
      ];
      for (const editor of vscodeEditors) {
        expect(getEditorExtraArgs(editor)).toEqual([]);
      }
    });

    it('should return [--new-window] for VS Code-family editors when newWindow is true', () => {
      const vscodeEditors: EditorType[] = [
        'vscode',
        'vscodium',
        'cursor',
        'windsurf',
      ];
      for (const editor of vscodeEditors) {
        expect(getEditorExtraArgs(editor, { newWindow: true })).toEqual([
          '--new-window',
        ]);
      }
    });

    it('should return [] for VS Code-family editors when newWindow is false', () => {
      const vscodeEditors: EditorType[] = [
        'vscode',
        'vscodium',
        'cursor',
        'windsurf',
      ];
      for (const editor of vscodeEditors) {
        expect(getEditorExtraArgs(editor, { newWindow: false })).toEqual([]);
      }
    });

    it('should return [] for all other editors', () => {
      const otherEditors: EditorType[] = [
        'vim',
        'neovim',
        'emacs',
        'hx',
        'sublimetext',
        'micro',
      ];
      for (const editor of otherEditors) {
        expect(getEditorExtraArgs(editor)).toEqual([]);
      }
    });
  });
});
