/**
 * recipe payload 與回應處理層 —— CP `arcrun-usable` 步驟 5 缺口②
 * SDD: workflow-discovery task 3.12
 *
 * 為什麼要有這三層（別刪）：
 *   舊 schema 只有 {canonical_id, endpoint, method, auth_service}（body 有但淺）
 *   ⇒ 帶 body 的 API 只能繞過 recipe 把整包寫進 workflow code；
 *     回應解析（rag_chat 的 finalize，2786 字元）綁死 Gemini 格式，換源必壞。
 *   leo：三層模型＝①零件 ②auth recipe ③payload recipe，第③層過去不存在。
 *
 * 本檔測純函式層（body_template 插值 / response_map 正規化），
 * 不打真外部 API——外部呼叫由 stage 端到端驗（features/09）。
 */
import { describe, it, expect } from 'vitest';
import { renderBodyTemplate, applyResponseMap } from '../src/lib/recipe-payload';

describe('body_template：payload 收回 recipe（第③層）', () => {
  it('巢狀結構的 {{var}} 都會被替換（不只 top-level）', () => {
    const out = renderBodyTemplate(
      { contents: [{ parts: [{ text: '{{prompt}}' }] }] },
      { prompt: '你好' },
    );
    expect(out).toEqual({ contents: [{ parts: [{ text: '你好' }] }] });
  });

  it('單一引用保留原型別（陣列/物件不被 stringify）', () => {
    const out = renderBodyTemplate(
      { messages: '{{history}}', n: '{{count}}' },
      { history: [{ role: 'user' }], count: 3 },
    ) as Record<string, unknown>;
    expect(out.messages).toEqual([{ role: 'user' }]);
    expect(out.n).toBe(3);
  });

  it('混合文字仍拼成字串', () => {
    const out = renderBodyTemplate({ q: '請回答：{{prompt}}' }, { prompt: '天氣' }) as Record<string, unknown>;
    expect(out.q).toBe('請回答：天氣');
  });

  it('支援 dot path 取值', () => {
    const out = renderBodyTemplate({ t: '{{assemble.data.prompt}}' }, {
      assemble: { data: { prompt: '深層值' } },
    }) as Record<string, unknown>;
    expect(out.t).toBe('深層值');
  });

  it('取不到的變數保留原樣（不靜默變 undefined，看得見才好 debug）', () => {
    const out = renderBodyTemplate({ t: '{{nope}}' }, {}) as Record<string, unknown>;
    expect(out.t).toBe('{{nope}}');
  });

  it('沒有 body_template → 回 undefined（呼叫端沿用既有行為）', () => {
    expect(renderBodyTemplate(undefined, { a: 1 })).toBeUndefined();
  });
});

describe('response_map：回應正規化（換源不必改 workflow）', () => {
  const geminiBody = {
    candidates: [{ content: { parts: [{ text: '【答】台北是首都' }] } }],
  };

  it('path 取值：Gemini 形狀 → 純文字', () => {
    const out = applyResponseMap(geminiBody, { text_path: 'candidates.0.content.parts.0.text' });
    expect(out.text).toBe('【答】台北是首都');
  });

  it('換源＝換 recipe：Claude 形狀用不同 path，同樣取得出文字', () => {
    const claudeBody = { content: [{ type: 'text', text: 'Claude 的答案' }] };
    const out = applyResponseMap(claudeBody, { text_path: 'content.0.text' });
    expect(out.text).toBe('Claude 的答案');
  });

  it('Workers AI 形狀（binding 回傳）同樣走 path', () => {
    const waiBody = { response: 'Workers AI 的答案' };
    const out = applyResponseMap(waiBody, { text_path: 'response' });
    expect(out.text).toBe('Workers AI 的答案');
  });

  it('思考型模型：thought=true 的 part 要被剔除，取最後一個非 thought', () => {
    const gemma = {
      candidates: [{
        content: {
          parts: [
            { text: '讓我想想…', thought: true },
            { text: '真正的答案' },
          ],
        },
      }],
    };
    const out = applyResponseMap(gemma, {
      text_path: 'candidates.0.content.parts',
      thinking_model: true,
    });
    expect(out.text).toBe('真正的答案');
  });

  it('淨化規則：剝掉【答】前的草稿前綴（實撞三型之一）', () => {
    const out = applyResponseMap(
      { r: 'Draft: 【答】正確內容' },
      { text_path: 'r', strip_prefixes: ['Draft:', '*', 'Answer:'], answer_marker: '【答】' },
    );
    expect(out.text).toBe('正確內容');
  });

  it('淨化規則：前綴組合順序不定 → 循環剝殼剝乾淨', () => {
    const out = applyResponseMap(
      { r: 'Answer: * 【答】內容' },
      { text_path: 'r', strip_prefixes: ['Draft:', '*', 'Answer:'], answer_marker: '【答】' },
    );
    expect(out.text).toBe('內容');
  });

  it('沒有 response_map → 原樣回傳（既有 recipe 行為完全不變）', () => {
    const out = applyResponseMap(geminiBody, undefined);
    expect(out.text).toBeUndefined();
    expect(out.raw).toEqual(geminiBody);
  });

  it('path 取不到 → 誠實回 undefined，不編造', () => {
    const out = applyResponseMap({ a: 1 }, { text_path: 'b.c.d' });
    expect(out.text).toBeUndefined();
  });
});
