"""Tests for the OCaml SX bridge."""

import asyncio
import os
import sys
import unittest

# Add project root to path
_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.ocaml_bridge import OcamlBridge, OcamlBridgeError, _escape, _parse_response, _serialize_for_ocaml


class TestHelpers(unittest.TestCase):
    """Test helper functions (no subprocess needed)."""

    def test_escape_basic(self):
        self.assertEqual(_escape('hello'), 'hello')
        self.assertEqual(_escape('say "hi"'), 'say \\"hi\\"')
        self.assertEqual(_escape('a\\b'), 'a\\\\b')
        self.assertEqual(_escape('line\nbreak'), 'line\\nbreak')

    def test_parse_response_ok_empty(self):
        self.assertEqual(_parse_response("(ok)"), ("ok", None))

    def test_parse_response_ok_number(self):
        self.assertEqual(_parse_response("(ok 3)"), ("ok", "3"))

    def test_parse_response_ok_string(self):
        kind, val = _parse_response('(ok "<div>hi</div>")')
        self.assertEqual(kind, "ok")
        self.assertEqual(val, "<div>hi</div>")

    def test_parse_response_error(self):
        kind, val = _parse_response('(error "something broke")')
        self.assertEqual(kind, "error")
        self.assertEqual(val, "something broke")

    def test_serialize_none(self):
        self.assertEqual(_serialize_for_ocaml(None), "nil")

    def test_serialize_bool(self):
        self.assertEqual(_serialize_for_ocaml(True), "true")
        self.assertEqual(_serialize_for_ocaml(False), "false")

    def test_serialize_number(self):
        self.assertEqual(_serialize_for_ocaml(42), "42")
        self.assertEqual(_serialize_for_ocaml(3.14), "3.14")

    def test_serialize_string(self):
        self.assertEqual(_serialize_for_ocaml("hello"), '"hello"')
        self.assertEqual(_serialize_for_ocaml('say "hi"'), '"say \\"hi\\""')

    def test_serialize_list(self):
        self.assertEqual(_serialize_for_ocaml([1, 2, 3]), "(list 1 2 3)")

    def test_serialize_dict(self):
        result = _serialize_for_ocaml({"a": 1})
        self.assertEqual(result, "{:a 1}")


class TestBridge(unittest.IsolatedAsyncioTestCase):
    """Integration tests — require the OCaml binary to be built."""

    @classmethod
    def setUpClass(cls):
        # Check if binary exists
        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(
                f"OCaml binary not found at {bin_path}. "
                f"Build with: cd hosts/ocaml && eval $(opam env) && dune build"
            )

    async def asyncSetUp(self):
        self.bridge = OcamlBridge()
        await self.bridge.start()

    async def asyncTearDown(self):
        await self.bridge.stop()

    async def test_ping(self):
        self.assertTrue(await self.bridge.ping())

    async def test_eval_arithmetic(self):
        result = await self.bridge.eval("(+ 1 2)")
        self.assertEqual(result, "3")

    async def test_eval_string(self):
        result = await self.bridge.eval('(str "hello" " " "world")')
        self.assertIn("hello world", result)

    async def test_render_simple(self):
        html = await self.bridge.render('(div (p "hello"))')
        self.assertEqual(html, "<div><p>hello</p></div>")

    async def test_render_attrs(self):
        html = await self.bridge.render('(div :class "card" (p "hi"))')
        self.assertIn('class="card"', html)
        self.assertIn("<p>hi</p>", html)

    async def test_render_void_element(self):
        html = await self.bridge.render("(br)")
        self.assertEqual(html, "<br />")

    async def test_load_source_defcomp(self):
        count = await self.bridge.load_source(
            '(defcomp ~test-card (&key title) (div :class "card" (h2 title)))'
        )
        self.assertEqual(count, 1)
        html = await self.bridge.render('(~test-card :title "Hello")')
        self.assertIn("Hello", html)
        self.assertIn("card", html)

    async def test_reset(self):
        await self.bridge.load_source("(define x 42)")
        result = await self.bridge.eval("x")
        self.assertEqual(result, "42")
        await self.bridge.reset()
        with self.assertRaises(OcamlBridgeError):
            await self.bridge.eval("x")

    async def test_render_conditional(self):
        html = await self.bridge.render('(if true (p "yes") (p "no"))')
        self.assertEqual(html, "<p>yes</p>")

    async def test_render_let(self):
        html = await self.bridge.render('(let (x "hi") (p x))')
        self.assertEqual(html, "<p>hi</p>")

    async def test_render_map(self):
        html = await self.bridge.render(
            "(map (lambda (x) (li x)) (list \"a\" \"b\" \"c\"))"
        )
        self.assertEqual(html, "<li>a</li><li>b</li><li>c</li>")

    async def test_render_fragment(self):
        html = await self.bridge.render('(<> (p "a") (p "b"))')
        self.assertEqual(html, "<p>a</p><p>b</p>")

    async def test_eval_error(self):
        with self.assertRaises(OcamlBridgeError):
            await self.bridge.eval("(undefined-symbol-xyz)")

    async def test_render_component_with_children(self):
        await self.bridge.load_source(
            '(defcomp ~wrapper (&rest children) (div :class "wrap" children))'
        )
        html = await self.bridge.render('(~wrapper (p "inside"))')
        self.assertIn("wrap", html)
        self.assertIn("<p>inside</p>", html)

    async def test_render_macro(self):
        await self.bridge.load_source(
            "(defmacro unless (cond &rest body) (list 'if (list 'not cond) (cons 'do body)))"
        )
        html = await self.bridge.render('(unless false (p "shown"))')
        self.assertEqual(html, "<p>shown</p>")

    # ------------------------------------------------------------------
    # ListRef regression tests — the `list` primitive returns ListRef
    # (mutable), not List (immutable). Macro expansions that construct
    # AST via `list` produce ListRef nodes. The renderer must handle
    # both List and ListRef at every structural match point.
    # ------------------------------------------------------------------

    async def test_render_macro_generates_cond(self):
        """Macro that programmatically builds a (cond ...) with list."""
        await self.bridge.load_source(
            "(defmacro pick (x) "
            "  (list 'cond "
            "    (list (list '= x 1) '(p \"one\")) "
            "    (list (list '= x 2) '(p \"two\")) "
            "    (list ':else '(p \"other\"))))"
        )
        html = await self.bridge.render("(pick 2)")
        self.assertEqual(html, "<p>two</p>")

    async def test_render_macro_generates_let(self):
        """Macro that programmatically builds a (let ...) with list."""
        await self.bridge.load_source(
            "(defmacro with-greeting (name &rest body) "
            "  (list 'let (list (list 'greeting (list 'str \"Hello \" name))) "
            "    (cons 'do body)))"
        )
        html = await self.bridge.render('(with-greeting "World" (p greeting))')
        self.assertEqual(html, "<p>Hello World</p>")

    async def test_render_macro_nested_html_tags(self):
        """Macro expansion containing nested HTML tags via list."""
        await self.bridge.load_source(
            "(defmacro card (title &rest body) "
            "  (list 'div ':class \"card\" "
            "    (list 'h2 title) "
            "    (cons 'do body)))"
        )
        html = await self.bridge.render('(card "Title" (p "content"))')
        self.assertIn('<div class="card">', html)
        self.assertIn("<h2>Title</h2>", html)
        self.assertIn("<p>content</p>", html)

    async def test_render_eval_returns_listref(self):
        """Values created at runtime via (list ...) are ListRef."""
        await self.bridge.load_source(
            "(define make-items (lambda () (list "
            '  (list "a") (list "b") (list "c"))))'
        )
        html = await self.bridge.render(
            "(ul (map (lambda (x) (li (first x))) (make-items)))"
        )
        self.assertIn("<li>a</li>", html)
        self.assertIn("<li>b</li>", html)
        self.assertIn("<li>c</li>", html)


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