/**
 * server instructions（`initialize` 時送出的那份，AI **沒辦法自己再要一次**）。
 *
 * 這份測試守的是 2026-08-13 leo 那句話：
 *   「它應該是**你的知識來源**⋯⋯**沒有它你是瞎的**。」
 *
 * 病灶（實測）：instructions 裡唯一提到「這裡有知識可查」的，是**選配的**【藏書地圖】段
 * （1500ms 逾時、失敗回 null、無聲消失）。一條 portal 連線就沒收到它 ⇒ 那個 session
 * 為了回答「Arcrun 是什麼」去讀了 682 行原始碼，而 `kbdb_search` 當下回得出 33 筆真答案。
 *
 * 所以本檔的驗收判準只有一句：
 *   **不管地圖抓不抓得到、身分是哪一種，讀完這份 instructions 的下一個動作都必須是「去查知識庫」。**
 */
import { describe, it, expect, beforeEach } from "vitest";
import { buildServerInstructions } from "../../src/mcp-handler.js";
import { __resetLibraryMapInstructionsCacheForTests } from "../../src/lib/library-map.js";
import type { Env } from "../../src/types.js";
import type { KnowledgeIdentity } from "../../src/lib/portal-client.js";

const SERVICE: KnowledgeIdentity = { kind: "service" };
const STALE: KnowledgeIdentity = { kind: "stale" };

/** 假 KBDB service binding（服務級憑據路徑）。 */
function makeEnv(respond: () => Response | Promise<Response>): Env {
  return { KBDB: { fetch: async () => respond() } } as unknown as Env;
}

const MAP_OK = () =>
  new Response(
    JSON.stringify({
      success: true,
      libraries: [
        { library: "kb", narrative: "leo 的知識庫主庫", top_entities: ["Arcrun"], triplet_count: 1854 },
      ],
      count: 1,
    }),
  );

/**
 * 「一個什麼都不知道的 AI 讀完，下一個動作會是什麼」的機械化判準。
 * 三份 instructions 都必須通過同一組斷言——通不過就是還沒修好。
 */
function expectPointsAtKnowledge(text: string) {
  // ① 明講這條連線後面有主人的知識庫
  expect(text).toContain("知識庫");
  // ② 指名工具與呼叫法（AI 不必猜工具名）
  expect(text).toContain("kbdb_search");
  expect(text).toContain("Arcrun 是什麼");
  // ③ 明確排除三條錯路
  expect(text).toContain("不是 grep 原始碼");
  expect(text).toContain("不是上網搜");
  expect(text).toContain("不是回答「我不知道」");
  // ④ 「沒查到」與「沒有」不可以混為一談
  expect(text).toContain("那是**讀不到**，不是**不存在**");
}

describe("buildServerInstructions — 三種情境都必須指向知識庫", () => {
  beforeEach(() => __resetLibraryMapInstructionsCacheForTests());

  it("① 地圖抓得到：地圖照舊注入，且知識段仍在", async () => {
    const text = await buildServerInstructions(makeEnv(MAP_OK), SERVICE);
    expectPointsAtKnowledge(text);
    expect(text).toContain("【藏書地圖】");
    expect(text).toContain("kb：leo 的知識庫主庫");
    // 地圖成功時不該同時出現「沒取到」的話
    expect(text).not.toContain("【藏書地圖：這次沒取到】");
  });

  it("② 地圖抓不到（HTTP 500）：明講「沒取到」，不靜默、也不等於沒有知識", async () => {
    const text = await buildServerInstructions(makeEnv(() => new Response("boom", { status: 500 })), SERVICE);
    expectPointsAtKnowledge(text);
    expect(text).toContain("【藏書地圖：這次沒取到】");
    expect(text).toContain("這是「地圖沒拿到」，不是「這裡沒有知識」");
  });

  it("② 地圖逾時／binding 爆炸：同樣明講，不擋連線（不 throw）", async () => {
    const env = {
      KBDB: {
        fetch: async () => {
          throw new Error("kbdb down");
        },
      },
    } as unknown as Env;
    const text = await buildServerInstructions(env, SERVICE);
    expectPointsAtKnowledge(text);
    expect(text).toContain("【藏書地圖：這次沒取到】");
  });

  it("② 地圖回空清單：也算沒取到，不可以長得像「這裡沒有知識」", async () => {
    const text = await buildServerInstructions(
      makeEnv(() => new Response(JSON.stringify({ success: true, libraries: [], count: 0 }))),
      SERVICE,
    );
    expect(text).toContain("【藏書地圖：這次沒取到】");
    expect(text).toContain("不是「這裡沒有知識」");
  });

  it("③ 舊 token（stale）：講成身分問題＋給可執行的修法，仍指向知識庫", async () => {
    const text = await buildServerInstructions(makeEnv(MAP_OK), STALE);
    expectPointsAtKnowledge(text);
    expect(text).toContain("舊版簽發的 token");
    expect(text).toContain("identity_missing");
    expect(text).toContain("重新連線");
    // 🔴 不可以講成「知識庫是空的」
    expect(text).toContain("這不代表知識庫是空的");
  });

  it("stale 不去打 KBDB（地圖本身就是情報，舊 token 不給）", async () => {
    let called = 0;
    const env = {
      KBDB: {
        fetch: async () => {
          called += 1;
          return MAP_OK();
        },
      },
    } as unknown as Env;
    await buildServerInstructions(env, STALE);
    expect(called).toBe(0);
  });

  it("Arcrun 指路段照舊存在（知識段是新增的，不是取代）", async () => {
    const text = await buildServerInstructions(makeEnv(MAP_OK), SERVICE);
    expect(text).toContain("# Arcrun — 你已經配備了這套工具，別上網找");
    expect(text).toContain("【先讀這裡】");
    // 步驟 4 不再指名一個可能沒被 seed 的 slug（leo21c 實測連 INDEX 都沒有）
    expect(text).toContain("arcrun_list_skills()");
    expect(text).not.toContain("arcrun_get_skill('INDEX')");
  });
});
