#!/bin/bash
# WASM kernel tests in Node.js — verifies the compiled sx_browser.bc.js
# handles HTML tags, rendering, signals, and components correctly.
# Does NOT require a running server or browser.
set -euo pipefail

cd "$(dirname "$0")/../../.."

node -e '
  // --- DOM stubs that track state ---
  function makeElement(tag) {
    var el = {
      tagName: tag,
      _attrs: {},
      _children: [],
      style: {},
      childNodes: [],
      children: [],
      textContent: "",
      setAttribute: function(k, v) { el._attrs[k] = v; },
      getAttribute: function(k) { return el._attrs[k] || null; },
      removeAttribute: function(k) { delete el._attrs[k]; },
      appendChild: function(c) { el._children.push(c); el.childNodes.push(c); el.children.push(c); return c; },
      insertBefore: function(c, ref) { el._children.push(c); el.childNodes.push(c); el.children.push(c); return c; },
      removeChild: function(c) { return c; },
      replaceChild: function(n, o) { return n; },
      cloneNode: function() { return makeElement(tag); },
      addEventListener: function() {},
      removeEventListener: function() {},
      dispatchEvent: function() {},
      get innerHTML() {
        // Reconstruct from children for simple cases
        return el._children.map(function(c) {
          if (c._isText) return c.textContent || "";
          if (c._isComment) return "<!--" + (c.textContent || "") + "-->";
          return c.outerHTML || "";
        }).join("");
      },
      set innerHTML(v) { el._children = []; el.childNodes = []; el.children = []; el.textContent = v; },
      get outerHTML() {
        var s = "<" + tag;
        var keys = Object.keys(el._attrs).sort();
        for (var i = 0; i < keys.length; i++) {
          s += " " + keys[i] + "=\"" + el._attrs[keys[i]] + "\"";
        }
        s += ">";
        var voids = ["br","hr","img","input","meta","link"];
        if (voids.indexOf(tag) >= 0) return s;
        s += el.innerHTML;
        s += "</" + tag + ">";
        return s;
      },
      dataset: new Proxy({}, {
        get: function(t, k) { return el._attrs["data-" + k.replace(/[A-Z]/g, function(c) { return "-" + c.toLowerCase(); })]; },
        set: function(t, k, v) { el._attrs["data-" + k.replace(/[A-Z]/g, function(c) { return "-" + c.toLowerCase(); })] = v; return true; }
      }),
      querySelectorAll: function() { return []; },
      querySelector: function() { return null; },
    };
    return el;
  }

  global.window = global;
  global.document = {
    createElement: makeElement,
    createDocumentFragment: function() {
      var f = makeElement("fragment");
      f.tagName = undefined;
      return f;
    },
    head: makeElement("head"),
    body: makeElement("body"),
    querySelector: function() { return null; },
    querySelectorAll: function() { return []; },
    createTextNode: function(s) { return {_isText:true, textContent:String(s), nodeType:3}; },
    addEventListener: function() {},
    createComment: function(s) { return {_isComment:true, textContent:s||"", nodeType:8}; },
    getElementsByTagName: function() { return []; },
  };
  global.localStorage = {getItem:function(){return null},setItem:function(){},removeItem:function(){}};
  global.CustomEvent = class { constructor(n,o){this.type=n;this.detail=(o||{}).detail||{}} };
  global.MutationObserver = class { observe(){} disconnect(){} };
  global.requestIdleCallback = function(fn) { return setTimeout(fn,0); };
  global.matchMedia = function() { return {matches:false}; };
  global.navigator = {serviceWorker:{register:function(){return Promise.resolve()}}};
  global.location = {href:"",pathname:"/",hostname:"localhost"};
  global.history = {pushState:function(){},replaceState:function(){}};
  global.fetch = function() { return Promise.resolve({ok:true,text:function(){return Promise.resolve("")}}); };
  global.setTimeout = setTimeout;
  global.clearTimeout = clearTimeout;
  global.XMLHttpRequest = class { open(){} send(){} };

  // --- Load kernel ---
  require("./shared/static/wasm/sx_browser.bc.js");
  var K = globalThis.SxKernel;
  if (!K) { console.error("FAIL: SxKernel not found"); process.exit(1); }

  // --- Register 8 FFI host primitives (normally done by sx-platform.js) ---
  K.registerNative("host-global", function(args) {
    var name = args[0];
    if (typeof globalThis !== "undefined" && name in globalThis) return globalThis[name];
    return null;
  });
  K.registerNative("host-get", function(args) {
    var obj = args[0], prop = args[1];
    if (obj == null) return null;
    var v = obj[prop];
    return v === undefined ? null : v;
  });
  K.registerNative("host-set!", function(args) {
    var obj = args[0], prop = args[1], val = args[2];
    if (obj != null) obj[prop] = val;
    return val;
  });
  K.registerNative("host-call", function(args) {
    var obj = args[0], method = args[1];
    var callArgs = args.slice(2);
    if (obj == null || typeof obj[method] !== "function") return null;
    var r = obj[method].apply(obj, callArgs);
    return r === undefined ? null : r;
  });
  K.registerNative("host-new", function(args) {
    var ctor = args[0];
    var ctorArgs = args.slice(1);
    return new (Function.prototype.bind.apply(ctor, [null].concat(ctorArgs)));
  });
  K.registerNative("host-callback", function(args) {
    var fn = args[0];
    return function() { return K.callFn(fn, Array.from(arguments)); };
  });
  K.registerNative("host-typeof", function(args) {
    return typeof args[0];
  });
  K.registerNative("host-await", function(args) { return args[0]; });

  // Platform constants
  K.eval("(define SX_VERSION \"test-1.0\")");
  K.eval("(define SX_ENGINE \"ocaml-vm-test\")");
  K.eval("(define parse sx-parse)");
  K.eval("(define serialize sx-serialize)");

  var pass = 0, fail = 0;
  function assert(name, got, expected) {
    if (got === expected) { pass++; }
    else { fail++; console.error("FAIL: " + name + "\n  got:      " + JSON.stringify(got) + "\n  expected: " + JSON.stringify(expected)); }
  }
  function assertIncludes(name, got, substr) {
    if (typeof got === "string" && got.includes(substr)) { pass++; }
    else { fail++; console.error("FAIL: " + name + "\n  got: " + JSON.stringify(got) + "\n  expected to include: " + JSON.stringify(substr)); }
  }
  function assertNotError(name, got) {
    if (typeof got === "string" && got.startsWith("Error:")) { fail++; console.error("FAIL: " + name + ": " + got); }
    else { pass++; }
  }

  // =====================================================================
  // Section 1: HTML tags and rendering
  // =====================================================================

  assert("arithmetic", K.eval("(+ 1 2)"), 3);
  assert("string", K.eval("(str \"hello\" \" world\")"), "hello world");

  // Tags as special forms — keywords preserved
  assert("div preserves keywords",
    K.eval("(inspect (div :class \"test\" \"hello\"))"),
    "(div :class \"test\" \"hello\")");

  assert("span preserves keywords",
    K.eval("(inspect (span :id \"x\" \"content\"))"),
    "(span :id \"x\" \"content\")");

  // render-to-html
  assert("render div+class", K.eval("(render-to-html (div :class \"card\" \"content\"))"), "<div class=\"card\">content</div>");
  assert("render h1+class", K.eval("(render-to-html (h1 :class \"title\" \"Hello\"))"), "<h1 class=\"title\">Hello</h1>");
  assert("render a+href", K.eval("(render-to-html (a :href \"/about\" \"About\"))"), "<a href=\"/about\">About</a>");
  assert("render nested", K.eval("(render-to-html (div :class \"outer\" (span :class \"inner\" \"text\")))"), "<div class=\"outer\"><span class=\"inner\">text</span></div>");
  assertIncludes("void element br", K.eval("(render-to-html (br))"), "br");

  // Component rendering
  K.eval("(defcomp ~test-card (&key title) (div :class \"card\" (h2 title)))");
  assert("component render", K.eval("(render-to-html (~test-card :title \"Hello\"))"), "<div class=\"card\"><h2>Hello</h2></div>");

  K.eval("(defcomp ~test-wrap (&key label) (div :class \"wrap\" (span label)))");
  assert("component nested", K.eval("(render-to-html (~test-wrap :label \"hi\"))"), "<div class=\"wrap\"><span>hi</span></div>");

  // Core primitives
  assert("list length", K.eval("(list 1 2 3)").items.length, 3);
  assert("first", K.eval("(first (list 1 2 3))"), 1);
  assert("len", K.eval("(len (list 1 2 3))"), 3);
  assert("map", K.eval("(len (map (fn (x) (+ x 1)) (list 1 2 3)))"), 3);

  // HTML tag registry
  assertNotError("HTML_TAGS defined", K.eval("(type-of HTML_TAGS)"));
  assert("is-html-tag? div", K.eval("(is-html-tag? \"div\")"), true);
  assert("is-html-tag? fake", K.eval("(is-html-tag? \"fake\")"), false);

  // =====================================================================
  // Load web stack modules (same as sx-platform.js loadWebStack)
  // =====================================================================
  var fs = require("fs");
  var webStackFiles = [
    "shared/static/wasm/sx/render.sx",
    "shared/static/wasm/sx/core-signals.sx",
    "shared/static/wasm/sx/signals.sx",
    "shared/static/wasm/sx/deps.sx",
    "shared/static/wasm/sx/router.sx",
    "shared/static/wasm/sx/page-helpers.sx",
    "shared/static/wasm/sx/freeze.sx",
    "shared/static/wasm/sx/dom.sx",
    "shared/static/wasm/sx/browser.sx",
    "shared/static/wasm/sx/adapter-html.sx",
    "shared/static/wasm/sx/adapter-sx.sx",
    "shared/static/wasm/sx/adapter-dom.sx",
    "shared/static/wasm/sx/boot-helpers.sx",
    "shared/static/wasm/sx/hypersx.sx",
    "shared/static/wasm/sx/engine.sx",
    "shared/static/wasm/sx/orchestration.sx",
    "shared/static/wasm/sx/boot.sx",
  ];
  var loadFails = [];
  var useBytecode = process.env.SX_TEST_BYTECODE === "1";
  if (K.beginModuleLoad) K.beginModuleLoad();
  for (var i = 0; i < webStackFiles.length; i++) {
    var loaded = false;
    if (useBytecode) {
      var bcPath = webStackFiles[i].replace(/\.sx$/, ".sxbc");
      try {
        var bcSrc = fs.readFileSync(bcPath, "utf8");
        global.__sxbcText = bcSrc;
        var bcResult = K.eval("(load-sxbc (first (parse (host-global \"__sxbcText\"))))");
        delete global.__sxbcText;
        if (typeof bcResult !== "string" || !bcResult.startsWith("Error")) {
          loaded = true;
        } else {
          loadFails.push(bcPath + " (sxbc): " + bcResult);
        }
      } catch(e) { delete global.__sxbcText; }
    }
    if (!loaded) {
      var src = fs.readFileSync(webStackFiles[i], "utf8");
      var r = K.load(src);
      if (typeof r === "string" && r.startsWith("Error")) {
        loadFails.push(webStackFiles[i] + ": " + r);
      }
    }
  }
  if (K.endModuleLoad) K.endModuleLoad();
  if (loadFails.length > 0) {
    console.error("Module load failures:");
    loadFails.forEach(function(f) { console.error("  " + f); });
  }

  // =====================================================================
  // Section 2: render-to-dom (requires working DOM stubs)
  // All DOM results are host objects — use host-get/dom-get-attr from SX
  // =====================================================================

  // Basic DOM rendering
  assert("dom tagName",
    K.eval("(host-get (render-to-dom (div :class \"test\" \"hello\") (global-env) nil) \"tagName\")"),
    "div");
  assert("dom class attr",
    K.eval("(dom-get-attr (render-to-dom (div :class \"test\" \"hello\") (global-env) nil) \"class\")"),
    "test");
  assertIncludes("dom outerHTML",
    K.eval("(host-get (render-to-dom (div :class \"test\" \"hello\") (global-env) nil) \"outerHTML\")"),
    "hello");

  // Nested DOM rendering
  assertIncludes("nested dom outerHTML",
    K.eval("(host-get (render-to-dom (div :class \"outer\" (span :id \"inner\" \"text\")) (global-env) nil) \"outerHTML\")"),
    "class=\"outer\"");

  // =====================================================================
  // Section 3: Reactive rendering — with-island-scope + deref
  // This is the critical test for the hydration bug.
  // with-island-scope should NOT strip attributes.
  // =====================================================================

  // 3a. with-island-scope should preserve static attributes
  assert("scoped static class",
    K.eval("(dom-get-attr (let ((d (list))) (with-island-scope (fn (x) (append! d x)) (fn () (render-to-dom (div :class \"scoped\" \"text\") (global-env) nil)))) \"class\")"),
    "scoped");

  // 3b. Signal deref in text position should render initial value
  assertIncludes("signal text initial value",
    K.eval("(host-get (let ((s (signal 42)) (d (list))) (with-island-scope (fn (x) (append! d x)) (fn () (render-to-dom (div (deref s)) (global-env) nil)))) \"outerHTML\")"),
    "42");

  // 3c. Signal deref in attribute position should set initial value
  assert("signal attr initial value",
    K.eval("(dom-get-attr (let ((s (signal \"active\")) (d (list))) (with-island-scope (fn (x) (append! d x)) (fn () (render-to-dom (div :class (deref s) \"content\") (global-env) nil)))) \"class\")"),
    "active");

  // 3d. After signal update, reactive DOM should update
  // render-to-dom needs unevaluated expr (as in real browser boot from parsed source)
  K.eval("(define test-reactive-sig (signal \"before\"))");
  assert("reactive attr update",
    K.eval("(let ((d (list))) (let ((el (with-island-scope (fn (x) (append! d x)) (fn () (render-to-dom (quote (div :class (deref test-reactive-sig) \"content\")) (global-env) nil))))) (reset! test-reactive-sig \"after\") (dom-get-attr el \"class\")))"),
    "after");

  // =====================================================================
  // Section 4: Letrec + perform resume (async _driveAsync)
  // =====================================================================

  // Define the letrec+perform pattern — this matches the test-runner island
  K.eval("(define __letrec-test-fn (letrec ((other (fn () \"from-other\")) (go (fn () (do (perform {:op \"io-sleep\" :args (list 50)}) (other))))) go))");

  // Get the function as a JS-callable value
  var letrecFn = K.eval("__letrec-test-fn");
  if (typeof letrecFn !== "function") {
    fail++; console.error("FAIL: letrec-fn not callable, got: " + typeof letrecFn);
  } else {
    // Call via callFn — same path as island click handlers
    var letrecResult = K.callFn(letrecFn, []);
    // Resume through all suspensions — tests that resume() preserves letrec env
    try {
      while (letrecResult && letrecResult.suspended) { letrecResult = letrecResult.resume(null); }
      assert("letrec sibling after perform resume", letrecResult, "from-other");
    } catch(e) {
      fail++; console.error("FAIL: letrec perform resume: " + (e.message || e));
    }
  }

  // Recursive letrec after perform — the wait-boot pattern
  K.eval("(define __wb-counter 0)");
  K.eval("(define __recur-test-fn (letrec ((recur (fn () (set! __wb-counter (+ __wb-counter 1)) (if (>= __wb-counter 3) \"done\" (do (perform {:op \"io-sleep\" :args (list 10)}) (recur)))))) (fn () (set! __wb-counter 0) (recur))))");

  var recurFn = K.eval("__recur-test-fn");
  if (typeof recurFn !== "function") {
    fail++; console.error("FAIL: recur-fn not callable, got: " + typeof recurFn);
  } else {
    var recurResult = K.callFn(recurFn, []);
    try {
      // Resume through all suspensions synchronously
      while (recurResult && recurResult.suspended) { recurResult = recurResult.resume(null); }
      assert("recursive letrec after perform", recurResult, "done");
      assert("recursive letrec counter", K.eval("__wb-counter"), 3);
    } catch(e) {
      fail++; console.error("FAIL: recursive letrec perform: " + (e.message || e));
    }
  }

  // =====================================================================
  // Summary
  // =====================================================================
  console.log("WASM kernel tests: " + pass + " passed, " + fail + " failed");
  if (fail > 0) process.exit(1);
'
