/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { vi, describe, it, expect, beforeEach, afterEach, } from 'vitest';
import { OutputFormat, FatalInputError, debugLogger, coreEvents, } from '@google/gemini-cli-core';
import { handleError, handleToolError, handleCancellationError, handleMaxTurnsExceededError, } from './errors.js';
import { runSyncCleanup } from './cleanup.js';
// Mock the cleanup module
vi.mock('./cleanup.js', () => ({
    runSyncCleanup: vi.fn(),
}));
// Mock the core modules
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
    const original = await importOriginal();
    return {
        ...original,
        parseAndFormatApiError: vi.fn((error) => {
            if (error instanceof Error) {
                return `API Error: ${error.message}`;
            }
            return `API Error: ${String(error)}`;
        }),
        JsonFormatter: vi.fn().mockImplementation(() => ({
            formatError: vi.fn((error, code, sessionId) => JSON.stringify({
                ...(sessionId && { session_id: sessionId }),
                error: {
                    type: error.constructor.name,
                    message: error.message,
                    ...(code && { code }),
                },
            }, null, 2)),
        })),
        StreamJsonFormatter: vi.fn().mockImplementation(() => ({
            emitEvent: vi.fn(),
            convertToStreamStats: vi.fn().mockReturnValue({
                total_tokens: 0,
                input_tokens: 0,
                output_tokens: 0,
                cached: 0,
                input: 0,
                duration_ms: 0,
                tool_calls: 0,
                models: {},
            }),
        })),
        uiTelemetryService: {
            getMetrics: vi.fn().mockReturnValue({}),
        },
        JsonStreamEventType: {
            RESULT: 'result',
        },
        coreEvents: {
            emitFeedback: vi.fn(),
        },
        FatalToolExecutionError: class extends Error {
            constructor(message) {
                super(message);
                this.name = 'FatalToolExecutionError';
                this.exitCode = 54;
            }
            exitCode;
        },
        FatalCancellationError: class extends Error {
            constructor(message) {
                super(message);
                this.name = 'FatalCancellationError';
                this.exitCode = 130;
            }
            exitCode;
        },
    };
});
describe('errors', () => {
    let mockConfig;
    let processExitSpy;
    let debugLoggerErrorSpy;
    let debugLoggerWarnSpy;
    let coreEventsEmitFeedbackSpy;
    let runSyncCleanupSpy;
    const TEST_SESSION_ID = 'test-session-123';
    beforeEach(() => {
        // Reset mocks
        vi.clearAllMocks();
        // Mock debugLogger
        debugLoggerErrorSpy = vi
            .spyOn(debugLogger, 'error')
            .mockImplementation(() => { });
        debugLoggerWarnSpy = vi
            .spyOn(debugLogger, 'warn')
            .mockImplementation(() => { });
        // Mock coreEvents
        coreEventsEmitFeedbackSpy = vi.mocked(coreEvents.emitFeedback);
        // Mock runSyncCleanup
        runSyncCleanupSpy = vi.mocked(runSyncCleanup);
        // Mock process.exit to throw instead of actually exiting
        processExitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => {
            throw new Error(`process.exit called with code: ${code}`);
        });
        // Create mock config
        mockConfig = {
            getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
            getContentGeneratorConfig: vi.fn().mockReturnValue({ authType: 'test' }),
            getSessionId: vi.fn().mockReturnValue(TEST_SESSION_ID),
        };
    });
    afterEach(() => {
        debugLoggerErrorSpy.mockRestore();
        debugLoggerWarnSpy.mockRestore();
        processExitSpy.mockRestore();
    });
    describe('handleError', () => {
        describe('in text mode', () => {
            beforeEach(() => {
                mockConfig.getOutputFormat.mockReturnValue(OutputFormat.TEXT);
            });
            it('should re-throw without logging to debugLogger', () => {
                const testError = new Error('Test error');
                expect(() => {
                    handleError(testError, mockConfig);
                }).toThrow(testError);
                expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
            });
            it('should handle non-Error objects', () => {
                const testError = 'String error';
                expect(() => {
                    handleError(testError, mockConfig);
                }).toThrow(testError);
            });
        });
        describe('in JSON mode', () => {
            beforeEach(() => {
                mockConfig.getOutputFormat.mockReturnValue(OutputFormat.JSON);
            });
            it('should format error as JSON, emit feedback exactly once, and exit with default code', () => {
                const testError = new Error('Test error');
                expect(() => {
                    handleError(testError, mockConfig);
                }).toThrow('process.exit called with code: 1');
                expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
                expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith('error', JSON.stringify({
                    session_id: TEST_SESSION_ID,
                    error: {
                        type: 'Error',
                        message: 'Test error',
                        code: 1,
                    },
                }, null, 2));
                expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
                expect(runSyncCleanupSpy).toHaveBeenCalled();
            });
            it('should use custom error code when provided and only surface once', () => {
                const testError = new Error('Test error');
                expect(() => {
                    handleError(testError, mockConfig, 42);
                }).toThrow('process.exit called with code: 42');
                expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
                expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith('error', JSON.stringify({
                    session_id: TEST_SESSION_ID,
                    error: {
                        type: 'Error',
                        message: 'Test error',
                        code: 42,
                    },
                }, null, 2));
                expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
            });
            it('should extract exitCode from FatalError instances and only surface once', () => {
                const fatalError = new FatalInputError('Fatal error');
                expect(() => {
                    handleError(fatalError, mockConfig);
                }).toThrow('process.exit called with code: 42');
                expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
                expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith('error', JSON.stringify({
                    session_id: TEST_SESSION_ID,
                    error: {
                        type: 'FatalInputError',
                        message: 'Fatal error',
                        code: 42,
                    },
                }, null, 2));
                expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
            });
            it('should handle error with code property', () => {
                const errorWithCode = new Error('Error with code');
                errorWithCode.code = 404;
                expect(() => {
                    handleError(errorWithCode, mockConfig);
                }).toThrow('process.exit called with code: 404');
            });
            it('should handle error with status property', () => {
                const errorWithStatus = new Error('Error with status');
                errorWithStatus.status = 'TIMEOUT';
                expect(() => {
                    handleError(errorWithStatus, mockConfig);
                }).toThrow('process.exit called with code: 1'); // string codes become 1
                expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith('error', JSON.stringify({
                    session_id: TEST_SESSION_ID,
                    error: {
                        type: 'Error',
                        message: 'Error with status',
                        code: 'TIMEOUT',
                    },
                }, null, 2));
            });
        });
        describe('in STREAM_JSON mode', () => {
            beforeEach(() => {
                mockConfig.getOutputFormat.mockReturnValue(OutputFormat.STREAM_JSON);
            });
            it('should emit result event, run cleanup, and exit', () => {
                const testError = new Error('Test error');
                expect(() => {
                    handleError(testError, mockConfig);
                }).toThrow('process.exit called with code: 1');
                expect(runSyncCleanupSpy).toHaveBeenCalled();
            });
            it('should extract exitCode from FatalError instances', () => {
                const fatalError = new FatalInputError('Fatal error');
                expect(() => {
                    handleError(fatalError, mockConfig);
                }).toThrow('process.exit called with code: 42');
            });
        });
    });
    describe('handleToolError', () => {
        const toolName = 'test-tool';
        const toolError = new Error('Tool failed');
        describe('in text mode', () => {
            beforeEach(() => {
                mockConfig.getOutputFormat.mockReturnValue(OutputFormat.TEXT);
            });
            it('should log error message to stderr (via debugLogger) for non-fatal', () => {
                handleToolError(toolName, toolError, mockConfig);
                expect(debugLoggerWarnSpy).toHaveBeenCalledWith('Error executing tool test-tool: Tool failed');
            });
            it('should use resultDisplay when provided', () => {
                handleToolError(toolName, toolError, mockConfig, 'CUSTOM_ERROR', 'Custom display message');
                expect(debugLoggerWarnSpy).toHaveBeenCalledWith('Error executing tool test-tool: Custom display message');
            });
            it('should emit feedback exactly once for fatal errors and not use debugLogger', () => {
                expect(() => {
                    handleToolError(toolName, toolError, mockConfig, 'no_space_left');
                }).toThrow('process.exit called with code: 54');
                expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
                expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith('error', 'Error executing tool test-tool: Tool failed');
                expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
                expect(runSyncCleanupSpy).toHaveBeenCalled();
            });
        });
        describe('in JSON mode', () => {
            beforeEach(() => {
                mockConfig.getOutputFormat.mockReturnValue(OutputFormat.JSON);
            });
            describe('non-fatal errors', () => {
                it('should log error message to stderr without exiting for recoverable errors', () => {
                    handleToolError(toolName, toolError, mockConfig, 'invalid_tool_params');
                    expect(debugLoggerWarnSpy).toHaveBeenCalledWith('Error executing tool test-tool: Tool failed');
                    // Should not exit for non-fatal errors
                    expect(processExitSpy).not.toHaveBeenCalled();
                    expect(coreEventsEmitFeedbackSpy).not.toHaveBeenCalled();
                });
                it('should not exit for file not found errors', () => {
                    handleToolError(toolName, toolError, mockConfig, 'file_not_found');
                    expect(debugLoggerWarnSpy).toHaveBeenCalledWith('Error executing tool test-tool: Tool failed');
                    expect(processExitSpy).not.toHaveBeenCalled();
                    expect(coreEventsEmitFeedbackSpy).not.toHaveBeenCalled();
                });
                it('should not exit for permission denied errors', () => {
                    handleToolError(toolName, toolError, mockConfig, 'permission_denied');
                    expect(debugLoggerWarnSpy).toHaveBeenCalledWith('Error executing tool test-tool: Tool failed');
                    expect(processExitSpy).not.toHaveBeenCalled();
                    expect(coreEventsEmitFeedbackSpy).not.toHaveBeenCalled();
                });
                it('should not exit for path not in workspace errors', () => {
                    handleToolError(toolName, toolError, mockConfig, 'path_not_in_workspace');
                    expect(debugLoggerWarnSpy).toHaveBeenCalledWith('Error executing tool test-tool: Tool failed');
                    expect(processExitSpy).not.toHaveBeenCalled();
                    expect(coreEventsEmitFeedbackSpy).not.toHaveBeenCalled();
                });
                it('should prefer resultDisplay over error message', () => {
                    handleToolError(toolName, toolError, mockConfig, 'invalid_tool_params', 'Display message');
                    expect(debugLoggerWarnSpy).toHaveBeenCalledWith('Error executing tool test-tool: Display message');
                    expect(processExitSpy).not.toHaveBeenCalled();
                });
            });
            describe('fatal errors', () => {
                it('should exit immediately for NO_SPACE_LEFT errors and only surface once', () => {
                    expect(() => {
                        handleToolError(toolName, toolError, mockConfig, 'no_space_left');
                    }).toThrow('process.exit called with code: 54');
                    expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
                    expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith('error', JSON.stringify({
                        session_id: TEST_SESSION_ID,
                        error: {
                            type: 'FatalToolExecutionError',
                            message: 'Error executing tool test-tool: Tool failed',
                            code: 'no_space_left',
                        },
                    }, null, 2));
                    expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
                    expect(runSyncCleanupSpy).toHaveBeenCalled();
                });
            });
        });
        describe('in STREAM_JSON mode', () => {
            beforeEach(() => {
                mockConfig.getOutputFormat.mockReturnValue(OutputFormat.STREAM_JSON);
            });
            it('should emit result event, run cleanup, and exit for fatal errors', () => {
                expect(() => {
                    handleToolError(toolName, toolError, mockConfig, 'no_space_left');
                }).toThrow('process.exit called with code: 54');
                expect(runSyncCleanupSpy).toHaveBeenCalled();
                expect(coreEventsEmitFeedbackSpy).not.toHaveBeenCalled(); // Stream mode uses emitEvent
            });
            it('should log to stderr and not exit for non-fatal errors', () => {
                handleToolError(toolName, toolError, mockConfig, 'invalid_tool_params');
                expect(debugLoggerWarnSpy).toHaveBeenCalledWith('Error executing tool test-tool: Tool failed');
                expect(processExitSpy).not.toHaveBeenCalled();
            });
        });
    });
    describe('handleCancellationError', () => {
        describe('in text mode', () => {
            beforeEach(() => {
                mockConfig.getOutputFormat.mockReturnValue(OutputFormat.TEXT);
            });
            it('should emit feedback exactly once, run cleanup, and exit with 130', () => {
                expect(() => {
                    handleCancellationError(mockConfig);
                }).toThrow('process.exit called with code: 130');
                expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
                expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith('error', 'Operation cancelled.');
                expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
                expect(runSyncCleanupSpy).toHaveBeenCalled();
            });
        });
        describe('in JSON mode', () => {
            beforeEach(() => {
                mockConfig.getOutputFormat.mockReturnValue(OutputFormat.JSON);
            });
            it('should format cancellation as JSON, emit feedback once, and exit with 130', () => {
                expect(() => {
                    handleCancellationError(mockConfig);
                }).toThrow('process.exit called with code: 130');
                expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
                expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith('error', JSON.stringify({
                    session_id: TEST_SESSION_ID,
                    error: {
                        type: 'FatalCancellationError',
                        message: 'Operation cancelled.',
                        code: 130,
                    },
                }, null, 2));
                expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
            });
        });
        describe('in STREAM_JSON mode', () => {
            beforeEach(() => {
                mockConfig.getOutputFormat.mockReturnValue(OutputFormat.STREAM_JSON);
            });
            it('should emit result event and exit with 130', () => {
                expect(() => {
                    handleCancellationError(mockConfig);
                }).toThrow('process.exit called with code: 130');
                expect(coreEventsEmitFeedbackSpy).not.toHaveBeenCalled();
            });
        });
    });
    describe('handleMaxTurnsExceededError', () => {
        describe('in text mode', () => {
            beforeEach(() => {
                mockConfig.getOutputFormat.mockReturnValue(OutputFormat.TEXT);
            });
            it('should emit feedback exactly once, run cleanup, and exit with 53', () => {
                expect(() => {
                    handleMaxTurnsExceededError(mockConfig);
                }).toThrow('process.exit called with code: 53');
                expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
                expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith('error', 'Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.');
                expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
                expect(runSyncCleanupSpy).toHaveBeenCalled();
            });
        });
        describe('in JSON mode', () => {
            beforeEach(() => {
                mockConfig.getOutputFormat.mockReturnValue(OutputFormat.JSON);
            });
            it('should format max turns error as JSON, emit feedback once, and exit with 53', () => {
                expect(() => {
                    handleMaxTurnsExceededError(mockConfig);
                }).toThrow('process.exit called with code: 53');
                expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledTimes(1);
                expect(coreEventsEmitFeedbackSpy).toHaveBeenCalledWith('error', JSON.stringify({
                    session_id: TEST_SESSION_ID,
                    error: {
                        type: 'FatalTurnLimitedError',
                        message: 'Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.',
                        code: 53,
                    },
                }, null, 2));
                expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
            });
        });
        describe('in STREAM_JSON mode', () => {
            beforeEach(() => {
                mockConfig.getOutputFormat.mockReturnValue(OutputFormat.STREAM_JSON);
            });
            it('should emit result event and exit with 53', () => {
                expect(() => {
                    handleMaxTurnsExceededError(mockConfig);
                }).toThrow('process.exit called with code: 53');
                expect(coreEventsEmitFeedbackSpy).not.toHaveBeenCalled();
            });
        });
    });
});
//# sourceMappingURL=errors.test.js.map