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 consumerUserId;
let consumerGroupId;
const createdRoleIds = [];
const createdGroupIds = [];
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="));
}

before(async () => {
  const stamp = `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
  adminUsername = `roles_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, 'Roles 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],
  );

  const consumer = await pool.query(
    `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active)
     VALUES ($1, $2, $3, 'Roles Consumer', 'en', true) RETURNING id`,
    [
      `roles_consumer_${stamp}`,
      `roles_consumer_${stamp}@example.com`,
      TEST_PASSWORD_HASH,
    ],
  );
  consumerUserId = consumer.rows[0].id;
  createdUserIds.push(consumerUserId);

  const grp = await pool.query(
    `INSERT INTO groups (name, description_en) VALUES ($1, 'roles test group') RETURNING id`,
    [`RolesGrp_${stamp}`],
  );
  consumerGroupId = grp.rows[0].id;
  createdGroupIds.push(consumerGroupId);

  adminCookie = await loginAndGetCookie(adminUsername, TEST_PASSWORD);
});

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,
    ]);
  }
  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();
});

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

test("POST /api/roles creates a role and returns 201", async () => {
  const name = uniqueRoleName("role_create");
  const res = await fetch(`${API_BASE}/api/roles`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Cookie: adminCookie },
    body: JSON.stringify({
      name,
      descriptionEn: "Created by automated test",
      descriptionAr: "تم إنشاؤه بواسطة الاختبار",
    }),
  });
  assert.equal(res.status, 201);
  const created = await res.json();
  createdRoleIds.push(created.id);
  assert.equal(created.name, name);
  assert.equal(created.descriptionEn, "Created by automated test");
  assert.equal(created.descriptionAr, "تم إنشاؤه بواسطة الاختبار");
  assert.equal(created.isSystem, false);

  const dbRow = await pool.query(`SELECT id, name FROM roles WHERE id = $1`, [
    created.id,
  ]);
  assert.equal(dbRow.rowCount, 1);
  assert.equal(dbRow.rows[0].name, name);
});

test("POST /api/roles with a duplicate name returns 409", async () => {
  const name = uniqueRoleName("role_dup");
  const first = await fetch(`${API_BASE}/api/roles`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Cookie: adminCookie },
    body: JSON.stringify({ name }),
  });
  assert.equal(first.status, 201);
  const firstBody = await first.json();
  createdRoleIds.push(firstBody.id);

  const dup = await fetch(`${API_BASE}/api/roles`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Cookie: adminCookie },
    body: JSON.stringify({ name }),
  });
  assert.equal(dup.status, 409);
  const body = await dup.json();
  assert.match(String(body.error || ""), /already taken/i);

  const dbRows = await pool.query(`SELECT id FROM roles WHERE name = $1`, [
    name,
  ]);
  assert.equal(dbRows.rowCount, 1, "duplicate must not insert a second row");
});

test("POST /api/roles rejects an invalid name with 400", async () => {
  const badName = "1bad name!";
  const res = await fetch(`${API_BASE}/api/roles`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Cookie: adminCookie },
    body: JSON.stringify({ name: badName }),
  });
  assert.equal(res.status, 400);
  // Scope the leak check to the specific bad name so this test is robust
  // when run in parallel with other suites that legitimately create roles.
  const leak = await pool.query(`SELECT id FROM roles WHERE name = $1`, [
    badName,
  ]);
  assert.equal(leak.rowCount, 0, "no role row should leak");
});

test("PATCH /api/roles/:id updates description without touching the name", async () => {
  const name = uniqueRoleName("role_patch");
  const create = await fetch(`${API_BASE}/api/roles`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Cookie: adminCookie },
    body: JSON.stringify({ name, descriptionEn: "before" }),
  });
  assert.equal(create.status, 201);
  const created = await create.json();
  createdRoleIds.push(created.id);

  const patch = await fetch(`${API_BASE}/api/roles/${created.id}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json", Cookie: adminCookie },
    body: JSON.stringify({ descriptionEn: "after", descriptionAr: "بعد" }),
  });
  assert.equal(patch.status, 200);
  const updated = await patch.json();
  assert.equal(updated.id, created.id);
  assert.equal(updated.name, name, "name must be unchanged");
  assert.equal(updated.descriptionEn, "after");
  assert.equal(updated.descriptionAr, "بعد");

  const dbRow = await pool.query(
    `SELECT name, description_en, description_ar FROM roles WHERE id = $1`,
    [created.id],
  );
  assert.equal(dbRow.rows[0].name, name);
  assert.equal(dbRow.rows[0].description_en, "after");
  assert.equal(dbRow.rows[0].description_ar, "بعد");
});

test("DELETE /api/roles/:id refuses to delete a system role with 400", async () => {
  const sysRow = await pool.query(
    `SELECT id FROM roles WHERE name = 'admin' LIMIT 1`,
  );
  assert.ok(sysRow.rowCount > 0, "system 'admin' role must exist");
  const sysId = sysRow.rows[0].id;

  const res = await fetch(`${API_BASE}/api/roles/${sysId}`, {
    method: "DELETE",
    headers: { Cookie: adminCookie },
  });
  assert.equal(res.status, 400);
  const body = await res.json();
  assert.match(String(body.error || ""), /system role/i);

  const stillThere = await pool.query(`SELECT id FROM roles WHERE id = $1`, [
    sysId,
  ]);
  assert.equal(stillThere.rowCount, 1, "system role must not be deleted");
});

test("DELETE /api/roles/:id returns 409 with userCount/groupCount when in use", async () => {
  const name = uniqueRoleName("role_inuse");
  const create = await fetch(`${API_BASE}/api/roles`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Cookie: adminCookie },
    body: JSON.stringify({ name }),
  });
  assert.equal(create.status, 201);
  const created = await create.json();
  createdRoleIds.push(created.id);

  await pool.query(
    `INSERT INTO user_roles (user_id, role_id) VALUES ($1, $2)`,
    [consumerUserId, created.id],
  );
  await pool.query(
    `INSERT INTO group_roles (group_id, role_id) VALUES ($1, $2)`,
    [consumerGroupId, created.id],
  );

  const res = await fetch(`${API_BASE}/api/roles/${created.id}`, {
    method: "DELETE",
    headers: { Cookie: adminCookie },
  });
  assert.equal(res.status, 409);
  const body = await res.json();
  assert.match(String(body.error || ""), /in use/i);
  assert.equal(body.userCount, 1);
  assert.equal(body.groupCount, 1);

  const stillThere = await pool.query(`SELECT id FROM roles WHERE id = $1`, [
    created.id,
  ]);
  assert.equal(stillThere.rowCount, 1, "in-use role must not be deleted");
});

test("DELETE /api/roles/:id returns 204 and removes the row when unused", async () => {
  const name = uniqueRoleName("role_del");
  const create = await fetch(`${API_BASE}/api/roles`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Cookie: adminCookie },
    body: JSON.stringify({ name }),
  });
  assert.equal(create.status, 201);
  const created = await create.json();

  const res = await fetch(`${API_BASE}/api/roles/${created.id}`, {
    method: "DELETE",
    headers: { Cookie: adminCookie },
  });
  assert.equal(res.status, 204);

  const gone = await pool.query(`SELECT id FROM roles WHERE id = $1`, [
    created.id,
  ]);
  assert.equal(gone.rowCount, 0, "role row should be gone after delete");
});
