/**
 * execution-logger 測試（KV 額度事故修復，2026-08-07）
 *
 * KBDB＝API-as-Wall（leo 2026-06-14）：cypher-executor 端不直連任何 D1，一律 fire-and-forget
 * fetch KBDB `/execution-log/record`。本檔驗的是「cypher 這一側」的職責，測試手法比照姊妹模組
 * execution-evaluator.test.ts（recordComponentStats，同款「fire-and-forget POST 統計」）：
 * `vi.stubGlobal('fetch', ...)` 直接攔截，不用 fetchMock。
 *   1. 送出的 payload 形狀正確（workflow_id/owner_id/verdict/duration_ms/message/target）
 *   2. target 從 trigger context 的 page_name/path 擷取（少記：不整包送 input）
 *   3. 任何錯誤（fetch reject、KBDB 回非 2xx）都不影響呼叫端（永不 throw）
 *
 * 「少記截斷長度」「A2 自我降級」的實際邏輯與驗證在 KBDB 端（kbdb/tests/execution-log.test.ts），
 * 因為決策/儲存都搬到 KBDB 做了，cypher 只是薄殼呼叫端。
 */
import { describe, it, expect, vi, afterEach } from 'vitest';
import { writeExecutionVerdict } from '../src/actions/execution-logger';
import type { Bindings } from '../src/types';

afterEach(() => vi.unstubAllGlobals());

function fakeEnv(): Bindings {
  return {
    KBDB_BASE_URL: 'https://kbdb.test',
    ENVIRONMENT: 'test',
  } as unknown as Bindings;
}

function stubFetchCapture(): { calls: Array<{ url: string; body: Record<string, unknown> }> } {
  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(JSON.stringify({ success: true, written: true, mode: 'log' }), { status: 200 });
  }));
  return { calls };
}

describe('writeExecutionVerdict — 送出正確 payload（少記，不整包 input）', () => {
  it('成功：POST 到 KBDB_BASE_URL/execution-log/record，帶 workflow_id/owner_id/verdict/duration_ms/message', async () => {
    const { calls } = stubFetchCapture();
    await writeExecutionVerdict(
      fakeEnv(), 'wf-1', [], 'success', 123, '執行完成', { page_name: 'a.md' }, 'ak_test',
    );
    expect(calls).toHaveLength(1);
    expect(calls[0].url).toBe('https://kbdb.test/execution-log/record');
    expect(calls[0].body).toEqual({
      workflow_id: 'wf-1',
      owner_id: 'ak_test',
      verdict: 'success',
      duration_ms: 123,
      message: '執行完成',
      target: 'a.md',
    });
  });

  it('target：page_name 優先，沒有時 fallback path；都沒有則為 null', async () => {
    const { calls: calls1 } = stubFetchCapture();
    await writeExecutionVerdict(fakeEnv(), 'wf-2', [], 'failed', 1, 'err', { path: 'docs/x.md' });
    expect(calls1[0].body.target).toBe('docs/x.md');

    vi.unstubAllGlobals();
    const { calls: calls2 } = stubFetchCapture();
    await writeExecutionVerdict(fakeEnv(), 'wf-3', [], 'failed', 1, 'err', undefined);
    expect(calls2[0].body.target).toBeNull();
  });

  it('不整包送 input：巨大的無關欄位不會出現在送出的 payload 裡', async () => {
    const { calls } = stubFetchCapture();
    await writeExecutionVerdict(fakeEnv(), 'wf-4', [], 'failed', 1, 'err', {
      page_name: 'a.md',
      unrelated_huge_field: 'z'.repeat(10000),
    });
    expect(Object.keys(calls[0].body).sort()).toEqual(
      ['duration_ms', 'message', 'owner_id', 'target', 'verdict', 'workflow_id'],
    );
  });

  it('沒有 apiKey（/execute 舊路徑）：owner_id 送 null，不炸', async () => {
    const { calls } = stubFetchCapture();
    await writeExecutionVerdict(fakeEnv(), 'wf-5', [], 'success', 1, 'ok');
    expect(calls[0].body.owner_id).toBeNull();
  });
});

describe('writeExecutionVerdict — 記錄失敗不影響主流程（永不 throw）', () => {
  it('KBDB 端點連不上（fetch reject）：函式仍正常 resolve', async () => {
    vi.stubGlobal('fetch', vi.fn(async () => { throw new Error('network down'); }));
    await expect(
      writeExecutionVerdict(fakeEnv(), 'wf-broken', [], 'failed', 1, '任何訊息'),
    ).resolves.toBeUndefined();
  });

  it('KBDB 回非 2xx（例如額度打滿的 5xx）：函式仍正常 resolve', async () => {
    vi.stubGlobal('fetch', vi.fn(async () =>
      new Response(JSON.stringify({ success: false, error: 'quota exceeded' }), { status: 500 }),
    ));
    await expect(
      writeExecutionVerdict(fakeEnv(), 'wf-broken2', [], 'failed', 1, '任何訊息'),
    ).resolves.toBeUndefined();
  });
});
