// #499: e2e for the iPad long-press visual feedback. dnd-kit's
// TouchSensor has a 200ms activation delay before a touch becomes a
// drag. Before #499, the row gave NO visual cue during that window —
// users would tap-and-hold a meeting to drag it but, seeing nothing
// happen, lift their finger early thinking the schedule didn't
// register the touch. The fix paints a subtle inset ring on the row
// while a touch pointer is pressed, gated on `pointerType === "touch"`
// so mouse / pen pointers are unaffected. We expose the press state
// as `data-pressing="true"` on the row tr for deterministic testing.
//
// We use Chromium mobile emulation (`isMobile` + `hasTouch`) so the
// browser actually emits TouchEvents and PointerEvents with
// `pointerType === "touch"`, then drive presses through CDP because
// Playwright's high-level touchscreen API only exposes `tap()`.
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 touch press-feedback test",
  );
}

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

test.use({
  viewport: { width: 768, height: 1024 },
  hasTouch: true,
  isMobile: true,
});

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 nextDailyNumberForDate(date) {
  const { rows } = await pool.query(
    `SELECT COALESCE(MAX(daily_number), 0) + 1 AS n
       FROM executive_meetings
      WHERE meeting_date = $1`,
    [date],
  );
  return rows[0].n;
}

async function insertMeeting({ meetingDate, titleEn, startTime, endTime }) {
  const dn = await nextDailyNumberForDate(meetingDate);
  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, $2, $3, $4, $5, 'scheduled')
     RETURNING id`,
    [dn, titleEn, meetingDate, startTime, endTime],
  );
  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 * 7);
  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("Touch (iPad): pressing a row paints data-pressing=true within the 200ms TouchSensor delay, clears on release, and tap-to-open dialog still works", async ({
  page,
}) => {
  await page.addInitScript(() => {
    try {
      window.localStorage.setItem("tx-lang", "en");
    } catch {
      /* ignore */
    }
  });

  const date = uniqueFutureDate(1);
  const id = await insertMeeting({
    meetingDate: date,
    titleEn: `Touch Press Feedback ${Date.now().toString(36)}`,
    startTime: "09:00:00",
    endTime: "10:00:00",
  });

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

  const row = page.getByTestId(`em-row-${id}`);
  await expect(row).toBeVisible({ timeout: 10_000 });
  // Baseline: not pressed.
  await expect(row).not.toHaveAttribute("data-pressing", "true");

  const numberCell = row.locator("td").first();
  await numberCell.scrollIntoViewIfNeeded();
  const box = await numberCell.boundingBox();
  expect(box).not.toBeNull();
  const x = box.x + box.width / 2;
  const y = box.y + box.height / 2;

  const cdp = await page.context().newCDPSession(page);

  // 1. Touch down — DON'T move (stay inside the 8px tolerance window).
  //    The press cue should appear immediately on pointerdown, well
  //    inside the TouchSensor's 200ms activation delay.
  await cdp.send("Input.dispatchTouchEvent", {
    type: "touchStart",
    touchPoints: [{ x, y, id: 1 }],
  });
  // Allow React to flush the state update from the capture-phase
  // pointerdown handler. Should be effectively immediate, but we
  // give 250ms upper bound so the assertion is robust on slow CI.
  await expect(row).toHaveAttribute("data-pressing", "true", {
    timeout: 250,
  });

  // 2. Release WITHOUT dragging — the cue must clear immediately and
  //    the tap-to-open quick-actions dialog must still fire (since
  //    the touch was a tap, not a long-press-into-drag).
  await cdp.send("Input.dispatchTouchEvent", {
    type: "touchEnd",
    touchPoints: [],
  });
  await expect(row).not.toHaveAttribute("data-pressing", "true", {
    timeout: 500,
  });

  // The original press WAS shorter than the 200ms drag-activation
  // delay (no waitForTimeout between touchStart and touchEnd above),
  // so dnd-kit treats it as a plain tap and the row's onClick fires
  // on touchend → quick-actions dialog opens.
  const surface = page.locator(`[data-testid="em-row-quick-${id}"]`);
  await expect(surface).toBeVisible({ timeout: 5_000 });
});

test("Touch (iPad): drag activation + drop reliably clears data-pressing (no stale ring after drag)", async ({
  page,
}) => {
  // #499 review: dnd-kit owns the touch sequence once a drag
  // activates and on some release paths the row's own pointerup
  // does not fire. The MeetingRow has a defensive effect that
  // watches `isDragging` and force-clears `isPressing` when drag
  // activates / ends — this test guards that path: long-press
  // (drag activates), move (drag in progress), release (drag ends),
  // assert `data-pressing` is cleared after the drop.
  await page.addInitScript(() => {
    try {
      window.localStorage.setItem("tx-lang", "en");
    } catch {
      /* ignore */
    }
  });

  const date = uniqueFutureDate(3);
  const idA = await insertMeeting({
    meetingDate: date,
    titleEn: `Touch Drag Cleanup A ${Date.now().toString(36)}`,
    startTime: "13:00:00",
    endTime: "14:00:00",
  });
  const idB = await insertMeeting({
    meetingDate: date,
    titleEn: `Touch Drag Cleanup B ${Date.now().toString(36)}`,
    startTime: "15:00:00",
    endTime: "16:00:00",
  });

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

  const rowA = page.getByTestId(`em-row-${idA}`);
  const rowB = page.getByTestId(`em-row-${idB}`);
  await expect(rowA).toBeVisible({ timeout: 10_000 });
  await expect(rowB).toBeVisible();

  const numberCellA = rowA.locator("td").first();
  await numberCellA.scrollIntoViewIfNeeded();
  const numBox = await numberCellA.boundingBox();
  const targetBox = await rowB.boundingBox();
  expect(numBox).not.toBeNull();
  expect(targetBox).not.toBeNull();
  const startX = numBox.x + numBox.width / 2;
  const startY = numBox.y + numBox.height / 2;
  const endY = targetBox.y + targetBox.height - 4;

  const cdp = await page.context().newCDPSession(page);
  await cdp.send("Input.dispatchTouchEvent", {
    type: "touchStart",
    touchPoints: [{ x: startX, y: startY, id: 1 }],
  });
  // Hold past 200ms TouchSensor activation delay.
  await page.waitForTimeout(350);
  // Per task spec, the press cue must remain visible AFTER drag
  // activates (not just during the pre-drag hold). Sanity-check the
  // attribute is still set right before we start dragging.
  await expect(rowA).toHaveAttribute("data-pressing", "true");
  // Drag down toward rowB.
  const steps = 12;
  for (let i = 1; i <= steps; i++) {
    const y = startY + ((endY - startY) * i) / steps;
    await cdp.send("Input.dispatchTouchEvent", {
      type: "touchMove",
      touchPoints: [{ x: startX, y, id: 1 }],
    });
    await page.waitForTimeout(20);
  }
  // Mid-drag: cue must STILL be visible — feedback continues
  // throughout the active drag, not just during the activation
  // delay. This is the explicit "continues while drag is active"
  // requirement from #499.
  await expect(rowA).toHaveAttribute("data-pressing", "true");
  // Release — drag ends. Whether the row's own pointerup fires or
  // dnd-kit cancels it, the `isDragging` cleanup effect must clear
  // `data-pressing`.
  await cdp.send("Input.dispatchTouchEvent", {
    type: "touchEnd",
    touchPoints: [],
  });
  // Both rows should settle without any lingering pressing state.
  await expect(rowA).not.toHaveAttribute("data-pressing", "true", {
    timeout: 1_000,
  });
  await expect(rowB).not.toHaveAttribute("data-pressing", "true", {
    timeout: 1_000,
  });
});

test("Touch (iPad): press cue persists during the long-press hold (>200ms TouchSensor delay)", async ({
  page,
}) => {
  await page.addInitScript(() => {
    try {
      window.localStorage.setItem("tx-lang", "en");
    } catch {
      /* ignore */
    }
  });

  const date = uniqueFutureDate(2);
  const id = await insertMeeting({
    meetingDate: date,
    titleEn: `Touch Press Hold ${Date.now().toString(36)}`,
    startTime: "11:00:00",
    endTime: "12:00:00",
  });

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

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

  const numberCell = row.locator("td").first();
  await numberCell.scrollIntoViewIfNeeded();
  const box = await numberCell.boundingBox();
  expect(box).not.toBeNull();
  const x = box.x + box.width / 2;
  const y = box.y + box.height / 2;

  const cdp = await page.context().newCDPSession(page);

  await cdp.send("Input.dispatchTouchEvent", {
    type: "touchStart",
    touchPoints: [{ x, y, id: 1 }],
  });
  // Hold past the 200ms TouchSensor activation delay — the cue must
  // still be on the row (this is the whole point of the fix: feedback
  // during the hold window so users don't lift early).
  await page.waitForTimeout(350);
  await expect(row).toHaveAttribute("data-pressing", "true");

  // Release — cue clears.
  await cdp.send("Input.dispatchTouchEvent", {
    type: "touchEnd",
    touchPoints: [],
  });
  await expect(row).not.toHaveAttribute("data-pressing", "true", {
    timeout: 500,
  });
});
