"""Tests exposing bugs after sx_ref.py removal.

These tests document all known breakages from removing the Python SX evaluator.
Each test targets a specific codepath that was depending on sx_ref.py and is now
broken.

Usage:
    pytest shared/sx/tests/test_post_removal_bugs.py -v
"""

import asyncio
import os
import sys
import unittest

_project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))
if _project_root not in sys.path:
    sys.path.insert(0, _project_root)

from shared.sx.parser import parse, parse_all, serialize
from shared.sx.types import Component, Symbol, Keyword, NIL


# ---------------------------------------------------------------------------
# Helper: load shared components fresh (no cache)
# ---------------------------------------------------------------------------

def _load_components_fresh():
    """Load shared components, clearing cache to force re-parse."""
    from shared.sx.jinja_bridge import _COMPONENT_ENV
    _COMPONENT_ENV.clear()
    from shared.sx.components import load_shared_components
    load_shared_components()
    return _COMPONENT_ENV


# ===========================================================================
# 1. register_components() loses all parameter information
# ===========================================================================

class TestComponentRegistration(unittest.TestCase):
    """register_components() hardcodes params=[] and has_children=False
    for every component, losing all parameter metadata."""

    @classmethod
    def setUpClass(cls):
        cls.env = _load_components_fresh()

    def test_shell_component_should_have_params(self):
        """~shared:shell/sx-page-shell has 17+ &key params but gets params=[]."""
        comp = self.env.get("~shared:shell/sx-page-shell")
        self.assertIsNotNone(comp, "Shell component not found")
        self.assertIsInstance(comp, Component)
        # BUG: params is [] — should include title, meta-html, csrf, etc.
        self.assertGreater(
            len(comp.params), 0,
            f"Shell component has params={comp.params} — expected 17+ keyword params"
        )

    def test_cssx_tw_should_have_tokens_param(self):
        """~cssx/tw needs a 'tokens' parameter."""
        comp = self.env.get("~cssx/tw")
        self.assertIsNotNone(comp, "~cssx/tw component not found")
        self.assertIn(
            "tokens", comp.params,
            f"~cssx/tw has params={comp.params} — expected 'tokens'"
        )

    def test_cart_mini_should_have_params(self):
        """~shared:fragments/cart-mini has &key params."""
        comp = self.env.get("~shared:fragments/cart-mini")
        self.assertIsNotNone(comp, "cart-mini component not found")
        self.assertGreater(
            len(comp.params), 0,
            f"cart-mini has params={comp.params} — expected keyword params"
        )

    def test_has_children_flag(self):
        """Components with &rest children should have has_children=True."""
        comp = self.env.get("~shared:shell/sx-page-shell")
        self.assertIsNotNone(comp)
        # Many components accept children but has_children is always False
        # Check any component that is known to accept &rest children
        # e.g. a layout component
        for name, val in self.env.items():
            if isinstance(val, Component):
                # Every component has has_children=False — at least some should be True
                pass
        # Count how many have has_children=True
        with_children = sum(
            1 for v in self.env.values()
            if isinstance(v, Component) and v.has_children
        )
        total = sum(1 for v in self.env.values() if isinstance(v, Component))
        # BUG: with_children is 0 — at least some components accept children
        self.assertGreater(
            with_children, 0,
            f"0/{total} components have has_children=True — at least some should"
        )

    def test_all_components_have_empty_params(self):
        """Show the scale of the bug — every single component has params=[]."""
        components_with_params = []
        components_without = []
        for name, val in self.env.items():
            if isinstance(val, Component):
                if val.params:
                    components_with_params.append(name)
                else:
                    components_without.append(name)
        # BUG: ALL components have empty params
        self.assertGreater(
            len(components_with_params), 0,
            f"ALL {len(components_without)} components have params=[] — none have parameters parsed"
        )


# ===========================================================================
# 2. Sync html.py rendering is completely broken
# ===========================================================================

class TestSyncHtmlRendering(unittest.TestCase):
    """html.py render() stubs _raw_eval/_trampoline — any evaluation crashes."""

    def test_html_render_simple_element(self):
        """Even simple elements with keyword attrs need _eval, which is stubbed."""
        from shared.sx.html import render
        # This should work — (div "hello") needs no eval
        result = render(parse('(div "hello")'), {})
        self.assertIn("hello", result)

    def test_html_render_with_keyword_attr(self):
        """Keyword attrs go through _eval, which raises RuntimeError."""
        from shared.sx.html import render
        try:
            result = render(parse('(div :class "test" "hello")'), {})
            # If it works, great
            self.assertIn("test", result)
        except RuntimeError as e:
            self.assertIn("sx_ref.py has been removed", str(e))
            self.fail(f"html.py render crashes on keyword attrs: {e}")

    def test_html_render_symbol_lookup(self):
        """Symbol lookup goes through _eval, which is stubbed."""
        from shared.sx.html import render
        try:
            result = render(parse('(div title)'), {"title": "Hello"})
            self.assertIn("Hello", result)
        except RuntimeError as e:
            self.assertIn("sx_ref.py has been removed", str(e))
            self.fail(f"html.py render crashes on symbol lookup: {e}")

    def test_html_render_component(self):
        """Component rendering needs _eval for kwarg evaluation."""
        from shared.sx.html import render
        env = _load_components_fresh()
        try:
            result = render(
                parse('(~shared:fragments/cart-mini :cart-count 0 :blog-url "" :cart-url "")'),
                env,
            )
            self.assertIn("cart-mini", result)
        except RuntimeError as e:
            self.assertIn("sx_ref.py has been removed", str(e))
            self.fail(f"html.py render crashes on component calls: {e}")

    def test_sx_jinja_function_broken(self):
        """The sx() Jinja helper is broken — it uses html_render internally."""
        from shared.sx.jinja_bridge import sx
        env = _load_components_fresh()
        try:
            result = sx('(div "hello")')
            self.assertIn("hello", result)
        except RuntimeError as e:
            self.assertIn("sx_ref.py has been removed", str(e))
            self.fail(f"sx() Jinja function is broken: {e}")


# ===========================================================================
# 3. Async render_to_html uses Python path, not OCaml
# ===========================================================================

class TestAsyncRenderToHtml(unittest.IsolatedAsyncioTestCase):
    """helpers.py render_to_html() deliberately uses Python async_eval,
    not the OCaml bridge. But Python eval is now broken."""

    async def test_render_to_html_uses_python_path(self):
        """render_to_html goes through async_render, not OCaml bridge."""
        from shared.sx.helpers import render_to_html
        env = _load_components_fresh()
        # The shell component has many &key params — none are bound because params=[]
        try:
            html = await render_to_html(
                "shared:shell/sx-page-shell",
                title="Test", csrf="abc", asset_url="/static",
                sx_js_hash="abc123",
            )
            self.assertIn("Test", html)
        except Exception as e:
            # Expected: either RuntimeError from stubs or EvalError from undefined symbols
            self.fail(
                f"render_to_html (Python path) failed: {type(e).__name__}: {e}\n"
                f"This should go through OCaml bridge instead"
            )

    async def test_async_render_component_no_params_bound(self):
        """async_eval.py _arender_component can't bind params because comp.params=[]."""
        from shared.sx.async_eval import async_render
        from shared.sx.primitives_io import RequestContext
        env = _load_components_fresh()
        # Create a simple component manually with correct params
        test_comp = Component(
            name="test/greeting",
            params=["name"],
            has_children=False,
            body=parse('(div (str "Hello " name))'),
        )
        env["~test/greeting"] = test_comp
        try:
            result = await async_render(
                parse('(~test/greeting :name "World")'),
                env,
                RequestContext(),
            )
            self.assertIn("Hello World", result)
        except Exception as e:
            self.fail(
                f"async_render failed even with correct params: {type(e).__name__}: {e}"
            )


# ===========================================================================
# 4. Dead imports from removed sx_ref.py
# ===========================================================================

class TestDeadImports(unittest.TestCase):
    """Files that import from sx_ref.py will crash when their codepaths execute."""

    def test_async_eval_defcomp(self):
        """async_eval.py _asf_defcomp should work as a stub (no sx_ref import)."""
        from shared.sx.async_eval import _asf_defcomp
        env = {}
        asyncio.run(_asf_defcomp(
            [Symbol("defcomp"), Symbol("~test"), [], [Symbol("div")]],
            env, None
        ))
        # Should register a minimal component in env
        self.assertIn("~test", env)

    def test_async_eval_defmacro(self):
        """async_eval.py _asf_defmacro should work as a stub (no sx_ref import)."""
        from shared.sx.async_eval import _asf_defmacro
        env = {}
        asyncio.run(_asf_defmacro(
            [Symbol("defmacro"), Symbol("test"), [], [Symbol("div")]],
            env, None
        ))
        self.assertIn("test", env)

    def test_async_eval_defstyle(self):
        """async_eval.py _asf_defstyle should be a no-op (no sx_ref import)."""
        from shared.sx.async_eval import _asf_defstyle
        result = asyncio.run(_asf_defstyle(
            [Symbol("defstyle"), Symbol("test"), [], [Symbol("div")]],
            {}, None
        ))
        # Should return NIL without crashing
        self.assertIsNotNone(result)

    def test_async_eval_defhandler(self):
        """async_eval.py _asf_defhandler should be a no-op (no sx_ref import)."""
        from shared.sx.async_eval import _asf_defhandler
        result = asyncio.run(_asf_defhandler(
            [Symbol("defhandler"), Symbol("test"), [], [Symbol("div")]],
            {}, None
        ))
        self.assertIsNotNone(result)

    def test_async_eval_continuation_reset(self):
        """async_eval.py _asf_reset imports eval_expr/trampoline from sx_ref."""
        # The cont_fn inside _asf_reset will crash when invoked
        from shared.sx.async_eval import _ASYNC_RENDER_FORMS
        reset_fn = _ASYNC_RENDER_FORMS.get("reset")
        # reset is defined in async_eval — the import is deferred to execution
        # Just verify the module doesn't have the import available
        try:
            from shared.sx.ref.sx_ref import eval_expr
            self.fail("sx_ref.py should not exist")
        except (ImportError, ModuleNotFoundError):
            pass  # Expected

    def test_ocaml_bridge_jit_compile(self):
        """ocaml_bridge.py _compile_adapter_module imports from sx_ref."""
        try:
            from shared.sx.ref.sx_ref import eval_expr, trampoline, PRIMITIVES
            self.fail("sx_ref.py should not exist — JIT compilation path is broken")
        except (ImportError, ModuleNotFoundError):
            pass  # Expected — confirms the bug

    def test_parser_reader_macro(self):
        """parser.py _try_reader_macro imports trampoline/call_lambda from sx_ref."""
        try:
            from shared.sx.ref.sx_ref import trampoline, call_lambda
            self.fail("sx_ref.py should not exist — reader macros are broken")
        except (ImportError, ModuleNotFoundError):
            pass  # Expected — confirms the bug

    def test_primitives_scope_prims(self):
        """primitives.py _lazy_scope_primitives silently fails to load scope prims."""
        from shared.sx.primitives import _PRIMITIVES
        # collect!, collected, clear-collected!, emitted, emit!, context
        # These are needed for CSSX but the import from sx_ref silently fails
        missing = []
        for name in ("collect!", "collected", "clear-collected!", "emitted", "emit!", "context"):
            if name not in _PRIMITIVES:
                missing.append(name)
        if missing:
            self.fail(
                f"Scope primitives missing from _PRIMITIVES (sx_ref import failed silently): {missing}\n"
                f"CSSX components depend on these for collect!/collected"
            )

    def test_deps_transitive_deps_ref_path(self):
        """deps.py transitive_deps imports from sx_ref when SX_USE_REF=1."""
        # The fallback path should still work
        from shared.sx.deps import transitive_deps
        env = _load_components_fresh()
        # Should work via fallback, not crash
        try:
            result = transitive_deps("~cssx/tw", env)
            self.assertIsInstance(result, set)
        except (ImportError, ModuleNotFoundError) as e:
            self.fail(f"transitive_deps crashed: {e}")

    def test_handlers_python_fallback(self):
        """handlers.py eval_handler Python fallback imports async_eval_ref."""
        # When not using OCaml, handler evaluation falls through to async_eval
        # The ref path (SX_USE_REF=1) would crash
        try:
            from shared.sx.ref.async_eval_ref import async_eval_to_sx
            self.fail("async_eval_ref.py should not exist")
        except (ImportError, ModuleNotFoundError):
            pass  # Expected


# ===========================================================================
# 5. ~cssx/tw signature mismatch
# ===========================================================================

class TestCssxTwSignature(unittest.TestCase):
    """~cssx/tw changed from (&key tokens) to (tokens) positional,
    but callers use :tokens keyword syntax."""

    @classmethod
    def setUpClass(cls):
        cls.env = _load_components_fresh()

    def test_cssx_tw_source_uses_positional(self):
        """Verify the current source has positional (tokens) not (&key tokens)."""
        import os
        cssx_path = os.path.join(
            os.path.dirname(__file__), "..", "templates", "cssx.sx"
        )
        with open(cssx_path) as f:
            source = f.read()
        # Check if it's positional or keyword
        if "(defcomp ~cssx/tw (tokens)" in source:
            # Positional — callers using :tokens will break
            self.fail(
                "~cssx/tw uses positional (tokens) but callers use :tokens keyword syntax.\n"
                "Should be: (defcomp ~cssx/tw (&key tokens) ...)"
            )
        elif "(defcomp ~cssx/tw (&key tokens)" in source:
            pass  # Correct
        else:
            # Unknown signature
            for line in source.split("\n"):
                if "defcomp ~cssx/tw" in line:
                    self.fail(f"Unexpected ~cssx/tw signature: {line.strip()}")

    def test_cssx_tw_callers_use_keyword(self):
        """Scan for callers that use :tokens keyword syntax."""
        import glob as glob_mod
        sx_dir = os.path.join(os.path.dirname(__file__), "../../..")
        keyword_callers = []
        positional_callers = []
        for fp in glob_mod.glob(os.path.join(sx_dir, "**/*.sx"), recursive=True):
            try:
                with open(fp) as f:
                    content = f.read()
            except Exception:
                continue
            if "~cssx/tw" not in content:
                continue
            for line_no, line in enumerate(content.split("\n"), 1):
                if "~cssx/tw" in line and "defcomp" not in line:
                    if ":tokens" in line:
                        keyword_callers.append(f"{fp}:{line_no}")
                    elif "(~cssx/tw " in line:
                        positional_callers.append(f"{fp}:{line_no}")

        if keyword_callers:
            # If signature is positional but callers use :tokens, that's a bug
            import os as os_mod
            cssx_path = os.path.join(
                os.path.dirname(__file__), "..", "templates", "cssx.sx"
            )
            with open(cssx_path) as f:
                source = f.read()
            if "(defcomp ~cssx/tw (tokens)" in source:
                self.fail(
                    f"~cssx/tw uses positional params but {len(keyword_callers)} callers use :tokens:\n"
                    + "\n".join(keyword_callers[:5])
                )


# ===========================================================================
# 6. OCaml bridge rendering (should work — this is the good path)
# ===========================================================================

class TestOcamlBridgeRendering(unittest.IsolatedAsyncioTestCase):
    """The OCaml bridge should handle all rendering correctly."""

    @classmethod
    def setUpClass(cls):
        from shared.sx.ocaml_bridge import _DEFAULT_BIN
        bin_path = os.path.abspath(_DEFAULT_BIN)
        if not os.path.isfile(bin_path):
            raise unittest.SkipTest("OCaml binary not found")

    async def asyncSetUp(self):
        from shared.sx.ocaml_bridge import OcamlBridge
        self.bridge = OcamlBridge()
        await self.bridge.start()

    async def asyncTearDown(self):
        if hasattr(self, 'bridge'):
            await self.bridge.stop()

    async def test_simple_element(self):
        result = await self.bridge.render('(div "hello")')
        self.assertIn("hello", result)

    async def test_element_with_keyword_attrs(self):
        result = await self.bridge.render('(div :class "test" "hello")')
        self.assertIn('class="test"', result)
        self.assertIn("hello", result)

    async def test_component_with_params(self):
        """OCaml should handle component parameter binding correctly."""
        # Use load_source to define a component (bypasses _ensure_components lock)
        await self.bridge.load_source('(defcomp ~test/greet (&key name) (div (str "Hello " name)))')
        result = await self.bridge.render('(~test/greet :name "World")')
        self.assertIn("Hello World", result)

    async def test_let_binding(self):
        result = await self.bridge.render('(let ((x "hello")) (div x))')
        self.assertIn("hello", result)

    async def test_conditional(self):
        result = await self.bridge.render('(if true (div "yes") (div "no"))')
        self.assertIn("yes", result)
        self.assertNotIn("no", result)

    async def test_cssx_tw_keyword_call(self):
        """Test that ~cssx/tw works when called with :tokens keyword.
        Components are loaded by _ensure_components() automatically."""
        try:
            result = await self.bridge.render('(div (~cssx/tw :tokens "bg-red-500") "content")')
            # Should produce a spread with CSS class, not an error
            self.assertNotIn("error", result.lower())
        except Exception as e:
            self.fail(f"~cssx/tw :tokens keyword call failed: {e}")

    async def test_cssx_tw_positional_call(self):
        """Test that ~cssx/tw works when called positionally."""
        try:
            result = await self.bridge.render('(div (~cssx/tw "bg-red-500") "content")')
            self.assertNotIn("error", result.lower())
        except Exception as e:
            self.fail(f"~cssx/tw positional call failed: {e}")

    async def test_repeated_renders_dont_crash(self):
        """Verify OCaml bridge handles multiple sequential renders."""
        for i in range(5):
            result = await self.bridge.render(f'(div "iter-{i}")')
            self.assertIn(f"iter-{i}", result)


# ===========================================================================
# 7. Scope primitives missing (collect!, collected, etc.)
# ===========================================================================

class TestScopePrimitives(unittest.TestCase):
    """Scope primitives needed by CSSX are missing because the import
    from sx_ref.py silently fails."""

    def test_python_primitives_have_scope_ops(self):
        """Check that collect!/collected/etc. are in _PRIMITIVES."""
        from shared.sx.primitives import _PRIMITIVES
        required = ["collect!", "collected", "clear-collected!",
                     "emitted", "emit!", "context"]
        missing = [p for p in required if p not in _PRIMITIVES]
        if missing:
            self.fail(
                f"Missing Python-side scope primitives: {missing}\n"
                f"These were provided by sx_ref.py — need OCaml bridge or Python stubs"
            )


# ===========================================================================
# 8. Query executor fallback path
# ===========================================================================

class TestQueryExecutorFallback(unittest.TestCase):
    """query_executor.py imports async_eval for its fallback path."""

    def test_query_executor_import(self):
        """query_executor can be imported without crashing."""
        try:
            import shared.sx.query_executor
        except Exception as e:
            self.fail(f"query_executor import crashed: {e}")


# ===========================================================================
# 9. End-to-end: sx_page shell rendering
# ===========================================================================

class TestShellRendering(unittest.IsolatedAsyncioTestCase):
    """The shell template needs to render through some path that works."""

    async def test_sx_page_shell_via_python(self):
        """render_to_html('shared:shell/sx-page-shell', ...) uses Python path.
        This is the actual failure from the production error log."""
        from shared.sx.helpers import render_to_html
        _load_components_fresh()
        try:
            html = await render_to_html(
                "shared:shell/sx-page-shell",
                title="Test Page",
                csrf="test-csrf",
                asset_url="/static",
                sx_js_hash="abc",
            )
            # Should produce full HTML document
            self.assertIn("<!doctype html>", html.lower())
            self.assertIn("Test Page", html)
        except Exception as e:
            self.fail(
                f"Shell rendering via Python path failed: {type(e).__name__}: {e}\n"
                f"This is the exact error seen in production — "
                f"render_to_html should use OCaml bridge"
            )


if __name__ == "__main__":
    unittest.main()
