// wasm/tests/functional.mjs
// Functional/browser tests — boot a static HTTP server over dist/, drive
// the SPA with headless Chromium via Playwright. Asserts:
//   * page loads, editor + tier/program radio groups present
//   * Each demo runs on the C and asm tiers and produces the canonical
//     native Python output.
//   * "All three" mode renders three tier blocks.
//
// Python (Pyodide) tier is exercised but its output is allowed to drift —
// loading Pyodide from CDN is flaky in CI and adds ~10 MB; we still assert
// it produces some output and matches roughly.

import { chromium } from "playwright";
import http from "node:http";
import fs from "node:fs";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";

const here = path.dirname(fileURLToPath(import.meta.url));
const dist = path.resolve(here, "..", "dist");
const repoRoot = path.resolve(here, "..", "..");
const demosDir = path.join(here, "..", "app", "demos");

const MIME = {
    ".html": "text/html", ".js": "text/javascript", ".mjs": "text/javascript",
    ".css": "text/css", ".wasm": "application/wasm", ".json": "application/json",
    ".lsp": "text/plain", ".py": "text/x-python",
};

function startServer(root, port) {
    return new Promise((resolve) => {
        const srv = http.createServer((req, res) => {
            const url = new URL(req.url, "http://localhost");
            let p = path.join(root, decodeURIComponent(url.pathname));
            if (fs.existsSync(p) && fs.statSync(p).isDirectory()) p = path.join(p, "index.html");
            if (!fs.existsSync(p)) { res.writeHead(404); res.end("not found"); return; }
            const ext = path.extname(p).toLowerCase();
            const ct = MIME[ext] || "application/octet-stream";
            res.writeHead(200, {
                "content-type": ct,
                "cross-origin-opener-policy": "same-origin",
                "cross-origin-embedder-policy": "require-corp",
                "cache-control": "no-store",
            });
            fs.createReadStream(p).pipe(res);
        });
        srv.listen(port, () => resolve(srv));
    });
}

const state = { pass: 0, fail: 0 };
function check(name, cond, detail) {
    if (cond) { state.pass++; console.log(`  ✓ ${name}`); }
    else      { state.fail++; console.log(`  ✗ ${name}${detail ? "\n      " + detail : ""}`); }
}

function nativePython(demoPath) {
    return execFileSync("python3", [path.join(repoRoot, "lumbda.py"), "--fast", demoPath], {
        encoding: "utf8", timeout: 120000,
    });
}

(async () => {
    const port = 8091;
    const server = await startServer(dist, port);
    const baseURL = `http://localhost:${port}/`;
    const browser = await chromium.launch({ headless: true });
    const ctx = await browser.newContext();
    const page = await ctx.newPage();
    page.on("pageerror", (e) => console.log("  ⟂ pageerror:", e.message));
    page.on("console", (msg) => {
        if (msg.type() === "error") console.log("  ⟂ console.error:", msg.text());
    });
    page.on("requestfailed", (req) => console.log("  ⟂ request failed:", req.url(), req.failure()?.errorText));

    try {
        await page.goto(baseURL, { waitUntil: "networkidle" });

        check("page loads", await page.title() !== "");
        check("editor mounted", (await page.locator(".cm-editor").count()) > 0);
        check("4 program radios", (await page.locator('input[name="program"]').count()) === 4);
        check("4 tier radios", (await page.locator('input[name="tier"]').count()) === 4);

        // Test each demo on C tier (fastest, deterministic).
        for (const demo of ["mandelbrot", "fib-ack", "sieve", "self-interp"]) {
            await page.locator(`input[name="program"][value="${demo}"]`).check();
            await page.locator('input[name="tier"][value="c"]').check();
            // wait for editor to update
            await page.waitForTimeout(150);
            await page.locator("#run").click();
            // wait for status to read ok or err
            await page.waitForFunction(
                () => /ok|err|completed/i.test(document.getElementById("status").textContent),
                null, { timeout: 30000 });
            const output = (await page.locator("#output pre").first().textContent()) || "";
            const demoPath = path.join(demosDir, demo + ".lsp");
            const canonical = nativePython(demoPath);
            check(`${demo} on C tier matches canonical`,
                output === canonical,
                `      expected: ${JSON.stringify(canonical.slice(0, 60))}…\n      got:      ${JSON.stringify(output.slice(0, 60))}…`);
        }

        // Asm tier — fib-ack only (smaller program, faster).
        await page.locator('input[name="program"][value="fib-ack"]').check();
        await page.locator('input[name="tier"][value="asm"]').check();
        await page.waitForTimeout(150);
        await page.locator("#run").click();
        await page.waitForFunction(
            () => /ok|err|completed/i.test(document.getElementById("status").textContent),
            null, { timeout: 30000 });
        const asmOut = (await page.locator("#output pre").first().textContent()) || "";
        const canonicalFA = nativePython(path.join(demosDir, "fib-ack.lsp"));
        check("fib-ack on asm tier matches canonical", asmOut === canonicalFA);

        // "All three" mode — 3 tier-blocks should render.
        await page.locator('input[name="program"][value="sieve"]').check();
        await page.locator('input[name="tier"][value="all"]').check();
        await page.waitForTimeout(150);
        await page.locator("#run").click();
        await page.waitForFunction(
            () => /ok|err|completed/i.test(document.getElementById("status").textContent),
            null, { timeout: 120000 });
        const blocks = await page.locator("#output .tier-block").count();
        check(`"all three" mode renders 3 tier blocks (got ${blocks})`, blocks === 3);

        // State-leak regression: run mandelbrot twice on the cached asm
        // instance and assert outputs match. Prior to commit 346b873's
        // env-NIL fix, every-other cell of row 2+ rendered " " instead
        // of the expected shade char.
        await page.locator('input[name="program"][value="mandelbrot"]').check();
        await page.locator('input[name="tier"][value="asm"]').check();
        await page.waitForTimeout(150);
        await page.locator("#run").click();
        await page.waitForFunction(
            () => /ok|err/i.test(document.getElementById("status").textContent),
            null, { timeout: 30000 });
        const asmRun1 = (await page.locator("#output pre").first().textContent()) || "";
        await page.locator("#run").click();
        await page.waitForFunction(
            () => /ok|err/i.test(document.getElementById("status").textContent),
            null, { timeout: 30000 });
        const asmRun2 = (await page.locator("#output pre").first().textContent()) || "";
        check("asm mandelbrot stable across two consecutive runs",
            asmRun1 === asmRun2 && asmRun1.length > 100);

    } catch (e) {
        state.fail++;
        console.log("  ✗ exception:", e.message);
    } finally {
        await browser.close();
        server.close();
        console.log(`\n${state.pass} passed, ${state.fail} failed`);
        process.exit(state.fail ? 1 : 0);
    }
})();
