// Cypher Executor 端到端測試
import { SELF } from 'cloudflare:test';
import { describe, it, expect } from 'vitest';
import { GraphExecutor } from '../src/graph-executor';
import type { ComponentRunner, ExecutionGraph } from '../src/types';

describe('GET /', () => {
  it('回傳服務狀態', async () => {
    const res = await SELF.fetch('http://localhost/');
    const data = await res.json() as Record<string, unknown>;
    expect(res.status).toBe(200);
    expect(data.service).toBe('arcrun-cypher-executor');
  });
});

describe('POST /validate', () => {
  it('驗證合法的圖定義', async () => {
    const res = await SELF.fetch('http://localhost/validate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        id: 'test-graph',
        name: '測試圖',
        nodes: [
          { id: 'n1', type: 'Input' },
          { id: 'n2', type: 'Output' },
        ],
        edges: [
          { from: 'n1', to: 'n2', type: 'PIPE' },
        ],
      }),
    });
    const data = await res.json() as { valid: boolean };
    expect(res.status).toBe(200);
    expect(data.valid).toBe(true);
  });

  it('偵測無效邊', async () => {
    const res = await SELF.fetch('http://localhost/validate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        id: 'bad-graph',
        name: '壞圖',
        nodes: [{ id: 'n1', type: 'Input' }],
        edges: [{ from: 'n1', to: 'n999', type: 'PIPE' }],
      }),
    });
    expect(res.status).toBe(400);
  });
});

describe('POST /execute', () => {
  it('PIPE 鏈: Input → passthrough → Output', async () => {
    const res = await SELF.fetch('http://localhost/execute', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        graph: {
          id: 'g1',
          name: 'PIPE 測試',
          nodes: [
            { id: 'input', type: 'Input', data: { message: 'hello' } },
            { id: 'pass', type: 'Component', componentId: 'comp_passthrough' },
            { id: 'output', type: 'Output' },
          ],
          edges: [
            { from: 'input', to: 'pass', type: 'PIPE' },
            { from: 'pass', to: 'output', type: 'PIPE' },
          ],
        },
        context: {},
      }),
    });
    const data = await res.json() as { success: boolean; data: { message: string }; trace: unknown[] };
    expect(res.status).toBe(200);
    expect(data.success).toBe(true);
    expect(data.data.message).toBe('hello');
    expect(data.trace.length).toBeGreaterThanOrEqual(3);
  });

  it('Component 執行: uppercase 轉換', async () => {
    const res = await SELF.fetch('http://localhost/execute', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        graph: {
          id: 'g2',
          name: 'uppercase 測試',
          nodes: [
            { id: 'input', type: 'Input', data: { text: 'hello world' } },
            { id: 'upper', type: 'Component', componentId: 'comp_uppercase' },
            { id: 'output', type: 'Output' },
          ],
          edges: [
            { from: 'input', to: 'upper', type: 'PIPE' },
            { from: 'upper', to: 'output', type: 'PIPE' },
          ],
        },
        context: {},
      }),
    });
    const data = await res.json() as { success: boolean; data: { text: string } };
    expect(data.success).toBe(true);
    expect(data.data.text).toBe('HELLO WORLD');
  });

  it('PIPE 鏈: 多層 counter 累加', async () => {
    const res = await SELF.fetch('http://localhost/execute', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        graph: {
          id: 'g3',
          name: 'counter 測試',
          nodes: [
            { id: 'input', type: 'Input', data: { count: 0 } },
            { id: 'c1', type: 'Component', componentId: 'comp_counter' },
            { id: 'c2', type: 'Component', componentId: 'comp_counter' },
            { id: 'c3', type: 'Component', componentId: 'comp_counter' },
            { id: 'output', type: 'Output' },
          ],
          edges: [
            { from: 'input', to: 'c1', type: 'PIPE' },
            { from: 'c1', to: 'c2', type: 'PIPE' },
            { from: 'c2', to: 'c3', type: 'PIPE' },
            { from: 'c3', to: 'output', type: 'PIPE' },
          ],
        },
        context: {},
      }),
    });
    const data = await res.json() as { success: boolean; data: { count: number } };
    expect(data.success).toBe(true);
    expect(data.data.count).toBe(3);
  });

  it('IF 條件分支', async () => {
    const res = await SELF.fetch('http://localhost/execute', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        graph: {
          id: 'g4',
          name: 'IF 測試',
          nodes: [
            { id: 'input', type: 'Input', data: { valid: true, text: 'go' } },
            { id: 'upper', type: 'Component', componentId: 'comp_uppercase' },
            { id: 'output', type: 'Output' },
          ],
          edges: [
            { from: 'input', to: 'upper', type: 'IF', condition: 'result.valid === true' },
            { from: 'upper', to: 'output', type: 'PIPE' },
          ],
        },
        context: {},
      }),
    });
    const data = await res.json() as { success: boolean; data: { text: string } };
    expect(data.success).toBe(true);
    expect(data.data.text).toBe('GO');
  });

  it('不存在的零件回傳失敗', async () => {
    const res = await SELF.fetch('http://localhost/execute', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        graph: {
          id: 'g5',
          name: '失敗測試',
          nodes: [
            { id: 'input', type: 'Input', data: {} },
            { id: 'bad', type: 'Component', componentId: 'comp_not_exist' },
          ],
          edges: [
            { from: 'input', to: 'bad', type: 'PIPE' },
          ],
        },
        context: {},
      }),
    });
    const data = await res.json() as { success: boolean; error: string };
    expect(data.success).toBe(false);
    expect(data.error).toContain('不存在');
  });

  it('缺少必填欄位回傳 400', async () => {
    const res = await SELF.fetch('http://localhost/execute', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ graph: { id: 'x' } }),
    });
    expect(res.status).toBe(400);
  });

});

// t117: FOREACH 全部項目失敗 → 錯誤訊息含 status code（GraphExecutor 單元測試）
describe('t117: FOREACH 全項失敗 → ExecutionError 含 status code', () => {
  it('FOREACH 所有項目 success:false（含 status 401）→ executor.execute() 拋出含 "401" 的錯誤', async () => {
    // mock loader：任何零件都回 {success:false, status:401, error:"HTTP 401"}
    const failLoader = async (_: string): Promise<ComponentRunner> =>
      async () => ({ success: false, status: 401, error: 'HTTP 401', data: { body: 'Unauthorized' } });

    const executor = new GraphExecutor(failLoader);

    const graph: ExecutionGraph = {
      id: 'foreach-fail-t117',
      name: 'FOREACH 全失敗',
      nodes: [
        { id: 'input', type: 'Input', data: { items: ['a', 'b'] } },
        { id: 'writer', type: 'Component', componentId: 'http_request' },
      ],
      edges: [
        { from: 'input', to: 'writer', type: 'FOREACH', iterator: 'item' },
      ],
    };

    // t117 核心驗證：全部失敗 → throw（不再靜默）
    await expect(executor.execute(graph, {})).rejects.toThrow(/401/);
  });

  it('FOREACH 部分項目成功 → 不拋出（只有全部失敗才報錯）', async () => {
    let callCount = 0;
    // 第一次呼叫失敗，第二次成功（部分失敗不觸發 t117 all-fail 路徑）
    const mixedLoader = async (_: string): Promise<ComponentRunner> =>
      async () => {
        callCount++;
        if (callCount === 1) return { success: false, status: 401, error: 'HTTP 401' };
        return { success: true, data: { ok: true } };
      };

    const executor = new GraphExecutor(mixedLoader);

    const graph: ExecutionGraph = {
      id: 'foreach-mixed-t117',
      name: 'FOREACH 部分失敗',
      nodes: [
        { id: 'input', type: 'Input', data: { items: ['a', 'b'] } },
        { id: 'writer', type: 'Component', componentId: 'http_request' },
      ],
      edges: [
        { from: 'input', to: 'writer', type: 'FOREACH', iterator: 'item' },
      ],
    };

    // 部分失敗 → 不拋出，正常回傳 results 陣列
    const result = await executor.execute(graph, {});
    expect(result).toBeDefined();
  });
});

// P8 短板齊平（2026-08-09）：節點輸出只在「下游有 PIPE 邊會讀」時才寫 KV。
// 背景：BUILD-006 原本每個節點（含 FOREACH 每一圈）都 put 一次 EXEC_CONTEXT，
// 但全 codebase 唯一讀點是 PIPE 邊的 kvGetNodeOutput——rag 系工作流
// （ON_SUCCESS＋對每個）一張卡白燒 15 次 KV write，把免費層 1,000/日
// 壓成比 Workers AI neurons 更短的板。此測試鎖住「無 PIPE 出邊＝零 KV put」
// 與「有 PIPE 出邊＝照舊寫、_kv_outputs 照舊可讀」兩個行為。
describe('P8：節點輸出 KV 寫入只服務 PIPE 讀者', () => {
  // 計數型 KV mock：只記 put 次數（kvSetNodeOutput 只用到 put；get 給 PIPE 讀）
  function countingKv() {
    const store = new Map<string, string>();
    let puts = 0;
    const kv = {
      put: async (k: string, v: string) => { puts++; store.set(k, v); },
      get: async (k: string) => store.get(k) ?? null,
    } as unknown as KVNamespace;
    return { kv, getPuts: () => puts };
  }

  it('ON_SUCCESS＋FOREACH 工作流（rag_ingest_card 形狀）→ 零 KV put', async () => {
    const loader = async (id: string): Promise<ComponentRunner> => async () => {
      if (id === 'parse') {
        return { success: true, blocks: [{ n: 1 }, { n: 2 }, { n: 3 }], rels: [{ r: 1 }, { r: 2 }] };
      }
      return { success: true, data: { ok: true } };
    };
    const executor = new GraphExecutor(loader);
    const graph: ExecutionGraph = {
      id: 'p8-no-pipe',
      name: 'rag 形狀（無 PIPE 邊）',
      nodes: [
        { id: 'input', type: 'Input', data: {} },
        { id: 'list_old', type: 'Component', componentId: 'http_request' },
        { id: 'parse_card', type: 'Component', componentId: 'parse' },
        { id: 'post_block', type: 'Component', componentId: 'http_request' },
        { id: 'post_triplet', type: 'Component', componentId: 'http_request' },
      ],
      edges: [
        { from: 'input', to: 'list_old', type: 'ON_SUCCESS' },
        { from: 'list_old', to: 'parse_card', type: 'ON_SUCCESS' },
        { from: 'parse_card', to: 'post_block', type: 'FOREACH', iterator: 'block' },
        { from: 'parse_card', to: 'post_triplet', type: 'FOREACH', iterator: 'rel' },
      ],
    };
    const { kv, getPuts } = countingKv();
    const result = await executor.execute(graph, {}, kv);
    expect(result).toBeDefined();
    // 修法前這裡是 8（list_old + parse_card + 3×post_block + 2×post_triplet ＋ input 不寫）
    expect(getPuts()).toBe(0);
  });

  it('PIPE 工作流 → 照舊寫 KV 且 _kv_outputs 傳遞不變（BUILD-006 語意保留）', async () => {
    const seen: Record<string, unknown>[] = [];
    const loader = async (id: string): Promise<ComponentRunner> => async (ctx) => {
      seen.push(ctx as Record<string, unknown>);
      return { success: true, data: { from: id } };
    };
    const executor = new GraphExecutor(loader);
    const graph: ExecutionGraph = {
      id: 'p8-pipe',
      name: 'PIPE 鏈',
      nodes: [
        { id: 'input', type: 'Input', data: { message: 'hi' } },
        { id: 'a', type: 'Component', componentId: 'comp_a' },
        { id: 'b', type: 'Component', componentId: 'comp_b' },
      ],
      edges: [
        { from: 'input', to: 'a', type: 'PIPE' },
        { from: 'a', to: 'b', type: 'PIPE' },
      ],
    };
    const { kv, getPuts } = countingKv();
    const result = await executor.execute(graph, {}, kv);
    expect(result).toBeDefined();
    // a 有 PIPE 出邊 → 寫；b 沒有出邊 → 不寫（原本 a、b 都寫＝2）
    expect(getPuts()).toBe(1);
    // 下游 b 收到的 context 帶 _kv_outputs.a（BUILD-006 讀路徑不變）
    const bCtx = seen[seen.length - 1];
    expect((bCtx._kv_outputs as Record<string, unknown>)?.a).toBeDefined();
  });
});
