/**
 * Journey: MetricTimeline — algorithm_update annotation badges (PROD-FIND-ANNOT-001)
 *
 * Verifies that Google algorithm update entries from config/algorithm_updates.php are
 * rendered on the Page Timeline with the correct type badge and (optional) external link:
 *
 *  A. Golden path — "March 2026 Core Update" appears in the Events list with
 *     a "Google Update" type badge when days=180 (window: 2025-12-21 → 2026-06-18).
 *     No DB seeding required — algorithm_update annotations come from static config,
 *     not from the DB. The series is filled with zero-rows for dates without GSC data
 *     (buildSeries always emits `days` entries), so series.length > 0 and the Events
 *     section renders normally.
 *
 *  B. Link behaviour — algorithm_update with null link renders no "Google announcement"
 *     external anchor. (March 2026 Core Update has url: null in config.)
 *
 *  C. Type badge integrity — the badge shows the human-readable label produced by
 *     getAnnotationTypeLabel() ('Google Update' for algorithm_update).
 *
 * == serial mode ==
 *
 * `beforeAll` reads .seed-data.json and resolves the site public_id via artisan
 * WITHOUT the `page`/`context` fixtures (which are per-test-scoped and can't be used
 * in beforeAll). This avoids the "context and page fixtures are not supported in
 * beforeAll" error seen in timeline-page.spec.ts.
 */

import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import { expect, test } from '../fixtures/auth';

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

// Serialize all tests — prevents worker restarts between describe blocks.
test.describe.configure({ mode: 'serial' });

// Background noise that fires regardless of the feature under test.
const KNOWN_NOISE = ['/api/lead-score/pql-signal', '/profile', 'ERR_TOO_MANY_REDIRECTS'];

// ---------------------------------------------------------------------------
// Helpers — no page fixture required (artisan + fs only)
// ---------------------------------------------------------------------------

/** Read seed data directly from .seed-data.json (written by global-setup.ts). */
function loadSeedDataFromFile(): { siteId: number } {
  const seedDataPath = path.join(__dirname, '..', '.seed-data.json');
  if (!fs.existsSync(seedDataPath)) {
    throw new Error(
      `Seed data not found at ${seedDataPath}. Did global setup run?`,
    );
  }
  return JSON.parse(fs.readFileSync(seedDataPath, 'utf8')) as { siteId: number };
}

/** Resolve site ULID public_id via artisan tinker (no page fixture in scope). */
function resolveSitePublicIdViaArtisan(siteId: number): string {
  const tmpFile = path.join('/tmp', `e2e_algoupd_${process.pid}_${Date.now()}.php`);
  try {
    fs.writeFileSync(
      tmpFile,
      `$s = \\App\\Models\\Site::find(${siteId}); echo $s ? $s->public_id : 'NOT_FOUND';`,
      'utf8',
    );
    const raw = execSync(`php artisan tinker --execute="$(cat ${tmpFile})" 2>&1`, {
      cwd: process.cwd(),
      encoding: 'utf8',
      timeout: 30_000,
      shell: '/bin/bash',
    });
    const result = raw
      .trim()
      .split('\n')
      .map((l) => l.trim())
      .filter(Boolean)
      .pop() ?? '';
    if (!result || result === 'NOT_FOUND') {
      throw new Error(`resolveSitePublicIdViaArtisan(${siteId}) failed. Raw:\n${raw}`);
    }
    return result;
  } finally {
    fs.unlinkSync(tmpFile);
  }
}

// The e2e test page URL (same as used in timeline-page.spec.ts).
const PAGE_URL = 'https://e2e-test.example.com/blog/seo-tips';
const PAGE_URL_ENCODED = encodeURIComponent(PAGE_URL);

// days=180 puts the window at 2025-12-21 → 2026-06-18, which includes
// "March 2026 Core Update" (started_at: 2026-03-10, url: null).
const DAYS = 180;

let sitePublicId = '';

const timelineUrl = () =>
  `/sites/${sitePublicId}/timeline/page?url=${PAGE_URL_ENCODED}&days=${DAYS}`;

// ---------------------------------------------------------------------------
// Suite A+B+C
// ---------------------------------------------------------------------------

test.describe(
  'Journey: MetricTimeline — algorithm_update annotation badges (PROD-FIND-ANNOT-001)',
  () => {
    test.beforeAll(async () => {
      // Read seed data WITHOUT the `page` fixture (avoids "page not supported in beforeAll").
      const seedData = loadSeedDataFromFile();
      sitePublicId = resolveSitePublicIdViaArtisan(seedData.siteId);
    });

    // -------------------------------------------------------------------------
    // A. Golden path
    // -------------------------------------------------------------------------

    test('A: loads without uncaught console errors (days=180)', async ({ page }) => {
      const errors: string[] = [];
      page.on('pageerror', (err) => errors.push(`pageerror: ${err.message}`));
      page.on('console', (msg) => {
        if (msg.type() === 'error') errors.push(`console.error: ${msg.text()}`);
      });

      await page.goto(timelineUrl(), { waitUntil: 'networkidle' });
      await expect(page).toHaveURL(new RegExp(`/sites/${sitePublicId}/timeline/page`));

      const appErrors = errors.filter(
        (e) =>
          !e.includes('extension') &&
          !e.includes('chrome-extension') &&
          !KNOWN_NOISE.some((n) => e.includes(n)),
      );
      expect(appErrors, `Uncaught errors:\n${appErrors.join('\n')}`).toHaveLength(0);
    });

    test('A: no failed XHR requests (4xx/5xx) on page load', async ({ page }) => {
      const failedRequests: string[] = [];
      page.on('response', (r) => {
        if (r.status() >= 400 && !KNOWN_NOISE.some((n) => r.url().includes(n))) {
          failedRequests.push(`${r.status()} ${r.url()}`);
        }
      });

      await page.goto(timelineUrl(), { waitUntil: 'networkidle' });
      expect(
        failedRequests,
        `Failed requests:\n${failedRequests.join('\n')}`,
      ).toHaveLength(0);
    });

    test('A: "Page Timeline" heading is visible', async ({ page }) => {
      await page.goto(timelineUrl(), { waitUntil: 'networkidle' });
      await expect(page.getByRole('heading', { name: /page timeline/i })).toBeVisible();
    });

    test('A: Events section is visible (algorithm_update from config populates annotations)', async ({
      page,
    }) => {
      await page.goto(timelineUrl(), { waitUntil: 'networkidle' });
      // annotations.length > 0 because the March 2026 Core Update falls in the window
      await expect(page.getByText('Events')).toBeVisible();
    });

    test('A: March 2026 Core Update annotation title is rendered in Events list', async ({
      page,
    }) => {
      await page.goto(timelineUrl(), { waitUntil: 'networkidle' });
      await expect(page.getByText('March 2026 Core Update')).toBeVisible();
    });

    // -------------------------------------------------------------------------
    // B. Link behaviour
    // -------------------------------------------------------------------------

    test('B: no "Google announcement" link when annotation.link is null', async ({ page }) => {
      // The March 2026 Core Update in config has url: null — the conditional link
      // must NOT render.
      await page.goto(timelineUrl(), { waitUntil: 'networkidle' });
      await expect(
        page.getByRole('link', { name: /google announcement/i }),
      ).not.toBeVisible();
    });

    // -------------------------------------------------------------------------
    // C. Type badge integrity
    // -------------------------------------------------------------------------

    test('C: algorithm_update annotation renders "Google Update" type badge', async ({
      page,
    }) => {
      await page.goto(timelineUrl(), { waitUntil: 'networkidle' });
      // Badge aria-label is "Event type: Google Update" (from getAnnotationTypeLabel)
      const badge = page.getByLabel('Event type: Google Update');
      await expect(badge).toBeVisible();
      await expect(badge).toHaveText('Google Update');
    });
  },
);
