#!/usr/bin/env node
/**
 * Verification that every auth cookie the app issues carries the hardened
 * attributes: HttpOnly + SameSite=Lax (+ Secure in production).
 *
 * Runs under plain Node — no build step:
 *   node scripts/verify_cookies.mjs
 *
 * Three layers:
 *   1. SOURCE assertion on the single source of truth — `cookieSecurityOptions()`
 *      in src/lib/auth/config.ts must declare httpOnly / sameSite / secure /
 *      path. Every cookie writer below is checked only for *delegating* to
 *      this helper (or to sessionCookieOptions, which itself delegates to
 *      it), not for re-declaring the attributes inline — the attributes only
 *      live in one place, so re-checking them at each call site just goes
 *      stale the next time a call site is refactored to delegate instead of
 *      inlining.
 *   2. DELEGATION assertions — session.ts, guest.ts and the Google OAuth
 *      handshake/callback routes must each go through the shared helper, and
 *      no cookie writer in the auth stack may set attributes without it.
 *   3. SERIALIZATION assertion — feed the exact attribute values the helper
 *      produces through Next's cookie serializer (NextResponse) and confirm
 *      the resulting Set-Cookie string contains `HttpOnly` and `SameSite=Lax`.
 */

import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";

const root = join(dirname(fileURLToPath(import.meta.url)), "..");

let failures = 0;
let checks = 0;

function check(name, condition, detail = "") {
  checks += 1;
  if (condition) {
    console.log(`  PASS  ${name}`);
  } else {
    failures += 1;
    console.error(`  FAIL  ${name}${detail ? ` — ${detail}` : ""}`);
  }
}

const read = (rel) => readFileSync(join(root, rel), "utf8");

// ---------------------------------------------------------------------------
// 1. Source-level attribute assertion on the single shared helper
// ---------------------------------------------------------------------------
console.log("\n[1] Core helper — src/lib/auth/config.ts (cookieSecurityOptions)");
{
  const src = read("src/lib/auth/config.ts");
  const fn = src.slice(src.indexOf("export function cookieSecurityOptions"));
  check("cookieSecurityOptions declares httpOnly: true", /\bhttpOnly:\s*true\b/.test(fn));
  check("cookieSecurityOptions declares sameSite: \"lax\"", /sameSite:\s*"lax"/.test(fn));
  check("cookieSecurityOptions declares secure: isProduction", /\bsecure:\s*isProduction\b/.test(fn));
  check("cookieSecurityOptions declares path: \"/\"", /path:\s*"\/"/.test(fn));
}

console.log("\n[2] Session cookie — src/lib/auth/session.ts (sessionCookieOptions)");
{
  const src = read("src/lib/auth/session.ts");
  check("sessionCookieOptions delegates to cookieSecurityOptions", /export function sessionCookieOptions[\s\S]*?cookieSecurityOptions\(/.test(src));
  check("session cookie is set through sessionCookieOptions (single source)", /cookieStore\.set\(SESSION_COOKIE,\s*token,\s*sessionCookieOptions\(expiresAt\)\)/.test(src));
  check("no other raw session-cookie set without the helper", (src.match(/cookieStore\.set\(/g) || []).length === 1);
}

console.log("\n[3] Guest cookie — src/lib/auth/guest.ts (applyGuestCookie)");
{
  const src = read("src/lib/auth/guest.ts");
  const block = src.slice(src.indexOf("response.cookies.set"));
  check("guest cookie is set via cookieSecurityOptions", /response\.cookies\.set\(\s*GUEST_COOKIE,\s*ctx\.cookieValue,\s*cookieSecurityOptions\(/.test(block));
}

console.log("\n[4] OAuth handshake cookies — src/app/api/auth/google/route.ts");
{
  const src = read("src/app/api/auth/google/route.ts");
  check("oauth state/verifier cookies are set via cookieSecurityOptions", /cookieSecurityOptions\(\{[^}]*\}\)/.test(src));
  check("both handshake cookies reuse the same options object", (src.match(/response\.cookies\.set\((OAUTH_STATE_COOKIE|OAUTH_VERIFIER_COOKIE),\s*\w+,\s*cookieOptions\)/g) || []).length === 2);
}

console.log("\n[5] OAuth session cookie — src/app/api/auth/google/callback/route.ts");
{
  const src = read("src/app/api/auth/google/callback/route.ts");
  // The callback sets the session cookie through the same shared helper, so the
  // attribute source is config.ts (asserted above). Confirm the delegation.
  check(
    "callback sets the session cookie via sessionCookieOptions",
    /response\.cookies\.set\(SESSION_COOKIE,\s*token,\s*sessionCookieOptions\(expiresAt\)\)/.test(src)
  );
}

console.log("\n[6] No cookie writer in the auth stack bypasses the shared helper");
{
  // Any `.cookies.set(` in the auth stack must be immediately followed by a
  // call to cookieSecurityOptions(...) or sessionCookieOptions(...) as its
  // options argument — never an inline object literal.
  const files = [
    "src/lib/auth/session.ts",
    "src/lib/auth/guest.ts",
    "src/app/api/auth/google/route.ts",
    "src/app/api/auth/google/callback/route.ts",
  ];
  for (const rel of files) {
    const src = read(rel);
    const setCalls = src.match(/\.cookies\.set\([^;]*?\)(?=;|\n)/gs) || [];
    const bypassing = setCalls.filter((c) => !/(cookieSecurityOptions|sessionCookieOptions|cookieOptions)\s*\(/.test(c) && !c.includes("cookieOptions)"));
    check(`${rel}: every .cookies.set() options arg comes from the shared helper`, bypassing.length === 0, bypassing.join(" | "));
  }
}

// ---------------------------------------------------------------------------
// 3. Serialization assertion (NextResponse + the exact attribute values)
// ---------------------------------------------------------------------------
console.log("\n[7] Serialized Set-Cookie contains HttpOnly and SameSite=Lax");
{
  // Next ships its server entry as CJS; createRequire gives us the same
  // resolution plain `require('next/server')` uses.
  const { createRequire } = await import("node:module");
  const require = createRequire(import.meta.url);
  const { NextResponse } = require("next/server");
  const res = NextResponse.json({ ok: true });
  res.cookies.set("sr_session", "test-token", {
    httpOnly: true,
    sameSite: "lax",
    secure: false, // dev — production flips via isProduction
    path: "/",
    expires: new Date(Date.now() + 3_600_000),
  });
  const header = res.headers.get("set-cookie") || "";
  check("Set-Cookie header present", header.length > 0);
  check("contains HttpOnly", /\bHttpOnly\b/i.test(header), header);
  check("contains SameSite (case-insensitive)", /SameSite=Lax/i.test(header), header);
  check("contains Path=/", /Path=\//i.test(header), header);
  check("session cookie is not visible to JS", !/\bHttpOnly=false\b/i.test(header));
}

// ---------------------------------------------------------------------------
console.log(`\n${checks} checks, ${failures} failure(s)`);
if (failures > 0) {
  console.error("COOKIE VERIFICATION FAILED");
  process.exit(1);
}
console.log("COOKIE VERIFICATION PASSED");
