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

// E2E coverage for the three "bulk-edit polish" features:
//   1. Edit toggle flips between an "Edit" label and a "Save" label
//      (and a Pencil↔Check icon), and a second click exits edit mode.
//   2. The new-meeting / edit-meeting dialog has a one-click
//      "Remove all attendees" button that empties the list.
//   3. Multi-select on the schedule + a single "Delete selected"
//      action removes every checked meeting and clears the selection.
//
// Assertions are language-agnostic: we use testIDs, aria attributes,
// and regexes rather than English/Arabic copy so the suite works
// regardless of which locale the persisted localStorage has stuck on.

const DATABASE_URL = process.env.DATABASE_URL;
if (!DATABASE_URL) {
  throw new Error(
    "DATABASE_URL must be set to run the bulk-actions UI tests",
  );
}

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

const RANDOM_DATE_BASE_DAYS =
  365 + Math.floor(Math.random() * 1000) + ((Date.now() / 1000) | 0) % 500;
function uniqueFutureDate(offsetDays) {
  const d = new Date();
  d.setUTCDate(d.getUTCDate() + RANDOM_DATE_BASE_DAYS + offsetDays * 7);
  return d.toISOString().slice(0, 10);
}

async function insertMeeting({
  meetingDate,
  dailyNumber,
  titleEn,
  titleAr,
  startTime,
  endTime,
}) {
  const { rows } = await pool.query(
    `INSERT INTO executive_meetings
       (daily_number, title_ar, title_en, meeting_date, start_time, end_time, status)
     VALUES ($1, $2, $3, $4, $5, $6, 'scheduled')
     RETURNING id`,
    [dailyNumber, titleAr, titleEn, meetingDate, startTime, endTime],
  );
  const id = rows[0].id;
  createdMeetingIds.push(id);
  return id;
}

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 resetSchedulePrefs(page) {
  await page.evaluate(() => {
    try {
      const keys = Object.keys(window.localStorage);
      for (const k of keys) {
        if (
          k.startsWith("em-schedule-edit-mode-v1") ||
          k === "em-schedule-cols-v1" ||
          k === "em-schedule-row-colors-v1" ||
          k === "em-current-meeting-highlight-v1"
        ) {
          window.localStorage.removeItem(k);
        }
      }
    } catch {
      /* ignore */
    }
  });
}

test.afterAll(async () => {
  if (createdMeetingIds.length > 0) {
    await pool.query(
      `DELETE FROM executive_meeting_attendees WHERE meeting_id = ANY($1::int[])`,
      [createdMeetingIds],
    );
    await pool.query(
      `DELETE FROM executive_meeting_audit_logs WHERE entity_type = 'meeting' AND entity_id = ANY($1::int[])`,
      [createdMeetingIds],
    );
    await pool.query(
      `DELETE FROM executive_meetings WHERE id = ANY($1::int[])`,
      [createdMeetingIds],
    );
  }
  await pool.end();
});

test("Schedule: edit toggle flips its label and icon when on, and a second click exits edit mode", async ({
  page,
}) => {
  await loginViaUi(page, "admin", "admin123");
  await page.goto("/executive-meetings");
  await resetSchedulePrefs(page);
  await page.reload();

  const toggle = page.getByTestId("em-edit-mode-toggle");
  await expect(toggle).toBeVisible();
  await expect(toggle).toHaveAttribute("aria-pressed", "false");

  // Capture the OFF-state label so we can prove it actually changes.
  const offLabel = (await toggle.textContent())?.trim() ?? "";
  expect(offLabel.length).toBeGreaterThan(0);
  // The icon SVG inside the OFF state must be the Pencil (lucide marks
  // its icons with a `lucide-pencil` class).
  await expect(toggle.locator("svg.lucide-pencil")).toHaveCount(1);
  await expect(toggle.locator("svg.lucide-check")).toHaveCount(0);

  // Flip on: aria-pressed flips, label/icon swap, and at least one
  // edit-mode-only affordance becomes visible (the per-row select
  // checkbox overlay) — proof we really entered edit mode.
  await toggle.click();
  await expect(toggle).toHaveAttribute("aria-pressed", "true");
  const onLabel = (await toggle.textContent())?.trim() ?? "";
  expect(onLabel.length).toBeGreaterThan(0);
  expect(onLabel).not.toBe(offLabel);
  await expect(toggle.locator("svg.lucide-check")).toHaveCount(1);
  await expect(toggle.locator("svg.lucide-pencil")).toHaveCount(0);
  await expect(
    page.locator('[data-testid^="em-row-select-"]').first(),
  ).toBeVisible();

  // Second click — back to OFF, original label/icon return.
  await toggle.click();
  await expect(toggle).toHaveAttribute("aria-pressed", "false");
  await expect(toggle).toHaveText(offLabel);
  await expect(toggle.locator("svg.lucide-pencil")).toHaveCount(1);
});

test("New-meeting dialog: 'Remove all attendees' button only renders when ≥1 attendee, and clears the in-memory list", async ({
  page,
}) => {
  await loginViaUi(page, "admin", "admin123");
  await page.goto("/executive-meetings");

  // Open the Manage tab → click "Add meeting" to launch the dialog.
  await page.getByTestId("em-nav-manage").click();
  await page.getByTestId("em-add-meeting").click();

  // Empty list → the bulk-clear button is NOT rendered.
  await expect(page.getByTestId("em-attendee-remove-all")).toHaveCount(0);

  // Add two attendees via the in-dialog "+ Add attendee" affordance.
  const addAttendeeBtn = page.getByTestId("em-attendee-add");
  await expect(addAttendeeBtn).toBeVisible();
  await addAttendeeBtn.click();
  await addAttendeeBtn.click();
  await expect(page.getByTestId("em-attendee-row-0")).toBeVisible();
  await expect(page.getByTestId("em-attendee-row-1")).toBeVisible();

  // Bulk-clear button now visible. Auto-confirm the native confirm()
  // and click it.
  await expect(page.getByTestId("em-attendee-remove-all")).toBeVisible();
  page.once("dialog", (d) => d.accept());
  await page.getByTestId("em-attendee-remove-all").click();

  // List is empty again — neither attendee row nor the bulk button
  // should be in the DOM.
  await expect(page.getByTestId("em-attendee-row-0")).toHaveCount(0);
  await expect(page.getByTestId("em-attendee-row-1")).toHaveCount(0);
  await expect(page.getByTestId("em-attendee-remove-all")).toHaveCount(0);
});

test("Schedule: multi-select + 'Delete selected' removes every checked meeting and clears the selection", async ({
  page,
}) => {
  const date = uniqueFutureDate(2);

  // Seed three meetings on the same future date so the schedule has
  // a known set we can multi-select against.
  const id1 = await insertMeeting({
    meetingDate: date,
    dailyNumber: 1,
    titleEn: `Bulk1 ${Date.now()}`,
    titleAr: `Bulk1 ${Date.now()}`,
    startTime: "09:00:00",
    endTime: "10:00:00",
  });
  const id2 = await insertMeeting({
    meetingDate: date,
    dailyNumber: 2,
    titleEn: `Bulk2 ${Date.now()}`,
    titleAr: `Bulk2 ${Date.now()}`,
    startTime: "10:00:00",
    endTime: "11:00:00",
  });
  const id3 = await insertMeeting({
    meetingDate: date,
    dailyNumber: 3,
    titleEn: `Bulk3 ${Date.now()}`,
    titleAr: `Bulk3 ${Date.now()}`,
    startTime: "11:00:00",
    endTime: "12:00:00",
  });

  await loginViaUi(page, "admin", "admin123");
  await page.goto("/executive-meetings");
  await resetSchedulePrefs(page);
  await page.reload();

  // Jump to the seeded date.
  const dateInput = page.locator('input[type="date"]').first();
  await dateInput.fill(date);

  // Wait for all three rows.
  await expect(page.getByTestId(`em-row-${id1}`)).toBeVisible({
    timeout: 10_000,
  });
  await expect(page.getByTestId(`em-row-${id2}`)).toBeVisible();
  await expect(page.getByTestId(`em-row-${id3}`)).toBeVisible();

  // View mode → no row checkboxes, no select-all in header, no toolbar.
  await expect(page.locator('[data-testid^="em-row-select-"]')).toHaveCount(0);
  await expect(page.getByTestId("em-bulk-select-all")).toHaveCount(0);
  await expect(page.getByTestId("em-bulk-toolbar")).toHaveCount(0);

  // Flip edit mode on. The tri-state "select all" appears in the
  // table header and per-row checkboxes appear, but the floating
  // bulk toolbar is NOT visible yet — it only renders once ≥1 row
  // is checked.
  await page.getByTestId("em-edit-mode-toggle").click();
  await expect(page.getByTestId("em-bulk-select-all")).toBeVisible();
  await expect(page.getByTestId(`em-row-select-${id1}`)).toBeVisible();
  await expect(page.getByTestId(`em-row-select-${id2}`)).toBeVisible();
  await expect(page.getByTestId(`em-row-select-${id3}`)).toBeVisible();
  await expect(page.getByTestId("em-bulk-toolbar")).toHaveCount(0);

  // Check rows 1 and 3 (skip 2 to confirm the selection is honored).
  await page.getByTestId(`em-row-select-${id1}`).click();
  await page.getByTestId(`em-row-select-${id3}`).click();
  // Bulk toolbar now appears with the count + delete button.
  await expect(page.getByTestId("em-bulk-toolbar")).toBeVisible();
  await expect(page.getByTestId("em-bulk-selection-count")).toHaveText(/2/);
  await expect(page.getByTestId("em-bulk-selection-count")).toHaveText(/3/);
  await expect(page.getByTestId("em-bulk-delete-selected")).toBeVisible();
  // Header tri-state is in the indeterminate visual state.
  await expect(page.getByTestId("em-bulk-select-all")).toHaveAttribute(
    "data-state",
    "indeterminate",
  );

  // Delete the two selected rows. Auto-accept the native confirm.
  page.once("dialog", (d) => d.accept());
  await page.getByTestId("em-bulk-delete-selected").click();

  // Rows 1 and 3 are gone; row 2 remains. Selection has cleared, so
  // the floating toolbar disappears entirely.
  await expect(page.getByTestId(`em-row-${id1}`)).toHaveCount(0, {
    timeout: 10_000,
  });
  await expect(page.getByTestId(`em-row-${id3}`)).toHaveCount(0);
  await expect(page.getByTestId(`em-row-${id2}`)).toBeVisible();
  await expect(page.getByTestId("em-bulk-toolbar")).toHaveCount(0);
  await expect(page.getByTestId("em-bulk-delete-selected")).toHaveCount(0);
  // Header select-all is back to unchecked.
  await expect(page.getByTestId("em-bulk-select-all")).toHaveAttribute(
    "data-state",
    "unchecked",
  );

  // DB should agree: ids 1 + 3 are gone, id 2 still present.
  const { rows: remaining } = await pool.query(
    `SELECT id FROM executive_meetings WHERE id = ANY($1::int[]) ORDER BY id`,
    [[id1, id2, id3]],
  );
  expect(remaining.map((r) => r.id)).toEqual([id2]);
});

test("Schedule: per-row select checkbox stays usable even when the # column is hidden via the column customizer", async ({
  page,
}) => {
  const date = uniqueFutureDate(3);

  const idA = await insertMeeting({
    meetingDate: date,
    dailyNumber: 1,
    titleEn: `HiddenCol1 ${Date.now()}`,
    titleAr: `HiddenCol1 ${Date.now()}`,
    startTime: "09:00:00",
    endTime: "10:00:00",
  });
  const idB = await insertMeeting({
    meetingDate: date,
    dailyNumber: 2,
    titleEn: `HiddenCol2 ${Date.now()}`,
    titleAr: `HiddenCol2 ${Date.now()}`,
    startTime: "10:00:00",
    endTime: "11:00:00",
  });

  await loginViaUi(page, "admin", "admin123");
  await page.goto("/executive-meetings");
  await resetSchedulePrefs(page);
  await page.reload();

  // Jump to seeded date and wait for both rows.
  const dateInput = page.locator('input[type="date"]').first();
  await dateInput.fill(date);
  await expect(page.getByTestId(`em-row-${idA}`)).toBeVisible({
    timeout: 10_000,
  });
  await expect(page.getByTestId(`em-row-${idB}`)).toBeVisible();

  // Hide the # column via the customize-columns popover.
  await page.getByTestId("em-customize-columns-trigger").click();
  await page.getByTestId("em-customize-toggle-number").click();
  // Close the popover by clicking the table area.
  await page.keyboard.press("Escape");

  // Enter edit mode. Per-row checkboxes must still appear (now as
  // overlays in the first visible cell), and selecting them must
  // still drive the bulk toolbar — even with the # column hidden,
  // the tri-state header overlay falls back to the new first
  // visible header.
  await page.getByTestId("em-edit-mode-toggle").click();
  await expect(page.getByTestId("em-bulk-select-all")).toBeVisible();
  await expect(page.getByTestId(`em-row-select-${idA}`)).toBeVisible();
  await expect(page.getByTestId(`em-row-select-${idB}`)).toBeVisible();
  // Toolbar is hidden until something is selected.
  await expect(page.getByTestId("em-bulk-toolbar")).toHaveCount(0);

  await page.getByTestId(`em-row-select-${idA}`).click();
  await expect(page.getByTestId("em-bulk-toolbar")).toBeVisible();
  await expect(page.getByTestId("em-bulk-selection-count")).toHaveText(/1/);
  await expect(page.getByTestId("em-bulk-delete-selected")).toBeVisible();

  // Restore the # column so we leave the persisted UI prefs in a
  // sane state for the next test.
  await page.getByTestId("em-customize-columns-trigger").click();
  await page.getByTestId("em-customize-toggle-number").click();
  await page.keyboard.press("Escape");
});

// Regression guard for merged rows. Earlier the per-row bulk-select
// overlay was anchored to the first visible *non-merged* cell, which
// meant a row whose merge swallowed the leading cells lost its
// checkbox entirely. The fix mirrors the overlay onto the merged
// <td>, so this test merges a row end-to-end via the row-actions
// menu and asserts the checkbox still renders + drives the bulk
// toolbar.
test("Schedule: a merged row still exposes the per-row bulk select checkbox", async ({
  page,
}) => {
  const date = uniqueFutureDate(4);

  const idA = await insertMeeting({
    meetingDate: date,
    dailyNumber: 1,
    titleEn: `MergedSel1 ${Date.now()}`,
    titleAr: `MergedSel1 ${Date.now()}`,
    startTime: "09:00:00",
    endTime: "10:00:00",
  });

  await loginViaUi(page, "admin", "admin123");
  await page.goto("/executive-meetings");
  await resetSchedulePrefs(page);
  await page.reload();

  const dateInput = page.locator('input[type="date"]').first();
  await dateInput.fill(date);
  await expect(page.getByTestId(`em-row-${idA}`)).toBeVisible({
    timeout: 10_000,
  });

  // The row-actions kebab only mounts when canMutate is true (edit
  // mode on). Turn edit mode on first, then open the kebab and pick
  // "Merge all". The header tri-state confirms edit mode is live.
  await page.getByTestId("em-edit-mode-toggle").click();
  await expect(page.getByTestId("em-bulk-select-all")).toBeVisible();
  await page
    .getByTestId(`em-row-actions-${idA}`)
    .click({ force: true });
  await page.getByTestId(`em-merge-trigger-${idA}`).click();
  await page.getByTestId(`em-merge-opt-all-${idA}`).click();

  // After merge the row collapses its scheduling columns into one
  // <td>; confirm the merged cell rendered.
  await expect(page.getByTestId(`em-merge-cell-${idA}`)).toBeVisible({
    timeout: 10_000,
  });

  // Edit mode is still on; assert the per-row checkbox is still
  // reachable on the merged row, and that selecting it lights up the
  // bulk toolbar exactly the same way an unmerged row would.
  await expect(page.getByTestId(`em-row-select-${idA}`)).toBeVisible();

  await page.getByTestId(`em-row-select-${idA}`).click();
  await expect(page.getByTestId("em-bulk-toolbar")).toBeVisible();
  await expect(page.getByTestId("em-bulk-selection-count")).toHaveText(/1/);
  await expect(page.getByTestId("em-bulk-delete-selected")).toBeVisible();
});
