// Site Smoke Tests — visit every page in the nav tree, run universal checks.
// No HTTP server. OCaml subprocess renders pages via epoch protocol.
// Playwright intercepts all navigation and serves rendered HTML + static assets.

const { test, expect } = require('playwright/test');
const path = require('path');
const fs = require('fs');
const { SxRenderer } = require('./sx-renderer');
const { universalSmoke, trackErrors } = require('./helpers');

const PROJECT_ROOT = path.resolve(__dirname, '../..');
const STATIC_DIR = path.join(PROJECT_ROOT, 'shared/static');
const WASM_DIR = path.join(STATIC_DIR, 'wasm');
const FAKE_ORIGIN = 'http://sx-sandbox';

// Mime types for static file serving
const MIME = {
  '.js': 'application/javascript',
  '.css': 'text/css',
  '.wasm': 'application/wasm',
  '.sx': 'text/plain',
  '.sxbc': 'application/octet-stream',
  '.json': 'application/json',
  '.svg': 'image/svg+xml',
  '.png': 'image/png',
  '.ico': 'image/x-icon',
};

/** Resolve a static file request to a local path. */
function resolveStatic(urlPath) {
  // /static/wasm/... → shared/static/wasm/...
  // /static/scripts/... → shared/static/scripts/...
  // /static/... → shared/static/...
  if (urlPath.startsWith('/static/')) {
    const rel = urlPath.replace(/\?.*$/, ''); // strip query params (cache busters)
    return path.join(PROJECT_ROOT, 'shared', rel);
  }
  return null;
}

/** Resolve .sx/.sxbc file requests (platform lazy-loads these). */
function resolveSxFile(urlPath) {
  const clean = urlPath.replace(/\?.*$/, '');
  // /sx/sx/... → PROJECT_ROOT/sx/sx/...
  // /wasm/sx/... → shared/static/wasm/sx/...
  if (clean.startsWith('/wasm/sx/') || clean.startsWith('/static/wasm/sx/')) {
    const rel = clean.replace(/^\/(?:static\/)?wasm\/sx\//, '');
    return path.join(WASM_DIR, 'sx', rel);
  }
  return null;
}

// ---- Shared renderer + page specs ----
let renderer;
let pageSpecs; // Map<url, {hasText?: string[], hasIsland?: string[]}>

test.beforeAll(async () => {
  renderer = new SxRenderer(PROJECT_ROOT);
  await renderer.ready();
  pageSpecs = await renderer.pageTestSpecs();
});

test.afterAll(async () => {
  if (renderer) renderer.close();
});

// ---- Test sections ----
// Group pages by top-level section for readable output.
// Each section is one test with steps for each page.

const SECTIONS = [
  { name: 'Geography',    prefix: '/sx/(geography' },
  { name: 'Language',     prefix: '/sx/(language' },
  { name: 'Applications', prefix: '/sx/(applications' },
  { name: 'Tools',        prefix: '/sx/(tools' },
  { name: 'Etc',          prefix: '/sx/(etc' },
  { name: 'Home',         prefix: '/sx/', exact: true },
];

function categorize(href) {
  for (const s of SECTIONS) {
    if (s.exact ? href === s.prefix : href.startsWith(s.prefix)) return s.name;
  }
  return 'Other';
}

for (const section of SECTIONS) {
  test(`smoke: ${section.name}`, async ({ page }) => {
    // Get all URLs for this section
    const allUrls = await renderer.navUrls();
    const urls = allUrls.filter(([href]) => categorize(href) === section.name);

    // Set up route interception — all requests go through us
    await page.route('**/*', async (route) => {
      const url = new URL(route.request().url());

      // Static assets from filesystem
      const staticPath = resolveStatic(url.pathname);
      if (staticPath && fs.existsSync(staticPath)) {
        const ext = path.extname(staticPath);
        await route.fulfill({
          path: staticPath,
          contentType: MIME[ext] || 'application/octet-stream',
        });
        return;
      }

      // .sx/.sxbc files
      const sxPath = resolveSxFile(url.pathname);
      if (sxPath && fs.existsSync(sxPath)) {
        const ext = path.extname(sxPath);
        await route.fulfill({
          path: sxPath,
          contentType: MIME[ext] || 'text/plain',
        });
        return;
      }

      // Page render via OCaml subprocess
      try {
        const html = await renderer.render(url.pathname);
        await route.fulfill({
          status: 200,
          contentType: 'text/html; charset=utf-8',
          body: html,
        });
      } catch (e) {
        await route.fulfill({
          status: 500,
          contentType: 'text/plain',
          body: `render error: ${e.message}`,
        });
      }
    });

    // Visit each page in this section
    for (const [href, label] of urls) {
      await test.step(`${label} — ${href}`, async () => {
        const errors = trackErrors(page);
        await page.goto(`${FAKE_ORIGIN}${href}`, {
          waitUntil: 'load',
          timeout: 30000,
        });

        // Wait for full hydration — WASM boot + island mounting
        try {
          await page.waitForSelector('html[data-sx-ready]', { timeout: 20000 });
        } catch (_) {
          // Hydration timeout is a hard failure
        }

        const result = await universalSmoke(page);

        // Check hydration completed
        const sxReady = await page.evaluate(() =>
          document.documentElement.getAttribute('data-sx-ready'));
        if (!sxReady) {
          result.failures.push('hydration failed: data-sx-ready not set');
          result.pass = false;
        }

        const consoleErrors = errors.errors();
        if (consoleErrors.length > 0) {
          result.failures.push(`console errors: ${consoleErrors.join('; ')}`);
          result.pass = false;
        }

        // Per-page assertions from page-tests.sx
        const spec = pageSpecs.get(href);
        if (spec) {
          if (spec.hasText) {
            const bodyText = await page.evaluate(() => document.body.textContent);
            for (const text of spec.hasText) {
              if (!bodyText.includes(text)) {
                result.failures.push(`missing text: "${text}"`);
                result.pass = false;
              }
            }
          }
          if (spec.hasIsland) {
            for (const island of spec.hasIsland) {
              const count = await page.locator(`[data-sx-island="${island}"]`).count();
              if (count === 0) {
                result.failures.push(`missing island: ${island}`);
                result.pass = false;
              }
            }
          }
        }

        expect.soft(result.failures, `${label}: ${result.failures.join(', ')}`).toEqual([]);
      });
    }
  });
}
