// Tests for the God-Mode audit trail + the 2FA-aware super-admin guard.
//
// next/headers and @/lib/auth are module-mocked ONCE at top level, sharing a
// mutable `fakeSession` / `fakeHeaders`, so both the audit helper and the
// super-admin guard can be driven deterministically outside a request scope.
// This file lives in src/lib/admin (not src/lib/auth) on purpose: admin.test.ts
// in src/lib/auth imports the REAL `auth`, so a @/lib/auth mock there would
// break it. The only sibling here (impersonation.test.ts) tests a pure helper
// and never touches the mocked modules, so the mock can't leak destructively.
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
import { env } from "@/env";
import { adminAuditLog } from "@/lib/db/admin-schema";

let fakeSession: {
  user: { id: string; email: string; twoFactorEnabled?: boolean };
} | null = null;
const fakeHeaders = new Headers();

mock.module("server-only", () => ({}));
mock.module("next/headers", () => ({
  headers: async () => fakeHeaders,
}));
mock.module("@/lib/auth", () => ({
  auth: { api: { getSession: async () => fakeSession } },
}));

// listAuditLog is gated on `audit:read` (Task 9 — it had NO gate before).
// Mocked here (rather than exercising the real DB-backed sys-permissions gate)
// so the DB-only test block below stays focused on the audit-trail behaviour;
// the gate's own pure logic is unit-tested in sys-permissions.test.ts.
//
// NOTE: `bun test <dir>` recurses into subdirectories, so the src/lib/admin
// bucket (test-all.ts) shares ONE process with every admin/**/*.test.ts file;
// whichever mock.module("@/lib/auth/sys-permissions", …) registers LAST wins
// for all of them. org-actions.ts also needs `getSysOperatorSession` — export
// it here too (even though audit.ts itself doesn't call it) so this mock can
// never clobber a sibling file that does.
const assertSysPermission = mock(async (_perm: string) => {});
const getSysOperatorSession = async () => ({
  userId: "admin1",
  email: "admin@example.invalid",
  kind: "admin-sys" as const,
  twoFactorEnabled: true,
});
mock.module("@/lib/auth/sys-permissions", () => ({
  assertSysPermission,
  getSysOperatorSession,
}));

describe("admin_audit_log schema", () => {
  test("exposes every documented column", () => {
    const cols = Object.fromEntries(
      Object.values(adminAuditLog).map((c) => [
        (c as { name: string }).name,
        c,
      ]),
    );
    for (const name of [
      "id",
      "actor_id",
      "actor_email",
      "action",
      "target_type",
      "target_id",
      "target_label",
      "metadata",
      "ip_address",
      "created_at",
    ]) {
      expect(cols[name], `missing column ${name}`).toBeDefined();
    }
    // Snapshot columns that must never be null.
    expect((adminAuditLog.actorEmail as { notNull: boolean }).notNull).toBe(
      true,
    );
    expect((adminAuditLog.action as { notNull: boolean }).notNull).toBe(true);
    expect((adminAuditLog.targetType as { notNull: boolean }).notNull).toBe(
      true,
    );
  });
});

describe("assertSuperAdminWith2FA", () => {
  const listed = (env.SUPER_ADMIN_EMAILS ?? "")
    .split(",")
    .map((e) => e.trim())
    .filter(Boolean);

  test("rejects a non-super-admin", async () => {
    const { assertSuperAdminWith2FA, getSuperAdminSession } = await import(
      "@/lib/auth/super-admin"
    );
    fakeSession = {
      user: {
        id: "u1",
        email: "nobody@example.invalid",
        twoFactorEnabled: true,
      },
    };
    expect(await getSuperAdminSession()).toBeNull();
    await expect(assertSuperAdminWith2FA()).rejects.toThrow("super-admin only");
  });

  test.skipIf(listed.length === 0)(
    "rejects a super-admin WITHOUT 2FA, allows one WITH 2FA",
    async () => {
      const { assertSuperAdminWith2FA, getSuperAdminSession } = await import(
        "@/lib/auth/super-admin"
      );
      // Super-admin but 2FA off → blocked.
      fakeSession = {
        user: { id: "u2", email: listed[0], twoFactorEnabled: false },
      };
      await expect(assertSuperAdminWith2FA()).rejects.toThrow("2FA required");

      // Super-admin with 2FA on → allowed.
      fakeSession = {
        user: { id: "u2", email: listed[0], twoFactorEnabled: true },
      };
      const ctx = await getSuperAdminSession();
      expect(ctx?.twoFactorEnabled).toBe(true);
      await expect(assertSuperAdminWith2FA()).resolves.toBeUndefined();
    },
  );
});

describe("recordAdminAction (executor path)", () => {
  test("executor path inserts via the executor and RETHROWS on failure", async () => {
    const { recordAdminAction } = await import("./audit");
    fakeSession = {
      user: { id: "actor-1", email: "boss@example.invalid" },
    };

    const inserted: unknown[] = [];
    const okExecutor = {
      insert: () => ({
        values: async (v: unknown) => {
          inserted.push(v);
        },
      }),
    };
    await recordAdminAction(
      { action: "x.y", targetType: "system" },
      okExecutor as never,
    );
    expect(inserted).toHaveLength(1);

    const failingExecutor = {
      insert: () => ({
        values: async () => {
          throw new Error("boom");
        },
      }),
    };
    await expect(
      recordAdminAction(
        { action: "x.y", targetType: "system" },
        failingExecutor as never,
      ),
    ).rejects.toThrow("boom");
  });
});

const RUN_DB = process.env.DB_TESTS === "1";

describe.skipIf(!RUN_DB)("recordAdminAction + listAuditLog (DB)", () => {
  let recordAdminAction: typeof import("./audit").recordAdminAction;
  let listAuditLog: typeof import("./audit").listAuditLog;
  let db: typeof import("@/lib/db/client").db;
  let table: typeof import("@/lib/db/admin-schema").adminAuditLog;
  let userTable: typeof import("@/lib/db/auth-schema").user;

  // The actor_id FK requires real user rows; seed the actors the tests reference.
  const ACTOR_IDS = ["actor-1", "a1", "a2"];

  beforeAll(async () => {
    ({ recordAdminAction, listAuditLog } = await import("./audit"));
    ({ db } = await import("@/lib/db/client"));
    ({ adminAuditLog: table } = await import("@/lib/db/admin-schema"));
    ({ user: userTable } = await import("@/lib/db/auth-schema"));
    const { assertTestDb } = await import("@/lib/db/assert-test-db");
    await assertTestDb();
    await db.delete(table);
    await db
      .insert(userTable)
      .values(
        ACTOR_IDS.map((id) => ({
          id,
          name: `Audit Test ${id}`,
          email: `${id}@audit-test.invalid`,
        })),
      )
      .onConflictDoNothing();
  });

  afterAll(async () => {
    const { inArray } = await import("drizzle-orm");
    await db.delete(table);
    await db.delete(userTable).where(inArray(userTable.id, ACTOR_IDS));
  });

  test("records the actor snapshot, IP (x-real-ip) and metadata", async () => {
    fakeSession = {
      user: { id: "actor-1", email: "boss@example.invalid" },
    };
    fakeHeaders.set("x-real-ip", "203.0.113.7");
    fakeHeaders.delete("x-forwarded-for");

    await recordAdminAction({
      action: "user.ban",
      targetType: "user",
      targetId: "victim-1",
      targetLabel: "victim@example.invalid",
      metadata: { reason: "spam" },
    });

    const { entries, total } = await listAuditLog();
    expect(total).toBe(1);
    const row = entries[0];
    expect(row.actorId).toBe("actor-1");
    expect(row.actorEmail).toBe("boss@example.invalid");
    expect(row.action).toBe("user.ban");
    expect(row.targetType).toBe("user");
    expect(row.targetId).toBe("victim-1");
    expect(row.targetLabel).toBe("victim@example.invalid");
    expect(row.metadata).toEqual({ reason: "spam" });
    expect(row.ipAddress).toBe("203.0.113.7");
    expect(row.createdAt).toBeInstanceOf(Date);
  });

  test("falls back to x-forwarded-for and 'unknown' actor with no session", async () => {
    await db.delete(table);
    fakeSession = null;
    fakeHeaders.delete("x-real-ip");
    fakeHeaders.set("x-forwarded-for", "198.51.100.4, 10.0.0.1");

    await recordAdminAction({ action: "system.ping", targetType: "system" });

    const { entries } = await listAuditLog();
    expect(entries).toHaveLength(1);
    expect(entries[0].actorId).toBeNull();
    expect(entries[0].actorEmail).toBe("unknown");
    expect(entries[0].ipAddress).toBe("198.51.100.4");
  });

  test("orders newest-first, paginates and filters", async () => {
    await db.delete(table);
    const base = Date.now();
    await db.insert(table).values([
      {
        actorId: "a1",
        actorEmail: "a1@x.io",
        action: "user.ban",
        targetType: "user",
        targetId: "u1",
        createdAt: new Date(base - 3000),
      },
      {
        actorId: "a2",
        actorEmail: "a2@x.io",
        action: "org.transfer_owner",
        targetType: "organization",
        targetId: "o1",
        createdAt: new Date(base - 2000),
      },
      {
        actorId: "a1",
        actorEmail: "a1@x.io",
        action: "user.unban",
        targetType: "user",
        targetId: "u1",
        createdAt: new Date(base - 1000),
      },
    ]);

    const all = await listAuditLog();
    // Gated on `audit:read` (Task 9) — spy-checked once here for the group.
    expect(assertSysPermission).toHaveBeenCalledWith("audit:read");
    expect(all.total).toBe(3);
    expect(all.entries.map((e) => e.action)).toEqual([
      "user.unban",
      "org.transfer_owner",
      "user.ban",
    ]);

    const page = await listAuditLog({ limit: 1, offset: 1 });
    expect(page.total).toBe(3);
    expect(page.entries).toHaveLength(1);
    expect(page.entries[0].action).toBe("org.transfer_owner");

    const users = await listAuditLog({ targetType: "user" });
    expect(users.total).toBe(2);
    expect(users.entries.every((e) => e.targetType === "user")).toBe(true);

    const a1 = await listAuditLog({ actorId: "a1" });
    expect(a1.total).toBe(2);

    const u1 = await listAuditLog({ targetId: "u1" });
    expect(u1.total).toBe(2);
  });

  test("filters by actorEmail (case-insensitive), action (contains), since/until", async () => {
    await db.delete(table);
    const base = Date.now();
    await db.insert(table).values([
      {
        actorId: "a1",
        actorEmail: "Boss@Example.IO",
        action: "user.ban",
        targetType: "user",
        targetId: "u1",
        createdAt: new Date(base - 3000),
      },
      {
        actorId: "a2",
        actorEmail: "other@example.io",
        action: "org.transfer_owner",
        targetType: "organization",
        targetId: "o1",
        createdAt: new Date(base - 2000),
      },
      {
        actorId: "a1",
        actorEmail: "boss@example.io",
        action: "user.unban",
        targetType: "user",
        targetId: "u1",
        createdAt: new Date(base - 1000),
      },
    ]);

    // actorEmail is matched case-insensitively → both "boss" rows.
    const byEmail = await listAuditLog({ actorEmail: "boss@example.io" });
    expect(byEmail.total).toBe(2);
    expect(byEmail.entries.every((e) => /boss/i.test(e.actorEmail))).toBe(true);

    // action contains "user." → ban + unban.
    const byAction = await listAuditLog({ action: "user." });
    expect(byAction.total).toBe(2);
    expect(byAction.entries.every((e) => e.action.startsWith("user."))).toBe(
      true,
    );

    // since lower-bounds createdAt (inclusive) → the two most recent rows.
    const since = await listAuditLog({ since: new Date(base - 2000) });
    expect(since.total).toBe(2);
    expect(since.entries.map((e) => e.action)).toEqual([
      "user.unban",
      "org.transfer_owner",
    ]);

    // until upper-bounds createdAt (inclusive) → the two oldest rows.
    const until = await listAuditLog({ until: new Date(base - 2000) });
    expect(until.total).toBe(2);
    expect(until.entries.map((e) => e.action)).toEqual([
      "org.transfer_owner",
      "user.ban",
    ]);

    // Combined filters AND together.
    const combined = await listAuditLog({
      actorEmail: "boss@example.io",
      action: "user.",
      since: new Date(base - 1500),
    });
    expect(combined.total).toBe(1);
    expect(combined.entries[0].action).toBe("user.unban");
  });
});
