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

// E2E coverage for Tasks #227 and #230 — "Restructure attendee cell
// controls around section headings" inside executive-meetings.
//
// What we verify:
//   1. The top "+ عنوان فرعي" chip is NEVER rendered, regardless of
//      cell state (empty / person-only / subheading-only / mixed).
//      The trailing "+ عنوان فرعي" button at the bottom of the cell is
//      the sole "start a heading" affordance, plus the after-section
//      chips between consecutive sections.
//   2. After-section "+ عنوان فرعي" chip: rendered after the last
//      person of each section that is followed by ANOTHER section
//      (i.e. a subheading comes next). Clicking it inserts a new
//      subheading at the boundary, BEFORE the existing next-section
//      subheading. Also rendered after a subheading whose next item is
//      another subheading (empty-section boundary).
//   3. Drag-and-drop reordering inside the manage dialog: picking up a
//      row by its grip handle and moving it down with the keyboard
//      reorders the attendee array, and saving persists the new order.
// All scenarios run in BOTH locales so the RTL/LTR mirror behaves the
// same way.

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

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

// Pick a far-future, randomised base so we never collide with other
// specs that also seed in the future.
const RANDOM_DATE_BASE_DAYS =
  2200 + 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 insertAttendee({
  meetingId,
  name,
  attendanceType,
  sortOrder,
  kind,
}) {
  await pool.query(
    `INSERT INTO executive_meeting_attendees
       (meeting_id, name, attendance_type, sort_order, kind)
     VALUES ($1, $2, $3, $4, $5)`,
    [meetingId, name, attendanceType, sortOrder, kind ?? "person"],
  );
}

async function getAttendeesOrdered(meetingId) {
  const { rows } = await pool.query(
    `SELECT name, attendance_type, sort_order, kind
     FROM executive_meeting_attendees
     WHERE meeting_id = $1
     ORDER BY sort_order ASC`,
    [meetingId],
  );
  return rows;
}

async function loginViaUi(page) {
  await page.goto("/login");
  await page.locator("#username").fill("admin");
  await page.locator("#password").fill("admin123");
  await Promise.all([
    page.waitForURL((url) => !url.pathname.endsWith("/login"), {
      timeout: 15_000,
    }),
    page.locator('form button[type="submit"]').click(),
  ]);
}

let originalAdminPreferredLanguage = null;
async function setAdminPreferredLanguage(lang) {
  const { rows } = await pool.query(
    `UPDATE users SET preferred_language = $1
     WHERE username = 'admin'
     RETURNING preferred_language`,
    [lang],
  );
  return rows[0]?.preferred_language;
}

// Toggle the global edit-mode toggle so the after-section
// "+ عنوان فرعي" chips and inline editing affordances render. The
// toggle lives in the schedule toolbar with
// data-testid="em-edit-mode-toggle".
async function ensureEditMode(page) {
  const toggle = page.getByTestId("em-edit-mode-toggle");
  await expect(toggle).toBeVisible({ timeout: 10_000 });
  const pressed = await toggle.getAttribute("aria-pressed");
  if (pressed !== "true") {
    await toggle.click();
    // Wait for the aria-pressed state to actually flip — the localStorage
    // write + state setter is async-ish in React, and clicking the chip
    // before edit mode commits would silently miss the conditional render.
    await expect(toggle).toHaveAttribute("aria-pressed", "true", {
      timeout: 5_000,
    });
  }
}

test.beforeAll(async () => {
  const { rows } = await pool.query(
    `SELECT preferred_language FROM users WHERE username = 'admin'`,
  );
  originalAdminPreferredLanguage = rows[0]?.preferred_language ?? "ar";
});

test.afterAll(async () => {
  if (originalAdminPreferredLanguage) {
    await pool
      .query(`UPDATE users SET preferred_language = $1 WHERE username = 'admin'`, [
        originalAdminPreferredLanguage,
      ])
      .catch(() => {
        /* best-effort */
      });
  }
  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();
});

const langOffsets = { en: 1, ar: 2 };
const langOffsets2 = { en: 3, ar: 4 };
const langOffsets3 = { en: 5, ar: 6 };
const langOffsets4 = { en: 7, ar: 8 };
const langOffsets5 = { en: 9, ar: 10 };

for (const lang of ["en", "ar"]) {
  test(`Schedule [${lang}]: top "+ subheading" chip is NEVER rendered (empty / person-only / subheading-only / mixed)`, async ({
    page,
  }) => {
    // Task #230: the top chip is gone in every state. We seed two
    // meetings that, between them, exercise all four cell states:
    //   meetingEmptyId: a brand-new meeting with zero attendees ⇒
    //                   the cell is EMPTY.
    //   meetingMixedId: three sections covering person-only,
    //                   subheading-only, and mixed (subheading +
    //                   persons) by spreading attendees across the
    //                   three attendance-type groups (internal,
    //                   virtual, external) which each render their own
    //                   AttendeeFlow.
    // For each addType we assert the top chip testid is absent. The
    // trailing "+ subheading" button stays as the cell-level fallback.
    const dateEmpty = uniqueFutureDate(langOffsets[lang]);
    const meetingEmptyId = await insertMeeting({
      meetingDate: dateEmpty,
      dailyNumber: 1,
      titleEn: `No Top Empty ${lang} ${Date.now().toString(36)}`,
      titleAr: `بدون علوي فارغ ${lang} ${Date.now().toString(36)}`,
      startTime: "09:00:00",
      endTime: "10:00:00",
    });

    const dateMixed = uniqueFutureDate(langOffsets[lang] + 50);
    const meetingMixedId = await insertMeeting({
      meetingDate: dateMixed,
      dailyNumber: 2,
      titleEn: `No Top Mixed ${lang} ${Date.now().toString(36)}`,
      titleAr: `بدون علوي مختلط ${lang} ${Date.now().toString(36)}`,
      startTime: "09:00:00",
      endTime: "10:00:00",
    });
    // internal: person-only (two persons, zero subheadings).
    await insertAttendee({
      meetingId: meetingMixedId,
      name: "InternalPerson1",
      attendanceType: "internal",
      sortOrder: 0,
      kind: "person",
    });
    await insertAttendee({
      meetingId: meetingMixedId,
      name: "InternalPerson2",
      attendanceType: "internal",
      sortOrder: 1,
      kind: "person",
    });
    // virtual: subheading-only (one subheading, zero persons).
    await insertAttendee({
      meetingId: meetingMixedId,
      name: "VirtualSoloHeader",
      attendanceType: "virtual",
      sortOrder: 2,
      kind: "subheading",
    });
    // external: mixed (subheading + person).
    await insertAttendee({
      meetingId: meetingMixedId,
      name: "ExternalHeader",
      attendanceType: "external",
      sortOrder: 3,
      kind: "subheading",
    });
    await insertAttendee({
      meetingId: meetingMixedId,
      name: "ExternalPerson",
      attendanceType: "external",
      sortOrder: 4,
      kind: "person",
    });

    await setAdminPreferredLanguage(lang);
    try {
      await loginViaUi(page);
      await page.goto("/executive-meetings");
      await expect(page.locator("html")).toHaveAttribute("lang", lang, {
        timeout: 5_000,
      });

      // EMPTY cell ⇒ default single addType group "internal" renders
      // with no items.
      await page.locator('input[type="date"]').first().fill(dateEmpty);
      const emptyRow = page.getByTestId(`em-row-${meetingEmptyId}`);
      await expect(emptyRow).toBeVisible({ timeout: 15_000 });
      await ensureEditMode(page);
      await expect(
        emptyRow.locator('[data-testid^="em-add-subheading-top-"]'),
      ).toHaveCount(0);
      // The trailing "+ subheading" button is still present so the
      // user can begin sectioning the cell.
      await expect(
        emptyRow.getByTestId("em-add-subheading-internal"),
      ).toBeVisible();

      // MIXED-meeting cell ⇒ three AttendeeFlow groups (internal /
      // virtual / external) each in a different state. The top chip
      // must be absent in every group. We prove all three groups
      // mounted by checking each one's per-group trailing "+" person
      // button is present (testid em-add-attendee-{addType}). NOTE:
      // since Task #234, the trailing "+ subheading" chip is suppressed
      // per group in split-mode cells and replaced by ONE cell-level
      // chip (testid em-add-subheading-cell-{meetingId}) — so the
      // per-group em-add-subheading-{addType} testids must NOT exist
      // here, and the cell-level chip MUST be visible.
      await page.locator('input[type="date"]').first().fill(dateMixed);
      const mixedRow = page.getByTestId(`em-row-${meetingMixedId}`);
      await expect(mixedRow).toBeVisible({ timeout: 15_000 });
      // Per-group "+" person buttons prove all three groups mounted.
      await expect(
        mixedRow.getByTestId("em-add-attendee-internal"),
      ).toBeVisible();
      await expect(
        mixedRow.getByTestId("em-add-attendee-virtual"),
      ).toBeVisible();
      await expect(
        mixedRow.getByTestId("em-add-attendee-external"),
      ).toBeVisible();
      // Per-group trailing "+ subheading" chips are SUPPRESSED in
      // split mode. The cell-level chip is the sole subheading
      // affordance below all three groups.
      await expect(
        mixedRow.getByTestId("em-add-subheading-internal"),
      ).toHaveCount(0);
      await expect(
        mixedRow.getByTestId("em-add-subheading-virtual"),
      ).toHaveCount(0);
      await expect(
        mixedRow.getByTestId("em-add-subheading-external"),
      ).toHaveCount(0);
      await expect(
        mixedRow.getByTestId(`em-add-subheading-cell-${meetingMixedId}`),
      ).toBeVisible();
      // Top chip absent in every group.
      await expect(
        mixedRow.locator('[data-testid^="em-add-subheading-top-"]'),
      ).toHaveCount(0);
      // Sanity counts (defence-in-depth so the assertion above isn't
      // trivially passing because the groups silently rendered empty).
      await expect(
        mixedRow.locator('[data-testid^="em-attendee-row-"]'),
      ).toHaveCount(3);
      await expect(
        mixedRow.locator('[data-testid^="em-attendee-subheading-"]'),
      ).toHaveCount(2);
    } finally {
      if (originalAdminPreferredLanguage) {
        await setAdminPreferredLanguage(originalAdminPreferredLanguage).catch(
          () => {
            /* best-effort; afterAll also restores */
          },
        );
      }
    }
  });

  test(`Schedule [${lang}]: after-section "+ subheading" chip inserts a subheading between two existing sections`, async ({
    page,
  }) => {
    const date = uniqueFutureDate(langOffsets[lang] + 100);
    const meetingId = await insertMeeting({
      meetingDate: date,
      dailyNumber: 2,
      titleEn: `Mid Subheading ${lang} ${Date.now().toString(36)}`,
      titleAr: `عنوان فرعي وسطي ${lang} ${Date.now().toString(36)}`,
      startTime: "09:00:00",
      endTime: "10:00:00",
    });
    // Seed: [Sec1Header, Alpha, Beta, Sec2Header, Gamma]. The
    // after-section chip is rendered after the last person of each
    // section that has another section right after it. Beta sits at
    // attendees idx 2 with Sec2Header at idx 3 → chip testid is
    // `em-add-subheading-after-2`. The trailing section (Gamma) has
    // nothing after it → no after-section chip there.
    await insertAttendee({
      meetingId,
      name: "Sec1Header",
      attendanceType: "internal",
      sortOrder: 0,
      kind: "subheading",
    });
    await insertAttendee({
      meetingId,
      name: "PersonAlpha",
      attendanceType: "internal",
      sortOrder: 1,
      kind: "person",
    });
    await insertAttendee({
      meetingId,
      name: "PersonBeta",
      attendanceType: "internal",
      sortOrder: 2,
      kind: "person",
    });
    await insertAttendee({
      meetingId,
      name: "Sec2Header",
      attendanceType: "internal",
      sortOrder: 3,
      kind: "subheading",
    });
    await insertAttendee({
      meetingId,
      name: "PersonGamma",
      attendanceType: "internal",
      sortOrder: 4,
      kind: "person",
    });

    await setAdminPreferredLanguage(lang);
    try {
      await loginViaUi(page);
      await page.goto("/executive-meetings");
      await expect(page.locator("html")).toHaveAttribute("lang", lang, {
        timeout: 5_000,
      });
      await page.locator('input[type="date"]').first().fill(date);

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

      const chip = row.getByTestId("em-add-subheading-after-2");
      await expect(chip).toBeVisible({ timeout: 10_000 });
      // The trailing section's last person (Gamma at idx 4) must NOT
      // have an after-section chip — the trailing "+ subheading"
      // button below already covers that slot.
      await expect(row.getByTestId("em-add-subheading-after-4")).toHaveCount(0);
      await chip.click();

      const pending = row.getByTestId("em-add-subheading-pending-internal");
      await expect(pending).toBeVisible({ timeout: 5_000 });
      const editor = pending.locator('[contenteditable="true"]').first();
      await expect(editor).toBeVisible({ timeout: 5_000 });
      await editor.click();
      await editor.fill("MidSection");
      await page.locator("body").click({ position: { x: 5, y: 5 } });

      await expect(
        row.locator('[data-testid^="em-attendee-row-"]'),
      ).toHaveCount(3, { timeout: 10_000 });
      await expect(
        row.locator('[data-testid^="em-attendee-subheading-"]'),
      ).toHaveCount(3);

      const persisted = await getAttendeesOrdered(meetingId);
      const stripTags = (s) => s.replace(/<[^>]+>/g, "").trim();
      const orderedNames = persisted.map((r) => stripTags(r.name));
      // The new subheading must land BEFORE Sec2Header (the existing
      // next-section subheading), at slot 3.
      expect(orderedNames).toEqual([
        "Sec1Header",
        "PersonAlpha",
        "PersonBeta",
        "MidSection",
        "Sec2Header",
        "PersonGamma",
      ]);
      expect(persisted.map((r) => r.kind)).toEqual([
        "subheading",
        "person",
        "person",
        "subheading",
        "subheading",
        "person",
      ]);
      expect(persisted.map((r) => r.sort_order)).toEqual([0, 1, 2, 3, 4, 5]);
    } finally {
      if (originalAdminPreferredLanguage) {
        await setAdminPreferredLanguage(originalAdminPreferredLanguage).catch(
          () => {
            /* best-effort; afterAll also restores */
          },
        );
      }
    }
  });

  test(`Schedule [${lang}]: after-section "+ subheading" chip renders after an EMPTY section (consecutive subheadings)`, async ({
    page,
  }) => {
    const date = uniqueFutureDate(langOffsets[lang] + 200);
    const meetingId = await insertMeeting({
      meetingDate: date,
      dailyNumber: 4,
      titleEn: `Empty Section ${lang} ${Date.now().toString(36)}`,
      titleAr: `قسم فارغ ${lang} ${Date.now().toString(36)}`,
      startTime: "09:00:00",
      endTime: "10:00:00",
    });
    // Seed: [SubA, SubB, PersonZ]. SubA leads an EMPTY section (next
    // item is also a subheading). The user must be able to insert a
    // new subheading at the boundary between SubA and SubB — the chip
    // is rendered AFTER SubA (testid em-add-subheading-after-0,
    // insertAtIndex=1).
    await insertAttendee({
      meetingId,
      name: "SubA",
      attendanceType: "internal",
      sortOrder: 0,
      kind: "subheading",
    });
    await insertAttendee({
      meetingId,
      name: "SubB",
      attendanceType: "internal",
      sortOrder: 1,
      kind: "subheading",
    });
    await insertAttendee({
      meetingId,
      name: "PersonZ",
      attendanceType: "internal",
      sortOrder: 2,
      kind: "person",
    });

    await setAdminPreferredLanguage(lang);
    try {
      await loginViaUi(page);
      await page.goto("/executive-meetings");
      await expect(page.locator("html")).toHaveAttribute("lang", lang, {
        timeout: 5_000,
      });
      await page.locator('input[type="date"]').first().fill(date);

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

      // The empty-section chip lives right after SubA (idx 0), since
      // SubB at idx 1 is also a subheading.
      const chip = row.getByTestId("em-add-subheading-after-0");
      await expect(chip).toBeVisible({ timeout: 10_000 });
      await chip.click();

      const pending = row.getByTestId("em-add-subheading-pending-internal");
      await expect(pending).toBeVisible({ timeout: 5_000 });
      const editor = pending.locator('[contenteditable="true"]').first();
      await expect(editor).toBeVisible({ timeout: 5_000 });
      await editor.click();
      await editor.fill("InsertedMid");
      await page.locator("body").click({ position: { x: 5, y: 5 } });

      await expect(
        row.locator('[data-testid^="em-attendee-subheading-"]'),
      ).toHaveCount(3, { timeout: 10_000 });

      const persisted = await getAttendeesOrdered(meetingId);
      const stripTags = (s) => s.replace(/<[^>]+>/g, "").trim();
      const orderedNames = persisted.map((r) => stripTags(r.name));
      // The new subheading must land at idx 1 (between SubA and SubB).
      expect(orderedNames).toEqual([
        "SubA",
        "InsertedMid",
        "SubB",
        "PersonZ",
      ]);
      expect(persisted.map((r) => r.kind)).toEqual([
        "subheading",
        "subheading",
        "subheading",
        "person",
      ]);
      expect(persisted.map((r) => r.sort_order)).toEqual([0, 1, 2, 3]);
    } finally {
      if (originalAdminPreferredLanguage) {
        await setAdminPreferredLanguage(originalAdminPreferredLanguage).catch(
          () => {
            /* best-effort; afterAll also restores */
          },
        );
      }
    }
  });

  test(`Manage [${lang}]: drag handle reorders attendees inside the dialog`, async ({
    page,
  }) => {
    const date = uniqueFutureDate(langOffsets2[lang]);
    const meetingId = await insertMeeting({
      meetingDate: date,
      dailyNumber: 1,
      titleEn: `Drag Reorder ${lang} ${Date.now().toString(36)}`,
      titleAr: `سحب وإعادة ترتيب ${lang} ${Date.now().toString(36)}`,
      startTime: "11:00:00",
      endTime: "12:00:00",
    });
    await insertAttendee({
      meetingId,
      name: "Drag-A",
      attendanceType: "internal",
      sortOrder: 0,
      kind: "person",
    });
    await insertAttendee({
      meetingId,
      name: "Drag-B",
      attendanceType: "internal",
      sortOrder: 1,
      kind: "person",
    });
    await insertAttendee({
      meetingId,
      name: "Drag-C",
      attendanceType: "internal",
      sortOrder: 2,
      kind: "person",
    });

    await setAdminPreferredLanguage(lang);
    try {
      await loginViaUi(page);
      await page.goto("/executive-meetings");
      await expect(page.locator("html")).toHaveAttribute("lang", lang, {
        timeout: 5_000,
      });

      // Switch to the Manage section via the page-level nav. The
      // manage dialog is only mounted from inside the ManageSection.
      const manageNav = page.getByTestId("em-nav-manage");
      await expect(manageNav).toBeVisible({ timeout: 10_000 });
      await manageNav.click();
      await page.locator('input[type="date"]').first().fill(date);

      // Open the edit dialog for our meeting (the pencil button on the
      // row uses the meeting id as its testid suffix).
      const editBtn = page.getByTestId(`em-edit-${meetingId}`);
      await expect(editBtn).toBeVisible({ timeout: 10_000 });
      await editBtn.click();

      // The dialog renders three sortable rows — verify drag handles
      // are present and aria-labelled.
      const handle0 = page.getByTestId("em-attendee-drag-0");
      await expect(handle0).toBeVisible({ timeout: 5_000 });

      // Use dnd-kit's KeyboardSensor flow to move row 0 down two
      // slots. This is more reliable across Playwright browser engines
      // than synthesised pointer drags. dnd-kit picks up Space and
      // moves with arrow keys, then drops with Space again.
      await handle0.focus();
      await page.keyboard.press("Space");
      await page.keyboard.press("ArrowDown");
      await page.keyboard.press("ArrowDown");
      await page.keyboard.press("Space");

      // Verify the visible name inputs reordered to [B, C, A] before we
      // even save — this catches DnD wiring without relying on a save
      // round-trip.
      const nameInputs = page.locator(
        '[data-testid^="em-attendee-row-"] input[placeholder]',
      );
      // Filter to the first input of each row (the name field).
      const firstNames = await page
        .locator('[data-testid^="em-attendee-row-"]')
        .evaluateAll((rows) =>
          rows.map((r) => {
            const inp = r.querySelector("input");
            return inp ? inp.value : "";
          }),
        );
      expect(firstNames).toEqual(["Drag-B", "Drag-C", "Drag-A"]);
      void nameInputs;

      // Save and verify persistence.
      const saveBtn = page.getByTestId("em-form-save");
      await saveBtn.click();
      // Wait for the dialog to close.
      await expect(saveBtn).toBeHidden({ timeout: 10_000 });

      const persisted = await getAttendeesOrdered(meetingId);
      expect(persisted.map((r) => r.name)).toEqual([
        "Drag-B",
        "Drag-C",
        "Drag-A",
      ]);
      expect(persisted.map((r) => r.sort_order)).toEqual([0, 1, 2]);
    } finally {
      if (originalAdminPreferredLanguage) {
        await setAdminPreferredLanguage(originalAdminPreferredLanguage).catch(
          () => {
            /* best-effort; afterAll also restores */
          },
        );
      }
    }
  });

  test(`Manage [${lang}]: drag-reorder works across mixed person + subheading rows`, async ({
    page,
  }) => {
    // Architect-flagged coverage: a meeting that contains BOTH person
    // rows and a subheading row, then dragged so the subheading lands
    // in a new section position. We verify (a) the kind survives the
    // round-trip, (b) the new order persists, (c) numbering on the
    // schedule view recomputes correctly after the kinds shift.
    const date = uniqueFutureDate(langOffsets3[lang]);
    const meetingId = await insertMeeting({
      meetingDate: date,
      dailyNumber: 1,
      titleEn: `Mixed Drag ${lang} ${Date.now().toString(36)}`,
      titleAr: `سحب مختلط ${lang} ${Date.now().toString(36)}`,
      startTime: "13:00:00",
      endTime: "14:00:00",
    });
    // Seed: [PersonM, SubX, PersonN, PersonO]. We will pick up
    // SubX (index 1) and move it down past PersonN (one ArrowDown).
    // Expected after drag: [PersonM, PersonN, SubX, PersonO].
    await insertAttendee({
      meetingId,
      name: "PersonM",
      attendanceType: "internal",
      sortOrder: 0,
      kind: "person",
    });
    await insertAttendee({
      meetingId,
      name: "MixSubX-K3",
      attendanceType: "internal",
      sortOrder: 1,
      kind: "subheading",
    });
    await insertAttendee({
      meetingId,
      name: "PersonN",
      attendanceType: "internal",
      sortOrder: 2,
      kind: "person",
    });
    await insertAttendee({
      meetingId,
      name: "PersonO",
      attendanceType: "internal",
      sortOrder: 3,
      kind: "person",
    });

    await setAdminPreferredLanguage(lang);
    try {
      await loginViaUi(page);
      await page.goto("/executive-meetings");

      const manageNav = page.getByTestId("em-nav-manage");
      await expect(manageNav).toBeVisible({ timeout: 10_000 });
      await manageNav.click();
      await page.locator('input[type="date"]').first().fill(date);

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

      // Pick up the subheading row (index 1) and move it down once.
      const handle1 = page.getByTestId("em-attendee-drag-1");
      await expect(handle1).toBeVisible({ timeout: 5_000 });
      await handle1.focus();
      await page.keyboard.press("Space");
      await page.keyboard.press("ArrowDown");
      await page.keyboard.press("Space");

      // Pre-save check: the row at index 1 is now PersonN (was the
      // subheading), and the row at index 2 is the subheading.
      const kinds = await page
        .locator('[data-testid^="em-attendee-row-"]')
        .evaluateAll((rows) =>
          rows.map((r) => r.getAttribute("data-attendee-kind")),
        );
      expect(kinds).toEqual(["person", "person", "subheading", "person"]);

      const saveBtn = page.getByTestId("em-form-save");
      await saveBtn.click();
      await expect(saveBtn).toBeHidden({ timeout: 10_000 });

      const persisted = await getAttendeesOrdered(meetingId);
      expect(persisted.map((r) => r.name)).toEqual([
        "PersonM",
        "PersonN",
        "MixSubX-K3",
        "PersonO",
      ]);
      expect(persisted.map((r) => r.kind)).toEqual([
        "person",
        "person",
        "subheading",
        "person",
      ]);
      expect(persisted.map((r) => r.sort_order)).toEqual([0, 1, 2, 3]);
    } finally {
      if (originalAdminPreferredLanguage) {
        await setAdminPreferredLanguage(originalAdminPreferredLanguage).catch(
          () => {
            /* best-effort; afterAll also restores */
          },
        );
      }
    }
  });

  test(`Schedule [${lang}]: split-mode cell shows ONE cell-level "+ subheading" chip and routes to the LAST visible group`, async ({
    page,
  }) => {
    // Task #234: in a split-mode cell (≥2 attendance groups visible),
    // the trailing "+ عنوان فرعي" chip must NOT be rendered per group;
    // a single cell-level chip below all groups is rendered instead.
    // Clicking it adds the new subheading to the LAST visible group in
    // render order (virtual → internal → external). Per-group "+"
    // person buttons are unaffected.
    //
    // hasSplit requires `virtual.length > 0 && (internal || external)`,
    // so we seed all three groups. Render order makes EXTERNAL the
    // last visible group, and the new subheading must persist at the
    // end of the external group's rows.
    const date = uniqueFutureDate(langOffsets4[lang]);
    const meetingId = await insertMeeting({
      meetingDate: date,
      dailyNumber: 7,
      titleEn: `Cell-Level Subheading ${lang} ${Date.now().toString(36)}`,
      titleAr: `عنوان فرعي للخلية ${lang} ${Date.now().toString(36)}`,
      startTime: "14:00:00",
      endTime: "15:00:00",
    });
    // Virtual: 1 person (just to satisfy hasSplit — render order
    // virtual → internal → external means virtual is first).
    await insertAttendee({
      meetingId,
      name: "VirtualP1",
      attendanceType: "virtual",
      sortOrder: 0,
      kind: "person",
    });
    // Internal: 2 persons (mirrors the "الحضور الداخلي" block from
    // the user's screenshot).
    await insertAttendee({
      meetingId,
      name: "InternalP1",
      attendanceType: "internal",
      sortOrder: 1,
      kind: "person",
    });
    await insertAttendee({
      meetingId,
      name: "InternalP2",
      attendanceType: "internal",
      sortOrder: 2,
      kind: "person",
    });
    // External: 2 persons (mirrors the "الخارجي" block — the LAST
    // visible group, so the cell-level chip routes here).
    await insertAttendee({
      meetingId,
      name: "ExternalP1",
      attendanceType: "external",
      sortOrder: 3,
      kind: "person",
    });
    await insertAttendee({
      meetingId,
      name: "ExternalP2",
      attendanceType: "external",
      sortOrder: 4,
      kind: "person",
    });

    await setAdminPreferredLanguage(lang);
    try {
      await loginViaUi(page);
      await page.goto("/executive-meetings");
      await expect(page.locator("html")).toHaveAttribute("lang", lang, {
        timeout: 5_000,
      });
      await page.locator('input[type="date"]').first().fill(date);

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

      // Exactly ONE cell-level chip is rendered.
      const cellChip = row.getByTestId(
        `em-add-subheading-cell-${meetingId}`,
      );
      await expect(cellChip).toBeVisible({ timeout: 10_000 });
      await expect(
        row.locator('[data-testid^="em-add-subheading-cell-"]'),
      ).toHaveCount(1);

      // Per-group trailing "+ subheading" chips are SUPPRESSED in all
      // three groups.
      await expect(
        row.getByTestId("em-add-subheading-virtual"),
      ).toHaveCount(0);
      await expect(
        row.getByTestId("em-add-subheading-internal"),
      ).toHaveCount(0);
      await expect(
        row.getByTestId("em-add-subheading-external"),
      ).toHaveCount(0);

      // Per-group "+" person buttons remain unchanged — one per group.
      await expect(row.getByTestId("em-add-attendee-virtual")).toBeVisible();
      await expect(row.getByTestId("em-add-attendee-internal")).toBeVisible();
      await expect(row.getByTestId("em-add-attendee-external")).toBeVisible();

      // Click the cell-level chip → pending input opens. Because the
      // last visible group is external, the pending ghost lives in
      // the external group's flow (testid uses the addType suffix).
      await cellChip.click();
      const pending = row.getByTestId(
        "em-add-subheading-pending-external",
      );
      await expect(pending).toBeVisible({ timeout: 5_000 });
      const editor = pending.locator('[contenteditable="true"]').first();
      await expect(editor).toBeVisible({ timeout: 5_000 });
      await editor.click();
      await editor.fill("CellChipSection");
      await page.locator("body").click({ position: { x: 5, y: 5 } });

      // Persisted: the new subheading lands at the end of the external
      // group (sort_order 5, after the two external persons).
      await expect(
        row.locator('[data-testid^="em-attendee-subheading-"]'),
      ).toHaveCount(1, { timeout: 10_000 });

      const persisted = await getAttendeesOrdered(meetingId);
      const stripTags = (s) => s.replace(/<[^>]+>/g, "").trim();
      expect(persisted.map((r) => stripTags(r.name))).toEqual([
        "VirtualP1",
        "InternalP1",
        "InternalP2",
        "ExternalP1",
        "ExternalP2",
        "CellChipSection",
      ]);
      expect(persisted.map((r) => r.kind)).toEqual([
        "person",
        "person",
        "person",
        "person",
        "person",
        "subheading",
      ]);
      expect(persisted.map((r) => r.attendance_type)).toEqual([
        "virtual",
        "internal",
        "internal",
        "external",
        "external",
        "external",
      ]);
      expect(persisted.map((r) => r.sort_order)).toEqual([0, 1, 2, 3, 4, 5]);
    } finally {
      if (originalAdminPreferredLanguage) {
        await setAdminPreferredLanguage(originalAdminPreferredLanguage).catch(
          () => {
            /* best-effort; afterAll also restores */
          },
        );
      }
    }
  });

  test(`Schedule [${lang}]: split-mode WITHOUT external — cell-level "+ subheading" chip routes to INTERNAL (last visible)`, async ({
    page,
  }) => {
    // Task #234 fallback path. The "last visible group" rule resolves
    // the cell-level chip's target at click-time as
    // external > internal > virtual. The previous test exercises the
    // external-wins path; this one locks the internal-wins fallback by
    // seeding ONLY virtual + internal (no external). The cell-level
    // chip must still be the sole subheading affordance, and clicking
    // it must persist a new subheading at the end of the INTERNAL
    // group (not virtual, even though virtual is the "last seeded"
    // group in sort_order).
    const date = uniqueFutureDate(langOffsets5[lang]);
    const meetingId = await insertMeeting({
      meetingDate: date,
      dailyNumber: 9,
      titleEn: `Cell Chip No External ${lang} ${Date.now().toString(36)}`,
      titleAr: `لا خارجي ${lang} ${Date.now().toString(36)}`,
      startTime: "16:00:00",
      endTime: "17:00:00",
    });
    // Virtual: 1 person (satisfies hasSplit).
    await insertAttendee({
      meetingId,
      name: "VirtP1",
      attendanceType: "virtual",
      sortOrder: 0,
      kind: "person",
    });
    // Internal: 2 persons (this is the LAST visible group in render
    // order virtual → internal → external when external is empty).
    await insertAttendee({
      meetingId,
      name: "IntP1",
      attendanceType: "internal",
      sortOrder: 1,
      kind: "person",
    });
    await insertAttendee({
      meetingId,
      name: "IntP2",
      attendanceType: "internal",
      sortOrder: 2,
      kind: "person",
    });

    await setAdminPreferredLanguage(lang);
    try {
      await loginViaUi(page);
      await page.goto("/executive-meetings");
      await expect(page.locator("html")).toHaveAttribute("lang", lang, {
        timeout: 5_000,
      });
      await page.locator('input[type="date"]').first().fill(date);

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

      // Exactly one cell-level chip; no per-group subheading chips.
      const cellChip = row.getByTestId(
        `em-add-subheading-cell-${meetingId}`,
      );
      await expect(cellChip).toBeVisible({ timeout: 10_000 });
      await expect(
        row.getByTestId("em-add-subheading-virtual"),
      ).toHaveCount(0);
      await expect(
        row.getByTestId("em-add-subheading-internal"),
      ).toHaveCount(0);
      // External didn't even render a group, so its subheading testid
      // is also absent (defence-in-depth).
      await expect(
        row.getByTestId("em-add-subheading-external"),
      ).toHaveCount(0);

      // Click → pending input opens in the INTERNAL group's flow.
      await cellChip.click();
      const pending = row.getByTestId(
        "em-add-subheading-pending-internal",
      );
      await expect(pending).toBeVisible({ timeout: 5_000 });
      const editor = pending.locator('[contenteditable="true"]').first();
      await expect(editor).toBeVisible({ timeout: 5_000 });
      await editor.click();
      await editor.fill("InternalTailSection");
      await page.locator("body").click({ position: { x: 5, y: 5 } });

      await expect(
        row.locator('[data-testid^="em-attendee-subheading-"]'),
      ).toHaveCount(1, { timeout: 10_000 });

      // Persisted: the new subheading lands at the end of the
      // internal group (sort_order 3, after the two internal persons).
      const persisted = await getAttendeesOrdered(meetingId);
      const stripTags = (s) => s.replace(/<[^>]+>/g, "").trim();
      expect(persisted.map((r) => stripTags(r.name))).toEqual([
        "VirtP1",
        "IntP1",
        "IntP2",
        "InternalTailSection",
      ]);
      expect(persisted.map((r) => r.kind)).toEqual([
        "person",
        "person",
        "person",
        "subheading",
      ]);
      expect(persisted.map((r) => r.attendance_type)).toEqual([
        "virtual",
        "internal",
        "internal",
        "internal",
      ]);
      expect(persisted.map((r) => r.sort_order)).toEqual([0, 1, 2, 3]);
    } finally {
      if (originalAdminPreferredLanguage) {
        await setAdminPreferredLanguage(originalAdminPreferredLanguage).catch(
          () => {
            /* best-effort; afterAll also restores */
          },
        );
      }
    }
  });
}
