/**
 * @license
 * Copyright 2026 HopCode Team Team
 * SPDX-License-Identifier: Apache-2.0
 */
import { describe, it, expect, vi, beforeEach, afterEach, afterAll, } from 'vitest';
// Mock cleanup module before importing anything else
const { mockRunExitCleanup } = vi.hoisted(() => ({
    mockRunExitCleanup: vi.fn().mockResolvedValue(undefined),
}));
vi.mock('../utils/cleanup.js', () => ({
    runExitCleanup: mockRunExitCleanup,
}));
// Mock the ACP SDK
const { mockConnectionState } = vi.hoisted(() => {
    const state = {
        resolve: () => { },
        promise: null,
        reset() {
            state.promise = new Promise((r) => {
                state.resolve = r;
            });
        },
    };
    state.reset();
    return { mockConnectionState: state };
});
vi.mock('@agentclientprotocol/sdk', () => ({
    AgentSideConnection: vi.fn().mockImplementation(() => ({
        get closed() {
            return mockConnectionState.promise;
        },
    })),
    ndJsonStream: vi.fn().mockReturnValue({}),
    RequestError: class RequestError extends Error {
    },
    PROTOCOL_VERSION: '1.0.0',
}));
// Mock stream conversion
vi.mock('node:stream', async (importOriginal) => {
    const actual = await importOriginal();
    return {
        ...actual,
        Writable: { ...actual.Writable, toWeb: vi.fn().mockReturnValue({}) },
        Readable: { ...actual.Readable, toWeb: vi.fn().mockReturnValue({}) },
    };
});
// Mock core dependencies
vi.mock('@hoptrendy/hopcode-core', () => ({
    createDebugLogger: () => ({
        debug: vi.fn(),
        error: vi.fn(),
        warn: vi.fn(),
        info: vi.fn(),
    }),
    APPROVAL_MODE_INFO: {},
    APPROVAL_MODES: [],
    AuthType: {},
    clearCachedCredentialFile: vi.fn(),
    HopCodeOAuth2Event: {},
    hopCodeOAuth2Events: { on: vi.fn(), off: vi.fn() },
    MCPServerConfig: {},
    SessionService: vi.fn(),
    tokenLimit: vi.fn(),
    SessionStartSource: {
        Startup: 'startup',
        Resume: 'resume',
    },
    SessionEndReason: {
        PromptInputExit: 'prompt_input_exit',
        Other: 'other',
    },
}));
vi.mock('./authMethods.js', () => ({ buildAuthMethods: vi.fn() }));
vi.mock('./service/filesystem.js', () => ({
    AcpFileSystemService: vi.fn(),
}));
vi.mock('../config/settings.js', () => ({ SettingScope: {} }));
vi.mock('../config/config.js', () => ({ loadCliConfig: vi.fn() }));
vi.mock('./session/Session.js', () => ({ Session: vi.fn() }));
vi.mock('../utils/acpModelUtils.js', () => ({
    formatAcpModelId: vi.fn(),
}));
import { runAcpAgent } from './acpAgent.js';
import { SessionEndReason } from '@hoptrendy/hopcode-core';
describe('runAcpAgent shutdown cleanup', () => {
    let processExitSpy;
    let processOnSpy;
    let processOffSpy;
    let stdinDestroySpy;
    let stdoutDestroySpy;
    let sigTermListeners;
    let sigIntListeners;
    let mockConfig;
    const mockSettings = { merged: {} };
    const mockArgv = {};
    beforeEach(() => {
        vi.clearAllMocks();
        // Reset mockConfig after clearAllMocks
        mockConfig = {
            initialize: vi.fn().mockResolvedValue(undefined),
            getHookSystem: vi.fn().mockReturnValue(undefined),
            getDisableAllHooks: vi.fn().mockReturnValue(false),
            hasHooksForEvent: vi.fn().mockReturnValue(false),
            getModel: vi.fn().mockReturnValue('test-model'),
        };
        mockRunExitCleanup.mockResolvedValue(undefined);
        mockConnectionState.reset();
        sigTermListeners = [];
        sigIntListeners = [];
        // Intercept signal handler registration
        processOnSpy = vi.spyOn(process, 'on').mockImplementation(((event, listener) => {
            if (event === 'SIGTERM')
                sigTermListeners.push(listener);
            if (event === 'SIGINT')
                sigIntListeners.push(listener);
            return process;
        }));
        processOffSpy = vi.spyOn(process, 'off').mockImplementation(((event, listener) => {
            if (event === 'SIGTERM') {
                sigTermListeners = sigTermListeners.filter((l) => l !== listener);
            }
            if (event === 'SIGINT') {
                sigIntListeners = sigIntListeners.filter((l) => l !== listener);
            }
            return process;
        }));
        // Mock process.exit to prevent actually exiting
        processExitSpy = vi
            .spyOn(process, 'exit')
            .mockImplementation((() => undefined));
        // Mock stdin/stdout destroy
        stdinDestroySpy = vi
            .spyOn(process.stdin, 'destroy')
            .mockImplementation(() => process.stdin);
        stdoutDestroySpy = vi
            .spyOn(process.stdout, 'destroy')
            .mockImplementation(() => process.stdout);
    });
    afterEach(() => {
        processExitSpy.mockRestore();
        stdinDestroySpy.mockRestore();
        stdoutDestroySpy.mockRestore();
        vi.clearAllMocks();
    });
    afterAll(() => {
        processOnSpy.mockRestore();
        processOffSpy.mockRestore();
    });
    it('calls runExitCleanup and process.exit on SIGTERM', async () => {
        // Start runAcpAgent (it will await connection.closed)
        const agentPromise = runAcpAgent(mockConfig, mockSettings, mockArgv);
        // Wait for signal handlers to be registered
        await vi.waitFor(() => {
            expect(sigTermListeners.length).toBeGreaterThan(0);
        });
        // Simulate SIGTERM from IDE
        sigTermListeners[0]('SIGTERM');
        // runExitCleanup is async, wait for it
        await vi.waitFor(() => {
            expect(mockRunExitCleanup).toHaveBeenCalledTimes(1);
        });
        await vi.waitFor(() => {
            expect(processExitSpy).toHaveBeenCalledWith(0);
        });
        // Resolve connection.closed so the promise settles
        mockConnectionState.resolve();
        await agentPromise;
    });
    it('calls runExitCleanup and process.exit on SIGINT', async () => {
        const agentPromise = runAcpAgent(mockConfig, mockSettings, mockArgv);
        // Wait for signal handlers to be registered
        await vi.waitFor(() => {
            expect(sigIntListeners.length).toBeGreaterThan(0);
        });
        sigIntListeners[0]('SIGINT');
        await vi.waitFor(() => {
            expect(mockRunExitCleanup).toHaveBeenCalledTimes(1);
        });
        await vi.waitFor(() => {
            expect(processExitSpy).toHaveBeenCalledWith(0);
        });
        mockConnectionState.resolve();
        await agentPromise;
    });
    it('only runs shutdown once even if multiple signals arrive', async () => {
        const agentPromise = runAcpAgent(mockConfig, mockSettings, mockArgv);
        // Wait for signal handlers to be registered
        await vi.waitFor(() => {
            expect(sigTermListeners.length).toBeGreaterThan(0);
        });
        // Send SIGTERM twice
        sigTermListeners[0]('SIGTERM');
        sigTermListeners[0]('SIGTERM');
        await vi.waitFor(() => {
            expect(mockRunExitCleanup).toHaveBeenCalledTimes(1);
        });
        mockConnectionState.resolve();
        await agentPromise;
    });
    it('still exits even if runExitCleanup throws', async () => {
        mockRunExitCleanup.mockRejectedValueOnce(new Error('cleanup failed'));
        const agentPromise = runAcpAgent(mockConfig, mockSettings, mockArgv);
        // Wait for signal handlers to be registered
        await vi.waitFor(() => {
            expect(sigTermListeners.length).toBeGreaterThan(0);
        });
        sigTermListeners[0]('SIGTERM');
        // process.exit should still be called via .finally()
        await vi.waitFor(() => {
            expect(processExitSpy).toHaveBeenCalledWith(0);
        });
        mockConnectionState.resolve();
        await agentPromise;
    });
});
describe('runAcpAgent SessionEnd hooks', () => {
    let processExitSpy;
    let processOnSpy;
    let processOffSpy;
    let stdinDestroySpy;
    let stdoutDestroySpy;
    let sigTermListeners;
    let sigIntListeners;
    let mockConfig;
    let mockHookSystem;
    const mockSettings = { merged: {} };
    const mockArgv = {};
    beforeEach(() => {
        vi.clearAllMocks();
        mockHookSystem = {
            fireSessionEndEvent: vi.fn().mockResolvedValue(undefined),
            fireSessionStartEvent: vi.fn().mockResolvedValue(undefined),
        };
        mockConfig = {
            initialize: vi.fn().mockResolvedValue(undefined),
            getHookSystem: vi.fn().mockReturnValue(mockHookSystem),
            getDisableAllHooks: vi.fn().mockReturnValue(false),
            hasHooksForEvent: vi.fn().mockReturnValue(true),
            getModel: vi.fn().mockReturnValue('test-model'),
        };
        mockRunExitCleanup.mockResolvedValue(undefined);
        mockConnectionState.reset();
        sigTermListeners = [];
        sigIntListeners = [];
        processOnSpy = vi.spyOn(process, 'on').mockImplementation(((event, listener) => {
            if (event === 'SIGTERM')
                sigTermListeners.push(listener);
            if (event === 'SIGINT')
                sigIntListeners.push(listener);
            return process;
        }));
        processOffSpy = vi.spyOn(process, 'off').mockImplementation(((event, listener) => {
            if (event === 'SIGTERM') {
                sigTermListeners = sigTermListeners.filter((l) => l !== listener);
            }
            if (event === 'SIGINT') {
                sigIntListeners = sigIntListeners.filter((l) => l !== listener);
            }
            return process;
        }));
        processExitSpy = vi
            .spyOn(process, 'exit')
            .mockImplementation((() => undefined));
        stdinDestroySpy = vi
            .spyOn(process.stdin, 'destroy')
            .mockImplementation(() => process.stdin);
        stdoutDestroySpy = vi
            .spyOn(process.stdout, 'destroy')
            .mockImplementation(() => process.stdout);
    });
    afterEach(() => {
        processExitSpy.mockRestore();
        stdinDestroySpy.mockRestore();
        stdoutDestroySpy.mockRestore();
        vi.clearAllMocks();
    });
    afterAll(() => {
        processOnSpy.mockRestore();
        processOffSpy.mockRestore();
    });
    it('fires SessionEnd hook with Other reason on SIGTERM', async () => {
        const agentPromise = runAcpAgent(mockConfig, mockSettings, mockArgv);
        await vi.waitFor(() => {
            expect(sigTermListeners.length).toBeGreaterThan(0);
        });
        sigTermListeners[0]('SIGTERM');
        await vi.waitFor(() => {
            expect(mockHookSystem.fireSessionEndEvent).toHaveBeenCalledWith(SessionEndReason.Other);
        });
        mockConnectionState.resolve();
        await agentPromise;
    });
    it('fires SessionEnd hook with Other reason on SIGINT', async () => {
        const agentPromise = runAcpAgent(mockConfig, mockSettings, mockArgv);
        await vi.waitFor(() => {
            expect(sigIntListeners.length).toBeGreaterThan(0);
        });
        sigIntListeners[0]('SIGINT');
        await vi.waitFor(() => {
            expect(mockHookSystem.fireSessionEndEvent).toHaveBeenCalledWith(SessionEndReason.Other);
        });
        mockConnectionState.resolve();
        await agentPromise;
    });
    it('fires SessionEnd hook with PromptInputExit on connection.closed', async () => {
        const agentPromise = runAcpAgent(mockConfig, mockSettings, mockArgv);
        // Resolve connection to simulate IDE disconnect
        mockConnectionState.resolve();
        await vi.waitFor(() => {
            expect(mockHookSystem.fireSessionEndEvent).toHaveBeenCalledWith(SessionEndReason.PromptInputExit);
        });
        await agentPromise;
    });
    it('does not fire SessionEnd hook when hooks are disabled', async () => {
        mockConfig.getDisableAllHooks = vi.fn().mockReturnValue(true);
        const agentPromise = runAcpAgent(mockConfig, mockSettings, mockArgv);
        await vi.waitFor(() => {
            expect(sigTermListeners.length).toBeGreaterThan(0);
        });
        sigTermListeners[0]('SIGTERM');
        await vi.waitFor(() => {
            expect(mockRunExitCleanup).toHaveBeenCalled();
        });
        // SessionEnd hook should NOT be called
        expect(mockHookSystem.fireSessionEndEvent).not.toHaveBeenCalled();
        mockConnectionState.resolve();
        await agentPromise;
    });
    it('does not fire SessionEnd hook when event not registered', async () => {
        mockConfig.hasHooksForEvent = vi.fn().mockReturnValue(false);
        const agentPromise = runAcpAgent(mockConfig, mockSettings, mockArgv);
        await vi.waitFor(() => {
            expect(sigTermListeners.length).toBeGreaterThan(0);
        });
        sigTermListeners[0]('SIGTERM');
        await vi.waitFor(() => {
            expect(mockRunExitCleanup).toHaveBeenCalled();
        });
        // SessionEnd hook should NOT be called
        expect(mockHookSystem.fireSessionEndEvent).not.toHaveBeenCalled();
        mockConnectionState.resolve();
        await agentPromise;
    });
    it('fires SessionEnd hook only once when SIGTERM triggers before connection.closed', async () => {
        const agentPromise = runAcpAgent(mockConfig, mockSettings, mockArgv);
        await vi.waitFor(() => {
            expect(sigTermListeners.length).toBeGreaterThan(0);
        });
        // Trigger SIGTERM first
        sigTermListeners[0]('SIGTERM');
        await vi.waitFor(() => {
            expect(mockHookSystem.fireSessionEndEvent).toHaveBeenCalledWith(SessionEndReason.Other);
        });
        // Now resolve connection.closed - this should NOT trigger another SessionEnd
        mockConnectionState.resolve();
        // Wait for the agent to complete
        await agentPromise;
        // SessionEnd should have been called exactly once
        expect(mockHookSystem.fireSessionEndEvent).toHaveBeenCalledTimes(1);
    });
});
//# sourceMappingURL=acpAgent.test.js.map