/**
 * kbdb_* 資料層工具：**用登入進來的那個人的身分查詢**（2026-08-12）。
 *
 * leo：「人類進 Portal 輸入帳密表示你是主人，可以查到你權限所有東西；AI 透過輸入帳密的
 *   MCP 查詢表示是授權的 AI，可以查到主人允許查的任何東西。」
 *   「掛上 MCP 並輸入帳密，那個動作本身就是授權」⇒ 下游不得再要求第二次認證。
 *
 * 本檔守三件事：
 *   ① 以帳密連線時，查詢**帶登入者的 portal session** 打 cypher `/portal/data/*`
 *      ——不再拿 KBDB 的服務內部金鑰直打 KBDB（那條路繞過所有庫過濾）。
 *   ② 呼叫端自帶的 owner_id **一律不生效**（範圍由帳號權限決定，不由呼叫端指定）。
 *   ③ 舊 token（沒有身分）**fail-closed**：誠實要求重新連線，不偷偷退回服務金鑰那條老路。
 *   ④ 服務級憑據（static token / partner key）維持既有 KBDB 直連（零回歸）。
 */
import { describe, it, expect } from "vitest";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { Env } from "../../../src/types.js";
import { registerAllKbdbDataTools } from "../../../src/tools/kbdb_data.js";
import type { KnowledgeIdentity } from "../../../src/lib/portal-client.js";

type ToolHandler = (args: Record<string, unknown>) => Promise<{
  content: { type: string; text: string }[];
  isError?: boolean;
}>;

function makeServer() {
  const tools = new Map<string, { description: string; handler: ToolHandler }>();
  const server = {
    tool(name: string, description: string, _schema: unknown, handler: ToolHandler) {
      tools.set(name, { description, handler });
    },
  };
  return { server: server as unknown as McpServer, tools };
}

/** 兩個 binding 都掛上，才驗得出「該走哪一條」——走錯的那條會被記錄下來。 */
function makeEnv(respond: (which: "cypher" | "kbdb", url: URL, init?: RequestInit) => Response) {
  const cypherCalls: { url: URL; init?: RequestInit }[] = [];
  const kbdbCalls: { url: URL; init?: RequestInit }[] = [];
  const env = {
    CYPHER_EXECUTOR: {
      fetch: async (input: string, init?: RequestInit) => {
        const url = new URL(input);
        cypherCalls.push({ url, init });
        return respond("cypher", url, init);
      },
    },
    KBDB: {
      fetch: async (input: string, init?: RequestInit) => {
        const url = new URL(input);
        kbdbCalls.push({ url, init });
        return respond("kbdb", url, init);
      },
    },
    KBDB_INTERNAL_TOKEN: "service-key-should-not-be-used-on-portal-path",
  } as unknown as Env;
  return { env, cypherCalls, kbdbCalls };
}

function parseResult(r: { content: { text: string }[] }) {
  return JSON.parse(r.content[0].text) as Record<string, unknown>;
}

const PORTAL: KnowledgeIdentity = {
  kind: "portal",
  portal: { session: "sess-abc", display_name: "Leo", role: "admin", libraries: ["kb"] },
};
const SERVICE: KnowledgeIdentity = { kind: "service" };
const STALE: KnowledgeIdentity = { kind: "stale" };

function tools(identity: KnowledgeIdentity, respond: Parameters<typeof makeEnv>[0]) {
  const { server, tools } = makeServer();
  const e = makeEnv(respond);
  registerAllKbdbDataTools(server, e.env, identity);
  return { tools, ...e };
}

const OK = () => new Response(JSON.stringify({ success: true, entries: [], records: [], count: 0 }));

describe("kbdb_* 以登入者身分查詢（portal 資料面）", () => {
  const cases: Array<{ tool: string; args: Record<string, unknown>; path: string; method?: string }> = [
    { tool: "kbdb_search", args: { q: "火星座標" }, path: "/portal/data/search" },
    { tool: "kbdb_query", args: { template: "triplet" }, path: "/portal/data/records/by-template/triplet" },
    { tool: "kbdb_get_record", args: { record_id: "rec_1" }, path: "/portal/data/records/rec_1" },
    { tool: "kbdb_list_templates", args: {}, path: "/portal/data/templates" },
    { tool: "kbdb_create_template", args: { name: "contact", slots: ["name"] }, path: "/portal/data/templates", method: "POST" },
    { tool: "kbdb_create_record", args: { template: "contact", values: { name: "Leo" } }, path: "/portal/data/records", method: "POST" },
  ];

  for (const c of cases) {
    it(`${c.tool} → 打 ${c.path}，帶登入者 session，完全不碰 KBDB 服務金鑰`, async () => {
      const { tools: t, cypherCalls, kbdbCalls } = tools(PORTAL, OK);
      const res = await t.get(c.tool)!.handler(c.args);
      expect(res.isError).toBeUndefined();

      // 走的是 cypher 的 portal 資料面，不是 KBDB 直連
      expect(kbdbCalls, `${c.tool} 不該直打 KBDB`).toHaveLength(0);
      expect(cypherCalls).toHaveLength(1);
      expect(cypherCalls[0].url.pathname).toBe(c.path);
      expect(cypherCalls[0].init?.method ?? "GET").toBe(c.method ?? "GET");

      // 帶的是「那個人的 session」，不是任何服務金鑰
      const auth = new Headers(cypherCalls[0].init!.headers as HeadersInit).get("Authorization");
      expect(auth).toBe("Bearer sess-abc");
      expect(auth).not.toContain("service-key");
    });
  }

  it("呼叫端自帶 owner_id 一律不生效（不讓呼叫端自己挑租戶／歸屬）", async () => {
    const { tools: t, cypherCalls } = tools(PORTAL, OK);
    await t.get("kbdb_search")!.handler({ q: "x", owner_id: "someone-else" });
    await t.get("kbdb_query")!.handler({ template: "triplet", owner_id: "someone-else" });
    for (const call of cypherCalls) {
      expect(call.url.searchParams.get("owner_id")).toBeNull();
    }
  });

  it("寫入時 owner_id 不從呼叫端 body 走（server 定死成登入者的歸屬）", async () => {
    const { tools: t, cypherCalls } = tools(PORTAL, OK);
    await t.get("kbdb_create_record")!.handler({
      template: "contact",
      values: { name: "Leo" },
      owner_id: "someone-else",
    });
    const body = JSON.parse(String(cypherCalls[0].init!.body)) as Record<string, unknown>;
    expect(body).not.toHaveProperty("owner_id");
  });

  it("越庫寫入被擋（403）→ 誠實講是權限問題", async () => {
    const { tools: t } = tools(PORTAL, () =>
      new Response(JSON.stringify({ error: '無「secret」庫的權限，不能寫入該庫' }), { status: 403 }),
    );
    const res = await t.get("kbdb_create_record")!.handler({
      template: "note",
      values: { library: "secret", body: "x" },
    });
    expect(res.isError).toBe(true);
    expect(parseResult(res).error_code).toBe("forbidden");
  });

  it("查不是自己的 record（404）→ 與「不存在」同一句話（不洩存在性）", async () => {
    const { tools: t } = tools(PORTAL, () =>
      new Response(JSON.stringify({ error: "找不到這筆資料" }), { status: 404 }),
    );
    const res = await t.get("kbdb_get_record")!.handler({ record_id: "rec_someone_else" });
    expect(res.isError).toBe(true);
    expect(parseResult(res).error_code).toBe("not_found");
    expect(String(parseResult(res).human_message)).toContain("不在你的權限範圍內");
  });

  it("session 過期（401）→ session_expired，不謊稱資料是空的", async () => {
    const { tools: t } = tools(PORTAL, () =>
      new Response(JSON.stringify({ error: "session 無效或已過期" }), { status: 401 }),
    );
    const res = await t.get("kbdb_search")!.handler({ q: "x" });
    expect(res.isError).toBe(true);
    expect(parseResult(res).error_code).toBe("session_expired");
  });
});

describe("fail-closed：舊 token 沒有身分就查不到東西（不退回服務金鑰）", () => {
  for (const name of [
    "kbdb_search",
    "kbdb_query",
    "kbdb_get_record",
    "kbdb_list_templates",
    "kbdb_create_template",
    "kbdb_create_record",
  ]) {
    it(`${name} → identity_missing，且一個查詢都不發`, async () => {
      const { tools: t, cypherCalls, kbdbCalls } = tools(STALE, OK);
      const res = await t.get(name)!.handler({
        q: "x",
        template: "t",
        record_id: "r",
        name: "n",
        slots: ["a"],
        values: { a: "b" },
      });
      expect(res.isError).toBe(true);
      expect(parseResult(res).error_code).toBe("identity_missing");
      expect(cypherCalls).toHaveLength(0);
      expect(kbdbCalls).toHaveLength(0);
    });
  }
});

describe("回歸：服務級憑據維持既有 KBDB 直連", () => {
  it("kbdb_search 仍直打 KBDB /entries/search，且照舊吃 owner_id", async () => {
    const { tools: t, cypherCalls, kbdbCalls } = tools(SERVICE, OK);
    const res = await t.get("kbdb_search")!.handler({ q: "x", owner_id: "leo" });
    expect(res.isError).toBeUndefined();
    expect(cypherCalls).toHaveLength(0);
    expect(kbdbCalls).toHaveLength(1);
    expect(kbdbCalls[0].url.pathname).toBe("/entries/search");
    expect(kbdbCalls[0].url.searchParams.get("owner_id")).toBe("leo");
  });

  it("kbdb_query / kbdb_get_record 路徑不變", async () => {
    const { tools: t, kbdbCalls } = tools(SERVICE, OK);
    await t.get("kbdb_query")!.handler({ template: "triplet" });
    await t.get("kbdb_get_record")!.handler({ record_id: "rec_1" });
    expect(kbdbCalls.map((c) => c.url.pathname)).toEqual([
      "/records/by-template/triplet",
      "/records/rec_1",
    ]);
  });
});
