import { describe, it, expect } from 'vitest';
import { buildDelegateRunTree, currentRunningTool, delegateStatusBadge, formatElapsed, formatTokens, summarizeDescendants, type DelegateRun } from './delegateRuns';

const run = (id: string, parentRunId: string | null, depth = 1): DelegateRun => ({
  delegateRunId: id, parentRunId, description: id, depth,
  status: 'success', startTs: '', endTs: null, eventCount: 0, toolCalls: 0,
});

describe('buildDelegateRunTree', () => {
  it('root run は children に子をぶら下げ、親子リンクが正しく張られる', () => {
    const tree = buildDelegateRunTree([run('P', null, 1), run('C', 'P', 2)]);
    expect(tree).toHaveLength(1);
    expect(tree[0].delegateRunId).toBe('P');
    expect(tree[0].children).toHaveLength(1);
    expect(tree[0].children[0].delegateRunId).toBe('C');
  });

  it('親が無い run は root にフォールバック（孤児を落とさない）', () => {
    const tree = buildDelegateRunTree([run('C', 'MISSING', 2)]);
    expect(tree.map((r) => r.delegateRunId)).toEqual(['C']);
  });

  it('delegate が無ければ空', () => {
    expect(buildDelegateRunTree([])).toEqual([]);
  });

  it('グループ単位で閉じる — 別ジョブの parentRunId は跨がない（cross-job parentRunId は孤児扱い）', () => {
    // ジョブ A のみのスライスを渡した場合、ジョブ B の run (b) が parentRunId として
    // ジョブ A の run (a) を参照していても、ここでは b は含まれていないため
    // a は root として扱われ、b が子になることはない。
    // 逆に、ジョブ A のスライス [a, b] を渡すと a が root で b が子になる。
    const runs = [
      run('a', null, 1),   // job-A の root run
      run('b', 'a', 2),    // job-A の子 run
    ];
    const tree = buildDelegateRunTree(runs as any);
    expect(tree).toHaveLength(1);          // a が root、b はその子
    expect(tree[0].children).toHaveLength(1);
  });

  it('startTs 逆順の runs を渡すと roots と children が startTs 昇順にソートされる', () => {
    const runs = [
      { ...run('C', 'P', 2), startTs: '2026-06-25T00:00:30Z' },
      { ...run('B', 'P', 2), startTs: '2026-06-25T00:00:20Z' },
      { ...run('P', null, 1), startTs: '2026-06-25T00:00:00Z' },
      { ...run('A', 'P', 2), startTs: '2026-06-25T00:00:10Z' },
    ];
    const tree = buildDelegateRunTree(runs);
    // root が 1 つ（P）
    expect(tree).toHaveLength(1);
    expect(tree[0].delegateRunId).toBe('P');
    // children が startTs 昇順（A, B, C）
    expect(tree[0].children).toHaveLength(3);
    expect(tree[0].children[0].delegateRunId).toBe('A');
    expect(tree[0].children[1].delegateRunId).toBe('B');
    expect(tree[0].children[2].delegateRunId).toBe('C');
  });

  it('startTs が同じ場合は delegateRunId で安定化', () => {
    const runs = [
      { ...run('C', 'P', 2), startTs: '2026-06-25T00:00:00Z' },
      { ...run('B', 'P', 2), startTs: '2026-06-25T00:00:00Z' },
      { ...run('P', null, 1), startTs: '2026-06-25T00:00:00Z' },
      { ...run('A', 'P', 2), startTs: '2026-06-25T00:00:00Z' },
    ];
    const tree = buildDelegateRunTree(runs);
    // children が ID の localeCompare 昇順（A, B, C）
    expect(tree[0].children).toHaveLength(3);
    expect(tree[0].children[0].delegateRunId).toBe('A');
    expect(tree[0].children[1].delegateRunId).toBe('B');
    expect(tree[0].children[2].delegateRunId).toBe('C');
  });
});

describe('delegateStatusBadge', () => {
  it('各 status を i18n キー + 色クラスに対応', () => {
    expect(delegateStatusBadge('success').labelKey).toBe('subtasks.delegateStatus.success');
    expect(delegateStatusBadge('aborted').labelKey).toBe('subtasks.delegateStatus.aborted');
    expect(delegateStatusBadge('needs_user_input').labelKey).toBe('subtasks.delegateStatus.needs_user_input');
    expect(delegateStatusBadge('running').labelKey).toBe('subtasks.delegateStatus.running');
    expect(delegateStatusBadge('success').cls).toContain('emerald');
  });
});

describe('formatElapsed', () => {
  it('ms / s / m を整形、clock skew は 0 にガード', () => {
    const t0 = '2026-06-25T00:00:00.000Z';
    expect(formatElapsed(t0, '2026-06-25T00:00:00.500Z', 0)).toBe('500ms');
    expect(formatElapsed(t0, '2026-06-25T00:00:05.000Z', 0)).toBe('5.0s');
    expect(formatElapsed(t0, '2026-06-25T00:01:05.000Z', 0)).toBe('1m5s');
    // endTs null → now を使用
    expect(formatElapsed(t0, null, new Date('2026-06-25T00:00:02.000Z').getTime())).toBe('2.0s');
    // skew（end < start）→ 0ms
    expect(formatElapsed('2026-06-25T00:00:10.000Z', '2026-06-25T00:00:00.000Z', 0)).toBe('0ms');
  });
});

describe('formatTokens', () => {
  it('999 → "999 tok"（3桁以下は整数）', () => {
    expect(formatTokens(999)).toBe('999 tok');
  });

  it('1000 → "1.0k tok"（k単位、1桁小数）', () => {
    expect(formatTokens(1000)).toBe('1.0k tok');
  });

  it('1234 → "1.2k tok"（丸める）', () => {
    expect(formatTokens(1234)).toBe('1.2k tok');
  });

  it('1_000_000 → "1.0M tok"（M単位）', () => {
    expect(formatTokens(1_000_000)).toBe('1.0M tok');
  });

  it('0 → "0 tok"', () => {
    expect(formatTokens(0)).toBe('0 tok');
  });

  it('999_999 → "1000.0k tok"（k単位上限）', () => {
    expect(formatTokens(999_999)).toBe('1000.0k tok');
  });
});

describe('currentRunningTool', () => {
  it('tool_call のみで pending になり、ツール名を返す', () => {
    const events = [
      { kind: 'tool_call', payload: { tool: 'Read' } },
    ];
    expect(currentRunningTool(events)).toBe('Read');
  });

  it('tool_call + tool_result で完結すると null になる', () => {
    const events = [
      { kind: 'tool_call', payload: { tool: 'Read' } },
      { kind: 'tool_result', payload: { tool: 'Read' } },
    ];
    expect(currentRunningTool(events)).toBeNull();
  });

  it('複数ツールが交互に来て、最後の call が未完なら最後のツール名を返す', () => {
    const events = [
      { kind: 'tool_call', payload: { tool: 'Read' } },
      { kind: 'tool_result', payload: { tool: 'Read' } },
      { kind: 'tool_call', payload: { tool: 'Write' } },
      { kind: 'tool_result', payload: { tool: 'Write' } },
      { kind: 'tool_call', payload: { tool: 'Bash' } },
    ];
    expect(currentRunningTool(events)).toBe('Bash');
  });
});

describe('summarizeDescendants', () => {
  it('子がなければ total=0', () => {
    const tree = buildDelegateRunTree([run('P', null, 1)]);
    const summary = summarizeDescendants(tree[0]);
    expect(summary).toEqual({ total: 0, failed: 0, running: 0 });
  });

  it('子が 1 つで status=success の場合', () => {
    const runs = [
      run('P', null, 1),
      run('C', 'P', 2),
    ];
    const tree = buildDelegateRunTree(runs);
    const summary = summarizeDescendants(tree[0]);
    expect(summary).toEqual({ total: 1, failed: 0, running: 0 });
  });

  it('孫に aborted がある場合、failed にカウント', () => {
    const runs = [
      run('P', null, 1),
      { ...run('C', 'P', 2), status: 'running' as const },
      { ...run('GC', 'C', 3), status: 'aborted' as const },
    ];
    const tree = buildDelegateRunTree(runs);
    const summary = summarizeDescendants(tree[0]);
    expect(summary.total).toBe(2);
    expect(summary.failed).toBe(1);
    expect(summary.running).toBe(1);
  });

  it('複数の running と failed を正しく集計', () => {
    const runs = [
      run('P', null, 1),
      { ...run('C1', 'P', 2), status: 'running' as const },
      { ...run('C2', 'P', 2), status: 'aborted' as const },
      { ...run('C3', 'P', 2), status: 'success' as const },
      { ...run('GC1', 'C1', 3), status: 'running' as const },
      { ...run('GC2', 'C2', 3), status: 'aborted' as const },
    ];
    const tree = buildDelegateRunTree(runs);
    const summary = summarizeDescendants(tree[0]);
    expect(summary.total).toBe(5);
    expect(summary.failed).toBe(2);
    expect(summary.running).toBe(2);
  });
});
