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;
const createdRoleIds = [];
const createdGroupIds = [];
const createdUserIds = [];
const createdPermissionIds = [];

let directUserId;
let groupedUserId;
let bothUserId;
let helperGroupId;
let mainRoleId;
let aloneRoleId;
let alsoGrantsP1RoleId;
let permA;
let permB;
let permC;

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 makeUser(prefix, stamp) {
  const r = await pool.query(
    `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active)
     VALUES ($1, $2, $3, $4, 'en', true) RETURNING id`,
    [
      `${prefix}_${stamp}`,
      `${prefix}_${stamp}@example.com`,
      TEST_PASSWORD_HASH,
      `${prefix} User`,
    ],
  );
  const id = r.rows[0].id;
  createdUserIds.push(id);
  return id;
}

async function makePermission(prefix, stamp) {
  const r = await pool.query(
    `INSERT INTO permissions (name, description_en) VALUES ($1, $2) RETURNING id, name`,
    [`${prefix}_${stamp}`, `${prefix} permission`],
  );
  const row = r.rows[0];
  createdPermissionIds.push(row.id);
  return row;
}

async function makeRole(prefix, stamp) {
  const r = await pool.query(
    `INSERT INTO roles (name, description_en) VALUES ($1, $2) RETURNING id`,
    [`${prefix}_${stamp}`, `${prefix} role`],
  );
  const id = r.rows[0].id;
  createdRoleIds.push(id);
  return id;
}

async function makeGroup(prefix, stamp) {
  const r = await pool.query(
    `INSERT INTO groups (name, description_en) VALUES ($1, $2) RETURNING id`,
    [`${prefix}_${stamp}`, `${prefix} group`],
  );
  const id = r.rows[0].id;
  createdGroupIds.push(id);
  return id;
}

before(async () => {
  const stamp = `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
  adminUsername = `imp_admin_${stamp}`;
  adminId = await makeUser("imp_admin", stamp);
  await pool.query(
    `INSERT INTO user_roles (user_id, role_id) SELECT $1, id FROM roles WHERE name = 'admin'`,
    [adminId],
  );
  // overwrite the username we want to log in with (makeUser uses prefix_stamp)
  adminUsername = (
    await pool.query(`SELECT username FROM users WHERE id = $1`, [adminId])
  ).rows[0].username;

  adminCookie = await loginAndGetCookie(adminUsername, TEST_PASSWORD);

  permA = await makePermission("perm_a", stamp);
  permB = await makePermission("perm_b", stamp);
  permC = await makePermission("perm_c", stamp);

  // Main role: has perms A, B, C.
  mainRoleId = await makeRole("imp_main", stamp);
  for (const p of [permA, permB, permC]) {
    await pool.query(
      `INSERT INTO role_permissions (role_id, permission_id) VALUES ($1, $2)`,
      [mainRoleId, p.id],
    );
  }

  // Alone role: has only perm A. Used to ensure a user keeps perm A after main loses it.
  aloneRoleId = await makeRole("imp_alone", stamp);
  alsoGrantsP1RoleId = aloneRoleId;
  await pool.query(
    `INSERT INTO role_permissions (role_id, permission_id) VALUES ($1, $2)`,
    [aloneRoleId, permA.id],
  );

  // directUser: has main role directly. No other roles.
  directUserId = await makeUser("imp_direct", stamp);
  await pool.query(
    `INSERT INTO user_roles (user_id, role_id) VALUES ($1, $2)`,
    [directUserId, mainRoleId],
  );

  // groupedUser: receives main role via a group.
  groupedUserId = await makeUser("imp_grouped", stamp);
  helperGroupId = await makeGroup("imp_helper", stamp);
  await pool.query(
    `INSERT INTO user_groups (user_id, group_id) VALUES ($1, $2)`,
    [groupedUserId, helperGroupId],
  );
  await pool.query(
    `INSERT INTO group_roles (group_id, role_id) VALUES ($1, $2)`,
    [helperGroupId, mainRoleId],
  );

  // bothUser: has main role directly AND aloneRole (which also grants perm A).
  bothUserId = await makeUser("imp_both", stamp);
  await pool.query(
    `INSERT INTO user_roles (user_id, role_id) VALUES ($1, $2)`,
    [bothUserId, mainRoleId],
  );
  await pool.query(
    `INSERT INTO user_roles (user_id, role_id) VALUES ($1, $2)`,
    [bothUserId, aloneRoleId],
  );
});

after(async () => {
  if (createdRoleIds.length) {
    await pool.query(`DELETE FROM user_roles WHERE role_id = ANY($1::int[])`, [
      createdRoleIds,
    ]);
    await pool.query(`DELETE FROM group_roles 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,
    ]);
  }
  if (createdGroupIds.length) {
    await pool.query(
      `DELETE FROM group_roles WHERE group_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 (createdPermissionIds.length) {
    await pool.query(
      `DELETE FROM role_permissions WHERE permission_id = ANY($1::int[])`,
      [createdPermissionIds],
    );
    await pool.query(`DELETE FROM permissions WHERE id = ANY($1::int[])`, [
      createdPermissionIds,
    ]);
  }
  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/roles/:id/permissions/impact-preview returns empty when no permissions are removed", async () => {
  const res = await fetch(
    `${API_BASE}/api/roles/${mainRoleId}/permissions/impact-preview`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json", Cookie: adminCookie },
      body: JSON.stringify({ permissionIds: [permA.id, permB.id, permC.id] }),
    },
  );
  assert.equal(res.status, 200);
  const body = await res.json();
  assert.deepEqual(body.removed, []);
  assert.equal(body.totalAffectedUsers, 0);
});

test("POST /api/roles/:id/permissions/impact-preview reports affected users and groups for each removed permission", async () => {
  // Remove perm B and perm C from the role (keep perm A).
  const res = await fetch(
    `${API_BASE}/api/roles/${mainRoleId}/permissions/impact-preview`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json", Cookie: adminCookie },
      body: JSON.stringify({ permissionIds: [permA.id] }),
    },
  );
  assert.equal(res.status, 200);
  const body = await res.json();

  assert.equal(body.removed.length, 2);
  const byName = new Map(body.removed.map((r) => [r.permissionName, r]));
  const b = byName.get(permB.name);
  const c = byName.get(permC.name);
  assert.ok(b, "expected perm B in removed list");
  assert.ok(c, "expected perm C in removed list");

  // Three role-holders: directUser (direct), groupedUser (via helperGroup), bothUser (direct).
  // For perm B and perm C, no other role grants them, so all 3 lose them.
  assert.equal(b.userCount, 3);
  assert.equal(c.userCount, 3);

  // The role is granted by 1 group (helperGroup).
  assert.equal(b.groupCount, 1);
  assert.equal(c.groupCount, 1);
  assert.equal(b.groups.length, 1);
  assert.equal(b.groups[0].id, helperGroupId);
  assert.ok(typeof b.groups[0].name === "string" && b.groups[0].name.length > 0);

  assert.equal(body.totalAffectedUsers, 3);
});

test("impact-preview excludes users that still get the permission via another role", async () => {
  // Remove ALL permissions, including perm A which is also granted to bothUser via aloneRole.
  const res = await fetch(
    `${API_BASE}/api/roles/${mainRoleId}/permissions/impact-preview`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json", Cookie: adminCookie },
      body: JSON.stringify({ permissionIds: [] }),
    },
  );
  assert.equal(res.status, 200);
  const body = await res.json();
  assert.equal(body.removed.length, 3);
  const byName = new Map(body.removed.map((r) => [r.permissionName, r]));
  const a = byName.get(permA.name);
  assert.ok(a, "expected perm A in removed list");
  // bothUser still has perm A via aloneRole, so only 2 of 3 lose it.
  assert.equal(a.userCount, 2);
  // For B and C nobody else grants them, so all 3 lose them.
  assert.equal(byName.get(permB.name).userCount, 3);
  assert.equal(byName.get(permC.name).userCount, 3);
  // Total distinct affected users is 3 (all three lose at least one permission).
  assert.equal(body.totalAffectedUsers, 3);
});

test("impact-preview returns 404 for an unknown role", async () => {
  const res = await fetch(
    `${API_BASE}/api/roles/9999999/permissions/impact-preview`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json", Cookie: adminCookie },
      body: JSON.stringify({ permissionIds: [] }),
    },
  );
  assert.equal(res.status, 404);
});

test("impact-preview returns 400 when permissionIds is missing or invalid", async () => {
  const noBody = await fetch(
    `${API_BASE}/api/roles/${mainRoleId}/permissions/impact-preview`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json", Cookie: adminCookie },
      body: JSON.stringify({}),
    },
  );
  assert.equal(noBody.status, 400);

  const wrong = await fetch(
    `${API_BASE}/api/roles/${mainRoleId}/permissions/impact-preview`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json", Cookie: adminCookie },
      body: JSON.stringify({ permissionIds: ["abc"] }),
    },
  );
  assert.equal(wrong.status, 400);
});

test("impact-preview requires admin", async () => {
  const username = `imp_consumer_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
  const r = await pool.query(
    `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active)
     VALUES ($1, $2, $3, 'consumer', 'en', true) RETURNING id`,
    [username, `${username}@example.com`, TEST_PASSWORD_HASH],
  );
  const consumerId = r.rows[0].id;
  createdUserIds.push(consumerId);
  const consumerCookie = await loginAndGetCookie(username, TEST_PASSWORD);
  const res = await fetch(
    `${API_BASE}/api/roles/${mainRoleId}/permissions/impact-preview`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json", Cookie: consumerCookie },
      body: JSON.stringify({ permissionIds: [] }),
    },
  );
  assert.ok(res.status === 401 || res.status === 403, `expected 401/403, got ${res.status}`);
});
