import { describe, it, expect } from "vitest";
import { Hono } from "hono";
import {
  randomToken,
  sha256Base64Url,
  sha256Hex,
  constantTimeEqual,
  verifyPkceS256,
} from "../../src/oauth/crypto.js";
import {
  putAuthCode,
  consumeAuthCode,
  putAccessToken,
  getAccessToken,
} from "../../src/oauth/store.js";
import {
  originOf,
  resourceUri,
  normalizeResource,
  resourceMatches,
  protectedResourceMetadata,
  authorizationServerMetadata,
  wwwAuthenticateHeader,
} from "../../src/oauth/metadata.js";
import { esc, consentPage } from "../../src/oauth/consent.js";
import { registerOAuthRoutes } from "../../src/oauth/routes.js";
import type { Env } from "../../src/types.js";

// ── 記憶體 KV mock（支援 expirationTtl → 用 exp 過期；delete）─────────────────────
function makeKV(): KVNamespace {
  const map = new Map<string, { value: string; exp?: number }>();
  const kv = {
    async put(key: string, value: string, opts?: { expirationTtl?: number }) {
      map.set(key, {
        value,
        exp: opts?.expirationTtl ? Date.now() + opts.expirationTtl * 1000 : undefined,
      });
    },
    async get(key: string) {
      const e = map.get(key);
      if (!e) return null;
      if (e.exp && e.exp < Date.now()) {
        map.delete(key);
        return null;
      }
      return e.value;
    },
    async delete(key: string) {
      map.delete(key);
    },
  };
  return kv as unknown as KVNamespace;
}

// PKCE：verifier "1234...43+ chars" → 直接算 challenge。
async function pkcePair() {
  const verifier = "a".repeat(64);
  const challenge = await sha256Base64Url(verifier);
  return { verifier, challenge };
}

/**
 * cypher `/portal/login` 的假替身（2026-08-12 起 MCP 的把關就是這支——用使用者自己的
 * Portal 帳密，沒有另一把 owner secret）。帳密對 → 回 session_token ＋ 身分欄位；不對 → 401。
 */
const GOOD_EMAIL = "leo@example.com";
const GOOD_PASSWORD = "correct horse";

function cypherMock(
  over: {
    /** null ＝ 登入成功但**不回** session_token（舊版 cypher）；預設回 "sess-abc" */
    sessionToken?: string | null;
    displayName?: string;
    role?: string;
    libraries?: string[];
    sessionExpiresIn?: number;
  } = {},
): { fetcher: Fetcher; calls: Array<{ email: string; password: string }> } {
  const calls: Array<{ email: string; password: string }> = [];
  const fetcher = {
    async fetch(req: Request) {
      const body = (await req.json()) as { email: string; password: string };
      calls.push(body);
      if (body.email !== GOOD_EMAIL || body.password !== GOOD_PASSWORD) {
        return new Response(JSON.stringify({ error: "email 或密碼錯誤" }), { status: 401 });
      }
      const sessionToken = over.sessionToken === undefined ? "sess-abc" : over.sessionToken;
      return new Response(
        JSON.stringify({
          success: true,
          ...(sessionToken ? { session_token: sessionToken } : {}),
          display_name: over.displayName ?? "Leo",
          role: over.role ?? "admin",
          libraries: over.libraries ?? ["*"],
          session_expires_in: over.sessionExpiresIn ?? 604800,
        }),
        { status: 200, headers: { "content-type": "application/json" } },
      );
    },
  } as unknown as Fetcher;
  return { fetcher, calls };
}

function baseEnv(over: Partial<Env> = {}): Env {
  return {
    COMPONENT_REGISTRY: {} as Fetcher,
    CYPHER_EXECUTOR: cypherMock().fetcher,
    KBDB: {} as Fetcher,
    KBDB_INTERNAL_TOKEN: "internal",
    OAUTH_KV: makeKV(),
    MCP_OWNER_NAMESPACE: "leo",
    ...over,
  } as Env;
}

// ── crypto ──────────────────────────────────────────────────────────────────────
describe("oauth/crypto", () => {
  it("randomToken 長度足夠且每次不同", () => {
    const a = randomToken();
    const b = randomToken();
    expect(a).not.toBe(b);
    expect(a.length).toBeGreaterThanOrEqual(40);
    expect(a).toMatch(/^[A-Za-z0-9\-_]+$/); // base64url 無 padding
  });

  it("sha256Hex/base64url 為已知值", async () => {
    // echo -n "abc" | sha256sum → ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
    expect(await sha256Hex("abc")).toBe(
      "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
    );
    // RFC 7636 附錄範例：verifier → challenge
    expect(await sha256Base64Url("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk")).toBe(
      "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
    );
  });

  it("constantTimeEqual 正確", () => {
    expect(constantTimeEqual("abc", "abc")).toBe(true);
    expect(constantTimeEqual("abc", "abd")).toBe(false);
    expect(constantTimeEqual("abc", "abcd")).toBe(false);
  });

  it("verifyPkceS256：S256 正確、拒 plain/短 verifier/竄改", async () => {
    const { verifier, challenge } = await pkcePair();
    expect(await verifyPkceS256(verifier, challenge, "S256")).toBe(true);
    expect(await verifyPkceS256(verifier, challenge, "plain")).toBe(false);
    expect(await verifyPkceS256(verifier, challenge, undefined)).toBe(false);
    expect(await verifyPkceS256("short", challenge, "S256")).toBe(false);
    expect(await verifyPkceS256("b".repeat(64), challenge, "S256")).toBe(false);
  });
});

// ── store（短效 KV）────────────────────────────────────────────────────────────
describe("oauth/store", () => {
  it("authorization code 一次性：consume 後即失效（防重放）", async () => {
    const kv = makeKV();
    await putAuthCode(kv, "code-1", {
      client_id: "c1",
      redirect_uri: "https://claude.ai/cb",
      code_challenge: "cc",
      code_challenge_method: "S256",
      scope: "mcp",
      resource: "https://mcp/mcp",
      namespace: "leo",
      portal: { session: "sess-abc", display_name: "Leo", role: "admin", libraries: ["*"] },
      portal_session_expires_in: 604800,
    });
    const first = await consumeAuthCode(kv, "code-1");
    expect(first?.namespace).toBe("leo");
    const second = await consumeAuthCode(kv, "code-1");
    expect(second).toBeNull();
  });

  it("access token 存取 + exp 過期回 null", async () => {
    const kv = makeKV();
    await putAccessToken(
      kv,
      "tok-1",
      { namespace: "leo", client_id: "c1", scope: "mcp", aud: "https://mcp/mcp", exp: Math.floor(Date.now() / 1000) + 100 },
      100,
    );
    expect((await getAccessToken(kv, "tok-1"))?.namespace).toBe("leo");
    // 已過期
    await putAccessToken(
      kv,
      "tok-2",
      { namespace: "leo", client_id: "c1", scope: "mcp", aud: "x", exp: Math.floor(Date.now() / 1000) - 10 },
      100,
    );
    expect(await getAccessToken(kv, "tok-2")).toBeNull();
  });

  it("KV key 為 hash（不把 raw token 當 key）", async () => {
    // 白盒：store 內部用 sha256Hex，這裡驗 hash 與 raw 不同即可
    expect(await sha256Hex("tok-1")).not.toContain("tok-1");
  });
});

// ── metadata ──────────────────────────────────────────────────────────────────
describe("oauth/metadata", () => {
  it("originOf / resourceUri", () => {
    expect(originOf("https://Mcp.Arcrun.dev/mcp")).toBe("https://mcp.arcrun.dev");
    expect(resourceUri("https://mcp.arcrun.dev")).toBe("https://mcp.arcrun.dev/mcp");
  });
  it("protectedResourceMetadata 必要欄位", () => {
    const m = protectedResourceMetadata("https://mcp.arcrun.dev");
    expect(m.resource).toBe("https://mcp.arcrun.dev/mcp");
    expect(m.authorization_servers).toEqual(["https://mcp.arcrun.dev"]);
  });
  it("authorizationServerMetadata 必要欄位（S256/code/none）", () => {
    const m = authorizationServerMetadata("https://mcp.arcrun.dev");
    expect(m.authorization_endpoint).toBe("https://mcp.arcrun.dev/authorize");
    expect(m.token_endpoint).toBe("https://mcp.arcrun.dev/token");
    expect(m.registration_endpoint).toBe("https://mcp.arcrun.dev/register");
    expect(m.response_types_supported).toEqual(["code"]);
    expect(m.code_challenge_methods_supported).toEqual(["S256"]);
    expect(m.token_endpoint_auth_methods_supported).toEqual(["none"]);
  });
  it("wwwAuthenticateHeader 指向 protected-resource metadata", () => {
    expect(wwwAuthenticateHeader("https://mcp.arcrun.dev")).toBe(
      'Bearer resource_metadata="https://mcp.arcrun.dev/.well-known/oauth-protected-resource"',
    );
  });
  it("normalizeResource：尾斜線 / 大小寫 scheme+host / 預設 port 都正規化成 canonical", () => {
    const canon = "https://mcp.arcrun.dev/mcp";
    expect(normalizeResource("https://mcp.arcrun.dev/mcp")).toBe(canon);
    expect(normalizeResource("https://mcp.arcrun.dev/mcp/")).toBe(canon); // 尾斜線
    expect(normalizeResource("HTTPS://Mcp.Arcrun.Dev/mcp")).toBe(canon); // 大小寫 scheme+host
    expect(normalizeResource("https://mcp.arcrun.dev:443/mcp")).toBe(canon); // 預設 port
    expect(normalizeResource("not a url")).toBeNull();
  });
  it("resourceMatches：canonical / 尾斜線變體都 true；別的 host/path false", () => {
    const origin = "https://mcp.arcrun.dev";
    expect(resourceMatches("https://mcp.arcrun.dev/mcp", origin)).toBe(true);
    expect(resourceMatches("https://mcp.arcrun.dev/mcp/", origin)).toBe(true);
    expect(resourceMatches("https://other.example.com/mcp", origin)).toBe(false);
    expect(resourceMatches("https://mcp.arcrun.dev/other", origin)).toBe(false);
    expect(resourceMatches("garbage", origin)).toBe(false);
  });
});

// ── consent escaping（XSS）────────────────────────────────────────────────────
describe("oauth/consent", () => {
  it("esc 跳脫 HTML 特殊字元", () => {
    expect(esc(`<script>"&'`)).toBe("&lt;script&gt;&quot;&amp;&#39;");
  });
  it("consentPage 把惡意 state 跳脫掉（不注入）", () => {
    const html = consentPage({
      client_id: "c1",
      redirect_uri: "https://claude.ai/cb",
      state: '"><img src=x onerror=alert(1)>',
      code_challenge: "cc",
      code_challenge_method: "S256",
      scope: "mcp",
      resource: "r",
    });
    expect(html).not.toContain("<img src=x");
    expect(html).toContain("&lt;img src=x");
  });
});

// ── 完整 OAuth 流程（route 整合）──────────────────────────────────────────────
function buildApp(env: Env) {
  const app = new Hono<{ Bindings: Env }>();
  registerOAuthRoutes(app);
  return {
    req: (path: string, init?: RequestInit) =>
      app.request("https://mcp.arcrun.dev" + path, init, env),
  };
}

describe("oauth flow (整合)", () => {
  it("well-known metadata 端點以 request origin 動態生成", async () => {
    const app = buildApp(baseEnv());
    const r = await app.req("/.well-known/oauth-protected-resource");
    expect(r.status).toBe(200);
    const j = await r.json();
    expect(j.resource).toBe("https://mcp.arcrun.dev/mcp");
    const r2 = await app.req("/.well-known/oauth-authorization-server");
    expect((await r2.json()).issuer).toBe("https://mcp.arcrun.dev");
  });

  it("/register 回 client_id（public client，無 secret）", async () => {
    const app = buildApp(baseEnv());
    const r = await app.req("/register", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ redirect_uris: ["https://claude.ai/cb"], client_name: "Claude" }),
    });
    expect(r.status).toBe(201);
    const j = await r.json();
    expect(j.client_id).toMatch(/^mcp_/);
    expect(j.token_endpoint_auth_method).toBe("none");
    expect(j).not.toHaveProperty("client_secret");
  });

  it("/register 擋不允許的 redirect_uri host", async () => {
    const app = buildApp(baseEnv());
    const r = await app.req("/register", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ redirect_uris: ["https://evil.example.com/cb"] }),
    });
    expect(r.status).toBe(400);
    expect((await r.json()).error).toBe("invalid_redirect_uri");
  });

  it("GET /authorize 要求 PKCE S256 且顯示同意頁", async () => {
    const app = buildApp(baseEnv());
    const { challenge } = await pkcePair();
    const ok = await app.req(
      `/authorize?response_type=code&client_id=c1&redirect_uri=${encodeURIComponent(
        "https://claude.ai/cb",
      )}&code_challenge=${challenge}&code_challenge_method=S256&state=xyz&scope=mcp`,
    );
    expect(ok.status).toBe(200);
    const consentHtml = await ok.text();
    // 同意頁問的是 Portal 帳密（不是另一把 owner secret）
    expect(consentHtml).toContain("Portal");
    expect(consentHtml).toContain('name="email"');
    expect(consentHtml).toContain('name="password"');
    // 缺 PKCE → 400
    const bad = await app.req(
      `/authorize?response_type=code&client_id=c1&redirect_uri=${encodeURIComponent(
        "https://claude.ai/cb",
      )}`,
    );
    expect(bad.status).toBe(400);
  });

  it("GET /authorize：不需要任何 owner 祕密就看得到同意頁（封測者接自己的 AI 不會死在這頁）", async () => {
    // 舊行為：未設 MCP_OWNER_SECRET → 503 ⇒ 每個封測者都卡住。現在把關是 Portal 帳密。
    const app = buildApp(baseEnv());
    const { challenge } = await pkcePair();
    const r = await app.req(
      `/authorize?response_type=code&client_id=c1&redirect_uri=${encodeURIComponent(
        "https://claude.ai/cb",
      )}&code_challenge=${challenge}&code_challenge_method=S256`,
    );
    expect(r.status).toBe(200);
  });

  it("完整 code→token：正確 Portal 帳密 + 正確 verifier → access_token", async () => {
    const env = baseEnv();
    const app = buildApp(env);
    const { verifier, challenge } = await pkcePair();
    const redirect = "https://claude.ai/cb";

    // POST /authorize 正確祕密 → 302 帶 code
    const authRes = await app.req("/authorize", {
      method: "POST",
      headers: { "content-type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        client_id: "c1",
        redirect_uri: redirect,
        state: "st-1",
        code_challenge: challenge,
        code_challenge_method: "S256",
        scope: "mcp",
        resource: "https://mcp.arcrun.dev/mcp",
        email: GOOD_EMAIL,
        password: GOOD_PASSWORD,
      }).toString(),
      redirect: "manual",
    });
    expect(authRes.status).toBe(302);
    const loc = new URL(authRes.headers.get("location")!);
    expect(loc.searchParams.get("state")).toBe("st-1");
    const code = loc.searchParams.get("code")!;
    expect(code).toBeTruthy();

    // POST /token 正確 verifier → access_token
    const tokRes = await app.req("/token", {
      method: "POST",
      headers: { "content-type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        grant_type: "authorization_code",
        code,
        code_verifier: verifier,
        redirect_uri: redirect,
        client_id: "c1",
      }).toString(),
    });
    expect(tokRes.status).toBe(200);
    const tok = await tokRes.json();
    expect(tok.token_type).toBe("Bearer");
    expect(tok.access_token).toBeTruthy();
    expect(tok.expires_in).toBeGreaterThan(0);

    // 換發的 token 綁定 owner namespace
    const at = await getAccessToken(env.OAUTH_KV!, tok.access_token);
    expect(at?.namespace).toBe("leo");
    expect(at?.aud).toBe("https://mcp.arcrun.dev/mcp");
  });

  // ── 2026-08-12：身分要接住並攜帶（本次修的病根）─────────────────────────────
  describe("登入者身分跟著 token 走（leo：掛上 MCP 並輸入帳密＝授權，下游不得再問一次）", () => {
    it("驗完帳密不是只留布林值：token 帶得出 portal session 與該帳號的可用知識庫", async () => {
      const env = baseEnv({ CYPHER_EXECUTOR: cypherMock({ libraries: ["kb"], displayName: "小明", role: "user" }).fetcher });
      const app = buildApp(env);
      const { verifier, challenge } = await pkcePair();
      const redirect = "https://claude.ai/cb";
      const authRes = await app.req("/authorize", {
        method: "POST",
        headers: { "content-type": "application/x-www-form-urlencoded" },
        body: new URLSearchParams({
          client_id: "c1",
          redirect_uri: redirect,
          code_challenge: challenge,
          code_challenge_method: "S256",
          email: GOOD_EMAIL,
          password: GOOD_PASSWORD,
        }).toString(),
        redirect: "manual",
      });
      const code = new URL(authRes.headers.get("location")!).searchParams.get("code")!;
      const tokRes = await app.req("/token", {
        method: "POST",
        headers: { "content-type": "application/x-www-form-urlencoded" },
        body: new URLSearchParams({
          grant_type: "authorization_code",
          code,
          code_verifier: verifier,
          redirect_uri: redirect,
        }).toString(),
      });
      const at = await getAccessToken(env.OAUTH_KV!, (await tokRes.json()).access_token);
      expect(at?.portal?.session).toBe("sess-abc");
      expect(at?.portal?.display_name).toBe("小明");
      expect(at?.portal?.role).toBe("user");
      expect(at?.portal?.libraries).toEqual(["kb"]);
    });

    it("**不同帳號登入 → token 帶的身分跟著換**（不是不管誰登入都同一格）", async () => {
      // 兩個帳號權限不同：一個全庫、一個只有 kb。token 裡的身分必須各自不同。
      const envA = baseEnv({ CYPHER_EXECUTOR: cypherMock({ sessionToken: "sess-A", displayName: "Leo", libraries: ["*"] }).fetcher });
      const envB = baseEnv({ CYPHER_EXECUTOR: cypherMock({ sessionToken: "sess-B", displayName: "小明", libraries: ["kb"] }).fetcher });

      async function tokenFor(env: Env) {
        const app = buildApp(env);
        const { verifier, challenge } = await pkcePair();
        const redirect = "https://claude.ai/cb";
        const a = await app.req("/authorize", {
          method: "POST",
          headers: { "content-type": "application/x-www-form-urlencoded" },
          body: new URLSearchParams({
            client_id: "c1",
            redirect_uri: redirect,
            code_challenge: challenge,
            code_challenge_method: "S256",
            email: GOOD_EMAIL,
            password: GOOD_PASSWORD,
          }).toString(),
          redirect: "manual",
        });
        const code = new URL(a.headers.get("location")!).searchParams.get("code")!;
        const t = await app.req("/token", {
          method: "POST",
          headers: { "content-type": "application/x-www-form-urlencoded" },
          body: new URLSearchParams({
            grant_type: "authorization_code",
            code,
            code_verifier: verifier,
            redirect_uri: redirect,
          }).toString(),
        });
        return getAccessToken(env.OAUTH_KV!, (await t.json()).access_token);
      }

      const a = await tokenFor(envA);
      const b = await tokenFor(envB);
      expect(a?.portal?.session).not.toBe(b?.portal?.session);
      expect(a?.portal?.libraries).toEqual(["*"]);
      expect(b?.portal?.libraries).toEqual(["kb"]);
    });

    it("access_token 活不過它底下的 portal session（TTL 取兩者較小）", async () => {
      const env = baseEnv({
        MCP_TOKEN_TTL: "2592000", // 30 天
        CYPHER_EXECUTOR: cypherMock({ sessionExpiresIn: 3600 }).fetcher, // session 只有 1 小時
      });
      const app = buildApp(env);
      const { verifier, challenge } = await pkcePair();
      const redirect = "https://claude.ai/cb";
      const a = await app.req("/authorize", {
        method: "POST",
        headers: { "content-type": "application/x-www-form-urlencoded" },
        body: new URLSearchParams({
          client_id: "c1",
          redirect_uri: redirect,
          code_challenge: challenge,
          code_challenge_method: "S256",
          email: GOOD_EMAIL,
          password: GOOD_PASSWORD,
        }).toString(),
        redirect: "manual",
      });
      const code = new URL(a.headers.get("location")!).searchParams.get("code")!;
      const t = await app.req("/token", {
        method: "POST",
        headers: { "content-type": "application/x-www-form-urlencoded" },
        body: new URLSearchParams({
          grant_type: "authorization_code",
          code,
          code_verifier: verifier,
          redirect_uri: redirect,
        }).toString(),
      });
      expect((await t.json()).expires_in).toBe(3600);
    });

    it("cypher 回 200 但沒給 session_token（舊版 cypher）→ 不發碼（不發一張沒有身分的 token）", async () => {
      const app = buildApp(baseEnv({ CYPHER_EXECUTOR: cypherMock({ sessionToken: null }).fetcher }));
      const { challenge } = await pkcePair();
      const r = await app.req("/authorize", {
        method: "POST",
        headers: { "content-type": "application/x-www-form-urlencoded" },
        body: new URLSearchParams({
          client_id: "c1",
          redirect_uri: "https://claude.ai/cb",
          code_challenge: challenge,
          code_challenge_method: "S256",
          email: GOOD_EMAIL,
          password: GOOD_PASSWORD,
        }).toString(),
        redirect: "manual",
      });
      expect(r.status).toBe(401);
      expect(r.headers.get("location")).toBeNull();
    });
  });

  it("錯誤 owner 祕密 → 401、不發 code", async () => {
    const app = buildApp(baseEnv());
    const { challenge } = await pkcePair();
    const r = await app.req("/authorize", {
      method: "POST",
      headers: { "content-type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        client_id: "c1",
        redirect_uri: "https://claude.ai/cb",
        code_challenge: challenge,
        code_challenge_method: "S256",
        email: GOOD_EMAIL,
        password: "WRONG",
      }).toString(),
      redirect: "manual",
    });
    expect(r.status).toBe(401);
    expect(r.headers.get("location")).toBeNull();
    expect(await r.text()).toContain("不正確");
  });

  it("/token 錯誤 verifier → invalid_grant；重用 code → invalid_grant", async () => {
    const env = baseEnv();
    const app = buildApp(env);
    const { challenge } = await pkcePair();
    const redirect = "https://claude.ai/cb";
    const authRes = await app.req("/authorize", {
      method: "POST",
      headers: { "content-type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        client_id: "c1",
        redirect_uri: redirect,
        code_challenge: challenge,
        code_challenge_method: "S256",
        email: GOOD_EMAIL,
        password: GOOD_PASSWORD,
      }).toString(),
      redirect: "manual",
    });
    const code = new URL(authRes.headers.get("location")!).searchParams.get("code")!;

    // 錯誤 verifier
    const bad = await app.req("/token", {
      method: "POST",
      headers: { "content-type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        grant_type: "authorization_code",
        code,
        code_verifier: "z".repeat(64),
        redirect_uri: redirect,
      }).toString(),
    });
    expect(bad.status).toBe(400);
    expect((await bad.json()).error).toBe("invalid_grant");

    // 該 code 已被 consume（即使失敗也一次性）→ 再用正確 verifier 也 invalid_grant
    const reuse = await app.req("/token", {
      method: "POST",
      headers: { "content-type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        grant_type: "authorization_code",
        code,
        code_verifier: "a".repeat(64),
        redirect_uri: redirect,
      }).toString(),
    });
    expect((await reuse.json()).error).toBe("invalid_grant");
  });

  it("未設 OAUTH_KV → /token 回 503（誠實，不假綠）", async () => {
    const app = buildApp(baseEnv({ OAUTH_KV: undefined }));
    const r = await app.req("/token", {
      method: "POST",
      headers: { "content-type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        grant_type: "authorization_code",
        code: "x",
        code_verifier: "a".repeat(64),
        redirect_uri: "https://claude.ai/cb",
      }).toString(),
    });
    expect(r.status).toBe(503);
  });
});

// ── RFC 8707 resource 簽發端驗證/正規化 ─────────────────────────────────────────
// 決策：resource 正規化後 == canonical → 接受（尾斜線/大小寫/預設 port 皆等價，aud 一律存 canonical，
//   /mcp 嚴格比對必過）；正規化後 != canonical（別的 host/path）→ 簽發端 fail-fast 拒 invalid_target。
//   若把尾斜線也拒，claude.ai 真送尾斜線變體會永久連不上（把 401 問題換位重現），故等價形接受才對。
describe("oauth resource（RFC 8707）簽發端把關", () => {
  const redirect = "https://claude.ai/cb";

  async function postAuthorize(app: ReturnType<typeof buildApp>, resource: string, challenge: string) {
    return app.req("/authorize", {
      method: "POST",
      headers: { "content-type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        client_id: "c1",
        redirect_uri: redirect,
        state: "st",
        code_challenge: challenge,
        code_challenge_method: "S256",
        resource,
        email: GOOD_EMAIL,
        password: GOOD_PASSWORD,
      }).toString(),
      redirect: "manual",
    });
  }

  it("尾斜線變體（等價 canonical）→ 正常發碼，且換出 token 的 aud 為 canonical", async () => {
    const env = baseEnv();
    const app = buildApp(env);
    const { verifier, challenge } = await pkcePair();
    const r = await postAuthorize(app, "https://mcp.arcrun.dev/mcp/", challenge);
    expect(r.status).toBe(302);
    const loc = new URL(r.headers.get("location")!);
    expect(loc.searchParams.get("error")).toBeNull(); // 未被拒
    const code = loc.searchParams.get("code")!;
    expect(code).toBeTruthy();
    const tokRes = await app.req("/token", {
      method: "POST",
      headers: { "content-type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        grant_type: "authorization_code",
        code,
        code_verifier: verifier,
        redirect_uri: redirect,
        resource: "https://mcp.arcrun.dev/mcp/", // token 端也帶尾斜線 → 正規化後仍通過
      }).toString(),
    });
    expect(tokRes.status).toBe(200);
    const at = await getAccessToken(env.OAUTH_KV!, (await tokRes.json()).access_token);
    expect(at?.aud).toBe("https://mcp.arcrun.dev/mcp"); // 存的是 canonical，與 partner-auth 嚴格比對一致
  });

  it("正確 canonical resource → 正常發碼", async () => {
    const app = buildApp(baseEnv());
    const { challenge } = await pkcePair();
    const r = await postAuthorize(app, "https://mcp.arcrun.dev/mcp", challenge);
    expect(r.status).toBe(302);
    expect(new URL(r.headers.get("location")!).searchParams.get("code")).toBeTruthy();
  });

  it("GET /authorize：別的 host 的 resource → redirect 帶 error=invalid_target（不顯示同意頁）", async () => {
    const app = buildApp(baseEnv());
    const { challenge } = await pkcePair();
    const r = await app.req(
      `/authorize?response_type=code&client_id=c1&redirect_uri=${encodeURIComponent(redirect)}` +
        `&code_challenge=${challenge}&code_challenge_method=S256&state=st` +
        `&resource=${encodeURIComponent("https://evil.example.com/mcp")}`,
    );
    expect(r.status).toBe(302);
    const loc = new URL(r.headers.get("location")!);
    expect(loc.searchParams.get("error")).toBe("invalid_target");
    expect(loc.searchParams.get("state")).toBe("st");
    expect(loc.searchParams.get("code")).toBeNull();
  });

  it("POST /authorize：別的 host 的 resource → redirect invalid_target，不發碼", async () => {
    const app = buildApp(baseEnv());
    const { challenge } = await pkcePair();
    const r = await postAuthorize(app, "https://evil.example.com/mcp", challenge);
    expect(r.status).toBe(302);
    const loc = new URL(r.headers.get("location")!);
    expect(loc.searchParams.get("error")).toBe("invalid_target");
    expect(loc.searchParams.get("code")).toBeNull();
  });

  it("POST /token：別的 host 的 resource → 400 invalid_target", async () => {
    const env = baseEnv();
    const app = buildApp(env);
    const { verifier, challenge } = await pkcePair();
    // 先正常拿一個 code（authorize 不帶 resource → 存 canonical）
    const authRes = await postAuthorize(app, "https://mcp.arcrun.dev/mcp", challenge);
    const code = new URL(authRes.headers.get("location")!).searchParams.get("code")!;
    const tokRes = await app.req("/token", {
      method: "POST",
      headers: { "content-type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        grant_type: "authorization_code",
        code,
        code_verifier: verifier,
        redirect_uri: redirect,
        resource: "https://evil.example.com/mcp", // token 端 resource 不符 → 拒
      }).toString(),
    });
    expect(tokRes.status).toBe(400);
    expect((await tokRes.json()).error).toBe("invalid_target");
  });
});

// ── 防 drift：對 OAUTH_KV 的每一次 put 都必須帶 expirationTtl（守儲存鐵律）──────────
// spy KV 記錄所有 put(key,value,opts)；跑完整流程後斷言沒有任何一次「無 TTL」的 put，
// 防未來有人往這顆短效 KV 塞長效資料（access_token / code 以外的東西）。
describe("oauth store drift guard：OAUTH_KV 的 put 一律帶 TTL", () => {
  function spyKV(): { kv: KVNamespace; puts: Array<{ key: string; opts?: { expirationTtl?: number } }> } {
    const map = new Map<string, string>();
    const puts: Array<{ key: string; opts?: { expirationTtl?: number } }> = [];
    const kv = {
      async put(key: string, value: string, opts?: { expirationTtl?: number }) {
        puts.push({ key, opts });
        map.set(key, value);
      },
      async get(key: string) {
        return map.get(key) ?? null;
      },
      async delete(key: string) {
        map.delete(key);
      },
    } as unknown as KVNamespace;
    return { kv, puts };
  }

  it("完整 authorize→token 流程中，OAUTH_KV 的每次 put 都有 expirationTtl>0", async () => {
    const { kv, puts } = spyKV();
    const env = baseEnv({ OAUTH_KV: kv });
    const app = buildApp(env);
    const { verifier, challenge } = await pkcePair();
    const redirect = "https://claude.ai/cb";

    const authRes = await app.req("/authorize", {
      method: "POST",
      headers: { "content-type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        client_id: "c1",
        redirect_uri: redirect,
        code_challenge: challenge,
        code_challenge_method: "S256",
        email: GOOD_EMAIL,
        password: GOOD_PASSWORD,
      }).toString(),
      redirect: "manual",
    });
    const code = new URL(authRes.headers.get("location")!).searchParams.get("code")!;
    await app.req("/token", {
      method: "POST",
      headers: { "content-type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        grant_type: "authorization_code",
        code,
        code_verifier: verifier,
        redirect_uri: redirect,
        client_id: "c1",
      }).toString(),
    });

    // 至少發生了 put（code + token 各一），且每一次都帶 TTL。
    expect(puts.length).toBeGreaterThanOrEqual(2);
    for (const p of puts) {
      expect(p.opts?.expirationTtl, `put ${p.key} 缺 expirationTtl`).toBeTypeOf("number");
      expect(p.opts!.expirationTtl!).toBeGreaterThan(0);
    }
  });

  it("直接呼叫 store 層 putAuthCode / putAccessToken 也一律帶 TTL", async () => {
    const { kv, puts } = spyKV();
    await putAuthCode(kv, "c", {
      client_id: "c1",
      redirect_uri: "https://claude.ai/cb",
      code_challenge: "cc",
      code_challenge_method: "S256",
      scope: "mcp",
      resource: "https://mcp/mcp",
      namespace: "leo",
    });
    await putAccessToken(
      kv,
      "t",
      { namespace: "leo", client_id: "c1", scope: "mcp", aud: "https://mcp/mcp", exp: 1 },
      100,
    );
    expect(puts).toHaveLength(2);
    for (const p of puts) {
      expect(p.opts?.expirationTtl).toBeGreaterThan(0);
    }
  });
});
