import { describe, it, expect, vi } from 'vitest';
import { ConversationManager, } from '../conversation-manager.js';
import { NullConversationManager } from '../null-conversation-manager.js';
import { Agent } from '../../agent/agent.js';
import { Message, TextBlock } from '../../index.js';
import { AfterModelCallEvent, BeforeModelCallEvent } from '../../hooks/events.js';
import { ContextWindowOverflowError } from '../../errors.js';
import { createMockAgent, invokeTrackedHook } from '../../__fixtures__/agent-helpers.js';
import { MockMessageModel } from '../../__fixtures__/mock-message-model.js';
import { warnOnce } from '../../logging/warn-once.js';
vi.mock('../../logging/warn-once.js', () => ({
    warnOnce: vi.fn(),
}));
class TestConversationManager extends ConversationManager {
    name = 'test:conversation-manager';
    reduceCallCount = 0;
    shouldReduce = true;
    constructor(options) {
        super(options);
    }
    reduce({ agent }) {
        this.reduceCallCount++;
        if (!this.shouldReduce)
            return false;
        agent.messages.splice(0, 1);
        return true;
    }
}
class ThresholdTestManager extends ConversationManager {
    name = 'test:threshold-manager';
    reduceCallCount = 0;
    shouldReduce = true;
    constructor(options) {
        super(options);
    }
    reduce({ agent }) {
        this.reduceCallCount++;
        if (!this.shouldReduce)
            return false;
        agent.messages.splice(0, 1);
        return true;
    }
}
describe('ConversationManager', () => {
    describe('initAgent', () => {
        it('registers both AfterModelCallEvent and BeforeModelCallEvent hooks', () => {
            const manager = new TestConversationManager();
            const mockAgent = createMockAgent();
            manager.initAgent(mockAgent);
            // Always registers both hooks now
            expect(mockAgent.trackedHooks).toHaveLength(2);
            expect(mockAgent.trackedHooks[0].eventType).toBe(AfterModelCallEvent);
            expect(mockAgent.trackedHooks[1].eventType).toBe(BeforeModelCallEvent);
        });
        it('calls reduce and sets retry=true on ContextWindowOverflowError when reduce returns true', async () => {
            const manager = new TestConversationManager();
            const messages = [
                new Message({ role: 'user', content: [new TextBlock('Message 1')] }),
                new Message({ role: 'assistant', content: [new TextBlock('Response 1')] }),
            ];
            const mockAgent = createMockAgent({ messages });
            manager.initAgent(mockAgent);
            const error = new ContextWindowOverflowError('overflow');
            const event = new AfterModelCallEvent({
                agent: mockAgent,
                model: {},
                attemptCount: 1,
                error,
                invocationState: {},
            });
            await invokeTrackedHook(mockAgent, event);
            expect(manager.reduceCallCount).toBe(1);
            expect(event.retry).toBe(true);
            expect(mockAgent.messages).toHaveLength(1);
        });
        it('does not set retry when reduce returns false', async () => {
            const manager = new TestConversationManager();
            manager.shouldReduce = false;
            const mockAgent = createMockAgent();
            manager.initAgent(mockAgent);
            const error = new ContextWindowOverflowError('overflow');
            const event = new AfterModelCallEvent({
                agent: mockAgent,
                model: {},
                attemptCount: 1,
                error,
                invocationState: {},
            });
            await invokeTrackedHook(mockAgent, event);
            expect(manager.reduceCallCount).toBe(1);
            expect(event.retry).toBeUndefined();
        });
        it('does not call reduce for non-overflow errors', async () => {
            const manager = new TestConversationManager();
            const mockAgent = createMockAgent();
            manager.initAgent(mockAgent);
            const error = new Error('some other error');
            const event = new AfterModelCallEvent({
                agent: mockAgent,
                model: {},
                attemptCount: 1,
                error,
                invocationState: {},
            });
            await invokeTrackedHook(mockAgent, event);
            expect(manager.reduceCallCount).toBe(0);
            expect(event.retry).toBeUndefined();
        });
        it('passes error to reduce when called due to overflow', async () => {
            const receivedArgs = [];
            class CapturingManager extends ConversationManager {
                name = 'test:capturing';
                reduce(args) {
                    receivedArgs.push(args);
                    return false;
                }
            }
            const manager = new CapturingManager();
            const mockAgent = createMockAgent();
            manager.initAgent(mockAgent);
            const error = new ContextWindowOverflowError('overflow');
            const event = new AfterModelCallEvent({
                agent: mockAgent,
                model: {},
                attemptCount: 1,
                error,
                invocationState: {},
            });
            await invokeTrackedHook(mockAgent, event);
            expect(receivedArgs).toHaveLength(1);
            expect(receivedArgs[0].error).toBe(error);
            expect(receivedArgs[0].agent).toBe(mockAgent);
        });
    });
    describe('proactiveCompression', () => {
        const mockModel = { getConfig: () => ({ contextWindowLimit: 1000 }) };
        it('always registers a BeforeModelCallEvent hook regardless of proactiveCompression setting', () => {
            const manager = new TestConversationManager();
            const mockAgent = createMockAgent();
            manager.initAgent(mockAgent);
            // Both hooks always registered
            expect(mockAgent.trackedHooks).toHaveLength(2);
            expect(mockAgent.trackedHooks[0].eventType).toBe(AfterModelCallEvent);
            expect(mockAgent.trackedHooks[1].eventType).toBe(BeforeModelCallEvent);
        });
        it('BeforeModelCallEvent handler is a no-op when proactiveCompression is not set', async () => {
            const manager = new ThresholdTestManager();
            const mockAgent = createMockAgent();
            manager.initAgent(mockAgent);
            const event = new BeforeModelCallEvent({
                agent: mockAgent,
                model: mockModel,
                invocationState: {},
                projectedInputTokens: 900, // Would exceed any threshold
            });
            await invokeTrackedHook(mockAgent, event);
            expect(manager.reduceCallCount).toBe(0);
        });
        it('uses default threshold of 0.7 when proactiveCompression is true', async () => {
            const manager = new ThresholdTestManager({ proactiveCompression: true });
            const messages = [
                new Message({ role: 'user', content: [new TextBlock('Message 1')] }),
                new Message({ role: 'assistant', content: [new TextBlock('Response 1')] }),
            ];
            const mockAgent = createMockAgent({ messages });
            manager.initAgent(mockAgent);
            // 650/1000 = 0.65 < 0.7 — should NOT trigger
            const event = new BeforeModelCallEvent({
                agent: mockAgent,
                model: mockModel,
                invocationState: {},
                projectedInputTokens: 650,
            });
            await invokeTrackedHook(mockAgent, event);
            expect(manager.reduceCallCount).toBe(0);
            // 800/1000 = 0.8 >= 0.7 — should trigger
            const event2 = new BeforeModelCallEvent({
                agent: mockAgent,
                model: mockModel,
                invocationState: {},
                projectedInputTokens: 800,
            });
            await invokeTrackedHook(mockAgent, event2);
            expect(manager.reduceCallCount).toBe(1);
        });
        it('calls reduce without error when projected tokens exceed custom threshold', async () => {
            const receivedArgs = [];
            class CapturingManager extends ConversationManager {
                name = 'test:capturing-threshold';
                reduce(args) {
                    receivedArgs.push(args);
                    args.agent.messages.splice(0, 1);
                    return true;
                }
            }
            const manager = new CapturingManager({ proactiveCompression: { compressionThreshold: 0.5 } });
            const messages = [
                new Message({ role: 'user', content: [new TextBlock('Message 1')] }),
                new Message({ role: 'assistant', content: [new TextBlock('Response 1')] }),
            ];
            const mockAgent = createMockAgent({ messages });
            manager.initAgent(mockAgent);
            const event = new BeforeModelCallEvent({
                agent: mockAgent,
                model: mockModel,
                invocationState: {},
                projectedInputTokens: 600, // 600/1000 = 0.6 >= 0.5
            });
            await invokeTrackedHook(mockAgent, event);
            expect(receivedArgs).toHaveLength(1);
            expect(receivedArgs[0].error).toBeUndefined();
            expect(receivedArgs[0].model).toBe(mockModel);
            expect(receivedArgs[0].agent).toBe(mockAgent);
        });
        it('does not call reduce when below threshold', async () => {
            const manager = new ThresholdTestManager({ proactiveCompression: { compressionThreshold: 0.7 } });
            const mockAgent = createMockAgent();
            manager.initAgent(mockAgent);
            const event = new BeforeModelCallEvent({
                agent: mockAgent,
                model: mockModel,
                invocationState: {},
                projectedInputTokens: 500, // 500/1000 = 0.5 < 0.7
            });
            await invokeTrackedHook(mockAgent, event);
            expect(manager.reduceCallCount).toBe(0);
        });
        it('does not call reduce when projectedInputTokens is undefined', async () => {
            const manager = new ThresholdTestManager({ proactiveCompression: true });
            const mockAgent = createMockAgent();
            manager.initAgent(mockAgent);
            const event = new BeforeModelCallEvent({
                agent: mockAgent,
                model: mockModel,
                invocationState: {},
            });
            await invokeTrackedHook(mockAgent, event);
            expect(manager.reduceCallCount).toBe(0);
        });
        it('uses 200k default when contextWindowLimit is undefined and logs warning', async () => {
            const manager = new ThresholdTestManager({ proactiveCompression: { compressionThreshold: 0.7 } });
            const messages = [
                new Message({ role: 'user', content: [new TextBlock('Message 1')] }),
                new Message({ role: 'assistant', content: [new TextBlock('Response 1')] }),
            ];
            const mockAgent = createMockAgent({ messages });
            manager.initAgent(mockAgent);
            const modelWithoutLimit = { getConfig: () => ({}) };
            // 150000/200000 = 0.75 >= 0.7 — should trigger with the 200k default
            const event = new BeforeModelCallEvent({
                agent: mockAgent,
                model: modelWithoutLimit,
                invocationState: {},
                projectedInputTokens: 150000,
            });
            await invokeTrackedHook(mockAgent, event);
            expect(manager.reduceCallCount).toBe(1);
            expect(warnOnce).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('contextWindowLimit is not set on the model, using default of 200000'));
        });
        it('does not trigger with 200k default when below threshold', async () => {
            const manager = new ThresholdTestManager({ proactiveCompression: { compressionThreshold: 0.7 } });
            const mockAgent = createMockAgent();
            manager.initAgent(mockAgent);
            const modelWithoutLimit = { getConfig: () => ({}) };
            // 100000/200000 = 0.5 < 0.7 — should NOT trigger
            const event = new BeforeModelCallEvent({
                agent: mockAgent,
                model: modelWithoutLimit,
                invocationState: {},
                projectedInputTokens: 100000,
            });
            await invokeTrackedHook(mockAgent, event);
            expect(manager.reduceCallCount).toBe(0);
        });
        it('swallows errors from proactive reduce and continues', async () => {
            class ThrowingManager extends ConversationManager {
                name = 'test:throwing';
                reduce({ error }) {
                    if (!error) {
                        throw new Error('proactive compression exploded');
                    }
                    return false;
                }
            }
            const manager = new ThrowingManager({ proactiveCompression: true });
            const mockAgent = createMockAgent();
            manager.initAgent(mockAgent);
            const event = new BeforeModelCallEvent({
                agent: mockAgent,
                model: mockModel,
                invocationState: {},
                projectedInputTokens: 800,
            });
            // Should not throw — error is swallowed
            await expect(invokeTrackedHook(mockAgent, event)).resolves.toBeUndefined();
        });
        it('throws on compressionThreshold <= 0', () => {
            expect(() => new ThresholdTestManager({ proactiveCompression: { compressionThreshold: 0 } })).toThrow('must be between 0 (exclusive) and 1 (inclusive)');
            expect(() => new ThresholdTestManager({ proactiveCompression: { compressionThreshold: -1 } })).toThrow('must be between 0 (exclusive) and 1 (inclusive)');
        });
        it('throws on compressionThreshold > 1', () => {
            expect(() => new ThresholdTestManager({ proactiveCompression: { compressionThreshold: 1.5 } })).toThrow('must be between 0 (exclusive) and 1 (inclusive)');
        });
        it('accepts compressionThreshold of exactly 1', () => {
            expect(() => new ThresholdTestManager({ proactiveCompression: { compressionThreshold: 1 } })).not.toThrow();
        });
    });
});
describe('overflow propagation', () => {
    it('propagates ContextWindowOverflowError out of the agent loop when reduce returns false', async () => {
        const model = new MockMessageModel();
        model.addTurn(new ContextWindowOverflowError('context window exceeded'));
        const agent = new Agent({
            model,
            conversationManager: new NullConversationManager(),
            printer: false,
        });
        await expect(agent.invoke('hello')).rejects.toThrow(ContextWindowOverflowError);
    });
});
//# sourceMappingURL=conversation-manager.test.js.map