import { describe, it, expect } from 'vitest';
import type { ToolCall, Message } from '../../llm/openai-compat.js';
import { classifyTerminalCalls, selectTerminalWinner, processTransitionCalls, recordControlToolResults } from './terminal-control.js';

function tc(id: string, name: string, args: unknown): ToolCall {
  return { id, type: 'function', function: { name, arguments: JSON.stringify(args) } };
}

// Codex follow-up #3: `needs_user_input` must document both the missing info and
// why a reasonable default could not be chosen. Runtime validation previously
// only required `missing_info`, weakening the "avoid unnecessary user questions"
// intent that the prompt/tool description already promise.
describe('selectTerminalWinner — needs_user_input requires why_no_default (Codex #3)', () => {
  it('rejects needs_user_input when why_no_default is missing', () => {
    const out = selectTerminalWinner(classifyTerminalCalls([
      tc('c1', 'complete', { status: 'needs_user_input', missing_info: 'which dataset?' }),
    ]));
    expect(out.kind).toBe('retry');
    if (out.kind === 'retry') expect(out.reason).toContain('why_no_default');
  });

  it('rejects needs_user_input when why_no_default is blank', () => {
    const out = selectTerminalWinner(classifyTerminalCalls([
      tc('c1', 'complete', { status: 'needs_user_input', missing_info: 'which dataset?', why_no_default: '   ' }),
    ]));
    expect(out.kind).toBe('retry');
  });

  it('accepts needs_user_input when both missing_info and why_no_default are present', () => {
    const out = selectTerminalWinner(classifyTerminalCalls([
      tc('c1', 'complete', { status: 'needs_user_input', missing_info: 'which dataset?', why_no_default: 'two equally-likely options' }),
    ]));
    expect(out.kind).toBe('native_winner');
  });

  it('leaves success/aborted validation unchanged (no why_no_default needed)', () => {
    const success = selectTerminalWinner(classifyTerminalCalls([tc('c1', 'complete', { status: 'success', result: 'the answer' })]));
    expect(success.kind).toBe('native_winner');
    const aborted = selectTerminalWinner(classifyTerminalCalls([tc('c2', 'complete', { status: 'aborted', abort_reason: 'lib failed' })]));
    expect(aborted.kind).toBe('native_winner');
  });
});

// Codex follow-up #1: a winning transition exits the movement without recording a
// tool result for its tool_call. Because the live Conversation is shared across
// movements, that leaves a dangling assistant tool_call in the next model request.
describe('processTransitionCalls — records tool results for control calls (Codex #1)', () => {
  const movement = { name: 'execute', rules: [{ condition: 'done', next: 'verify' }] };

  it('records a tool result for the winning transition call', () => {
    const messages: Message[] = [];
    const res = processTransitionCalls(
      [tc('t1', 'transition', { next_step: 'verify', summary: 's' })],
      movement, '', [], messages,
    );
    expect(res?.next).toBe('verify');
    const resultIds = new Set(messages.filter((m) => m.role === 'tool').map((m) => m.tool_call_id));
    expect(resultIds.has('t1')).toBe(true);
  });

  it('records tool results for transition calls superseded by the winner', () => {
    const messages: Message[] = [];
    const res = processTransitionCalls(
      [
        tc('t1', 'transition', { next_step: 'verify', summary: 's1' }),
        tc('t2', 'transition', { next_step: 'verify', summary: 's2' }),
      ],
      movement, '', [], messages,
    );
    expect(res?.next).toBe('verify');
    const resultIds = new Set(messages.filter((m) => m.role === 'tool').map((m) => m.tool_call_id));
    expect(resultIds.has('t1')).toBe(true);
    expect(resultIds.has('t2')).toBe(true);
  });

  it('records an error result for an invalid transition then an ack for a later valid one — no dangling', () => {
    const messages: Message[] = [];
    const res = processTransitionCalls(
      [
        tc('bad', 'transition', { next_step: 'nope', summary: 's' }),
        tc('good', 'transition', { next_step: 'verify', summary: 's' }),
      ],
      movement, '', [], messages,
    );
    expect(res?.next).toBe('verify');
    const resultIds = new Set(messages.filter((m) => m.role === 'tool').map((m) => m.tool_call_id));
    expect(resultIds.has('bad')).toBe(true); // invalid call still gets an Error result
    expect(resultIds.has('good')).toBe(true);
  });

  it('returns null and records error results when every transition target is invalid', () => {
    const messages: Message[] = [];
    const res = processTransitionCalls(
      [tc('x', 'transition', { next_step: 'nope', summary: 's' })],
      movement, '', [], messages,
    );
    expect(res).toBeNull();
    const resultIds = new Set(messages.filter((m) => m.role === 'tool').map((m) => m.tool_call_id));
    expect(resultIds.has('x')).toBe(true);
  });
});

describe('recordControlToolResults — winner + superseded control calls (Codex #1)', () => {
  it('records the winner ack and an ignored result for every superseded call', () => {
    const messages: Message[] = [];
    recordControlToolResults(
      messages,
      tc('win', 'complete', { status: 'success', result: 'done' }),
      [tc('dup', 'complete', { status: 'success', result: 'done' }), tc('tr', 'transition', { next_step: 'verify', summary: 's' })],
      'Completed with status="success".',
    );
    const byId = new Map(messages.filter((m) => m.role === 'tool').map((m) => [m.tool_call_id, m.content]));
    expect(byId.get('win')).toBe('Completed with status="success".');
    expect(byId.has('dup')).toBe(true);
    expect(byId.has('tr')).toBe(true);
  });
});
