// Run SX test specs against sx-browser.js.
//
// sx-browser.js parses and evaluates test specs — SX tests itself.
// This script provides only platform functions (error catching, reporting).
//
// Usage:
//   node shared/sx/tests/run.js                     # run all available specs
//   node shared/sx/tests/run.js eval                # run only test-eval.sx
//   node shared/sx/tests/run.js eval parser router   # run specific specs
//   node shared/sx/tests/run.js --legacy            # run monolithic test.sx

Object.defineProperty(globalThis, "document", { value: undefined, writable: true });
var path = require("path");
var fs = require("fs");
var Sx = require(path.resolve(__dirname, "../../static/scripts/sx-browser.js"));

// --- Test state ---
var suiteStack = [];
var passed = 0, failed = 0, testNum = 0;

// --- Helpers ---
function isNil(x) { return x === Sx.NIL || x === null || x === undefined; }

function deepEqual(a, b) {
  if (a === b) return true;
  if (isNil(a) && isNil(b)) return true;
  if (typeof a !== typeof b) return false;
  if (Array.isArray(a) && Array.isArray(b)) {
    if (a.length !== b.length) return false;
    for (var i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
    return true;
  }
  if (a && typeof a === "object" && b && typeof b === "object") {
    var ka = Object.keys(a), kb = Object.keys(b);
    if (ka.length !== kb.length) return false;
    for (var j = 0; j < ka.length; j++) if (!deepEqual(a[ka[j]], b[ka[j]])) return false;
    return true;
  }
  return false;
}

// --- Platform functions injected into the SX env ---
var env = {
  // Error catching — calls an SX thunk, returns result dict
  "try-call": function(thunk) {
    try {
      Sx.eval([thunk], env);
      return { ok: true };
    } catch(e) {
      return { ok: false, error: e.message || String(e) };
    }
  },

  // Test reporting
  "report-pass": function(name) {
    testNum++;
    passed++;
    var fullName = suiteStack.concat([name]).join(" > ");
    console.log("ok " + testNum + " - " + fullName);
  },
  "report-fail": function(name, error) {
    testNum++;
    failed++;
    var fullName = suiteStack.concat([name]).join(" > ");
    console.log("not ok " + testNum + " - " + fullName);
    console.log("  # " + error);
  },

  // Suite context
  "push-suite": function(name) { suiteStack.push(name); },
  "pop-suite": function() { suiteStack.pop(); },

  // Primitives that sx-browser.js has internally but doesn't expose through env
  "equal?": function(a, b) { return deepEqual(a, b); },
  "eq?": function(a, b) { return a === b; },
  "boolean?": function(x) { return typeof x === "boolean"; },
  "string-length": function(s) { return String(s).length; },
  "substring": function(s, start, end) { return String(s).slice(start, end); },
  "string-contains?": function(s, needle) { return String(s).indexOf(needle) !== -1; },
  "upcase": function(s) { return String(s).toUpperCase(); },
  "downcase": function(s) { return String(s).toLowerCase(); },
  "reverse": function(c) { return c ? c.slice().reverse() : []; },
  "flatten": function(c) {
    var r = [];
    for (var i = 0; i < (c||[]).length; i++) {
      if (Array.isArray(c[i])) for (var j = 0; j < c[i].length; j++) r.push(c[i][j]);
      else r.push(c[i]);
    }
    return r;
  },
  "has-key?": function(d, k) { return d && typeof d === "object" && k in d; },
  "append": function(c, x) { return Array.isArray(x) ? (c||[]).concat(x) : (c||[]).concat([x]); },
  "for-each-indexed": function(f, coll) {
    for (var i = 0; i < (coll||[]).length; i++) {
      Sx.eval([f, i, coll[i]], env);
    }
  },
  "dict-set!": function(d, k, v) { if (d) d[k] = v; },
  "dict-has?": function(d, k) { return d && typeof d === "object" && k in d; },
  "dict-get": function(d, k) { return d ? d[k] : undefined; },
  "starts-with?": function(s, prefix) { return String(s).indexOf(prefix) === 0; },
  "ends-with?": function(s, suffix) { var str = String(s); return str.indexOf(suffix) === str.length - suffix.length; },
  "slice": function(s, start, end) { return end !== undefined ? s.slice(start, end) : s.slice(start); },
  "inc": function(n) { return n + 1; },
  "append!": function(arr, item) { if (Array.isArray(arr)) arr.push(item); },
  "dict": function() { return {}; },
  "for-each": function(f, coll) {
    for (var i = 0; i < (coll||[]).length; i++) {
      Sx.eval([f, coll[i]], env);
    }
  },

  // --- Parser platform functions (for test-parser.sx) ---
  "sx-parse": function(source) { return Sx.parseAll(source); },
  "sx-serialize": function(val) {
    // Basic serializer for test roundtrips
    if (val === Sx.NIL || val === null || val === undefined) return "nil";
    if (typeof val === "boolean") return val ? "true" : "false";
    if (typeof val === "number") return String(val);
    if (typeof val === "string") return '"' + val.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
    // Check Symbol/Keyword BEFORE generic object — they are objects too
    if (val && (val._sym || val._sx_symbol)) return val.name;
    if (val && (val._kw || val._sx_keyword)) return ":" + val.name;
    if (Array.isArray(val)) return "(" + val.map(function(x) { return env["sx-serialize"](x); }).join(" ") + ")";
    if (val && typeof val === "object") {
      var parts = [];
      Object.keys(val).forEach(function(k) {
        parts.push(":" + k);
        parts.push(env["sx-serialize"](val[k]));
      });
      return "{" + parts.join(" ") + "}";
    }
    return String(val);
  },
  "make-symbol": function(name) { return Sx.sym ? Sx.sym(name) : { _sx_symbol: true, name: name, toString: function() { return name; } }; },
  "make-keyword": function(name) { return Sx.kw ? Sx.kw(name) : { _sx_keyword: true, name: name, toString: function() { return name; } }; },
  "symbol-name": function(s) { return s && s.name ? s.name : String(s); },
  "keyword-name": function(k) { return k && k.name ? k.name : String(k); },

  // --- Render platform function (for test-render.sx) ---
  "render-html": function(sxSource) {
    if (!Sx.renderToHtml) throw new Error("render-to-html not available — html adapter not bootstrapped");
    var exprs = Sx.parseAll(sxSource);
    var result = "";
    for (var i = 0; i < exprs.length; i++) {
      result += Sx.renderToHtml(exprs[i], env);
    }
    return result;
  },
};

// --- Resolve which test specs to run ---
var refDir = path.resolve(__dirname, "../ref");
var args = process.argv.slice(2);

// Available spec modules and their platform requirements
var SPECS = {
  "eval":    { file: "test-eval.sx",    needs: [] },
  "parser":  { file: "test-parser.sx",  needs: ["sx-parse"] },
  "router":  { file: "test-router.sx",  needs: [] },
  "render":  { file: "test-render.sx",  needs: ["render-html"] },
  "deps":    { file: "test-deps.sx",    needs: [] },
  "engine":  { file: "test-engine.sx",  needs: [] },
};

function evalFile(filename) {
  var filepath = path.resolve(refDir, filename);
  if (!fs.existsSync(filepath)) {
    console.log("# SKIP " + filename + " (file not found)");
    return;
  }
  var src = fs.readFileSync(filepath, "utf8");
  var exprs = Sx.parseAll(src);
  for (var i = 0; i < exprs.length; i++) {
    Sx.eval(exprs[i], env);
  }
}

// Legacy mode — run monolithic test.sx
if (args[0] === "--legacy") {
  console.log("TAP version 13");
  evalFile("test.sx");
} else {
  // Determine which specs to run
  var specsToRun;
  if (args.length > 0) {
    specsToRun = args;
  } else {
    // Auto-discover: run all specs whose platform functions are available
    specsToRun = Object.keys(SPECS);
  }

  console.log("TAP version 13");

  // Always load framework first
  evalFile("test-framework.sx");

  // Load router.sx if testing router (it defines the functions being tested)
  for (var si = 0; si < specsToRun.length; si++) {
    var specName = specsToRun[si];
    var spec = SPECS[specName];
    if (!spec) {
      console.log("# SKIP unknown spec: " + specName);
      continue;
    }

    // Check platform requirements
    var canRun = true;
    for (var ni = 0; ni < spec.needs.length; ni++) {
      if (!(spec.needs[ni] in env)) {
        console.log("# SKIP " + specName + " (missing: " + spec.needs[ni] + ")");
        canRun = false;
        break;
      }
    }
    if (!canRun) continue;

    // Load prerequisite spec modules
    if (specName === "router") {
      if (Sx.splitPathSegments) {
        env["split-path-segments"] = Sx.splitPathSegments;
        env["parse-route-pattern"] = Sx.parseRoutePattern;
        env["match-route-segments"] = Sx.matchRouteSegments;
        env["match-route"] = Sx.matchRoute;
        env["find-matching-route"] = Sx.findMatchingRoute;
        env["make-route-segment"] = Sx.makeRouteSegment;
      } else {
        evalFile("router.sx");
      }
    }

    if (specName === "deps") {
      if (Sx.scanRefs) {
        env["scan-refs"] = Sx.scanRefs;
        env["scan-components-from-source"] = Sx.scanComponentsFromSource;
        env["transitive-deps"] = Sx.transitiveDeps;
        env["compute-all-deps"] = Sx.computeAllDeps;
        env["components-needed"] = Sx.componentsNeeded;
        env["page-component-bundle"] = Sx.pageComponentBundle;
        env["page-css-classes"] = Sx.pageCssClasses;
        env["scan-io-refs"] = Sx.scanIoRefs;
        env["transitive-io-refs"] = Sx.transitiveIoRefs;
        env["compute-all-io-refs"] = Sx.computeAllIoRefs;
        env["component-pure?"] = Sx.componentPure_p;
        env["test-env"] = function() { return env; };
      }
    }

    if (specName === "engine") {
      if (Sx.parseTime) {
        env["parse-time"] = Sx.parseTime;
        env["parse-trigger-spec"] = Sx.parseTriggerSpec;
        env["default-trigger"] = Sx.defaultTrigger;
        env["parse-swap-spec"] = Sx.parseSwapSpec;
        env["parse-retry-spec"] = Sx.parseRetrySpec;
        env["next-retry-ms"] = function(cur, cap) { return Math.min(cur * 2, cap); };
        env["filter-params"] = Sx.filterParams;
      }
    }

    console.log("# --- " + specName + " ---");
    evalFile(spec.file);
  }
}

// --- Summary ---
console.log("");
console.log("1.." + testNum);
console.log("# tests " + (passed + failed));
console.log("# pass  " + passed);
if (failed > 0) {
  console.log("# fail  " + failed);
  process.exit(1);
}
