/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { vi, describe, it, expect, beforeEach, afterEach, } from 'vitest';
import { checkHasEditorType, getDiffCommand, openDiff, allowEditorTypeInSandbox, isEditorAvailable, } from './editor.js';
import { execSync, spawn } from 'node:child_process';
vi.mock('child_process', () => ({
    execSync: vi.fn(),
    spawn: vi.fn(),
}));
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('checkHasEditorType', () => {
        const testCases = [
            { 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'] },
        ];
        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.mockReturnValue(Buffer.from(`/usr/bin/${commands[0]}`));
                    expect(checkHasEditorType(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
                            .mockImplementationOnce(() => {
                            throw new Error(); // first command not found
                        })
                            .mockReturnValueOnce(Buffer.from(`/usr/bin/${commands[1]}`)); // second command found
                        expect(checkHasEditorType(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.mockImplementation(() => {
                        throw new Error(); // all commands not found
                    });
                    expect(checkHasEditorType(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.mockReturnValue(Buffer.from(`C:\\Program Files\\...\\${win32Commands[0]}`));
                    expect(checkHasEditorType(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
                            .mockImplementationOnce(() => {
                            throw new Error(); // first command not found
                        })
                            .mockReturnValueOnce(Buffer.from(`C:\\Program Files\\...\\${win32Commands[1]}`)); // second command found
                        expect(checkHasEditorType(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.mockImplementation(() => {
                        throw new Error(); // all commands not found
                    });
                    expect(checkHasEditorType(editor)).toBe(false);
                    expect(execSync).toHaveBeenCalledTimes(win32Commands.length);
                });
            });
        }
    });
    describe('getDiffCommand', () => {
        const guiEditors = [
            { 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'] },
        ];
        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.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
                        .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.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.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
                        .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.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 = [
            { 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', () => {
            const command = getDiffCommand('old.txt', 'new.txt', 'emacs');
            expect(command).toEqual({
                command: 'emacs',
                args: ['--eval', '(ediff "old.txt" "new.txt")'],
            });
        });
        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 spawnEditors = [
            'vscode',
            'vscodium',
            'windsurf',
            'cursor',
            'zed',
        ];
        for (const editor of spawnEditors) {
            it(`should call spawn for ${editor}`, async () => {
                const mockSpawn = {
                    on: vi.fn((event, cb) => {
                        if (event === 'close') {
                            cb(0);
                        }
                    }),
                };
                spawn.mockReturnValue(mockSpawn);
                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: true,
                });
                expect(mockSpawn.on).toHaveBeenCalledWith('close', expect.any(Function));
                expect(mockSpawn.on).toHaveBeenCalledWith('error', expect.any(Function));
            });
            it(`should reject if spawn for ${editor} fails`, async () => {
                const mockError = new Error('spawn error');
                const mockSpawn = {
                    on: vi.fn((event, cb) => {
                        if (event === 'error') {
                            cb(mockError);
                        }
                    }),
                };
                spawn.mockReturnValue(mockSpawn);
                await expect(openDiff('old.txt', 'new.txt', editor, () => { })).rejects.toThrow('spawn error');
            });
            it(`should reject if ${editor} exits with non-zero code`, async () => {
                const mockSpawn = {
                    on: vi.fn((event, cb) => {
                        if (event === 'close') {
                            cb(1);
                        }
                    }),
                };
                spawn.mockReturnValue(mockSpawn);
                await expect(openDiff('old.txt', 'new.txt', editor, () => { })).rejects.toThrow(`${editor} exited with code 1`);
            });
        }
        const execSyncEditors = ['vim', 'neovim', 'emacs'];
        for (const editor of execSyncEditors) {
            it(`should call execSync for ${editor} on non-windows`, async () => {
                Object.defineProperty(process, 'platform', { value: 'linux' });
                await openDiff('old.txt', 'new.txt', editor, () => { });
                expect(execSync).toHaveBeenCalledTimes(1);
                const diffCommand = getDiffCommand('old.txt', 'new.txt', editor);
                const expectedCommand = `${diffCommand.command} ${diffCommand.args.map((arg) => `"${arg}"`).join(' ')}`;
                expect(execSync).toHaveBeenCalledWith(expectedCommand, {
                    stdio: 'inherit',
                    encoding: 'utf8',
                });
            });
            it(`should call execSync for ${editor} on windows`, async () => {
                Object.defineProperty(process, 'platform', { value: 'win32' });
                await openDiff('old.txt', 'new.txt', editor, () => { });
                expect(execSync).toHaveBeenCalledTimes(1);
                const diffCommand = getDiffCommand('old.txt', 'new.txt', editor);
                const expectedCommand = `${diffCommand.command} ${diffCommand.args.join(' ')}`;
                expect(execSync).toHaveBeenCalledWith(expectedCommand, {
                    stdio: 'inherit',
                    encoding: 'utf8',
                });
            });
        }
        it('should log an error if diff command is not available', async () => {
            const consoleErrorSpy = vi
                .spyOn(console, '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('onEditorClose callback', () => {
            it('should call onEditorClose for execSync editors', async () => {
                execSync.mockReturnValue(Buffer.from(`/usr/bin/`));
                const onEditorClose = vi.fn();
                await openDiff('old.txt', 'new.txt', 'vim', onEditorClose);
                expect(execSync).toHaveBeenCalledTimes(1);
                expect(onEditorClose).toHaveBeenCalledTimes(1);
            });
            it('should call onEditorClose for execSync editors when an error is thrown', async () => {
                execSync.mockImplementation(() => {
                    throw new Error('test error');
                });
                const onEditorClose = vi.fn();
                openDiff('old.txt', 'new.txt', 'vim', onEditorClose);
                expect(execSync).toHaveBeenCalledTimes(1);
                expect(onEditorClose).toHaveBeenCalledTimes(1);
            });
            it('should not call onEditorClose for spawn editors', async () => {
                const onEditorClose = vi.fn();
                const mockSpawn = {
                    on: vi.fn((event, cb) => {
                        if (event === 'close') {
                            cb(0);
                        }
                    }),
                };
                spawn.mockReturnValue(mockSpawn);
                await openDiff('old.txt', 'new.txt', 'vscode', onEditorClose);
                expect(spawn).toHaveBeenCalledTimes(1);
                expect(onEditorClose).not.toHaveBeenCalled();
            });
        });
    });
    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 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);
        });
        const guiEditors = [
            'vscode',
            'vscodium',
            'windsurf',
            'cursor',
            'zed',
        ];
        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`, () => {
                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.mockReturnValue(Buffer.from('/usr/bin/code'));
            expect(isEditorAvailable('vscode')).toBe(true);
        });
        it('should return false for vscode when not installed and not in sandbox mode', () => {
            execSync.mockImplementation(() => {
                throw new Error();
            });
            expect(isEditorAvailable('vscode')).toBe(false);
        });
        it('should return false for vscode when installed and in sandbox mode', () => {
            execSync.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.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.mockReturnValue(Buffer.from('/usr/bin/emacs'));
            vi.stubEnv('SANDBOX', 'sandbox');
            expect(isEditorAvailable('emacs')).toBe(true);
        });
        it('should return true for neovim when installed and in sandbox mode', () => {
            execSync.mockReturnValue(Buffer.from('/usr/bin/nvim'));
            vi.stubEnv('SANDBOX', 'sandbox');
            expect(isEditorAvailable('neovim')).toBe(true);
        });
    });
});
//# sourceMappingURL=editor.test.js.map