/**
 * sx-test.js — String renderer for sx.js (Node-only, used by test harness).
 *
 * Provides Sx.renderToString() for server-side / test rendering.
 * Assumes sx.js is loaded first and Sx global is available.
 */
;(function (Sx) {
  "use strict";

  // Pull references from Sx internals
  var NIL = Sx.NIL;
  var _eval = Sx._eval;
  var _types = Sx._types;
  var RawHTML = _types.RawHTML;

  function isNil(x) { return x === NIL || x === null || x === undefined; }
  function isSym(x) { return x && x._sym === true; }
  function isKw(x) { return x && x._kw === true; }
  function isLambda(x) { return x && x._lambda === true; }
  function isComponent(x) { return x && x._component === true; }
  function isMacro(x) { return x && x._macro === true; }
  function isRaw(x) { return x && x._raw === true; }
  function isSxTruthy(x) { return x !== false && !isNil(x); }

  function merge(target) {
    for (var i = 1; i < arguments.length; i++) {
      var src = arguments[i];
      if (src) for (var k in src) target[k] = src[k];
    }
    return target;
  }

  // Use the same tag/attr sets as sx.js
  var HTML_TAGS = Sx._renderDOM ? null : null; // We'll use a local copy
  var _HTML_TAGS_STR =
    "html head body title meta link style script base noscript " +
    "header footer main nav aside section article address hgroup " +
    "h1 h2 h3 h4 h5 h6 " +
    "div p blockquote pre figure figcaption ul ol li dl dt dd hr " +
    "a span em strong small s cite q abbr code var samp kbd sub sup " +
    "i b u mark ruby rt rp bdi bdo br wbr time data " +
    "ins del " +
    "img picture source iframe embed object param video audio track canvas map area " +
    "table caption colgroup col thead tbody tfoot tr td th " +
    "form input textarea button select option optgroup label fieldset legend " +
    "details summary dialog " +
    "svg path circle rect line ellipse polyline polygon text g defs use " +
    "clippath lineargradient radialgradient stop pattern mask " +
    "tspan textpath foreignobject";
  var _VOID_STR = "area base br col embed hr img input link meta param source track wbr";
  var _BOOL_STR = "disabled checked readonly required selected autofocus autoplay " +
    "controls loop muted multiple hidden open novalidate";

  function makeSet(str) {
    var s = {}, parts = str.split(/\s+/);
    for (var i = 0; i < parts.length; i++) if (parts[i]) s[parts[i]] = true;
    return s;
  }

  HTML_TAGS = makeSet(_HTML_TAGS_STR);
  var VOID_ELEMENTS = makeSet(_VOID_STR);
  var BOOLEAN_ATTRS = makeSet(_BOOL_STR);

  // Access expandMacro via Sx._eval on a defmacro — we need to replicate macro expansion
  // Actually, we need the internal expandMacro. Let's check if Sx exposes it.
  // Sx._eval handles macro expansion internally, so we can call sxEval for macro forms.
  var sxEval = _eval;

  // _isRenderExpr — check if an expression is a render-only form
  function _isRenderExpr(v) {
    if (!Array.isArray(v) || !v.length) return false;
    var h = v[0];
    if (!isSym(h)) return false;
    var n = h.name;
    if (n === "<>" || n === "raw!" || n === "if" || n === "when" || n === "cond" ||
        n === "case" || n === "let" || n === "let*" || n === "begin" || n === "do" ||
        n === "map" || n === "map-indexed" || n === "filter" || n === "for-each") return true;
    if (n.charAt(0) === "~") return true;
    if (HTML_TAGS[n]) return true;
    return false;
  }

  // --- String Renderer ---

  function escapeText(s) { return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); }
  function escapeAttr(s) { return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); }

  function renderStr(expr, env) {
    if (isNil(expr) || expr === false || expr === true) return "";
    if (isRaw(expr)) return expr.html;
    if (typeof expr === "string") return escapeText(expr);
    if (typeof expr === "number") return escapeText(String(expr));
    if (isSym(expr)) return renderStr(sxEval(expr, env), env);
    if (isKw(expr)) return escapeText(expr.name);
    if (Array.isArray(expr)) { if (!expr.length) return ""; return renderStrList(expr, env); }
    if (expr && typeof expr === "object") return "";
    return escapeText(String(expr));
  }

  function renderStrList(expr, env) {
    var head = expr[0];
    if (!isSym(head)) {
      var parts = [];
      for (var i = 0; i < expr.length; i++) parts.push(renderStr(expr[i], env));
      return parts.join("");
    }
    var name = head.name;

    if (name === "raw!") {
      var ps = [];
      for (var ri = 1; ri < expr.length; ri++) {
        var v = sxEval(expr[ri], env);
        if (isRaw(v)) ps.push(v.html);
        else if (typeof v === "string") ps.push(v);
        else if (!isNil(v)) ps.push(String(v));
      }
      return ps.join("");
    }
    if (name === "<>") {
      var fs = [];
      for (var fi = 1; fi < expr.length; fi++) fs.push(renderStr(expr[fi], env));
      return fs.join("");
    }
    if (name === "if") {
      return isSxTruthy(sxEval(expr[1], env))
        ? renderStr(expr[2], env)
        : (expr.length > 3 ? renderStr(expr[3], env) : "");
    }
    if (name === "when") {
      if (!isSxTruthy(sxEval(expr[1], env))) return "";
      var ws = [];
      for (var wi = 2; wi < expr.length; wi++) ws.push(renderStr(expr[wi], env));
      return ws.join("");
    }
    if (name === "let" || name === "let*") {
      var bindings = expr[1], local = merge({}, env);
      if (Array.isArray(bindings)) {
        if (bindings.length && Array.isArray(bindings[0])) {
          for (var li = 0; li < bindings.length; li++) {
            local[isSym(bindings[li][0]) ? bindings[li][0].name : bindings[li][0]] = sxEval(bindings[li][1], local);
          }
        } else {
          for (var lj = 0; lj < bindings.length; lj += 2) {
            local[isSym(bindings[lj]) ? bindings[lj].name : bindings[lj]] = sxEval(bindings[lj + 1], local);
          }
        }
      }
      var ls = [];
      for (var lk = 2; lk < expr.length; lk++) ls.push(renderStr(expr[lk], local));
      return ls.join("");
    }
    if (name === "begin" || name === "do") {
      var bs = [];
      for (var bi = 1; bi < expr.length; bi++) bs.push(renderStr(expr[bi], env));
      return bs.join("");
    }
    if (name === "define" || name === "defcomp" || name === "defmacro" || name === "defhandler") { sxEval(expr, env); return ""; }

    // Macro expansion in string renderer
    if (name in env && isMacro(env[name])) {
      var smExp = Sx._expandMacro(env[name], expr.slice(1), env);
      return renderStr(smExp, env);
    }

    // Higher-order forms — render-aware
    if (name === "map") {
      var mapFn = sxEval(expr[1], env), mapColl = sxEval(expr[2], env);
      if (!Array.isArray(mapColl)) return "";
      var mapParts = [];
      for (var mi = 0; mi < mapColl.length; mi++) {
        if (isLambda(mapFn)) mapParts.push(renderLambdaStr(mapFn, [mapColl[mi]], env));
        else mapParts.push(renderStr(mapFn(mapColl[mi]), env));
      }
      return mapParts.join("");
    }
    if (name === "map-indexed") {
      var mixFn = sxEval(expr[1], env), mixColl = sxEval(expr[2], env);
      if (!Array.isArray(mixColl)) return "";
      var mixParts = [];
      for (var mxi = 0; mxi < mixColl.length; mxi++) {
        if (isLambda(mixFn)) mixParts.push(renderLambdaStr(mixFn, [mxi, mixColl[mxi]], env));
        else mixParts.push(renderStr(mixFn(mxi, mixColl[mxi]), env));
      }
      return mixParts.join("");
    }
    if (name === "filter") {
      var filtFn = sxEval(expr[1], env), filtColl = sxEval(expr[2], env);
      if (!Array.isArray(filtColl)) return "";
      var filtParts = [];
      for (var fli = 0; fli < filtColl.length; fli++) {
        var keep = isLambda(filtFn) ? Sx._callLambda(filtFn, [filtColl[fli]], env) : filtFn(filtColl[fli]);
        if (isSxTruthy(keep)) filtParts.push(renderStr(filtColl[fli], env));
      }
      return filtParts.join("");
    }

    if (HTML_TAGS[name]) return renderStrElement(name, expr.slice(1), env);

    if (name.charAt(0) === "~") {
      var comp = env[name];
      if (isComponent(comp)) return renderStrComponent(comp, expr.slice(1), env);
      console.warn("sx.js: unknown component " + name);
      return '<div style="background:#fef2f2;border:1px solid #fca5a5;color:#991b1b;' +
        'padding:4px 8px;margin:2px;border-radius:4px;font-size:12px;font-family:monospace">' +
        'Unknown component: ' + escapeText(name) + '</div>';
    }

    return renderStr(sxEval(expr, env), env);
  }

  function renderStrElement(tag, args, env) {
    var attrs = [], children = [];
    var i = 0;
    while (i < args.length) {
      if (isKw(args[i]) && i + 1 < args.length) {
        var aname = args[i].name, aval = sxEval(args[i + 1], env);
        i += 2;
        if (isNil(aval) || aval === false) continue;
        if (BOOLEAN_ATTRS[aname]) { if (aval) attrs.push(" " + aname); }
        else if (aval === true) attrs.push(" " + aname);
        else attrs.push(" " + aname + '="' + escapeAttr(String(aval)) + '"');
      } else {
        children.push(args[i]);
        i++;
      }
    }
    var open = "<" + tag + attrs.join("") + ">";
    if (VOID_ELEMENTS[tag]) return open;
    var isRawText = (tag === "script" || tag === "style");
    var inner = [];
    for (var ci = 0; ci < children.length; ci++) {
      var child = children[ci];
      if (isRawText && typeof child === "string") inner.push(child);
      else if (isRawText && isSym(child)) inner.push(String(sxEval(child, env)));
      else inner.push(renderStr(child, env));
    }
    return open + inner.join("") + "</" + tag + ">";
  }

  function renderLambdaStr(fn, args, env) {
    var local = merge({}, fn.closure, env);
    for (var i = 0; i < fn.params.length; i++) local[fn.params[i]] = args[i];
    return renderStr(fn.body, local);
  }

  function renderStrComponent(comp, args, env) {
    var kwargs = {}, children = [];
    var i = 0;
    while (i < args.length) {
      if (isKw(args[i]) && i + 1 < args.length) {
        var v = args[i + 1];
        if (typeof v === "string" || typeof v === "number" ||
          typeof v === "boolean" || isNil(v) || isKw(v)) {
          kwargs[args[i].name] = v;
        } else if (isSym(v)) {
          kwargs[args[i].name] = sxEval(v, env);
        } else if (Array.isArray(v) && v.length && isSym(v[0])) {
          if (_isRenderExpr(v)) {
            kwargs[args[i].name] = new RawHTML(renderStr(v, env));
          } else {
            kwargs[args[i].name] = sxEval(v, env);
          }
        } else {
          kwargs[args[i].name] = v;
        }
        i += 2;
      } else { children.push(args[i]); i++; }
    }
    var local = merge({}, comp.closure, env);
    for (var pi = 0; pi < comp.params.length; pi++) {
      var p = comp.params[pi];
      local[p] = (p in kwargs) ? kwargs[p] : NIL;
    }
    if (comp.hasChildren) {
      var cs = [];
      for (var ci = 0; ci < children.length; ci++) cs.push(renderStr(children[ci], env));
      local["children"] = new RawHTML(cs.join(""));
    }
    return renderStr(comp.body, local);
  }

  // --- Public API ---

  Sx.renderToString = function (exprOrText, extraEnv) {
    var expr = typeof exprOrText === "string" ? Sx.parse(exprOrText) : exprOrText;
    var env = extraEnv ? merge({}, Sx.getEnv(), extraEnv) : Sx.getEnv();
    return renderStr(expr, env);
  };

  Sx._renderStr = renderStr;

})(Sx);
