/**
 * Security Events Integration Suite
 *
 * Exercises the audit-log foundation against a real Postgres: events written
 * through `logSecurityEvent` come back through `listRecentSecurityEvents` with
 * the right type/email, raw IPs are never persisted (only their 64-hex SHA-256
 * digest), jsonb metadata round-trips, and a bogus user id (FK violation) is
 * swallowed instead of throwing — logging must never break a request.
 *
 * Run with:  node --import tsx tests/integration/security_events.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 { security_events, users } from "../../src/lib/schema/db";
import { createUser } from "../../src/lib/auth/accounts";
import {
  logSecurityEvent,
  listRecentSecurityEvents,
  SecurityEventType,
} from "../../src/lib/auth/securityEvents";

/** Namespaced so cleanup can never touch a real account. */
const RUN_ID = Date.now().toString(36);
const LOCAL_PART = `authtest-${RUN_ID}`;
const BASE_EMAIL = `${LOCAL_PART}@gmail.com`;
const PLAINTEXT_IP = "203.0.113.7";
/** Unique marker in metadata; scopes event cleanup to exactly this run. */
const MARKER = `security-events-${RUN_ID}`;

/** Events written by this suite, so cleanup can delete them explicitly by id. */
const createdEventIds: string[] = [];

async function cleanup() {
  // The suite's own event rows, scoped by the unique metadata marker.
  if (createdEventIds.length > 0) {
    for (const id of createdEventIds) {
      await db.delete(security_events).where(eq(security_events.id, id)).catch(() => undefined);
    }
    createdEventIds.length = 0;
  }
  // Belt and suspenders: anything this run left behind (e.g. a marker that did
  // not round-trip) is swept by the same email namespace the fixtures use.
  await db
    .delete(security_events)
    .where(like(security_events.email, "authtest-%"))
    .catch(() => undefined);
  // Fixture users; the security_events.user_id FK is ON DELETE SET NULL, which
  // is why the event rows above are deleted first.
  await db.delete(users).where(like(users.emailKey, "authtest-%")).catch(() => undefined);
}

function registerSuites() {
  describe("Security events — persistence and privacy", () => {
    test("a logged event appears with the right type/email and hashed IP", async () => {
      const user = await createUser({
        email: BASE_EMAIL,
        name: "Security Event Test",
        passwordHash: null,
      });

      await logSecurityEvent({
        type: SecurityEventType.LOGIN_FAILED,
        userId: user.id,
        email: user.email ?? BASE_EMAIL,
        ip: PLAINTEXT_IP,
        userAgent: "security-events-test/1.0",
        metadata: { marker: MARKER, reason: "bad_password", attempts: 3 },
      });

      const rows = await db
        .select()
        .from(security_events)
        .where(eq(security_events.userId, user.id));
      expect(rows.length).toBe(1);

      const row = rows[0];
      createdEventIds.push(row.id);

      expect(row.type).toBe(SecurityEventType.LOGIN_FAILED);
      expect(row.email).toBe(BASE_EMAIL);
      expect(row.userId).toBe(user.id);
      expect(row.userAgent).toBe("security-events-test/1.0");

      // Raw IP is never stored: the column holds the 64-hex SHA-256 digest.
      expect(row.ipHash).toMatch(/^[0-9a-f]{64}$/);
      expect(row.ipHash).not.toBe(PLAINTEXT_IP);
      expect(JSON.stringify(row)).not.toContain(PLAINTEXT_IP);

      // jsonb metadata round-trips as a real object.
      expect(row.metadataJson).toEqual({ marker: MARKER, reason: "bad_password", attempts: 3 });

      // And the read API surfaces it again.
      const recent = await listRecentSecurityEvents(50, SecurityEventType.LOGIN_FAILED);
      const hit = recent.find((e) => e.email === BASE_EMAIL && e.type === SecurityEventType.LOGIN_FAILED);
      expect(hit).toBeDefined();
      expect(hit?.metadata).toEqual({ marker: MARKER, reason: "bad_password", attempts: 3 });
    });

    test("logSecurityEvent never throws on a bogus userId (FK violation)", async () => {
      const bogusId = `usr_missing_${RUN_ID}`;

      let threw: unknown = null;
      try {
        await logSecurityEvent({
          type: SecurityEventType.SIGNUP_FAILED,
          userId: bogusId,
          email: `${LOCAL_PART}-missing@example.com`,
          ip: PLAINTEXT_IP,
          metadata: { marker: MARKER, reason: "bogus-user" },
        });
      } catch (error) {
        threw = error;
      }

      // The FK error must be swallowed, never propagated to the caller.
      expect(threw).toBe(null);

      // And no row may exist for the nonexistent user.
      const rows = await db
        .select()
        .from(security_events)
        .where(eq(security_events.userId, bogusId));
      expect(rows.length).toBe(0);
    });

    test("an event without a user or IP is still stored with nulls", async () => {
      await logSecurityEvent({
        type: SecurityEventType.PASSWORD_RESET_REQUESTED,
        email: `${LOCAL_PART}-nouser@example.com`,
        metadata: { marker: MARKER, reason: "account_not_found" },
      });

      const rows = await db
        .select()
        .from(security_events)
        .where(eq(security_events.email, `${LOCAL_PART}-nouser@example.com`));
      expect(rows.length).toBe(1);

      const row = rows[0];
      createdEventIds.push(row.id);
      expect(row.userId).toBeNull();
      expect(row.ipHash).toBeNull();
      expect(row.metadataJson).toEqual({ marker: MARKER, reason: "account_not_found" });
    });
  });
}

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",
        "    node --import tsx tests/integration/security_events.test.ts",
        "",
      ].join("\n")
    );
    await pool.end();
    return;
  }

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