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 consumerId;
let consumerUsername;
let consumerCookie;
let targetRoleId;
let seededPermissionIds = [];
const createdRoleIds = [];
const createdUserIds = [];

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 getDbPermissionIdsForRole(roleId) {
  const rows = await pool.query(
    `SELECT permission_id FROM role_permissions WHERE role_id = $1 ORDER BY permission_id`,
    [roleId],
  );
  return rows.rows.map((r) => r.permission_id);
}

before(async () => {
  const stamp = `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;

  // Admin user — used only to seed the target role with a known permission set
  // so we can verify later that the rejected non-admin calls did not mutate it.
  adminUsername = `role_perm_nonadmin_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, 'Role Perm NonAdmin 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);

  // Regular (non-admin) user — has no role assignments at all.
  consumerUsername = `role_perm_nonadmin_user_${stamp}`;
  const c = await pool.query(
    `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active)
     VALUES ($1, $2, $3, 'Role Perm NonAdmin User', 'en', true) RETURNING id`,
    [consumerUsername, `${consumerUsername}@example.com`, TEST_PASSWORD_HASH],
  );
  consumerId = c.rows[0].id;
  createdUserIds.push(consumerId);
  await pool.query(
    `INSERT INTO user_groups (user_id, group_id)
     SELECT $1, id FROM groups WHERE name = 'Everyone'
     ON CONFLICT DO NOTHING`,
    [consumerId],
  );
  consumerCookie = await loginAndGetCookie(consumerUsername, TEST_PASSWORD);

  // Create a target role via the admin API and seed it with two permissions
  // so DELETE has something concrete to attempt to remove and we can detect
  // any unwanted mutation in either direction.
  const roleName = `role_perm_nonadmin_target_${stamp}`;
  const roleRes = await fetch(`${API_BASE}/api/roles`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Cookie: adminCookie },
    body: JSON.stringify({ name: roleName }),
  });
  assert.equal(roleRes.status, 201);
  const roleBody = await roleRes.json();
  targetRoleId = roleBody.id;
  createdRoleIds.push(targetRoleId);

  const permRows = await pool.query(
    `SELECT id FROM permissions ORDER BY id LIMIT 2`,
  );
  assert.ok(permRows.rowCount >= 2, "need at least 2 seeded permissions");
  seededPermissionIds = [permRows.rows[0].id, permRows.rows[1].id];

  const seed = await fetch(
    `${API_BASE}/api/roles/${targetRoleId}/permissions`,
    {
      method: "PUT",
      headers: { "Content-Type": "application/json", Cookie: adminCookie },
      body: JSON.stringify({ permissionIds: seededPermissionIds }),
    },
  );
  assert.equal(seed.status, 200);
  assert.deepEqual(
    await getDbPermissionIdsForRole(targetRoleId),
    [...seededPermissionIds].sort((a, b) => a - b),
  );
});

after(async () => {
  if (createdRoleIds.length) {
    await pool.query(
      `DELETE FROM role_permission_audit WHERE role_id = ANY($1::int[])`,
      [createdRoleIds],
    );
    await pool.query(
      `DELETE FROM role_permissions WHERE role_id = ANY($1::int[])`,
      [createdRoleIds],
    );
    await pool.query(`DELETE FROM roles WHERE id = ANY($1::int[])`, [
      createdRoleIds,
    ]);
  }
  for (const uid of createdUserIds) {
    if (uid !== undefined) {
      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("PUT /api/roles/:id/permissions returns 403 for a non-admin user and does not mutate the role", async () => {
  const before = await getDbPermissionIdsForRole(targetRoleId);

  const res = await fetch(
    `${API_BASE}/api/roles/${targetRoleId}/permissions`,
    {
      method: "PUT",
      headers: { "Content-Type": "application/json", Cookie: consumerCookie },
      body: JSON.stringify({ permissionIds: [] }),
    },
  );
  assert.equal(res.status, 403, `expected 403, got ${res.status}`);

  // The role's permission set must be byte-for-byte identical after the
  // rejected call — the requireAdmin guard must short-circuit before any
  // DB write happens.
  assert.deepEqual(await getDbPermissionIdsForRole(targetRoleId), before);
});

test("POST /api/roles/:id/permissions returns 403 for a non-admin user and does not mutate the role", async () => {
  const before = await getDbPermissionIdsForRole(targetRoleId);

  // Pick a permission id that is NOT already on the role so that, if the
  // guard ever regressed, we'd see the row count change.
  const extraRow = await pool.query(
    `SELECT id FROM permissions WHERE id <> ALL($1::int[]) ORDER BY id LIMIT 1`,
    [before],
  );
  assert.ok(extraRow.rowCount > 0, "need a permission not already on the role");
  const extraPermissionId = extraRow.rows[0].id;

  const res = await fetch(
    `${API_BASE}/api/roles/${targetRoleId}/permissions`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json", Cookie: consumerCookie },
      body: JSON.stringify({ permissionId: extraPermissionId }),
    },
  );
  assert.equal(res.status, 403, `expected 403, got ${res.status}`);

  assert.deepEqual(await getDbPermissionIdsForRole(targetRoleId), before);
});

test("DELETE /api/roles/:id/permissions/:permissionId returns 403 for a non-admin user and does not mutate the role", async () => {
  const before = await getDbPermissionIdsForRole(targetRoleId);
  assert.ok(
    before.length > 0,
    "target role should still have its seeded permissions",
  );
  const targetPermissionId = before[0];

  const res = await fetch(
    `${API_BASE}/api/roles/${targetRoleId}/permissions/${targetPermissionId}`,
    { method: "DELETE", headers: { Cookie: consumerCookie } },
  );
  assert.equal(res.status, 403, `expected 403, got ${res.status}`);

  assert.deepEqual(await getDbPermissionIdsForRole(targetRoleId), before);
});
