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 expected header strings from the SAME locale bundles the app
// renders from, so the assertions stay in sync if the copy ever
// changes.
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") };
function attendanceLabel(lang, kind) {
  const em = localeBundles[lang]?.executiveMeetings;
  if (!em) throw new Error(`Missing executiveMeetings namespace in ${lang}.json`);
  const key = `${kind}Attendance`; // virtualAttendance | internalAttendance | externalAttendance
  const val = em[key];
  if (!val) throw new Error(`Missing locale key executiveMeetings.${key} in ${lang}.json`);
  return val;
}

// E2E coverage for Task #204: the per-group attendee headers
// ("Virtual Attendance / Internal Attendance / External Attendance")
// rendered above each attendee group in the schedule's attendee cell.
//
// Before #204 the headers were `text-xs` with em-dash decoration and
// were easy to mistake for a stray attendee name. After #204 they
// render bolder, larger, centered, and with a hairline underline so
// they read clearly as section labels. These tests seed a meeting with
// attendees in two groups (virtual + internal) on a far-future date —
// so we never collide with real schedule data — and assert that BOTH
// expected localised headers render with the new stable
// `em-attendee-group-header` testid, in BOTH locales.

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

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

const RANDOM_DATE_BASE_DAYS =
  730 + 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 }) {
  await pool.query(
    `INSERT INTO executive_meeting_attendees
       (meeting_id, name, attendance_type, sort_order)
     VALUES ($1, $2, $3, $4)`,
    [meetingId, name, attendanceType, sortOrder],
  );
}

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(),
  ]);
}

// Admin's preferred_language in the DB drives AuthContext to call
// i18n.changeLanguage(...) right after login/auth restore — that
// would clobber any localStorage-only override we apply from the
// test. We capture the seeded value before mutating it per-test, then
// restore it in afterAll so other specs/sessions see no drift.
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 () => {
  // Restore admin's preferred language so we leave the DB exactly as
  // we found it.
  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}]: attendee group headers render with the localised label for multi-group meetings`, async ({
    page,
  }) => {
    // Seed: one meeting with 2 virtual + 3 internal attendees so the
    // attendee cell renders BOTH a "Virtual Attendance" header and an
    // "Internal Attendance" header. Each lang gets its OWN unique
    // future date so the (meeting_date, daily_number) unique
    // constraint never collides between iterations.
    const date = uniqueFutureDate(langOffsets[lang]);
    const meetingId = await insertMeeting({
      meetingDate: date,
      dailyNumber: 1,
      titleEn: `Group Header ${lang} ${Date.now().toString(36)}`,
      titleAr: `عناوين المجموعة ${lang} ${Date.now().toString(36)}`,
      startTime: "09:00:00",
      endTime: "10:00:00",
    });
    await insertAttendee({
      meetingId,
      name: "Virtual Person A",
      attendanceType: "virtual",
      sortOrder: 0,
    });
    await insertAttendee({
      meetingId,
      name: "Virtual Person B",
      attendanceType: "virtual",
      sortOrder: 1,
    });
    await insertAttendee({
      meetingId,
      name: "Internal Person A",
      attendanceType: "internal",
      sortOrder: 2,
    });
    await insertAttendee({
      meetingId,
      name: "Internal Person B",
      attendanceType: "internal",
      sortOrder: 3,
    });
    await insertAttendee({
      meetingId,
      name: "Internal Person C",
      attendanceType: "internal",
      sortOrder: 4,
    });

    // Align the admin's preferred_language with the locale this test
    // exercises so AuthContext does not flip the UI back to the seed
    // value after login. Wrap in try/finally so we restore the
    // original value as soon as the test finishes — even on assertion
    // failure — minimising the window in which the shared admin row
    // carries a test-set language.
    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 });

      const headers = row.locator('[data-testid="em-attendee-group-header"]');
      await expect(headers).toHaveCount(2, { timeout: 10_000 });

      // We seeded EXACTLY one virtual group and one internal group, so
      // require BOTH expected localised labels to appear (substring
      // match because the virtual header may be suffixed with the
      // platform name, e.g. " — Zoom"). This rejects any regression
      // that drops a header, mislabels a group, or renders the same
      // label twice. Labels are read from the SAME locale bundle the
      // app renders from so copy edits won't silently break the spec.
      const virtualLabel = attendanceLabel(lang, "virtual");
      const internalLabel = attendanceLabel(lang, "internal");
      const externalLabel = attendanceLabel(lang, "external");

      const seenTexts = [];
      for (let idx = 0; idx < 2; idx++) {
        seenTexts.push((await headers.nth(idx).innerText()).trim());
      }
      const hasVirtual = seenTexts.some((t) => t.includes(virtualLabel));
      const hasInternal = seenTexts.some((t) => t.includes(internalLabel));
      const hasExternal = seenTexts.some((t) => t.includes(externalLabel));
      expect(
        hasVirtual,
        `expected a "${virtualLabel}" header; saw ${JSON.stringify(seenTexts)}`,
      ).toBe(true);
      expect(
        hasInternal,
        `expected a "${internalLabel}" header; saw ${JSON.stringify(seenTexts)}`,
      ).toBe(true);
      expect(
        hasExternal,
        `did not seed external attendees; "${externalLabel}" header should be absent. Saw ${JSON.stringify(seenTexts)}`,
      ).toBe(false);
    } finally {
      if (originalAdminPreferredLanguage) {
        await setAdminPreferredLanguage(originalAdminPreferredLanguage).catch(
          () => {
            /* best-effort; afterAll also restores */
          },
        );
      }
    }
  });

  // Regression guard for the single-group fallback: a meeting with
  // attendees in only one group (e.g. all internal) and no virtual
  // platform must render NO `em-attendee-group-header` — that path
  // bypasses AttendeeGroup entirely. Without this guard a regression
  // could start rendering a stray "Internal Attendance" header above
  // every solo-group meeting.
  test(`Schedule [${lang}]: single-group meeting renders no attendee group header`, async ({
    page,
  }) => {
    const date = uniqueFutureDate(langOffsets[lang] + 5);
    const meetingId = await insertMeeting({
      meetingDate: date,
      dailyNumber: 1,
      titleEn: `Solo Group ${lang} ${Date.now().toString(36)}`,
      titleAr: `مجموعة فردية ${lang} ${Date.now().toString(36)}`,
      startTime: "09:00:00",
      endTime: "10:00:00",
    });
    await insertAttendee({
      meetingId,
      name: "Internal Person A",
      attendanceType: "internal",
      sortOrder: 0,
    });
    await insertAttendee({
      meetingId,
      name: "Internal Person B",
      attendanceType: "internal",
      sortOrder: 1,
    });

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

      const headers = row.locator('[data-testid="em-attendee-group-header"]');
      await expect(headers).toHaveCount(0);
    } finally {
      if (originalAdminPreferredLanguage) {
        await setAdminPreferredLanguage(originalAdminPreferredLanguage).catch(
          () => {
            /* best-effort */
          },
        );
      }
    }
  });
}
