/**
 * CSRF Flow Integration Suite
 *
 * End-to-end double-submit CSRF check against a real handler: a logged-in user
 * exists (fixture account + session row), and `POST /api/auth/logout` is driven
 * with and without the matching `sr_csrf` cookie / `x-csrf-token` header.
 *
 * Run with:  npx tsx tests/integration/csrf_flow.test.ts
 *
 * Requires a reachable DATABASE_URL with the migrations applied
 * (`docker compose up -d postgres && npm run db:push`). Without a database the
 * script reports a skip and exits 0 rather than pretending to have passed.
 */

// Keep first: populates DATABASE_URL before the database module below reads it.
import "./loadEnv";

import { eq, like } from "drizzle-orm";
import { NextRequest } from "next/server";
import { describe, test, expect, beforeAll, runAllTests } from "../e2e/runner";
import { db, isDatabaseAvailable, pool } from "../../src/lib/db";
import { sessions, users } from "../../src/lib/schema/db";
import { createUser } from "../../src/lib/auth/accounts";
import { hashPassword } from "../../src/lib/auth/password";
import { issueToken } from "../../src/lib/auth/tokens";
import { POST as logoutPOST } from "../../src/app/api/auth/logout/route";
import {
  CSRF_COOKIE,
  CSRF_HEADER,
  issueCsrfToken,
} from "../../src/lib/auth/csrf";

/** Namespaced so cleanup can never touch a real account. */
const RUN_ID = Date.now().toString(36);
const LOCAL_PART = `authtest-csrf-${RUN_ID}`;
const BASE_EMAIL = `${LOCAL_PART}@gmail.com`;

async function cleanup() {
  // Children cascade from users; the LIKE keeps this scoped to authtest rows.
  await db.delete(users).where(like(users.emailKey, `authtest-%`));
}

/** A NextRequest — the same object shape Next.js passes to route handlers. */
function logoutRequest(headers?: HeadersInit): NextRequest {
  return new NextRequest("http://localhost:3000/api/auth/logout", {
    method: "POST",
    ...(headers ? { headers } : {}),
  });
}

function registerSuites() {
  describe("CSRF flow — double-submit guard on POST /api/auth/logout", () => {
    beforeAll(async () => {
      const user = await createUser({
        email: BASE_EMAIL,
        name: "CSRF Flow Test",
        passwordHash: await hashPassword("Sicher1234!"),
      });
      await db.insert(sessions).values({
        id: issueToken().hash,
        userId: user.id,
        expiresAt: new Date(Date.now() + 60_000),
      });
    });

    test("the fixture user and session exist", async () => {
      const userRows = await db.select().from(users).where(eq(users.emailKey, BASE_EMAIL));
      expect(userRows.length).toBe(1);
      const sessionRows = await db
        .select()
        .from(sessions)
        .where(eq(sessions.userId, userRows[0].id));
      expect(sessionRows.length).toBe(1);
    });

    test("a logout without a CSRF token is blocked with 403 csrf_failed", async () => {
      const response = await logoutPOST(logoutRequest());
      expect(response.status).toBe(403);
      expect(await response.json()).toEqual({ error: "csrf_failed" });
    });

    test("a logout with a matching cookie + header succeeds", async () => {
      const token = issueCsrfToken();
      // This is the one call in the suite that reaches the real route handler
      // past the CSRF guard, so it hits `destroySession()` -> `cookies()`.
      // Outside actual Next.js request handling there is no request-scoped
      // AsyncLocalStorage for `cookies()` to read, so it throws; the route
      // already treats that as non-fatal (logout must not "stick" a user
      // signed in) and logs it via console.error. That's expected only in
      // this direct-handler test harness, so it's muted here rather than in
      // the route, which must keep logging real failures in production.
      const originalConsoleError = console.error;
      console.error = () => {};
      let response: Response;
      try {
        response = await logoutPOST(
          logoutRequest({
            cookie: `${CSRF_COOKIE}=${token}`,
            [CSRF_HEADER]: token,
          })
        );
      } finally {
        console.error = originalConsoleError;
      }
      expect(response.status).toBe(200);
      const body = (await response.json()) as { status?: string };
      expect(body.status).toBe("signed_out");
    });

    test("a logout with a mismatched header is blocked with 403", async () => {
      const response = await logoutPOST(
        logoutRequest({
          cookie: `${CSRF_COOKIE}=${issueCsrfToken()}`,
          [CSRF_HEADER]: issueCsrfToken(),
        })
      );
      expect(response.status).toBe(403);
      expect(await response.json()).toEqual({ error: "csrf_failed" });
    });

    test("a logout with a cookie but no header is blocked with 403", async () => {
      const response = await logoutPOST(
        logoutRequest({
          cookie: `${CSRF_COOKIE}=${issueCsrfToken()}`,
        })
      );
      expect(response.status).toBe(403);
      expect(await response.json()).toEqual({ error: "csrf_failed" });
    });
  });
}

async function main() {
  if (!(await isDatabaseAvailable())) {
    console.log(
      [
        "",
        "  SKIPPED — no database reachable at DATABASE_URL.",
        "",
        "  Start one and apply the schema, then re-run:",
        "    docker compose up -d postgres",
        "    npm run db:push",
        "    npx tsx tests/integration/csrf_flow.test.ts",
        "",
      ].join("\n")
    );
    await pool.end();
    return;
  }

  // Leftovers from an interrupted earlier run would break the fixture.
  await cleanup();
  registerSuites();

  let passed = false;
  try {
    passed = await runAllTests();
  } finally {
    await cleanup();
    await pool.end();
  }

  if (!passed) process.exit(1);
}

main().catch(async (error) => {
  console.error("CSRF flow suite crashed:", error);
  await pool.end().catch(() => undefined);
  process.exit(1);
});
