import { describe, expect, it, vi } from 'vitest';
import { Agent } from '../../../agent/agent.js';
import { HookRegistryImplementation } from '../../../hooks/registry.js';
import { AfterModelCallEvent, BeforeToolCallEvent } from '../../../hooks/events.js';
import { Interrupt, InterruptState } from '../../../interrupt.js';
import { confirm, guide } from '../../../interventions/actions.js';
import { Message, TextBlock } from '../../../types/messages.js';
import { SteeringHandler } from '../handlers/handler.js';
function getHookRegistry(agent) {
    return agent._hooksRegistry;
}
describe('SteeringHandler', () => {
    const toolUse = { name: 'searchWeb', toolUseId: 'tu-1', input: { q: 'hi' } };
    function makeBeforeToolCallEvent(agent) {
        return new BeforeToolCallEvent({ agent, toolUse, tool: undefined, invocationState: {} });
    }
    function makeAfterModelCallEvent(agent) {
        return new AfterModelCallEvent({
            agent,
            model: {},
            invocationState: {},
            attemptCount: 0,
            stopData: {
                message: new Message({ role: 'assistant', content: [new TextBlock('response')] }),
                stopReason: 'endTurn',
            },
        });
    }
    it('routes beforeToolCall to subclass override with the event', async () => {
        const seen = {};
        class Spy extends SteeringHandler {
            name = 'spy';
            async beforeToolCall(event) {
                seen.agent = event.agent;
                seen.toolUse = event.toolUse;
                return guide('try again');
            }
        }
        const agent = new Agent({ interventions: [new Spy()] });
        await agent.initialize();
        const event = makeBeforeToolCallEvent(agent);
        await getHookRegistry(agent).invokeCallbacks(event);
        expect(seen.agent).toBe(agent);
        expect(seen.toolUse).toEqual(toolUse);
        expect(event.cancel).toContain('GUIDANCE:');
        expect(event.cancel).toContain('try again');
    });
    it('routes afterModelCall to subclass override with the event', async () => {
        const seen = {};
        class Spy extends SteeringHandler {
            name = 'spy';
            async afterModelCall(event) {
                if (!event.stopData)
                    return { type: 'proceed' };
                seen.message = event.stopData.message;
                seen.stopReason = event.stopData.stopReason;
                return guide('be terser');
            }
        }
        const agent = new Agent({ interventions: [new Spy()] });
        await agent.initialize();
        const event = makeAfterModelCallEvent(agent);
        await getHookRegistry(agent).invokeCallbacks(event);
        expect(seen.message).toBeDefined();
        expect(seen.stopReason).toBe('endTurn');
        expect(event.retry).toBe(true);
    });
    it('exposes provider context to subclasses via getSteeringContext', async () => {
        const fakeProvider = {
            name: 'fake',
            observeAgent() { },
            get context() {
                return { type: 'fake', tokens: 42 };
            },
        };
        let observedContext;
        class ContextReader extends SteeringHandler {
            name = 'context-reader';
            async beforeToolCall() {
                observedContext = this.getSteeringContext();
                return { type: 'proceed' };
            }
        }
        const agent = new Agent({ interventions: [new ContextReader({ contextProviders: [fakeProvider] })] });
        await agent.initialize();
        await getHookRegistry(agent).invokeCallbacks(makeBeforeToolCallEvent(agent));
        expect(observedContext).toEqual([{ type: 'fake', tokens: 42 }]);
    });
    it('siblings with distinct names can coexist on one agent', () => {
        class A extends SteeringHandler {
            name = 'steer:tool';
        }
        class B extends SteeringHandler {
            name = 'steer:model';
        }
        expect(() => new Agent({ interventions: [new A(), new B()] })).not.toThrow();
    });
    it('does not invoke afterModelCall body when stopData is missing', async () => {
        const called = vi.fn();
        class Spy extends SteeringHandler {
            name = 'spy';
            async afterModelCall(event) {
                if (event.stopData)
                    called();
                return { type: 'proceed' };
            }
        }
        const agent = new Agent({ interventions: [new Spy()] });
        await agent.initialize();
        const event = new AfterModelCallEvent({
            agent,
            model: {},
            invocationState: {},
            attemptCount: 0,
        });
        await getHookRegistry(agent).invokeCallbacks(event);
        expect(called).not.toHaveBeenCalled();
    });
    it('confirm decision flows through the interrupt system on resume (approved)', async () => {
        class Approver extends SteeringHandler {
            name = 'approver';
            async beforeToolCall() {
                return confirm('approve searchWeb?');
            }
        }
        const agent = new Agent({ interventions: [new Approver()] });
        await agent.initialize();
        // Preload an approval response so event.interrupt() returns it instead of pausing
        const interruptId = `hook:beforeToolCall:${toolUse.toolUseId}:approver`;
        const interruptState = agent._interruptState;
        interruptState.interrupts[interruptId] = new Interrupt({
            id: interruptId,
            name: 'approver',
            response: 'yes',
            source: 'hook',
        });
        const event = makeBeforeToolCallEvent(agent);
        await getHookRegistry(agent).invokeCallbacks(event);
        expect(event.cancel).toBe(false);
    });
    it('confirm decision sets cancel when human denies', async () => {
        class Approver extends SteeringHandler {
            name = 'approver';
            async beforeToolCall() {
                return confirm('approve searchWeb?');
            }
        }
        const agent = new Agent({ interventions: [new Approver()] });
        await agent.initialize();
        const interruptId = `hook:beforeToolCall:${toolUse.toolUseId}:approver`;
        const interruptState = agent._interruptState;
        interruptState.interrupts[interruptId] = new Interrupt({
            id: interruptId,
            name: 'approver',
            response: 'no',
            source: 'hook',
        });
        const event = makeBeforeToolCallEvent(agent);
        await getHookRegistry(agent).invokeCallbacks(event);
        expect(event.cancel).toBe('CONFIRMATION_FAILED: approve searchWeb?');
    });
});
//# sourceMappingURL=handler.test.js.map