// e2e: a recipient toggles a checklist item directly from the
// incoming-note popup, and the change is visible to the sender (and
// persists server-side).
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");

const TEST_PASSWORD = "TestPass123!";
const TEST_PASSWORD_HASH =
  "$2b$10$Bs636ukPMyz01nKrsi.5m.JlDXSN22AVCvn8cgPWWDbo5yJRQX2vu";

const pool = new pg.Pool({ connectionString: DATABASE_URL });
const createdUserIds = [];
const createdNoteIds = [];

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

async function createUser(prefix, displayEn) {
  const username = `${prefix}_${uniq()}`;
  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, displayEn],
  );
  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, displayEn };
}

test.afterAll(async () => {
  if (createdNoteIds.length > 0) {
    await pool.query(`DELETE FROM note_recipients WHERE note_id = ANY($1::int[])`, [createdNoteIds]);
    await pool.query(`DELETE FROM notes WHERE id = ANY($1::int[])`, [createdNoteIds]);
  }
  if (createdUserIds.length > 0) {
    await pool.query(`DELETE FROM notes WHERE user_id = ANY($1::int[])`, [createdUserIds]);
    await pool.query(`DELETE FROM notifications WHERE user_id = ANY($1::int[])`, [createdUserIds]);
    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) {
  await page.goto("/login");
  await page.locator("#username").fill(username);
  await page.locator("#password").fill(TEST_PASSWORD);
  await Promise.all([
    page.waitForURL((url) => !url.pathname.endsWith("/login"), { timeout: 15_000 }),
    page.locator('form button[type="submit"]').click(),
  ]);
}

test("recipient ticks a checklist item in the popup; sender sees the update", async ({
  browser,
}) => {
  test.setTimeout(120_000);
  const sender = await createUser("clcollab_sender", "CL Collab Sender");
  const recipient = await createUser("clcollab_recipient", "CL Collab Recipient");

  const senderCtx = await browser.newContext();
  const recipientCtx = await browser.newContext();
  const senderPage = await senderCtx.newPage();
  const recipientPage = await recipientCtx.newPage();

  try {
    await loginViaUi(recipientPage, recipient.username);
    await recipientPage.goto("/");
    await recipientPage.waitForTimeout(3500);

    await loginViaUi(senderPage, sender.username);
    await senderPage.goto("/notes");
    await expect(senderPage.getByTestId("notes-page")).toBeVisible();

    // Compose a checklist note with two items.
    await senderPage.getByTestId("notes-composer-open").click();
    await senderPage.getByTestId("notes-composer-kind-toggle").click();
    const list = senderPage.getByTestId("notes-composer-checklist-list");
    await expect(list).toBeVisible();
    await senderPage.getByRole("button", { name: "Add item" }).click();
    const itemInputs = list.locator("input:not([type=checkbox])");
    await itemInputs.first().fill("Buy milk");
    await itemInputs.first().press("Enter");
    await itemInputs.nth(1).fill("Walk dog");
    await itemInputs.nth(1).blur();

    const createPromise = senderPage.waitForResponse(
      (r) =>
        new URL(r.url()).pathname === "/api/notes" &&
        r.request().method() === "POST" &&
        r.status() < 300,
      { timeout: 15_000 },
    );
    await senderPage.getByTestId("notes-composer-save").click();
    const createResp = await createPromise;
    const created = await createResp.json();
    createdNoteIds.push(created.id);

    // Send to the recipient.
    const card = senderPage.getByTestId(`note-card-${created.id}`);
    await card.hover();
    await senderPage.getByTestId(`note-card-send-${created.id}`).click();
    await senderPage.getByTestId("send-note-search").fill(recipient.displayEn);
    await senderPage.getByTestId(`send-recipient-option-${recipient.id}`).click();
    const sendPromise = senderPage.waitForResponse(
      (r) =>
        new URL(r.url()).pathname === `/api/notes/${created.id}/send` &&
        r.status() < 300,
      { timeout: 15_000 },
    );
    await senderPage.getByTestId("send-note-submit").click();
    await senderPage.getByTestId("send-note-confirm-submit").click();
    await sendPromise;

    // Recipient sees the popup with the checklist.
    const popup = recipientPage.getByTestId("incoming-note-popup");
    await expect(popup).toBeVisible({ timeout: 5_000 });
    const checklist = recipientPage.getByTestId("incoming-note-popup-checklist");
    await expect(checklist).toBeVisible();

    // Tick the first item from the popup. The first checkbox in the
    // checklist has data-testid `incoming-note-popup-checklist-check-<id>`.
    const firstCheckbox = checklist.locator('input[type="checkbox"]').first();
    const togglePromise = recipientPage.waitForResponse(
      (r) =>
        /\/api\/notes\/\d+\/checklist\/.+\/toggle$/.test(
          new URL(r.url()).pathname,
        ) && r.status() < 300,
      { timeout: 15_000 },
    );
    await firstCheckbox.check();
    await togglePromise;

    // After the mutation settles the popup checkbox must remain
    // checked — the actor doesn't get a socket echo, so this verifies
    // the popup queue patch driven by the mutation response.
    await expect(firstCheckbox).toBeChecked({ timeout: 3_000 });
    // Wait one paint frame and re-assert to guard against late state
    // resets that would only manifest after the mutation finishes.
    await recipientPage.waitForTimeout(400);
    await expect(firstCheckbox).toBeChecked();

    // Server-side state confirms the toggle persisted (and was mirrored
    // to the recipient snapshot).
    const { rows } = await pool.query(`SELECT items FROM notes WHERE id = $1`, [
      created.id,
    ]);
    expect(rows[0].items[0].done).toBe(true);

    // Sender's open notes page reflects the shared change after the
    // socket fanout invalidates the cache. We confirm both DB state
    // (recipient snapshot mirrored) AND the sender-rendered card on
    // their /notes page after a refresh — the latter is the actual UX
    // promise of "everyone sees the same shared state".
    await expect
      .poll(
        async () =>
          (
            await pool.query(`SELECT items FROM note_recipients WHERE note_id = $1`, [
              created.id,
            ])
          ).rows[0].items[0].done,
        { timeout: 5_000 },
      )
      .toBe(true);

    await senderPage.reload();
    await expect(senderPage.getByTestId("notes-page")).toBeVisible();
    const senderCard = senderPage.getByTestId(`note-card-${created.id}`);
    await expect(senderCard).toBeVisible();
    const senderCheckboxes = senderCard.locator('input[type="checkbox"]');
    await expect(senderCheckboxes.first()).toBeChecked({ timeout: 5_000 });
  } finally {
    await senderCtx.close();
    await recipientCtx.close();
  }
});
