#!/usr/bin/env node
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
import { chromium } from 'playwright-core';

const baseUrl = (process.env.Q4_E2E_URL || process.argv[2] || 'http://127.0.0.1:3280').replace(/\/$/, '');
const chrome = process.env.Q4_CHROME || '/usr/bin/google-chrome';
const outDir = path.resolve('.selfcheck/shots');
await fs.mkdir(outDir, { recursive: true });

function collectFailures(page) {
  const failures = [];
  page.on('pageerror', (error) => failures.push(`pageerror: ${error.stack || error}`));
  page.on('console', (message) => {
    if (message.type() === 'error') failures.push(`console: ${message.text()}`);
  });
  return failures;
}

async function assertCanvasWebGLReady(page) {
  const sample = await page.locator('#gameCanvas').evaluate((canvas) => {
    const context = canvas.getContext('webgl2');
    return {
      context: Boolean(context),
      version: context?.getParameter(context.VERSION) || null,
      rejects2d: canvas.getContext('2d') === null,
      width: canvas.width,
      height: canvas.height
    };
  });
  assert.equal(sample.context, true, 'game canvas must expose WebGL2');
  assert.match(sample.version || '', /WebGL 2/i, `unexpected graphics context: ${JSON.stringify(sample)}`);
  assert.equal(sample.rejects2d, true, 'game canvas must not switch from WebGL2 to 2D');
  assert.ok(sample.width >= 300 && sample.height >= 200, `canvas is unexpectedly small: ${JSON.stringify(sample)}`);
}

async function assertNoViewportOverflow(page) {
  const result = await page.evaluate(() => ({
    documentWidth: document.documentElement.scrollWidth,
    viewportWidth: innerWidth,
    documentHeight: document.documentElement.scrollHeight,
    viewportHeight: innerHeight,
    clippedText: [...document.querySelectorAll('button, strong, h1, h2, p, span')]
      .filter((element) => {
        const style = getComputedStyle(element);
        const rect = element.getBoundingClientRect();
        if (element.classList.contains('sr-only') || style.display === 'none' || style.visibility === 'hidden' || rect.width === 0 || rect.height === 0) return false;
        return element.scrollWidth > element.clientWidth + 2 || element.scrollHeight > element.clientHeight + 2;
      })
      .map((element) => `${element.tagName.toLowerCase()}#${element.id}.${element.className}`)
      .slice(0, 20)
  }));
  assert.ok(result.documentWidth <= result.viewportWidth + 1, `horizontal overflow: ${JSON.stringify(result)}`);
  assert.ok(result.documentHeight <= result.viewportHeight + 1, `vertical overflow: ${JSON.stringify(result)}`);
  assert.deepEqual(result.clippedText, [], `visible text is clipped: ${JSON.stringify(result.clippedText)}`);
}

async function desktopSmoke(browser) {
  const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, locale: 'pl-PL' });
  const page = await context.newPage();
  const failures = collectFailures(page);
  const response = await page.goto(`${baseUrl}/?nodirect=1`, { waitUntil: 'networkidle' });
  assert.equal(response?.status(), 200);
  await page.locator('#runtimeTitle').waitFor({ state: 'visible' });
  assert.match(await page.locator('#runtimeTitle').textContent(), /q4base|gotowe|silnik/i);
  assert.equal(await page.evaluate(() => crossOriginIsolated), true, 'COOP/COEP isolation must be active');
  await assertCanvasWebGLReady(page);
  await assertNoViewportOverflow(page);

  await page.locator('#menuButton').click();
  await page.locator('#aboutButton').click();
  await page.locator('#aboutDialog').waitFor({ state: 'visible' });
  const legalLinks = await page.locator('#aboutDialog a[href^="/legal/"], #aboutDialog a[href^="/source/"]').evaluateAll(
    (links) => links.map((link) => link.getAttribute('href'))
  );
  assert.ok(legalLinks.includes('/legal/GPL-3.0.txt'));
  assert.ok(legalLinks.includes('/legal/QUAKE4-SDK-EULA.rtf'));
  assert.ok(legalLinks.includes('/source/openq4-web-source.tar.xz'));
  await page.screenshot({ path: path.join(outDir, 'q4-shell-desktop.png'), fullPage: true });
  assert.deepEqual(failures, [], failures.join('\n'));
  await context.close();
}

async function mobileSmoke(browser) {
  const context = await browser.newContext({
    viewport: { width: 844, height: 390 },
    screen: { width: 844, height: 390 },
    hasTouch: true,
    isMobile: true,
    deviceScaleFactor: 2,
    locale: 'pl-PL'
  });
  const page = await context.newPage();
  const failures = collectFailures(page);
  await page.goto(`${baseUrl}/?nodirect=1`, { waitUntil: 'networkidle' });
  await page.locator('#runtimeTitle').waitFor({ state: 'visible' });
  await assertCanvasWebGLReady(page);
  await assertNoViewportOverflow(page);

  const manifest = await page.evaluate(async () => (await fetch('/manifest.webmanifest')).json());
  assert.equal(manifest.orientation, 'landscape');
  await page.evaluate(async () => navigator.serviceWorker?.ready);

  const inputEvidence = await page.evaluate(() => {
    const events = [];
    const router = window.Q4App.input;
    router.sink = {
      action: (name, down, source) => events.push({ kind: 'action', name, down, source }),
      move: (x, y, source) => events.push({ kind: 'move', x, y, source }),
      look: (x, y, source) => events.push({ kind: 'look', x, y, source }),
      weapon: (direction, source) => events.push({ kind: 'weapon', direction, source })
    };
    document.body.classList.add('engine-active');
    const overlay = document.querySelector('#touchOverlay');
    overlay.setAttribute('aria-hidden', 'false');

    const dispatch = (element, type, id, x, y) => element.dispatchEvent(new PointerEvent(type, {
      pointerId: id, pointerType: 'touch', clientX: x, clientY: y, bubbles: true, cancelable: true, isPrimary: id === 41
    }));
    const move = document.querySelector('#moveZone');
    move.setPointerCapture = () => {};
    const moveRect = move.getBoundingClientRect();
    dispatch(move, 'pointerdown', 41, moveRect.left + moveRect.width * 0.5, moveRect.top + moveRect.height * 0.5);
    dispatch(move, 'pointermove', 41, moveRect.left + moveRect.width * 0.8, moveRect.top + moveRect.height * 0.2);

    const fire = document.querySelector('[data-action="fire"]');
    fire.setPointerCapture = () => {};
    const fireRect = fire.getBoundingClientRect();
    dispatch(fire, 'pointerdown', 42, fireRect.left + fireRect.width * 0.5, fireRect.top + fireRect.height * 0.5);
    dispatch(fire, 'pointerup', 42, fireRect.left + fireRect.width * 0.5, fireRect.top + fireRect.height * 0.5);

    const ptt = document.querySelector('[data-action="ptt"]');
    ptt.setPointerCapture = () => {};
    const pttRect = ptt.getBoundingClientRect();
    dispatch(ptt, 'pointerdown', 43, pttRect.left + pttRect.width * 0.5, pttRect.top + pttRect.height * 0.5);
    dispatch(ptt, 'pointerup', 43, pttRect.left + pttRect.width * 0.5, pttRect.top + pttRect.height * 0.5);
    dispatch(move, 'pointerup', 41, moveRect.left + moveRect.width * 0.8, moveRect.top + moveRect.height * 0.2);

    const controls = [...overlay.querySelectorAll('.touch-button, .stick-base')]
      .map((element) => ({ label: element.getAttribute('aria-label') || element.className, rect: element.getBoundingClientRect().toJSON() }))
      .filter((entry) => entry.rect.width > 0 && entry.rect.height > 0);
    const overlaps = [];
    for (let left = 0; left < controls.length; left += 1) {
      for (let right = left + 1; right < controls.length; right += 1) {
        const a = controls[left].rect;
        const b = controls[right].rect;
        const width = Math.max(0, Math.min(a.right, b.right) - Math.max(a.left, b.left));
        const height = Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top));
        if (width * height > 4) overlaps.push([controls[left].label, controls[right].label, width * height]);
      }
    }
    return { events, overlaps };
  });
  assert.ok(inputEvidence.events.some((event) => event.kind === 'move' && Math.abs(event.x) > 0.2), JSON.stringify(inputEvidence));
  assert.ok(inputEvidence.events.some((event) => event.kind === 'action' && event.name === 'fire' && event.down), JSON.stringify(inputEvidence));
  assert.ok(inputEvidence.events.some((event) => event.kind === 'action' && event.name === 'ptt' && event.down), JSON.stringify(inputEvidence));
  assert.deepEqual(inputEvidence.overlaps, [], `touch controls overlap: ${JSON.stringify(inputEvidence.overlaps)}`);
  await page.screenshot({ path: path.join(outDir, 'q4-shell-mobile.png'), fullPage: true });
  assert.deepEqual(failures, [], failures.join('\n'));
  await context.close();
}

for (const pathname of ['/healthz', '/legal/NOTICE.txt', '/legal/GPL-3.0.txt', '/legal/QUAKE4-SDK-EULA.rtf']) {
  const response = await fetch(`${baseUrl}${pathname}`);
  assert.equal(response.status, 200, `${pathname} must be public`);
  assert.ok(Number(response.headers.get('content-length')) > 0, `${pathname} must not be empty`);
}

const browser = await chromium.launch({
  executablePath: chrome,
  headless: true,
  args: ['--enable-unsafe-swiftshader', '--use-angle=swiftshader', '--disable-dev-shm-usage', '--no-sandbox']
});
try {
  await desktopSmoke(browser);
  await mobileSmoke(browser);
  process.stdout.write(`Q4 shell E2E passed at ${baseUrl}; screenshots: ${outDir}\n`);
} finally {
  await browser.close();
}
