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

const createdUserIds = [];
const createdConversationIds = [];

async function createUser(prefix) {
  const username = `${prefix}_${Date.now().toString(36)}_${Math.random()
    .toString(36)
    .slice(2, 8)}`;
  const { rows } = await pool.query(
    `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active)
     VALUES ($1, $2, $3, 'Leave Test', 'en', true) RETURNING id`,
    [username, `${username}@example.com`, TEST_PASSWORD_HASH],
  );
  const id = rows[0].id;
  createdUserIds.push(id);
  await pool.query(
    `INSERT INTO user_roles (user_id, role_id)
     SELECT $1, id FROM roles WHERE name = 'user'`,
    [id],
  );
  return { id, username };
}

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");
  assert.ok(setCookie, "expected Set-Cookie header from login");
  const sid = setCookie
    .split(",")
    .map((c) => c.split(";")[0].trim())
    .find((c) => c.startsWith("connect.sid="));
  assert.ok(sid, "expected connect.sid cookie");
  return sid;
}

async function createGroup(creatorId, members) {
  const { rows } = await pool.query(
    `INSERT INTO conversations (name_en, is_group, created_by)
     VALUES ('Leave Test Group', true, $1) RETURNING id`,
    [creatorId],
  );
  const conversationId = rows[0].id;
  createdConversationIds.push(conversationId);

  // Insert participants in a deterministic joined_at order so that auto
  // promotion picks the expected user.
  let offsetMs = 0;
  for (const m of members) {
    await pool.query(
      `INSERT INTO conversation_participants
         (conversation_id, user_id, is_admin, joined_at)
       VALUES ($1, $2, $3, NOW() - ($4 || ' milliseconds')::interval)`,
      [conversationId, m.userId, !!m.isAdmin, 10000 - offsetMs],
    );
    offsetMs += 1000;
  }
  return conversationId;
}

async function getParticipants(conversationId) {
  const { rows } = await pool.query(
    `SELECT user_id AS "userId", is_admin AS "isAdmin"
     FROM conversation_participants
     WHERE conversation_id = $1
     ORDER BY joined_at ASC`,
    [conversationId],
  );
  return rows;
}

async function conversationExists(conversationId) {
  const { rows } = await pool.query(
    `SELECT 1 FROM conversations WHERE id = $1`,
    [conversationId],
  );
  return rows.length > 0;
}

async function leave(conversationId, cookie, body) {
  const res = await fetch(
    `${API_BASE}/api/conversations/${conversationId}/leave`,
    {
      method: "POST",
      headers: {
        Cookie: cookie,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body ?? {}),
    },
  );
  if (process.env.LEAVE_TEST_DEBUG && res.status >= 400) {
    const text = await res.clone().text();
    console.error(
      `[leave debug] status=${res.status} cookie=${cookie} body=${text}`,
    );
  }
  return res;
}

after(async () => {
  if (createdConversationIds.length > 0) {
    await pool.query(
      `DELETE FROM messages WHERE conversation_id = ANY($1::int[])`,
      [createdConversationIds],
    );
    await pool.query(
      `DELETE FROM conversation_participants WHERE conversation_id = ANY($1::int[])`,
      [createdConversationIds],
    );
    await pool.query(
      `DELETE FROM conversations WHERE id = ANY($1::int[])`,
      [createdConversationIds],
    );
  }
  if (createdUserIds.length > 0) {
    await pool.query(`DELETE FROM user_roles WHERE user_id = ANY($1::int[])`, [
      createdUserIds,
    ]);
    await pool.query(`DELETE FROM users WHERE id = ANY($1::int[])`, [
      createdUserIds,
    ]);
  }
  await pool.end();
});

test("solo member leaving a group deletes the conversation", async () => {
  const solo = await createUser("leave_solo");
  const conv = await createGroup(solo.id, [{ userId: solo.id, isAdmin: true }]);
  const cookie = await loginAndGetCookie(solo.username, TEST_PASSWORD);

  const res = await leave(conv, cookie);
  assert.equal(res.status, 200);
  const json = await res.json();
  assert.equal(json.success, true);

  assert.equal(
    await conversationExists(conv),
    false,
    "conversation should be deleted when last member leaves",
  );
});

test("sole admin leaving auto-promotes the earliest remaining member", async () => {
  const admin = await createUser("leave_admin_auto");
  const m1 = await createUser("leave_m1");
  const m2 = await createUser("leave_m2");
  // admin joined first, then m1, then m2 (handled by createGroup ordering)
  const conv = await createGroup(admin.id, [
    { userId: admin.id, isAdmin: true },
    { userId: m1.id, isAdmin: false },
    { userId: m2.id, isAdmin: false },
  ]);
  const cookie = await loginAndGetCookie(admin.username, TEST_PASSWORD);

  const res = await leave(conv, cookie);
  assert.equal(res.status, 200);

  const parts = await getParticipants(conv);
  assert.equal(parts.length, 2);
  assert.ok(
    !parts.find((p) => p.userId === admin.id),
    "leaver must be removed",
  );
  const promoted = parts.find((p) => p.isAdmin);
  assert.ok(promoted, "an admin should have been auto-promoted");
  assert.equal(
    promoted.userId,
    m1.id,
    "earliest joined remaining member should be promoted",
  );
});

test("sole admin leaving with a chosen successor promotes that user", async () => {
  const admin = await createUser("leave_admin_chosen");
  const m1 = await createUser("leave_chosen_m1");
  const m2 = await createUser("leave_chosen_m2");
  const conv = await createGroup(admin.id, [
    { userId: admin.id, isAdmin: true },
    { userId: m1.id, isAdmin: false },
    { userId: m2.id, isAdmin: false },
  ]);
  const cookie = await loginAndGetCookie(admin.username, TEST_PASSWORD);

  const res = await leave(conv, cookie, { successorId: m2.id });
  assert.equal(res.status, 200);

  const parts = await getParticipants(conv);
  assert.equal(parts.length, 2);
  const promoted = parts.find((p) => p.isAdmin);
  assert.ok(promoted, "an admin should be present after handoff");
  assert.equal(
    promoted.userId,
    m2.id,
    "the chosen successor should be the new admin",
  );
});

test("sole admin leaving with an invalid successor returns 400 and does not leave", async () => {
  const admin = await createUser("leave_admin_bad");
  const m1 = await createUser("leave_bad_m1");
  const outsider = await createUser("leave_bad_outsider");
  const conv = await createGroup(admin.id, [
    { userId: admin.id, isAdmin: true },
    { userId: m1.id, isAdmin: false },
  ]);
  const cookie = await loginAndGetCookie(admin.username, TEST_PASSWORD);

  const res = await leave(conv, cookie, { successorId: outsider.id });
  assert.equal(res.status, 400);

  const parts = await getParticipants(conv);
  assert.equal(parts.length, 2, "membership should be unchanged");
  const adminRow = parts.find((p) => p.userId === admin.id);
  assert.ok(adminRow, "leaver should still be a member");
  assert.equal(adminRow.isAdmin, true, "leaver should still be admin");
  const m1Row = parts.find((p) => p.userId === m1.id);
  assert.equal(m1Row.isAdmin, false, "m1 should not have been promoted");
});

test("non-admin leaving a group does not promote anyone", async () => {
  const admin = await createUser("leave_keep_admin");
  const m1 = await createUser("leave_nonadmin_leaver");
  const m2 = await createUser("leave_nonadmin_other");
  const conv = await createGroup(admin.id, [
    { userId: admin.id, isAdmin: true },
    { userId: m1.id, isAdmin: false },
    { userId: m2.id, isAdmin: false },
  ]);
  const cookie = await loginAndGetCookie(m1.username, TEST_PASSWORD);

  const res = await leave(conv, cookie);
  assert.equal(res.status, 200);

  const parts = await getParticipants(conv);
  assert.equal(parts.length, 2);
  assert.ok(
    !parts.find((p) => p.userId === m1.id),
    "leaver must be removed",
  );
  const admins = parts.filter((p) => p.isAdmin);
  assert.equal(admins.length, 1, "exactly the original admin remains admin");
  assert.equal(admins[0].userId, admin.id);
  const m2Row = parts.find((p) => p.userId === m2.id);
  assert.equal(m2Row.isAdmin, false, "non-admin member should not be promoted");
});
