// Full site test suite — data-driven, discovers features from the DOM.
// Starts a real OCaml HTTP server subprocess (no Docker).
//
// Usage:
//   npx playwright test tests/playwright/site-full.spec.js
//   npx playwright test tests/playwright/site-full.spec.js -g "hyperscript"
//   npx playwright test tests/playwright/site-full.spec.js -g "home"

const { test, expect } = require('playwright/test');
const { SiteServer } = require('./site-server');
const { waitForSxReady, trackErrors, universalSmoke } = require('./helpers');

let server;

test.beforeAll(async () => {
  server = new SiteServer();
  await server.start();
});

test.afterAll(async () => {
  if (server) server.stop();
});

// ---------------------------------------------------------------------------
// Feature report
// ---------------------------------------------------------------------------

function featureReport(pageName, entries) {
  const features = [];
  const lines = entries.map(e => {
    if (e.ok) features.push(e.feature);
    return `  ${e.ok ? '\u2713' : '\u2717'} ${e.label}`;
  });
  const pass = entries.every(e => e.ok);
  console.log(`\nPage: ${pageName}\n${lines.join('\n')}\n  Features tested: ${features.join(', ')}\n`);
  return { pass, features, failures: entries.filter(e => !e.ok).map(e => e.label) };
}

// ---------------------------------------------------------------------------
// Auto-discovery: probe a page for testable features
// ---------------------------------------------------------------------------

async function discoverPage(page) {
  return page.evaluate(() => {
    const islands = Array.from(document.querySelectorAll('[data-sx-island]')).map(el => ({
      name: el.getAttribute('data-sx-island'),
      hydrated: el.children.length > 0,
    }));
    const hsElements = Array.from(document.querySelectorAll('[_]')).map(el => ({
      tag: el.tagName,
      text: el.textContent.trim().substring(0, 40),
      src: el.getAttribute('_'),
      activated: !!el.__sx_data,
    }));
    const sxGetLinks = Array.from(document.querySelectorAll('a[sx-get]')).map(a => ({
      href: a.getAttribute('href'),
      text: a.textContent.trim().substring(0, 30),
    }));
    const buttons = document.querySelectorAll('button').length;
    const contentLen = (document.querySelector('#sx-content') || document.body).textContent.length;
    return { islands, hsElements, sxGetLinks, buttons, contentLen };
  });
}

// ---------------------------------------------------------------------------
// HS element testing — clicks each element and checks for effect
// ---------------------------------------------------------------------------

async function testHsElement(page, index) {
  return page.evaluate((idx) => {
    const el = document.querySelectorAll('[_]')[idx];
    if (!el) return { ok: false, reason: 'not found' };
    const src = el.getAttribute('_');

    // Snapshot state before click
    const classBefore = el.className;
    const htmlBefore = el.innerHTML;
    const textBefore = el.textContent;

    // Check for target elements referenced in the HS source
    let targetBefore = null;
    const targetMatch = src.match(/#([\w-]+)/);
    if (targetMatch) {
      const tgt = document.getElementById(targetMatch[1]);
      if (tgt) targetBefore = tgt.innerHTML;
    }
    const attrMatch = src.match(/@([\w-]+)/);
    let attrBefore = null;
    if (attrMatch) attrBefore = el.getAttribute(attrMatch[1]);

    el.click();

    // Check what changed (synchronous effects only — async waits tested separately)
    const classAfter = el.className;
    const htmlAfter = el.innerHTML;
    const textAfter = el.textContent;
    let targetAfter = null;
    if (targetMatch) {
      const tgt = document.getElementById(targetMatch[1]);
      if (tgt) targetAfter = tgt.innerHTML;
    }
    let attrAfter = null;
    if (attrMatch) attrAfter = el.getAttribute(attrMatch[1]);

    const classChanged = classBefore !== classAfter;
    const htmlChanged = htmlBefore !== htmlAfter;
    const textChanged = textBefore !== textAfter;
    const targetChanged = targetBefore !== null && targetBefore !== targetAfter;
    const attrChanged = attrBefore !== null && attrBefore !== attrAfter;
    const anyChange = classChanged || htmlChanged || textChanged || targetChanged || attrChanged;

    return {
      ok: anyChange,
      src: src.substring(0, 60),
      changes: {
        class: classChanged ? classAfter.substring(0, 60) : null,
        html: htmlChanged,
        text: textChanged,
        target: targetChanged,
        attr: attrChanged ? attrAfter : null,
      },
    };
  }, index);
}

// HS elements with async effects (wait) — need Promise-based check
async function testHsWaitElement(page, index) {
  return page.evaluate((idx) => {
    return new Promise(resolve => {
      const el = document.querySelectorAll('[_]')[idx];
      if (!el) return resolve({ ok: false, reason: 'not found' });
      const classBefore = el.className;
      el.click();
      const classAfterClick = el.className;
      const addedClass = classAfterClick !== classBefore;

      // Wait for async effect to complete (up to 5s)
      let checks = 0;
      const poll = setInterval(() => {
        checks++;
        if (el.className !== classAfterClick || checks > 25) {
          clearInterval(poll);
          resolve({
            ok: addedClass && el.className !== classAfterClick,
            src: el.getAttribute('_').substring(0, 60),
            classFlow: [classBefore.substring(0, 40), classAfterClick.substring(0, 40), el.className.substring(0, 40)],
          });
        }
      }, 200);
    });
  }, index);
}

// ---------------------------------------------------------------------------
// HOME
// ---------------------------------------------------------------------------

test('home', async ({ page }) => {
  const errors = trackErrors(page);
  const entries = [];

  // Set cookie to step 7, then load page — SSR should render at 7
  await page.context().addCookies([{
    name: 'sx-home-stepper', value: '7',
    url: server.baseUrl,
  }]);

  // Screenshot-based flash detection.
  // 1. Load with JS disabled → pure SSR render → screenshot
  // 2. Load with JS enabled → hydration runs → screenshot
  // 3. Compare pixels. Any difference = visible flash.

  // Step 1: SSR screenshot (no JS)
  await page.route('**/*.js', route => {
    if (route.request().url().includes('sx_browser') || route.request().url().includes('sx-platform')) {
      route.abort();
    } else {
      route.continue();
    }
  });
  await page.goto(server.baseUrl + '/sx/', { waitUntil: 'networkidle', timeout: 30000 });
  const ssrScreenshot = await page.screenshot({ clip: { x: 0, y: 0, width: 800, height: 600 } });
  const ssrIndex = await page.evaluate(() => {
    const m = document.body.textContent.match(/(\d+)\s*\/\s*16/);
    return m ? m[1] : null;
  });

  // Step 2: Hydrated screenshot (with JS)
  await page.unrouteAll();
  await page.goto(server.baseUrl + '/sx/', { waitUntil: 'domcontentloaded', timeout: 30000 });
  await waitForSxReady(page);
  // Wait a tick for any deferred effects
  await page.waitForTimeout(500);
  const hydratedScreenshot = await page.screenshot({ clip: { x: 0, y: 0, width: 800, height: 600 } });
  const hydratedIndex = await page.evaluate(() => {
    const m = document.body.textContent.match(/(\d+)\s*\/\s*16/);
    return m ? m[1] : null;
  });

  // Step 3: Compare
  entries.push({ ok: ssrIndex === hydratedIndex, label: `No flash: SSR=${ssrIndex} hydrated=${hydratedIndex} (cookie=7)`, feature: 'no-flash' });
  const pixelMatch = Buffer.from(ssrScreenshot).equals(Buffer.from(hydratedScreenshot));
  entries.push({ ok: pixelMatch, label: `No visual flash: ${pixelMatch ? 'screenshots identical' : 'screenshots differ'}`, feature: 'no-visual-flash' });

  const info = await discoverPage(page);
  entries.push({ ok: true, label: 'Boot: data-sx-ready', feature: 'boot' });

  // Islands
  for (const island of info.islands) {
    entries.push({ ok: island.hydrated, label: `Island: ${island.name} ${island.hydrated ? 'hydrated' : 'NOT hydrated'}`, feature: island.name });
  }

  // Stepper
  const stepperWorks = await page.evaluate(() => {
    const btns = document.querySelectorAll('button');
    if (btns.length < 2) return false;
    const before = document.body.textContent;
    btns[btns.length - 1].click(); // ▶ button
    const after = document.body.textContent;
    return before !== after;
  });
  entries.push({ ok: stepperWorks, label: 'Stepper: click changes content', feature: 'stepper' });

  // Smoke + errors
  const smoke = await universalSmoke(page);
  entries.push({ ok: smoke.pass, label: `Smoke: ${smoke.pass ? 'all pass' : smoke.failures.join(', ')}`, feature: 'smoke' });
  const errs = errors.errors().filter(e => !e.includes('[jit] FAIL'));
  entries.push({ ok: errs.length === 0, label: `Console: ${errs.length} errors${errs.length > 0 ? ' — ' + errs[0].substring(0, 100) : ''}`, feature: 'no-errors' });

  const r = featureReport('/sx/', entries);
  expect(r.pass, r.failures.join('\n')).toBe(true);
});

// ---------------------------------------------------------------------------
// HYPERSCRIPT — test every _="..." element
// ---------------------------------------------------------------------------

test('hyperscript', async ({ page }) => {
  const errors = trackErrors(page);
  const entries = [];

  await page.goto(server.baseUrl + '/sx/(applications.(hyperscript))', { waitUntil: 'domcontentloaded', timeout: 30000 });
  await waitForSxReady(page);
  entries.push({ ok: true, label: 'Boot: data-sx-ready', feature: 'boot' });

  const info = await discoverPage(page);
  entries.push({ ok: info.hsElements.length > 0, label: `HS elements: ${info.hsElements.length} found`, feature: 'hs-discovery' });

  const activated = info.hsElements.filter(e => e.activated).length;
  entries.push({ ok: activated === info.hsElements.length, label: `HS activation: ${activated}/${info.hsElements.length}`, feature: 'hs-activation' });

  // Test each HS element
  for (let i = 0; i < info.hsElements.length; i++) {
    const hs = info.hsElements[i];
    const hasWait = hs.src.includes('wait');

    if (hasWait) {
      const result = await testHsWaitElement(page, i);
      entries.push({
        ok: result.ok,
        label: `HS[${i}] ${hs.src.substring(0, 50)}: ${result.ok ? 'async cycle OK' : 'no async effect'}`,
        feature: `hs-${i}-wait`,
      });
    } else {
      const result = await testHsElement(page, i);
      entries.push({
        ok: result.ok,
        label: `HS[${i}] ${hs.src.substring(0, 50)}: ${result.ok ? 'changed' : 'no effect'}`,
        feature: `hs-${i}`,
      });
    }
  }

  // Smoke + errors
  const smoke = await universalSmoke(page);
  entries.push({ ok: smoke.pass, label: `Smoke: ${smoke.pass ? 'all pass' : smoke.failures.join(', ')}`, feature: 'smoke' });
  const errs = errors.errors().filter(e => !e.includes('[jit] FAIL'));
  entries.push({ ok: errs.length === 0, label: `Console: ${errs.length} errors`, feature: 'no-errors' });

  const r = featureReport('/sx/(applications.(hyperscript))', entries);
  expect(r.pass, r.failures.join('\n')).toBe(true);
});

// ---------------------------------------------------------------------------
// SPA NAVIGATION — click sx-get links, verify content swaps
// ---------------------------------------------------------------------------

test('spa-navigation', async ({ page }) => {
  const entries = [];

  await page.goto(server.baseUrl + '/sx/', { waitUntil: 'domcontentloaded', timeout: 30000 });
  await waitForSxReady(page);

  const sections = [
    { label: 'Geography', href: '/sx/(geography)' },
    { label: 'Language', href: '/sx/(language)' },
    { label: 'Applications', href: '/sx/(applications)' },
    { label: 'Tools', href: '/sx/(tools)' },
    { label: 'Etc', href: '/sx/(etc)' },
  ];

  for (const { label, href } of sections) {
    // Click the sx-get link using Playwright's click (fires real DOM event)
    const link = page.locator(`a[href="${href}"]`).first();
    const linkVisible = await link.isVisible().catch(() => false);

    if (!linkVisible) {
      entries.push({ ok: false, label: `SPA → ${label}: link not visible`, feature: `spa-${label.toLowerCase()}` });
      continue;
    }

    await link.click();

    // Wait for content swap — URL should change or content should update
    try {
      await page.waitForFunction(
        (h) => {
          const content = (document.querySelector('#sx-content') || document.body).textContent;
          return content.length > 100;
        },
        href,
        { timeout: 5000 }
      );
      const url = page.url();
      const hasContent = await page.evaluate(() =>
        (document.querySelector('#sx-content') || document.body).textContent.length > 100
      );
      entries.push({ ok: hasContent, label: `SPA → ${label}: content loaded (${url.split('/').pop()})`, feature: `spa-${label.toLowerCase()}` });
    } catch (e) {
      entries.push({ ok: false, label: `SPA → ${label}: timeout`, feature: `spa-${label.toLowerCase()}` });
    }

    // Navigate back to home for next test
    await page.goto(server.baseUrl + '/sx/', { waitUntil: 'domcontentloaded', timeout: 30000 });
    await waitForSxReady(page);
  }

  const r = featureReport('SPA navigation', entries);
  expect(r.pass, r.failures.join('\n')).toBe(true);
});

// ---------------------------------------------------------------------------
// SECTION PAGES — every page in each section boots and renders
// ---------------------------------------------------------------------------

const SECTIONS = [
  { name: 'geography', entry: '/sx/(geography)', linkPrefix: '/sx/(geography' },
  { name: 'language', entry: '/sx/(language)', linkPrefix: '/sx/(language' },
  { name: 'applications', entry: '/sx/(applications)', linkPrefix: '/sx/(applications' },
  { name: 'tools', entry: '/sx/(tools)', linkPrefix: '/sx/(tools' },
  { name: 'etc', entry: '/sx/(etc)', linkPrefix: '/sx/(etc' },
];

for (const section of SECTIONS) {
  test(`pages: ${section.name}`, async ({ page }) => {
    const entries = [];

    // Discover all pages in this section
    await page.goto(server.baseUrl + section.entry, { waitUntil: 'domcontentloaded', timeout: 30000 });
    try { await waitForSxReady(page); } catch (e) {}

    const links = await page.evaluate((prefix) => {
      return Array.from(document.querySelectorAll('a[href]'))
        .map(a => a.getAttribute('href'))
        .filter(h => h && h.startsWith(prefix))
        .filter((v, i, a) => a.indexOf(v) === i);
    }, section.linkPrefix);

    const allUrls = [section.entry, ...links.filter(l => l !== section.entry)];

    for (const url of allUrls) {
      await page.goto(server.baseUrl + url, { waitUntil: 'domcontentloaded', timeout: 30000 });
      try { await waitForSxReady(page, 15000); } catch (e) {}

      const info = await discoverPage(page);
      const smoke = await universalSmoke(page);
      const pageName = url.replace('/sx/', '');

      entries.push({
        ok: smoke.pass && info.contentLen > 50,
        label: `${pageName}: ${info.contentLen} chars, ${info.islands.length} islands, ${info.hsElements.length} hs${smoke.pass ? '' : ' [' + smoke.failures.join(', ') + ']'}`,
        feature: pageName,
      });
    }

    const passed = entries.filter(e => e.ok).length;
    console.log(`\nSection: ${section.name} — ${passed}/${entries.length} pages`);
    entries.forEach(e => console.log(`  ${e.ok ? '\u2713' : '\u2717'} ${e.label}`));

    const failures = entries.filter(e => !e.ok);
    expect(failures.length, failures.map(f => f.label).join('\n')).toBe(0);
  });
}
