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)}`;

let adminId;
let adminUsername;
let adminCookie;
const insertedAuditIds = [];
const TEST_TAG = `target_filter_test_${STAMP}`;

// A handful of unique parent IDs so we can verify targetId filtering.
const PARENT_SERVICE_ID = 800100001;
const PARENT_SERVICE_ID_OTHER = 800100002;
const PARENT_GROUP_ID = 800200001;
const PARENT_APP_ID = 800300001;

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({ 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`,
    [
      adminId,
      action,
      targetType,
      targetId,
      JSON.stringify({ ...metadata, testTag: TEST_TAG }),
    ],
  );
  insertedAuditIds.push(r.rows[0].id);
  return r.rows[0].id;
}

before(async () => {
  adminUsername = `audit_target_admin_${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 Target Admin', 'en', true) RETURNING id`,
    [adminUsername, `${adminUsername}@example.com`, TEST_PASSWORD_HASH],
  );
  adminId = a.rows[0].id;
  await pool.query(
    `INSERT INTO user_roles (user_id, role_id) SELECT $1, id FROM roles WHERE name = 'admin'`,
    [adminId],
  );
  adminCookie = await loginAndGetCookie(adminUsername, TEST_PASSWORD);

  // History for the parent service that will be force-deleted.
  await insertAuditRow({
    action: "service.create",
    targetType: "service",
    targetId: PARENT_SERVICE_ID,
    metadata: { nameEn: "ParentSvc" },
  });
  await insertAuditRow({
    action: "service.update",
    targetType: "service",
    targetId: PARENT_SERVICE_ID,
    metadata: { changes: 2 },
  });
  await insertAuditRow({
    action: "service.force_delete",
    targetType: "service",
    targetId: PARENT_SERVICE_ID,
    metadata: { nameEn: "ParentSvc", orderCount: 7, messageCount: 3 },
  });
  // A different service so we can prove targetId narrows correctly.
  await insertAuditRow({
    action: "service.create",
    targetType: "service",
    targetId: PARENT_SERVICE_ID_OTHER,
    metadata: { nameEn: "OtherSvc" },
  });

  // Group and app rows so we can prove targetType-only filtering works.
  await insertAuditRow({
    action: "group.create",
    targetType: "group",
    targetId: PARENT_GROUP_ID,
    metadata: { name: "Grp" },
  });
  await insertAuditRow({
    action: "group.force_delete",
    targetType: "group",
    targetId: PARENT_GROUP_ID,
    metadata: { name: "Grp", memberCount: 4 },
  });
  await insertAuditRow({
    action: "app.create",
    targetType: "app",
    targetId: PARENT_APP_ID,
    metadata: { nameEn: "AppX" },
  });
});

after(async () => {
  if (insertedAuditIds.length) {
    await pool.query(`DELETE FROM audit_logs WHERE id = ANY($1::int[])`, [
      insertedAuditIds,
    ]);
  }
  if (adminId !== undefined) {
    await pool.query(`DELETE FROM audit_logs WHERE actor_user_id = $1`, [
      adminId,
    ]);
    await pool.query(`DELETE FROM user_roles WHERE user_id = $1`, [adminId]);
    await pool.query(`DELETE FROM users WHERE id = $1`, [adminId]);
  }
  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: adminCookie },
  });
  assert.equal(res.status, 200);
  return res.json();
}

test("targetType filter narrows results to that target_type", async () => {
  const body = await listWithParams("targetType=group&limit=200");
  const ours = ownEntries(body);
  assert.ok(ours.length >= 2, "expected at least the seeded group rows");
  for (const e of ours) {
    assert.equal(e.targetType, "group", `unexpected targetType ${e.targetType}`);
  }
});

test("targetType=app excludes service and group rows", async () => {
  const body = await listWithParams("targetType=app&limit=200");
  const ours = ownEntries(body);
  assert.ok(ours.some((e) => e.targetId === PARENT_APP_ID));
  assert.ok(!ours.some((e) => e.targetType === "service"));
  assert.ok(!ours.some((e) => e.targetType === "group"));
});

test("targetType + targetId returns only that single entity's history", async () => {
  const body = await listWithParams(
    `targetType=service&targetId=${PARENT_SERVICE_ID}&limit=200`,
  );
  const ours = ownEntries(body);
  // 3 seeded rows for that service: create, update, force_delete.
  assert.equal(ours.length, 3);
  for (const e of ours) {
    assert.equal(e.targetType, "service");
    assert.equal(e.targetId, PARENT_SERVICE_ID);
  }
  const actions = ours.map((e) => e.action).sort();
  assert.deepEqual(actions, [
    "service.create",
    "service.force_delete",
    "service.update",
  ]);
});

test("targetType + targetId excludes other entities of the same type", async () => {
  const body = await listWithParams(
    `targetType=service&targetId=${PARENT_SERVICE_ID}&limit=200`,
  );
  const ours = ownEntries(body);
  assert.ok(
    !ours.some((e) => e.targetId === PARENT_SERVICE_ID_OTHER),
    "other service rows must not appear",
  );
});

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

test("targetType combines with forcedOnly filter", async () => {
  const body = await listWithParams(
    "targetType=service&forcedOnly=true&limit=200",
  );
  const ours = ownEntries(body);
  assert.ok(ours.some((e) => e.action === "service.force_delete"));
  // Non-force rows must be excluded by forcedOnly.
  assert.ok(!ours.some((e) => e.action === "service.create"));
  assert.ok(!ours.some((e) => e.action === "service.update"));
});

test("CSV export honors targetType + targetId filter", async () => {
  const res = await fetch(
    `${API_BASE}/api/admin/audit-logs/export?targetType=service&targetId=${PARENT_SERVICE_ID}`,
    { headers: { Cookie: adminCookie, 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.ok(ourLines.length >= 3);
  for (const line of ourLines) {
    assert.match(
      line,
      new RegExp(`,service,${PARENT_SERVICE_ID},`),
      `expected every CSV row to be the targeted service: ${line}`,
    );
  }
});
