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

let adminId;
let adminUsername;
let adminCookie;
let nonAdminCookie;
let nonAdminId;
const createdUserIds = [];
const createdGroupIds = [];
const createdRoleIds = [];

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="));
}

function uniqueName(prefix) {
  return `${prefix}_${Date.now().toString(36)}${Math.random()
    .toString(36)
    .slice(2, 6)}`;
}

async function createSubjectUser() {
  const username = uniqueName("user_audit_subj");
  const r = await pool.query(
    `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active)
     VALUES ($1, $2, $3, 'Subject', 'en', true) RETURNING id`,
    [username, `${username}@example.com`, TEST_PASSWORD_HASH],
  );
  const id = r.rows[0].id;
  createdUserIds.push(id);
  return { id, username };
}

async function createGroup() {
  const name = uniqueName("user_audit_grp");
  const res = await fetch(`${API_BASE}/api/groups`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Cookie: adminCookie },
    body: JSON.stringify({ name }),
  });
  assert.equal(res.status, 201);
  const body = await res.json();
  createdGroupIds.push(body.id);
  return body;
}

async function createRole() {
  const name = uniqueName("user_audit_role");
  const res = await fetch(`${API_BASE}/api/roles`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Cookie: adminCookie },
    body: JSON.stringify({ name }),
  });
  assert.equal(res.status, 201);
  const body = await res.json();
  createdRoleIds.push(body.id);
  return body;
}

before(async () => {
  adminUsername = uniqueName("user_audit_admin");
  const a = await pool.query(
    `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active)
     VALUES ($1, $2, $3, 'User Audit Admin', 'en', true) RETURNING id`,
    [adminUsername, `${adminUsername}@example.com`, TEST_PASSWORD_HASH],
  );
  adminId = a.rows[0].id;
  createdUserIds.push(adminId);
  await pool.query(
    `INSERT INTO user_roles (user_id, role_id) SELECT $1, id FROM roles WHERE name = 'admin'`,
    [adminId],
  );
  await pool.query(
    `INSERT INTO user_groups (user_id, group_id)
     SELECT $1, id FROM groups WHERE name IN ('Everyone', 'Admins')
     ON CONFLICT DO NOTHING`,
    [adminId],
  );
  adminCookie = await loginAndGetCookie(adminUsername, TEST_PASSWORD);

  // A non-admin user used to assert /audit is admin-only.
  const naUsername = uniqueName("user_audit_nonadmin");
  const na = await pool.query(
    `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active)
     VALUES ($1, $2, $3, 'Non Admin', 'en', true) RETURNING id`,
    [naUsername, `${naUsername}@example.com`, TEST_PASSWORD_HASH],
  );
  nonAdminId = na.rows[0].id;
  createdUserIds.push(nonAdminId);
  nonAdminCookie = await loginAndGetCookie(naUsername, TEST_PASSWORD);
});

after(async () => {
  if (createdUserIds.length) {
    await pool.query(
      `DELETE FROM permission_audit WHERE target_kind = 'user' AND target_id = ANY($1::int[])`,
      [createdUserIds],
    );
  }
  if (createdGroupIds.length) {
    await pool.query(
      `DELETE FROM permission_audit WHERE target_kind = 'group' AND target_id = ANY($1::int[])`,
      [createdGroupIds],
    );
    await pool.query(
      `DELETE FROM user_groups WHERE group_id = ANY($1::int[])`,
      [createdGroupIds],
    );
    await pool.query(`DELETE FROM groups WHERE id = ANY($1::int[])`, [
      createdGroupIds,
    ]);
  }
  if (createdRoleIds.length) {
    await pool.query(
      `DELETE FROM user_roles WHERE role_id = ANY($1::int[])`,
      [createdRoleIds],
    );
    await pool.query(`DELETE FROM roles WHERE id = ANY($1::int[])`, [
      createdRoleIds,
    ]);
  }
  for (const uid of createdUserIds) {
    await pool.query(`DELETE FROM user_groups WHERE user_id = $1`, [uid]);
    await pool.query(`DELETE FROM user_roles WHERE user_id = $1`, [uid]);
    await pool.query(`DELETE FROM users WHERE id = $1`, [uid]);
  }
  await pool.end();
});

test("POST /api/users/:id/roles writes a user.roles permission_audit row transactionally", async () => {
  const subject = await createSubjectUser();
  const role = await createRole();

  const before = await pool.query(
    `SELECT COUNT(*)::int AS c FROM permission_audit
      WHERE target_kind = 'user' AND target_id = $1`,
    [subject.id],
  );
  assert.equal(before.rows[0].c, 0);

  const res = await fetch(`${API_BASE}/api/users/${subject.id}/roles`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Cookie: adminCookie },
    body: JSON.stringify({ roleName: role.name }),
  });
  assert.equal(res.status, 200);

  const rows = await pool.query(
    `SELECT actor_user_id, change_kind, previous_ids, new_ids
       FROM permission_audit
      WHERE target_kind = 'user' AND target_id = $1`,
    [subject.id],
  );
  assert.equal(rows.rowCount, 1);
  const row = rows.rows[0];
  assert.equal(row.actor_user_id, adminId);
  assert.equal(row.change_kind, "user.roles");
  assert.deepEqual(row.previous_ids, []);
  assert.deepEqual(row.new_ids, [role.id]);
});

test("PATCH /api/users/:id with groupIds writes a user.groups permission_audit row", async () => {
  const subject = await createSubjectUser();
  const g1 = await createGroup();
  const g2 = await createGroup();

  // First write: empty → [g1, g2]
  const r1 = await fetch(`${API_BASE}/api/users/${subject.id}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json", Cookie: adminCookie },
    body: JSON.stringify({ groupIds: [g1.id, g2.id] }),
  });
  assert.equal(r1.status, 200);

  await new Promise((r) => setTimeout(r, 25));

  // Second write: [g1, g2] → [g2]
  const r2 = await fetch(`${API_BASE}/api/users/${subject.id}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json", Cookie: adminCookie },
    body: JSON.stringify({ groupIds: [g2.id] }),
  });
  assert.equal(r2.status, 200);

  const rows = await pool.query(
    `SELECT change_kind, previous_ids, new_ids
       FROM permission_audit
      WHERE target_kind = 'user' AND target_id = $1
      ORDER BY id`,
    [subject.id],
  );
  assert.equal(rows.rowCount, 2);
  for (const row of rows.rows) {
    assert.equal(row.change_kind, "user.groups");
  }
  const sortedIds = (a) => [...a].sort((x, y) => x - y);
  assert.deepEqual(rows.rows[0].previous_ids, []);
  assert.deepEqual(sortedIds(rows.rows[0].new_ids), sortedIds([g1.id, g2.id]));
  assert.deepEqual(sortedIds(rows.rows[1].previous_ids), sortedIds([g1.id, g2.id]));
  assert.deepEqual(rows.rows[1].new_ids, [g2.id]);
});

test("PATCH /api/users/:id with groupIds mirrors onto each affected group's history", async () => {
  const subject = await createSubjectUser();
  const g1 = await createGroup();
  const g2 = await createGroup();

  // Add user to [g1, g2] → expect mirrored group.users rows on g1 AND g2.
  let res = await fetch(`${API_BASE}/api/users/${subject.id}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json", Cookie: adminCookie },
    body: JSON.stringify({ groupIds: [g1.id, g2.id] }),
  });
  assert.equal(res.status, 200);

  for (const g of [g1, g2]) {
    const rows = await pool.query(
      `SELECT change_kind, previous_ids, new_ids FROM permission_audit
        WHERE target_kind = 'group' AND target_id = $1 ORDER BY id`,
      [g.id],
    );
    assert.equal(rows.rowCount, 1);
    assert.equal(rows.rows[0].change_kind, "group.users");
    assert.deepEqual(rows.rows[0].previous_ids, []);
    assert.deepEqual(rows.rows[0].new_ids, [subject.id]);
  }

  // Now drop g1 → expect a mirrored group.users row only on g1.
  res = await fetch(`${API_BASE}/api/users/${subject.id}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json", Cookie: adminCookie },
    body: JSON.stringify({ groupIds: [g2.id] }),
  });
  assert.equal(res.status, 200);

  const g1Rows = await pool.query(
    `SELECT change_kind, previous_ids, new_ids FROM permission_audit
      WHERE target_kind = 'group' AND target_id = $1 ORDER BY id`,
    [g1.id],
  );
  assert.equal(g1Rows.rowCount, 2);
  assert.deepEqual(g1Rows.rows[1].previous_ids, [subject.id]);
  assert.deepEqual(g1Rows.rows[1].new_ids, []);

  const g2Rows = await pool.query(
    `SELECT COUNT(*)::int AS c FROM permission_audit
      WHERE target_kind = 'group' AND target_id = $1`,
    [g2.id],
  );
  assert.equal(g2Rows.rows[0].c, 1);
});

test("GET /api/users/:id/audit returns entries newest first with diff fields, actor and pagination", async () => {
  const subject = await createSubjectUser();
  const g1 = await createGroup();
  const g2 = await createGroup();
  const g3 = await createGroup();
  const role = await createRole();

  const sets = [[g1.id], [g1.id, g2.id], [g3.id]];
  for (const s of sets) {
    const r = await fetch(`${API_BASE}/api/users/${subject.id}`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json", Cookie: adminCookie },
      body: JSON.stringify({ groupIds: s }),
    });
    assert.equal(r.status, 200);
    await new Promise((r) => setTimeout(r, 10));
  }
  // One role mutation too — to exercise filtering by changeKind via the
  // cross-kind history.
  const ar = await fetch(`${API_BASE}/api/users/${subject.id}/roles`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Cookie: adminCookie },
    body: JSON.stringify({ roleName: role.name }),
  });
  assert.equal(ar.status, 200);

  const allRes = await fetch(`${API_BASE}/api/users/${subject.id}/audit`, {
    headers: { Cookie: adminCookie },
  });
  assert.equal(allRes.status, 200);
  const all = await allRes.json();
  assert.equal(all.totalCount, 4);
  assert.equal(all.entries.length, 4);
  // Latest entry is the role addition.
  const latest = all.entries[0];
  assert.equal(latest.changeKind, "user.roles");
  assert.deepEqual(latest.addedIds, [role.id]);
  assert.deepEqual(latest.removedIds, []);
  assert.ok(latest.actor);
  assert.equal(latest.actor.id, adminId);
  assert.equal(latest.actor.username, adminUsername);

  // Pagination: limit=2 should yield nextOffset=2.
  const pageRes = await fetch(
    `${API_BASE}/api/users/${subject.id}/audit?limit=2`,
    { headers: { Cookie: adminCookie } },
  );
  assert.equal(pageRes.status, 200);
  const page = await pageRes.json();
  assert.equal(page.entries.length, 2);
  assert.equal(page.totalCount, 4);
  assert.equal(page.nextOffset, 2);

  // Date range: today must include all 4.
  const today = new Date().toISOString().slice(0, 10);
  const rangeRes = await fetch(
    `${API_BASE}/api/users/${subject.id}/audit?from=${today}&to=${today}`,
    { headers: { Cookie: adminCookie } },
  );
  assert.equal(rangeRes.status, 200);
  const range = await rangeRes.json();
  assert.equal(range.totalCount, 4);

  // Inverted range → 400.
  const tomorrow = new Date(Date.now() + 86400000).toISOString().slice(0, 10);
  const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
  const badRes = await fetch(
    `${API_BASE}/api/users/${subject.id}/audit?from=${tomorrow}&to=${yesterday}`,
    { headers: { Cookie: adminCookie } },
  );
  assert.equal(badRes.status, 400);
});

test("GET /api/users/:id/audit is admin-only and 404s on unknown user", async () => {
  const subject = await createSubjectUser();
  const res = await fetch(`${API_BASE}/api/users/${subject.id}/audit`, {
    headers: { Cookie: nonAdminCookie },
  });
  assert.equal(res.status, 403);

  const missing = await fetch(`${API_BASE}/api/users/99999999/audit`, {
    headers: { Cookie: adminCookie },
  });
  assert.equal(missing.status, 404);
});
