/**
 * Reset Token Expiry Integration Suite (Task D)
 *
 * Verifies the "Expire reset links" hardening requirement: password-reset links
 * are only valid for a bounded time window, are single-use, and consuming one
 * invalidates every existing session for the account.
 *
 * The implementation under test lives in `src/lib/auth/accounts.ts`
 * (`issuePasswordResetLink`, `peekPasswordResetToken`,
 * `resetPasswordWithToken`) plus `PASSWORD_RESET_TTL_MS` in
 * `src/lib/auth/config.ts`. This suite only verifies — it never modifies those
 * files (accounts.ts is owned by Task C).
 *
 * Run with:  npx tsx tests/integration/reset_token_expiry.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 { password_reset_tokens, sessions, users } from "../../src/lib/schema/db";
import {
  createUser,
  findUserByEmail,
  issuePasswordResetLink,
  peekPasswordResetToken,
  resetPasswordWithToken,
} from "../../src/lib/auth/accounts";
import { hashPassword, verifyPassword } from "../../src/lib/auth/password";
import { hashToken, issueToken } from "../../src/lib/auth/tokens";
import { PASSWORD_RESET_TTL_MS, VERIFICATION_TTL_MS } from "../../src/lib/auth/config";

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

function extractToken(devLink: string | undefined): string {
  if (!devLink) {
    throw new Error(
      "No dev link returned — the mailer tried to actually send. `loadEnv` should have cleared the SMTP variables; check it is still the first import."
    );
  }
  const token = new URL(devLink).searchParams.get("token");
  if (!token) throw new Error(`No token in dev link: ${devLink}`);
  return token;
}

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("Reset token expiry — issuance & TTL window", () => {
    test("a freshly issued link is valid and expires inside the TTL window", async () => {
      const user = await createUser({
        email: `${LOCAL_PART}-ttl-window@example.com`,
        passwordHash: await hashPassword("Original123!"),
      });

      const mail = await issuePasswordResetLink(user, "en");
      const token = extractToken(mail.devLink);

      // Peeking reports it live without spending it.
      expect(await peekPasswordResetToken(token)).toBe("valid");

      // The stored row carries an expiry bounded by the configured TTL.
      const rows = await db
        .select()
        .from(password_reset_tokens)
        .where(eq(password_reset_tokens.id, hashToken(token)));
      expect(rows.length).toBe(1);

      const remainingMs = rows[0].expiresAt.getTime() - Date.now();
      expect(remainingMs).toBeGreaterThan(0);
      expect(remainingMs).toBeLessThanOrEqual(PASSWORD_RESET_TTL_MS);
      // It was issued moments ago, so the window must be ~the full TTL, not a
      // token that merely "has some time left".
      expect(remainingMs).toBeGreaterThan(PASSWORD_RESET_TTL_MS - 60_000);
    });
  });

  describe("Reset token expiry — single use", () => {
    test("consuming a valid token succeeds, installs the password, marks it consumed", async () => {
      const user = await createUser({
        email: `${LOCAL_PART}-single-use@example.com`,
        passwordHash: await hashPassword("Original123!"),
      });

      const mail = await issuePasswordResetLink(user, "en");
      const token = extractToken(mail.devLink);

      const outcome = await resetPasswordWithToken(token, "BrandNew5678!");
      expect(outcome.status).toBe("success");

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

      // The token row is marked consumed so a replay cannot work.
      const rows = await db
        .select()
        .from(password_reset_tokens)
        .where(eq(password_reset_tokens.id, hashToken(token)));
      expect(rows.length).toBe(1);
      expect(rows[0].consumedAt !== null).toBe(true);
    });

    test("a reset token cannot be replayed after being spent", async () => {
      const user = await createUser({
        email: `${LOCAL_PART}-replay@example.com`,
        passwordHash: await hashPassword("Original123!"),
      });

      const mail = await issuePasswordResetLink(user, "en");
      const token = extractToken(mail.devLink);

      expect((await resetPasswordWithToken(token, "FirstUse123!")).status).toBe("success");
      expect((await resetPasswordWithToken(token, "SecondUse123!")).status).toBe("invalid");
      expect(await peekPasswordResetToken(token)).toBe("invalid");

      // The second attempt must not have taken effect.
      const updated = await findUserByEmail(user.email ?? "");
      expect(await verifyPassword("FirstUse123!", updated?.passwordHash ?? null)).toBe(true);
      expect(await verifyPassword("SecondUse123!", updated?.passwordHash ?? null)).toBe(false);
    });
  });

  describe("Reset token expiry — elapsed tokens", () => {
    test("an expired token reads expired and cannot change the password", async () => {
      const user = await createUser({
        email: `${LOCAL_PART}-elapsed@example.com`,
        passwordHash: await hashPassword("Original123!"),
      });

      const { token, hash } = issueToken();
      await db.insert(password_reset_tokens).values({
        id: hash,
        userId: user.id,
        expiresAt: new Date(Date.now() - 60_000),
      });

      expect(await peekPasswordResetToken(token)).toBe("expired");
      expect((await resetPasswordWithToken(token, "Replacement123!")).status).toBe("expired");

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

  describe("Reset token expiry — session invalidation", () => {
    test("consuming a reset link drops every existing session for the account", async () => {
      const user = await createUser({
        email: `${LOCAL_PART}-sessions@example.com`,
        passwordHash: await hashPassword("Original123!"),
      });

      await db.insert(sessions).values({
        id: issueToken().hash,
        userId: user.id,
        expiresAt: new Date(Date.now() + 60_000),
      });
      await db.insert(sessions).values({
        id: issueToken().hash,
        userId: user.id,
        expiresAt: new Date(Date.now() + 60_000),
      });

      // Precondition: the two sessions really exist.
      const before = await db.select().from(sessions).where(eq(sessions.userId, user.id));
      expect(before.length).toBe(2);

      const mail = await issuePasswordResetLink(user, "en");
      const outcome = await resetPasswordWithToken(extractToken(mail.devLink), "AnotherOne901!");
      expect(outcome.status).toBe("success");

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

  describe("Reset token expiry — configuration constants", () => {
    test("PASSWORD_RESET_TTL_MS is 15–60 minutes and below VERIFICATION_TTL_MS", () => {
      // The requirement: reset links should be valid ~15–60 minutes, and reset
      // links must live far shorter than confirmation links.
      expect(PASSWORD_RESET_TTL_MS).toBeGreaterThanOrEqual(15 * 60 * 1000);
      expect(PASSWORD_RESET_TTL_MS).toBeLessThanOrEqual(60 * 60 * 1000);
      expect(PASSWORD_RESET_TTL_MS).toBeLessThan(VERIFICATION_TTL_MS);
    });
  });
}

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

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