import { describe, it, expect, vi } from 'vitest';
import type { LLMEvent, OpenAICompatClient } from '../../llm/openai-compat.js';
import type { EventLogger } from '../../progress/event-log.js';
import type { MovementWatchdogs } from './watchdogs.js';
import type { AgentLoopCallbacks } from './types.js';
import { runLlmIteration } from './llm-iteration.js';

class FakeClient {
  timeoutMs = 0;
  readonly calls: unknown[] = [];
  constructor(private readonly events: LLMEvent[]) {}
  async *chat(messages: unknown): AsyncGenerator<LLMEvent> {
    this.calls.push(messages);
    for (const e of this.events) yield e;
  }
}

function captureLogger(): EventLogger & { kinds: string[] } {
  const kinds: string[] = [];
  const lg = {
    kinds,
    emit: (kind: string) => { kinds.push(kind); return 'e'; },
    startCorrelation: () => 'c',
    child: () => lg,
    describe: () => ({ runId: 'r', seq: 0, degraded: false }),
  };
  return lg as unknown as EventLogger & { kinds: string[] };
}

function fakeWatchdogs(): MovementWatchdogs & { marked: string[] } {
  const marked: string[] = [];
  return {
    marked,
    markToolUse: (n: string) => { marked.push(n); },
    maybeFire: () => {},
  } as MovementWatchdogs & { marked: string[] };
}

function run(events: LLMEvent[], extra: Partial<{ callbacks: AgentLoopCallbacks; toolsUsed: string[] }> = {}) {
  const client = new FakeClient(events) as unknown as OpenAICompatClient;
  const watchdogs = fakeWatchdogs();
  const logger = captureLogger();
  const toolsUsed = extra.toolsUsed ?? [];
  return runLlmIteration({
    client, messages: [{ role: 'user', content: 'hi' }], tools: [],
    callbacks: extra.callbacks, toolsUsed, watchdogs,
    movementName: 'm1', iteration: 0, events: logger, userId: 'u1',
  }).then((res) => ({ res, toolsUsed, watchdogs, logger }));
}

describe('runLlmIteration', () => {
  it('accumulates text and returns pending tool calls + duration', async () => {
    const { res } = await run([
      { type: 'text', text: 'hello world' },
      { type: 'tool_use', id: 'r1', name: 'Read', input: { file_path: 'a' } },
      { type: 'done', usage: { prompt_tokens: 10, completion_tokens: 5 } },
    ]);
    expect(res.accumulatedText).toBe('hello world');
    expect(res.pendingToolCalls).toHaveLength(1);
    expect(res.hadError).toBe(false);
    expect(res.lastUsage).toEqual({ prompt_tokens: 10, completion_tokens: 5 });
    expect(typeof res.llmDurationMs).toBe('number');
    expect(res.llmDurationMs).toBeGreaterThanOrEqual(0);
  });

  it('records regular tool names in toolsUsed but excludes control tools', async () => {
    const { res, toolsUsed, watchdogs } = await run([
      { type: 'tool_use', id: 'r1', name: 'Read', input: {} },
      { type: 'tool_use', id: 'c1', name: 'complete', input: { status: 'success' } },
      { type: 'done' },
    ]);
    expect(toolsUsed).toEqual(['Read']);            // control tool excluded
    expect(res.pendingToolCalls).toHaveLength(2);    // but both are returned for the loop
    expect(watchdogs.marked).toEqual(['Read', 'complete']); // watchdog sees every tool
  });

  it('deduplicates repeated regular tool names', async () => {
    const { toolsUsed } = await run([
      { type: 'tool_use', id: 'r1', name: 'Read', input: {} },
      { type: 'tool_use', id: 'r2', name: 'Read', input: {} },
      { type: 'done' },
    ]);
    expect(toolsUsed).toEqual(['Read']);
  });

  it('forwards onToolUse only for regular tools and fires onLLMCall + onTextPreview', async () => {
    const onToolUse = vi.fn();
    const onLLMCall = vi.fn();
    const onTextPreview = vi.fn();
    await run([
      { type: 'text', text: 'prose' },
      { type: 'tool_use', id: 'r1', name: 'Read', input: {} },
      { type: 'tool_use', id: 'c1', name: 'complete', input: { status: 'success' } },
      { type: 'done' },
    ], { callbacks: { onToolUse, onLLMCall, onTextPreview } });
    expect(onToolUse).toHaveBeenCalledTimes(1);
    expect(onToolUse).toHaveBeenCalledWith('Read', {}, expect.anything());
    expect(onTextPreview).toHaveBeenCalledWith('m1', 'prose');
    expect(onLLMCall).toHaveBeenCalledTimes(1);
    const info = onLLMCall.mock.calls[0][0];
    expect(info.toolCalls).toBe(2);
    expect(info.textChars).toBe('prose'.length);
    expect(info.hadError).toBe(false);
  });

  it('emits llm_call_start and llm_call_end trace events', async () => {
    const { logger } = await run([{ type: 'text', text: 'x' }, { type: 'done' }]);
    expect(logger.kinds).toContain('llm_call_start');
    expect(logger.kinds).toContain('llm_call_end');
  });

  it('does not fire onTextPreview when there is no text', async () => {
    const onTextPreview = vi.fn();
    await run([
      { type: 'tool_use', id: 'r1', name: 'Read', input: {} },
      { type: 'done' },
    ], { callbacks: { onTextPreview } });
    expect(onTextPreview).not.toHaveBeenCalled();
  });
});
