// #197: end-to-end coverage for the audit log's actorUserId filter and its
// CSV-export counterpart. The buildWhere clause already supported the
// filter; these tests pin down the wiring (param parsing, filter
// composition, and CSV export honouring it).
import { test, before, after } from "node:test";
import assert from "node:assert/strict";
import pg from "pg";

const API_BASE = process.env.TEST_API_BASE ?? "http://localhost:8080";
const DATABASE_URL = process.env.DATABASE_URL;
if (!DATABASE_URL) {
  throw new Error("DATABASE_URL must be set to run these tests");
}

const TEST_PASSWORD = "TestPass123!";
const TEST_PASSWORD_HASH =
  "$2b$10$Bs636ukPMyz01nKrsi.5m.JlDXSN22AVCvn8cgPWWDbo5yJRQX2vu";

const pool = new pg.Pool({ connectionString: DATABASE_URL });

const STAMP = `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
const TEST_TAG = `actor_filter_test_${STAMP}`;

let adminAId;
let adminAUsername;
let adminACookie;
let adminBId;
let adminBUsername;

const insertedAuditIds = [];

const TARGET_ID_A = 800400001;
const TARGET_ID_B = 800400002;
const TARGET_ID_C = 800400003;

async function loginAndGetCookie(username, password) {
  const res = await fetch(`${API_BASE}/api/auth/login`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ username, password }),
  });
  assert.equal(res.status, 200, `login expected 200, got ${res.status}`);
  const setCookie = res.headers.get("set-cookie");
  return setCookie
    .split(",")
    .map((c) => c.split(";")[0].trim())
    .find((c) => c.startsWith("connect.sid="));
}

async function insertAuditRow({ actorId, action, targetType, targetId, metadata }) {
  const r = await pool.query(
    `INSERT INTO audit_logs (actor_user_id, action, target_type, target_id, metadata)
     VALUES ($1, $2, $3, $4, $5::jsonb) RETURNING id`,
    [
      actorId,
      action,
      targetType,
      targetId,
      JSON.stringify({ ...metadata, testTag: TEST_TAG }),
    ],
  );
  insertedAuditIds.push(r.rows[0].id);
  return r.rows[0].id;
}

before(async () => {
  adminAUsername = `audit_actor_admin_a_${STAMP}`;
  adminBUsername = `audit_actor_admin_b_${STAMP}`;
  const a = await pool.query(
    `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active)
     VALUES ($1, $2, $3, 'Audit Actor A', 'en', true) RETURNING id`,
    [adminAUsername, `${adminAUsername}@example.com`, TEST_PASSWORD_HASH],
  );
  adminAId = a.rows[0].id;
  const b = await pool.query(
    `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active)
     VALUES ($1, $2, $3, 'Audit Actor B', 'en', true) RETURNING id`,
    [adminBUsername, `${adminBUsername}@example.com`, TEST_PASSWORD_HASH],
  );
  adminBId = b.rows[0].id;
  await pool.query(
    `INSERT INTO user_roles (user_id, role_id) SELECT $1, id FROM roles WHERE name = 'admin'`,
    [adminAId],
  );
  await pool.query(
    `INSERT INTO user_roles (user_id, role_id) SELECT $1, id FROM roles WHERE name = 'admin'`,
    [adminBId],
  );
  adminACookie = await loginAndGetCookie(adminAUsername, TEST_PASSWORD);

  // Two rows by Admin A.
  await insertAuditRow({
    actorId: adminAId,
    action: "service.create",
    targetType: "service",
    targetId: TARGET_ID_A,
    metadata: { nameEn: "ASvc1" },
  });
  await insertAuditRow({
    actorId: adminAId,
    action: "service.update",
    targetType: "service",
    targetId: TARGET_ID_B,
    metadata: { changes: 3 },
  });
  // One row by Admin B on a third target so we can prove the filter
  // narrows by *who acted*, not by what was acted on.
  await insertAuditRow({
    actorId: adminBId,
    action: "service.create",
    targetType: "service",
    targetId: TARGET_ID_C,
    metadata: { nameEn: "BSvc" },
  });
});

after(async () => {
  if (insertedAuditIds.length) {
    await pool.query(`DELETE FROM audit_logs WHERE id = ANY($1::int[])`, [
      insertedAuditIds,
    ]);
  }
  for (const id of [adminAId, adminBId]) {
    if (id !== undefined) {
      await pool.query(`DELETE FROM audit_logs WHERE actor_user_id = $1`, [id]);
      await pool.query(`DELETE FROM user_roles WHERE user_id = $1`, [id]);
      await pool.query(`DELETE FROM users WHERE id = $1`, [id]);
    }
  }
  await pool.end();
});

function ownEntries(body) {
  return body.entries.filter(
    (e) =>
      e.metadata &&
      typeof e.metadata === "object" &&
      e.metadata.testTag === TEST_TAG,
  );
}

async function listWithParams(qs) {
  const res = await fetch(`${API_BASE}/api/admin/audit-logs?${qs}`, {
    headers: { Cookie: adminACookie },
  });
  assert.equal(res.status, 200);
  return res.json();
}

test("actorUserId filter narrows results to that acting user", async () => {
  const body = await listWithParams(`actorUserId=${adminAId}&limit=200`);
  const ours = ownEntries(body);
  assert.equal(ours.length, 2, "expected exactly the two seeded admin-A rows");
  for (const e of ours) {
    assert.equal(
      e.actor?.id,
      adminAId,
      `unexpected actor ${e.actor?.id ?? "null"}`,
    );
  }
  const targetIds = ours.map((e) => e.targetId).sort();
  assert.deepEqual(targetIds, [TARGET_ID_A, TARGET_ID_B]);
});

test("actorUserId filter excludes other actors on the same target_type", async () => {
  const body = await listWithParams(`actorUserId=${adminAId}&limit=200`);
  const ours = ownEntries(body);
  assert.ok(
    !ours.some((e) => e.actor?.id === adminBId),
    "admin B's row must not appear in admin A's filtered list",
  );
  assert.ok(!ours.some((e) => e.targetId === TARGET_ID_C));
});

test("actorUserId combines with targetId to scope to one (actor, target)", async () => {
  const body = await listWithParams(
    `actorUserId=${adminAId}&targetType=service&targetId=${TARGET_ID_A}&limit=200`,
  );
  const ours = ownEntries(body);
  assert.equal(ours.length, 1);
  assert.equal(ours[0].actor?.id, adminAId);
  assert.equal(ours[0].targetId, TARGET_ID_A);
});

test("invalid actorUserId returns 400", async () => {
  const res = await fetch(
    `${API_BASE}/api/admin/audit-logs?actorUserId=not-a-number`,
    { headers: { Cookie: adminACookie } },
  );
  assert.equal(res.status, 400);
  const body = await res.json();
  assert.match(body.error ?? "", /actorUserId/);
});

test("zero / negative actorUserId returns 400", async () => {
  for (const bad of ["0", "-1"]) {
    const res = await fetch(
      `${API_BASE}/api/admin/audit-logs?actorUserId=${bad}`,
      { headers: { Cookie: adminACookie } },
    );
    assert.equal(res.status, 400, `expected 400 for actorUserId=${bad}`);
  }
});

test("CSV export honors the actorUserId filter", async () => {
  const res = await fetch(
    `${API_BASE}/api/admin/audit-logs/export?actorUserId=${adminAId}`,
    { headers: { Cookie: adminACookie, Accept: "text/csv" } },
  );
  assert.equal(res.status, 200);
  assert.match(res.headers.get("content-type") ?? "", /text\/csv/);
  const text = await res.text();
  const ourLines = text
    .split(/\r?\n/)
    .filter((line) => line.includes(TEST_TAG));
  assert.equal(ourLines.length, 2, "expected exactly the two admin-A rows");
  for (const line of ourLines) {
    assert.match(
      line,
      new RegExp(`,${adminAUsername},`),
      `expected every CSV row to be authored by admin A: ${line}`,
    );
    assert.ok(
      !line.includes(adminBUsername),
      `admin B must not appear in the filtered export: ${line}`,
    );
  }
});
