"""Test bootstrapper transpilation: js.sx and py.sx."""
from __future__ import annotations

import os
import re

import pytest
from shared.sx.parser import parse, parse_all
from shared.sx.ref.run_js_sx import compile_ref_to_js, load_js_sx
from shared.sx.ref.platform_js import (
    ADAPTER_FILES,
    SPEC_MODULES,
    extract_defines,
)
from shared.sx.ref.bootstrap_py import PyEmitter
from shared.sx.types import Symbol, Keyword


class TestJsSxTranslation:
    """js.sx self-hosting bootstrapper handles all SX constructs."""

    def _translate(self, sx_source: str) -> str:
        """Translate a single SX expression to JS using js.sx."""
        from shared.sx.ref.sx_ref import evaluate
        env = load_js_sx()
        expr = parse(sx_source)
        env["_def_expr"] = expr
        return evaluate(
            [Symbol("js-expr"), Symbol("_def_expr")], env
        )

    def test_simple_dict(self):
        result = self._translate('{"name" "hello"}')
        assert '"name"' in result
        assert '"hello"' in result

    def test_function_call_in_dict(self):
        result = self._translate('{"parsed" (parse-route-pattern (get page "path"))}')
        assert "parseRoutePattern" in result
        assert "Symbol" not in result


class TestPyEmitterNativeDict:
    """Python bootstrapper must handle native Python dicts from {:key val} syntax."""

    def test_simple_string_values(self):
        expr = parse('{"name" "hello"}')
        py = PyEmitter().emit(expr)
        assert py == "{'name': 'hello'}"

    def test_function_call_value(self):
        """Dict value containing a function call must emit the call, not raw AST."""
        expr = parse('{"parsed" (parse-route-pattern (get page "path"))}')
        py = PyEmitter().emit(expr)
        assert "parse_route_pattern" in py
        assert "Symbol" not in py

    def test_multiple_keys(self):
        expr = parse('{"a" 1 "b" (+ x 2)}')
        py = PyEmitter().emit(expr)
        assert "'a': 1" in py
        assert "'b': (x + 2)" in py


# ---------------------------------------------------------------------------
# Platform mapping and PRIMITIVES validation
#
# Catches two classes of bugs:
#   1. Spec defines missing from compiled JS: a function defined in an .sx
#      spec file doesn't appear in the compiled output (e.g. because the
#      spec module wasn't included).
#   2. Missing PRIMITIVES registration: a function is declared in
#      primitives.sx but not registered in PRIMITIVES[...], so runtime-
#      evaluated SX (island bodies) gets "Undefined symbol" errors.
# ---------------------------------------------------------------------------

_REF_DIR = os.path.join(os.path.dirname(__file__), "..", "ref")


class TestPlatformMapping:
    """Verify compiled JS output contains all spec-defined functions."""

    def test_compiled_defines_present_in_js(self):
        """Every top-level define from spec files must appear in compiled JS output."""
        js_output = compile_ref_to_js(
            spec_modules=list(SPEC_MODULES.keys()),
        )

        # Collect all var/function definitions from the JS
        defined_in_js = set(re.findall(r'\bvar\s+(\w+)\s*=', js_output))
        defined_in_js.update(re.findall(r'\bfunction\s+(\w+)\s*\(', js_output))

        all_defs: set[str] = set()
        for filename, _label in (
            [("eval.sx", "eval"), ("render.sx", "render")]
            + list(ADAPTER_FILES.values())
            + list(SPEC_MODULES.values())
        ):
            filepath = os.path.join(_REF_DIR, filename)
            if not os.path.exists(filepath):
                continue
            for name, _expr in extract_defines(open(filepath).read()):
                all_defs.add(name)

        # Use js.sx RENAMES to map SX names → JS names
        env = load_js_sx()
        renames = env.get("js-renames", {})

        missing = []
        for sx_name in sorted(all_defs):
            # Check if there's an explicit rename
            js_name = renames.get(sx_name)
            if js_name is None:
                # Auto-mangle: hyphens → camelCase, ! → _b, ? → _p
                js_name = _auto_mangle(sx_name)
            if js_name not in defined_in_js:
                missing.append(f"{sx_name} → {js_name}")

        if missing:
            pytest.fail(
                f"{len(missing)} spec definitions not found in compiled JS "
                f"(compile with all spec_modules):\n  "
                + "\n  ".join(missing)
            )


def _auto_mangle(name: str) -> str:
    """Approximate js.sx's auto-mangle for SX name → JS identifier."""
    # Remove ~, replace ?, !, -, *, >
    n = name
    if n.startswith("~"):
        n = n[1:]
    n = n.replace("?", "_p").replace("!", "_b")
    n = n.replace("->", "_to_").replace(">=", "_gte").replace("<=", "_lte")
    n = n.replace(">", "_gt").replace("<", "_lt")
    n = n.replace("*", "_star_").replace("/", "_slash_")
    # camelCase from hyphens
    parts = n.split("-")
    if len(parts) > 1:
        n = parts[0] + "".join(p.capitalize() for p in parts[1:])
    return n


class TestPrimitivesRegistration:
    """Functions callable from runtime-evaluated SX must be in PRIMITIVES[...]."""

    def test_declared_primitives_registered(self):
        """Every primitive declared in primitives.sx must have a PRIMITIVES[...] entry."""
        from shared.sx.ref.boundary_parser import parse_primitives_sx

        declared = parse_primitives_sx()

        js_output = compile_ref_to_js()
        registered = set(re.findall(r'PRIMITIVES\["([^"]+)"\]', js_output))

        # Aliases
        aliases = {
            "downcase": "lower",
            "upcase": "upper",
            "eq?": "=",
            "eqv?": "=",
            "equal?": "=",
        }
        for alias, canonical in aliases.items():
            if alias in declared and canonical in registered:
                declared = declared - {alias}

        # Extension-only primitives
        extension_only = {"continuation?"}
        declared = declared - extension_only

        missing = declared - registered
        if missing:
            pytest.fail(
                f"{len(missing)} primitives declared in primitives.sx but "
                f"not registered in PRIMITIVES[...]:\n  "
                + "\n  ".join(sorted(missing))
            )

    def test_signal_runtime_primitives_registered(self):
        """Signal/reactive functions used by island bodies must be in PRIMITIVES."""
        required = {
            "signal", "signal?", "deref", "reset!", "swap!",
            "computed", "effect", "batch", "resource",
            "def-store", "use-store", "emit-event", "on-event", "bridge-event",
            "promise-delayed", "promise-then",
            "dom-focus", "dom-tag-name", "dom-get-prop",
            "stop-propagation", "error-message", "schedule-idle",
            "set-interval", "clear-interval",
            "reactive-text", "create-text-node",
            "dom-set-text-content", "dom-listen", "dom-dispatch", "event-detail",
        }

        js_output = compile_ref_to_js()
        registered = set(re.findall(r'PRIMITIVES\["([^"]+)"\]', js_output))

        missing = required - registered
        if missing:
            pytest.fail(
                f"{len(missing)} signal/reactive primitives not registered "
                f"in PRIMITIVES[...]:\n  "
                + "\n  ".join(sorted(missing))
            )
