import { describe, it, expect } from 'vitest';
import { reconstructDelegateRuns } from './delegate-runs.js';
import type { EventBase } from './event-log.js';

function ev(partial: Partial<EventBase> & { kind: string }): EventBase {
  return {
    v: 1, ts: partial.ts ?? '2026-06-25T00:00:00.000Z', seq: partial.seq ?? 0,
    eventId: partial.eventId ?? `e${partial.seq ?? 0}`, runId: 'job-run',
    kind: partial.kind, payload: partial.payload ?? {},
    correlationId: partial.correlationId, movement: partial.movement,
  } as EventBase;
}

describe('reconstructDelegateRuns', () => {
  it('正常 run: start/complete をペアリングし内部イベントを束ねる', () => {
    const events = [
      ev({ seq: 1, kind: 'delegate_start', payload: { delegateRunId: 'R1', parentRunId: null, description: 'tweet-1 の深掘り', depth: 1 }, ts: '2026-06-25T00:00:01.000Z' }),
      ev({ seq: 2, kind: 'tool_call', correlationId: 'R1', payload: { tool: 'WebFetch' } }),
      ev({ seq: 3, kind: 'tool_result', correlationId: 'R1', payload: { tool: 'WebFetch' } }),
      ev({ seq: 4, kind: 'delegate_complete', payload: { delegateRunId: 'R1', parentRunId: null, description: 'tweet-1 の深掘り', depth: 1, next: 'COMPLETE', status: 'success' }, ts: '2026-06-25T00:00:09.000Z' }),
    ];
    const runs = reconstructDelegateRuns(events);
    expect(runs).toHaveLength(1);
    expect(runs[0]).toMatchObject({
      delegateRunId: 'R1', parentRunId: null, description: 'tweet-1 の深掘り',
      depth: 1, status: 'success', startTs: '2026-06-25T00:00:01.000Z',
      endTs: '2026-06-25T00:00:09.000Z', eventCount: 2, toolCalls: 1,
    });
  });

  it('complete 欠落 → status running', () => {
    const events = [ev({ seq: 1, kind: 'delegate_start', payload: { delegateRunId: 'R2', parentRunId: null, description: 'x', depth: 1 } })];
    const runs = reconstructDelegateRuns(events);
    expect(runs[0].status).toBe('running');
    expect(runs[0].endTs).toBeNull();
  });

  it('同説明の2 run を delegateRunId で分離（誤併合しない）', () => {
    const events = [
      ev({ seq: 1, kind: 'delegate_start', payload: { delegateRunId: 'A', parentRunId: null, description: '同じ説明', depth: 1 } }),
      ev({ seq: 2, kind: 'delegate_complete', payload: { delegateRunId: 'A', parentRunId: null, description: '同じ説明', depth: 1, next: 'COMPLETE', status: 'success' } }),
      ev({ seq: 3, kind: 'delegate_start', payload: { delegateRunId: 'B', parentRunId: null, description: '同じ説明', depth: 1 } }),
      ev({ seq: 4, kind: 'delegate_complete', payload: { delegateRunId: 'B', parentRunId: null, description: '同じ説明', depth: 1, next: 'ABORT', status: 'aborted' } }),
    ];
    const runs = reconstructDelegateRuns(events);
    expect(runs.map((r) => r.delegateRunId).sort()).toEqual(['A', 'B']);
    expect(runs.find((r) => r.delegateRunId === 'B')!.status).toBe('aborted');
  });

  it('depth-2 ネスト: parentRunId が親 run を指す', () => {
    const events = [
      ev({ seq: 1, kind: 'delegate_start', payload: { delegateRunId: 'P', parentRunId: null, description: 'outer', depth: 1 } }),
      ev({ seq: 2, kind: 'delegate_start', correlationId: 'P', payload: { delegateRunId: 'C', parentRunId: 'P', description: 'inner', depth: 2 } }),
    ];
    const runs = reconstructDelegateRuns(events);
    const child = runs.find((r) => r.delegateRunId === 'C')!;
    expect(child.parentRunId).toBe('P');
    expect(child.depth).toBe(2);
  });

  it('delegate イベントが無ければ空配列', () => {
    expect(reconstructDelegateRuns([ev({ seq: 1, kind: 'tool_call', payload: {} })])).toEqual([]);
  });

  it('resultPreview がある場合に復元される', () => {
    const events = [
      ev({ seq: 1, kind: 'delegate_start', payload: { delegateRunId: 'R3', parentRunId: null, description: 'test', depth: 1 } }),
      ev({ seq: 2, kind: 'delegate_complete', payload: { delegateRunId: 'R3', parentRunId: null, description: 'test', depth: 1, next: 'COMPLETE', status: 'success', resultPreview: 'Success result preview' } }),
    ];
    const runs = reconstructDelegateRuns(events);
    expect(runs[0].resultPreview).toBe('Success result preview');
  });

  it('resultPreview がない場合に null になる', () => {
    const events = [
      ev({ seq: 1, kind: 'delegate_start', payload: { delegateRunId: 'R4', parentRunId: null, description: 'test', depth: 1 } }),
      ev({ seq: 2, kind: 'delegate_complete', payload: { delegateRunId: 'R4', parentRunId: null, description: 'test', depth: 1, next: 'COMPLETE', status: 'success' } }),
    ];
    const runs = reconstructDelegateRuns(events);
    expect(runs[0].resultPreview).toBeNull();
  });

  it('llm_call_end イベントをトークン集計：prompt 100 + completion 50 = 150', () => {
    const events = [
      ev({ seq: 1, kind: 'delegate_start', payload: { delegateRunId: 'R5', parentRunId: null, description: 'test', depth: 1 } }),
      ev({ seq: 2, kind: 'llm_call_end', correlationId: 'R5', payload: { promptTokens: 100, completionTokens: 50 } }),
      ev({ seq: 3, kind: 'delegate_complete', payload: { delegateRunId: 'R5', parentRunId: null, description: 'test', depth: 1, next: 'COMPLETE', status: 'success' } }),
    ];
    const runs = reconstructDelegateRuns(events);
    expect(runs[0].totalTokens).toBe(150);
  });

  it('複数 llm_call_end を累積：100+50 + 200+100 = 450', () => {
    const events = [
      ev({ seq: 1, kind: 'delegate_start', payload: { delegateRunId: 'R6', parentRunId: null, description: 'test', depth: 1 } }),
      ev({ seq: 2, kind: 'llm_call_end', correlationId: 'R6', payload: { promptTokens: 100, completionTokens: 50 } }),
      ev({ seq: 3, kind: 'llm_call_end', correlationId: 'R6', payload: { promptTokens: 200, completionTokens: 100 } }),
      ev({ seq: 4, kind: 'delegate_complete', payload: { delegateRunId: 'R6', parentRunId: null, description: 'test', depth: 1, next: 'COMPLETE', status: 'success' } }),
    ];
    const runs = reconstructDelegateRuns(events);
    expect(runs[0].totalTokens).toBe(450);
  });

  it('llm_call_end がトークン欠落のときは 0 として扱う', () => {
    const events = [
      ev({ seq: 1, kind: 'delegate_start', payload: { delegateRunId: 'R7', parentRunId: null, description: 'test', depth: 1 } }),
      ev({ seq: 2, kind: 'llm_call_end', correlationId: 'R7', payload: { /* no tokens */ } }),
      ev({ seq: 3, kind: 'delegate_complete', payload: { delegateRunId: 'R7', parentRunId: null, description: 'test', depth: 1, next: 'COMPLETE', status: 'success' } }),
    ];
    const runs = reconstructDelegateRuns(events);
    expect(runs[0].totalTokens).toBe(0);
  });

  it('llm_call_end がなければ totalTokens = 0', () => {
    const events = [
      ev({ seq: 1, kind: 'delegate_start', payload: { delegateRunId: 'R8', parentRunId: null, description: 'test', depth: 1 } }),
      ev({ seq: 2, kind: 'tool_call', correlationId: 'R8', payload: { tool: 'WebFetch' } }),
      ev({ seq: 3, kind: 'delegate_complete', payload: { delegateRunId: 'R8', parentRunId: null, description: 'test', depth: 1, next: 'COMPLETE', status: 'success' } }),
    ];
    const runs = reconstructDelegateRuns(events);
    expect(runs[0].totalTokens).toBe(0);
  });

  it('toolSummary: Write ×2（同一 file_path）+ WebFetch ×1 → [Write:2, WebFetch:1]（初出順）', () => {
    const events = [
      ev({ seq: 1, kind: 'delegate_start', payload: { delegateRunId: 'R9', parentRunId: null, description: 'test', depth: 1 } }),
      ev({ seq: 2, kind: 'tool_call', correlationId: 'R9', payload: { tool: 'Write', args: { file_path: 'output.txt' } } }),
      ev({ seq: 3, kind: 'tool_call', correlationId: 'R9', payload: { tool: 'WebFetch' } }),
      ev({ seq: 4, kind: 'tool_call', correlationId: 'R9', payload: { tool: 'Write', args: { file_path: 'output.txt' } } }),
      ev({ seq: 5, kind: 'delegate_complete', payload: { delegateRunId: 'R9', parentRunId: null, description: 'test', depth: 1, next: 'COMPLETE', status: 'success' } }),
    ];
    const runs = reconstructDelegateRuns(events);
    expect(runs[0].toolSummary).toEqual([
      { tool: 'Write', count: 2 },
      { tool: 'WebFetch', count: 1 },
    ]);
  });

  it('filesChanged: Write と Edit の file_path を重複除去して記録', () => {
    const events = [
      ev({ seq: 1, kind: 'delegate_start', payload: { delegateRunId: 'R10', parentRunId: null, description: 'test', depth: 1 } }),
      ev({ seq: 2, kind: 'tool_call', correlationId: 'R10', payload: { tool: 'Write', args: { file_path: 'file1.txt' } } }),
      ev({ seq: 3, kind: 'tool_call', correlationId: 'R10', payload: { tool: 'Edit', args: { file_path: 'file2.txt' } } }),
      ev({ seq: 4, kind: 'tool_call', correlationId: 'R10', payload: { tool: 'Write', args: { file_path: 'file1.txt' } } }),
      ev({ seq: 5, kind: 'delegate_complete', payload: { delegateRunId: 'R10', parentRunId: null, description: 'test', depth: 1, next: 'COMPLETE', status: 'success' } }),
    ];
    const runs = reconstructDelegateRuns(events);
    expect(runs[0].filesChanged).toEqual(['file1.txt', 'file2.txt']);
  });

  it('filesChanged: Read など他ツールの file_path は入らない', () => {
    const events = [
      ev({ seq: 1, kind: 'delegate_start', payload: { delegateRunId: 'R11', parentRunId: null, description: 'test', depth: 1 } }),
      ev({ seq: 2, kind: 'tool_call', correlationId: 'R11', payload: { tool: 'Read', args: { file_path: 'input.txt' } } }),
      ev({ seq: 3, kind: 'tool_call', correlationId: 'R11', payload: { tool: 'Write', args: { file_path: 'output.txt' } } }),
      ev({ seq: 4, kind: 'delegate_complete', payload: { delegateRunId: 'R11', parentRunId: null, description: 'test', depth: 1, next: 'COMPLETE', status: 'success' } }),
    ];
    const runs = reconstructDelegateRuns(events);
    expect(runs[0].filesChanged).toEqual(['output.txt']);
  });

  it('tool_call がない run では toolSummary と filesChanged が空配列', () => {
    const events = [
      ev({ seq: 1, kind: 'delegate_start', payload: { delegateRunId: 'R12', parentRunId: null, description: 'test', depth: 1 } }),
      ev({ seq: 2, kind: 'llm_call_end', correlationId: 'R12', payload: { promptTokens: 100, completionTokens: 50 } }),
      ev({ seq: 3, kind: 'delegate_complete', payload: { delegateRunId: 'R12', parentRunId: null, description: 'test', depth: 1, next: 'COMPLETE', status: 'success' } }),
    ];
    const runs = reconstructDelegateRuns(events);
    expect(runs[0].toolSummary).toEqual([]);
    expect(runs[0].filesChanged).toEqual([]);
  });

  it('tool_call に args がなくてもクラッシュしない', () => {
    const events = [
      ev({ seq: 1, kind: 'delegate_start', payload: { delegateRunId: 'R13', parentRunId: null, description: 'test', depth: 1 } }),
      ev({ seq: 2, kind: 'tool_call', correlationId: 'R13', payload: { tool: 'WebFetch' } }),
      ev({ seq: 3, kind: 'tool_call', correlationId: 'R13', payload: { tool: 'Write' } }),
      ev({ seq: 4, kind: 'delegate_complete', payload: { delegateRunId: 'R13', parentRunId: null, description: 'test', depth: 1, next: 'COMPLETE', status: 'success' } }),
    ];
    const runs = reconstructDelegateRuns(events);
    expect(runs[0].toolSummary).toEqual([
      { tool: 'WebFetch', count: 1 },
      { tool: 'Write', count: 1 },
    ]);
    expect(runs[0].filesChanged).toEqual([]);
  });
});
