"""Tests for the HSX-style HTML renderer."""

import pytest
from shared.sx import parse, evaluate
from shared.sx.html import render, escape_text, escape_attr


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def r(text, env=None):
    """Parse and render a single expression."""
    return render(parse(text), env)


# ---------------------------------------------------------------------------
# Escaping
# ---------------------------------------------------------------------------

class TestEscaping:
    def test_escape_text_ampersand(self):
        assert escape_text("A & B") == "A &amp; B"

    def test_escape_text_lt_gt(self):
        assert escape_text("<script>") == "&lt;script&gt;"

    def test_escape_attr_quotes(self):
        assert escape_attr('he said "hi"') == "he said &quot;hi&quot;"

    def test_escape_attr_all(self):
        assert escape_attr('&<>"') == "&amp;&lt;&gt;&quot;"


# ---------------------------------------------------------------------------
# Primitives / atoms
# ---------------------------------------------------------------------------

class TestAtoms:
    def test_string(self):
        assert r('"Hello"') == "Hello"

    def test_string_with_entities(self):
        assert r('"<b>bold</b>"') == "&lt;b&gt;bold&lt;/b&gt;"

    def test_integer(self):
        assert r("42") == "42"

    def test_float(self):
        assert r("3.14") == "3.14"

    def test_nil(self):
        assert r("nil") == ""

    def test_true(self):
        assert r("true") == ""

    def test_false(self):
        assert r("false") == ""

    def test_keyword(self):
        assert r(":hello") == "hello"


# ---------------------------------------------------------------------------
# Simple HTML elements
# ---------------------------------------------------------------------------

class TestElements:
    def test_div(self):
        assert r('(div "Hello")') == "<div>Hello</div>"

    def test_empty_div(self):
        assert r("(div)") == "<div></div>"

    def test_nested(self):
        assert r('(div (p "Hello"))') == "<div><p>Hello</p></div>"

    def test_multiple_children(self):
        assert r('(ul (li "One") (li "Two"))') == \
            "<ul><li>One</li><li>Two</li></ul>"

    def test_mixed_text_and_elements(self):
        assert r('(p "Hello " (strong "world"))') == \
            "<p>Hello <strong>world</strong></p>"

    def test_deep_nesting(self):
        assert r('(div (div (div (span "deep"))))') == \
            "<div><div><div><span>deep</span></div></div></div>"


# ---------------------------------------------------------------------------
# Attributes
# ---------------------------------------------------------------------------

class TestAttributes:
    def test_single_attr(self):
        assert r('(div :class "card")') == '<div class="card"></div>'

    def test_multiple_attrs(self):
        html = r('(div :class "card" :id "main")')
        assert 'class="card"' in html
        assert 'id="main"' in html

    def test_attr_with_children(self):
        assert r('(div :class "card" (p "Body"))') == \
            '<div class="card"><p>Body</p></div>'

    def test_attr_escaping(self):
        assert r('(div :title "A & B")') == '<div title="A &amp; B"></div>'

    def test_attr_with_quotes(self):
        assert r("""(div :title "say \\"hi\\"")""") == \
            '<div title="say &quot;hi&quot;"></div>'

    def test_false_attr_omitted(self):
        env = {"flag": False}
        html = render(parse("(div :hidden flag)"), env)
        assert html == "<div></div>"

    def test_nil_attr_omitted(self):
        env = {"flag": None}
        html = render(parse("(div :data-x flag)"), env)
        assert html == "<div></div>"

    def test_numeric_attr(self):
        assert r('(input :tabindex 1)') == '<input tabindex="1">'


# ---------------------------------------------------------------------------
# Boolean attributes
# ---------------------------------------------------------------------------

class TestBooleanAttrs:
    def test_disabled_true(self):
        env = {"yes": True}
        html = render(parse("(button :disabled yes)"), env)
        assert html == "<button disabled></button>"

    def test_disabled_false(self):
        env = {"no": False}
        html = render(parse("(button :disabled no)"), env)
        assert html == "<button></button>"

    def test_checked(self):
        env = {"c": True}
        html = render(parse('(input :type "checkbox" :checked c)'), env)
        assert 'checked' in html
        assert 'type="checkbox"' in html

    def test_required(self):
        env = {"r": True}
        html = render(parse("(input :required r)"), env)
        assert "required" in html


# ---------------------------------------------------------------------------
# Void elements
# ---------------------------------------------------------------------------

class TestVoidElements:
    def test_br(self):
        assert r("(br)") == "<br>"

    def test_img(self):
        html = r('(img :src "/photo.jpg" :alt "Photo")')
        assert html == '<img src="/photo.jpg" alt="Photo">'

    def test_input(self):
        html = r('(input :type "text" :name "q")')
        assert html == '<input type="text" name="q">'

    def test_hr(self):
        assert r("(hr)") == "<hr>"

    def test_meta(self):
        html = r('(meta :charset "utf-8")')
        assert html == '<meta charset="utf-8">'

    def test_link(self):
        html = r('(link :rel "stylesheet" :href "/s.css")')
        assert html == '<link rel="stylesheet" href="/s.css">'

    def test_void_no_closing_tag(self):
        # Void elements must not have a closing tag
        html = r("(br)")
        assert "</br>" not in html


# ---------------------------------------------------------------------------
# Fragment (<>)
# ---------------------------------------------------------------------------

class TestFragments:
    def test_basic_fragment(self):
        assert r('(<> (li "A") (li "B"))') == "<li>A</li><li>B</li>"

    def test_empty_fragment(self):
        assert r("(<>)") == ""

    def test_nested_fragment(self):
        html = r('(ul (<> (li "1") (li "2")))')
        assert html == "<ul><li>1</li><li>2</li></ul>"


# ---------------------------------------------------------------------------
# raw!
# ---------------------------------------------------------------------------

class TestRawHtml:
    def test_raw_string(self):
        assert r('(raw! "<b>bold</b>")') == "<b>bold</b>"

    def test_raw_no_escaping(self):
        html = r('(raw! "<script>alert(1)</script>")')
        assert "<script>" in html

    def test_raw_with_variable(self):
        env = {"content": "<em>hi</em>"}
        html = render(parse("(raw! content)"), env)
        assert html == "<em>hi</em>"


# ---------------------------------------------------------------------------
# Components (defcomp / ~prefix)
# ---------------------------------------------------------------------------

class TestComponents:
    def test_basic_component(self):
        env = {}
        evaluate(parse('(defcomp ~shared:misc/badge (&key label) (span :class "badge" label))'), env)
        html = render(parse('(~shared:misc/badge :label "New")'), env)
        assert html == '<span class="badge">New</span>'

    def test_component_with_children(self):
        env = {}
        evaluate(parse(
            '(defcomp ~card (&key title &rest children)'
            '  (div :class "card" (h2 title) children))'
        ), env)
        html = render(parse('(~card :title "Hi" (p "Body"))'), env)
        assert html == '<div class="card"><h2>Hi</h2><p>Body</p></div>'

    def test_nested_components(self):
        env = {}
        evaluate(parse('(defcomp ~inner (&key text) (em text))'), env)
        evaluate(parse(
            '(defcomp ~outer (&key label)'
            '  (div (~inner :text label)))'
        ), env)
        html = render(parse('(~outer :label "Hello")'), env)
        assert html == "<div><em>Hello</em></div>"

    def test_component_with_conditional(self):
        env = {}
        evaluate(parse(
            '(defcomp ~maybe (&key show text)'
            '  (when show (span text)))'
        ), env)
        assert render(parse('(~maybe :show true :text "Yes")'), env) == "<span>Yes</span>"
        assert render(parse('(~maybe :show false :text "No")'), env) == ""


# ---------------------------------------------------------------------------
# Expressions in render context
# ---------------------------------------------------------------------------

class TestExpressions:
    def test_let_in_render(self):
        html = r('(let ((x "Hello")) (p x))')
        assert html == "<p>Hello</p>"

    def test_if_true_branch(self):
        html = r('(if true (p "Yes") (p "No"))')
        assert html == "<p>Yes</p>"

    def test_if_false_branch(self):
        html = r('(if false (p "Yes") (p "No"))')
        assert html == "<p>No</p>"

    def test_when_true(self):
        html = r('(when true (p "Shown"))')
        assert html == "<p>Shown</p>"

    def test_when_false(self):
        html = r('(when false (p "Hidden"))')
        assert html == ""

    def test_map_rendering(self):
        html = r('(map (lambda (x) (li x)) (list "A" "B" "C"))')
        assert html == "<li>A</li><li>B</li><li>C</li>"

    def test_variable_in_element(self):
        env = {"name": "World"}
        html = render(parse('(p (str "Hello " name))'), env)
        assert html == "<p>Hello World</p>"

    def test_str_in_attr(self):
        env = {"slug": "apple"}
        html = render(parse('(a :href (str "/p/" slug) "Link")'), env)
        assert html == '<a href="/p/apple">Link</a>'


# ---------------------------------------------------------------------------
# Full page structure
# ---------------------------------------------------------------------------

class TestFullPage:
    def test_simple_page(self):
        html = r('''
            (html :lang "en"
              (head (title "Test"))
              (body (h1 "Hello")))
        ''')
        assert '<html lang="en">' in html
        assert "<title>Test</title>" in html
        assert "<h1>Hello</h1>" in html
        assert "</body>" in html
        assert "</html>" in html

    def test_form(self):
        html = r('''
            (form :method "post" :action "/login"
              (label :for "user" "Username")
              (input :type "text" :name "user" :required true)
              (button :type "submit" "Login"))
        ''')
        assert '<form method="post" action="/login">' in html
        assert '<label for="user">Username</label>' in html
        assert 'required' in html
        assert "<button" in html

    def test_table(self):
        html = r('''
            (table
              (thead (tr (th "Name") (th "Price")))
              (tbody
                (tr (td "Apple") (td "1.50"))
                (tr (td "Banana") (td "0.75"))))
        ''')
        assert "<thead>" in html
        assert "<th>Name</th>" in html
        assert "<td>Apple</td>" in html


# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------

class TestEdgeCases:
    def test_empty_list(self):
        assert render([], {}) == ""

    def test_none(self):
        assert render(None, {}) == ""

    def test_dict_not_rendered(self):
        assert render({"a": 1}, {}) == ""

    def test_number_list(self):
        # A list of plain numbers (not a call) renders each
        assert render([1, 2, 3], {}) == "123"

    def test_string_list(self):
        assert render(["a", "b"], {}) == "ab"
