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

// #490: e2e for the iPad / touch path of drag-to-rotate on the
// Executive Meetings daily schedule. After #489 the dedicated grip
// handle is gone — the whole <tr> is the drag handle, drag works in
// view mode (no edit toggle), and the server endpoint is
// /api/executive-meetings/rotate-content (NOT /reorder).
//
// On iOS Safari (iPad), pointer events fire for touch and a
// PointerSensor with a small distance threshold competes with the OS
// scroll claim — the browser reclaims the gesture as a vertical scroll
// before the finger has moved 6px, so the drag never activates. The
// fix in #490 splits sensors into MouseSensor (mouse only) +
// TouchSensor (delay-based long-press), which is what this spec
// guards. A regression that brings PointerSensor back for the row
// sensors would silently break every iPad user but pass the desktop
// reorder test.

const DATABASE_URL = process.env.DATABASE_URL;
if (!DATABASE_URL) {
  throw new Error(
    "DATABASE_URL must be set to run the executive-meetings touch-reorder UI test",
  );
}

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

// Configure this whole spec to use an iPad-shaped, touch-enabled
// browser context. `isMobile` enables Chromium's mobile emulation
// (which makes the browser actually emit TouchEvents in addition to
// pointer/mouse), and `hasTouch` enables the touch input pipeline so
// CDP `Input.dispatchTouchEvent` calls are honored. Both are required
// for dnd-kit's TouchSensor to receive `touchstart`/`touchmove`/
// `touchend` and activate on long-press.
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 setLangEn(page) {
  await page.addInitScript(() => {
    try {
      window.localStorage.setItem("tx-lang", "en");
    } catch {
      /* localStorage may not be ready before navigation */
    }
  });
}

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,
  titleAr,
  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, $3, $4, $5, $6, 'scheduled')
     RETURNING id`,
    [dn, titleAr, titleEn, meetingDate, startTime, endTime],
  );
  const id = rows[0].id;
  createdMeetingIds.push(id);
  return id;
}

async function readMeeting(id) {
  const { rows } = await pool.query(
    `SELECT title_en, start_time, end_time, daily_number
       FROM executive_meetings WHERE id = $1`,
    [id],
  );
  return rows[0];
}

// Pick a meeting date that won't collide with real schedule data OR
// with stale rows leftover from a previously-failed run.
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): long-press on a row and drag it down rotates content via TouchSensor without edit mode", async ({
  page,
}) => {
  await setLangEn(page);

  const date = uniqueFutureDate(1);
  const uniq = `${Date.now().toString(36)}_${Math.random()
    .toString(36)
    .slice(2, 6)}`;
  const idA = await insertMeeting({
    meetingDate: date,
    titleEn: `Touch Drag A ${uniq}`,
    titleAr: `سحب لمسي أ ${uniq}`,
    startTime: "09:00:00",
    endTime: "10:00:00",
  });
  const idB = await insertMeeting({
    meetingDate: date,
    titleEn: `Touch Drag B ${uniq}`,
    titleAr: `سحب لمسي ب ${uniq}`,
    startTime: "11:00:00",
    endTime: "12:00:00",
  });

  await loginViaUi(page, "admin", "admin123");
  await page.goto("/executive-meetings");
  // #490: explicitly clear any persisted edit-mode pref left by other
  // specs that share the demo `admin` user, then reload — the spec
  // regression we're guarding is "drag works on iPad WITHOUT entering
  // edit mode", so we must start from a known view-mode baseline.
  await page.evaluate(() => {
    try {
      const keys = Object.keys(window.localStorage);
      for (const k of keys) {
        if (k.startsWith("em-schedule-edit-mode-v1")) {
          window.localStorage.removeItem(k);
        }
      }
    } catch {
      /* ignore */
    }
  });
  await page.reload();
  await page.locator('input[type="date"]').first().fill(date);

  // Sanity: the edit-mode toggle must read OFF — drag should still work.
  const editToggle = page.getByTestId("em-edit-mode-toggle");
  if (await editToggle.count()) {
    await expect(editToggle).toHaveAttribute("aria-pressed", "false");
  }

  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();

  // #489 + #490: the dedicated grip handle is gone — drag from the
  // # cell (first <td>) which contains a plain <span> with the daily
  // number and no skip-listed children. Drag works in view mode (no
  // edit toggle required).
  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 endX = startX;
  // Aim BELOW the second row's vertical center so dnd-kit places A
  // after B and the rotated content order becomes [B, A].
  const endY = targetBox.y + targetBox.height - 4;

  // Drive touches through CDP because Playwright's high-level
  // `page.touchscreen` API only exposes `tap()` — no long-press +
  // drag primitive — and dnd-kit's TouchSensor specifically listens
  // for native touchstart/touchmove/touchend events.
  const cdp = await page.context().newCDPSession(page);

  const rotatePromise = page.waitForResponse(
    (resp) => {
      const u = new URL(resp.url());
      return (
        u.pathname === "/api/executive-meetings/rotate-content" &&
        resp.request().method() === "POST" &&
        resp.ok()
      );
    },
    { timeout: 15_000 },
  );

  // 1. Press down and HOLD without moving so the TouchSensor's 200ms
  //    activation delay can elapse. We wait > 250ms (well above the
  //    threshold) to avoid timing flakiness on slow CI machines, and
  //    stay inside the 8px tolerance window which is what tells the
  //    TouchSensor "this is a long-press, not a scroll".
  await cdp.send("Input.dispatchTouchEvent", {
    type: "touchStart",
    touchPoints: [{ x: startX, y: startY, id: 1 }],
  });
  await page.waitForTimeout(350);

  // 2. Drag down toward the target row in several intermediate steps
  //    so dnd-kit sees enough touchmove events to compute the overlap
  //    with the row below and emit a sortable change.
  const steps = 12;
  for (let i = 1; i <= steps; i++) {
    const x = startX + ((endX - startX) * i) / steps;
    const y = startY + ((endY - startY) * i) / steps;
    await cdp.send("Input.dispatchTouchEvent", {
      type: "touchMove",
      touchPoints: [{ x, y, id: 1 }],
    });
    await page.waitForTimeout(20);
  }

  // 3. Release.
  await cdp.send("Input.dispatchTouchEvent", {
    type: "touchEnd",
    touchPoints: [],
  });

  await rotatePromise;

  // Wait for the rotation to land in the DB. After dragging A (was
  // 09:00) below B, A should carry slot[2] (11:00) and B should carry
  // slot[1] (09:00) — content rotates while time/daily-number slots
  // stay anchored.
  await expect
    .poll(async () => (await readMeeting(idA)).start_time, {
      timeout: 10_000,
    })
    .toBe("11:00:00");

  const after = {
    A: await readMeeting(idA),
    B: await readMeeting(idB),
  };
  expect(after.A.title_en).toBe(`Touch Drag A ${uniq}`);
  expect(after.B.title_en).toBe(`Touch Drag B ${uniq}`);
  expect(String(after.A.start_time)).toBe("11:00:00");
  expect(String(after.A.end_time)).toBe("12:00:00");
  expect(String(after.B.start_time)).toBe("09:00:00");
  expect(String(after.B.end_time)).toBe("10:00:00");
  // Daily numbers stay anchored chronologically: B (now earlier) keeps
  // the smaller dn, A (now later) the larger.
  expect(after.B.daily_number).toBeLessThan(after.A.daily_number);
});
