import { describe, it, expect } from 'vitest';
import {
  makeStreamJsonParser,
  makeStreamJsonState,
  parseStreamJsonLine,
  type AgentEventList,
} from '../stream-json-parser.js';
import type { AgentEvent } from '../agent-backend.js';
import type { AcpToolSnapshot } from '../acp-tool-snapshot.js';

// Helpers to JSON-encode the representative Claude-Code stream-json lines.
const sys = (sessionId: string) =>
  JSON.stringify({ type: 'system', subtype: 'init', session_id: sessionId, tools: ['read', 'edit'] });

const streamEvent = (event: unknown) => JSON.stringify({ type: 'stream_event', event });

const textDelta = (index: number, text: string) =>
  streamEvent({ type: 'content_block_delta', index, delta: { type: 'text_delta', text } });

const thinkingDelta = (index: number, thinking: string) =>
  streamEvent({ type: 'content_block_delta', index, delta: { type: 'thinking_delta', thinking } });

const toolStart = (index: number, id: string, name: string) =>
  streamEvent({ type: 'content_block_start', index, content_block: { type: 'tool_use', id, name } });

const inputJsonDelta = (index: number, partial: string) =>
  streamEvent({ type: 'content_block_delta', index, delta: { type: 'input_json_delta', partial_json: partial } });

const blockStop = (index: number) => streamEvent({ type: 'content_block_stop', index });

const resultLine = (input: number, output: number, sessionId?: string) =>
  JSON.stringify({ type: 'result', subtype: 'success', session_id: sessionId, usage: { input_tokens: input, output_tokens: output } });

describe('parseStreamJsonLine (pure per-line mapping)', () => {
  it('captures session_id from the system init line and emits no events', () => {
    const state = makeStreamJsonState();
    const events = parseStreamJsonLine(sys('sess-abc'), state);
    expect(events).toEqual([]);
    expect(state.sessionId).toBe('sess-abc');
  });

  it('maps a text_delta stream_event → a text event', () => {
    const state = makeStreamJsonState();
    expect(parseStreamJsonLine(textDelta(0, 'Hello'), state)).toEqual([{ type: 'text', text: 'Hello' }]);
  });

  it('maps a thinking_delta stream_event → a reasoning event', () => {
    const state = makeStreamJsonState();
    expect(parseStreamJsonLine(thinkingDelta(0, 'pondering'), state)).toEqual([
      { type: 'reasoning', text: 'pondering' },
    ]);
  });

  it('tolerates a garbage / non-JSON line (returns [], no throw)', () => {
    const state = makeStreamJsonState();
    expect(parseStreamJsonLine('not json at all {{{', state)).toEqual([]);
    expect(parseStreamJsonLine('', state)).toEqual([]);
    expect(parseStreamJsonLine('   ', state)).toEqual([]);
    // A truncated/partial JSON object also yields [] rather than throwing.
    expect(parseStreamJsonLine('{"type":"stream_event","eve', state)).toEqual([]);
  });

  it('ignores unknown top-level line types and the user (tool-result) line', () => {
    const state = makeStreamJsonState();
    expect(parseStreamJsonLine(JSON.stringify({ type: 'user', message: {} }), state)).toEqual([]);
    expect(parseStreamJsonLine(JSON.stringify({ type: 'whatever' }), state)).toEqual([]);
  });

  it('assembles a tool call across input_json_delta chunks (split across lines)', () => {
    const state = makeStreamJsonState();
    // start → tool_call (running, empty args)
    const start = parseStreamJsonLine(toolStart(1, 'toolu_1', 'edit_file'), state);
    expect(start).toHaveLength(1);
    expect(start[0]!.type).toBe('tool_call');
    const startSnap = (start[0] as { type: 'tool_call'; toolCall: AcpToolSnapshot }).toolCall;
    expect(startSnap.toolCallId).toBe('toolu_1');
    expect(startSnap.title).toBe('edit_file');
    expect(startSnap.status).toBe('in_progress');
    expect(startSnap.rawInput).toEqual({});

    // args streamed in fragments — no events until stop
    expect(parseStreamJsonLine(inputJsonDelta(1, '{"path":"a'), state)).toEqual([]);
    expect(parseStreamJsonLine(inputJsonDelta(1, '.ts","content":'), state)).toEqual([]);
    expect(parseStreamJsonLine(inputJsonDelta(1, '"hi"}'), state)).toEqual([]);

    // stop → tool_update with the parsed, fully-assembled input
    const stop = parseStreamJsonLine(blockStop(1), state);
    expect(stop).toHaveLength(1);
    expect(stop[0]!.type).toBe('tool_update');
    const stopSnap = (stop[0] as { type: 'tool_update'; toolCall: AcpToolSnapshot }).toolCall;
    expect(stopSnap.toolCallId).toBe('toolu_1');
    expect(stopSnap.status).toBe('completed');
    expect(stopSnap.rawInput).toEqual({ path: 'a.ts', content: 'hi' });
  });

  it('falls back to {_raw} when accumulated tool args are not valid JSON', () => {
    const state = makeStreamJsonState();
    parseStreamJsonLine(toolStart(0, 'toolu_x', 'run'), state);
    parseStreamJsonLine(inputJsonDelta(0, '{"broken'), state);
    const stop = parseStreamJsonLine(blockStop(0), state);
    const snap = (stop[0] as { type: 'tool_update'; toolCall: AcpToolSnapshot }).toolCall;
    expect(snap.rawInput).toEqual({ _raw: '{"broken' });
  });

  it('captures usage from message_delta and result lines', () => {
    const state = makeStreamJsonState();
    parseStreamJsonLine(streamEvent({ type: 'message_delta', usage: { output_tokens: 42 } }), state);
    expect(state.usage.outputTokens).toBe(42);
    parseStreamJsonLine(resultLine(100, 250, 'sess-z'), state);
    expect(state.usage.inputTokens).toBe(100);
    expect(state.usage.outputTokens).toBe(250);
    expect(state.sessionId).toBe('sess-z');
  });

  it('maps a terminal assistant message (fallback) → text + reasoning + tool events', () => {
    const state = makeStreamJsonState();
    const line = JSON.stringify({
      type: 'assistant',
      session_id: 'sess-asst',
      message: {
        content: [
          { type: 'thinking', thinking: 'let me think' },
          { type: 'text', text: 'Here is the answer' },
          { type: 'tool_use', id: 'toolu_9', name: 'view_file', input: { path: 'x.ts' } },
        ],
        usage: { input_tokens: 5, output_tokens: 7 },
      },
    });
    const events = parseStreamJsonLine(line, state);
    expect(events).toEqual([
      { type: 'reasoning', text: 'let me think' },
      { type: 'text', text: 'Here is the answer' },
      {
        type: 'tool_update',
        toolCall: { toolCallId: 'toolu_9', title: 'view_file', kind: null, status: 'completed', rawInput: { path: 'x.ts' } },
      },
    ]);
    expect(state.usage).toEqual({ inputTokens: 5, outputTokens: 7 });
    expect(state.sessionId).toBe('sess-asst');
  });
});

describe('makeStreamJsonParser (stateful wrapper over a full turn)', () => {
  it('streams a representative turn: init → text → thinking → tool → result', () => {
    const parser = makeStreamJsonParser();
    const all: AgentEvent[] = [];
    const feed = (line: string): AgentEventList => {
      const evs = parser.push(line);
      all.push(...evs);
      return evs;
    };

    feed(sys('sess-1'));
    feed(textDelta(0, 'Reading '));
    feed(textDelta(0, 'the file. '));
    feed(thinkingDelta(0, 'I should edit it'));
    feed(toolStart(1, 'toolu_a', 'edit_file'));
    feed(inputJsonDelta(1, '{"path":'));
    feed(inputJsonDelta(1, '"main.ts"}'));
    feed(blockStop(1));
    feed(textDelta(0, 'Done.'));
    feed(resultLine(120, 80, 'sess-1'));

    expect(all).toEqual([
      { type: 'text', text: 'Reading ' },
      { type: 'text', text: 'the file. ' },
      { type: 'reasoning', text: 'I should edit it' },
      {
        type: 'tool_call',
        toolCall: { toolCallId: 'toolu_a', title: 'edit_file', kind: null, status: 'in_progress', rawInput: {} },
      },
      {
        type: 'tool_update',
        toolCall: { toolCallId: 'toolu_a', title: 'edit_file', kind: null, status: 'completed', rawInput: { path: 'main.ts' } },
      },
      { type: 'text', text: 'Done.' },
    ]);

    expect(parser.usage()).toEqual({ inputTokens: 120, outputTokens: 80 });
    expect(parser.sessionId()).toBe('sess-1');
  });

  it('a garbage line interleaved mid-turn does not derail subsequent parsing', () => {
    const parser = makeStreamJsonParser();
    expect(parser.push(textDelta(0, 'a'))).toEqual([{ type: 'text', text: 'a' }]);
    expect(parser.push('>>> not json <<<')).toEqual([]);
    expect(parser.push(textDelta(0, 'b'))).toEqual([{ type: 'text', text: 'b' }]);
  });
});
