import { beforeEach, describe, expect, it, vi } from 'vitest';
import { PluginLifecycleManager } from '../lifecycle.js';
describe('PluginLifecycleManager', () => {
    let manager;
    let pluginRegistry;
    let toolRegistry;
    let logger;
    const mockRunId = 'run_test';
    const mockPluginId = 'plugin_test';
    beforeEach(() => {
        // Create mock registries and logger
        pluginRegistry = {
            register: vi.fn(),
            unregister: vi.fn(),
            getOrThrow: vi.fn(),
            findByName: vi.fn(),
            getAll: vi.fn(() => []),
        };
        toolRegistry = {
            register: vi.fn(),
            unregister: vi.fn(),
            execute: vi.fn(),
            getAll: vi.fn(() => []),
        };
        logger = {
            child: vi.fn(() => ({
                info: vi.fn(),
                warn: vi.fn(),
                error: vi.fn(),
                debug: vi.fn(),
            })),
            info: vi.fn(),
            warn: vi.fn(),
            error: vi.fn(),
            debug: vi.fn(),
        };
        manager = new PluginLifecycleManager({
            pluginRegistry,
            toolRegistry,
            log: logger,
            hookTimeoutMs: 5000,
        });
    });
    describe('executeHooks', () => {
        it('should return empty array when no hooks registered', async () => {
            const results = await manager.executeHooks('run_start', { runId: mockRunId });
            expect(results).toEqual([]);
        });
        it('should execute all registered hooks for an event', async () => {
            const hook1Handler = vi.fn(async () => ({ action: 'continue' }));
            const hook2Handler = vi.fn(async () => ({ action: 'continue' }));
            // Register hooks manually
            manager['hookHandlers'].set('run_start', [
                { pluginId: 'plugin_1', handler: hook1Handler },
                { pluginId: 'plugin_2', handler: hook2Handler },
            ]);
            const results = await manager.executeHooks('run_start', { runId: mockRunId });
            expect(results).toHaveLength(2);
            expect(hook1Handler).toHaveBeenCalled();
            expect(hook2Handler).toHaveBeenCalled();
        });
        it('should handle hook timeout', async () => {
            const slowHandler = vi.fn(() => new Promise((resolve) => {
                setTimeout(() => resolve({ action: 'continue' }), 10000);
            }));
            manager['hookHandlers'].set('run_start', [{ pluginId: mockPluginId, handler: slowHandler }]);
            const managerWithShortTimeout = new PluginLifecycleManager({
                pluginRegistry,
                toolRegistry,
                log: logger,
                hookTimeoutMs: 10, // Very short timeout
            });
            managerWithShortTimeout['hookHandlers'].set('run_start', [
                { pluginId: mockPluginId, handler: slowHandler },
            ]);
            const results = await managerWithShortTimeout.executeHooks('run_start', {
                runId: mockRunId,
            });
            expect(results).toHaveLength(1);
            expect(results[0]?.action).toBe('error');
            if (results[0]?.action === 'error') {
                expect(results[0].message).toContain('timeout');
            }
        });
        describe('Hook ordering semantics', () => {
            it('should execute pre_* hooks in registration order (first registered first)', async () => {
                const executionOrder = [];
                const handler1 = vi.fn(async () => {
                    executionOrder.push('hook1');
                    return { action: 'continue' };
                });
                const handler2 = vi.fn(async () => {
                    executionOrder.push('hook2');
                    return { action: 'continue' };
                });
                const handler3 = vi.fn(async () => {
                    executionOrder.push('hook3');
                    return { action: 'continue' };
                });
                manager['hookHandlers'].set('pre_tool_use', [
                    { pluginId: 'plugin_1', handler: handler1 },
                    { pluginId: 'plugin_2', handler: handler2 },
                    { pluginId: 'plugin_3', handler: handler3 },
                ]);
                await manager.executeHooks('pre_tool_use', { runId: mockRunId });
                expect(executionOrder).toEqual(['hook1', 'hook2', 'hook3']);
            });
            it('should execute post_* hooks in reverse registration order (last registered first)', async () => {
                const executionOrder = [];
                const handler1 = vi.fn(async () => {
                    executionOrder.push('hook1');
                    return { action: 'continue' };
                });
                const handler2 = vi.fn(async () => {
                    executionOrder.push('hook2');
                    return { action: 'continue' };
                });
                const handler3 = vi.fn(async () => {
                    executionOrder.push('hook3');
                    return { action: 'continue' };
                });
                manager['hookHandlers'].set('post_tool_use', [
                    { pluginId: 'plugin_1', handler: handler1 },
                    { pluginId: 'plugin_2', handler: handler2 },
                    { pluginId: 'plugin_3', handler: handler3 },
                ]);
                await manager.executeHooks('post_tool_use', { runId: mockRunId });
                // Reverse order: hook3 -> hook2 -> hook1
                expect(executionOrder).toEqual(['hook3', 'hook2', 'hook1']);
            });
            it('should execute non-pre/post hooks in registration order', async () => {
                const executionOrder = [];
                const handler1 = vi.fn(async () => {
                    executionOrder.push('hook1');
                    return { action: 'continue' };
                });
                const handler2 = vi.fn(async () => {
                    executionOrder.push('hook2');
                    return { action: 'continue' };
                });
                manager['hookHandlers'].set('run_start', [
                    { pluginId: 'plugin_1', handler: handler1 },
                    { pluginId: 'plugin_2', handler: handler2 },
                ]);
                await manager.executeHooks('run_start', { runId: mockRunId });
                expect(executionOrder).toEqual(['hook1', 'hook2']);
            });
        });
        describe('Flow control: action priority', () => {
            it('should short-circuit on error action', async () => {
                const handler1 = vi.fn(async () => {
                    return { action: 'error', message: 'Hook failed' };
                });
                const handler2 = vi.fn(async () => {
                    return { action: 'continue' };
                });
                manager['hookHandlers'].set('run_start', [
                    { pluginId: 'plugin_1', handler: handler1 },
                    { pluginId: 'plugin_2', handler: handler2 },
                ]);
                const results = await manager.executeHooks('run_start', { runId: mockRunId });
                expect(results).toHaveLength(1);
                expect(results[0]?.action).toBe('error');
                expect(handler2).not.toHaveBeenCalled();
            });
            it('should short-circuit on skip action', async () => {
                const handler1 = vi.fn(async () => {
                    return { action: 'skip', reason: 'Condition not met' };
                });
                const handler2 = vi.fn(async () => {
                    return { action: 'continue' };
                });
                manager['hookHandlers'].set('pre_tool_use', [
                    { pluginId: 'plugin_1', handler: handler1 },
                    { pluginId: 'plugin_2', handler: handler2 },
                ]);
                const results = await manager.executeHooks('pre_tool_use', { runId: mockRunId });
                expect(results).toHaveLength(1);
                expect(results[0]?.action).toBe('skip');
                expect(handler2).not.toHaveBeenCalled();
            });
            it('should short-circuit and return resume action', async () => {
                const handler1 = vi.fn(async () => {
                    return { action: 'resume', input: 'new_input_value' };
                });
                const handler2 = vi.fn(async () => {
                    return { action: 'continue' };
                });
                manager['hookHandlers'].set('pre_llm_call', [
                    { pluginId: 'plugin_1', handler: handler1 },
                    { pluginId: 'plugin_2', handler: handler2 },
                ]);
                const results = await manager.executeHooks('pre_llm_call', { runId: mockRunId });
                expect(results).toHaveLength(1);
                expect(results[0]?.action).toBe('resume');
                if (results[0]?.action === 'resume') {
                    expect(results[0].input).toBe('new_input_value');
                }
                expect(handler2).not.toHaveBeenCalled();
            });
            it('should short-circuit and return retry action', async () => {
                const handler1 = vi.fn(async () => {
                    return { action: 'retry' };
                });
                const handler2 = vi.fn(async () => {
                    return { action: 'continue' };
                });
                // Use pre_* hook for forward execution order (plugin_1 runs first and short-circuits).
                // post_* hooks run in reverse order for cleanup semantics.
                manager['hookHandlers'].set('pre_llm_call', [
                    { pluginId: 'plugin_1', handler: handler1 },
                    { pluginId: 'plugin_2', handler: handler2 },
                ]);
                const results = await manager.executeHooks('pre_llm_call', { runId: mockRunId });
                expect(results).toHaveLength(1);
                expect(results[0]?.action).toBe('retry');
                expect(handler2).not.toHaveBeenCalled();
            });
            it('should continue executing on modify action', async () => {
                const handler1 = vi.fn(async () => {
                    return { action: 'modify', input: { updated: true } };
                });
                const handler2 = vi.fn(async () => {
                    return { action: 'continue' };
                });
                manager['hookHandlers'].set('pre_tool_use', [
                    { pluginId: 'plugin_1', handler: handler1 },
                    { pluginId: 'plugin_2', handler: handler2 },
                ]);
                const results = await manager.executeHooks('pre_tool_use', { runId: mockRunId });
                expect(results).toHaveLength(2);
                expect(results[0]?.action).toBe('modify');
                expect(results[1]?.action).toBe('continue');
                expect(handler2).toHaveBeenCalled();
            });
            it('should continue executing on continue action', async () => {
                const handler1 = vi.fn(async () => {
                    return { action: 'continue' };
                });
                const handler2 = vi.fn(async () => {
                    return { action: 'continue' };
                });
                manager['hookHandlers'].set('iteration_start', [
                    { pluginId: 'plugin_1', handler: handler1 },
                    { pluginId: 'plugin_2', handler: handler2 },
                ]);
                const results = await manager.executeHooks('iteration_start', { runId: mockRunId });
                expect(results).toHaveLength(2);
                expect(handler1).toHaveBeenCalled();
                expect(handler2).toHaveBeenCalled();
            });
        });
        describe('Hook context', () => {
            it('should pass correct context to hook handler', async () => {
                let capturedContext = null;
                const handler = vi.fn(async (ctx) => {
                    capturedContext = ctx;
                    return { action: 'continue' };
                });
                manager['hookHandlers'].set('pre_tool_use', [{ pluginId: mockPluginId, handler }]);
                const contextData = {
                    runId: mockRunId,
                    toolName: 'test_tool',
                    toolInput: { key: 'value' },
                };
                await manager.executeHooks('pre_tool_use', contextData);
                expect(capturedContext).not.toBeNull();
                const ctx = capturedContext;
                expect(ctx.runId).toBe(mockRunId);
                expect(ctx.pluginId).toBe(mockPluginId);
                expect(ctx.event).toBe('pre_tool_use');
                expect(ctx.toolName).toBe('test_tool');
                expect(ctx.toolInput).toEqual({ key: 'value' });
            });
            it('should include iteration number in context when provided', async () => {
                let capturedContext = null;
                const handler = vi.fn(async (ctx) => {
                    capturedContext = ctx;
                    return { action: 'continue' };
                });
                manager['hookHandlers'].set('iteration_end', [{ pluginId: mockPluginId, handler }]);
                await manager.executeHooks('iteration_end', {
                    runId: mockRunId,
                    iteration: 5,
                });
                const ctx = capturedContext;
                expect(ctx.iteration).toBe(5);
            });
        });
        describe('Hook execution logging', () => {
            it('should emit hook_executed event with correct metadata', async () => {
                const events = [];
                manager.on((evt) => events.push(evt));
                const handler = vi.fn(async () => {
                    return { action: 'continue' };
                });
                manager['hookHandlers'].set('run_start', [{ pluginId: mockPluginId, handler }]);
                await manager.executeHooks('run_start', { runId: mockRunId });
                const hookExecutedEvents = events.filter((evt) => evt.type === 'plugin_hook_executed');
                expect(hookExecutedEvents).toHaveLength(1);
                const event = hookExecutedEvents[0];
                expect(event?.pluginId).toBe(mockPluginId);
                expect(event?.hookEvent).toBe('run_start');
                expect(typeof event?.durationMs).toBe('number');
                expect(event?.durationMs).toBeGreaterThanOrEqual(0);
            });
        });
        describe('Exception handling', () => {
            it('should catch thrown exceptions and return error action', async () => {
                const handler = vi.fn(async () => {
                    throw new Error('Handler crashed');
                });
                manager['hookHandlers'].set('run_start', [{ pluginId: mockPluginId, handler }]);
                const results = await manager.executeHooks('run_start', { runId: mockRunId });
                expect(results).toHaveLength(1);
                expect(results[0]?.action).toBe('error');
                if (results[0]?.action === 'error') {
                    expect(results[0].message).toContain('Handler crashed');
                }
            });
            it('should not throw when handler throws', async () => {
                const handler = vi.fn(async () => {
                    throw new Error('Handler failed');
                });
                manager['hookHandlers'].set('run_start', [{ pluginId: mockPluginId, handler }]);
                const executePromise = manager.executeHooks('run_start', { runId: mockRunId });
                await expect(executePromise).resolves.not.toThrow();
            });
        });
        describe('RunEvent emission', () => {
            it('should emit plugin_hook_executing and plugin_hook_completed when emitRunEvent provided', async () => {
                const emitted = [];
                const emitRunEvent = vi.fn(async (event) => {
                    emitted.push(event);
                });
                const handler = vi.fn(async () => ({ action: 'modify', input: { x: 1 } }));
                manager['hookHandlers'].set('pre_tool_use', [{ pluginId: mockPluginId, handler }]);
                await manager.executeHooks('pre_tool_use', { runId: mockRunId, toolName: 't', toolInput: {} }, emitRunEvent);
                expect(emitted).toHaveLength(2);
                expect(emitted[0]).toMatchObject({
                    type: 'plugin_hook_executing',
                    runId: mockRunId,
                    pluginId: mockPluginId,
                    hookEvent: 'pre_tool_use',
                });
                expect(emitted[1]).toMatchObject({
                    type: 'plugin_hook_completed',
                    runId: mockRunId,
                    pluginId: mockPluginId,
                    hookEvent: 'pre_tool_use',
                });
                expect(emitted[1].result).toEqual({ action: 'modify', input: { x: 1 } });
            });
            it('should not emit RunEvents when emitRunEvent omitted', async () => {
                const handler = vi.fn(async () => ({ action: 'continue' }));
                manager['hookHandlers'].set('run_start', [{ pluginId: mockPluginId, handler }]);
                await expect(manager.executeHooks('run_start', { runId: mockRunId })).resolves.toHaveLength(1);
            });
        });
    });
});
//# sourceMappingURL=lifecycle.test.js.map