#!/usr/bin/env node
/**
 * sx-harness.js — Node.js test harness for the SX WASM kernel.
 *
 * Uses happy-dom for DOM APIs and loads the kernel + bytecode
 * in the same way the browser does, but 10-50x faster than Playwright.
 *
 * Usage:
 *   const { createSxEnv } = require('./sx-harness');
 *   const env = await createSxEnv();
 *   env.setHTML('<div id="main-panel"><span data-sx-island="counter"></span></div>');
 *   env.boot();
 *   // inspect DOM, signals, etc.
 *   env.close();
 */

const { Window } = require('happy-dom');
const fs = require('fs');
const path = require('path');

const WASM_DIR = path.resolve(__dirname, '../../shared/static/wasm');
const SX_DIR = path.join(WASM_DIR, 'sx');

/**
 * Create an SX environment with happy-dom + WASM kernel.
 * @param {object} opts
 * @param {string} opts.html - initial document body HTML
 * @param {boolean} opts.boot - run boot-init after loading (default: false)
 * @param {string[]} opts.components - SX component source strings to preload
 */
async function createSxEnv(opts = {}) {
  const win = new Window({ url: 'http://localhost:8013/sx/' });
  const doc = win.document;

  // ---- Expose happy-dom globals the kernel expects ----
  const g = win;
  g.global = g;
  // XHR for synchronous bytecode loading
  g.XMLHttpRequest = createSyncXHR();
  // requestIdleCallback (not in happy-dom)
  g.requestIdleCallback = (fn) => g.setTimeout(fn, 0);
  // matchMedia stub
  g.matchMedia = () => ({ matches: false, addEventListener: () => {} });
  // navigator
  if (!g.navigator) g.navigator = {};
  g.navigator.serviceWorker = { register: () => Promise.resolve() };

  // ---- Set up as globalThis for the kernel ----
  const origGlobal = globalThis;
  const keysToRestore = [];

  // Hoist happy-dom globals to Node globalThis so require() + kernel see them
  const propsToHoist = [
    'window', 'document', 'Element', 'Text', 'DocumentFragment', 'Document',
    'Event', 'CustomEvent', 'MutationObserver', 'AbortController', 'Headers',
    'HTMLElement', 'Node', 'Promise',
    'localStorage', 'sessionStorage', 'location', 'history', 'navigator',
    'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval',
    'requestAnimationFrame', 'cancelAnimationFrame', 'requestIdleCallback',
    'fetch', 'XMLHttpRequest', 'matchMedia',
  ];
  for (const k of propsToHoist) {
    if (k in g) {
      keysToRestore.push([k, globalThis[k]]);
      globalThis[k] = g[k];
    }
  }
  globalThis.window = g;
  keysToRestore.push(['window', undefined]);

  // ---- Console capture ----
  const logs = [];
  const origConsoleLog = console.log;
  const origConsoleError = console.error;
  const origConsoleWarn = console.warn;
  console.log = (...args) => { logs.push({ type: 'log', text: args.join(' ') }); };
  console.error = (...args) => { logs.push({ type: 'error', text: args.join(' ') }); };
  console.warn = (...args) => { logs.push({ type: 'warn', text: args.join(' ') }); };

  // ---- Load WASM kernel (js_of_ocaml mode) ----
  const kernelPath = path.join(WASM_DIR, 'sx_browser.bc.js');
  // Clear from require cache so each env gets a fresh kernel
  delete require.cache[require.resolve(kernelPath)];
  require(kernelPath);

  if (!globalThis.SxKernel) {
    throw new Error('SxKernel not set after loading sx_browser.bc.js');
  }
  const K = globalThis.SxKernel;

  // ---- Load platform (registers FFI, loads .sxbc web stack) ----
  const platformPath = path.join(WASM_DIR, 'sx-platform.js');
  delete require.cache[require.resolve(platformPath)];
  require(platformPath);

  // ---- Set initial HTML ----
  if (opts.html) {
    doc.body.innerHTML = opts.html;
  }

  // ---- Load components ----
  if (opts.components) {
    for (const src of opts.components) {
      K.load(src);
    }
  }

  // ---- Boot ----
  if (opts.boot) {
    globalThis.Sx.init();
  }

  // ---- Build environment object ----
  const env = {
    window: g,
    document: doc,
    K,
    Sx: globalThis.Sx,
    logs,

    /** Set body HTML. */
    setHTML(html) { doc.body.innerHTML = html; },

    /** Run SX boot-init. */
    boot() { globalThis.Sx.init(); },

    /** Evaluate SX expression, return result. */
    eval(expr) { return K.eval(expr); },

    /** Load SX source (defines, etc). */
    load(src) { return K.load(src); },

    /** Query a DOM element. */
    query(sel) { return doc.querySelector(sel); },

    /** Query all matching DOM elements. */
    queryAll(sel) { return Array.from(doc.querySelectorAll(sel)); },

    /** Get all islands in the document. */
    islands() {
      return Array.from(doc.querySelectorAll('[data-sx-island]')).map(el => ({
        name: el.getAttribute('data-sx-island'),
        element: el,
        hydrated: el._sxHydrated || false,
      }));
    },

    /** Simulate a click on an element. */
    click(selOrEl) {
      const el = typeof selOrEl === 'string' ? doc.querySelector(selOrEl) : selOrEl;
      if (!el) throw new Error(`click: element not found: ${selOrEl}`);
      el.click();
    },

    /** Get text content of element. */
    text(sel) {
      const el = doc.querySelector(sel);
      return el ? el.textContent : null;
    },

    /** Wait for microtasks + timers to flush. */
    async tick(ms = 0) {
      await new Promise(r => g.setTimeout(r, ms));
      // Flush any pending microtasks
      await new Promise(r => setImmediate(r));
    },

    /** Get captured console logs. */
    getLogs(filter) {
      if (!filter) return logs;
      return logs.filter(l => l.text.includes(filter));
    },

    /** Get errors from console. */
    getErrors() {
      return logs.filter(l => l.type === 'error');
    },

    /** Clean up. */
    close() {
      console.log = origConsoleLog;
      console.error = origConsoleError;
      console.warn = origConsoleWarn;
      for (const [k, v] of keysToRestore) {
        if (v === undefined) delete globalThis[k];
        else globalThis[k] = v;
      }
      win.close();
    },
  };

  return env;
}

/**
 * Create a synchronous XMLHttpRequest that reads from the filesystem.
 * The kernel uses sync XHR to load .sxbc files during boot.
 */
function createSyncXHR() {
  return class SyncXHR {
    constructor() {
      this.status = 0;
      this.responseText = '';
      this._method = '';
      this._url = '';
    }
    open(method, url, async) {
      this._method = method;
      this._url = url;
    }
    send() {
      // Map URL to local file path
      let filePath = this._url;
      // Strip query params
      filePath = filePath.split('?')[0];
      // Handle relative paths — resolve against WASM_DIR
      if (!filePath.startsWith('/') && !filePath.startsWith('file:')) {
        filePath = path.join(WASM_DIR, filePath);
      }
      // Strip file:// prefix
      filePath = filePath.replace(/^file:\/\//, '');

      try {
        this.responseText = fs.readFileSync(filePath, 'utf8');
        this.status = 200;
      } catch (e) {
        this.status = 404;
        this.responseText = '';
      }
    }
    setRequestHeader() {}
    getResponseHeader() { return null; }
  };
}

module.exports = { createSxEnv };
