#!/usr/bin/env node
/**
 * Batched HS conformance runner — option 2 (per-process kernel isolation).
 *
 * Each batch spawns a fresh Node process running tests/hs-run-filtered.js
 * with HS_START/HS_END set, so the WASM kernel's JIT cache starts empty.
 * Avoids the cumulative slowdown that hits the 1-process runner around
 * test 500-700 (compiled lambdas accumulate, allocation stalls).
 *
 * Usage:
 *   node tests/hs-run-batched.js
 *   HS_BATCH_SIZE=100 node tests/hs-run-batched.js
 *   HS_PARALLEL=4 node tests/hs-run-batched.js
 */
const { spawnSync, spawn } = require('child_process');
const path = require('path');
const fs = require('fs');

const FILTERED = path.join(__dirname, 'hs-run-filtered.js');
const TOTAL = parseInt(process.env.HS_TOTAL || '1514');
const FROM = parseInt(process.env.HS_FROM || '0');
const BATCH_SIZE = parseInt(process.env.HS_BATCH_SIZE || '150');
const PARALLEL = parseInt(process.env.HS_PARALLEL || '1');
const VERBOSE = !!process.env.HS_VERBOSE;

function makeBatches() {
  const batches = [];
  for (let i = FROM; i < TOTAL; i += BATCH_SIZE) {
    batches.push({ start: i, end: Math.min(i + BATCH_SIZE, TOTAL) });
  }
  return batches;
}

function runBatch({ start, end }) {
  const t0 = Date.now();
  const r = spawnSync('node', [FILTERED], {
    env: { ...process.env, HS_START: String(start), HS_END: String(end) },
    encoding: 'utf8',
    timeout: 1800_000, // 30 min per batch hard cap
  });
  const out = (r.stdout || '') + (r.stderr || '');
  const elapsed = Date.now() - t0;
  return { start, end, elapsed, out, code: r.status };
}

function parseBatch(out) {
  const result = { pass: 0, fail: 0, failures: [], slow: [], timeouts: [] };
  const m = out.match(/Results:\s+(\d+)\/(\d+)/);
  if (m) {
    result.pass = parseInt(m[1]);
    const total = parseInt(m[2]);
    result.fail = total - result.pass;
  }
  // Capture each "[suite] name: error" failure line
  const failSection = out.split('All failures:')[1] || '';
  for (const line of failSection.split('\n')) {
    const fm = line.match(/^\s*\[([^\]]+)\]\s+(.+?):\s*(.*)$/);
    if (fm) result.failures.push({ suite: fm[1], name: fm[2], err: fm[3] || '(empty)' });
  }
  for (const line of out.split('\n')) {
    const sm = line.match(/SLOW: test (\d+) took (\d+)ms \[([^\]]+)\] (.+)$/);
    if (sm) result.slow.push({ idx: +sm[1], ms: +sm[2], suite: sm[3], name: sm[4] });
    const tm = line.match(/TIMEOUT: test (\d+) \[([^\]]+)\] (.+)$/);
    if (tm) result.timeouts.push({ idx: +tm[1], suite: tm[2], name: tm[3] });
  }
  return result;
}

function fmtTime(ms) {
  if (ms < 1000) return `${ms}ms`;
  if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
  return `${Math.floor(ms / 60_000)}m${Math.round((ms % 60_000) / 1000)}s`;
}

async function runParallel(batches, concurrency) {
  const results = new Array(batches.length);
  let cursor = 0;
  async function worker() {
    while (cursor < batches.length) {
      const i = cursor++;
      results[i] = await new Promise((resolve) => {
        const t0 = Date.now();
        let out = '';
        const child = spawn('node', [FILTERED], {
          env: { ...process.env, HS_START: String(batches[i].start), HS_END: String(batches[i].end) },
        });
        child.stdout.on('data', d => out += d);
        child.stderr.on('data', d => out += d);
        child.on('exit', (code) => resolve({ ...batches[i], elapsed: Date.now() - t0, out, code }));
      });
      const r = parseBatch(results[i].out);
      process.stderr.write(`  batch ${batches[i].start}-${batches[i].end}: ${r.pass}/${r.pass + r.fail} (${fmtTime(results[i].elapsed)})\n`);
    }
  }
  await Promise.all(Array.from({ length: concurrency }, worker));
  return results;
}

(async () => {
  const batches = makeBatches();
  const t0 = Date.now();
  process.stderr.write(`Running ${TOTAL} tests in ${batches.length} batches of ${BATCH_SIZE} (parallelism=${PARALLEL})\n`);

  let results;
  if (PARALLEL > 1) {
    results = await runParallel(batches, PARALLEL);
  } else {
    results = [];
    for (const b of batches) {
      const r = runBatch(b);
      results.push(r);
      const p = parseBatch(r.out);
      process.stderr.write(`  batch ${b.start}-${b.end}: ${p.pass}/${p.pass + p.fail} (${fmtTime(r.elapsed)})\n`);
    }
  }

  let totalPass = 0, totalFail = 0;
  const allFailures = [];
  const allTimeouts = [];
  const slowest = [];
  for (const r of results) {
    const p = parseBatch(r.out);
    totalPass += p.pass;
    totalFail += p.fail;
    allFailures.push(...p.failures);
    allTimeouts.push(...p.timeouts);
    slowest.push(...p.slow);
    if (VERBOSE) process.stdout.write(r.out);
  }

  const totalElapsed = Date.now() - t0;
  process.stdout.write(`\n=== Conformance ===\n`);
  process.stdout.write(`Total: ${totalPass}/${totalPass + totalFail} (${(100 * totalPass / (totalPass + totalFail)).toFixed(2)}%)\n`);
  process.stdout.write(`Wall: ${fmtTime(totalElapsed)} across ${batches.length} batches\n`);

  if (allFailures.length) {
    process.stdout.write(`\nFailures (${allFailures.length}):\n`);
    for (const f of allFailures) process.stdout.write(`  [${f.suite}] ${f.name}: ${f.err}\n`);
  }
  if (allTimeouts.length && allTimeouts.length !== allFailures.length) {
    process.stdout.write(`\nTimeouts (${allTimeouts.length}):\n`);
    for (const t of allTimeouts) process.stdout.write(`  [${t.suite}] ${t.name}\n`);
  }
  slowest.sort((a, b) => b.ms - a.ms);
  if (slowest.length) {
    process.stdout.write(`\nSlowest 10 tests:\n`);
    for (const s of slowest.slice(0, 10)) process.stdout.write(`  ${s.ms}ms [${s.suite}] ${s.name}\n`);
  }

  process.exit(totalFail > 0 ? 1 : 0);
})();
