/**
 * Password-Change Database Integration Suite
 *
 * Exercises `changePasswordForUser` — the logged-in password-change flow behind
 * `POST /api/auth/change-password`. The core of the hardening: when a user
 * changes their password, EVERY existing session row for the account must be
 * deleted, so a device that was already signed in (e.g. one an attacker had
 * access through) loses access instantly. The route then issues a fresh session
 * for the current device; that re-issuing lives in the route handler, not here.
 *
 * Run with:  npx tsx tests/integration/password_change.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 { sessions, users } from "../../src/lib/schema/db";
import { changePasswordForUser, createUser, findUserByEmail } from "../../src/lib/auth/accounts";
import { hashPassword, verifyPassword } from "../../src/lib/auth/password";
import { createSessionRecord } from "../../src/lib/auth/session";
import { issueToken } 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("Password change — DB", () => {
    test("an unknown user id reports invalid", async () => {
      const outcome = await changePasswordForUser(
        "usr_nonexistent",
        "Whatever123!",
        "NewPassword123!"
      );
      expect(outcome.status).toBe("invalid");
    });

    test("a Google-only account (no password hash) reports no_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 changePasswordForUser(user.id, "Whatever123!", "NewPassword123!");
      expect(outcome.status).toBe("no_password");

      // Nothing changed for the account.
      const unchanged = await findUserByEmail(user.email ?? "");
      expect(unchanged?.passwordHash).toBe(null);
    });

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

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

      const after = await findUserByEmail(user.email ?? "");
      expect(after?.passwordHash).toBe(before?.passwordHash);
      expect(await verifyPassword("Original123!", after?.passwordHash ?? null)).toBe(true);
      expect(await verifyPassword("Replacement123!", after?.passwordHash ?? null)).toBe(false);
    });

    test("success replaces the hash: old password stops verifying, new one works", async () => {
      const user = await createUser({
        email: `${LOCAL_PART}-success@example.com`,
        passwordHash: await hashPassword("Original123!"),
      });

      const outcome = await changePasswordForUser(user.id, "Original123!", "BrandNew456!");
      expect(outcome.status).toBe("success");
      // Narrow the discriminated union so TS knows `userId` exists.
      if (outcome.status !== "success") throw new Error("expected success");
      expect(outcome.userId).toBe(user.id);

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

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

      // Three devices signed in before the password changes: two via the real
      // session-issuing helper, one inserted directly.
      await createSessionRecord(user.id, { remember: false, userAgent: "MacBook/Chrome" });
      await createSessionRecord(user.id, { remember: true, userAgent: "iPhone/Safari" });
      await db.insert(sessions).values({
        id: issueToken().hash,
        userId: user.id,
        expiresAt: new Date(Date.now() + 60_000),
      });

      const seeded = await db.select().from(sessions).where(eq(sessions.userId, user.id));
      expect(seeded.length).toBe(3);

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

      // Every old token is dead — this is the whole point of the hardening.
      const remaining = await db.select().from(sessions).where(eq(sessions.userId, user.id));
      expect(remaining.length).toBe(0);

      // The account row itself survives the rotation.
      const updated = await findUserByEmail(user.email ?? "");
      expect(updated).not.toBe(null);
    });
  });
}

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/password_change.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("Password change suite crashed:", error);
  await pool.end().catch(() => undefined);
  process.exit(1);
});
