import { test, expect } from "@playwright/test";
import pg from "pg";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";

const __dirname = dirname(fileURLToPath(import.meta.url));

// Load the same locale bundles the app reads from so any copy edits to
// the subheading labels stay covered without test churn.
function loadLocale(lang) {
  const p = join(__dirname, "..", "src", "locales", `${lang}.json`);
  return JSON.parse(readFileSync(p, "utf8"));
}
const localeBundles = { en: loadLocale("en"), ar: loadLocale("ar") };

// E2E coverage for Task #207 — custom subheadings inside the attendees
// cell. Subheadings are user-defined section labels that live alongside
// person rows in a meeting's attendee list, but are NOT counted as
// attendees and do NOT advance the running "1-, 2-, 3-…" person index.
//
// What we verify:
//   1. Mixed group: a meeting with 2 internal persons + 1 subheading
//      between them + 1 more person renders all four rows. The
//      subheading carries `data-testid="em-attendee-subheading-…"`,
//      and the three persons get the indices 1-, 2-, 3-.
//   2. Zero subheading: a meeting with only persons keeps the legacy
//      numbering exactly as before (no extra "subheading" testids,
//      no off-by-one).
// Both scenarios run in BOTH locales so the RTL/LTR mirror behaves the
// same.

const DATABASE_URL = process.env.DATABASE_URL;
if (!DATABASE_URL) {
  throw new Error(
    "DATABASE_URL must be set to run the executive-meetings subheading UI tests",
  );
}
// Parse to silence the "unused" lint until we use the bundle below.
void localeBundles;

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

// Pick a far-future, randomised base so we never collide with real
// scheduled meetings while running side by side with other specs that
// also seed in the future.
const RANDOM_DATE_BASE_DAYS =
  900 + 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 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;
}

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 };
for (const lang of ["en", "ar"]) {
  test(`Schedule [${lang}]: subheading rows render and do not advance the person index`, async ({
    page,
  }) => {
    const date = uniqueFutureDate(langOffsets[lang]);
    const meetingId = await insertMeeting({
      meetingDate: date,
      dailyNumber: 1,
      titleEn: `Subheading Mixed ${lang} ${Date.now().toString(36)}`,
      titleAr: `عنوان فرعي مختلط ${lang} ${Date.now().toString(36)}`,
      startTime: "09:00:00",
      endTime: "10:00:00",
    });
    // Internal group with a subheading wedged between the second and
    // third person. The subheading starts a fresh numbered section, so
    // persons render as 1-, 2- (before the subheading) and 1- (after),
    // PLUS the subheading row itself.
    await insertAttendee({
      meetingId,
      name: "Person Alpha",
      attendanceType: "internal",
      sortOrder: 0,
      kind: "person",
    });
    await insertAttendee({
      meetingId,
      name: "Person Beta",
      attendanceType: "internal",
      sortOrder: 1,
      kind: "person",
    });
    await insertAttendee({
      meetingId,
      // Distinctive label so the assertion below cannot trip on a
      // person name that incidentally contains the same word.
      name: "GuestsLabel-X9",
      attendanceType: "internal",
      sortOrder: 2,
      kind: "subheading",
    });
    await insertAttendee({
      meetingId,
      name: "Person Gamma",
      attendanceType: "internal",
      sortOrder: 3,
      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 });

      // Exactly one subheading row in this meeting.
      const subheadings = row.locator(
        '[data-testid^="em-attendee-subheading-"]',
      );
      await expect(subheadings).toHaveCount(1, { timeout: 10_000 });
      await expect(subheadings.first()).toContainText("GuestsLabel-X9");

      // Three person rows rendered (subheading is excluded by the
      // person-only `em-attendee-row-…` testid).
      const personRows = row.locator('[data-testid^="em-attendee-row-"]');
      await expect(personRows).toHaveCount(3);

      // Numbering restarts after the subheading: section 1 has Alpha
      // and Beta numbered "1-" and "2-"; section 2 has Gamma alone,
      // numbered "1-" (the cell has a subheading so we always show the
      // index, even for a solo person inside a section).
      const indices = row.locator('[data-testid^="em-attendee-index-"]');
      await expect(indices).toHaveCount(3);
      const indexTexts = await indices.allInnerTexts();
      const trimmed = indexTexts.map((s) => s.trim().replace(/\s+/g, ""));
      expect(trimmed).toEqual(["1-", "2-", "1-"]);
    } finally {
      if (originalAdminPreferredLanguage) {
        await setAdminPreferredLanguage(originalAdminPreferredLanguage).catch(
          () => {
            /* best-effort; afterAll also restores */
          },
        );
      }
    }
  });

  test(`Schedule [${lang}]: subheading-only group survives deletion of all its persons (hasSplit preservation)`, async ({
    page,
  }) => {
    // Regression guard for the hasSplit edge case the architect flagged:
    // if a group still has a subheading after the user deletes every
    // person in it, the group must keep its own AttendeeGroup wrapper
    // (and its label) — its subheading should NOT silently fold into
    // a sibling group's row. We seed virtual = [subheading] +
    // internal = [persons] directly via the DB to skip the editing
    // path and assert only the rendering invariant.
    const date = uniqueFutureDate(langOffsets[lang] + 10);
    const meetingId = await insertMeeting({
      meetingDate: date,
      dailyNumber: 1,
      titleEn: `Sub Solo Group ${lang} ${Date.now().toString(36)}`,
      titleAr: `قسم بعنوان فرعي ${lang} ${Date.now().toString(36)}`,
      startTime: "09:00:00",
      endTime: "10:00:00",
    });
    // Virtual group: ONE subheading row, zero persons. This is the
    // post-delete state described in the architect comment.
    await insertAttendee({
      meetingId,
      name: "Virtual Block Label",
      attendanceType: "virtual",
      sortOrder: 0,
      kind: "subheading",
    });
    // Internal group: two real persons.
    await insertAttendee({
      meetingId,
      name: "Internal Person 1",
      attendanceType: "internal",
      sortOrder: 1,
      kind: "person",
    });
    await insertAttendee({
      meetingId,
      name: "Internal Person 2",
      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 });

      // The subheading row must still be in the DOM, with its label
      // text intact.
      const subheadings = row.locator(
        '[data-testid^="em-attendee-subheading-"]',
      );
      await expect(subheadings).toHaveCount(1, { timeout: 10_000 });
      await expect(subheadings.first()).toContainText("Virtual Block Label");

      // The two internal persons must still be numbered 1- and 2- (the
      // virtual subheading does NOT get a number, and crucially does
      // NOT push the internal counter to start at 2).
      const indices = row.locator('[data-testid^="em-attendee-index-"]');
      await expect(indices).toHaveCount(2);
      const indexTexts = (await indices.allInnerTexts()).map((s) =>
        s.trim().replace(/\s+/g, ""),
      );
      expect(indexTexts).toEqual(["1-", "2-"]);
    } finally {
      if (originalAdminPreferredLanguage) {
        await setAdminPreferredLanguage(originalAdminPreferredLanguage).catch(
          () => {
            /* best-effort */
          },
        );
      }
    }
  });

  test(`Schedule [${lang}]: meeting with no subheadings still numbers persons 1..N with no stray subheading rows`, async ({
    page,
  }) => {
    const date = uniqueFutureDate(langOffsets[lang] + 5);
    const meetingId = await insertMeeting({
      meetingDate: date,
      dailyNumber: 1,
      titleEn: `No Subheadings ${lang} ${Date.now().toString(36)}`,
      titleAr: `بدون عناوين فرعية ${lang} ${Date.now().toString(36)}`,
      startTime: "09:00:00",
      endTime: "10:00:00",
    });
    await insertAttendee({
      meetingId,
      name: "Solo Person A",
      attendanceType: "internal",
      sortOrder: 0,
      kind: "person",
    });
    await insertAttendee({
      meetingId,
      name: "Solo Person B",
      attendanceType: "internal",
      sortOrder: 1,
      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 });

      // Zero subheadings — make sure the new code path didn't sneak
      // one in for legacy "person"-only meetings.
      const subheadings = row.locator(
        '[data-testid^="em-attendee-subheading-"]',
      );
      await expect(subheadings).toHaveCount(0);

      const indices = row.locator('[data-testid^="em-attendee-index-"]');
      await expect(indices).toHaveCount(2);
      const trimmed = (await indices.allInnerTexts()).map((s) =>
        s.trim().replace(/\s+/g, ""),
      );
      expect(trimmed).toEqual(["1-", "2-"]);
    } finally {
      if (originalAdminPreferredLanguage) {
        await setAdminPreferredLanguage(originalAdminPreferredLanguage).catch(
          () => {
            /* best-effort */
          },
        );
      }
    }
  });
}
