#!/usr/bin/env node
// test_bytecode_repeat.js — Regression test for bytecode when/do/perform bug
//
// Tests that (when cond (do (perform ...) (recurse))) correctly resumes
// the do continuation after perform/cek_resume in bytecode-compiled code.
//
// The bug: bytecode-compiled hs-repeat-times only iterates 2x instead of 3x
// because the do continuation is lost after perform suspension.
//
// Source-loaded code works (CEK handles when/do/perform correctly).
// Bytecode-compiled code fails (VM/CEK handoff loses the continuation).
//
// Usage: node hosts/ocaml/browser/test_bytecode_repeat.js
//
// Expected output when bug is fixed:
//   SOURCE:   6 suspensions (3 iterations × 2 waits) ✓
//   BYTECODE: 6 suspensions (3 iterations × 2 waits) ✓

const fs = require('fs');
const path = require('path');

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

// --- Minimal DOM stubs ---
function makeElement(tag) {
  const el = {
    tagName: tag, _attrs: {}, _classes: new Set(), style: {},
    childNodes: [], children: [], textContent: '', nodeType: 1,
    classList: {
      add(c) { el._classes.add(c); },
      remove(c) { el._classes.delete(c); },
      contains(c) { return el._classes.has(c); },
    },
    setAttribute(k, v) { el._attrs[k] = String(v); },
    getAttribute(k) { return el._attrs[k] || null; },
    removeAttribute(k) { delete el._attrs[k]; },
    appendChild(c) { el.children.push(c); el.childNodes.push(c); return c; },
    insertBefore(c) { el.children.push(c); el.childNodes.push(c); return c; },
    removeChild(c) { return c; }, replaceChild(n) { return n; },
    cloneNode() { return makeElement(tag); },
    addEventListener() {}, removeEventListener() {}, dispatchEvent() {},
    get className() { return [...el._classes].join(' '); },
    get innerHTML() { return ''; }, set innerHTML(v) {},
    get outerHTML() { return '<' + tag + '>'; },
    dataset: {}, querySelectorAll() { return []; }, querySelector() { return null; },
  };
  return el;
}

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

async function main() {
  // Load WASM kernel
  require(path.join(WASM_DIR, 'sx_browser.bc.js'));
  const K = globalThis.SxKernel;
  if (!K) { console.error('FATAL: SxKernel not found'); process.exit(1); }

  // Register FFI
  K.registerNative('host-global', args => (args[0] in globalThis) ? globalThis[args[0]] : null);
  K.registerNative('host-get', args => { if (args[0] == null) return null; const v = args[0][args[1]]; return v === undefined ? null : v; });
  K.registerNative('host-set!', args => { if (args[0] != null) args[0][args[1]] = args[2]; return args[2]; });
  K.registerNative('host-call', args => {
    const [obj, method, ...rest] = args;
    if (obj == null || typeof obj[method] !== 'function') return null;
    return obj[method].apply(obj, rest) ?? null;
  });
  K.registerNative('host-new', args => new (Function.prototype.bind.apply(args[0], [null, ...args.slice(1)])));
  K.registerNative('host-callback', args => {
    const fn = args[0];
    return function() { return K.callFn(fn, Array.from(arguments)); };
  });
  K.registerNative('host-typeof', args => typeof args[0]);
  K.registerNative('host-await', args => args[0]);

  K.eval('(define SX_VERSION "test-bc-repeat")');
  K.eval('(define SX_ENGINE "ocaml-vm-test")');
  K.eval('(define parse sx-parse)');
  K.eval('(define serialize sx-serialize)');

  // DOM stubs for HS runtime
  K.eval('(define dom-add-class (fn (el cls) (host-call (host-get el "classList") "add" cls)))');
  K.eval('(define dom-remove-class (fn (el cls) (host-call (host-get el "classList") "remove" cls)))');
  K.eval('(define dom-has-class? (fn (el cls) (host-call (host-get el "classList") "contains" cls)))');
  K.eval('(define dom-listen (fn (target event-name handler) (handler {:type event-name :target target})))');

  // --- Test helper: count suspensions ---
  function countSuspensions(result) {
    return new Promise(resolve => {
      let count = 0;
      function drive(r) {
        if (!r || !r.suspended) { resolve(count); return; }
        count++;
        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);
        if (opName === 'io-sleep' || opName === 'wait') {
          setTimeout(() => {
            try { drive(r.resume(null)); }
            catch(e) { console.error('  resume error:', e.message); resolve(count); }
          }, 1);
        } else { resolve(count); }
      }
      drive(result);
    });
  }

  let pass = 0, fail = 0;
  function assert(name, got, expected) {
    if (got === expected) { pass++; console.log(`  ✓ ${name}`); }
    else { fail++; console.error(`  ✗ ${name}: got ${got}, expected ${expected}`); }
  }

  // =====================================================================
  // Test 1: SOURCE — load hs-repeat-times from .sx, call with perform
  // =====================================================================
  console.log('\n=== Test: SOURCE-loaded hs-repeat-times ===');

  // Load from source
  const hsFiles = ['tokenizer', 'parser', 'compiler', 'runtime'];
  for (const f of hsFiles) {
    K.load(fs.readFileSync(path.join(PROJECT_ROOT, 'lib/hyperscript', f + '.sx'), 'utf8'));
  }

  // Build handler and call it
  K.eval(`(define _src-handler
    (eval-expr
      (list 'fn '(me)
        (list 'let '((it nil) (event {:type "click"}))
          (hs-to-sx-from-source "on click repeat 3 times add .active to me then wait 1ms then remove .active then wait 1ms end")))))`);

  const srcMe = makeElement('button');
  K.eval('(define _src-me (host-global "_srcMe"))');
  global._srcMe = srcMe;
  K.eval('(define _src-me (host-global "_srcMe"))');

  let srcResult;
  try { srcResult = K.callFn(K.eval('_src-handler'), [srcMe]); }
  catch(e) { console.error('Source call error:', e.message); }

  const srcSuspensions = await countSuspensions(srcResult);
  assert('source: 6 suspensions (3 iters × 2 waits)', srcSuspensions, 6);

  // =====================================================================
  // Test 2: BYTECODE — load hs-repeat-times from .sxbc, call with perform
  // =====================================================================
  console.log('\n=== Test: BYTECODE-loaded hs-repeat-times ===');

  // Reload from bytecode — overwrite the source-defined versions
  if (K.beginModuleLoad) K.beginModuleLoad();
  for (const f of ['hs-tokenizer', 'hs-parser', 'hs-compiler', 'hs-runtime']) {
    const bcPath = path.join(SX_DIR, f + '.sxbc');
    if (fs.existsSync(bcPath)) {
      const bcSrc = fs.readFileSync(bcPath, 'utf8');
      K.load('(load-sxbc (first (parse "' + bcSrc.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '")))');
    }
  }
  if (K.endModuleLoad) K.endModuleLoad();

  // Build handler with the bytecode-loaded hs-repeat-times
  K.eval(`(define _bc-handler
    (eval-expr
      (list 'fn '(me)
        (list 'let '((it nil) (event {:type "click"}))
          (hs-to-sx-from-source "on click repeat 3 times add .active to me then wait 1ms then remove .active then wait 1ms end")))))`);

  const bcMe = makeElement('button');
  global._bcMe = bcMe;
  K.eval('(define _bc-me (host-global "_bcMe"))');

  let bcResult;
  try { bcResult = K.callFn(K.eval('_bc-handler'), [bcMe]); }
  catch(e) { console.error('Bytecode call error:', e.message); }

  const bcSuspensions = await countSuspensions(bcResult);
  assert('bytecode: 6 suspensions (3 iters × 2 waits)', bcSuspensions, 6);

  // =====================================================================
  // Test 3: Minimal — just hs-repeat-times + perform, no hyperscript
  // =====================================================================
  console.log('\n=== Test: Minimal repeat + perform ===');

  // Source version
  K.eval('(define _src-count 0)');
  K.eval(`(define _src-repeat-fn
    (fn (n thunk)
      (define do-repeat
        (fn (i) (when (< i n) (do (thunk) (do-repeat (+ i 1))))))
      (do-repeat 0)))`);
  K.eval(`(define _src-repeat-thunk
    (eval-expr '(fn () (_src-repeat-fn 3 (fn () (set! _src-count (+ _src-count 1)) (perform (list 'io-sleep 1)))))))`);

  let minSrcResult;
  try { minSrcResult = K.callFn(K.eval('_src-repeat-thunk'), []); }
  catch(e) { console.error('Minimal source error:', e.message); }
  const minSrcSusp = await countSuspensions(minSrcResult);
  const minSrcCount = K.eval('_src-count');
  assert('minimal source: 3 suspensions', minSrcSusp, 3);
  assert('minimal source: count=3', minSrcCount, 3);

  // =====================================================================
  // Summary
  // =====================================================================
  console.log(`\n${pass} passed, ${fail} failed`);
  process.exit(fail > 0 ? 1 : 0);
}

main().catch(e => { console.error('FATAL:', e.message); process.exit(1); });
