/**
 * Tests for the IAP- and App-Attest-gated Claude BFF Worker.
 *
 * Run offline, zero installs:
 *   node --experimental-strip-types --test worker.test.ts
 *
 * The crypto verifiers and the stores are injectable, so these tests exercise
 * the real decision-and-proxy logic, the part that decides who gets through,
 * what they may spend, and what gets forwarded, without Apple's certificate
 * chain, a real device, or Durable Objects.
 */
import { test } from "node:test";
import assert from "node:assert/strict";
import { parseList, evaluateEntitlement } from "./entitlement.ts";
import { evaluateSpend, spendConfig } from "./limits.ts";
import { TokenBudget, type BudgetStore } from "./budget.ts";
import { AttestKey, type AttestKeyStore } from "./attestkey.ts";
import {
  AttestChallenge,
  CHALLENGE_TTL_MS,
  type ChallengeStore,
} from "./challenge.ts";
import {
  RevocationRecord,
  revocationFromNotification,
  type RevocationStore,
} from "./revocation.ts";
import type { JWSTransaction } from "./transaction.ts";
import type { TransactionVerifier } from "./storekit.ts";
import type { AppAttestVerifier } from "./appattest.ts";
import { handleRequest, type Handlers } from "./worker.ts";
import type { Env } from "./env.ts";

const baseCfg = () => ({
  allowedBundleIds: new Set(["com.example.app"]),
  allowedProductIds: null as Set<string> | null,
  now: 1_000_000,
});

const prod = (extra: Partial<JWSTransaction> = {}): JWSTransaction => ({
  bundleId: "com.example.app",
  environment: "Production",
  ...extra,
});

test("parseList trims and drops empties", () => {
  assert.deepEqual([...parseList(" a , b ,, c ")], ["a", "b", "c"]);
  assert.equal(parseList(undefined).size, 0);
});

test("entitlement: allowed bundle + Production + future expiry passes", () => {
  assert.deepEqual(
    evaluateEntitlement(prod({ expiresDate: 2_000_000 }), baseCfg()),
    { ok: true },
  );
});

test("entitlement: non-subscription (no expiresDate) passes", () => {
  assert.deepEqual(evaluateEntitlement(prod(), baseCfg()), { ok: true });
});

test("entitlement: sandbox environment is rejected (403)", () => {
  const txn = prod({ environment: "Sandbox", expiresDate: 2_000_000 });
  assert.deepEqual(evaluateEntitlement(txn, baseCfg()), {
    ok: false,
    status: 403,
    reason: "environment_not_production",
  });
});

test("entitlement: missing environment is rejected (403)", () => {
  const txn: JWSTransaction = { bundleId: "com.example.app" };
  assert.deepEqual(evaluateEntitlement(txn, baseCfg()), {
    ok: false,
    status: 403,
    reason: "environment_not_production",
  });
});

test("entitlement: wrong bundle is rejected (403)", () => {
  const txn = prod({ bundleId: "com.evil.app", expiresDate: 2_000_000 });
  assert.deepEqual(evaluateEntitlement(txn, baseCfg()), {
    ok: false,
    status: 403,
    reason: "bundle_id_not_allowed",
  });
});

test("entitlement: expired subscription is rejected (403)", () => {
  const txn = prod({ expiresDate: 500_000 });
  assert.deepEqual(evaluateEntitlement(txn, baseCfg()), {
    ok: false,
    status: 403,
    reason: "entitlement_expired",
  });
});

test("entitlement: revoked purchase is rejected (403)", () => {
  const txn = prod({ expiresDate: 2_000_000, revocationDate: 600_000 });
  assert.deepEqual(evaluateEntitlement(txn, baseCfg()), {
    ok: false,
    status: 403,
    reason: "entitlement_revoked",
  });
});

test("entitlement: product allowlist is enforced when set", () => {
  const cfg = { ...baseCfg(), allowedProductIds: new Set(["pro.monthly"]) };
  assert.equal(
    evaluateEntitlement(prod({ productId: "pro.monthly" }), cfg).ok,
    true,
  );
  assert.deepEqual(evaluateEntitlement(prod({ productId: "free.tier" }), cfg), {
    ok: false,
    status: 403,
    reason: "product_id_not_allowed",
  });
});

// --- spend decision (pure) ---

const spendCfg = (over = {}) => ({
  allowedModels: new Set(["claude-opus-4-8"]) as Set<string> | null,
  maxTokensLimit: 1024 as number | null,
  dailyTokenBudget: null as number | null,
  ...over,
});

test("spendConfig: null when no knob is set, active when any is", () => {
  assert.equal(spendConfig({ ANTHROPIC_API_KEY: "x" }), null);
  const cfg = spendConfig({ ANTHROPIC_API_KEY: "x", ALLOWED_MODELS: "m" });
  assert.deepEqual(cfg, {
    allowedModels: new Set(["m"]),
    maxTokensLimit: null,
    dailyTokenBudget: null,
  });
});

test("spend: allowed model under the cap passes", () => {
  assert.deepEqual(
    evaluateSpend({ model: "claude-opus-4-8", max_tokens: 256 }, spendCfg()),
    { ok: true, maxTokens: 256 },
  );
});

test("spend: model outside the allowlist is rejected (403)", () => {
  assert.deepEqual(
    evaluateSpend({ model: "claude-haiku-4-5", max_tokens: 256 }, spendCfg()),
    { ok: false, status: 403, reason: "model_not_allowed" },
  );
  assert.equal(evaluateSpend({ max_tokens: 256 }, spendCfg()).ok, false);
});

test("spend: max_tokens is required when a cap or budget is configured", () => {
  for (const max_tokens of [undefined, 0, -5, 1.5, "16"]) {
    assert.deepEqual(
      evaluateSpend({ model: "claude-opus-4-8", max_tokens }, spendCfg()),
      { ok: false, status: 400, reason: "max_tokens_required" },
    );
  }
  const budgetOnly = spendCfg({
    allowedModels: null,
    maxTokensLimit: null,
    dailyTokenBudget: 1000,
  });
  assert.equal(evaluateSpend({}, budgetOnly).ok, false);
});

test("spend: max_tokens over the cap is rejected (400)", () => {
  assert.deepEqual(
    evaluateSpend({ model: "claude-opus-4-8", max_tokens: 2048 }, spendCfg()),
    { ok: false, status: 400, reason: "max_tokens_exceeds_limit" },
  );
});

test("spend: non-object body is rejected (400)", () => {
  for (const body of [null, [], "x", 7]) {
    assert.deepEqual(evaluateSpend(body, spendCfg()), {
      ok: false,
      status: 400,
      reason: "body_not_json_object",
    });
  }
});

// --- Durable Objects, tested directly with a fake storage ---

function fakeState() {
  const map = new Map<string, unknown>();
  return {
    storage: {
      get: async (key: string) => map.get(key),
      put: async (key: string, value: unknown) => {
        map.set(key, value);
      },
    },
  };
}

test("TokenBudget: debits, refuses over-limit without debiting, resets daily", async () => {
  const dobj = new TokenBudget(fakeState());
  const debit = async (tokens: number, day: string) => {
    const res = await dobj.fetch(
      new Request("https://budget/debit", {
        method: "POST",
        body: JSON.stringify({ tokens, limit: 1000, day }),
      }),
    );
    return res.json();
  };
  assert.deepEqual(await debit(600, "2026-06-12"), { ok: true, remaining: 400 });
  assert.deepEqual(await debit(600, "2026-06-12"), { ok: false, remaining: 400 });
  // The refused request was not debited; a smaller one still fits.
  assert.deepEqual(await debit(400, "2026-06-12"), { ok: true, remaining: 0 });
  // A new day resets the window.
  assert.deepEqual(await debit(600, "2026-06-13"), { ok: true, remaining: 400 });
});

test("AttestKey: stores the key, accepts only strictly increasing counters", async () => {
  const dobj = new AttestKey(fakeState());
  const call = async (body: unknown) => {
    const res = await dobj.fetch(
      new Request("https://attest-key/", {
        method: "POST",
        body: JSON.stringify(body),
      }),
    );
    return res.json();
  };
  assert.deepEqual(await call({ action: "get" }), { spki: null });
  assert.deepEqual(await call({ action: "put", spkiBase64: "spki" }), {
    ok: true,
  });
  assert.deepEqual(await call({ action: "get" }), { spki: "spki" });
  assert.deepEqual(await call({ action: "bump", counter: 1 }), { ok: true });
  assert.deepEqual(await call({ action: "bump", counter: 2 }), { ok: true });
  assert.deepEqual(await call({ action: "bump", counter: 2 }), { ok: false }); // replay
  assert.deepEqual(await call({ action: "bump", counter: 1 }), { ok: false }); // rollback
});

test("AttestChallenge: consumed exactly once, within its window", async () => {
  const dobj = new AttestChallenge(fakeState());
  const call = async (action: "issue" | "consume", now: number) => {
    const res = await dobj.fetch(
      new Request("https://challenge/", {
        method: "POST",
        body: JSON.stringify({ action, now }),
      }),
    );
    return res.json();
  };
  // Consuming before issuing fails.
  assert.deepEqual(await call("consume", 1_000), { ok: false });
  await call("issue", 1_000);
  assert.deepEqual(await call("consume", 2_000), { ok: true });
  assert.deepEqual(await call("consume", 3_000), { ok: false }); // spent

  // A fresh challenge past its window fails.
  const stale = new AttestChallenge(fakeState());
  const staleCall = async (action: "issue" | "consume", now: number) => {
    const res = await stale.fetch(
      new Request("https://challenge/", {
        method: "POST",
        body: JSON.stringify({ action, now }),
      }),
    );
    return res.json();
  };
  await staleCall("issue", 1_000);
  assert.deepEqual(await staleCall("consume", 1_000 + CHALLENGE_TTL_MS + 1), {
    ok: false,
  });
});

test("RevocationRecord: revoke sets, unrevoke clears", async () => {
  const dobj = new RevocationRecord(fakeState());
  const call = async (body: unknown) => {
    const res = await dobj.fetch(
      new Request("https://revocation/", {
        method: "POST",
        body: JSON.stringify(body),
      }),
    );
    return res.json();
  };
  assert.deepEqual(await call({ action: "get" }), { revoked: false });
  assert.deepEqual(await call({ action: "revoke", reason: "REFUND" }), {
    revoked: true,
  });
  assert.deepEqual(await call({ action: "get" }), { revoked: true });
  assert.deepEqual(await call({ action: "unrevoke" }), { revoked: false });
  assert.deepEqual(await call({ action: "get" }), { revoked: false });
});

// --- revocation mapping (pure) ---

test("revocation: REFUND and REVOKE revoke, REFUND_REVERSED restores", () => {
  const txn = { originalTransactionId: "txn-1" };
  assert.deepEqual(revocationFromNotification("REFUND", txn), {
    kind: "revoke",
    originalTransactionId: "txn-1",
    reason: "REFUND",
  });
  assert.deepEqual(revocationFromNotification("REVOKE", txn), {
    kind: "revoke",
    originalTransactionId: "txn-1",
    reason: "REVOKE",
  });
  assert.deepEqual(revocationFromNotification("REFUND_REVERSED", txn), {
    kind: "unrevoke",
    originalTransactionId: "txn-1",
  });
  assert.equal(revocationFromNotification("SUBSCRIBED", txn), null);
  assert.equal(revocationFromNotification("TEST", txn), null);
  assert.equal(revocationFromNotification("REFUND", {}), null);
});

// --- handleRequest integration ---

const storeKitEnv: Env = {
  ANTHROPIC_API_KEY: "sk-ant-PLACEHOLDER",
  ALLOWED_BUNDLE_IDS: "com.example.app",
  APPLE_ROOT_CA_SHA256: "00",
};

function messagesRequest(
  headers: Record<string, string>,
  path = "/v1/messages",
  body: unknown = { model: "claude-opus-4-8", max_tokens: 16, messages: [] },
): Request {
  return new Request(`https://bff.example.com${path}`, {
    method: "POST",
    headers: { "content-type": "application/json", ...headers },
    body: JSON.stringify(body),
  });
}

const passTxn: TransactionVerifier = async () =>
  prod({ expiresDate: Date.now() + 60_000 });

function memoryKeyStore(
  initial: Record<string, string> = {},
): AttestKeyStore & { map: Map<string, string> } {
  const map = new Map(Object.entries(initial));
  const counters = new Map<string, number>();
  return {
    map,
    getPublicKey: async (keyId) => map.get(keyId) ?? null,
    putPublicKey: async (keyId, spkiBase64) => {
      map.set(keyId, spkiBase64);
      counters.set(keyId, 0);
    },
    bump: async (keyId, counter) => {
      const last = counters.get(keyId) ?? 0;
      if (counter <= last) return false;
      counters.set(keyId, counter);
      return true;
    },
  };
}

function memoryChallengeStore(): ChallengeStore & { issued: Set<string> } {
  const issued = new Set<string>();
  let n = 0;
  return {
    issued,
    issue: async () => {
      const challenge = `challenge-${++n}`;
      issued.add(challenge);
      return challenge;
    },
    consume: async (challenge) => issued.delete(challenge),
  };
}

function memoryRevocationStore(
  revoked: string[] = [],
): RevocationStore & { map: Map<string, string> } {
  const map = new Map(revoked.map((id) => [id, "REFUND"]));
  return {
    map,
    isRevoked: async (id) => map.has(id),
    revoke: async (id, reason) => {
      map.set(id, reason);
    },
    unrevoke: async (id) => {
      map.delete(id);
    },
  };
}

function memoryBudgetStore(): BudgetStore & { seen: string[] } {
  const used = new Map<string, number>();
  const seen: string[] = [];
  return {
    seen,
    debit: async (callerId, tokens, limit) => {
      seen.push(callerId);
      const u = used.get(callerId) ?? 0;
      if (u + tokens > limit) {
        return { ok: false, remaining: Math.max(0, limit - u) };
      }
      used.set(callerId, u + tokens);
      return { ok: true, remaining: limit - u - tokens };
    },
  };
}

function handlers(over: Partial<Handlers> = {}): Handlers {
  const keys = memoryKeyStore({ "key-1": "spki" });
  const challenges = memoryChallengeStore();
  return {
    verifyTransaction: passTxn,
    verifyAppAttest: async () => {},
    verifyAttestation: async () => ({ spkiBase64: "spki" }),
    verifyNotification: async () => ({
      notificationType: "TEST",
      transaction: {},
    }),
    budgetStore: () => null,
    attestKeyStore: () => keys,
    challengeStore: () => challenges,
    revocationStore: () => null,
    ...over,
  };
}

/** Mock global fetch for one call, capturing the upstream request. */
async function withMockedFetch<T>(
  run: () => Promise<T>,
): Promise<{ result: T; calls: Array<{ url: string; init: RequestInit }> }> {
  const calls: Array<{ url: string; init: RequestInit }> = [];
  const orig = globalThis.fetch;
  globalThis.fetch = (async (url: unknown, init: RequestInit) => {
    calls.push({ url: String(url), init });
    return new Response(JSON.stringify({ ok: true }), { status: 200 });
  }) as typeof fetch;
  try {
    return { result: await run(), calls };
  } finally {
    globalThis.fetch = orig;
  }
}

test("handleRequest: no gate configured → 500, no upstream call", async () => {
  const res = await handleRequest(
    messagesRequest({ "X-IAP-Transaction": "ok" }),
    { ANTHROPIC_API_KEY: "sk-ant-PLACEHOLDER" },
    handlers(),
  );
  assert.equal(res.status, 500);
});

test("handleRequest: disallowed upstream path → 403", async () => {
  const res = await handleRequest(
    messagesRequest({ "X-IAP-Transaction": "ok" }, "/v1/models"),
    storeKitEnv,
    handlers(),
  );
  assert.equal(res.status, 403);
});

test("handleRequest: missing transaction header → 401, verifier never called", async () => {
  let called = false;
  const res = await handleRequest(
    messagesRequest({}),
    storeKitEnv,
    handlers({
      verifyTransaction: async () => {
        called = true;
        return prod();
      },
    }),
  );
  assert.equal(res.status, 401);
  assert.equal(called, false);
});

test("handleRequest: invalid JWS → 401, no upstream call", async () => {
  const { result, calls } = await withMockedFetch(() =>
    handleRequest(
      messagesRequest({ "X-IAP-Transaction": "bad" }),
      storeKitEnv,
      handlers({
        verifyTransaction: async () => {
          throw new Error("bad signature");
        },
      }),
    ),
  );
  assert.equal(result.status, 401);
  assert.equal(calls.length, 0);
});

test("handleRequest: unentitled caller → 403, no upstream call", async () => {
  const { result, calls } = await withMockedFetch(() =>
    handleRequest(
      messagesRequest({ "X-IAP-Transaction": "ok" }),
      storeKitEnv,
      handlers({
        verifyTransaction: async () => prod({ bundleId: "com.evil.app" }),
      }),
    ),
  );
  assert.equal(result.status, 403);
  assert.equal(calls.length, 0);
});

test("handleRequest: entitled caller → forwards with a clean, injected header set", async () => {
  const { result, calls } = await withMockedFetch(() =>
    handleRequest(
      messagesRequest({
        "X-IAP-Transaction": "ok",
        cookie: "session=abc",
        "anthropic-beta": "evil-flag",
      }),
      storeKitEnv,
      handlers(),
    ),
  );
  assert.equal(result.status, 200);
  assert.equal(calls[0].url, "https://api.anthropic.com/v1/messages");
  const h = new Headers(calls[0].init.headers);
  assert.equal(h.get("x-api-key"), "sk-ant-PLACEHOLDER");
  assert.equal(h.get("anthropic-version"), "2023-06-01");
  assert.equal(h.get("content-type"), "application/json");
  // Nothing the client sent leaks upstream.
  assert.equal(h.get("X-IAP-Transaction"), null);
  assert.equal(h.get("cookie"), null);
  assert.equal(h.get("anthropic-beta"), null);
});

test("handleRequest: revoked originalTransactionId → 403, no upstream call", async () => {
  const revocations = memoryRevocationStore(["txn-9"]);
  const { result, calls } = await withMockedFetch(() =>
    handleRequest(
      messagesRequest({ "X-IAP-Transaction": "ok" }),
      storeKitEnv,
      handlers({
        verifyTransaction: async () =>
          prod({
            expiresDate: Date.now() + 60_000,
            originalTransactionId: "txn-9",
          }),
        revocationStore: () => revocations,
      }),
    ),
  );
  assert.equal(result.status, 403);
  assert.equal(calls.length, 0);
});

// --- App Attest gate ---

const appAttestEnv: Env = {
  ANTHROPIC_API_KEY: "sk-ant-PLACEHOLDER",
  APP_ATTEST_TEAM_ID: "ABCDE12345",
  APP_ATTEST_BUNDLE_ID: "com.example.app",
};

test("handleRequest: App Attest only, missing assertion → 401", async () => {
  const res = await handleRequest(messagesRequest({}), appAttestEnv, handlers());
  assert.equal(res.status, 401);
});

test("handleRequest: App Attest only, valid assertion → forwards", async () => {
  const { result, calls } = await withMockedFetch(() =>
    handleRequest(
      messagesRequest({
        "X-App-Attest-Assertion": "ok",
        "X-App-Attest-Key-Id": "key-1",
      }),
      appAttestEnv,
      handlers(),
    ),
  );
  assert.equal(result.status, 200);
  assert.equal(calls[0].url, "https://api.anthropic.com/v1/messages");
});

test("handleRequest: App Attest failure → 401, no upstream call", async () => {
  const failAttest: AppAttestVerifier = async () => {
    throw new Error("bad assertion");
  };
  const { result, calls } = await withMockedFetch(() =>
    handleRequest(
      messagesRequest({
        "X-App-Attest-Assertion": "bad",
        "X-App-Attest-Key-Id": "key-1",
      }),
      appAttestEnv,
      handlers({ verifyAppAttest: failAttest }),
    ),
  );
  assert.equal(result.status, 401);
  assert.equal(calls.length, 0);
});

test("handleRequest: App Attest configured without stores → 500", async () => {
  const res = await handleRequest(
    messagesRequest({
      "X-App-Attest-Assertion": "ok",
      "X-App-Attest-Key-Id": "key-1",
    }),
    appAttestEnv,
    handlers({ attestKeyStore: () => null }),
  );
  assert.equal(res.status, 500);
});

test("handleRequest: replayed counter (via the injected key store) → 401", async () => {
  // The fake verifier exercises the store wiring: it consumes the counter the
  // way the real verifier does, after every other check.
  const verifyAppAttest: AppAttestVerifier = async (a, _cfg, keys) => {
    if (!(await keys.bump(a.keyId, 7))) {
      throw new Error("replayed");
    }
  };
  const h = handlers({ verifyAppAttest });
  const send = () =>
    handleRequest(
      messagesRequest({
        "X-App-Attest-Assertion": "ok",
        "X-App-Attest-Key-Id": "key-1",
      }),
      appAttestEnv,
      h,
    );
  const { result: first } = await withMockedFetch(send);
  assert.equal(first.status, 200);
  const { result: replay, calls } = await withMockedFetch(send);
  assert.equal(replay.status, 401);
  assert.equal(calls.length, 0);
});

test("handleRequest: both gates configured, request must clear both", async () => {
  const bothEnv: Env = { ...storeKitEnv, ...appAttestEnv };
  const failAttest: AppAttestVerifier = async () => {
    throw new Error("bad assertion");
  };
  const { result: rejected } = await withMockedFetch(() =>
    handleRequest(
      messagesRequest({
        "X-IAP-Transaction": "ok",
        "X-App-Attest-Assertion": "bad",
        "X-App-Attest-Key-Id": "key-1",
      }),
      bothEnv,
      handlers({ verifyAppAttest: failAttest }),
    ),
  );
  assert.equal(rejected.status, 401);

  const { result: ok } = await withMockedFetch(() =>
    handleRequest(
      messagesRequest({
        "X-IAP-Transaction": "ok",
        "X-App-Attest-Assertion": "ok",
        "X-App-Attest-Key-Id": "key-1",
      }),
      bothEnv,
      handlers(),
    ),
  );
  assert.equal(ok.status, 200);
});

// --- spend controls through the worker ---

const spendEnv: Env = {
  ...storeKitEnv,
  ALLOWED_MODELS: "claude-opus-4-8",
  MAX_TOKENS_LIMIT: "1024",
};

test("handleRequest: disallowed model → 403, no upstream call", async () => {
  const { result, calls } = await withMockedFetch(() =>
    handleRequest(
      messagesRequest({ "X-IAP-Transaction": "ok" }, "/v1/messages", {
        model: "claude-haiku-4-5",
        max_tokens: 16,
        messages: [],
      }),
      spendEnv,
      handlers(),
    ),
  );
  assert.equal(result.status, 403);
  assert.equal(calls.length, 0);
});

test("handleRequest: max_tokens over the cap or missing → 400", async () => {
  for (const body of [
    { model: "claude-opus-4-8", max_tokens: 4096, messages: [] },
    { model: "claude-opus-4-8", messages: [] },
  ]) {
    const { result, calls } = await withMockedFetch(() =>
      handleRequest(
        messagesRequest({ "X-IAP-Transaction": "ok" }, "/v1/messages", body),
        spendEnv,
        handlers(),
      ),
    );
    assert.equal(result.status, 400);
    assert.equal(calls.length, 0);
  }
});

test("handleRequest: spend gate forwards the body it inspected, verbatim", async () => {
  const body = { model: "claude-opus-4-8", max_tokens: 16, messages: [] };
  const { calls } = await withMockedFetch(() =>
    handleRequest(
      messagesRequest({ "X-IAP-Transaction": "ok" }, "/v1/messages", body),
      spendEnv,
      handlers(),
    ),
  );
  assert.equal(calls[0].init.body, JSON.stringify(body));
});

test("handleRequest: budget configured without a store → 500", async () => {
  const res = await handleRequest(
    messagesRequest({ "X-IAP-Transaction": "ok" }),
    { ...storeKitEnv, DAILY_TOKEN_BUDGET: "100" },
    handlers(),
  );
  assert.equal(res.status, 500);
});

test("handleRequest: daily budget exhausts → 200 then 429, one upstream call", async () => {
  const budget = memoryBudgetStore();
  const env: Env = { ...storeKitEnv, DAILY_TOKEN_BUDGET: "100" };
  const h = handlers({
    verifyTransaction: async () =>
      prod({
        expiresDate: Date.now() + 60_000,
        originalTransactionId: "txn-1",
      }),
    budgetStore: () => budget,
  });
  const send = () =>
    handleRequest(
      messagesRequest({ "X-IAP-Transaction": "ok" }, "/v1/messages", {
        model: "claude-opus-4-8",
        max_tokens: 80,
        messages: [],
      }),
      env,
      h,
    );
  const { result: first, calls: firstCalls } = await withMockedFetch(send);
  assert.equal(first.status, 200);
  assert.equal(firstCalls.length, 1);
  const { result: second, calls: secondCalls } = await withMockedFetch(send);
  assert.equal(second.status, 429);
  assert.equal(secondCalls.length, 0);
  assert.deepEqual(await second.json(), {
    error: "daily_token_budget_exhausted",
    remaining: 20,
  });
});

test("handleRequest: budget keys on originalTransactionId, or verified keyId", async () => {
  // StoreKit: the purchase identity wins.
  const viaStoreKit = memoryBudgetStore();
  await withMockedFetch(() =>
    handleRequest(
      messagesRequest({ "X-IAP-Transaction": "ok" }),
      { ...storeKitEnv, DAILY_TOKEN_BUDGET: "100" },
      handlers({
        verifyTransaction: async () =>
          prod({
            expiresDate: Date.now() + 60_000,
            originalTransactionId: "txn-1",
          }),
        budgetStore: () => viaStoreKit,
      }),
    ),
  );
  assert.deepEqual(viaStoreKit.seen, ["txn-1"]);

  // App Attest only: the device key the assertion just proved.
  const viaKeyId = memoryBudgetStore();
  await withMockedFetch(() =>
    handleRequest(
      messagesRequest({
        "X-App-Attest-Assertion": "ok",
        "X-App-Attest-Key-Id": "key-1",
      }),
      { ...appAttestEnv, DAILY_TOKEN_BUDGET: "100" },
      handlers({ budgetStore: () => viaKeyId }),
    ),
  );
  assert.deepEqual(viaKeyId.seen, ["key-1"]);

  // StoreKit without an originalTransactionId yields no verified identity.
  const noIdentity = memoryBudgetStore();
  const { result } = await withMockedFetch(() =>
    handleRequest(
      messagesRequest({ "X-IAP-Transaction": "ok" }),
      { ...storeKitEnv, DAILY_TOKEN_BUDGET: "100" },
      handlers({ budgetStore: () => noIdentity }),
    ),
  );
  assert.equal(result.status, 403);
  assert.deepEqual(noIdentity.seen, []);
});

// --- service routes ---

test("notifications route: verified REFUND lands in the revocation store", async () => {
  const revocations = memoryRevocationStore();
  const res = await handleRequest(
    messagesRequest({}, "/apple/notifications", { signedPayload: "jws" }),
    storeKitEnv,
    handlers({
      verifyNotification: async () => ({
        notificationType: "REFUND",
        transaction: { originalTransactionId: "txn-7" },
      }),
      revocationStore: () => revocations,
    }),
  );
  assert.equal(res.status, 200);
  assert.equal(await revocations.isRevoked("txn-7"), true);
});

test("notifications route: bad signature → 401, store untouched", async () => {
  const revocations = memoryRevocationStore();
  const res = await handleRequest(
    messagesRequest({}, "/apple/notifications", { signedPayload: "forged" }),
    storeKitEnv,
    handlers({
      verifyNotification: async () => {
        throw new Error("bad signature");
      },
      revocationStore: () => revocations,
    }),
  );
  assert.equal(res.status, 401);
  assert.equal(revocations.map.size, 0);
});

test("notifications route: ignored type and malformed body", async () => {
  const revocations = memoryRevocationStore();
  const h = handlers({ revocationStore: () => revocations });
  const ignored = await handleRequest(
    messagesRequest({}, "/apple/notifications", { signedPayload: "jws" }),
    storeKitEnv,
    h, // default fake verifier returns TEST
  );
  assert.equal(ignored.status, 200);
  assert.equal(revocations.map.size, 0);

  const malformed = await handleRequest(
    new Request("https://bff.example.com/apple/notifications", {
      method: "POST",
      body: "not json",
    }),
    storeKitEnv,
    h,
  );
  assert.equal(malformed.status, 400);
});

test("notifications route: 404 without a revocation store", async () => {
  const res = await handleRequest(
    messagesRequest({}, "/apple/notifications", { signedPayload: "jws" }),
    storeKitEnv,
    handlers(), // revocationStore: () => null
  );
  assert.equal(res.status, 404);
});

test("challenge route: issues a one-time challenge, 404 when unconfigured", async () => {
  const challenges = memoryChallengeStore();
  const issued = await handleRequest(
    messagesRequest({}, "/app-attest/challenge", {}),
    appAttestEnv,
    handlers({ challengeStore: () => challenges }),
  );
  assert.equal(issued.status, 200);
  const { challenge } = (await issued.json()) as { challenge: string };
  assert.equal(challenges.issued.has(challenge), true);

  const unconfigured = await handleRequest(
    messagesRequest({}, "/app-attest/challenge", {}),
    storeKitEnv, // App Attest not configured
    handlers(),
  );
  assert.equal(unconfigured.status, 404);
});

test("registration route: issued challenge + verified attestation → 204, once", async () => {
  const keys = memoryKeyStore();
  const challenges = memoryChallengeStore();
  const env: Env = { ...appAttestEnv, APP_ATTEST_ROOT_CA: "PEM_PLACEHOLDER" };
  const h = handlers({
    attestKeyStore: () => keys,
    challengeStore: () => challenges,
  });
  const challenge = await challenges.issue();
  const register = () =>
    handleRequest(
      messagesRequest({}, "/app-attest/register", {
        keyId: "key-2",
        attestation: "cbor",
        challenge,
      }),
      env,
      h,
    );
  const res = await register();
  assert.equal(res.status, 204);
  assert.equal(keys.map.get("key-2"), "spki");
  // The same challenge never registers twice.
  const replay = await register();
  assert.equal(replay.status, 401);
});

test("registration route: unissued challenge → 401, verifier never called", async () => {
  const keys = memoryKeyStore();
  let called = false;
  const env: Env = { ...appAttestEnv, APP_ATTEST_ROOT_CA: "PEM_PLACEHOLDER" };
  const res = await handleRequest(
    messagesRequest({}, "/app-attest/register", {
      keyId: "key-2",
      attestation: "cbor",
      challenge: "never-issued",
    }),
    env,
    handlers({
      attestKeyStore: () => keys,
      verifyAttestation: async () => {
        called = true;
        return { spkiBase64: "spki" };
      },
    }),
  );
  assert.equal(res.status, 401);
  assert.equal(called, false);
  assert.equal(keys.map.size, 0);
});

test("registration route: invalid attestation → 401, key not stored", async () => {
  const keys = memoryKeyStore();
  const challenges = memoryChallengeStore();
  const env: Env = { ...appAttestEnv, APP_ATTEST_ROOT_CA: "PEM_PLACEHOLDER" };
  const challenge = await challenges.issue();
  const res = await handleRequest(
    messagesRequest({}, "/app-attest/register", {
      keyId: "key-2",
      attestation: "forged",
      challenge,
    }),
    env,
    handlers({
      attestKeyStore: () => keys,
      challengeStore: () => challenges,
      verifyAttestation: async () => {
        throw new Error("bad attestation");
      },
    }),
  );
  assert.equal(res.status, 401);
  assert.equal(keys.map.size, 0);
});

test("registration route: missing fields → 400, unconfigured → 404", async () => {
  const env: Env = { ...appAttestEnv, APP_ATTEST_ROOT_CA: "PEM_PLACEHOLDER" };
  const missing = await handleRequest(
    messagesRequest({}, "/app-attest/register", { keyId: "key-2" }),
    env,
    handlers(),
  );
  assert.equal(missing.status, 400);

  const unconfigured = await handleRequest(
    messagesRequest({}, "/app-attest/register", {
      keyId: "key-2",
      attestation: "cbor",
      challenge: "uuid",
    }),
    appAttestEnv, // no APP_ATTEST_ROOT_CA
    handlers(),
  );
  assert.equal(unconfigured.status, 404);
});
