#!/usr/bin/env node
/**
 * Run SX spec tests in Node.js using the bootstrapped evaluator.
 *
 * Usage:
 *   node hosts/javascript/run_tests.js                    # all spec tests
 *   node hosts/javascript/run_tests.js test-primitives    # specific test
 */
const fs = require("fs");
const path = require("path");

// Provide globals that sx-browser.js expects
global.window = global;
global.addEventListener = () => {};
global.self = global;
global.document = {
  createElement: () => ({ style: {}, setAttribute: () => {}, appendChild: () => {}, children: [] }),
  createDocumentFragment: () => ({ appendChild: () => {}, children: [], childNodes: [] }),
  head: { appendChild: () => {} },
  body: { appendChild: () => {} },
  querySelector: () => null,
  querySelectorAll: () => [],
  createTextNode: (s) => ({ textContent: s }),
  addEventListener: () => {},
};
global.localStorage = { getItem: () => null, setItem: () => {}, removeItem: () => {} };
global.CustomEvent = class CustomEvent { 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: () => Promise.resolve() } };
global.location = { href: "", pathname: "/", hostname: "localhost" };
global.history = { pushState: () => {}, replaceState: () => {} };
global.fetch = () => Promise.resolve({ ok: true, text: () => Promise.resolve("") });
global.setTimeout = setTimeout;
global.clearTimeout = clearTimeout;
global.console = console;

// Load the bootstrapped evaluator
// Use --full flag to load a full-spec build (if available)
const fullBuild = process.argv.includes("--full");
const jsPath = fullBuild
  ? path.join(__dirname, "..", "..", "shared", "static", "scripts", "sx-full-test.js")
  : path.join(__dirname, "..", "..", "shared", "static", "scripts", "sx-browser.js");
if (fullBuild && !fs.existsSync(jsPath)) {
  console.error("Full test build not found. Run: python3 hosts/javascript/cli.py --extensions continuations --spec-modules types --output shared/static/scripts/sx-full-test.js");
  process.exit(1);
}
const Sx = require(jsPath);
if (!Sx || !Sx.parse) {
  console.error("Failed to load Sx evaluator");
  process.exit(1);
}

// Reset render mode — boot process may have set it to true
if (Sx.setRenderActive) Sx.setRenderActive(false);

// Test infrastructure
let passCount = 0;
let failCount = 0;
const suiteStack = [];

// Build env with all primitives + spec functions
const env = Sx.getEnv ? Object.assign({}, Sx.getEnv()) : {};

// Additional test helpers needed by spec tests
env["sx-parse"] = function(s) { return Sx.parse(s); };
env["sx-parse-one"] = function(s) { const r = Sx.parse(s); return r && r.length > 0 ? r[0] : null; };
env["test-env"] = function() { return Sx.getEnv ? Object.assign({}, Sx.getEnv()) : {}; };
env["cek-eval"] = function(s) {
  const parsed = Sx.parse(s);
  if (!parsed || parsed.length === 0) return null;
  return Sx.eval(parsed[0], Sx.getEnv ? Object.assign({}, Sx.getEnv()) : {});
};
env["eval-expr-cek"] = function(expr, e) { return Sx.eval(expr, e || env); };
env["env-get"] = function(e, k) { return e && e[k] !== undefined ? e[k] : null; };
env["env-has?"] = function(e, k) { return e && k in e; };
env["env-bind!"] = function(e, k, v) { if (e) e[k] = v; return v; };
env["env-set!"] = function(e, k, v) { if (e) e[k] = v; return v; };
env["env-extend"] = function(e) { return Object.create(e); };
env["env-merge"] = function(a, b) { return Object.assign({}, a, b); };

// Missing primitives referenced by tests
// primitive? is now in platform.py PRIMITIVES
env["contains-char?"] = function(s, c) { return typeof s === "string" && typeof c === "string" && s.indexOf(c) >= 0; };
env["escape-string"] = function(s) { return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t"); };
env["trim-right"] = function(s) { return typeof s === "string" ? s.trimEnd() : s; };
env["sha3-256"] = function(s) {
  // Simple hash stub for testing — not real SHA3
  var h = 0;
  for (var i = 0; i < s.length; i++) { h = ((h << 5) - h + s.charCodeAt(i)) | 0; }
  h = Math.abs(h);
  var hex = h.toString(16);
  while (hex.length < 64) hex = "0" + hex;
  return hex;
};
env["upcase"] = function(s) { return s.toUpperCase(); };
env["downcase"] = function(s) { return s.toLowerCase(); };
env["make-keyword"] = function(name) { return new Sx.Keyword(name); };
env["string-length"] = function(s) { return s.length; };
env["dict-get"] = function(d, k) { return d && d[k] !== undefined ? d[k] : null; };
env["apply"] = function(f) {
  var args = Array.prototype.slice.call(arguments, 1);
  var lastArg = args.pop();
  if (Array.isArray(lastArg)) args = args.concat(lastArg);
  return f.apply(null, args);
};

// Deep equality
function deepEqual(a, b) {
  if (a === b) return true;
  if (a == null || b == null) return a == b;
  if (typeof a !== typeof b) return false;
  if (Array.isArray(a) && Array.isArray(b)) {
    if (a.length !== b.length) return false;
    return a.every((v, i) => deepEqual(v, b[i]));
  }
  if (typeof a === "object") {
    const ka = Object.keys(a).filter(k => k !== "_nil");
    const kb = Object.keys(b).filter(k => k !== "_nil");
    if (ka.length !== kb.length) return false;
    return ka.every(k => deepEqual(a[k], b[k]));
  }
  return false;
}
env["equal?"] = deepEqual;
env["identical?"] = function(a, b) { return a === b; };

// Continuation support
env["make-continuation"] = function(fn) {
  // Continuation must be callable as a function AND have _continuation flag
  var c = function(v) { return fn(v !== undefined ? v : null); };
  c._continuation = true;
  c.fn = fn;
  c.call = function(v) { return fn(v !== undefined ? v : null); };
  return c;
};
env["continuation?"] = function(x) { return x != null && x._continuation === true; };
env["continuation-fn"] = function(c) { return c.fn; };

// Render helpers
// render-html: the tests call this with an SX source string, parse it, and render to HTML
// IMPORTANT: renderToHtml sets a global _renderMode flag but never resets it.
// We must reset it after each call so subsequent eval calls don't go through the render path.
env["render-html"] = function(src, e) {
  var result;
  if (typeof src === "string") {
    var parsed = Sx.parse(src);
    if (!parsed || parsed.length === 0) return "";
    var expr = parsed.length === 1 ? parsed[0] : [{ name: "do" }].concat(parsed);
    if (Sx.renderToHtml) {
      result = Sx.renderToHtml(expr, e || (Sx.getEnv ? Object.assign({}, Sx.getEnv()) : {}));
    } else {
      result = Sx.serialize(expr);
    }
  } else {
    if (Sx.renderToHtml) {
      result = Sx.renderToHtml(src, e || env);
    } else {
      result = Sx.serialize(src);
    }
  }
  // Reset render mode so subsequent eval calls don't go through DOM/HTML render path
  if (Sx.setRenderActive) Sx.setRenderActive(false);
  return result;
};
// Also register render-to-html directly
env["render-to-html"] = env["render-html"];

// Type system helpers — available when types module is included

// test-prim-types: dict of primitive return types for type inference
env["test-prim-types"] = function() {
  return {
    "+": "number", "-": "number", "*": "number", "/": "number",
    "mod": "number", "inc": "number", "dec": "number",
    "abs": "number", "min": "number", "max": "number",
    "floor": "number", "ceil": "number", "round": "number",
    "str": "string", "upper": "string", "lower": "string",
    "trim": "string", "join": "string", "replace": "string",
    "format": "string", "substr": "string",
    "=": "boolean", "<": "boolean", ">": "boolean",
    "<=": "boolean", ">=": "boolean", "!=": "boolean",
    "not": "boolean", "nil?": "boolean", "empty?": "boolean",
    "number?": "boolean", "string?": "boolean", "boolean?": "boolean",
    "list?": "boolean", "dict?": "boolean", "symbol?": "boolean",
    "keyword?": "boolean", "contains?": "boolean", "has-key?": "boolean",
    "starts-with?": "boolean", "ends-with?": "boolean",
    "len": "number", "first": "any", "rest": "list",
    "last": "any", "nth": "any", "cons": "list",
    "append": "list", "concat": "list", "reverse": "list",
    "sort": "list", "slice": "list", "range": "list",
    "flatten": "list", "keys": "list", "vals": "list",
    "map-dict": "dict", "assoc": "dict", "dissoc": "dict",
    "merge": "dict", "dict": "dict",
    "get": "any", "type-of": "string",
  };
};

// test-prim-param-types: dict of primitive param type specs
env["test-prim-param-types"] = function() {
  return {
    "+": {"positional": [["a", "number"]], "rest-type": "number"},
    "-": {"positional": [["a", "number"]], "rest-type": "number"},
    "*": {"positional": [["a", "number"]], "rest-type": "number"},
    "/": {"positional": [["a", "number"]], "rest-type": "number"},
    "inc": {"positional": [["n", "number"]], "rest-type": null},
    "dec": {"positional": [["n", "number"]], "rest-type": null},
    "upper": {"positional": [["s", "string"]], "rest-type": null},
    "lower": {"positional": [["s", "string"]], "rest-type": null},
    "keys": {"positional": [["d", "dict"]], "rest-type": null},
    "vals": {"positional": [["d", "dict"]], "rest-type": null},
  };
};

// Component type accessors
env["component-param-types"] = function(c) {
  return c && c._paramTypes ? c._paramTypes : null;
};
env["component-set-param-types!"] = function(c, t) {
  if (c) c._paramTypes = t;
  return null;
};
env["component-params"] = function(c) {
  return c && c.params ? c.params : null;
};
env["component-body"] = function(c) {
  return c && c.body ? c.body : null;
};
env["component-has-children"] = function(c) {
  return c && c.has_children ? c.has_children : false;
};

// Aser test helper: parse SX source, evaluate via aser, return wire format string
env["render-sx"] = function(source) {
  const exprs = Sx.parse(source);
  const parts = [];
  for (const expr of exprs) {
    const result = Sx.renderToSx(expr, env);
    if (result !== null && result !== undefined && result !== Sx.NIL) {
      parts.push(typeof result === "string" ? result : Sx.serialize(result));
    }
  }
  return parts.join("");
};

// Mock request/state primitives for test-handlers.sx
const _mockState = {};
env["now"] = function(fmt) { return new Date().toISOString(); };
env["state-get"] = function(key, dflt) { return key in _mockState ? _mockState[key] : (dflt !== undefined ? dflt : null); };
env["state-set!"] = function(key, val) { _mockState[key] = val; return val; };
env["state-clear!"] = function(key) { delete _mockState[key]; return null; };
env["request-method"] = function() { return "GET"; };
env["request-arg"] = function(name, dflt) { return dflt !== undefined ? dflt : null; };
env["request-form"] = function(name, dflt) { return dflt !== undefined ? dflt : ""; };
env["request-headers-all"] = function() { return {}; };
env["request-form-all"] = function() { return {}; };
env["request-args-all"] = function() { return {}; };
env["request-content-type"] = function() { return "text/html"; };

// Platform test functions
env["try-call"] = function(thunk) {
  try {
    Sx.eval([thunk], env);
    return { ok: true };
  } catch (e) {
    return { ok: false, error: e.message || String(e) };
  }
};

env["report-pass"] = function(name) {
  passCount++;
  const ctx = suiteStack.join(" > ");
  console.log(`  PASS: ${ctx} > ${name}`);
  return null;
};

env["report-fail"] = function(name, error) {
  failCount++;
  const ctx = suiteStack.join(" > ");
  console.log(`  FAIL: ${ctx} > ${name}: ${error}`);
  return null;
};

env["push-suite"] = function(name) {
  suiteStack.push(name);
  console.log(`${"  ".repeat(suiteStack.length - 1)}Suite: ${name}`);
  return null;
};

env["pop-suite"] = function() {
  suiteStack.pop();
  return null;
};

env["test-allowed?"] = function(name) { return true; };

// Load test framework
const projectDir = path.join(__dirname, "..", "..");
const specTests = path.join(projectDir, "spec", "tests");
const libTests = path.join(projectDir, "lib", "tests");
const webTests = path.join(projectDir, "web", "tests");

const frameworkSrc = fs.readFileSync(path.join(specTests, "test-framework.sx"), "utf8");
const frameworkExprs = Sx.parse(frameworkSrc);
for (const expr of frameworkExprs) {
  Sx.eval(expr, env);
}

// Load test harness (mock IO platform)
const harnessPath = path.join(projectDir, "spec", "harness.sx");
if (fs.existsSync(harnessPath)) {
  const harnessSrc = fs.readFileSync(harnessPath, "utf8");
  const harnessExprs = Sx.parse(harnessSrc);
  for (const expr of harnessExprs) {
    try { Sx.eval(expr, env); } catch (e) {
      console.error(`Error loading harness.sx: ${e.message}`);
    }
  }
}

// Load canonical.sx (content-addressing, serialization)
const canonicalPath = path.join(projectDir, "spec", "canonical.sx");
if (fs.existsSync(canonicalPath)) {
  const canonicalSrc = fs.readFileSync(canonicalPath, "utf8");
  const canonicalExprs = Sx.parse(canonicalSrc);
  for (const expr of canonicalExprs) {
    try { Sx.eval(expr, env); } catch (e) {
      console.error(`Error loading canonical.sx: ${e.message}`);
    }
  }
}

// Load sx-swap.sx (needed by spec/tests/test-sx-swap.sx)
const swapPath = path.join(projectDir, "lib", "sx-swap.sx");
if (fs.existsSync(swapPath)) {
  const swapSrc = fs.readFileSync(swapPath, "utf8");
  const swapExprs = Sx.parse(swapSrc);
  for (const expr of swapExprs) {
    try { Sx.eval(expr, env); } catch (e) {
      console.error(`Error loading sx-swap.sx: ${e.message}`);
    }
  }
}

// Load spec library files (define-library modules imported by tests)
for (const libFile of ["stdlib.sx", "signals.sx", "coroutines.sx"]) {
  const libPath = path.join(projectDir, "spec", libFile);
  if (fs.existsSync(libPath)) {
    const libSrc = fs.readFileSync(libPath, "utf8");
    const libExprs = Sx.parse(libSrc);
    for (const expr of libExprs) {
      try { Sx.eval(expr, env); } catch (e) {
        console.error(`Error loading spec/${libFile}: ${e.message}`);
      }
    }
  }
}

// Load tw system (needed by spec/tests/test-tw.sx)
const twDir = path.join(projectDir, "shared", "sx", "templates");
for (const twFile of ["tw-type.sx", "tw-layout.sx", "tw.sx"]) {
  const twPath = path.join(twDir, twFile);
  if (fs.existsSync(twPath)) {
    const twSrc = fs.readFileSync(twPath, "utf8");
    const twExprs = Sx.parse(twSrc);
    for (const expr of twExprs) {
      try { Sx.eval(expr, env); } catch (e) {
        console.error(`Error loading ${twFile}: ${e.message}`);
      }
    }
  }
}

// Load compiler + VM from lib/ when running full tests
if (fullBuild) {
  const libDir = path.join(projectDir, "lib");
  for (const libFile of ["bytecode.sx", "compiler.sx", "vm.sx", "tree-tools.sx"]) {
    const libPath = path.join(libDir, libFile);
    if (fs.existsSync(libPath)) {
      const src = fs.readFileSync(libPath, "utf8");
      const exprs = Sx.parse(src);
      for (const expr of exprs) {
        try { Sx.eval(expr, env); } catch (e) {
          console.error(`Error loading ${libFile}: ${e.message}`);
        }
      }
    }
  }
  // Load web harnesses (DOM mocking, signals, rendering awareness)
  const webDir = path.join(projectDir, "web");
  for (const webFile of ["harness-web.sx", "harness-reactive.sx"]) {
    const wp = path.join(webDir, webFile);
    if (fs.existsSync(wp)) {
      const src = fs.readFileSync(wp, "utf8");
      const exprs = Sx.parse(src);
      for (const expr of exprs) {
        try { Sx.eval(expr, env); } catch (e) {
          console.error(`Error loading ${webFile}: ${e.message}`);
        }
      }
    }
  }
  // Load stepper-lib (shared stepper functions used by lib/tests/test-stepper.sx)
  const stepperLibPath = path.join(projectDir, "sx", "sx", "stepper-lib.sx");
  if (fs.existsSync(stepperLibPath)) {
    const src = fs.readFileSync(stepperLibPath, "utf8");
    const exprs = Sx.parse(src);
    for (const expr of exprs) {
      try { Sx.eval(expr, env); } catch (e) {
        console.error(`Error loading stepper-lib.sx: ${e.message}`);
      }
    }
  }
}

// Determine which tests to run
const args = process.argv.slice(2).filter(a => !a.startsWith("--"));
let testFiles = [];

if (args.length > 0) {
  // Specific test files — search spec, lib, and web test dirs
  for (const arg of args) {
    const name = arg.endsWith(".sx") ? arg : `${arg}.sx`;
    const specPath = path.join(specTests, name);
    const libPath = path.join(libTests, name);
    const webPath = path.join(webTests, name);
    if (fs.existsSync(specPath)) testFiles.push(specPath);
    else if (fs.existsSync(libPath)) testFiles.push(libPath);
    else if (fs.existsSync(webPath)) testFiles.push(webPath);
    else console.error(`Test file not found: ${name}`);
  }
} else {
  // All spec tests (core language — always run)
  for (const f of fs.readdirSync(specTests).sort()) {
    if (f.startsWith("test-") && f.endsWith(".sx") && f !== "test-framework.sx") {
      testFiles.push(path.join(specTests, f));
    }
  }
  // Library tests (only with --full — require compiler, vm, signals, etc.)
  if (fullBuild) {
    for (const f of fs.readdirSync(libTests).sort()) {
      if (f.startsWith("test-") && f.endsWith(".sx")) {
        testFiles.push(path.join(libTests, f));
      }
    }
  }
}

// Run tests
for (const testFile of testFiles) {
  const name = path.basename(testFile);
  console.log("=" .repeat(60));
  console.log(`Running ${name}`);
  console.log("=" .repeat(60));

  try {
    const src = fs.readFileSync(testFile, "utf8");
    const exprs = Sx.parse(src);
    for (const expr of exprs) {
      Sx.eval(expr, env);
    }
  } catch (e) {
    console.error(`ERROR in ${name}: ${e.message}`);
    failCount++;
  }
}

// Summary
console.log("=" .repeat(60));
console.log(`Results: ${passCount} passed, ${failCount} failed`);
console.log("=" .repeat(60));

process.exit(failCount > 0 ? 1 : 0);
