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 groupAdminUsername;
let memberUsername;
let outsiderUsername;
let groupAdminId;
let memberId;
let outsiderId;
let groupAdminCookie;
let memberCookie;
let outsiderCookie;
let groupId;
let restrictedAppId;
let unrestrictedAppId;
let permissionId;

async function login(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)}`;
  groupAdminUsername = `gv_gadmin_${stamp}`;
  memberUsername = `gv_member_${stamp}`;
  outsiderUsername = `gv_out_${stamp}`;

  // Create three users without any direct admin role.
  const ga = await pool.query(
    `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active)
     VALUES ($1, $2, $3, 'Group Admin', 'en', true) RETURNING id`,
    [groupAdminUsername, `${groupAdminUsername}@ex.com`, TEST_PASSWORD_HASH],
  );
  groupAdminId = ga.rows[0].id;

  const m = await pool.query(
    `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active)
     VALUES ($1, $2, $3, 'Member', 'en', true) RETURNING id`,
    [memberUsername, `${memberUsername}@ex.com`, TEST_PASSWORD_HASH],
  );
  memberId = m.rows[0].id;

  const o = await pool.query(
    `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active)
     VALUES ($1, $2, $3, 'Outsider', 'en', true) RETURNING id`,
    [outsiderUsername, `${outsiderUsername}@ex.com`, TEST_PASSWORD_HASH],
  );
  outsiderId = o.rows[0].id;

  // Give member + outsider the basic 'user' role so they can list apps.
  await pool.query(
    `INSERT INTO user_roles (user_id, role_id)
     SELECT u.id, r.id FROM users u, roles r
     WHERE u.id IN ($1, $2, $3) AND r.name = 'user'`,
    [groupAdminId, memberId, outsiderId],
  );

  // Create a fresh group, attach 'admin' role to it (group-derived admin).
  const grp = await pool.query(
    `INSERT INTO groups (name, description_en) VALUES ($1, $2)
     RETURNING id`,
    [`vis_group_${stamp}`, "visibility test group"],
  );
  groupId = grp.rows[0].id;

  // group-derived admin → groupAdmin user
  await pool.query(
    `INSERT INTO group_roles (group_id, role_id)
     SELECT $1, id FROM roles WHERE name = 'admin'`,
    [groupId],
  );
  await pool.query(
    `INSERT INTO user_groups (user_id, group_id) VALUES ($1, $2)`,
    [groupAdminId, groupId],
  );

  // Create an unrestricted app (no permissions attached).
  const unr = await pool.query(
    `INSERT INTO apps (name_en, name_ar, slug, icon_name, route, color, sort_order, is_active)
     VALUES ($1, $1, $2, 'Grid', '/test-unr', '#6366f1', 999, true) RETURNING id`,
    [`UnrestrictedApp_${stamp}`, `unr_${stamp}`],
  );
  unrestrictedAppId = unr.rows[0].id;

  // Create a restricted app gated by a fresh permission.
  const rest = await pool.query(
    `INSERT INTO apps (name_en, name_ar, slug, icon_name, route, color, sort_order, is_active)
     VALUES ($1, $1, $2, 'Grid', '/test-rest', '#6366f1', 998, true) RETURNING id`,
    [`RestrictedApp_${stamp}`, `rest_${stamp}`],
  );
  restrictedAppId = rest.rows[0].id;

  const perm = await pool.query(
    `INSERT INTO permissions (name, description_en) VALUES ($1, $2) RETURNING id`,
    [`vis_perm_${stamp}`, "visibility test perm"],
  );
  permissionId = perm.rows[0].id;

  await pool.query(
    `INSERT INTO app_permissions (app_id, permission_id) VALUES ($1, $2)`,
    [restrictedAppId, permissionId],
  );

  // Grant the restricted app to the group via group_apps → only group members see it.
  await pool.query(
    `INSERT INTO group_apps (group_id, app_id) VALUES ($1, $2)`,
    [groupId, restrictedAppId],
  );

  // Add 'member' user to the group too (so they should see the restricted app).
  await pool.query(
    `INSERT INTO user_groups (user_id, group_id) VALUES ($1, $2)`,
    [memberId, groupId],
  );

  groupAdminCookie = await login(groupAdminUsername, TEST_PASSWORD);
  memberCookie = await login(memberUsername, TEST_PASSWORD);
  outsiderCookie = await login(outsiderUsername, TEST_PASSWORD);
});

after(async () => {
  await pool.query(`DELETE FROM group_apps WHERE group_id = $1`, [groupId]);
  await pool.query(`DELETE FROM group_roles WHERE group_id = $1`, [groupId]);
  await pool.query(
    `DELETE FROM user_groups WHERE user_id = ANY($1::int[]) OR group_id = $2`,
    [[groupAdminId, memberId, outsiderId], groupId],
  );
  await pool.query(
    `DELETE FROM user_roles WHERE user_id = ANY($1::int[])`,
    [[groupAdminId, memberId, outsiderId]],
  );
  await pool.query(`DELETE FROM groups WHERE id = $1`, [groupId]);
  await pool.query(
    `DELETE FROM app_permissions WHERE app_id = ANY($1::int[])`,
    [[restrictedAppId, unrestrictedAppId]],
  );
  await pool.query(`DELETE FROM permissions WHERE id = $1`, [permissionId]);
  await pool.query(
    `DELETE FROM apps WHERE id = ANY($1::int[])`,
    [[restrictedAppId, unrestrictedAppId]],
  );
  await pool.query(`DELETE FROM users WHERE id = ANY($1::int[])`, [
    [groupAdminId, memberId, outsiderId],
  ]);
  await pool.end();
});

async function listApps(cookie) {
  const res = await fetch(`${API_BASE}/api/apps`, {
    headers: { cookie },
  });
  assert.equal(res.status, 200);
  return res.json();
}

test("group-derived admin sees both restricted and unrestricted apps", async () => {
  const apps = await listApps(groupAdminCookie);
  const ids = apps.map((a) => a.id);
  assert.ok(ids.includes(unrestrictedAppId), "admin should see unrestricted app");
  assert.ok(ids.includes(restrictedAppId), "admin should see restricted app");
});

test("group member sees the group-granted restricted app and the unrestricted one", async () => {
  const apps = await listApps(memberCookie);
  const ids = apps.map((a) => a.id);
  assert.ok(ids.includes(unrestrictedAppId), "member should see unrestricted app");
  assert.ok(
    ids.includes(restrictedAppId),
    "member should see restricted app via group_apps",
  );
});

test("non-member outsider sees unrestricted but NOT the restricted app", async () => {
  const apps = await listApps(outsiderCookie);
  const ids = apps.map((a) => a.id);
  assert.ok(ids.includes(unrestrictedAppId), "outsider should see unrestricted app");
  assert.ok(
    !ids.includes(restrictedAppId),
    "outsider must NOT see restricted app (no permission, no group)",
  );
});
