// UI test for the leave-group successor picker dialog in chat.tsx.
//
// Mirrors the backend coverage in
// artifacts/api-server/tests/conversations-leave.test.mjs but at the UI layer:
// it drives the actual dialog rendered by artifacts/tx-os/src/pages/chat.tsx
// in a real browser, including:
//   - The cancel path (close the dialog -> nothing changes).
//   - The chosen-successor path (pick a specific member -> they become admin).
//
// Run with:
//   DATABASE_URL=... pnpm --filter @workspace/tx-os exec playwright test
//
// Browser binaries can be installed once with:
//   pnpm --filter @workspace/tx-os exec playwright install chromium
//
// The test seeds its own users + group via the database and cleans up after
// itself, so it can run against the live dev environment without polluting it.

import { test, expect } from "@playwright/test";
import pg from "pg";

const DATABASE_URL = process.env.DATABASE_URL;
if (!DATABASE_URL) {
  throw new Error("DATABASE_URL must be set to run the leave-group UI test");
}

// Same convention as artifacts/api-server/tests/conversations-leave.test.mjs:
// a precomputed bcrypt hash of the literal password "TestPass123!".
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 = [];

function uniqueSuffix() {
  return `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
}

async function createUser(prefix) {
  const username = `${prefix}_${uniqueSuffix()}`;
  const { rows } = 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`,
    [username, `${username}@example.com`, TEST_PASSWORD_HASH, prefix],
  );
  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, displayName: prefix };
}

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

  // Insert participants in deterministic joined_at order so that the auto
  // promotion would pick the *first* non-admin member. The test then verifies
  // that an explicit successor choice can override that default.
  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, groupName };
}

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

test.afterAll(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();
});

async function loginViaUi(page, username, password) {
  await page.goto("/login");
  await page.locator("#username").fill(username);
  await page.locator("#password").fill(password);
  await Promise.all([
    page.waitForURL((url) => !url.pathname.endsWith("/login"), {
      timeout: 15_000,
    }),
    page.locator('form button[type="submit"]').click(),
  ]);
}

async function openLeaveDialog(page, groupName) {
  await page.goto("/chat");
  // Wait for the conversation list to load and click the seeded group.
  await page.getByRole("button", { name: new RegExp(groupName) }).first().click();
  // Open the actions sheet, then "Leave group".
  await page.locator('[data-testid="button-chat-actions"]').click();
  await expect(page.locator('[data-testid="dialog-chat-actions"]')).toBeVisible();
  await page.locator('[data-testid="button-leave-group"]').click();
  await expect(page.locator('[data-testid="dialog-confirm-leave"]')).toBeVisible();
  await expect(page.locator('[data-testid="successor-chooser"]')).toBeVisible();
}

test.describe("Leave group: successor picker dialog", () => {
  test("cancel path leaves the group and admin assignments unchanged", async ({
    page,
  }) => {
    const admin = await createUser("leave_ui_cancel_admin");
    const m1 = await createUser("leave_ui_cancel_m1");
    const m2 = await createUser("leave_ui_cancel_m2");
    const { conversationId, groupName } = await createGroup(admin.id, [
      { userId: admin.id, isAdmin: true },
      { userId: m1.id, isAdmin: false },
      { userId: m2.id, isAdmin: false },
    ]);

    await loginViaUi(page, admin.username, TEST_PASSWORD);
    await openLeaveDialog(page, groupName);

    // Auto option is selected by default; both successor options are listed.
    await expect(
      page.locator('[data-testid="successor-option-auto"] input[type="radio"]'),
    ).toBeChecked();
    await expect(
      page.locator(`[data-testid="successor-option-${m1.id}"]`),
    ).toBeVisible();
    await expect(
      page.locator(`[data-testid="successor-option-${m2.id}"]`),
    ).toBeVisible();

    // Click the Cancel button inside the leave dialog.
    await page
      .locator('[data-testid="dialog-confirm-leave"] button')
      .filter({ hasText: /^(Cancel|إلغاء)$/ })
      .click();

    await expect(
      page.locator('[data-testid="dialog-confirm-leave"]'),
    ).toBeHidden();

    // Membership and admin flags must be unchanged after a cancel.
    const parts = await getParticipants(conversationId);
    expect(parts).toHaveLength(3);
    const adminRow = parts.find((p) => p.userId === admin.id);
    const m1Row = parts.find((p) => p.userId === m1.id);
    const m2Row = parts.find((p) => p.userId === m2.id);
    expect(adminRow?.isAdmin).toBe(true);
    expect(m1Row?.isAdmin).toBe(false);
    expect(m2Row?.isAdmin).toBe(false);
  });

  test("picking a specific successor promotes that member, not the auto pick", async ({
    page,
  }) => {
    const admin = await createUser("leave_ui_pick_admin");
    const m1 = await createUser("leave_ui_pick_m1"); // would be the auto pick
    const m2 = await createUser("leave_ui_pick_m2"); // explicitly chosen
    const { conversationId, groupName } = await createGroup(admin.id, [
      { userId: admin.id, isAdmin: true },
      { userId: m1.id, isAdmin: false },
      { userId: m2.id, isAdmin: false },
    ]);

    await loginViaUi(page, admin.username, TEST_PASSWORD);
    await openLeaveDialog(page, groupName);

    // Pick the later-joined member (m2) as the successor.
    await page
      .locator(`[data-testid="successor-option-${m2.id}"] input[type="radio"]`)
      .check();
    await expect(
      page.locator(`[data-testid="successor-option-${m2.id}"] input[type="radio"]`),
    ).toBeChecked();
    await expect(
      page.locator('[data-testid="successor-option-auto"] input[type="radio"]'),
    ).not.toBeChecked();

    // Confirm the leave.
    const leavePromise = page.waitForResponse(
      (resp) =>
        resp.url().includes(`/api/conversations/${conversationId}/leave`) &&
        resp.request().method() === "POST",
    );
    await page.locator('[data-testid="button-confirm-leave"]').click();
    const leaveResp = await leavePromise;
    expect(leaveResp.status()).toBe(200);

    // The dialog closes and the user is returned to the conversation list.
    await expect(
      page.locator('[data-testid="dialog-confirm-leave"]'),
    ).toBeHidden();

    // The chosen successor — not the auto pick — must be the new admin.
    const parts = await getParticipants(conversationId);
    expect(parts).toHaveLength(2);
    expect(parts.find((p) => p.userId === admin.id)).toBeUndefined();
    const m1Row = parts.find((p) => p.userId === m1.id);
    const m2Row = parts.find((p) => p.userId === m2.id);
    expect(m1Row?.isAdmin).toBe(false);
    expect(m2Row?.isAdmin).toBe(true);
  });
});
