// 單元測試：execution-evaluator — 從 trace 導出每顆零件成敗 + 回寫 registry
// SDD: system-dev/docs/3-specs/arcrun-core-mvp/design.md「執行統計設計」

import { describe, it, expect, vi, afterEach } from 'vitest';
import { componentVerdictsFromTrace, recordComponentStats } from '../src/actions/execution-evaluator';
import type { GraphNode, TraceStep } from '../src/types';

const NODES: GraphNode[] = [
  { id: 'input', type: 'Input' },
  { id: 'fetch', type: 'Component', componentId: 'http_request' },
  { id: 'transform', type: 'Component', componentId: 'code' },
  { id: 'output', type: 'Output' },
];

function step(nodeId: string, over: Partial<TraceStep> = {}): TraceStep {
  return { nodeId, type: 'Component', input: {}, output: { ok: true }, duration_ms: 10, ...over };
}

describe('componentVerdictsFromTrace', () => {
  it('只算 Component 節點；Input/Output 跳過', () => {
    const verdicts = componentVerdictsFromTrace(NODES, [
      step('input', { type: 'Input' }),
      step('fetch'),
      step('output', { type: 'Output' }),
    ]);
    expect(verdicts).toEqual([{ component_id: 'http_request', success: true, duration_ms: 10 }]);
  });

  it('trace 有 error → 該零件記失敗', () => {
    const verdicts = componentVerdictsFromTrace(NODES, [
      step('fetch', { error: 'boom', output: null }),
    ]);
    expect(verdicts).toEqual([{ component_id: 'http_request', success: false, duration_ms: 10 }]);
  });

  it('output.success === false → 記失敗（makeHttpRunner 對非 2xx 不 throw）', () => {
    const verdicts = componentVerdictsFromTrace(NODES, [
      step('fetch', { output: { success: false, status: 500, error: 'oops' } }),
    ]);
    expect(verdicts[0].success).toBe(false);
  });

  it('FOREACH 同節點多筆 trace → 每次執行各記一次樣本', () => {
    const verdicts = componentVerdictsFromTrace(NODES, [
      step('fetch'),
      step('fetch', { error: 'x', output: null }),
      step('fetch'),
    ]);
    expect(verdicts).toHaveLength(3);
    expect(verdicts.map(v => v.success)).toEqual([true, false, true]);
  });
});

describe('recordComponentStats', () => {
  afterEach(() => vi.unstubAllGlobals());

  it('對每顆零件各發一次 POST /analytics/record（fire-and-forget）', async () => {
    const calls: Array<{ url: string; body: Record<string, unknown> }> = [];
    vi.stubGlobal('fetch', vi.fn(async (url: string, init: RequestInit) => {
      calls.push({ url: String(url), body: JSON.parse(String(init.body)) });
      return new Response('{}', { status: 200 });
    }));

    await recordComponentStats(
      { REGISTRY_BASE_URL: 'http://registry.local' },
      NODES,
      [step('fetch'), step('transform', { error: 'bad', output: null })],
    );

    expect(calls).toHaveLength(2);
    expect(calls[0].url).toBe('http://registry.local/analytics/record');
    expect(calls[0].body).toEqual({ canonical_id: 'http_request', success: true, duration_ms: 10 });
    expect(calls[1].body).toEqual({ canonical_id: 'code', success: false, duration_ms: 10 });
  });

  it('registry 打不到也不 throw（統計失敗不影響執行）', async () => {
    vi.stubGlobal('fetch', vi.fn(async () => { throw new Error('network down'); }));
    await expect(
      recordComponentStats({ REGISTRY_BASE_URL: 'http://registry.local' }, NODES, [step('fetch')]),
    ).resolves.toBeUndefined();
  });

  it('無 REGISTRY_BASE_URL 也無 WORKER_SUBDOMAIN → 靜默略過不打', async () => {
    const fetchSpy = vi.fn();
    vi.stubGlobal('fetch', fetchSpy);
    await recordComponentStats({}, NODES, [step('fetch')]);
    expect(fetchSpy).not.toHaveBeenCalled();
  });

  it('未設 REGISTRY_BASE_URL → 用 wasmWorkerUrl 慣例組 registry URL', async () => {
    const calls: string[] = [];
    vi.stubGlobal('fetch', vi.fn(async (url: string) => {
      calls.push(String(url));
      return new Response('{}', { status: 200 });
    }));
    await recordComponentStats({ WORKER_SUBDOMAIN: 'uncle6-me' }, NODES, [step('fetch')]);
    expect(calls[0]).toBe('https://arcrun-registry.uncle6-me.workers.dev/analytics/record');
  });
});
