import { describe, it, expect } from 'vitest';
import { ContextManager } from '../context-manager.js';
import { applyContextManagerUpdate, buildContextOverflowResult } from './context-control.js';
import type { Movement } from './types.js';

function mv(defaultNext?: string): Movement {
  return { name: 'execute', edit: true, persona: 'worker', instruction: '', allowedTools: [], rules: [], defaultNext };
}

function forcingCM(): ContextManager {
  return new ContextManager({ thresholds: [{ ratio: 0.005, action: 'force_transition' }], limitTokens: 10_000 });
}

const USAGE = { prompt_tokens: 80, completion_tokens: 10 };

// Codex follow-up #2: context-overflow force_transition used `movement.defaultNext`
// directly, so a terminal default (COMPLETE/ASK) became a false success/ask on
// context loss — and skipped the worker retry path. It must abort like the
// prompt-guard overflow path (buildContextOverflowResult) already does.
describe('applyContextManagerUpdate — terminal-default normalization on force_transition (Codex #2)', () => {
  it('aborts instead of falsely completing when defaultNext is COMPLETE', () => {
    const res = applyContextManagerUpdate(forcingCM(), USAGE, 0, mv('COMPLETE'), [], [], undefined, undefined);
    expect(res?.next).toBe('ABORT');
    expect(res?.abortCode).toBe('context_overflow');
  });

  it('aborts instead of falsely asking when defaultNext is ASK', () => {
    const res = applyContextManagerUpdate(forcingCM(), USAGE, 0, mv('ASK'), [], [], undefined, undefined);
    expect(res?.next).toBe('ABORT');
    expect(res?.abortCode).toBe('context_overflow');
  });

  it('aborts when there is no defaultNext', () => {
    const res = applyContextManagerUpdate(forcingCM(), USAGE, 0, mv(undefined), [], [], undefined, undefined);
    expect(res?.next).toBe('ABORT');
    expect(res?.abortCode).toBe('context_overflow');
  });

  it('honors a mid-piece movement default on force_transition', () => {
    const res = applyContextManagerUpdate(forcingCM(), USAGE, 0, mv('verify'), [], [], undefined, undefined);
    expect(res?.next).toBe('verify');
    expect(res?.abortCode).toBeUndefined();
  });
});

// buildContextOverflowResult already aborts on terminal defaults; these guard that
// both overflow paths keep the same policy.
describe('buildContextOverflowResult — same terminal-default policy (Codex #2)', () => {
  it('normalizes a COMPLETE default to ABORT', async () => {
    const res = await buildContextOverflowResult(mv('COMPLETE'), 'overflow', [], []);
    expect(res.next).toBe('ABORT');
    expect(res.abortCode).toBe('context_overflow');
  });

  it('honors a mid-piece movement default', async () => {
    const res = await buildContextOverflowResult(mv('verify'), 'overflow', [], []);
    expect(res.next).toBe('verify');
  });
});

// Livelock fix: when the client blocks a request pre-send but the local guard
// re-check finds nothing it can shrink, returning "recovered" resends the
// byte-identical request, which the client blocks again — an infinite spin
// that burns maxIterations at ~10ms per iteration (observed in production:
// 195 consecutive 14ms error calls). A recovery that changed nothing is not
// a recovery; the movement must end via the context-overflow path instead.
describe('handleLLMError — client preflight block (livelock fix)', () => {
  const blockedMessage =
    'LLM request blocked before send: estimated prompt size 102,415 tokens exceeds safe limit 102,400 tokens (80% of context 128,000). Narrow the requested content.';

  it('ends the movement instead of spinning when the guard cannot shrink anything', async () => {
    const { handleLLMError } = await import('./context-control.js');
    const res = await handleLLMError(blockedMessage, {
      movement: mv(undefined),
      messages: [{ role: 'user', content: 'tiny prompt' }],
      tools: [],
      toolsUsed: ['Grep'],
      contextManager: new ContextManager({ limitTokens: 128_000 }),
      promptGuardRatio: 0.8,
      runIsolatedLlm: async () => 'summary',
    });
    expect(res).not.toBeNull();
    expect(res?.next).toBe('ABORT');
    expect(res?.abortCode).toBe('context_overflow');
  });

  it('still returns null (continue) when the guard actually freed space', async () => {
    const { handleLLMError } = await import('./context-control.js');
    const bigContent = 'A'.repeat(21_600); // ~6,170 tokens per read
    const readPair = (callId: string): import('../../llm/openai-compat.js').Message[] => ([
      {
        role: 'assistant',
        tool_calls: [{
          id: callId,
          type: 'function',
          function: { name: 'Read', arguments: JSON.stringify({ file_path: 'output/big.txt' }) },
        }],
      },
      { role: 'tool', tool_call_id: callId, content: bigContent },
    ]);
    // Two reads of the same file: ~12,400 tokens total against a safe limit of
    // 8,000 — over the limit, but the file-read dedup stage can free one copy.
    const messages = [...readPair('c1'), ...readPair('c2')];
    const res = await handleLLMError(
      'LLM request blocked before send: estimated prompt size 12,400 tokens exceeds safe limit 8,000 tokens (80% of context 10,000).',
      {
        movement: mv(undefined),
        messages,
        tools: [],
        toolsUsed: [],
        contextManager: new ContextManager({ limitTokens: 10_000 }),
        promptGuardRatio: 0.8,
        runIsolatedLlm: async () => 'summary',
      },
    );
    expect(res).toBeNull();
  });
});

// Reserve-cap follow-up: on large-context models the guard's re-check derived
// its threshold as `limit - min(ratioReserve, 32k)`, which is LOOSER than the
// client's actual safe limit (e.g. 230,144 vs 209,715 on a 262k model). A
// prompt inside that ~20k band made the guard say "within limits, nothing to
// shrink" even though real recovery (compacting a huge tool result) was
// possible. The recovery re-check must target the safe limit the client
// actually enforced, not a re-derived, capped one.
describe('handleLLMError — recovery honors the client-reported safe limit on large-context models', () => {
  it('compacts down to the reported limit and continues instead of giving up', async () => {
    const { handleLLMError } = await import('./context-control.js');
    // 262,144-token model: client safe limit 209,715; guard re-derivation
    // with the 32k reserve cap would allow up to 230,144.
    // One giant compactable tool result puts the prompt at ~215k — inside
    // the disagreement band.
    const bigToolResult = 'A'.repeat(750_000); // ~214,300 tokens
    const messages: import('../../llm/openai-compat.js').Message[] = [
      {
        role: 'assistant',
        tool_calls: [{
          id: 'g1',
          type: 'function',
          function: { name: 'Grep', arguments: JSON.stringify({ pattern: 'x' }) },
        }],
      },
      { role: 'tool', tool_call_id: 'g1', content: bigToolResult },
    ];
    const res = await handleLLMError(
      'LLM request blocked before send: estimated prompt size 214,500 tokens exceeds safe limit 209,715 tokens (80% of context 262,144).',
      {
        movement: mv(undefined),
        messages,
        tools: [],
        toolsUsed: [],
        contextManager: new ContextManager({ limitTokens: 262_144 }),
        promptGuardRatio: 0.8,
        runIsolatedLlm: async () => 'summary',
      },
    );
    // Recovery succeeded by compaction — continue the loop.
    expect(res).toBeNull();
    // The oversized tool result was actually replaced.
    const toolMsg = messages.find((m) => m.role === 'tool');
    expect(typeof toolMsg?.content === 'string' && toolMsg.content.length).toBeLessThan(750_000);
  });
});
