import { test, expect } from "bun:test";
import {
  google,
  github,
  discord,
  defineProvider,
  startAuth,
  exchangeCode,
  completeAuth,
  randomToken,
  discoverOidc,
  validateOidcClaims,
} from "../src/index.ts";

const CREDS = { clientId: "cid", clientSecret: "secret" };

test("presets have the right endpoints + scopes", () => {
  expect(google(CREDS).authorizeUrl).toContain("accounts.google.com");
  expect(github(CREDS).scopes).toContain("user:email");
  expect(discord(CREDS).userInfoUrl).toContain("discord.com/api/users/@me");
});

test("startAuth builds an authorize URL with PKCE + state", async () => {
  const { url, state, verifier } = await startAuth(google(CREDS), {
    redirectUri: "https://app.test/cb",
  });
  const u = new URL(url);
  expect(u.origin + u.pathname).toBe("https://accounts.google.com/o/oauth2/v2/auth");
  expect(u.searchParams.get("client_id")).toBe("cid");
  expect(u.searchParams.get("redirect_uri")).toBe("https://app.test/cb");
  expect(u.searchParams.get("response_type")).toBe("code");
  expect(u.searchParams.get("scope")).toBe("openid email profile");
  expect(u.searchParams.get("state")).toBe(state);
  expect(u.searchParams.get("code_challenge")).toBeTruthy();
  expect(u.searchParams.get("code_challenge_method")).toBe("S256");
  expect(u.searchParams.get("access_type")).toBe("offline"); // provider default param
  expect(verifier.length).toBeGreaterThan(20);
});

test("randomToken is URL-safe and unique", () => {
  const a = randomToken();
  const b = randomToken();
  expect(a).not.toBe(b);
  expect(a).toMatch(/^[A-Za-z0-9_-]+$/);
});

test("exchangeCode posts the code + PKCE verifier and parses tokens", async () => {
  let captured: { url: string; body: string } | null = null;
  const fakeFetch = (async (url: string, init: RequestInit) => {
    captured = { url: String(url), body: String(init.body) };
    return new Response(JSON.stringify({ access_token: "tok", token_type: "Bearer" }), {
      headers: { "content-type": "application/json" },
    });
  }) as unknown as typeof fetch;

  const tokens = await exchangeCode(github(CREDS), {
    code: "abc",
    redirectUri: "https://app.test/cb",
    verifier: "ver123",
    fetch: fakeFetch,
  });
  expect(tokens.access_token).toBe("tok");
  expect(captured!.url).toBe("https://github.com/login/oauth/access_token");
  expect(captured!.body).toContain("code=abc");
  expect(captured!.body).toContain("code_verifier=ver123");
  expect(captured!.body).toContain("grant_type=authorization_code");
});

test("completeAuth maps the provider profile (custom provider)", async () => {
  const provider = defineProvider({
    name: "acme",
    authorizeUrl: "https://acme.test/authorize",
    tokenUrl: "https://acme.test/token",
    userInfoUrl: "https://acme.test/me",
    scopes: ["email"],
    clientId: "cid",
    clientSecret: "secret",
    mapProfile: (raw) => ({ id: String(raw.user_id), email: raw.mail as string, name: "n", raw }),
  });
  const fakeFetch = (async (url: string) => {
    if (String(url).endsWith("/token"))
      return new Response(JSON.stringify({ access_token: "t" }), {
        headers: { "content-type": "application/json" },
      });
    return new Response(JSON.stringify({ user_id: 99, mail: "x@acme.test" }), {
      headers: { "content-type": "application/json" },
    });
  }) as unknown as typeof fetch;

  const { profile } = await completeAuth(provider, {
    code: "c",
    redirectUri: "r",
    fetch: fakeFetch,
  });
  expect(profile.id).toBe("99");
  expect(profile.email).toBe("x@acme.test");
});

test("OIDC discovery enforces issuer and secure required endpoints", async () => {
  const valid = await discoverOidc("https://issuer.example/", (async () =>
    Response.json({
      issuer: "https://issuer.example",
      authorization_endpoint: "https://issuer.example/authorize",
      token_endpoint: "https://issuer.example/token",
      jwks_uri: "https://keys.example/jwks",
    })) as unknown as typeof fetch);
  expect(valid.jwks_uri).toBe("https://keys.example/jwks");
  await expect(
    discoverOidc("https://issuer.example", (async () =>
      Response.json({
        issuer: "https://attacker.example",
        authorization_endpoint: "https://issuer.example/authorize",
        token_endpoint: "http://issuer.example/token",
        jwks_uri: "https://issuer.example/jwks",
      })) as unknown as typeof fetch),
  ).rejects.toThrow("issuer mismatch");
});

test("OIDC claim conformance enforces nonce, subject, and authorized party", () => {
  const valid = {
    sub: "user-1",
    iss: "https://issuer.example",
    aud: ["client-1", "api"],
    azp: "client-1",
    exp: 200,
    iat: 100,
    nonce: "nonce-1",
  };
  expect(() => validateOidcClaims(valid, { clientId: "client-1", nonce: "nonce-1" })).not.toThrow();
  expect(() =>
    validateOidcClaims({ ...valid, nonce: "wrong" }, { clientId: "client-1", nonce: "nonce-1" }),
  ).toThrow("nonce");
  expect(() => validateOidcClaims({ ...valid, azp: "other" }, { clientId: "client-1" })).toThrow(
    "authorized party",
  );
});
