// E2E regression for the per-note checklist (to-do list) option.
// 1) Composer can switch to checklist mode, items can be added and saved.
// 2) After reload, the saved checklist persists with its items + done state.
// 3) Sending a checklist note delivers the items to the recipient snapshot.
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 this UI test");
}

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

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

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

test.afterAll(async () => {
  if (createdUserIds.length > 0) {
    await pool.query(
      `DELETE FROM note_recipients WHERE sender_user_id = ANY($1::int[]) OR recipient_user_id = ANY($1::int[])`,
      [createdUserIds],
    );
    await pool.query(`DELETE FROM notes 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("composer creates a checklist note that persists across reloads", async ({
  page,
}) => {
  const user = await createUser("notes_checklist_e2e", "Checklist Tester");
  await loginViaUi(page, user.username);

  await page.goto("/notes");
  await expect(page.getByTestId("notes-page")).toBeVisible();

  // Open composer and switch to checklist.
  await page.getByTestId("notes-composer").click();
  await page.getByTestId("notes-composer-kind-toggle").click();

  // Add two items via the "Add item" button.
  const composer = page.getByTestId("notes-composer");
  await composer.getByTestId("notes-composer-checklist-add").click();
  // The newly inserted row gets focus; type into it.
  await page.keyboard.type("Buy milk");
  await composer.getByTestId("notes-composer-checklist-add").click();
  await page.keyboard.type("Walk the dog");

  // Save via the explicit Save button (synchronous, no portal-click risk).
  const savePromise = page.waitForResponse(
    (r) =>
      new URL(r.url()).pathname === "/api/notes" &&
      r.request().method() === "POST" &&
      r.status() >= 200 &&
      r.status() < 300,
  );
  await page.getByTestId("notes-composer-save").click();
  const resp = await savePromise;
  const note = await resp.json();
  expect(note.kind).toBe("checklist");
  expect(Array.isArray(note.items)).toBe(true);
  expect(note.items.map((i) => i.text)).toEqual(["Buy milk", "Walk the dog"]);

  // The note card should render the checklist with both rows visible.
  const card = page.getByTestId(`note-card-${note.id}`);
  await expect(card).toBeVisible();
  await expect(card.getByText("Buy milk")).toBeVisible();
  await expect(card.getByText("Walk the dog")).toBeVisible();

  // Tick the first item from the card, expect a PATCH that flips done.
  const firstItemId = note.items[0].id;
  const togglePromise = page.waitForResponse(
    (r) =>
      new URL(r.url()).pathname === `/api/notes/${note.id}` &&
      r.request().method() === "PATCH" &&
      r.status() >= 200 &&
      r.status() < 300,
  );
  await card
    .getByTestId(`note-card-checklist-${note.id}-check-${firstItemId}`)
    .click();
  const patched = await togglePromise;
  const patchedNote = await patched.json();
  expect(patchedNote.kind).toBe("checklist");
  const patchedFirst = patchedNote.items.find((i) => i.id === firstItemId);
  expect(patchedFirst?.done).toBe(true);

  // Reload — the persisted checklist with the toggled state must come back.
  await page.reload();
  await expect(page.getByTestId(`note-card-${note.id}`)).toBeVisible();
  const reloadedFirst = page.getByTestId(
    `note-card-checklist-${note.id}-check-${firstItemId}`,
  );
  await expect(reloadedFirst).toBeChecked();
});

test("PATCH items without kind normalizes against the note's current kind", async ({
  page,
}) => {
  const user = await createUser("notes_checklist_norm", "Norm Tester");
  await loginViaUi(page, user.username);

  // Create a plain text note (kind defaults to "text").
  const textResp = await page.request.post("/api/notes", {
    data: { title: `text-note ${uniq()}`, content: "hello" },
  });
  expect(textResp.ok()).toBe(true);
  const textNote = await textResp.json();
  expect(textNote.kind).toBe("text");

  // Patch only `items` (no kind). Server must coerce items back to null
  // because the note's current kind is "text".
  const patchTextResp = await page.request.patch(`/api/notes/${textNote.id}`, {
    data: { items: [{ id: "x", text: "should be dropped", done: false }] },
  });
  expect(patchTextResp.ok()).toBe(true);
  const patchedText = await patchTextResp.json();
  expect(patchedText.kind).toBe("text");
  expect(patchedText.items).toBeNull();

  // Now create a checklist note and patch only `items` (no kind). Items
  // must persist because the note's current kind is "checklist".
  const clResp = await page.request.post("/api/notes", {
    data: {
      title: `cl-note ${uniq()}`,
      kind: "checklist",
      items: [{ id: "a", text: "first", done: false }],
    },
  });
  const cl = await clResp.json();
  const patchClResp = await page.request.patch(`/api/notes/${cl.id}`, {
    data: { items: [{ id: "a", text: "first", done: true }] },
  });
  expect(patchClResp.ok()).toBe(true);
  const patchedCl = await patchClResp.json();
  expect(patchedCl.kind).toBe("checklist");
  expect(patchedCl.items[0].done).toBe(true);
});

test("checklist notes are delivered to recipients with their items", async ({
  page,
}) => {
  const sender = await createUser("notes_checklist_send_s", "Sender");
  const recipient = await createUser("notes_checklist_send_r", "Recipient");
  await loginViaUi(page, sender.username);

  // Create the checklist note directly via the API (we already cover the
  // composer flow above; this test focuses on the send/snapshot path).
  const createResp = await page.request.post("/api/notes", {
    data: {
      title: `send-checklist ${uniq()}`,
      kind: "checklist",
      items: [
        { id: "a", text: "Item A", done: false },
        { id: "b", text: "Item B", done: true },
      ],
    },
  });
  expect(createResp.ok()).toBe(true);
  const note = await createResp.json();

  const sendResp = await page.request.post(`/api/notes/${note.id}/send`, {
    data: { recipientUserIds: [recipient.id] },
  });
  expect(sendResp.ok()).toBe(true);

  // Log out and back in as the recipient, then read the inbox.
  await page.request.post("/api/auth/logout").catch(() => {});
  await loginViaUi(page, recipient.username);

  const receivedResp = await page.request.get("/api/notes/received?archived=false");
  expect(receivedResp.ok()).toBe(true);
  const received = await receivedResp.json();
  const delivered = received.find((r) => r.id === note.id);
  expect(delivered, "checklist note should appear in recipient inbox").toBeTruthy();
  expect(delivered.kind).toBe("checklist");
  expect(delivered.items.map((i) => i.text)).toEqual(["Item A", "Item B"]);
  expect(delivered.items[1].done).toBe(true);
});
