/**
 * global-setup.ts — Playwright globalSetup: /health readiness gate + deterministic DB seed
 *
 * Runs ONCE before any spec file. Plain Node.js only (fetch + mysql2/promise).
 * No @playwright/test imports — globalSetup runs outside the worker context (Pitfall 2).
 *
 * Step 1 (D-08 readiness gate):
 *   Poll baseURL/health until 200 OK (60s timeout, fail fast on expiry).
 *
 * Step 2 (D-05/D-06/D-07 deterministic seed):
 *   TRUNCATE → INSERT seed rows onto calendar_id=10 + lists for user_id=1.
 *   Idempotent: truncate-first guarantees the same row counts on every run.
 *
 * Anchor values the specs assert on — do NOT change without updating the specs:
 *   - calendar_events.title  = 'Seeded Test Event'
 *   - lists.name             = 'E2E Grocery List'
 *   - list_items.text        = 'Milk', 'Eggs'
 *
 * Run:
 *   pnpm --filter @familysync/pwa test:e2e
 */

import mysql from 'mysql2/promise';

export default async function globalSetup(): Promise<void> {
  // ── Step 0: Fail-closed environment guard (CR-01 — data-loss prevention) ─────
  // This setup TRUNCATEs four tables against whatever DB_* points at. Mirror the
  // hard guard in apps/api/src/auth/devBypass.ts so an operator with prod DB_*
  // still exported can never wipe production data.
  //   1. NODE_ENV === 'production' is the hard FIRST guard (checked before any
  //      other env var), matching devBypass.ts.
  //   2. The harness contract requires DEV_AUTH_BYPASS=true (the same flag the API
  //      needs to serve Dev User 1) — refuse to seed without it.
  if (process.env.NODE_ENV === 'production') {
    throw new Error(
      'global-setup refused: NODE_ENV=production. The E2E seed TRUNCATEs tables and must never run against production.',
    );
  }
  if (process.env.DEV_AUTH_BYPASS !== 'true') {
    throw new Error(
      'global-setup refused: DEV_AUTH_BYPASS is not "true". The harness only runs against a dev-bypass stack; ' +
        'refusing to TRUNCATE/seed an unconfirmed database. Export DEV_AUTH_BYPASS=true (and point DB_* at the dev DB) to proceed.',
    );
  }

  // ── Step 1: Readiness gate (D-08) ───────────────────────────────────────────
  const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:5173';
  const deadline = Date.now() + 60_000;

  // Use an explicit success flag (WR-02): inferring success from `Date.now() >= deadline`
  // after the loop can misreport a success that arrived in the final second as a timeout,
  // because the `await fetch` itself can push the clock past the deadline before the
  // post-loop check runs.
  let ready = false;
  while (Date.now() < deadline) {
    try {
      const res = await fetch(`${baseURL}/health`);
      if (res.ok) {
        ready = true;
        break;
      }
    } catch {
      // ECONNREFUSED or network error — stack not ready yet, keep polling
    }
    await new Promise<void>((r) => setTimeout(r, 1_000));
  }

  if (!ready) {
    throw new Error(
      `health check never returned 200 at ${baseURL}/health — is the dev stack up?\n` +
        `Ensure the API is running with DEV_AUTH_BYPASS=true and the Vite dev server is on ${baseURL}.`,
    );
  }

  // ── Step 1b: DEV_AUTH_BYPASS reachability gate (WR-01) ──────────────────────
  // /health is unauthenticated and returns 200 even if the API was started WITHOUT
  // DEV_AUTH_BYPASS=true. In that case every spec would fail at the first /api/me or
  // /api/events with a 302 redirect to Authelia. Probe /api/me here so a mis-started
  // API fails loudly IN SETUP with a clear message instead of ~40 confusing spec failures.
  // redirect:'manual' surfaces the Authelia redirect as an opaque/3xx response instead of
  // silently following it.
  const meRes = await fetch(`${baseURL}/api/me`, { redirect: 'manual' });
  if (!meRes.ok) {
    throw new Error(
      `/api/me did not return 200 (got ${meRes.status} ${meRes.type}) at ${baseURL}/api/me — ` +
        `the API is reachable but DEV_AUTH_BYPASS is almost certainly NOT set in the API process.\n` +
        `A 3xx/opaqueredirect here means /api/me is redirecting to Authelia. ` +
        `Restart the API with DEV_AUTH_BYPASS=true so it serves Dev User 1 without OIDC.`,
    );
  }

  // ── Step 2: Reset-and-seed (D-06 deterministic, D-07 in globalSetup) ────────
  // Uses exact env-var names from apps/api/src/db/client.ts.
  // DB_HOST defaults to '127.0.0.1' (NOT 'localhost') — per project memory api-integration-test-db.
  const conn = await mysql.createConnection({
    host: process.env.DB_HOST ?? '127.0.0.1',
    port: Number(process.env.DB_PORT ?? 3306),
    user: process.env.DB_USER ?? 'familysync',
    password: process.env.DB_PASSWORD ?? '',
    database: process.env.DB_NAME ?? 'familysync',
  });

  try {
    // Disable FK checks so TRUNCATE order is unconstrained
    await conn.execute('SET FOREIGN_KEY_CHECKS=0');
    await conn.execute('TRUNCATE TABLE list_items');
    await conn.execute('TRUNCATE TABLE list_shares');
    await conn.execute('TRUNCATE TABLE lists');
    await conn.execute('TRUNCATE TABLE calendar_events');
    await conn.execute('SET FOREIGN_KEY_CHECKS=1');

    // CI guard (Pitfall 4): ensure calendar row id=10 exists before inserting events.
    // INSERT IGNORE is a no-op if the row already exists (dev DB), creates it if not (CI fresh DB).
    await conn.execute(
      `INSERT IGNORE INTO calendars (id, user_id, url, display_name, color, is_shared)
       VALUES (10, 1, 'https://caldav.fastmail.com/dav/calendars/e2e/', 'FamilySync', '#4A90D9', true)`,
    );

    // Seed one timed (not all-day) calendar event on shared calendar id=10.
    // Anchor to NOON TODAY (UTC) — deliberately NOT "tomorrow" (deep-review BL-02):
    // both device profiles are phone-width and render the month-agenda view of the
    // CURRENT month. On the last day of a month "tomorrow" rolls into the next month
    // and disappears from the rendered grid, making any "seeded event is visible"
    // assertion date-fragile. Noon-today lands on today's local calendar date in every
    // project timezone and is always inside the current-month view.
    const uid = 'e2e-seed-event-001';
    const _now = new Date();
    const futureStart = new Date(
      Date.UTC(_now.getUTCFullYear(), _now.getUTCMonth(), _now.getUTCDate(), 12, 0, 0),
    );
    // MariaDB TIMESTAMP requires 'YYYY-MM-DD HH:MM:SS' format, not ISO 8601 with 'T'.
    const futureStartUtc = futureStart
      .toISOString()
      .replace('T', ' ')
      .replace(/\.\d+Z$/, '');
    const rawVevent = [
      'BEGIN:VCALENDAR',
      'VERSION:2.0',
      'PRODID:-//FamilySync E2E//EN',
      'BEGIN:VEVENT',
      `UID:${uid}`,
      `DTSTART:${futureStart
        .toISOString()
        .replace(/[-:]/g, '')
        .replace(/\.\d+Z$/, 'Z')}`,
      `DTEND:${new Date(futureStart.getTime() + 60 * 60 * 1000)
        .toISOString()
        .replace(/[-:]/g, '')
        .replace(/\.\d+Z$/, 'Z')}`,
      'SUMMARY:Seeded Test Event',
      'END:VEVENT',
      'END:VCALENDAR',
    ].join('\r\n');

    await conn.execute(
      `INSERT INTO calendar_events
         (calendar_id, uid, etag, raw_vevent, title, dtstart_utc, all_day, has_rrule)
       VALUES (10, ?, 'e2e-etag-001', ?, 'Seeded Test Event', ?, false, false)`,
      [uid, rawVevent, futureStartUtc],
    );

    // Seed one shared list owned by user 1 (D-05 populated half).
    // belt-and-suspenders: seed BOTH owner_id=1 AND a list_shares row for user_id=1
    // so /api/lists returns the list regardless of whether it filters by owner or by share.
    const [listResult] = (await conn.execute(
      `INSERT INTO lists (owner_id, name, is_shared) VALUES (1, 'E2E Grocery List', true)`,
    )) as mysql.ResultSetHeader[];
    const listId = listResult.insertId;

    await conn.execute(`INSERT INTO list_shares (list_id, user_id) VALUES (?, 1)`, [listId]);

    // Two active items with fractional-indexing rank strings — appear in the active section.
    // 'Milk' and 'Eggs' are the stable anchor texts that lists.spec.ts asserts on.
    await conn.execute(
      `INSERT INTO list_items (list_id, text, checked, rank) VALUES (?, 'Milk', false, 'a0'), (?, 'Eggs', false, 'a1')`,
      [listId, listId],
    );
  } finally {
    await conn.end();
  }
}
