/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { vi, describe, it, expect, beforeEach, afterEach, } from 'vitest';
import { EventEmitter } from 'node:events';
import { RELAUNCH_EXIT_CODE } from './processUtils.js';
import { spawn } from 'node:child_process';
vi.mock('node:child_process', async (importOriginal) => {
    const actual = await importOriginal();
    return {
        ...actual,
        spawn: vi.fn(),
    };
});
const mockedSpawn = vi.mocked(spawn);
// Import the functions initially
import { relaunchAppInChildProcess, relaunchOnExitCode } from './relaunch.js';
describe('relaunchOnExitCode', () => {
    let processExitSpy;
    let stdinResumeSpy;
    beforeEach(() => {
        processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
            throw new Error('PROCESS_EXIT_CALLED');
        });
        stdinResumeSpy = vi
            .spyOn(process.stdin, 'resume')
            .mockImplementation(() => process.stdin);
        vi.clearAllMocks();
    });
    afterEach(() => {
        processExitSpy.mockRestore();
        stdinResumeSpy.mockRestore();
    });
    it('should exit with non-RELAUNCH_EXIT_CODE', async () => {
        const runner = vi.fn().mockResolvedValue(0);
        await expect(relaunchOnExitCode(runner)).rejects.toThrow('PROCESS_EXIT_CALLED');
        expect(runner).toHaveBeenCalledTimes(1);
        expect(processExitSpy).toHaveBeenCalledWith(0);
    });
    it('should continue running when RELAUNCH_EXIT_CODE is returned', async () => {
        let callCount = 0;
        const runner = vi.fn().mockImplementation(async () => {
            callCount++;
            if (callCount === 1)
                return RELAUNCH_EXIT_CODE;
            if (callCount === 2)
                return RELAUNCH_EXIT_CODE;
            return 0; // Exit on third call
        });
        await expect(relaunchOnExitCode(runner)).rejects.toThrow('PROCESS_EXIT_CALLED');
        expect(runner).toHaveBeenCalledTimes(3);
        expect(processExitSpy).toHaveBeenCalledWith(0);
    });
    it('should handle runner errors', async () => {
        const error = new Error('Runner failed');
        const runner = vi.fn().mockRejectedValue(error);
        await expect(relaunchOnExitCode(runner)).rejects.toThrow('PROCESS_EXIT_CALLED');
        expect(runner).toHaveBeenCalledTimes(1);
        expect(stdinResumeSpy).toHaveBeenCalled();
        expect(processExitSpy).toHaveBeenCalledWith(1);
    });
});
describe('relaunchAppInChildProcess', () => {
    let processExitSpy;
    let stdinPauseSpy;
    let stdinResumeSpy;
    // Store original values to restore later
    const originalEnv = { ...process.env };
    const originalExecArgv = [...process.execArgv];
    const originalArgv = [...process.argv];
    const originalExecPath = process.execPath;
    beforeEach(() => {
        vi.clearAllMocks();
        process.env = { ...originalEnv };
        delete process.env['HOPCODE_NO_RELAUNCH'];
        process.execArgv = [...originalExecArgv];
        process.argv = [...originalArgv];
        process.execPath = '/usr/bin/node';
        processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
            throw new Error('PROCESS_EXIT_CALLED');
        });
        stdinPauseSpy = vi
            .spyOn(process.stdin, 'pause')
            .mockImplementation(() => process.stdin);
        stdinResumeSpy = vi
            .spyOn(process.stdin, 'resume')
            .mockImplementation(() => process.stdin);
    });
    afterEach(() => {
        process.env = { ...originalEnv };
        process.execArgv = [...originalExecArgv];
        process.argv = [...originalArgv];
        process.execPath = originalExecPath;
        processExitSpy.mockRestore();
        stdinPauseSpy.mockRestore();
        stdinResumeSpy.mockRestore();
    });
    describe('when HOPCODE_NO_RELAUNCH is set', () => {
        it('should return early without spawning a child process', async () => {
            process.env['HOPCODE_NO_RELAUNCH'] = 'true';
            await relaunchAppInChildProcess(['--test'], ['--verbose']);
            expect(mockedSpawn).not.toHaveBeenCalled();
            expect(processExitSpy).not.toHaveBeenCalled();
        });
    });
    describe('when HOPCODE_NO_RELAUNCH is not set', () => {
        beforeEach(() => {
            delete process.env['HOPCODE_NO_RELAUNCH'];
        });
        it('should construct correct node arguments from execArgv, additionalNodeArgs, script, additionalScriptArgs, and argv', () => {
            // Test the argument construction logic directly by extracting it into a testable function
            // This tests the same logic that's used in relaunchAppInChildProcess
            // Setup test data to verify argument ordering
            const mockExecArgv = ['--inspect=9229', '--trace-warnings'];
            const mockArgv = [
                '/usr/bin/node',
                '/path/to/cli.js',
                'command',
                '--flag=value',
                '--verbose',
            ];
            const additionalNodeArgs = [
                '--max-old-space-size=4096',
                '--experimental-modules',
            ];
            const additionalScriptArgs = ['--model', 'gemini-1.5-pro', '--debug'];
            // Extract the argument construction logic from relaunchAppInChildProcess
            const script = mockArgv[1];
            const scriptArgs = mockArgv.slice(2);
            const nodeArgs = [
                ...mockExecArgv,
                ...additionalNodeArgs,
                script,
                ...additionalScriptArgs,
                ...scriptArgs,
            ];
            // Verify the argument construction follows the expected pattern:
            // [...process.execArgv, ...additionalNodeArgs, script, ...additionalScriptArgs, ...scriptArgs]
            const expectedArgs = [
                // Original node execution arguments
                '--inspect=9229',
                '--trace-warnings',
                // Additional node arguments passed to function
                '--max-old-space-size=4096',
                '--experimental-modules',
                // The script path
                '/path/to/cli.js',
                // Additional script arguments passed to function
                '--model',
                'gemini-1.5-pro',
                '--debug',
                // Original script arguments (everything after the script in process.argv)
                'command',
                '--flag=value',
                '--verbose',
            ];
            expect(nodeArgs).toEqual(expectedArgs);
        });
        it('should handle empty additional arguments correctly', () => {
            // Test edge cases with empty arrays
            const mockExecArgv = ['--trace-warnings'];
            const mockArgv = ['/usr/bin/node', '/app/cli.js', 'start'];
            const additionalNodeArgs = [];
            const additionalScriptArgs = [];
            // Extract the argument construction logic
            const script = mockArgv[1];
            const scriptArgs = mockArgv.slice(2);
            const nodeArgs = [
                ...mockExecArgv,
                ...additionalNodeArgs,
                script,
                ...additionalScriptArgs,
                ...scriptArgs,
            ];
            const expectedArgs = ['--trace-warnings', '/app/cli.js', 'start'];
            expect(nodeArgs).toEqual(expectedArgs);
        });
        it('should handle complex argument patterns', () => {
            // Test with various argument types including flags with values, boolean flags, etc.
            const mockExecArgv = ['--max-old-space-size=8192'];
            const mockArgv = [
                '/usr/bin/node',
                '/cli.js',
                '--config=/path/to/config.json',
                '--verbose',
                'subcommand',
                '--output',
                'file.txt',
            ];
            const additionalNodeArgs = ['--inspect-brk=9230'];
            const additionalScriptArgs = ['--model=gpt-4', '--temperature=0.7'];
            const script = mockArgv[1];
            const scriptArgs = mockArgv.slice(2);
            const nodeArgs = [
                ...mockExecArgv,
                ...additionalNodeArgs,
                script,
                ...additionalScriptArgs,
                ...scriptArgs,
            ];
            const expectedArgs = [
                '--max-old-space-size=8192',
                '--inspect-brk=9230',
                '/cli.js',
                '--model=gpt-4',
                '--temperature=0.7',
                '--config=/path/to/config.json',
                '--verbose',
                'subcommand',
                '--output',
                'file.txt',
            ];
            expect(nodeArgs).toEqual(expectedArgs);
        });
        // Note: Additional integration tests for spawn behavior are complex due to module mocking
        // limitations with ES modules. The core logic is tested in relaunchOnExitCode tests.
        it('should handle null exit code from child process', async () => {
            process.argv = ['/usr/bin/node', '/app/cli.js'];
            const mockChild = createMockChildProcess(0, false); // Don't auto-close
            mockedSpawn.mockImplementation(() => {
                // Emit close with null code immediately
                setImmediate(() => {
                    mockChild.emit('close', null);
                });
                return mockChild;
            });
            // Start the relaunch process
            const promise = relaunchAppInChildProcess([], []);
            await expect(promise).rejects.toThrow('PROCESS_EXIT_CALLED');
            // Should default to exit code 1
            expect(processExitSpy).toHaveBeenCalledWith(1);
        });
    });
});
/**
 * Creates a mock child process that emits events asynchronously
 */
function createMockChildProcess(exitCode = 0, autoClose = false) {
    const mockChild = new EventEmitter();
    Object.assign(mockChild, {
        stdin: null,
        stdout: null,
        stderr: null,
        stdio: [null, null, null],
        pid: 12345,
        killed: false,
        exitCode: null,
        signalCode: null,
        spawnargs: [],
        spawnfile: '',
        kill: vi.fn(),
        send: vi.fn(),
        disconnect: vi.fn(),
        unref: vi.fn(),
        ref: vi.fn(),
    });
    if (autoClose) {
        setImmediate(() => {
            mockChild.emit('close', exitCode);
        });
    }
    return mockChild;
}
//# sourceMappingURL=relaunch.test.js.map