"""Test sx.js string renderer matches Python renderer output.

Runs sx.js through Node.js and compares output with Python.
"""
from __future__ import annotations

import json
import subprocess
from pathlib import Path

import pytest

from shared.sx.parser import parse, parse_all
from shared.sx.html import render as py_render
from shared.sx.ref.sx_ref import evaluate

SX_JS = Path(__file__).resolve().parents[2] / "static" / "scripts" / "sx.js"
SX_TEST_JS = Path(__file__).resolve().parents[2] / "static" / "scripts" / "sx-test.js"


def _js_render(sx_text: str, components_text: str = "") -> str:
    """Run sx.js + sx-test.js in Node and return the renderToString result."""
    import tempfile, os
    # Build a small Node script that requires the source files
    script = f"""
    globalThis.document = undefined;  // no DOM needed for string render
    // sx.js IIFE uses (typeof window !== "undefined" ? window : this).
    // In Node file mode, `this` is module.exports, not globalThis.
    // Patch: make the IIFE target globalThis so Sx is accessible.
    var _origThis = this;
    Object.defineProperty(globalThis, 'document', {{ value: undefined, writable: true }});
    (function() {{
      var _savedThis = globalThis;
      {SX_JS.read_text()}
      // Hoist Sx from module.exports to globalThis if needed
      if (typeof Sx === 'undefined' && typeof module !== 'undefined' && module.exports && module.exports.Sx) {{
        globalThis.Sx = module.exports.Sx;
      }}
    }}).call(globalThis);
    {SX_TEST_JS.read_text()}
    if ({json.dumps(components_text)}) Sx.loadComponents({json.dumps(components_text)});
    var result = Sx.renderToString({json.dumps(sx_text)});
    process.stdout.write(result);
    """
    # Write to temp file to avoid OS arg length limits
    fd, tmp = tempfile.mkstemp(suffix=".js")
    try:
        with os.fdopen(fd, "w") as f:
            f.write(script)
        result = subprocess.run(
            ["node", tmp],
            capture_output=True, text=True, timeout=5,
        )
    finally:
        os.unlink(tmp)
    if result.returncode != 0:
        pytest.fail(f"Node.js error:\n{result.stderr}")
    return result.stdout


class TestParserParity:
    """Parser produces equivalent structures."""

    def test_simple_element(self):
        assert _js_render('(div "hello")') == '<div>hello</div>'

    def test_nested_elements(self):
        html = _js_render('(div :class "card" (p "text"))')
        assert html == '<div class="card"><p>text</p></div>'

    def test_void_element(self):
        assert _js_render('(img :src "a.jpg")') == '<img src="a.jpg">'
        assert _js_render('(br)') == '<br>'

    def test_boolean_attr(self):
        assert _js_render('(input :disabled true :type "text")') == '<input disabled type="text">'

    def test_nil_attr_omitted(self):
        assert _js_render('(div :class nil "hi")') == '<div>hi</div>'

    def test_false_attr_omitted(self):
        assert _js_render('(div :class false "hi")') == '<div>hi</div>'

    def test_numbers(self):
        assert _js_render('(span 42)') == '<span>42</span>'

    def test_escaping(self):
        html = _js_render('(div "<script>alert(1)</script>")')
        assert "&lt;script&gt;" in html


class TestSpecialForms:
    """Special forms render correctly."""

    def test_if_true(self):
        assert _js_render('(if true (span "yes") (span "no"))') == '<span>yes</span>'

    def test_if_false(self):
        assert _js_render('(if false (span "yes") (span "no"))') == '<span>no</span>'

    def test_if_nil(self):
        assert _js_render('(if nil (span "yes") (span "no"))') == '<span>no</span>'

    def test_when_true(self):
        assert _js_render('(when true (span "yes"))') == '<span>yes</span>'

    def test_when_false(self):
        assert _js_render('(when false (span "yes"))') == ''

    def test_str(self):
        assert _js_render('(div (str "a" "b" "c"))') == '<div>abc</div>'

    def test_fragment(self):
        assert _js_render('(<> (span "a") (span "b"))') == '<span>a</span><span>b</span>'

    def test_let(self):
        assert _js_render('(let ((x "hello")) (div x))') == '<div>hello</div>'

    def test_let_clojure_style(self):
        assert _js_render('(let (x "hello" y "world") (div (str x " " y)))') == '<div>hello world</div>'

    def test_and(self):
        assert _js_render('(when (and true true) (span "ok"))') == '<span>ok</span>'
        assert _js_render('(when (and true false) (span "ok"))') == ''

    def test_or(self):
        assert _js_render('(div (or nil "fallback"))') == '<div>fallback</div>'


class TestComponents:
    """Component definition and rendering."""

    CARD = '(defcomp ~card (&key title) (div :class "card" (h2 title)))'

    def test_simple_component(self):
        html = _js_render('(~card :title "Hello")', self.CARD)
        assert html == '<div class="card"><h2>Hello</h2></div>'

    def test_component_with_children(self):
        comp = '(defcomp ~box (&key &rest children) (div :class "box" (raw! children)))'
        html = _js_render('(~box (p "inside"))', comp)
        assert html == '<div class="box"><p>inside</p></div>'

    def test_component_with_conditional(self):
        comp = '(defcomp ~shared:misc/badge (&key show label) (when show (span label)))'
        assert _js_render('(~shared:misc/badge :show true :label "ok")', comp) == '<span>ok</span>'
        assert _js_render('(~shared:misc/badge :show false :label "ok")', comp) == ''

    def test_nested_components(self):
        comps = """
        (defcomp ~inner (&key text) (span text))
        (defcomp ~outer (&key label) (div (~inner :text label)))
        """
        html = _js_render('(~outer :label "hi")', comps)
        assert html == '<div><span>hi</span></div>'


class TestClientComponentsTag:
    """client_components_tag() generates valid sx for JS consumption."""

    def test_emits_script_tag(self):
        from shared.sx.jinja_bridge import client_components_tag, register_components, _COMPONENT_ENV
        # Register a test component
        register_components('(defcomp ~test-cct (&key label) (span label))')
        try:
            tag = client_components_tag("test-cct")
            assert tag.startswith('<script type="text/sx" data-components>')
            assert tag.endswith('</script>')
            assert "defcomp ~test-cct" in tag
        finally:
            _COMPONENT_ENV.pop("~test-cct", None)

    def test_roundtrip_through_js(self):
        """Component emitted by client_components_tag renders identically in JS."""
        from shared.sx.jinja_bridge import client_components_tag, register_components, _COMPONENT_ENV
        register_components('(defcomp ~test-rt (&key title) (div :class "rt" title))')
        try:
            tag = client_components_tag("test-rt")
            # Extract the sx source from the script tag
            sx_source = tag.replace('<script type="text/sx" data-components>', '').replace('</script>', '')
            js_html = _js_render('(~test-rt :title "hello")', sx_source)
            py_html = py_render(parse('(~test-rt :title "hello")'), _COMPONENT_ENV)
            assert js_html == py_html
        finally:
            _COMPONENT_ENV.pop("~test-rt", None)


class TestPythonParity:
    """JS string renderer matches Python renderer output."""

    CASES = [
        '(div :class "main" (p "hello"))',
        '(div (if true "yes" "no"))',
        '(div (when false "hidden"))',
        '(span (str "a" "-" "b"))',
        '(<> (div "one") (div "two"))',
        '(ul (li "a") (li "b") (li "c"))',
        '(input :type "text" :disabled true :value "x")',
        '(div :class nil :id "ok" "text")',
        '(img :src "photo.jpg" :alt "A photo")',
        '(table (tr (td "cell")))',
    ]

    @pytest.mark.parametrize("sx_text", CASES)
    def test_matches_python(self, sx_text):
        py_html = py_render(parse(sx_text))
        js_html = _js_render(sx_text)
        assert js_html == py_html, f"Mismatch for {sx_text!r}:\n  PY: {py_html!r}\n  JS: {js_html!r}"

    COMP_CASES = [
        (
            '(defcomp ~tag (&key label colour) (span :class (str "tag-" colour) label))',
            '(~tag :label "new" :colour "red")',
        ),
        (
            '(defcomp ~wrap (&key &rest children) (div :class "w" (raw! children)))',
            '(~wrap (p "a") (p "b"))',
        ),
    ]

    @pytest.mark.parametrize("comp_text,call_text", COMP_CASES)
    def test_component_matches_python(self, comp_text, call_text):
        env = {}
        evaluate(parse(comp_text), env)
        py_html = py_render(parse(call_text), env)
        js_html = _js_render(call_text, comp_text)
        assert js_html == py_html

    MAP_CASES = [
        # map with lambda returning HTML element
        (
            "",
            '(ul (map (lambda (x) (li x)) ("a" "b" "c")))',
        ),
        # map with lambda returning component
        (
            '(defcomp ~item (&key name) (span :class "item" name))',
            '(div (map (lambda (t) (~item :name (get t "name"))) ({"name" "Alice"} {"name" "Bob"})))',
        ),
        # map-indexed with lambda
        (
            "",
            '(ul (map-indexed (lambda (i x) (li (str i ". " x))) ("foo" "bar")))',
        ),
    ]

    @pytest.mark.parametrize("comp_text,call_text", MAP_CASES)
    def test_map_lambda_render(self, comp_text, call_text):
        env = {}
        if comp_text:
            for expr in parse_all(comp_text):
                evaluate(expr, env)
        py_html = py_render(parse(call_text), env)
        js_html = _js_render(call_text, comp_text)
        assert js_html == py_html, f"Mismatch:\n  PY: {py_html!r}\n  JS: {js_html!r}"
