import { test, expect } from "bun:test";
import { withSecurityHeaders, isWebSocketOriginAllowed, resolveRequestUrl } from "../src/index.ts";

const req = (headers: Record<string, string> = {}) => new Request("https://x/", { headers });

function scriptSrc(csp: string): string {
  return csp
    .split(";")
    .map((s) => s.trim())
    .find((s) => s.startsWith("script-src"))!;
}

test("CSP nonce is added to script-src and drops unsafe-inline", () => {
  const res = withSecurityHeaders(req(), new Response("x"), "development", undefined, "ABC123");
  const directive = scriptSrc(res.headers.get("content-security-policy")!);
  expect(directive).toContain("'nonce-ABC123'");
  expect(directive).not.toContain("'unsafe-inline'");
});

test("without a nonce, dev script-src keeps unsafe-inline (for HMR)", () => {
  const res = withSecurityHeaders(req(), new Response("x"), "development");
  expect(scriptSrc(res.headers.get("content-security-policy")!)).toContain("'unsafe-inline'");
});

test("CORS credentials + origin:* is refused (credentials dropped)", () => {
  const res = withSecurityHeaders(
    req({ origin: "https://evil.test" }),
    new Response("x"),
    "production",
    {
      cors: { enabled: true, origin: "*", credentials: true },
    },
  );
  expect(res.headers.get("access-control-allow-credentials")).toBeNull();
});

test("production sets HSTS + strict CSP", () => {
  const res = withSecurityHeaders(req(), new Response("x"), "production");
  expect(res.headers.get("strict-transport-security")).toContain("max-age=");
  expect(res.headers.get("content-security-policy")).toContain("default-src 'self'");
});

test("permissions policy overrides merge with restrictive defaults", () => {
  const res = withSecurityHeaders(req(), new Response("x"), "development", {
    permissionsPolicy: { camera: ["self"] },
  });
  const policy = res.headers.get("permissions-policy")!;
  expect(policy).toContain("camera=(self)");
  expect(policy).toContain("microphone=()");
});

test("isWebSocketOriginAllowed blocks cross-site WS (CSWSH), allows same-origin", () => {
  const wsReq = (origin: string | null, host: string) =>
    new Request("http://x/realtime/c", {
      headers: origin ? { origin, host } : { host },
    });
  expect(isWebSocketOriginAllowed(wsReq("http://app.test", "app.test"))).toBe(true); // same-origin
  expect(isWebSocketOriginAllowed(wsReq("http://evil.test", "app.test"))).toBe(false); // cross-site
  expect(isWebSocketOriginAllowed(wsReq(null, "app.test"))).toBe(true); // native client, no cookies
  // Explicit CORS allowlist opens a cross-origin WS.
  expect(
    isWebSocketOriginAllowed(wsReq("http://other.test", "app.test"), {
      cors: { enabled: true, origin: "http://other.test" },
    }),
  ).toBe(true);
});

test("resolveRequestUrl honors standard Forwarded headers behind trusted proxies", () => {
  const request = new Request("http://127.0.0.1:3000/path", {
    headers: { forwarded: 'for=192.0.2.1;proto=https;host="workroot.in"' },
  });
  expect(resolveRequestUrl(request, true).href).toBe("https://workroot.in/path");
  expect(resolveRequestUrl(request, false).href).toBe("http://127.0.0.1:3000/path");
});

test("default permissions policy does not emit unsupported unload", () => {
  const res = withSecurityHeaders(req(), new Response("x"), "development");
  expect(res.headers.get("permissions-policy") ?? "").not.toContain("unload=");
});
