#!/usr/bin/env node
// Standalone smoke test — no Pulsar / no Babel needed.
// Loads lib/* by stripping ESM syntax with a simple regex transform.

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

function loadAsCJS(filepath, depExports = {}) {
  let src = fs.readFileSync(filepath, 'utf8');
  src = src.replace(/^'use babel';\s*$/gm, '');
  src = src.replace(/^import\s+\{([^}]+)\}\s+from\s+['"][^'"]+['"];?/gm,
    (_, names) => names.split(',').map(n => `const ${n.trim()} = __DEPS__.${n.trim()};`).join('\n')
  );
  const exportNames = [];
  src = src.replace(/^export\s+function\s+(\w+)/gm, (_, n) => { exportNames.push(n); return `function ${n}`; });
  src = src.replace(/^export\s+const\s+(\w+)/gm,    (_, n) => { exportNames.push(n); return `const ${n}`; });
  const wrapper = `(function(__DEPS__){\n${src}\nreturn { ${exportNames.join(', ')} };\n})`;
  return eval(wrapper)(depExports);
}

const libdir = path.join(__dirname, '..', 'lib');
const lcxl = loadAsCJS(path.join(libdir, 'lcxl-map.js'));
const kw   = loadAsCJS(path.join(libdir, 'tidal-keywords.js'));
const role = loadAsCJS(path.join(libdir, 'role-classifier.js'));
const parser = loadAsCJS(path.join(libdir, 'parser.js'), { ...lcxl, ...kw, ...role });

function assert(cond, msg) {
  if (!cond) { console.error('  ✘ ' + msg); process.exitCode = 1; }
  else       { console.log('  ✓ ' + msg); }
}

function run(label, filepath, expects) {
  console.log(`\n[${label}] ${filepath}`);
  if (!fs.existsSync(filepath)) { console.log('  (file missing, skipping)'); return; }
  const text = fs.readFileSync(filepath, 'utf8');
  let result;
  const t0 = Date.now();
  try {
    result = parser.parse(text);
  } catch (e) {
    console.error('  ✘ parser threw: ' + e.message);
    process.exitCode = 1;
    return;
  }
  const dt = Date.now() - t0;
  console.log(`  parsed in ${dt}ms  (orbits=${result.orbits.length} groups=${Object.keys(result.groups).length} errors=${result.errors.length})`);
  assert(dt < 50, `parse < 50ms`);
  assert(result.errors.length === 0, `no parse errors  ${result.errors.length ? '— ' + result.errors.join(' | ') : ''}`);

  if (expects) {
    for (const e of expects) {
      const orbit = result.orbits.find(o => o.n === e.n);
      assert(orbit, `orbit d${e.n} found`);
      if (!orbit) continue;
      if (e.soundContains) assert(
        (orbit.sound || '').includes(e.soundContains),
        `d${e.n}.sound contains "${e.soundContains}"  (got "${orbit.sound}")`
      );
      if (e.bindings) {
        for (const b of e.bindings) {
          const found = orbit.bindings.find(x => x.control.label === b.label && x.effect === b.effect);
          assert(found, `d${e.n} has binding ${b.label}:${b.effect}  (have: ${orbit.bindings.map(x => x.control.label + ':' + x.effect).join(',')})`);
        }
      }
    }
  }

  return result;
}

console.log('=== pulsar-parvagues-hud parser smoke ===');

run('bullet_train', '/home/pln/Work/Sound/Tidal/bullet_train.tidal', [
  { n: 1, soundContains: 'jazz' },          // # "jazz" wins over bare "k k k k*<1 2>"
  { n: 4, soundContains: 'bassWarsaw' },
  { n: 8, soundContains: 'jungle_breaks' }
]);

run('techno_orage', '/home/pln/Work/Sound/Tidal/live/midi/nova/techno/techno_orage.tidal', [
  { n: 3, soundContains: 'orage' },         // not "<1 2 3>" — synth name wins
  { n: 6, soundContains: 'clap' },          // d6 special (B3) — should still be picked up
  { n: 10, soundContains: 'risers' }
]);

const ns = run('nouveau_soleil', '/home/pln/Work/Sound/Tidal/live/midi/nova/dnb/nouveau_soleil.tidal', [
  { n: 4, soundContains: 'bass_gameboy' },
  { n: 5, soundContains: 'synth_gameboy' },
  { n: 7, soundContains: 'moogBass', bindings: [{ label: 'B7', effect: 'octer' }, { label: 'C7', effect: 'room' }] },
  { n: 8, soundContains: 'breaks165' }
]);

// Dump a compact view of nouveau_soleil for human eyeballing.
if (ns) {
  console.log('\n--- nouveau_soleil dump ---');
  console.log('GROUPS: ' + Object.entries(ns.groups).map(([k, v]) => `${k}:${v.control.label}:${v.effect}`).join(' · '));
  for (const o of ns.orbits) {
    const sound = o.sound ? `"${o.sound.slice(0,28)}"` : '(no sound)';
    const binds = o.bindings.map(b => `${b.control.label}:${b.effect}`).join(' · ');
    const name = o.name ? `—${o.name}— ` : '';
    const role = o.role ? ` {${o.role}/${o.roleSource}}` : '';
    console.log(`  d${o.n} ${name}${sound}${role}  [${binds}]`);
  }
}

console.log('\n=== smoke complete ===');
