#!/usr/bin/env node
/**
 * Run hyperscript behavioral tests in Node.js with per-test timeout via worker_threads.
 */
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
const fs = require('fs');
const path = require('path');

if (!isMainThread) {
  // ─── Worker: run a single test ─────────────────────────────────
  const { testIdx } = workerData;
  const PROJECT = path.resolve(__dirname, '..');
  const WASM_DIR = path.join(PROJECT, 'shared/static/wasm');
  const SX_DIR = path.join(WASM_DIR, 'sx');

  eval(fs.readFileSync(path.join(WASM_DIR, 'sx_browser.bc.js'), 'utf8'));
  const K = globalThis.SxKernel;

  // Minimal DOM mock
  class El {
    constructor(t) { this.tagName=t.toUpperCase(); this.nodeName=this.tagName; this.nodeType=1; this.id=''; this.className=''; this.classList=new CL(this); this.style={}; this.attributes={}; this.children=[]; this.childNodes=[]; this.parentElement=null; this.parentNode=null; this.textContent=''; this.innerHTML=''; this._listeners={}; this.dataset={}; this.open=false; }
    setAttribute(n,v) { this.attributes[n]=v; if(n==='id')this.id=v; if(n==='class'){this.className=v;this.classList._s(v);} }
    getAttribute(n) { return this.attributes[n]!==undefined?this.attributes[n]:null; }
    removeAttribute(n) { delete this.attributes[n]; }
    hasAttribute(n) { return n in this.attributes; }
    addEventListener(e,f) { if(!this._listeners[e])this._listeners[e]=[]; this._listeners[e].push(f); }
    removeEventListener(e,f) { if(this._listeners[e])this._listeners[e]=this._listeners[e].filter(x=>x!==f); }
    dispatchEvent(ev) { ev.target=this; ev.currentTarget=this; for(const f of (this._listeners[ev.type]||[])){try{f(ev);}catch(e){}} if(ev.bubbles&&!ev._s&&this.parentElement){ev.currentTarget=this.parentElement;this.parentElement.dispatchEvent(ev);} return !ev.defaultPrevented; }
    appendChild(c) { if(c.parentElement)c.parentElement.removeChild(c); c.parentElement=this; c.parentNode=this; this.children.push(c); this.childNodes.push(c); return c; }
    removeChild(c) { this.children=this.children.filter(x=>x!==c); this.childNodes=this.childNodes.filter(x=>x!==c); c.parentElement=null; c.parentNode=null; return c; }
    insertBefore(n,r) { const i=this.children.indexOf(r); if(i>=0){this.children.splice(i,0,n);this.childNodes.splice(i,0,n);}else{this.children.push(n);this.childNodes.push(n);} n.parentElement=this;n.parentNode=this; return n; }
    replaceChild(n,o) { const i=this.children.indexOf(o); if(i>=0){this.children[i]=n;this.childNodes[i]=n;} n.parentElement=this;n.parentNode=this; o.parentElement=null;o.parentNode=null; return o; }
    querySelector(s) { return fnd(this,s); }
    querySelectorAll(s) { return fndAll(this,s); }
    closest(s) { let e=this; while(e){if(mt(e,s))return e; e=e.parentElement;} return null; }
    matches(s) { return mt(this,s); }
    contains(o) { if(o===this)return true; for(const c of this.children)if(c===o||c.contains(o))return true; return false; }
    cloneNode(d) { const e=new El(this.tagName.toLowerCase()); Object.assign(e.attributes,this.attributes); e.id=this.id; e.className=this.className; e.classList._s(this.className); Object.assign(e.style,this.style); e.textContent=this.textContent; e.innerHTML=this.innerHTML; if(d)for(const c of this.children)e.appendChild(c.cloneNode(true)); return e; }
    focus(){} blur(){} click(){ this.dispatchEvent(new Ev('click',{bubbles:true})); } remove(){ if(this.parentElement)this.parentElement.removeChild(this); }
    get firstElementChild() { return this.children[0]||null; }
    get lastElementChild() { return this.children[this.children.length-1]||null; }
    get nextElementSibling() { if(!this.parentElement)return null; const i=this.parentElement.children.indexOf(this); return this.parentElement.children[i+1]||null; }
    get previousElementSibling() { if(!this.parentElement)return null; const i=this.parentElement.children.indexOf(this); return i>0?this.parentElement.children[i-1]:null; }
    showModal(){this.open=true;this.setAttribute('open','');} show(){this.open=true;} close(){this.open=false;this.removeAttribute('open');}
    getAnimations(){return [];} getBoundingClientRect(){return{top:0,left:0,width:100,height:100};} scrollIntoView(){}
  }
  class CL { constructor(e){this._e=e;this._s=new Set();} _s(str){this._s=new Set((str||'').split(/\s+/).filter(Boolean));} add(...c){for(const x of c)this._s.add(x);this._e.className=[...this._s].join(' ');} remove(...c){for(const x of c)this._s.delete(x);this._e.className=[...this._s].join(' ');} toggle(c,f){if(f!==undefined){if(f)this.add(c);else this.remove(c);return f;} if(this._s.has(c)){this.remove(c);return false;}else{this.add(c);return true;}} contains(c){return this._s.has(c);} get length(){return this._s.size;} }
  class Ev { constructor(t,o={}){this.type=t;this.bubbles=o.bubbles||false;this.cancelable=o.cancelable!==false;this.defaultPrevented=false;this._s=false;this.target=null;this.currentTarget=null;this.detail=o.detail||null;} preventDefault(){this.defaultPrevented=true;} stopPropagation(){this._s=true;} stopImmediatePropagation(){this._s=true;} }
  function mt(e,s){if(!e||!e.tagName)return false;if(s.startsWith('#'))return e.id===s.slice(1);if(s.startsWith('.'))return e.classList.contains(s.slice(1));if(s.includes('#')){const[t,i]=s.split('#');return e.tagName.toLowerCase()===t&&e.id===i;} return e.tagName.toLowerCase()===s.toLowerCase();}
  function fnd(e,s){for(const c of(e.children||[])){if(mt(c,s))return c;const f=fnd(c,s);if(f)return f;}return null;}
  function fndAll(e,s){const r=[];for(const c of(e.children||[])){if(mt(c,s))r.push(c);r.push(...fndAll(c,s));}return r;}

  const _body=new El('body'); const _html=new El('html'); _html.appendChild(_body);
  const document={body:_body,documentElement:_html,createElement(t){return new El(t);},createElementNS(n,t){return new El(t);},createDocumentFragment(){return new El('fragment');},createTextNode(t){return{nodeType:3,textContent:t,data:t};},getElementById(i){return fnd(_body,'#'+i);},querySelector(s){return fnd(_body,s);},querySelectorAll(s){return fndAll(_body,s);},createEvent(t){return new Ev(t);},addEventListener(){},removeEventListener(){}};
  globalThis.document=document; globalThis.window=globalThis; globalThis.HTMLElement=El; globalThis.Element=El; globalThis.Event=Ev; globalThis.CustomEvent=Ev; globalThis.NodeList=Array; globalThis.HTMLCollection=Array;
  globalThis.getComputedStyle=(e)=>e.style; globalThis.requestAnimationFrame=(f)=>setTimeout(f,0); globalThis.cancelAnimationFrame=(i)=>clearTimeout(i);
  globalThis.MutationObserver=class{observe(){}disconnect(){}}; globalThis.ResizeObserver=class{observe(){}disconnect(){}}; globalThis.IntersectionObserver=class{observe(){}disconnect(){}};
  globalThis.navigator={userAgent:'node'}; globalThis.location={href:'http://localhost/',pathname:'/',search:'',hash:''}; globalThis.history={pushState(){},replaceState(){},back(){},forward(){}};

  K.registerNative('host-global',a=>{const n=a[0];return(n in globalThis)?globalThis[n]:null;});
  K.registerNative('host-get',a=>{if(a[0]==null)return null;const v=a[0][a[1]];return v===undefined?null:v;});
  K.registerNative('host-set!',a=>{if(a[0]!=null)a[0][a[1]]=a[2];return a[2];});
  K.registerNative('host-call',a=>{const[o,m,...r]=a;if(o==null){const f=globalThis[m];return typeof f==='function'?f.apply(null,r):null;}if(typeof o[m]!=='function')return null;try{return o[m].apply(o,r)||null;}catch(e){return null;}});
  K.registerNative('host-new',a=>{const C=typeof a[0]==='string'?globalThis[a[0]]:a[0];return typeof C==='function'?new C(...a.slice(1)):null;});
  K.registerNative('host-callback',a=>{const fn=a[0];if(typeof fn==='function'&&fn.__sx_handle===undefined)return fn;if(fn&&fn.__sx_handle!==undefined)return function(){const r=K.callFn(fn,Array.from(arguments));if(globalThis._driveAsync)globalThis._driveAsync(r);return r;};return function(){};});
  K.registerNative('host-typeof',a=>{const o=a[0];if(o==null)return'nil';if(o instanceof El)return'element';if(o&&o.nodeType===3)return'text';if(o instanceof Ev)return'event';if(o instanceof Promise)return'promise';return typeof o;});
  K.registerNative('host-await',a=>{const[p,cb]=a;if(p&&typeof p.then==='function'){const f=(cb&&cb.__sx_handle!==undefined)?v=>K.callFn(cb,[v]):()=>{};p.then(f);}});
  K.registerNative('load-library!',()=>false);

  globalThis._driveAsync=function driveAsync(r,d){d=d||0;if(d>200||!r||!r.suspended)return;const req=r.request;const items=req&&(req.items||req);const op=items&&items[0];const opName=typeof op==='string'?op:(op&&op.name)||String(op);function doResume(v){try{const x=r.resume(v);driveAsync(x,d+1);}catch(e){}}if(opName==='io-sleep'||opName==='wait')doResume(null);else if(opName==='io-fetch')doResume({ok:true,text:''});else if(opName==='io-settle')doResume(null);else if(opName==='io-wait-event')doResume(null);else if(opName==='io-transition')doResume(null);};

  K.eval('(define SX_VERSION "hs-test-1.0")');
  K.eval('(define SX_ENGINE "ocaml-vm-sandbox")');
  K.eval('(define parse sx-parse)');
  K.eval('(define serialize sx-serialize)');

  const WEB=['render','core-signals','signals','deps','router','page-helpers','freeze','dom','browser','adapter-html','adapter-sx','adapter-dom','boot-helpers','hypersx','engine','orchestration','boot'];
  const HS=['hs-tokenizer','hs-parser','hs-compiler','hs-runtime','hs-integration'];
  K.beginModuleLoad();
  for(const mod of[...WEB,...HS]){const sp=path.join(SX_DIR,mod+'.sx');const lp=path.join(PROJECT,'lib/hyperscript',mod.replace(/^hs-/,'')+'.sx');let s;try{s=fs.existsSync(sp)?fs.readFileSync(sp,'utf8'):fs.readFileSync(lp,'utf8');}catch(e){continue;}try{K.load(s);}catch(e){}}
  K.endModuleLoad();

  K.eval('(define _test-registry (list))'); K.eval('(define _test-suite "")');
  K.eval('(define push-suite (fn (name) (set! _test-suite name)))');
  K.eval('(define pop-suite (fn () (set! _test-suite "")))');
  K.eval('(define try-call (fn (thunk) (set! _test-registry (append _test-registry (list {:suite _test-suite :thunk thunk}))) {:ok true}))');
  K.eval('(define report-pass (fn (name) (let ((i (- (len _test-registry) 1))) (when (>= i 0) (dict-set! (nth _test-registry i) "name" name)))))');
  K.eval('(define report-fail (fn (name error) (let ((i (- (len _test-registry) 1))) (when (>= i 0) (dict-set! (nth _test-registry i) "name" name)))))');

  for(const f of['spec/harness.sx','spec/tests/test-framework.sx','spec/tests/test-hyperscript-behavioral.sx']){try{K.load(fs.readFileSync(path.join(PROJECT,f),'utf8'));}catch(e){}}

  // Run single test
  _body.children=[]; _body.childNodes=[]; _body.innerHTML='';
  const suite=K.eval(`(get (nth _test-registry ${testIdx}) "suite")`)||'';
  const name=K.eval(`(get (nth _test-registry ${testIdx}) "name")`)||`test-${testIdx}`;
  let ok=false, err=null;
  try{
    const thunk=K.eval(`(get (nth _test-registry ${testIdx}) "thunk")`);
    if(!thunk){err='no thunk';}else{const r=K.callFn(thunk,[]);if(r&&r.suspended)globalThis._driveAsync(r);ok=true;}
  }catch(e){err=(e.message||'').slice(0,150);}
  parentPort.postMessage({suite,name,ok,err});
  process.exit(0);
}

// ─── Main thread ─────────────────────────────────────────────────
const PROJECT = path.resolve(__dirname, '..');

async function runTest(idx) {
  return new Promise((resolve) => {
    const worker = new Worker(__filename, { workerData: { testIdx: idx } });
    const timer = setTimeout(() => { worker.terminate(); resolve({ suite: '', name: `test-${idx}`, ok: false, err: 'TIMEOUT' }); }, 8000);
    worker.on('message', (msg) => { clearTimeout(timer); resolve(msg); });
    worker.on('error', (e) => { clearTimeout(timer); resolve({ suite: '', name: `test-${idx}`, ok: false, err: 'CRASH: ' + (e.message || '').slice(0, 80) }); });
    worker.on('exit', (code) => { if (code !== 0) { clearTimeout(timer); resolve({ suite: '', name: `test-${idx}`, ok: false, err: 'EXIT: ' + code }); } });
  });
}

async function main() {
  // First, get test count by loading in main thread
  const WASM_DIR = path.join(PROJECT, 'shared/static/wasm');
  eval(fs.readFileSync(path.join(WASM_DIR, 'sx_browser.bc.js'), 'utf8'));
  const K = globalThis.SxKernel;
  K.registerNative('host-global',a=>null);K.registerNative('host-get',a=>null);K.registerNative('host-set!',a=>null);K.registerNative('host-call',a=>null);K.registerNative('host-new',a=>null);K.registerNative('host-callback',a=>function(){});K.registerNative('host-typeof',a=>'nil');K.registerNative('host-await',a=>null);K.registerNative('load-library!',()=>false);
  K.eval('(define SX_VERSION "hs-test-1.0")');K.eval('(define SX_ENGINE "ocaml-vm-sandbox")');K.eval('(define parse sx-parse)');K.eval('(define serialize sx-serialize)');
  const SX_DIR=path.join(WASM_DIR,'sx');
  const WEB=['render','core-signals','signals','deps','router','page-helpers','freeze','dom','browser','adapter-html','adapter-sx','adapter-dom','boot-helpers','hypersx','engine','orchestration','boot'];
  const HS=['hs-tokenizer','hs-parser','hs-compiler','hs-runtime','hs-integration'];
  K.beginModuleLoad();
  for(const mod of[...WEB,...HS]){const sp=path.join(SX_DIR,mod+'.sx');const lp=path.join(PROJECT,'lib/hyperscript',mod.replace(/^hs-/,'')+'.sx');let s;try{s=fs.existsSync(sp)?fs.readFileSync(sp,'utf8'):fs.readFileSync(lp,'utf8');}catch(e){continue;}try{K.load(s);}catch(e){}}
  K.endModuleLoad();
  K.eval('(define _test-registry (list))');K.eval('(define _test-suite "")');
  K.eval('(define push-suite (fn (name) (set! _test-suite name)))');
  K.eval('(define pop-suite (fn () (set! _test-suite "")))');
  K.eval('(define try-call (fn (thunk) (set! _test-registry (append _test-registry (list {:suite _test-suite :thunk thunk}))) {:ok true}))');
  K.eval('(define report-pass (fn (n) true))');K.eval('(define report-fail (fn (n e) true))');
  for(const f of['spec/harness.sx','spec/tests/test-framework.sx','spec/tests/test-hyperscript-behavioral.sx']){try{K.load(fs.readFileSync(path.join(PROJECT,f),'utf8'));}catch(e){}}

  const testCount = K.eval('(len _test-registry)');
  // Get names
  const testNames = [];
  for (let i = 0; i < testCount; i++) {
    testNames.push({
      s: K.eval(`(get (nth _test-registry ${i}) "suite")`) || '',
      n: K.eval(`(get (nth _test-registry ${i}) "name")`) || `test-${i}`,
    });
  }

  console.log(`Running ${testCount} tests...`);

  // Run tests — 4 workers at a time
  const results = [];
  const BATCH = 4;
  for (let i = 0; i < testCount; i += BATCH) {
    const batch = [];
    for (let j = i; j < Math.min(i + BATCH, testCount); j++) {
      batch.push(runTest(j).then(r => {
        r.suite = r.suite || testNames[j].s;
        r.name = r.name || testNames[j].n;
        return r;
      }));
    }
    const batchResults = await Promise.all(batch);
    results.push(...batchResults);
    if ((i + BATCH) % 100 < BATCH) process.stderr.write(`  ${Math.min(i + BATCH, testCount)}/${testCount}...\n`);
  }

  // Tally
  let passed = 0, failed = 0;
  const cats = {};
  const errTypes = {};
  for (const r of results) {
    if (!cats[r.suite]) cats[r.suite] = { p: 0, f: 0, errs: [] };
    if (r.ok) { passed++; cats[r.suite].p++; }
    else {
      failed++;
      cats[r.suite].f++;
      cats[r.suite].errs.push({ name: r.name, err: r.err });
      let t = 'other';
      if (r.err === 'TIMEOUT') t = 'timeout';
      else if (r.err && r.err.includes('NOT IMPLEMENTED')) t = 'stub';
      else if (r.err && r.err.includes('Assertion')) t = 'assert-fail';
      else if (r.err && r.err.includes('Expected')) t = 'wrong-value';
      else if (r.err && r.err.includes('Undefined symbol')) t = 'undef-sym';
      else if (r.err && r.err.includes('Unhandled')) t = 'unhandled';
      else if (r.err && r.err.includes('CRASH')) t = 'crash';
      else if (r.err && r.err.includes('EXIT')) t = 'exit';
      errTypes[t] = (errTypes[t] || 0) + 1;
    }
  }

  console.log(`\nResults: ${passed}/${passed + failed} (${(100 * passed / (passed + failed)).toFixed(0)}%)\n`);

  console.log('By category (sorted by pass rate):');
  for (const [cat, s] of Object.entries(cats).sort((a, b) => {
    const ra = a[1].p / (a[1].p + a[1].f);
    const rb = b[1].p / (b[1].p + b[1].f);
    return rb - ra;
  })) {
    const total = s.p + s.f;
    const pct = (100 * s.p / total).toFixed(0);
    const mark = s.f === 0 ? `✓ ${s.p}` : `${s.p}/${total} (${pct}%)`;
    console.log(`  ${cat}: ${mark}`);
  }

  console.log('\nFailure types:');
  for (const [t, n] of Object.entries(errTypes).sort((a, b) => b[1] - a[1])) {
    console.log(`  ${t}: ${n}`);
  }

  const uniqueErrors = {};
  for (const r of results.filter(r => !r.ok)) {
    const e = (r.err || '').slice(0, 100);
    if (!uniqueErrors[e]) uniqueErrors[e] = 0;
    uniqueErrors[e]++;
  }
  console.log(`\nUnique errors (${Object.keys(uniqueErrors).length}):`);
  for (const [e, n] of Object.entries(uniqueErrors).sort((a, b) => b[1] - a[1]).slice(0, 25)) {
    console.log(`  [${n}x] ${e}`);
  }
}

main().catch(e => { console.error(e); process.exit(1); });
