/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { HookEventHandler } from './hookEventHandler.js';
import { NotificationType, SessionStartSource, HookEventName, HookType, } from './types.js';
// Mock debugLogger
const mockDebugLogger = vi.hoisted(() => ({
    log: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
    debug: vi.fn(),
}));
// Mock coreEvents
const mockCoreEvents = vi.hoisted(() => ({
    emitFeedback: vi.fn(),
    emitHookStart: vi.fn(),
    emitHookEnd: vi.fn(),
}));
vi.mock('../utils/debugLogger.js', () => ({
    debugLogger: mockDebugLogger,
}));
vi.mock('../utils/events.js', () => ({
    coreEvents: mockCoreEvents,
}));
vi.mock('../telemetry/clearcut-logger/clearcut-logger.js', () => ({
    ClearcutLogger: {
        getInstance: vi.fn().mockReturnValue({
            logHookCallEvent: vi.fn(),
        }),
    },
}));
describe('HookEventHandler', () => {
    let hookEventHandler;
    let mockConfig;
    let mockHookPlanner;
    let mockHookRunner;
    let mockHookAggregator;
    beforeEach(() => {
        vi.resetAllMocks();
        mockConfig = {
            getSessionId: vi.fn().mockReturnValue('test-session'),
            getWorkingDir: vi.fn().mockReturnValue('/test/project'),
            getGeminiClient: vi.fn().mockReturnValue({
                getChatRecordingService: vi.fn().mockReturnValue({
                    getConversationFilePath: vi
                        .fn()
                        .mockReturnValue('/test/project/.gemini/tmp/chats/session.json'),
                }),
            }),
        };
        mockHookPlanner = {
            createExecutionPlan: vi.fn(),
        };
        mockHookRunner = {
            executeHooksParallel: vi.fn(),
            executeHooksSequential: vi.fn(),
        };
        mockHookAggregator = {
            aggregateResults: vi.fn(),
        };
        hookEventHandler = new HookEventHandler(mockConfig, mockHookPlanner, mockHookRunner, mockHookAggregator);
    });
    describe('fireBeforeToolEvent', () => {
        it('should fire BeforeTool event with correct input', async () => {
            const mockPlan = [
                {
                    hookConfig: {
                        type: HookType.Command,
                        command: './test.sh',
                    },
                    eventName: HookEventName.BeforeTool,
                },
            ];
            const mockResults = [
                {
                    success: true,
                    duration: 100,
                    hookConfig: {
                        type: HookType.Command,
                        command: './test.sh',
                        timeout: 30000,
                    },
                    eventName: HookEventName.BeforeTool,
                },
            ];
            const mockAggregated = {
                success: true,
                allOutputs: [],
                errors: [],
                totalDuration: 100,
            };
            vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue({
                eventName: HookEventName.BeforeTool,
                hookConfigs: mockPlan.map((p) => p.hookConfig),
                sequential: false,
            });
            vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue(mockResults);
            vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue(mockAggregated);
            const result = await hookEventHandler.fireBeforeToolEvent('EditTool', {
                file: 'test.txt',
            });
            expect(mockHookPlanner.createExecutionPlan).toHaveBeenCalledWith(HookEventName.BeforeTool, { toolName: 'EditTool' });
            expect(mockHookRunner.executeHooksParallel).toHaveBeenCalledWith([mockPlan[0].hookConfig], HookEventName.BeforeTool, expect.objectContaining({
                session_id: 'test-session',
                cwd: '/test/project',
                hook_event_name: 'BeforeTool',
                tool_name: 'EditTool',
                tool_input: { file: 'test.txt' },
            }), expect.any(Function), expect.any(Function));
            // Verify event emission via callbacks
            const onHookStart = vi.mocked(mockHookRunner.executeHooksParallel).mock
                .calls[0][3];
            const onHookEnd = vi.mocked(mockHookRunner.executeHooksParallel).mock
                .calls[0][4];
            if (onHookStart)
                onHookStart(mockPlan[0].hookConfig, 0);
            expect(mockCoreEvents.emitHookStart).toHaveBeenCalledWith({
                hookName: './test.sh',
                eventName: HookEventName.BeforeTool,
                hookIndex: 1,
                totalHooks: 1,
            });
            if (onHookEnd)
                onHookEnd(mockPlan[0].hookConfig, mockResults[0]);
            expect(mockCoreEvents.emitHookEnd).toHaveBeenCalledWith({
                hookName: './test.sh',
                eventName: HookEventName.BeforeTool,
                success: true,
            });
            expect(result).toBe(mockAggregated);
        });
        it('should return empty result when no hooks to execute', async () => {
            vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue(null);
            const result = await hookEventHandler.fireBeforeToolEvent('EditTool', {});
            expect(result.success).toBe(true);
            expect(result.allOutputs).toHaveLength(0);
            expect(result.errors).toHaveLength(0);
            expect(result.totalDuration).toBe(0);
        });
        it('should handle execution errors gracefully', async () => {
            vi.mocked(mockHookPlanner.createExecutionPlan).mockImplementation(() => {
                throw new Error('Planning failed');
            });
            const result = await hookEventHandler.fireBeforeToolEvent('EditTool', {});
            expect(result.success).toBe(false);
            expect(result.errors).toHaveLength(1);
            expect(result.errors[0].message).toBe('Planning failed');
            expect(mockDebugLogger.error).toHaveBeenCalled();
        });
        it('should emit feedback when some hooks fail', async () => {
            const mockPlan = [
                {
                    type: HookType.Command,
                    command: './fail.sh',
                },
            ];
            const mockResults = [
                {
                    success: false,
                    duration: 50,
                    hookConfig: mockPlan[0],
                    eventName: HookEventName.BeforeTool,
                    error: new Error('Failed to execute'),
                },
            ];
            const mockAggregated = {
                success: false,
                allOutputs: [],
                errors: [new Error('Failed to execute')],
                totalDuration: 50,
            };
            vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue({
                eventName: HookEventName.BeforeTool,
                hookConfigs: mockPlan,
                sequential: false,
            });
            vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue(mockResults);
            vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue(mockAggregated);
            await hookEventHandler.fireBeforeToolEvent('EditTool', {});
            expect(mockCoreEvents.emitFeedback).toHaveBeenCalledWith('warning', expect.stringContaining('./fail.sh'));
            expect(mockCoreEvents.emitFeedback).toHaveBeenCalledWith('warning', expect.stringContaining('F12'));
        });
        it('should fire BeforeTool event with MCP context when provided', async () => {
            const mockPlan = [
                {
                    hookConfig: {
                        type: HookType.Command,
                        command: './test.sh',
                    },
                    eventName: HookEventName.BeforeTool,
                },
            ];
            const mockResults = [
                {
                    success: true,
                    duration: 100,
                    hookConfig: {
                        type: HookType.Command,
                        command: './test.sh',
                        timeout: 30000,
                    },
                    eventName: HookEventName.BeforeTool,
                },
            ];
            const mockAggregated = {
                success: true,
                allOutputs: [],
                errors: [],
                totalDuration: 100,
            };
            vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue({
                eventName: HookEventName.BeforeTool,
                hookConfigs: mockPlan.map((p) => p.hookConfig),
                sequential: false,
            });
            vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue(mockResults);
            vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue(mockAggregated);
            const mcpContext = {
                server_name: 'my-mcp-server',
                tool_name: 'read_file',
                command: 'npx',
                args: ['-y', '@my-org/mcp-server'],
            };
            const result = await hookEventHandler.fireBeforeToolEvent('my-mcp-server__read_file', { path: '/etc/passwd' }, mcpContext);
            expect(mockHookRunner.executeHooksParallel).toHaveBeenCalledWith([mockPlan[0].hookConfig], HookEventName.BeforeTool, expect.objectContaining({
                session_id: 'test-session',
                cwd: '/test/project',
                hook_event_name: 'BeforeTool',
                tool_name: 'my-mcp-server__read_file',
                tool_input: { path: '/etc/passwd' },
                mcp_context: mcpContext,
            }), expect.any(Function), expect.any(Function));
            expect(result).toBe(mockAggregated);
        });
        it('should not include mcp_context when not provided', async () => {
            const mockPlan = [
                {
                    hookConfig: {
                        type: HookType.Command,
                        command: './test.sh',
                    },
                    eventName: HookEventName.BeforeTool,
                },
            ];
            const mockResults = [
                {
                    success: true,
                    duration: 100,
                    hookConfig: {
                        type: HookType.Command,
                        command: './test.sh',
                        timeout: 30000,
                    },
                    eventName: HookEventName.BeforeTool,
                },
            ];
            const mockAggregated = {
                success: true,
                allOutputs: [],
                errors: [],
                totalDuration: 100,
            };
            vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue({
                eventName: HookEventName.BeforeTool,
                hookConfigs: mockPlan.map((p) => p.hookConfig),
                sequential: false,
            });
            vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue(mockResults);
            vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue(mockAggregated);
            await hookEventHandler.fireBeforeToolEvent('EditTool', {
                file: 'test.txt',
            });
            const callArgs = vi.mocked(mockHookRunner.executeHooksParallel).mock
                .calls[0][2];
            expect(callArgs).not.toHaveProperty('mcp_context');
        });
    });
    describe('fireAfterToolEvent', () => {
        it('should fire AfterTool event with tool response', async () => {
            const mockPlan = [
                {
                    hookConfig: {
                        type: HookType.Command,
                        command: './after.sh',
                    },
                    eventName: HookEventName.AfterTool,
                },
            ];
            const mockResults = [
                {
                    success: true,
                    duration: 100,
                    hookConfig: {
                        type: HookType.Command,
                        command: './test.sh',
                        timeout: 30000,
                    },
                    eventName: HookEventName.BeforeTool,
                },
            ];
            const mockAggregated = {
                success: true,
                allOutputs: [],
                errors: [],
                totalDuration: 100,
            };
            vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue({
                eventName: HookEventName.BeforeTool,
                hookConfigs: mockPlan.map((p) => p.hookConfig),
                sequential: false,
            });
            vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue(mockResults);
            vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue(mockAggregated);
            const toolInput = { file: 'test.txt' };
            const toolResponse = { success: true, content: 'File edited' };
            const result = await hookEventHandler.fireAfterToolEvent('EditTool', toolInput, toolResponse);
            expect(mockHookRunner.executeHooksParallel).toHaveBeenCalledWith([mockPlan[0].hookConfig], HookEventName.AfterTool, expect.objectContaining({
                tool_name: 'EditTool',
                tool_input: toolInput,
                tool_response: toolResponse,
            }), expect.any(Function), expect.any(Function));
            expect(result).toBe(mockAggregated);
        });
        it('should fire AfterTool event with MCP context when provided', async () => {
            const mockPlan = [
                {
                    hookConfig: {
                        type: HookType.Command,
                        command: './after.sh',
                    },
                    eventName: HookEventName.AfterTool,
                },
            ];
            const mockResults = [
                {
                    success: true,
                    duration: 100,
                    hookConfig: {
                        type: HookType.Command,
                        command: './after.sh',
                        timeout: 30000,
                    },
                    eventName: HookEventName.AfterTool,
                },
            ];
            const mockAggregated = {
                success: true,
                allOutputs: [],
                errors: [],
                totalDuration: 100,
            };
            vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue({
                eventName: HookEventName.AfterTool,
                hookConfigs: mockPlan.map((p) => p.hookConfig),
                sequential: false,
            });
            vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue(mockResults);
            vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue(mockAggregated);
            const toolInput = { path: '/etc/passwd' };
            const toolResponse = { success: true, content: 'File content' };
            const mcpContext = {
                server_name: 'my-mcp-server',
                tool_name: 'read_file',
                url: 'https://mcp.example.com',
            };
            const result = await hookEventHandler.fireAfterToolEvent('my-mcp-server__read_file', toolInput, toolResponse, mcpContext);
            expect(mockHookRunner.executeHooksParallel).toHaveBeenCalledWith([mockPlan[0].hookConfig], HookEventName.AfterTool, expect.objectContaining({
                tool_name: 'my-mcp-server__read_file',
                tool_input: toolInput,
                tool_response: toolResponse,
                mcp_context: mcpContext,
            }), expect.any(Function), expect.any(Function));
            expect(result).toBe(mockAggregated);
        });
    });
    describe('fireBeforeAgentEvent', () => {
        it('should fire BeforeAgent event with prompt', async () => {
            const mockPlan = [
                {
                    hookConfig: {
                        type: HookType.Command,
                        command: './before_agent.sh',
                    },
                    eventName: HookEventName.BeforeAgent,
                },
            ];
            const mockResults = [
                {
                    success: true,
                    duration: 100,
                    hookConfig: {
                        type: HookType.Command,
                        command: './test.sh',
                        timeout: 30000,
                    },
                    eventName: HookEventName.BeforeTool,
                },
            ];
            const mockAggregated = {
                success: true,
                allOutputs: [],
                errors: [],
                totalDuration: 100,
            };
            vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue({
                eventName: HookEventName.BeforeTool,
                hookConfigs: mockPlan.map((p) => p.hookConfig),
                sequential: false,
            });
            vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue(mockResults);
            vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue(mockAggregated);
            const prompt = 'Please help me with this task';
            const result = await hookEventHandler.fireBeforeAgentEvent(prompt);
            expect(mockHookRunner.executeHooksParallel).toHaveBeenCalledWith([mockPlan[0].hookConfig], HookEventName.BeforeAgent, expect.objectContaining({
                prompt,
            }), expect.any(Function), expect.any(Function));
            expect(result).toBe(mockAggregated);
        });
    });
    describe('fireNotificationEvent', () => {
        it('should fire Notification event', async () => {
            const mockPlan = [
                {
                    hookConfig: {
                        type: HookType.Command,
                        command: './notification-hook.sh',
                    },
                    eventName: HookEventName.Notification,
                },
            ];
            const mockResults = [
                {
                    success: true,
                    duration: 50,
                    hookConfig: {
                        type: HookType.Command,
                        command: './notification-hook.sh',
                        timeout: 30000,
                    },
                    eventName: HookEventName.Notification,
                },
            ];
            const mockAggregated = {
                success: true,
                allOutputs: [],
                errors: [],
                totalDuration: 50,
            };
            vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue({
                eventName: HookEventName.Notification,
                hookConfigs: mockPlan.map((p) => p.hookConfig),
                sequential: false,
            });
            vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue(mockResults);
            vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue(mockAggregated);
            const message = 'Tool execution requires permission';
            const result = await hookEventHandler.fireNotificationEvent(NotificationType.ToolPermission, message, { type: 'ToolPermission', title: 'Test Permission' });
            expect(mockHookRunner.executeHooksParallel).toHaveBeenCalledWith([mockPlan[0].hookConfig], HookEventName.Notification, expect.objectContaining({
                notification_type: 'ToolPermission',
                details: { type: 'ToolPermission', title: 'Test Permission' },
            }), expect.any(Function), expect.any(Function));
            expect(result).toBe(mockAggregated);
        });
    });
    describe('fireSessionStartEvent', () => {
        it('should fire SessionStart event with source', async () => {
            const mockPlan = [
                {
                    hookConfig: {
                        type: HookType.Command,
                        command: './session_start.sh',
                    },
                    eventName: HookEventName.SessionStart,
                },
            ];
            const mockResults = [
                {
                    success: true,
                    duration: 200,
                    hookConfig: {
                        type: HookType.Command,
                        command: './session_start.sh',
                        timeout: 30000,
                    },
                    eventName: HookEventName.SessionStart,
                },
            ];
            const mockAggregated = {
                success: true,
                allOutputs: [],
                errors: [],
                totalDuration: 200,
            };
            vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue({
                eventName: HookEventName.SessionStart,
                hookConfigs: mockPlan.map((p) => p.hookConfig),
                sequential: false,
            });
            vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue(mockResults);
            vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue(mockAggregated);
            const result = await hookEventHandler.fireSessionStartEvent(SessionStartSource.Startup);
            expect(mockHookPlanner.createExecutionPlan).toHaveBeenCalledWith(HookEventName.SessionStart, { trigger: 'startup' });
            expect(mockHookRunner.executeHooksParallel).toHaveBeenCalledWith([mockPlan[0].hookConfig], HookEventName.SessionStart, expect.objectContaining({
                source: 'startup',
            }), expect.any(Function), expect.any(Function));
            expect(result).toBe(mockAggregated);
        });
    });
    describe('fireBeforeModelEvent', () => {
        it('should fire BeforeModel event with LLM request', async () => {
            const mockPlan = [
                {
                    hookConfig: {
                        type: HookType.Command,
                        command: './model-hook.sh',
                    },
                    eventName: HookEventName.BeforeModel,
                },
            ];
            const mockResults = [
                {
                    success: true,
                    duration: 150,
                    hookConfig: {
                        type: HookType.Command,
                        command: './model-hook.sh',
                        timeout: 30000,
                    },
                    eventName: HookEventName.BeforeModel,
                },
            ];
            const mockAggregated = {
                success: true,
                allOutputs: [],
                errors: [],
                totalDuration: 150,
            };
            vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue({
                eventName: HookEventName.BeforeModel,
                hookConfigs: mockPlan.map((p) => p.hookConfig),
                sequential: false,
            });
            vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue(mockResults);
            vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue(mockAggregated);
            const llmRequest = {
                model: 'gemini-pro',
                config: { temperature: 0.7 },
                contents: [{ role: 'user', parts: [{ text: 'Hello' }] }],
            };
            const result = await hookEventHandler.fireBeforeModelEvent(llmRequest);
            expect(mockHookRunner.executeHooksParallel).toHaveBeenCalledWith([mockPlan[0].hookConfig], HookEventName.BeforeModel, expect.objectContaining({
                llm_request: expect.objectContaining({
                    model: 'gemini-pro',
                    messages: expect.arrayContaining([
                        expect.objectContaining({
                            role: 'user',
                            content: 'Hello',
                        }),
                    ]),
                }),
            }), expect.any(Function), expect.any(Function));
            expect(result).toBe(mockAggregated);
        });
    });
    describe('failure suppression', () => {
        it('should suppress duplicate feedback for the same failing hook and request context', async () => {
            const mockHook = {
                type: HookType.Command,
                command: './fail.sh',
                name: 'failing-hook',
            };
            const mockResults = [
                {
                    success: false,
                    duration: 10,
                    hookConfig: mockHook,
                    eventName: HookEventName.AfterModel,
                    error: new Error('Failed'),
                },
            ];
            const mockAggregated = {
                success: false,
                allOutputs: [],
                errors: [new Error('Failed')],
                totalDuration: 10,
            };
            vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue({
                eventName: HookEventName.AfterModel,
                hookConfigs: [mockHook],
                sequential: false,
            });
            vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue(mockResults);
            vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue(mockAggregated);
            const llmRequest = { model: 'test', contents: [] };
            const llmResponse = { candidates: [] };
            // First call - should emit feedback
            await hookEventHandler.fireAfterModelEvent(llmRequest, llmResponse);
            expect(mockCoreEvents.emitFeedback).toHaveBeenCalledTimes(1);
            // Second call with SAME request - should NOT emit feedback
            await hookEventHandler.fireAfterModelEvent(llmRequest, llmResponse);
            expect(mockCoreEvents.emitFeedback).toHaveBeenCalledTimes(1);
            // Third call with DIFFERENT request - should emit feedback again
            const differentRequest = { model: 'different', contents: [] };
            await hookEventHandler.fireAfterModelEvent(differentRequest, llmResponse);
            expect(mockCoreEvents.emitFeedback).toHaveBeenCalledTimes(2);
        });
    });
    describe('createBaseInput', () => {
        it('should create base input with correct fields', async () => {
            const mockPlan = [
                {
                    hookConfig: {
                        type: HookType.Command,
                        command: './test.sh',
                    },
                    eventName: HookEventName.BeforeTool,
                },
            ];
            vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue({
                eventName: HookEventName.BeforeTool,
                hookConfigs: mockPlan.map((p) => p.hookConfig),
                sequential: false,
            });
            vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue([]);
            vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue({
                success: true,
                allOutputs: [],
                errors: [],
                totalDuration: 0,
            });
            await hookEventHandler.fireBeforeToolEvent('TestTool', {});
            expect(mockHookRunner.executeHooksParallel).toHaveBeenCalledWith(expect.any(Array), HookEventName.BeforeTool, expect.objectContaining({
                session_id: 'test-session',
                transcript_path: '/test/project/.gemini/tmp/chats/session.json',
                cwd: '/test/project',
                hook_event_name: 'BeforeTool',
                timestamp: expect.any(String),
            }), expect.any(Function), expect.any(Function));
        });
    });
});
//# sourceMappingURL=hookEventHandler.test.js.map