/**
 * Account Deletion Integration Suite
 *
 * Exercises `deleteUserAccount` — the logged-in account-deletion flow behind
 * `DELETE /api/auth/delete-account`. Covers the same shape of guarantees as
 * `password_change.test.ts` (unknown user, Google-only account, wrong
 * password, success), plus the part specific to deletion: that removing the
 * user row actually cascades to every table that depends on it, while
 * `licenses` and `security_events` survive with their `userId` nulled out.
 *
 * Run with:  npx tsx tests/integration/account_deletion.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 { describe, test, expect, runAllTests } from "../e2e/runner";
import { db, isDatabaseAvailable, pool } from "../../src/lib/db";
import {
  licenses,
  line_items,
  oauth_accounts,
  projects,
  receipts,
  security_events,
  sessions,
  users,
} from "../../src/lib/schema/db";
import { createUser, deleteUserAccount, findUserByEmail } from "../../src/lib/auth/accounts";
import { hashPassword, verifyPassword } from "../../src/lib/auth/password";
import { createSessionRecord } from "../../src/lib/auth/session";
import { newId } from "../../src/lib/auth/tokens";

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

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

function registerSuites() {
  describe("Account deletion — verification", () => {
    test("an unknown user id reports invalid", async () => {
      const outcome = await deleteUserAccount("usr_nonexistent", "Whatever123!");
      expect(outcome.status).toBe("invalid");
    });

    test("a Google-only account (no password hash) deletes without a password", async () => {
      const user = await createUser({
        email: `${LOCAL_PART}-google@example.com`,
        emailVerified: true, // no passwordHash → Google-only
      });
      expect(user.passwordHash).toBe(null);

      const outcome = await deleteUserAccount(user.id, null);
      expect(outcome.status).toBe("success");

      expect(await findUserByEmail(user.email ?? "")).toBe(null);
    });

    test("a wrong password is rejected and the account survives", async () => {
      const user = await createUser({
        email: `${LOCAL_PART}-wrong@example.com`,
        passwordHash: await hashPassword("Original123!"),
      });

      const outcome = await deleteUserAccount(user.id, "NotThePassword!");
      expect(outcome.status).toBe("wrong_password");

      const still = await findUserByEmail(user.email ?? "");
      expect(still).not.toBe(null);
      expect(await verifyPassword("Original123!", still?.passwordHash ?? null)).toBe(true);
    });

    test("a missing password on a password-protected account is rejected", async () => {
      const user = await createUser({
        email: `${LOCAL_PART}-missing@example.com`,
        passwordHash: await hashPassword("Original123!"),
      });

      const outcome = await deleteUserAccount(user.id, null);
      expect(outcome.status).toBe("wrong_password");
      expect(await findUserByEmail(user.email ?? "")).not.toBe(null);
    });

    test("the correct password deletes the account", async () => {
      const user = await createUser({
        email: `${LOCAL_PART}-success@example.com`,
        passwordHash: await hashPassword("Original123!"),
      });

      const outcome = await deleteUserAccount(user.id, "Original123!");
      expect(outcome.status).toBe("success");
      expect(await findUserByEmail(user.email ?? "")).toBe(null);
    });
  });

  describe("Account deletion — cascade", () => {
    test("dependent rows cascade away; licenses and security events survive with userId nulled", async () => {
      const user = await createUser({
        email: `${LOCAL_PART}-cascade@example.com`,
        passwordHash: await hashPassword("Original123!"),
      });

      await createSessionRecord(user.id, { remember: false, userAgent: "MacBook/Chrome" });

      await db.insert(oauth_accounts).values({
        id: newId("oau"),
        userId: user.id,
        provider: "google",
        providerAccountId: `google-${RUN_ID}`,
      });

      const projectId = newId("prj");
      await db.insert(projects).values({
        id: projectId,
        userId: user.id,
        name: "Reisekosten",
      });

      const receiptId = newId("rcp");
      await db.insert(receipts).values({
        id: receiptId,
        userId: user.id,
        imageHash: `hash-${RUN_ID}`,
        projectId,
        totalAmount: "42.00",
      });

      await db.insert(line_items).values({
        id: newId("li"),
        receiptId,
        description: "Kaffee",
        price: "3.50",
      });

      await db.insert(licenses).values({
        id: newId("lic"),
        userId: user.id,
        licenseKey: `key-${RUN_ID}`,
        plan: "lifetime",
      });

      await db.insert(security_events).values({
        id: newId("evt"),
        type: "login.success",
        userId: user.id,
        email: user.email,
      });

      const outcome = await deleteUserAccount(user.id, "Original123!");
      expect(outcome.status).toBe("success");

      // Everything keyed to the user directly, plus the receipt's own child
      // row, is gone.
      expect((await db.select().from(sessions).where(eq(sessions.userId, user.id))).length).toBe(0);
      expect(
        (await db.select().from(oauth_accounts).where(eq(oauth_accounts.userId, user.id))).length
      ).toBe(0);
      expect((await db.select().from(projects).where(eq(projects.userId, user.id))).length).toBe(0);
      expect((await db.select().from(receipts).where(eq(receipts.userId, user.id))).length).toBe(0);
      expect(
        (await db.select().from(line_items).where(eq(line_items.receiptId, receiptId))).length
      ).toBe(0);

      // Billing history and the audit trail survive, disowned rather than deleted.
      const licenseRows = await db.select().from(licenses).where(eq(licenses.licenseKey, `key-${RUN_ID}`));
      expect(licenseRows.length).toBe(1);
      expect(licenseRows[0].userId).toBe(null);

      const eventRows = await db
        .select()
        .from(security_events)
        .where(eq(security_events.email, user.email ?? ""));
      expect(eventRows.length >= 1).toBe(true);
      expect(eventRows.every((row) => row.userId === null)).toBe(true);
    });
  });
}

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/account_deletion.test.ts",
        "",
      ].join("\n")
    );
    await pool.end();
    return;
  }

  // Leftovers from an interrupted earlier run would break the assertions.
  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("Account deletion suite crashed:", error);
  await pool.end().catch(() => undefined);
  process.exit(1);
});
