// Coverage for — quick-preview indicators on the row-actions
// kebab menu's main view:
//
//   1. The "Row color" item shows a tiny swatch reflecting the row's
//      currently saved color (or a hatched neutral dot for "default"),
//      so users can tell at a glance which color is set without
//      drilling into the swatch sub-view.
//
//   2. The "Merge cells" item shows a "Merged: <cols>" badge when the
//      row has a stored merge, naming the spanned columns. Hidden
//      entirely on rows with no stored merge.
//
// The badge text + dot color must update whenever the underlying state
// changes. To keep selectors stable, the indicators ride along inside
// the existing `em-row-color-trigger` / `em-merge-trigger-{id}` buttons
// rather than introducing new top-level menu items.
//
// Setup follows the same pattern as the merge spec: insert seeded
// meetings on a far-future date directly via pg, drive the UI through
// the schedule's date picker, then clean up via afterAll.

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 row-actions previews UI tests",
  );
}

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

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 setLangEn(page) {
  await page.addInitScript(() => {
    try {
      window.localStorage.setItem("tx-lang", "en");
    } catch {
      /* ignore */
    }
  });
}

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

async function ensureEditModeOn(page) {
  const toggle = page.getByTestId("em-edit-mode-toggle");
  await expect(toggle).toBeVisible();
  if ((await toggle.getAttribute("aria-pressed")) === "false") {
    await toggle.click();
  }
  await expect(toggle).toHaveAttribute("aria-pressed", "true");
}

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

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 * 11);
  return d.toISOString().slice(0, 10);
}

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("Row actions menu: Row color item shows an inline swatch reflecting the saved color", async ({
  page,
}) => {
  await setLangEn(page);

  const date = uniqueFutureDate(3);
  const uniq = `${Date.now().toString(36)}_${Math.random()
    .toString(36)
    .slice(2, 6)}`;
  const meetingId = await insertMeeting({
    meetingDate: date,
    dailyNumber: 1,
    titleEn: `Color preview ${uniq}`,
    titleAr: `معاينة اللون ${uniq}`,
    startTime: "09:00:00",
    endTime: "09:30:00",
  });

  await loginViaUi(page, "admin", "admin123");
  await page.goto("/executive-meetings");
  await resetSchedulePrefs(page);
  await page.reload();
  await page.locator('input[type="date"]').first().fill(date);

  const row = page.getByTestId(`em-row-${meetingId}`);
  await expect(row).toBeVisible({ timeout: 10_000 });
  await ensureEditModeOn(page);

  const kebab = row.locator('[data-testid^="em-row-actions-"]').first();
  await kebab.click({ force: true });

  // First open: no color picked yet → indicator advertises "default".
  // The dot itself is decorative (aria-hidden), so we assert via its
  // data-color-key attribute rather than scraping background CSS.
  const indicator = page.getByTestId(`em-row-color-indicator-${meetingId}`);
  await expect(indicator).toHaveCount(1);
  await expect(indicator).toHaveAttribute("data-color-key", "default");

  // Pick the red swatch via the existing sub-view.
  await page.getByTestId("em-row-color-trigger").click();
  await page.getByTestId("em-row-color-red").click();
  // Picking a color closes the popover (existing behavior).
  await expect(page.getByTestId("em-row-color-trigger")).toHaveCount(0);

  // Re-open the kebab and confirm the inline preview now reflects red.
  // The picker uses #fee2e2 → rgb(254, 226, 226), so we also sanity
  // check the computed background to make sure the dot is actually
  // rendering the chosen tint (not just dataset-tagged correctly).
  await kebab.click({ force: true });
  await expect(indicator).toHaveAttribute("data-color-key", "red");
  const bg = await indicator.evaluate(
    (el) => getComputedStyle(el).backgroundColor,
  );
  expect(bg).toMatch(/254,\s*226,\s*226/);

  // Reset back to default and confirm the indicator returns to the
  // hatched / "default" state, so future runs against this meeting (or
  // its leftover localStorage) don't lock in red.
  await page.getByTestId("em-row-color-trigger").click();
  await page.getByTestId("em-row-color-default").click();
  await expect(page.getByTestId("em-row-color-trigger")).toHaveCount(0);
  await kebab.click({ force: true });
  await expect(indicator).toHaveAttribute("data-color-key", "default");
});

test("Row actions menu: Merge cells item shows a 'Merged: …' badge naming the spanned columns", async ({
  page,
}) => {
  await setLangEn(page);

  const date = uniqueFutureDate(4);
  const uniq = `${Date.now().toString(36)}_${Math.random()
    .toString(36)
    .slice(2, 6)}`;

  // Row WITHOUT a stored merge — should not render a merge badge at all.
  const plainId = await insertMeeting({
    meetingDate: date,
    dailyNumber: 1,
    titleEn: `Plain row ${uniq}`,
    titleAr: `صف عادي ${uniq}`,
    startTime: "09:00:00",
    endTime: "09:30:00",
  });

  // Row WITH a meeting+attendees merge — kebab anchors on the merged
  // cell, and the badge should read "Merged: Meeting + Attendees".
  const mergedId = await insertMeeting({
    meetingDate: date,
    dailyNumber: 2,
    titleEn: `Merged row ${uniq}`,
    titleAr: `صف مدموج ${uniq}`,
    startTime: "10:00:00",
    endTime: "10:30:00",
    mergeStartColumn: "meeting",
    mergeEndColumn: "attendees",
    mergeText: "merged note",
  });

  await loginViaUi(page, "admin", "admin123");
  await page.goto("/executive-meetings");
  await resetSchedulePrefs(page);
  await page.reload();
  await page.locator('input[type="date"]').first().fill(date);

  const plainRow = page.getByTestId(`em-row-${plainId}`);
  const mergedRow = page.getByTestId(`em-row-${mergedId}`);
  await expect(plainRow).toBeVisible({ timeout: 10_000 });
  await expect(mergedRow).toBeVisible({ timeout: 10_000 });
  await ensureEditModeOn(page);

  // Plain row: open the kebab and confirm there is NO merge indicator.
  const plainKebab = plainRow
    .locator('[data-testid^="em-row-actions-"]')
    .first();
  await plainKebab.click({ force: true });
  await expect(
    page.getByTestId(`em-merge-trigger-${plainId}`),
  ).toBeVisible();
  await expect(
    page.getByTestId(`em-merge-indicator-${plainId}`),
  ).toHaveCount(0);
  // Close the popover and WAIT for it to actually unmount before
  // poking the next row. Without this, a force:true click on the
  // merged-row kebab can land on the still-open Delete item that
  // sits over the merged row, triggering an accidental delete confirm
  // and never opening the merged-row popover at all.
  await page.keyboard.press("Escape");
  await expect(
    page.getByTestId(`em-merge-trigger-${plainId}`),
  ).toHaveCount(0);

  // Resolve the rendered column-header labels at runtime so the
  // assertion is language-agnostic — admin's preferredLanguage gets
  // applied by AuthContext after login and may override the
  // tx-lang=en init script seed, so we cannot hard-code the English
  // strings here.
  const meetingColLabel = (
    await page.getByTestId("em-col-header-meeting").first().textContent()
  )?.trim();
  const attendeesColLabel = (
    await page.getByTestId("em-col-header-attendees").first().textContent()
  )?.trim();
  expect(meetingColLabel).toBeTruthy();
  expect(attendeesColLabel).toBeTruthy();

  // Merged row: open the kebab and confirm the badge appears, naming
  // the spanned columns. We check both `data-testid` and that the
  // visible text contains both column labels in canonical order
  // joined with " + ", which is what the activeBadge i18n template
  // produces in either language.
  const mergedKebab = mergedRow
    .locator('[data-testid^="em-row-actions-"]')
    .first();
  await mergedKebab.click({ force: true });
  // Sanity: the popover actually opened on the merged row.
  await expect(
    page.getByTestId(`em-merge-trigger-${mergedId}`),
  ).toBeVisible();
  const mergedBadge = page.getByTestId(`em-merge-indicator-${mergedId}`);
  await expect(mergedBadge).toBeVisible();
  await expect(mergedBadge).toContainText(
    `${meetingColLabel} + ${attendeesColLabel}`,
  );
});
