/**
 * CSRF Token Suite — pure logic, no database required.
 *
 * Covers the double-submit token contract in `src/lib/auth/csrf.ts`: issuance
 * (fresh, 32-byte, base64url), constant-time comparison semantics, the Origin
 * allow-list, and the client mirror in `src/lib/csrf/client.ts` (which must
 * stay in sync with the server constants it cannot import).
 */

import { describe, test, expect } from "./runner";
import {
  CSRF_COOKIE,
  CSRF_HEADER,
  csrfCookieOptions,
  isAllowedOrigin,
  issueCsrfToken,
  requireCsrf,
  tokensMatch,
  validateCsrf,
} from "../../src/lib/auth/csrf";
import { siteUrl, hostIsSiteFirstParty } from "../../src/lib/seo/site";
import {
  apiFetch,
  CSRF_COOKIE as CLIENT_CSRF_COOKIE,
  CSRF_HEADER as CLIENT_CSRF_HEADER,
} from "../../src/lib/csrf/client";

function requestWith(init?: RequestInit): Request {
  return new Request("http://localhost:3000/api/auth/logout", {
    method: "POST",
    ...init,
  });
}

describe("CSRF — token issuance", () => {
  test("tokens are unique across many issuances", () => {
    const tokens = new Set(Array.from({ length: 500 }, () => issueCsrfToken()));
    expect(tokens.size).toBe(500);
  });

  test("tokens are 32 random bytes in base64url — 43 chars, URL-safe alphabet", () => {
    const token = issueCsrfToken();
    expect(token.length).toBe(43);
    expect(/^[A-Za-z0-9_-]+$/.test(token)).toBe(true);
    expect(token.includes("+")).toBe(false);
    expect(token.includes("/")).toBe(false);
    expect(token.includes("=")).toBe(false);
  });

  test("cookie options are the documented double-submit shape", () => {
    const options = csrfCookieOptions();
    expect(options.httpOnly).toBe(false); // JS must be able to read the token
    expect(options.sameSite).toBe("lax");
    expect(options.path).toBe("/");
    expect(options.maxAge).toBe(86400);
    expect(typeof options.secure).toBe("boolean");
  });

  test("client wrapper constants mirror the server contract", () => {
    expect(CLIENT_CSRF_COOKIE).toBe(CSRF_COOKIE);
    expect(CLIENT_CSRF_HEADER).toBe(CSRF_HEADER);
    expect(CSRF_COOKIE).toBe("sr_csrf");
    expect(CSRF_HEADER).toBe("x-csrf-token");
  });
});

describe("CSRF — tokensMatch (constant-time comparison)", () => {
  test("equal tokens match", () => {
    const token = issueCsrfToken();
    expect(tokensMatch(token, token)).toBe(true);
  });

  test("same-length but different tokens never match", () => {
    const a = issueCsrfToken();
    const b = issueCsrfToken();
    expect(a === b).toBe(false);
    expect(tokensMatch(a, b)).toBe(false);
  });

  test("unequal lengths are rejected without comparing content", () => {
    expect(tokensMatch("abc", "abcd")).toBe(false);
    expect(tokensMatch("abcd", "abc")).toBe(false);
    expect(tokensMatch("", "a")).toBe(false);
    expect(tokensMatch("a", "")).toBe(false);
  });

  test("undefined on either side never matches", () => {
    const token = issueCsrfToken();
    expect(tokensMatch(undefined, undefined)).toBe(false);
    expect(tokensMatch(token, undefined)).toBe(false);
    expect(tokensMatch(undefined, token)).toBe(false);
  });

  test("empty strings compare as equal (length 0, no bytes differ)", () => {
    expect(tokensMatch("", "")).toBe(true);
  });
});

describe("CSRF — first-party host validation (OAuth return target)", () => {
  const siteHost = new URL(siteUrl).host;

  test("the site host itself is first-party", () => {
    expect(hostIsSiteFirstParty(siteHost)).toBe(true);
  });

  test("app. and admin. subdomains are first-party (dashboard/admin routing)", () => {
    expect(hostIsSiteFirstParty(`app.${siteHost}`)).toBe(true);
    expect(hostIsSiteFirstParty(`admin.${siteHost}`)).toBe(true);
    expect(hostIsSiteFirstParty(`APP.${siteHost.toUpperCase()}`)).toBe(true);
  });

  test("other hosts are NOT first-party — no open redirect through the cookie", () => {
    expect(hostIsSiteFirstParty(`evil.${siteHost}`)).toBe(false);
    expect(hostIsSiteFirstParty("app.scan-receipts.app.evil.example")).toBe(false);
    expect(hostIsSiteFirstParty("attacker.example")).toBe(false);
    expect(hostIsSiteFirstParty("scanreceipts.app")).toBe(false);
  });
});

describe("CSRF — origin allow-list", () => {
  test("a request without an Origin header is trusted (non-browser client)", () => {
    expect(isAllowedOrigin(requestWith())).toBe(true);
  });

  test("the configured site origin is allowed", () => {
    expect(isAllowedOrigin(requestWith({ headers: { origin: siteUrl } }))).toBe(true);
  });

  test("a foreign origin is rejected", () => {
    expect(
      isAllowedOrigin(requestWith({ headers: { origin: "https://evil.example" } }))
    ).toBe(false);
  });

  test("a same-host but different scheme is rejected", () => {
    const http = siteUrl.replace(/^https:/, "http:");
    if (http !== siteUrl) {
      expect(isAllowedOrigin(requestWith({ headers: { origin: http } }))).toBe(false);
    }
  });

  test("the first-party app./admin. subdomains are allowed (dashboard/admin routing)", () => {
    const host = new URL(siteUrl).host;
    const forHost = (label: string, scheme = "https") =>
      requestWith({ headers: { origin: `${scheme}://${label}.${host}` } });
    expect(isAllowedOrigin(forHost("app"))).toBe(true);
    expect(isAllowedOrigin(forHost("admin"))).toBe(true);
  });

  test("a non-routed subdomain of the site is rejected", () => {
    const host = new URL(siteUrl).host;
    expect(
      isAllowedOrigin(requestWith({ headers: { origin: `https://evil.${host}` } }))
    ).toBe(false);
  });

  test("a lookalike host that merely ends with the site domain is rejected", () => {
    const host = new URL(siteUrl).host;
    expect(
      isAllowedOrigin(requestWith({ headers: { origin: `https://app.${host}.evil.example` } }))
    ).toBe(false);
  });

  test("a malformed origin header is rejected, never crashes", () => {
    expect(isAllowedOrigin(requestWith({ headers: { origin: "not a url" } }))).toBe(false);
    expect(isAllowedOrigin(requestWith({ headers: { origin: "" } }))).toBe(false);
  });
});

describe("CSRF — validateCsrf / requireCsrf on plain Requests", () => {
  test("missing cookie and header fails the full check with a 403 csrf_failed", async () => {
    const request = requestWith();
    expect(validateCsrf(request)).toBe(false);

    const blocked = requireCsrf(request);
    expect(blocked).not.toBeNull();
    expect(blocked!.status).toBe(403);
    expect(await blocked!.json()).toEqual({ error: "csrf_failed" });
  });

  test("a matching cookie + header passes", () => {
    const token = issueCsrfToken();
    const request = requestWith({
      headers: {
        cookie: `${CSRF_COOKIE}=${token}`,
        [CSRF_HEADER]: token,
      },
    });
    expect(validateCsrf(request)).toBe(true);
    expect(requireCsrf(request)).toBeNull();
  });

  test("a mismatched header is rejected", () => {
    const request = requestWith({
      headers: {
        cookie: `${CSRF_COOKIE}=${issueCsrfToken()}`,
        [CSRF_HEADER]: issueCsrfToken(),
      },
    });
    expect(validateCsrf(request)).toBe(false);
  });

  test("a cookie without a header is rejected", () => {
    const request = requestWith({
      headers: { cookie: `${CSRF_COOKIE}=${issueCsrfToken()}` },
    });
    expect(validateCsrf(request)).toBe(false);
  });

  test("a header without a cookie is rejected", () => {
    const request = requestWith({
      headers: { [CSRF_HEADER]: issueCsrfToken() },
    });
    expect(validateCsrf(request)).toBe(false);
  });

  test("a valid token from a foreign origin is rejected by the origin check", () => {
    const token = issueCsrfToken();
    const request = requestWith({
      headers: {
        origin: "https://evil.example",
        cookie: `${CSRF_COOKIE}=${token}`,
        [CSRF_HEADER]: token,
      },
    });
    expect(validateCsrf(request)).toBe(false);
  });
});

describe("CSRF — apiFetch client wrapper", () => {
  const originalFetch = globalThis.fetch;

  test("apiFetch echoes the sr_csrf cookie into the x-csrf-token header", async () => {
    let capturedInit: RequestInit | undefined;
    (globalThis as { fetch: typeof fetch }).fetch = (input, init) => {
      capturedInit = init;
      return Promise.resolve(new Response("{}", { status: 200 }));
    };
    (globalThis as { document?: unknown }).document = {
      cookie: `${CSRF_COOKIE}=abc123; other=1`,
    };

    try {
      await apiFetch("/api/test", { method: "POST", body: "x" });
      const headers = new Headers(capturedInit?.headers);
      expect(headers.get(CSRF_HEADER)).toBe("abc123");
      expect(capturedInit?.method).toBe("POST");
      expect(capturedInit?.body).toBe("x");
    } finally {
      (globalThis as { fetch: typeof fetch }).fetch = originalFetch;
      delete (globalThis as { document?: unknown }).document;
    }
  });

  test("apiFetch preserves existing headers and adds the CSRF header", async () => {
    let capturedInit: RequestInit | undefined;
    (globalThis as { fetch: typeof fetch }).fetch = (input, init) => {
      capturedInit = init;
      return Promise.resolve(new Response("{}", { status: 200 }));
    };
    (globalThis as { document?: unknown }).document = {
      cookie: `${CSRF_COOKIE}=tok123`,
    };

    try {
      await apiFetch("/api/test", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ a: 1 }),
      });
      const headers = new Headers(capturedInit?.headers);
      expect(headers.get("Content-Type")).toBe("application/json");
      expect(headers.get(CSRF_HEADER)).toBe("tok123");
    } finally {
      (globalThis as { fetch: typeof fetch }).fetch = originalFetch;
      delete (globalThis as { document?: unknown }).document;
    }
  });

  test("apiFetch sends no CSRF header when the cookie is absent", async () => {
    let capturedInit: RequestInit | undefined;
    (globalThis as { fetch: typeof fetch }).fetch = (input, init) => {
      capturedInit = init;
      return Promise.resolve(new Response("{}", { status: 200 }));
    };
    (globalThis as { document?: unknown }).document = { cookie: "other=1" };

    try {
      await apiFetch("/api/test", { method: "POST" });
      const headers = new Headers(capturedInit?.headers);
      expect(headers.get(CSRF_HEADER)).toBe(null);
    } finally {
      (globalThis as { fetch: typeof fetch }).fetch = originalFetch;
      delete (globalThis as { document?: unknown }).document;
    }
  });
});
