// #489: e2e for "drag a meeting from anywhere on its row to a new
// position". The schedule's time column AND daily numbers stay
// anchored to the physical rows; only the meeting CONTENT (title,
// attendees, notes, color, status, merge) rotates through the slots.
//
// We drag row #1 (Alpha @ 09:00) down to row #3 (Charlie @ 11:00).
// Expected DB result on the day:
//   - The (start_time, end_time, daily_number) tuples on the date are
//     unchanged set-wise — slot[1] = (09:00, 09:30, dn1), slot[2] =
//     (10:00, 10:30, dn2), slot[3] = (11:00, 11:30, dn3) all still
//     exist.
//   - But the Alpha meeting (originally slot[1]) now carries the
//     slot[3] tuple and the Bravo + Charlie meetings rotated up.
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-drag test");
}
const pool = new pg.Pool({ connectionString: DATABASE_URL });
const createdMeetingIds = [];

function pickFutureDate(offsetDays) {
  const d = new Date(Date.now() + offsetDays * 86_400_000);
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, "0");
  const day = String(d.getDate()).padStart(2, "0");
  return `${y}-${m}-${day}`;
}

const DATE_DRAG = pickFutureDate(40);

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({ date, titleEn, startTime, endTime }) {
  const dn = await nextDailyNumberForDate(date);
  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, date, 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];
}

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 gotoTestDate(page, date) {
  await page.goto("/executive-meetings");
  const dateInput = page.locator('input[type="date"]').first();
  await dateInput.waitFor({ state: "visible", timeout: 10_000 });
  await dateInput.fill(date);
  await dateInput.dispatchEvent("change");
}

test.afterAll(async () => {
  if (createdMeetingIds.length > 0) {
    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_meeting_attendees WHERE meeting_id = ANY($1::int[])`,
      [createdMeetingIds],
    );
    await pool.query(
      `DELETE FROM executive_meetings WHERE id = ANY($1::int[])`,
      [createdMeetingIds],
    );
  }
  await pool.end();
});

test("dragging a row from position 1 to position 3 rotates content but anchors time + daily numbers", async ({
  page,
}) => {
  const alphaId = await insertMeeting({
    date: DATE_DRAG,
    titleEn: "Drag Row Alpha",
    startTime: "09:00:00",
    endTime: "09:30:00",
  });
  const bravoId = await insertMeeting({
    date: DATE_DRAG,
    titleEn: "Drag Row Bravo",
    startTime: "10:00:00",
    endTime: "10:30:00",
  });
  const charlieId = await insertMeeting({
    date: DATE_DRAG,
    titleEn: "Drag Row Charlie",
    startTime: "11:00:00",
    endTime: "11:30:00",
  });
  // Snapshot the per-physical-row daily numbers before the drag — they
  // must still match (sorted by start_time) after the rotation.
  const beforeAlpha = await readMeeting(alphaId);
  const beforeBravo = await readMeeting(bravoId);
  const beforeCharlie = await readMeeting(charlieId);
  const dnSet = new Set([
    beforeAlpha.daily_number,
    beforeBravo.daily_number,
    beforeCharlie.daily_number,
  ]);

  await loginViaUi(page, "admin", "admin123");
  await gotoTestDate(page, DATE_DRAG);
  const alphaRow = page.locator(`[data-testid="em-row-${alphaId}"]`);
  const charlieRow = page.locator(`[data-testid="em-row-${charlieId}"]`);
  await expect(alphaRow).toBeVisible({ timeout: 10_000 });
  await expect(charlieRow).toBeVisible();

  // #489: row drag must work for any user with mutate permission
  // even when edit mode is OFF — we deliberately do NOT toggle edit
  // mode here. The row's useSortable is gated on `dragEnabled`
  // (raw canMutate), independent of `effectiveCanMutate` which gates
  // inline-cell editing.

  // The whole <tr> is the drag handle now (the dedicated grip is
  // gone). dnd-kit's PointerSensor activates after 6px of movement.
  // safeRowDragListeners skips drags that start over interactive
  // descendants (buttons, inputs, time/edit cells, row-actions,
  // bulk-select). The # cell only contains a plain <span> with the
  // daily number — no skip-listed children — so it is the safest
  // drag-start surface.
  const alphaNumberCell = page
    .locator(`[data-testid="em-row-${alphaId}"] td`)
    .first();
  const numBox = await alphaNumberCell.boundingBox();
  const charlieBox = await charlieRow.boundingBox();
  if (!numBox || !charlieBox) {
    throw new Error("rows + # cell must have a bounding box for the drag");
  }
  const startX = numBox.x + numBox.width / 2;
  const startY = numBox.y + numBox.height / 2;
  const endX = startX;
  const endY = charlieBox.y + charlieBox.height - 4;

  // Wait for the rotateContent POST so we don't race on the DB poll.
  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 },
  );

  await page.mouse.move(startX, startY);
  await page.mouse.down();
  // Trip the 6px activation distance with vertical motion (the row
  // axis we actually want to reorder along), then continue to the
  // target row's lower edge so dnd-kit's collision detection settles
  // on Charlie as the over-target rather than Bravo.
  await page.mouse.move(startX, startY + 12, { steps: 5 });
  await page.mouse.move(endX, endY, { steps: 15 });
  await page.mouse.up();

  await rotatePromise;

  // Wait for the rotation to land in the DB.
  await expect
    .poll(async () => (await readMeeting(alphaId)).start_time, {
      timeout: 10_000,
    })
    .toBe("11:00:00");

  const alphaAfter = await readMeeting(alphaId);
  const bravoAfter = await readMeeting(bravoId);
  const charlieAfter = await readMeeting(charlieId);

  // Alpha rotated to position 3 → carries the slot[3] tuple.
  expect(alphaAfter.start_time).toBe("11:00:00");
  expect(alphaAfter.end_time).toBe("11:30:00");
  // Titles are GLUED to their meeting rows — the content moves as a
  // unit. The user dragged the Alpha CONTENT to the bottom slot.
  expect(alphaAfter.title_en).toBe("Drag Row Alpha");
  expect(bravoAfter.title_en).toBe("Drag Row Bravo");
  expect(charlieAfter.title_en).toBe("Drag Row Charlie");
  // Bravo + Charlie rotated up by one slot.
  expect(bravoAfter.start_time).toBe("09:00:00");
  expect(bravoAfter.end_time).toBe("09:30:00");
  expect(charlieAfter.start_time).toBe("10:00:00");
  expect(charlieAfter.end_time).toBe("10:30:00");

  // Daily numbers — the SET on the day is unchanged (the # column
  // stays anchored to physical rows in chronological order). After the
  // renumber-by-start-time, Bravo (now 09:00) holds the smallest dn,
  // Charlie (10:00) the middle, Alpha (11:00) the largest.
  const afterDnSet = new Set([
    alphaAfter.daily_number,
    bravoAfter.daily_number,
    charlieAfter.daily_number,
  ]);
  expect(afterDnSet).toEqual(dnSet);
  expect(bravoAfter.daily_number).toBeLessThan(charlieAfter.daily_number);
  expect(charlieAfter.daily_number).toBeLessThan(alphaAfter.daily_number);
});
