import { describe, it, expect, vi, beforeEach } from 'vitest';
import { AgentOrchestrator } from './orchestrator.js';
import { ModelRouter } from '../../models/router.js';
import type { ChatResponse, ModelClient } from '../../models/types.js';
import { ToolRegistry, ToolExecutor } from '../../tools/index.js';
import { HookEngine } from '../../hooks/engine.js';
import { MemoryStore } from '../../memory/store.js';
import type { Session } from '../../session/index.js';
import { mkdtempSync, rmSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { auditLogger, initAuditLogger } from '../../audit/index.js';
import type { AuditLogger } from '../../audit/index.js';
import type { Message } from '../../models/types.js';

describe('AgentOrchestrator', () => {
  let mockDefaultClient: ModelClient;
  let mockFastClient: ModelClient;
  let mockComplexClient: ModelClient;
  let mockRouter: ModelRouter;

  const createMockClient = (name: string, inputTokens = 100, outputTokens = 50): ModelClient => ({
    chat: vi.fn().mockResolvedValue({
      content: `${name} response`,
      stopReason: 'end_turn',
      usage: { inputTokens, outputTokens },
    }),
  });

  beforeEach(() => {
    mockDefaultClient = createMockClient('default', 100, 50);
    mockFastClient = createMockClient('fast', 50, 25);
    mockComplexClient = createMockClient('complex', 200, 100);

    mockRouter = new ModelRouter({
      default: mockDefaultClient,
      fast: mockFastClient,
      complex: mockComplexClient,
      fallbackChain: [],
    });
  });

  const requireClient = (tier: 'default' | 'fast' | 'complex'): ModelClient => {
    const client = mockRouter.getClient(tier);
    if (!client) {
      throw new Error(`Expected ${tier} model client to exist in test router`);
    }
    return client;
  };

  describe('delegate()', () => {
    it('routes to the correct tier when specified', async () => {
      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are a helpful assistant.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
      });

      const result = await orchestrator.delegate({
        tier: 'fast',
        systemPrompt: 'Summarize this text',
        message: 'This is a test message',
        maxTokens: 1000,
      });

      expect(result.content).toBe('fast response');
      expect(result.tier).toBe('fast');
    });

    it('includes tools when requested', async () => {
      const mockToolRegistry = new ToolRegistry();
      const hooks = new HookEngine({
        confirm: ['*'],
        log: [],
        silent: [],
      });
      const mockToolExecutor = new ToolExecutor(mockToolRegistry, hooks);

      const mockFastChatClient = requireClient('fast');
      const mockFastChatFn = vi.fn().mockResolvedValue({
        content: 'response with tools',
        stopReason: 'end_turn',
        usage: { inputTokens: 100, outputTokens: 50 },
      } as ChatResponse);

      Object.assign(mockFastChatClient, { chat: mockFastChatFn });

      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are helpful.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
        toolRegistry: mockToolRegistry,
        toolExecutor: mockToolExecutor,
      });

      await orchestrator.delegate({
        tier: 'fast',
        systemPrompt: 'Use available tools',
        message: 'Help me analyze data',
        tools: true,
      });

      expect(mockFastChatFn).toHaveBeenCalled();
    });

    it('falls back to default tier when requested tier is unavailable', async () => {
      const routerWithoutComplex = new ModelRouter({
        default: mockDefaultClient,
        fast: mockFastClient,
        fallbackChain: [],
      });

      const orchestrator = new AgentOrchestrator({
        modelRouter: routerWithoutComplex,
        systemPrompt: 'You are a helpful assistant.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
      });

      const result = await orchestrator.delegate({
        tier: 'complex',
        systemPrompt: 'Analyze deeply',
        message: 'This is complex',
      });

      expect(result.content).toBe('default response');
      expect(result.tier).toBe('default');
    });

    it('tracks cumulative usage after delegate calls', async () => {
      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are helpful.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
      });

      await orchestrator.delegate({
        tier: 'fast',
        systemPrompt: 'Fast task',
        message: 'Fast message',
      });

      await orchestrator.delegate({
        tier: 'complex',
        systemPrompt: 'Complex task',
        message: 'Complex message',
      });

      await orchestrator.delegate({
        tier: 'fast',
        systemPrompt: 'Another fast task',
        message: 'Another fast message',
      });

      const usage = orchestrator.getDelegationUsage();

      expect(usage.fast).toEqual({
        inputTokens: 100,
        outputTokens: 50,
        calls: 2,
      });

      expect(usage.complex).toEqual({
        inputTokens: 200,
        outputTokens: 100,
        calls: 1,
      });

      expect(usage.default).toBeUndefined();
    });

    it('tracks usage across tiers correctly', async () => {
      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are helpful.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
      });

      await orchestrator.delegate({
        tier: 'fast',
        systemPrompt: 'Fast task',
        message: 'Fast message',
      });

      await orchestrator.delegate({
        tier: 'fast',
        systemPrompt: 'Another fast task',
        message: 'Another fast message',
      });

      const usage = orchestrator.getDelegationUsage();

      expect(usage.fast.inputTokens).toBe(100);
      expect(usage.fast.outputTokens).toBe(50);
      expect(usage.fast.calls).toBe(2);
    });

    it('logs delegation details with tier and token counts', async () => {
      const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are helpful.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
      });

      await orchestrator.delegate({
        tier: 'fast',
        systemPrompt: 'Fast task',
        message: 'Fast message',
      });

      expect(consoleSpy).toHaveBeenCalledWith(
        '[Flynn:delegate] tier=fast tokens=50+25',
      );

      consoleSpy.mockRestore();
    });
  });

  describe('getDelegationTier()', () => {
    it('returns correct tier for each task type', () => {
      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are helpful.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
      });

      expect(orchestrator.getDelegationTier('compaction')).toBe('fast');
      expect(orchestrator.getDelegationTier('memory_extraction')).toBe('default');
      expect(orchestrator.getDelegationTier('classification')).toBe('complex');
      expect(orchestrator.getDelegationTier('tool_summarisation')).toBe('default');
      expect(orchestrator.getDelegationTier('complex_reasoning')).toBe('complex');
    });

    it('returns tier that was explicitly configured', () => {
      const customDelegation = {
        compaction: 'local' as const,
        memory_extraction: 'fast' as const,
        classification: 'complex' as const,
        tool_summarisation: 'default' as const,
        complex_reasoning: 'local' as const,
      };

      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are helpful.',
        primaryTier: 'default',
        delegation: customDelegation,
        maxDelegationDepth: 10,
      });

      expect(orchestrator.getDelegationTier('compaction')).toBe('local');
      expect(orchestrator.getDelegationTier('memory_extraction')).toBe('fast');
      expect(orchestrator.getDelegationTier('complex_reasoning')).toBe('local');
    });
  });

  describe('process()', () => {
    it('proxies to NativeAgent for user messages', async () => {
      const mockDefaultChatClient = requireClient('default');
      const mockDefaultChatFn = vi.fn().mockResolvedValue({
        content: 'Agent response',
        stopReason: 'end_turn',
        usage: { inputTokens: 150, outputTokens: 75 },
      } as ChatResponse);

      Object.assign(mockDefaultChatClient, { chat: mockDefaultChatFn });

      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are a helpful agent.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
      });

      const response = await orchestrator.process('Hello, agent!');

      expect(response).toBe('Agent response');
    });

    it('maintains conversation history through process()', async () => {
      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are a helpful agent.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
      });

      await orchestrator.process('Hello');
      await orchestrator.process('How are you?');
      await orchestrator.process('Tell me about yourself');

      const history = orchestrator.getHistory();

      expect(history).toHaveLength(6);
      expect(history[0]).toEqual({ role: 'user', content: 'Hello' });
      expect(history[1]).toEqual({ role: 'assistant', content: 'default response' });
      expect(history[2]).toEqual({ role: 'user', content: 'How are you?' });
      expect(history[3]).toEqual({ role: 'assistant', content: 'default response' });
      expect(history[4]).toEqual({ role: 'user', content: 'Tell me about yourself' });
      expect(history[5]).toEqual({ role: 'assistant', content: 'default response' });
    });

    it('uses adaptive memory injection strategy when configured', async () => {
      const tempDir = mkdtempSync(join(tmpdir(), 'flynn-orchestrator-memory-'));
      const memoryStore = new MemoryStore({ dir: tempDir, maxContextTokens: 2000 });
      memoryStore.writeCategory('user', 'preferences', 'User prefers concise output.', 'replace');

      const mockDefaultChatClient = requireClient('default');
      const mockDefaultChatFn = vi.fn().mockResolvedValue({
        content: 'Agent response',
        stopReason: 'end_turn',
        usage: { inputTokens: 50, outputTokens: 25 },
      } as ChatResponse);
      Object.assign(mockDefaultChatClient, { chat: mockDefaultChatFn });

      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are a helpful agent.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
        memoryStore,
        memoryInjectionStrategy: 'adaptive',
        memoryMaxInjectionTokens: 100,
      });

      await orchestrator.process('Keep this concise please');

      expect(mockDefaultChatFn).toHaveBeenCalled();
      const callArgs = mockDefaultChatFn.mock.calls[0][0];
      expect(callArgs.system).toContain('# Memory Context');
      expect(callArgs.system).toContain('concise');

      rmSync(tempDir, { recursive: true, force: true });
    });

    it('falls back to default memory context when adaptive injection errors', async () => {
      const tempDir = mkdtempSync(join(tmpdir(), 'flynn-orchestrator-memory-fallback-'));
      const memoryStore = new MemoryStore({ dir: tempDir, maxContextTokens: 2000 });
      memoryStore.write('user', 'Fallback memory content', 'replace');
      const getPromptSectionsSpy = vi.spyOn(memoryStore, 'getPromptSections').mockImplementationOnce(() => {
        throw new Error('boom');
      });

      const mockDefaultChatClient = requireClient('default');
      const mockDefaultChatFn = vi.fn().mockResolvedValue({
        content: 'Agent response',
        stopReason: 'end_turn',
        usage: { inputTokens: 50, outputTokens: 25 },
      } as ChatResponse);
      Object.assign(mockDefaultChatClient, { chat: mockDefaultChatFn });

      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are a helpful agent.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
        memoryStore,
        memoryInjectionStrategy: 'adaptive',
        memoryMaxInjectionTokens: 100,
      });

      await orchestrator.process('test message');

      const callArgs = mockDefaultChatFn.mock.calls[0][0];
      expect(callArgs.system).toContain('Fallback memory content');

      getPromptSectionsSpy.mockRestore();
      rmSync(tempDir, { recursive: true, force: true });
    });

    it('appends daily memory log entries when enabled', async () => {
      const tempDir = mkdtempSync(join(tmpdir(), 'flynn-orchestrator-daily-log-'));
      const memoryStore = new MemoryStore({ dir: tempDir, maxContextTokens: 2000 });

      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are a helpful agent.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
        memoryStore,
        memoryDailyLogEnabled: true,
        memoryDailyLogNamespacePrefix: 'daily',
      });

      await orchestrator.process('Log this turn');
      const date = new Date().toISOString().slice(0, 10);
      const dailyLog = memoryStore.read(`daily/${date}`);

      expect(dailyLog).toContain('Log this turn');
      expect(dailyLog).toContain('default response');
      expect(dailyLog).toContain('tool_calls: 0');
      expect(dailyLog).toContain('- session: unknown');
      expect(dailyLog).toContain('- channel: unknown');
      expect(dailyLog).toContain('- sender: unknown');

      rmSync(tempDir, { recursive: true, force: true });
    });

    it('honors daily log truncation and metadata toggles', async () => {
      const tempDir = mkdtempSync(join(tmpdir(), 'flynn-orchestrator-daily-log-config-'));
      const memoryStore = new MemoryStore({ dir: tempDir, maxContextTokens: 2000 });
      const session: Session = {
        id: 'ws:test-user',
        addMessage: vi.fn(),
        getHistory: vi.fn(() => []),
        clear: vi.fn(),
        replaceHistory: vi.fn(),
        getConfig: vi.fn(() => undefined),
        setConfig: vi.fn(),
        deleteConfig: vi.fn(),
      };

      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are a helpful agent.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
        session,
        memoryStore,
        memoryDailyLogEnabled: true,
        memoryDailyLogNamespacePrefix: 'daily',
        memoryDailyLogIncludeSessionMetadata: false,
        memoryDailyLogMaxUserChars: 120,
        memoryDailyLogMaxAssistantChars: 150,
      });

      await orchestrator.process('x'.repeat(500));
      const date = new Date().toISOString().slice(0, 10);
      const dailyLog = memoryStore.read(`daily/${date}`);

      expect(dailyLog).toContain('...[truncated]');
      expect(dailyLog).not.toContain('- session:');
      expect(dailyLog).not.toContain('- channel:');
      expect(dailyLog).not.toContain('- sender:');

      rmSync(tempDir, { recursive: true, force: true });
    });

    it('runs proactive per-turn extraction when enabled and threshold is met', async () => {
      const tempDir = mkdtempSync(join(tmpdir(), 'flynn-orchestrator-proactive-extract-'));
      const memoryStore = new MemoryStore({ dir: tempDir, maxContextTokens: 2000 });

      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are a helpful agent.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
        memoryStore,
        memoryProactiveExtractEnabled: true,
        memoryProactiveExtractMinToolCalls: 0,
        memoryProactiveExtractNamespace: 'global',
      });

      await orchestrator.process('Capture durable context');
      const extracted = memoryStore.read('global');

      expect(extracted).toContain('default response');

      rmSync(tempDir, { recursive: true, force: true });
    });
  });

  describe('compact()', () => {
    it('emits compaction audit event with message counts (not token counts)', async () => {
      const compactClient: ModelClient = {
        chat: vi.fn().mockResolvedValue({
          content: 'summary',
          stopReason: 'end_turn',
          usage: { inputTokens: 8, outputTokens: 4 },
        }),
      };
      const compactRouter = new ModelRouter({
        default: compactClient,
        fast: compactClient,
        complex: compactClient,
        fallbackChain: [],
      });

      const history: Message[] = [
        { role: 'user', content: 'u1' },
        { role: 'assistant', content: 'a1' },
        { role: 'user', content: 'u2' },
        { role: 'assistant', content: 'a2' },
        { role: 'user', content: 'u3' },
        { role: 'assistant', content: 'a3' },
      ];
      const session: Session = {
        id: 'session-compact-audit',
        addMessage: vi.fn((m: Message) => { history.push(m); }),
        getHistory: vi.fn(() => [...history]),
        clear: vi.fn(() => { history.length = 0; }),
        replaceHistory: vi.fn((msgs: Message[]) => {
          history.length = 0;
          history.push(...msgs);
        }),
        getConfig: vi.fn(() => undefined),
        setConfig: vi.fn(),
        deleteConfig: vi.fn(),
      };

      const sessionCompact = vi.fn();
      const previousAuditLogger = auditLogger;
      initAuditLogger({ sessionCompact } as unknown as AuditLogger);
      try {
        const orchestrator = new AgentOrchestrator({
          modelRouter: compactRouter,
          systemPrompt: 'You are helpful.',
          session,
          primaryTier: 'default',
          delegation: {
            compaction: 'fast',
            memory_extraction: 'default',
            classification: 'complex',
            tool_summarisation: 'default',
            complex_reasoning: 'complex',
          },
          maxDelegationDepth: 10,
          compaction: {
            thresholdPct: 80,
            keepTurns: 1,
            summaryMaxTokens: 128,
            importanceThreshold: 1,
          },
        });

        const result = await orchestrator.compact();
        expect(result).not.toBeNull();
        expect(sessionCompact).toHaveBeenCalledWith(expect.objectContaining({
          session_id: 'session-compact-audit',
          messages_before: 6,
          messages_after: 3,
        }));
      } finally {
        initAuditLogger(previousAuditLogger as unknown as AuditLogger);
      }
    });
  });

  describe('reset()', () => {
    it('clears primary agent conversation history', async () => {
      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are a helpful agent.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
      });

      await orchestrator.process('Hello');
      await orchestrator.process('How are you?');

      expect(orchestrator.getHistory()).toHaveLength(4);

      orchestrator.reset();

      expect(orchestrator.getHistory()).toHaveLength(0);
    });

    it('can be called multiple times', async () => {
      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are a helpful agent.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
      });

      await orchestrator.process('Hello');
      orchestrator.reset();

      expect(orchestrator.getHistory()).toHaveLength(0);

      await orchestrator.process('World');
      orchestrator.reset();

      expect(orchestrator.getHistory()).toHaveLength(0);
    });
  });

  describe('getDelegationUsage()', () => {
    it('returns copy of usage stats (doesn\'t expose internal map)', async () => {
      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are helpful.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
      });

      await orchestrator.delegate({
        tier: 'fast',
        systemPrompt: 'Fast task',
        message: 'Fast message',
      });

      const usage1 = orchestrator.getDelegationUsage();
      const usage2 = orchestrator.getDelegationUsage();

      expect(usage1).toEqual(usage2);

      usage1.fast.inputTokens = 999;

      expect(usage2.fast.inputTokens).toBe(50);
    });

    it('returns empty object when no usage tracked', async () => {
      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are helpful.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
      });

      const usage = orchestrator.getDelegationUsage();

      expect(usage).toEqual({});
    });
  });

  describe('getHistory()', () => {
    it('returns conversation history from primary agent', async () => {
      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are a helpful agent.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
      });

      await orchestrator.process('Hello');
      await orchestrator.process('How are you?');

      const history = orchestrator.getHistory();

      expect(history).toHaveLength(4);
      expect(history[0]).toEqual({ role: 'user', content: 'Hello' });
      expect(history[1]).toEqual({ role: 'assistant', content: 'default response' });
      expect(history[2]).toEqual({ role: 'user', content: 'How are you?' });
      expect(history[3]).toEqual({ role: 'assistant', content: 'default response' });
    });

    it('returns empty array when no history', async () => {
      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are a helpful agent.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
      });

      const history = orchestrator.getHistory();

      expect(history).toEqual([]);
    });
  });

  describe('process()', () => {
    it('rolls back tool-loop provider errors, hard-trims on context overflow, and retries once', async () => {
      let callCount = 0;
      const mockClient: ModelClient = {
        chat: vi.fn().mockImplementation(async () => {
          callCount++;
          if (callCount === 1) {
            return {
              content: '',
              stopReason: 'tool_use',
              usage: { inputTokens: 10, outputTokens: 5 },
              toolCalls: [{ id: 'call_1', name: 'test.echo', args: { text: 'hi' } }],
            } as ChatResponse;
          }
          if (callCount === 2) {
            // Simulate llama.cpp context overflow buried inside an aggregated router error.
            throw new Error(
              'llama-server error (400): {"error":{"type":"exceedcontextsizeerror","nprompttokens":9183,"nctx":4096}}',
            );
          }
          return {
            content: 'ok',
            stopReason: 'end_turn',
            usage: { inputTokens: 10, outputTokens: 5 },
          } as ChatResponse;
        }),
      };

      const router = new ModelRouter({
        default: mockClient,
        fallbackChain: [],
      });

      // Minimal Session stub that supports rollback via replaceHistory().
      const history: Message[] = [];
      const session: Session = {
        id: 'test',
        addMessage: vi.fn((m: Message) => { history.push(m); }),
        getHistory: vi.fn(() => [...history]),
        clear: vi.fn(() => { history.length = 0; }),
        replaceHistory: vi.fn((msgs: Message[]) => {
          history.length = 0;
          history.push(...msgs);
        }),
        getConfig: vi.fn(() => undefined),
        setConfig: vi.fn(),
        deleteConfig: vi.fn(),
      };

      const registry = new ToolRegistry();
      registry.register({
        name: 'test.echo',
        description: 'echo',
        inputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
        execute: async (args: { text?: string }) => ({ success: true, output: String(args.text ?? '') }),
      });

      const hooks = new HookEngine({ confirm: [], log: [], silent: [] });
      const executor = new ToolExecutor(registry, hooks);

      const orchestrator = new AgentOrchestrator({
        modelRouter: router,
        systemPrompt: 'You are helpful.',
        session,
        toolRegistry: registry,
        toolExecutor: executor,
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 3,
      });

      const res = await orchestrator.process('hello');
      expect(res).toBe('ok');

      // Ensure we didn't persist the low-level error string in history.
      const textHistory = history
        .map(m => (typeof m.content === 'string' ? m.content : ''))
        .join('\n');
      expect(textHistory).not.toContain('Error in tool loop');
    });

    it('auto-escalates to complex tier when primary tier fails', async () => {
      const defaultClient: ModelClient = {
        chat: vi.fn().mockRejectedValue(new Error('default tier down')),
      };
      const complexClient: ModelClient = {
        chat: vi.fn().mockResolvedValue({
          content: 'complex recovered response',
          stopReason: 'end_turn',
          usage: { inputTokens: 20, outputTokens: 10 },
        }),
      };
      const router = new ModelRouter({
        default: defaultClient,
        complex: complexClient,
        fallbackChain: [],
      });

      const sessionHistory: Message[] = [];
      const session: Session = {
        id: 'auto-escalate',
        addMessage: vi.fn((m: Message) => { sessionHistory.push(m); }),
        getHistory: vi.fn(() => [...sessionHistory]),
        clear: vi.fn(() => { sessionHistory.length = 0; }),
        replaceHistory: vi.fn((msgs: Message[]) => {
          sessionHistory.length = 0;
          sessionHistory.push(...msgs);
        }),
        getConfig: vi.fn(() => undefined),
        setConfig: vi.fn(),
        deleteConfig: vi.fn(),
      };

      const orchestrator = new AgentOrchestrator({
        modelRouter: router,
        systemPrompt: 'You are helpful.',
        session,
        primaryTier: 'default',
        autoEscalate: true,
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 3,
      });

      const res = await orchestrator.process('please recover');
      expect(res).toBe('complex recovered response');
      expect(orchestrator.getModelTier()).toBe('default');
      expect(defaultClient.chat).toHaveBeenCalled();
      expect(complexClient.chat).toHaveBeenCalled();
    });

    it('returns friendly error when auto-escalate is disabled and primary tier fails', async () => {
      const defaultClient: ModelClient = {
        chat: vi.fn().mockRejectedValue(new Error('primary tier failure')),
      };
      const router = new ModelRouter({
        default: defaultClient,
        fallbackChain: [],
      });

      const sessionHistory: Message[] = [];
      const session: Session = {
        id: 'no-auto-escalate',
        addMessage: vi.fn((m: Message) => { sessionHistory.push(m); }),
        getHistory: vi.fn(() => [...sessionHistory]),
        clear: vi.fn(() => { sessionHistory.length = 0; }),
        replaceHistory: vi.fn((msgs: Message[]) => {
          sessionHistory.length = 0;
          sessionHistory.push(...msgs);
        }),
        getConfig: vi.fn(() => undefined),
        setConfig: vi.fn(),
        deleteConfig: vi.fn(),
      };

      const orchestrator = new AgentOrchestrator({
        modelRouter: router,
        systemPrompt: 'You are helpful.',
        session,
        primaryTier: 'default',
        autoEscalate: false,
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 3,
      });

      const res = await orchestrator.process('fail');
      expect(res).toContain('model/provider error');
      expect(orchestrator.getModelTier()).toBe('default');
    });
  });

  describe('setModelTier()', () => {
    it('sets model tier on primary agent', () => {
      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are helpful.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
      });

      orchestrator.setModelTier('fast');

      expect(orchestrator.getModelTier()).toBe('fast');
    });

    it('allows tier changes after initialization', () => {
      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are helpful.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
      });

      expect(orchestrator.getModelTier()).toBe('default');

      orchestrator.setModelTier('complex');

      expect(orchestrator.getModelTier()).toBe('complex');

      orchestrator.setModelTier('fast');

      expect(orchestrator.getModelTier()).toBe('fast');
    });
  });

  describe('setOnToolUse()', () => {
    it('sets tool-use callback on primary agent', () => {
      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are helpful.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
      });

      const callback = vi.fn();

      orchestrator.setOnToolUse(callback);

      expect(orchestrator.getModelTier()).toBe('default');
    });

    it('allows callback changes', () => {
      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are helpful.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
      });

      const callback1 = vi.fn();
      const callback2 = vi.fn();

      orchestrator.setOnToolUse(callback1);

      expect(orchestrator.getModelTier()).toBe('default');

      orchestrator.setOnToolUse(callback2);

      expect(orchestrator.getModelTier()).toBe('default');
    });
  });

  describe('context budget and proactive maintenance', () => {
    it('reports context budget with estimated usage and threshold', async () => {
      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are helpful.',
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
        contextWindow: 1000,
        compaction: {
          thresholdPct: 80,
          keepTurns: 2,
          summaryMaxTokens: 256,
          importanceThreshold: 1,
        },
      });

      await orchestrator.process('hello world');
      const budget = orchestrator.getContextBudget();

      expect(budget.contextWindow).toBe(1000);
      expect(budget.thresholdPct).toBe(80);
      expect(budget.thresholdTokens).toBe(800);
      expect(budget.estimatedTokens).toBeGreaterThan(0);
      expect(budget.remainingTokens).toBeLessThanOrEqual(1000);
    });

    it('creates proactive checkpoint alerts and writes checkpoint to memory', async () => {
      const tempDir = mkdtempSync(join(tmpdir(), 'flynn-orchestrator-checkpoint-'));
      const memoryStore = new MemoryStore({ dir: tempDir, maxContextTokens: 2000 });
      const large = 'x'.repeat(2400);
      const history: Message[] = [
        { role: 'user', content: large },
        { role: 'assistant', content: large },
      ];
      const session: Session = {
        id: 'ws:context-test',
        addMessage: vi.fn((m: Message) => { history.push(m); }),
        getHistory: vi.fn(() => [...history]),
        clear: vi.fn(() => { history.length = 0; }),
        replaceHistory: vi.fn((msgs: Message[]) => {
          history.length = 0;
          history.push(...msgs);
        }),
        getConfig: vi.fn(() => undefined),
        setConfig: vi.fn(),
        deleteConfig: vi.fn(),
      };

      const sessionCheckpoint = vi.fn();
      const previousAuditLogger = auditLogger;
      initAuditLogger({ sessionCheckpoint } as unknown as AuditLogger);

      const orchestrator = new AgentOrchestrator({
        modelRouter: mockRouter,
        systemPrompt: 'You are helpful.',
        session,
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'default',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
        memoryStore,
        contextWindow: 2000,
        compaction: {
          thresholdPct: 95,
          keepTurns: 1,
          summaryMaxTokens: 256,
          importanceThreshold: 1,
          proactive: {
            enabled: true,
            warnPct: 40,
            checkpointPct: 50,
            autoCompactPct: 95,
            checkpointCooldownMs: 1000,
            memoryNamespace: 'session/checkpoints',
          },
        },
      });

      try {
        await orchestrator.process('ping');
        const alert = orchestrator.consumeContextAlert();
        expect(alert?.level).toBe('checkpoint');
        expect(alert?.actions.checkpointSaved).toBe(true);
        expect(alert?.actions.checkpointNamespace).toContain('session/checkpoints');
        expect(orchestrator.consumeContextAlert()).toBeUndefined();

        const stored = memoryStore.read('session/checkpoints/ws/context-test');
        expect(stored.length).toBeGreaterThan(0);
        expect(sessionCheckpoint).toHaveBeenCalledWith(expect.objectContaining({
          session_id: 'ws:context-test',
        }));
      } finally {
        initAuditLogger(previousAuditLogger as unknown as AuditLogger);
        rmSync(tempDir, { recursive: true, force: true });
      }
    });

    it('auto-compacts proactively at critical threshold and emits alert', async () => {
      const compactClient: ModelClient = {
        chat: vi.fn().mockResolvedValue({
          content: 'summary',
          stopReason: 'end_turn',
          usage: { inputTokens: 8, outputTokens: 4 },
        }),
      };
      const compactRouter = new ModelRouter({
        default: compactClient,
        fast: compactClient,
        fallbackChain: [],
      });

      const large = 'y'.repeat(2400);
      const history: Message[] = [
        { role: 'user', content: large },
        { role: 'assistant', content: large },
        { role: 'user', content: large },
        { role: 'assistant', content: large },
      ];
      const session: Session = {
        id: 'ws:auto-compact',
        addMessage: vi.fn((m: Message) => { history.push(m); }),
        getHistory: vi.fn(() => [...history]),
        clear: vi.fn(() => { history.length = 0; }),
        replaceHistory: vi.fn((msgs: Message[]) => {
          history.length = 0;
          history.push(...msgs);
        }),
        getConfig: vi.fn(() => undefined),
        setConfig: vi.fn(),
        deleteConfig: vi.fn(),
      };

      const sessionAutoCompact = vi.fn();
      const sessionCompact = vi.fn();
      const previousAuditLogger = auditLogger;
      initAuditLogger({ sessionAutoCompact, sessionCompact } as unknown as AuditLogger);

      const orchestrator = new AgentOrchestrator({
        modelRouter: compactRouter,
        systemPrompt: 'You are helpful.',
        session,
        primaryTier: 'default',
        delegation: {
          compaction: 'fast',
          memory_extraction: 'fast',
          classification: 'complex',
          tool_summarisation: 'default',
          complex_reasoning: 'complex',
        },
        maxDelegationDepth: 10,
        contextWindow: 2000,
        compaction: {
          thresholdPct: 99,
          keepTurns: 1,
          summaryMaxTokens: 128,
          importanceThreshold: 1,
          proactive: {
            enabled: true,
            warnPct: 70,
            checkpointPct: 80,
            autoCompactPct: 90,
            checkpointCooldownMs: 1000,
            memoryNamespace: 'session/checkpoints',
          },
        },
      });

      try {
        await orchestrator.process('continue');
        const alert = orchestrator.consumeContextAlert();
        expect(alert?.actions.autoCompacted).toBe(true);
        expect(history.length).toBeLessThan(6);
        expect(sessionAutoCompact).toHaveBeenCalledWith(expect.objectContaining({
          session_id: 'ws:auto-compact',
          compacted: true,
        }));
      } finally {
        initAuditLogger(previousAuditLogger as unknown as AuditLogger);
      }
    });
  });
});
