import { expect, test } from "bun:test";
import { createRemoteJwks, verifyJwtWithJwks } from "../src/index.ts";

function base64url(value: Uint8Array): string {
  let binary = "";
  for (const byte of value) binary += String.fromCharCode(byte);
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}

async function rsaKey(kid: string) {
  const pair = (await crypto.subtle.generateKey(
    {
      name: "RSASSA-PKCS1-v1_5",
      modulusLength: 2048,
      publicExponent: new Uint8Array([1, 0, 1]),
      hash: "SHA-256",
    },
    true,
    ["sign", "verify"],
  )) as CryptoKeyPair;
  const jwk = await crypto.subtle.exportKey("jwk", pair.publicKey);
  return { pair, jwk: { ...jwk, kid, alg: "RS256", use: "sig" } };
}

async function sign(privateKey: CryptoKey, kid: string, claims: Record<string, unknown>) {
  const encoder = new TextEncoder();
  const header = base64url(encoder.encode(JSON.stringify({ alg: "RS256", typ: "JWT", kid })));
  const payload = base64url(encoder.encode(JSON.stringify(claims)));
  const input = `${header}.${payload}`;
  const signature = await crypto.subtle.sign(
    "RSASSA-PKCS1-v1_5",
    privateKey,
    encoder.encode(input),
  );
  return `${input}.${base64url(new Uint8Array(signature))}`;
}

test("remote JWKS verifies RS256 claims and refreshes immediately for key rotation", async () => {
  const first = await rsaKey("key-1");
  const second = await rsaKey("key-2");
  const documents = [{ keys: [first.jwk] }, { keys: [second.jwk] }];
  let fetches = 0;
  const jwks = createRemoteJwks("https://issuer.example/jwks", {
    now: () => 0,
    cacheTtlMs: 60_000,
    fetch: (async () =>
      Response.json(
        documents[Math.min(fetches++, documents.length - 1)],
      )) as unknown as typeof fetch,
  });
  const tokenOne = await sign(first.pair.privateKey, "key-1", {
    sub: "user-1",
    iss: "https://issuer.example",
    aud: "client-1",
    iat: 100,
    exp: 200,
  });
  expect(
    (
      await verifyJwtWithJwks(tokenOne, jwks, {
        issuer: "https://issuer.example",
        audience: "client-1",
        now: 150,
      })
    ).sub,
  ).toBe("user-1");
  expect(fetches).toBe(1);

  const tokenTwo = await sign(second.pair.privateKey, "key-2", {
    sub: "user-2",
    iss: "https://issuer.example",
    aud: "client-1",
    exp: 200,
  });
  expect((await verifyJwtWithJwks(tokenTwo, jwks, { now: 150 })).sub).toBe("user-2");
  expect(fetches).toBe(2);
  expect(jwks.stats()).toMatchObject({ fetches: 2, hits: 2, keys: 1 });
  await expect(verifyJwtWithJwks(tokenOne, jwks, { now: 150 })).rejects.toThrow("Unknown key");
});

test("remote JWKS rejects insecure, oversized, and incompatible key sets", async () => {
  expect(() => createRemoteJwks("http://issuer.example/jwks")).toThrow("HTTPS");
  const oversized = createRemoteJwks("https://issuer.example/jwks", {
    maxBytes: 10,
    fetch: (async () => Response.json({ keys: [] })) as unknown as typeof fetch,
  });
  await expect(oversized.refresh()).rejects.toThrow("too large");

  const incompatible = createRemoteJwks("https://issuer.example/jwks", {
    fetch: (async () =>
      Response.json({
        keys: [{ kid: "ec", kty: "EC", alg: "ES256" }],
      })) as unknown as typeof fetch,
  });
  await incompatible.refresh();
  await expect(incompatible.resolve("ec", "RS256")).rejects.toThrow("Unknown key");
});
