#!/usr/bin/env node
// test_hs_repeat.js — Debug hyperscript repeat+wait continuation bug
//
// Runs the exact expression that fails in the browser:
//   on click repeat 3 times add .active to me then wait 300ms
//     then remove .active then wait 300ms end
//
// Uses the real WASM kernel with perform/resume_vm, NOT mock IO.
// Waits are shortened to 1ms. All IO suspensions are logged.
//
// Usage: node hosts/ocaml/browser/test_hs_repeat.js

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

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

// --- DOM stubs with class tracking ---
function makeElement(tag) {
  const el = {
    tagName: tag, _attrs: {}, _children: [], _classes: new Set(),
    style: {}, childNodes: [], children: [], textContent: '',
    nodeType: 1,
    classList: {
      add(c) { el._classes.add(c); console.log(`  [dom] classList.add("${c}") → {${[...el._classes]}}`); },
      remove(c) { el._classes.delete(c); console.log(`  [dom] classList.remove("${c}") → {${[...el._classes]}}`); },
      contains(c) { return el._classes.has(c); },
      toggle(c) { if (el._classes.has(c)) el._classes.delete(c); else el._classes.add(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); el.children.push(c); return c; },
    insertBefore(c) { el._children.push(c); el.childNodes.push(c); el.children.push(c); return c; },
    removeChild(c) { return c; },
    replaceChild(n) { return n; },
    cloneNode() { return makeElement(tag); },
    addEventListener() {}, removeEventListener() {}, dispatchEvent() {},
    get innerHTML() {
      return el._children.map(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 = []; },
    get outerHTML() {
      let s = '<' + tag;
      for (const k of Object.keys(el._attrs).sort()) s += ` ${k}="${el._attrs[k]}"`;
      s += '>';
      if (['br','hr','img','input','meta','link'].includes(tag)) return s;
      return s + el.innerHTML + '</' + tag + '>';
    },
    dataset: new Proxy({}, {
      get(_, k) { return el._attrs['data-' + k.replace(/[A-Z]/g, c => '-' + c.toLowerCase())]; },
      set(_, k, v) { el._attrs['data-' + k.replace(/[A-Z]/g, c => '-' + c.toLowerCase())] = v; return true; }
    }),
    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); }
  console.log('WASM kernel loaded');

  // Register FFI primitives
  K.registerNative('host-global', args => {
    const name = args[0];
    return (name in globalThis) ? globalThis[name] : null;
  });
  K.registerNative('host-get', args => {
    const [obj, prop] = args;
    if (obj == null) return null;
    const v = obj[prop];
    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;
    const r = obj[method].apply(obj, rest);
    return r === undefined ? null : r;
  });
  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-hs-1.0")');
  K.eval('(define SX_ENGINE "ocaml-vm-wasm-test")');
  K.eval('(define parse sx-parse)');
  K.eval('(define serialize sx-serialize)');

  // Stub DOM primitives that HS runtime calls
  // dom-listen fires handler immediately (simulates the event)
  K.eval('(define dom-add-class (fn (el cls) (dict-set! (get el "classes") cls true) nil))');
  K.eval('(define dom-remove-class (fn (el cls) (dict-delete! (get el "classes") cls) nil))');
  K.eval('(define dom-has-class? (fn (el cls) (dict-has? (get el "classes") cls)))');
  K.eval('(define dom-listen (fn (target event-name handler) (handler {:type event-name :target target})))');

  // Load hyperscript modules
  const hsFiles = [
    'lib/hyperscript/tokenizer.sx',
    'lib/hyperscript/parser.sx',
    'lib/hyperscript/compiler.sx',
    'lib/hyperscript/runtime.sx',
  ];
  for (const f of hsFiles) {
    const src = fs.readFileSync(path.join(PROJECT_ROOT, f), 'utf8');
    const r = K.load(src);
    if (typeof r === 'string' && r.startsWith('Error')) {
      console.error(`Load failed: ${f}: ${r}`);
      process.exit(1);
    }
  }
  console.log('Hyperscript modules loaded');

  // Compile the expression
  const compiled = K.eval('(hs-to-sx-from-source "on click repeat 3 times add .active to me then wait 300ms then remove .active then wait 300ms end")');
  console.log('Compiled:', K.eval(`(inspect '${typeof compiled === 'string' ? compiled : '?'})`));
  // Actually get it as a string
  const compiledStr = K.eval('(inspect (hs-to-sx-from-source "on click repeat 3 times add .active to me then wait 300ms then remove .active then wait 300ms end"))');
  console.log('Compiled SX:', compiledStr);

  // Create handler function (same as hs-handler does)
  K.eval('(define _test-me {:tag "button" :id "test" :classes {} :_hs-activated true})');

  // Build the handler — wraps compiled SX in (fn (me) (let ((it nil) (event ...)) <sx>))
  const handlerSrc = K.eval('(inspect (hs-to-sx-from-source "on click repeat 3 times add .active to me then wait 300ms then remove .active then wait 300ms end"))');
  K.eval(`(define _test-handler
    (eval-expr
      (list 'fn '(me)
        (list 'let '((it nil) (event {:type "click" :target _test-me}))
          (hs-to-sx-from-source "on click repeat 3 times add .active to me then wait 300ms then remove .active then wait 300ms end")))))`);

  console.log('\n=== Invoking handler (simulates click event) ===');
  console.log('Expected: 3 iterations × (add .active, wait 300, remove .active, wait 300)');
  console.log('Expected: 6 IO suspensions total\n');

  // Call the handler — this will suspend on the first hs-wait (perform)
  let suspensionCount = 0;
  let result;
  try {
    result = K.callFn(K.eval('_test-handler'), [K.eval('_test-me')]);
  } catch(e) {
    console.error('Initial call error:', e.message);
    process.exit(1);
  }

  // Drive async suspension chain with real timeouts (1ms instead of 300ms)
  function driveAsync(res) {
    return new Promise((resolve) => {
      function step(r) {
        if (!r || !r.suspended) {
          console.log(`\n=== Done. Total suspensions: ${suspensionCount} (expected: 6) ===`);
          console.log(`Result: ${r === null ? 'null' : typeof r === 'object' ? JSON.stringify(r) : r}`);
          resolve();
          return;
        }

        suspensionCount++;
        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);
        const arg = items && items[1];

        console.log(`Suspension #${suspensionCount}: op=${opName} arg=${arg}`);

        if (opName === 'io-sleep' || opName === 'wait') {
          // Resume after 1ms (not real 300ms)
          setTimeout(() => {
            try {
              const resumed = r.resume(null);
              console.log(`  Resumed: suspended=${resumed && resumed.suspended}, type=${typeof resumed}`);
              step(resumed);
            } catch(e) {
              console.error(`  Resume error: ${e.message}`);
              resolve();
            }
          }, 1);
        } else {
          console.log(`  Unhandled IO op: ${opName}`);
          resolve();
        }
      }
      step(res);
    });
  }

  await driveAsync(result);

  // Check final element state
  const classes = K.eval('(get _test-me "classes")');
  console.log('\nFinal element classes:', JSON.stringify(classes));
}

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