/*
 * The 393px check from AGENTS.md §5, run over the Motion routes.
 *
 *   node scripts/motion-overflow-check.mjs        # PIG_WEB_URL=http://127.0.0.1:8975
 *
 * `scripts/screenshots.mjs` already asserts zero horizontal overflow, but only
 * on the ten pages it photographs, and Motion added five it does not know
 * about. The rule it enforces has been fixed at individual call sites three
 * separate times, so a new page group that nobody measures is exactly how it
 * comes back.
 *
 * Checked per route, in light and dark, at 393 and 1440:
 *  - scrollWidth === clientWidth on the document (the sideways-scroll failure)
 *  - no element wider than the viewport (which one, if so — the message has to
 *    name the offender or nobody can act on it)
 *  - nothing thrown to the console, and no failed request
 */
import { chromium } from 'playwright';

const BASE = process.env.PIG_WEB_URL ?? 'http://127.0.0.1:8975';

const ROUTES = [
  { slug: 'motion', path: '/motion' },
  { slug: 'library', path: '/motion/library' },
  { slug: 'engagements', path: '/motion/engagements' },
];

const VIEWPORTS = [
  { name: 'mobile', width: 393, height: 852 },
  { name: 'desktop', width: 1440, height: 900 },
];

const browser = await chromium.launch({ channel: 'chrome' });
let failures = 0;

/** The detail pages need a real id, so they are discovered rather than typed. */
const api = await fetch(`${BASE}/api/motion/templates`).then((r) => r.json());
const template = api.templates?.[0];
if (template) ROUTES.push({ slug: 'template', path: `/motion/library/${template.id}` });
const engagements = await fetch(`${BASE}/api/motion/engagements`).then((r) => r.json());
const engagement = engagements.engagements?.[0];
if (engagement) ROUTES.push({ slug: 'engagement', path: `/motion/engagements/${engagement.id}` });

for (const theme of ['light', 'dark']) {
  for (const vp of VIEWPORTS) {
    const ctx = await browser.newContext({
      viewport: { width: vp.width, height: vp.height },
      colorScheme: theme,
      isMobile: vp.name === 'mobile',
      hasTouch: vp.name === 'mobile',
    });
    /*
     * The sidebar and the dock are per-device states in localStorage; left to
     * whatever a human last set, the measurement is not reproducible. The key
     * names are `pig.sidebarOpen` and `pig.piggyDockOpen` — an earlier version
     * of this file invented `pig.sidebar` and `pig.piggy.dock`, which set two
     * keys nothing reads and left the real state untouched, so the run looked
     * pinned and was not.
     */
    await ctx.addInitScript(`
      localStorage.setItem('pig.sidebarOpen', 'true');
      localStorage.setItem('pig.piggyDockOpen', 'false');
      localStorage.setItem('pig.themeMode', '${theme}');
    `);
    /*
     * colorScheme alone only sets prefers-color-scheme, and the appearance
     * preference is stored SERVER-side and adopted after hydration — so the
     * dark pass rendered light a beat after first paint and measured the wrong
     * theme. Rewriting the profile response is what screenshots.mjs does, and
     * for the same reason.
     */
    await ctx.route('**/api/me/profile', async (route) => {
      const response = await route.fetch();
      const body = await response.json().catch(() => ({}));
      await route.fulfill({ json: { ...body, themeMode: theme } });
    });

    for (const route of ROUTES) {
      const page = await ctx.newPage();
      const problems = [];
      page.on('console', (m) => {
        if (m.type() !== 'error') return;
        if (m.text().includes('/api/piggy/') || m.text().includes('Failed to load resource')) return;
        problems.push(`console: ${m.text()}`);
      });
      page.on('pageerror', (e) => problems.push(`threw: ${e.message}`));
      page.on('response', (r) => {
        // Piggy is a separate container and is normally down on a dev host, so
        // its 503 is a fact about the environment rather than about the page.
        // Every other status belongs to the route under test.
        if (r.status() >= 400 && !r.url().includes('/api/piggy/')) {
          problems.push(`${r.status()} ${r.url().replace(BASE, '')}`);
        }
      });

      await page.goto(`${BASE}${route.path}`, { waitUntil: 'networkidle' });
      // The pages are lazy chunks behind a Suspense boundary; a measurement
      // taken before the fallback resolves measures the spinner.
      await page.waitForTimeout(1200);

      const measured = await page.evaluate(() => {
        const doc = document.documentElement;
        const overflow = doc.scrollWidth - doc.clientWidth;
        const wide = [...document.querySelectorAll('*')]
          .filter((el) => el.getBoundingClientRect().width > doc.clientWidth + 1)
          .slice(0, 3)
          .map((el) => `${el.tagName.toLowerCase()}.${(el.className?.baseVal ?? el.className ?? '')
            .toString()
            .split(' ')
            .slice(0, 4)
            .join('.')}`);
        return {
          overflow,
          wide,
          theme: doc.getAttribute('data-theme'),
          chars: document.body.innerText.trim().length,
          text: document.body.innerText.slice(0, 120),
        };
      });

      const label = `${route.slug} ${vp.name} ${theme}`;
      if (measured.theme !== theme) {
        // Without this the dark pass silently measured the light palette, and a
        // dark-only overflow — a wider border, a different font fallback —
        // would have gone unseen while the run reported ok.
        failures++;
        console.log(`FAIL ${label}: rendered data-theme=${measured.theme}, not ${theme}`);
      } else if (measured.chars < 120) {
        // A page that throws inside its own body still measures zero overflow.
        failures++;
        console.log(`FAIL ${label}: rendered only ${measured.chars} characters — the page is blank`);
      } else if (measured.overflow !== 0) {
        failures++;
        console.log(`FAIL ${label}: overflows by ${measured.overflow}px — ${measured.wide.join(', ')}`);
      } else if (problems.length) {
        failures++;
        console.log(`FAIL ${label}: ${[...new Set(problems)].slice(0, 4).join(' | ')}`);
      } else {
        console.log(`ok   ${label}`);
      }
      await page.close();
    }
    await ctx.close();
  }
}

await browser.close();
console.log(failures === 0 ? '\nevery Motion route: no overflow, no error' : `\n${failures} failed`);
process.exit(failures === 0 ? 0 : 1);
