/**
 * Cookie Flags Suite
 *
 * Every auth cookie must be hardened the same way: httpOnly, a deliberate
 * SameSite policy, Secure whenever the app serves TLS, path "/", and a sane
 * lifetime. All writers (session, guest, OAuth handshake, CSRF) go through the
 * single `cookieSecurityOptions` builder, so one test of the builder plus the
 * public option shapes covers every cookie the auth system can write. The
 * CSRF cookie is the sole deliberate exception: it overrides httpOnly to
 * false so client JS can read it, everything else stays shared.
 * Pure logic only — no server, no database.
 *
 * Run standalone:  npx tsx tests/e2e/cookie_flags.test.ts
 */

import { describe, test, expect, runAllTests } from "./runner";
import { cookieSecurityOptions } from "../../src/lib/auth/config";
import { sessionCookieOptions } from "../../src/lib/auth/session";
import { applyGuestCookie, GUEST_COOKIE } from "../../src/lib/auth/guest";
import { csrfCookieOptions } from "../../src/lib/auth/csrf";
import { NextResponse } from "next/server";

/** Must stay in lockstep with the writers: 10 min for the OAuth handshake, 1y for guests. */
const HANDSHAKE_MAX_AGE_SECONDS = 600;
const GUEST_MAX_AGE_SECONDS = 60 * 60 * 24 * 365;
const CSRF_MAX_AGE_SECONDS = 86400;

describe("Cookie flags — central builder", () => {
  test("always pins httpOnly, path / and a deliberate SameSite", () => {
    const base = cookieSecurityOptions();
    expect(base.httpOnly).toBe(true);
    expect(base.path).toBe("/");
    expect(base.sameSite).toBe("lax");
  });

  test("the flags survive an override — extras cannot drop the defaults", () => {
    const withMaxAge = cookieSecurityOptions({ maxAge: HANDSHAKE_MAX_AGE_SECONDS });
    expect(withMaxAge.httpOnly).toBe(true);
    expect(withMaxAge.path).toBe("/");
    expect(withMaxAge.sameSite).toBe("lax");
    expect(withMaxAge.maxAge).toBe(HANDSHAKE_MAX_AGE_SECONDS);
  });

  test("secure follows the environment — production flips it on", () => {
    const options = cookieSecurityOptions();
    // `isProduction` is read from NODE_ENV at module load; Secure must be on
    // exactly when the app runs in production (TLS) and off in local dev.
    expect(options.secure).toBe(process.env.NODE_ENV === "production");
  });

  test("a sameSite override is honoured — lax stays the default", () => {
    expect(cookieSecurityOptions().sameSite).toBe("lax");
    expect(cookieSecurityOptions({ sameSite: "strict" }).sameSite).toBe("strict");
    expect(cookieSecurityOptions({ sameSite: "none" }).sameSite).toBe("none");
  });
});

describe("Cookie flags — session cookie", () => {
  test("sessionCookieOptions carries the hardening set plus the expiry", () => {
    const expiresAt = new Date(Date.now() + 60 * 60 * 1000);
    const options = sessionCookieOptions(expiresAt);
    expect(options.httpOnly).toBe(true);
    expect(options.sameSite).toBe("lax");
    expect(options.path).toBe("/");
    expect(options.secure).toBe(process.env.NODE_ENV === "production");
    expect(options.expires).toBe(expiresAt);
  });
});

describe("Cookie flags — OAuth handshake", () => {
  test("the oauth options include the maxAge when one is passed", () => {
    const options = cookieSecurityOptions({ maxAge: HANDSHAKE_MAX_AGE_SECONDS });
    expect(options.maxAge).toBe(HANDSHAKE_MAX_AGE_SECONDS);
    expect(options.httpOnly).toBe(true);
    expect(options.path).toBe("/");
    expect(options.sameSite).toBe("lax");
  });
});

describe("Cookie flags — guest cookie", () => {
  test("applyGuestCookie writes a fully hardened one-year cookie", () => {
    const response = NextResponse.json({});
    applyGuestCookie(response, { bucket: "guest_test", cookieValue: "guest_test" });

    const cookie = response.cookies.get(GUEST_COOKIE);
    expect(cookie?.httpOnly).toBe(true);
    expect(cookie?.sameSite).toBe("lax");
    expect(cookie?.path).toBe("/");
    expect(cookie?.maxAge).toBe(GUEST_MAX_AGE_SECONDS);
    expect(cookie?.secure).toBe(process.env.NODE_ENV === "production");
  });

  test("applyGuestCookie leaves the response untouched when there is nothing to persist", () => {
    const response = NextResponse.json({});
    const out = applyGuestCookie(response, { bucket: "guest_test", cookieValue: null });
    expect(out.cookies.get(GUEST_COOKIE)).toBeUndefined();
  });
});

describe("Cookie flags — CSRF double-submit cookie", () => {
  test("csrfCookieOptions goes through the shared builder, with httpOnly deliberately off", () => {
    const options = csrfCookieOptions();
    // httpOnly must be false here — client JS has to read this one to echo it
    // as a header — everything else still comes from cookieSecurityOptions.
    expect(options.httpOnly).toBe(false);
    expect(options.sameSite).toBe("lax");
    expect(options.path).toBe("/");
    expect(options.maxAge).toBe(CSRF_MAX_AGE_SECONDS);
    expect(options.secure).toBe(process.env.NODE_ENV === "production");
  });

  test("only httpOnly diverges from the shared builder's defaults", () => {
    const shared = cookieSecurityOptions({ maxAge: CSRF_MAX_AGE_SECONDS });
    const csrf = csrfCookieOptions();
    expect(csrf.sameSite).toBe(shared.sameSite);
    expect(csrf.path).toBe(shared.path);
    expect(csrf.secure).toBe(shared.secure);
    expect(csrf.maxAge).toBe(shared.maxAge);
    expect(csrf.httpOnly).not.toBe(shared.httpOnly);
  });
});

async function main() {
  const passed = await runAllTests();
  if (!passed) process.exit(1);
}

main().catch((error) => {
  console.error("Cookie flags suite crashed:", error);
  process.exit(1);
});
