;; ==========================================================================
;; test-render-html.sx — Exhaustive tests for HTML rendering
;;
;; Tests render-to-html against the HTML serialization specification.
;; Every test verifies the SX renderer produces correct HTML strings.
;;
;; Requires: test-framework.sx, adapter-html.sx loaded.
;; ==========================================================================


;; Helper: render a QUOTED SX expression to HTML string.
;; The expression is first evaluated in the env (to resolve symbols),
;; then the result is passed to render-to-html.
;; For simple values (strings, numbers), use rh-val instead.
(define rh
  (fn (expr)
    (let ((env (env-extend (test-env))))
      (render-to-html expr env))))

;; Helper: render a literal value (no evaluation needed).
;; Uses render-value-to-html which skips the eval-expr dispatch.
(define rh-val
  (fn (val)
    (render-value-to-html val (env-extend (test-env)))))

;; Helper: render with a pre-built env
(define rh-env
  (fn (expr env)
    (render-to-html expr env)))


;; --------------------------------------------------------------------------
;; 1. Text content and literals
;; --------------------------------------------------------------------------

(defsuite "html-text"
  (deftest "string renders as escaped text"
    (assert-equal "hello" (rh-val "hello")))

  (deftest "number renders as string"
    (assert-equal "42" (rh-val 42)))

  (deftest "float renders as string"
    (assert-equal "3.14" (rh-val 3.14)))

  (deftest "boolean true renders as text"
    (assert-equal "true" (rh-val true)))

  (deftest "boolean false renders as text"
    (assert-equal "false" (rh-val false)))

  (deftest "nil renders as empty string"
    (assert-equal "" (rh-val nil)))

  (deftest "keyword renders as text"
    (assert-equal "hello" (rh-val :hello))))


;; --------------------------------------------------------------------------
;; 2. HTML escaping — content
;; --------------------------------------------------------------------------

(defsuite "html-escaping-content"
  (deftest "ampersand escaped in text"
    (assert-equal "a &amp; b" (rh-val "a & b")))

  (deftest "less-than escaped in text"
    (assert-equal "a &lt; b" (rh-val "a < b")))

  (deftest "greater-than escaped in text"
    (assert-equal "a &gt; b" (rh-val "a > b")))

  (deftest "multiple special chars escaped"
    (assert-equal "&lt;b&gt;tag&lt;/b&gt;"
      (rh-val "<b>tag</b>")))

  (deftest "text inside element is escaped"
    (assert-equal "<p>a &amp; b</p>"
      (rh '(p "a & b")))))


;; --------------------------------------------------------------------------
;; 3. HTML escaping — attributes
;; --------------------------------------------------------------------------

(defsuite "html-escaping-attrs"
  (deftest "ampersand escaped in attribute value"
    (assert-equal "<div title=\"a &amp; b\"></div>"
      (rh '(div :title "a & b"))))

  (deftest "angle brackets escaped in attribute value"
    (assert-equal "<div title=\"&lt;b&gt;\"></div>"
      (rh '(div :title "<b>")))))


;; --------------------------------------------------------------------------
;; 4. Normal elements — open tag, children, close tag
;; --------------------------------------------------------------------------

(defsuite "html-normal-elements"
  (deftest "div with text"
    (assert-equal "<div>hello</div>" (rh '(div "hello"))))

  (deftest "p with text"
    (assert-equal "<p>paragraph</p>" (rh '(p "paragraph"))))

  (deftest "span with text"
    (assert-equal "<span>inline</span>" (rh '(span "inline"))))

  (deftest "empty div"
    (assert-equal "<div></div>" (rh '(div))))

  (deftest "nested elements"
    (assert-equal "<div><p>inner</p></div>"
      (rh '(div (p "inner")))))

  (deftest "multiple children"
    (assert-equal "<div><p>a</p><p>b</p></div>"
      (rh '(div (p "a") (p "b")))))

  (deftest "deep nesting"
    (assert-equal "<div><section><article><p>deep</p></article></section></div>"
      (rh '(div (section (article (p "deep")))))))

  (deftest "mixed text and element children"
    (assert-equal "<p>hello <strong>world</strong></p>"
      (rh '(p "hello " (strong "world"))))))


;; --------------------------------------------------------------------------
;; 5. Void elements — self-closing, no children
;; --------------------------------------------------------------------------

(defsuite "html-void-elements"
  (deftest "br"
    (assert-equal "<br />" (rh '(br))))

  (deftest "hr"
    (assert-equal "<hr />" (rh '(hr))))

  (deftest "img with src"
    (assert-equal "<img src=\"photo.jpg\" />"
      (rh '(img :src "photo.jpg"))))

  (deftest "input with type"
    (assert-equal "<input type=\"text\" />"
      (rh '(input :type "text"))))

  (deftest "meta with charset"
    (assert-equal "<meta charset=\"utf-8\" />"
      (rh '(meta :charset "utf-8"))))

  (deftest "link with rel and href"
    (assert-equal "<link rel=\"stylesheet\" href=\"style.css\" />"
      (rh '(link :rel "stylesheet" :href "style.css"))))

  (deftest "source with src"
    (assert-equal "<source src=\"video.mp4\" />"
      (rh '(source :src "video.mp4"))))

  (deftest "col"
    (assert-equal "<col />" (rh '(col))))

  (deftest "wbr"
    (assert-equal "<wbr />" (rh '(wbr)))))


;; --------------------------------------------------------------------------
;; 6. Boolean attributes — name only when truthy
;; --------------------------------------------------------------------------

(defsuite "html-boolean-attrs"
  (deftest "checked true"
    (assert-equal "<input checked />"
      (rh '(input :checked true))))

  (deftest "checked false omitted"
    (assert-equal "<input />"
      (rh '(input :checked false))))

  (deftest "disabled true"
    (assert-equal "<button disabled>click</button>"
      (rh '(button :disabled true "click"))))

  (deftest "disabled false omitted"
    (assert-equal "<button>click</button>"
      (rh '(button :disabled false "click"))))

  (deftest "readonly"
    (assert-equal "<input readonly />"
      (rh '(input :readonly true))))

  (deftest "required"
    (assert-equal "<input required />"
      (rh '(input :required true))))

  (deftest "multiple"
    (assert-equal "<select multiple></select>"
      (rh '(select :multiple true))))

  (deftest "hidden"
    (assert-equal "<div hidden></div>"
      (rh '(div :hidden true))))

  (deftest "autofocus"
    (assert-equal "<input autofocus />"
      (rh '(input :autofocus true))))

  (deftest "autoplay"
    (assert-equal "<video autoplay></video>"
      (rh '(video :autoplay true))))

  (deftest "loop"
    (assert-equal "<video loop></video>"
      (rh '(video :loop true))))

  (deftest "muted"
    (assert-equal "<video muted></video>"
      (rh '(video :muted true))))

  (deftest "controls"
    (assert-equal "<audio controls></audio>"
      (rh '(audio :controls true))))

  (deftest "selected"
    (assert-equal "<option selected>yes</option>"
      (rh '(option :selected true "yes"))))

  (deftest "open (details)"
    (assert-equal "<details open></details>"
      (rh '(details :open true))))

  (deftest "defer"
    (assert-equal "<script defer></script>"
      (rh '(script :defer true))))

  (deftest "async"
    (assert-equal "<script async></script>"
      (rh '(script :async true))))

  (deftest "novalidate"
    (assert-equal "<form novalidate></form>"
      (rh '(form :novalidate true)))))


;; --------------------------------------------------------------------------
;; 7. Regular attributes
;; --------------------------------------------------------------------------

(defsuite "html-regular-attrs"
  (deftest "class attribute"
    (assert-equal "<div class=\"container\"></div>"
      (rh '(div :class "container"))))

  (deftest "id attribute"
    (assert-equal "<div id=\"main\"></div>"
      (rh '(div :id "main"))))

  (deftest "style attribute"
    (assert-equal "<div style=\"color: red\"></div>"
      (rh '(div :style "color: red"))))

  (deftest "data-* attribute"
    (assert-equal "<div data-value=\"42\"></div>"
      (rh '(div :data-value "42"))))

  (deftest "aria-* attribute"
    (assert-equal "<div aria-label=\"close\"></div>"
      (rh '(div :aria-label "close"))))

  (deftest "multiple attributes"
    (assert-equal "<a href=\"/\" class=\"link\">home</a>"
      (rh '(a :href "/" :class "link" "home"))))

  (deftest "nil attribute omitted"
    (assert-equal "<div></div>"
      (rh '(div :class nil))))

  (deftest "numeric attribute value"
    (assert-equal "<input maxlength=\"10\" />"
      (rh '(input :maxlength 10)))))


;; --------------------------------------------------------------------------
;; 8. Fragments — children without wrapper
;; --------------------------------------------------------------------------

(defsuite "html-fragments"
  (deftest "fragment renders children without wrapper"
    (assert-equal "<p>a</p><p>b</p>"
      (rh '(<> (p "a") (p "b")))))

  (deftest "empty fragment"
    (assert-equal "" (rh '(<>))))

  (deftest "fragment with text"
    (assert-equal "hello world"
      (rh '(<> "hello " "world"))))

  (deftest "nested fragment"
    (assert-equal "<p>a</p><p>b</p>"
      (rh '(<> (<> (p "a")) (p "b"))))))


;; --------------------------------------------------------------------------
;; 9. Raw HTML — unescaped passthrough
;; --------------------------------------------------------------------------

(defsuite "html-raw"
  (deftest "raw! passes through unescaped"
    (assert-equal "<b>bold</b>"
      (rh '(raw! "<b>bold</b>"))))

  (deftest "raw! with multiple args"
    (assert-equal "<em>a</em><em>b</em>"
      (rh '(raw! "<em>a</em>" "<em>b</em>")))))


;; --------------------------------------------------------------------------
;; 10. Heading levels
;; --------------------------------------------------------------------------

(defsuite "html-headings"
  (deftest "h1" (assert-equal "<h1>title</h1>" (rh '(h1 "title"))))
  (deftest "h2" (assert-equal "<h2>sub</h2>" (rh '(h2 "sub"))))
  (deftest "h3" (assert-equal "<h3>sec</h3>" (rh '(h3 "sec"))))
  (deftest "h4" (assert-equal "<h4>sub</h4>" (rh '(h4 "sub"))))
  (deftest "h5" (assert-equal "<h5>sub</h5>" (rh '(h5 "sub"))))
  (deftest "h6" (assert-equal "<h6>sub</h6>" (rh '(h6 "sub")))))


;; --------------------------------------------------------------------------
;; 11. Lists (HTML)
;; --------------------------------------------------------------------------

(defsuite "html-lists"
  (deftest "unordered list"
    (assert-equal "<ul><li>a</li><li>b</li></ul>"
      (rh '(ul (li "a") (li "b")))))

  (deftest "ordered list"
    (assert-equal "<ol><li>1</li><li>2</li></ol>"
      (rh '(ol (li "1") (li "2")))))

  (deftest "definition list"
    (assert-equal "<dl><dt>term</dt><dd>def</dd></dl>"
      (rh '(dl (dt "term") (dd "def"))))))


;; --------------------------------------------------------------------------
;; 12. Tables
;; --------------------------------------------------------------------------

(defsuite "html-tables"
  (deftest "basic table"
    (assert-equal "<table><tr><td>cell</td></tr></table>"
      (rh '(table (tr (td "cell"))))))

  (deftest "table with header"
    (assert-equal "<table><thead><tr><th>col</th></tr></thead><tbody><tr><td>val</td></tr></tbody></table>"
      (rh '(table (thead (tr (th "col"))) (tbody (tr (td "val"))))))))


;; --------------------------------------------------------------------------
;; 13. Forms
;; --------------------------------------------------------------------------

(defsuite "html-forms"
  (deftest "form with action"
    (assert-equal "<form action=\"/submit\"></form>"
      (rh '(form :action "/submit"))))

  (deftest "input types"
    (assert-equal "<input type=\"email\" />"
      (rh '(input :type "email"))))

  (deftest "textarea"
    (assert-equal "<textarea>content</textarea>"
      (rh '(textarea "content"))))

  (deftest "select with options"
    (assert-equal "<select><option>a</option><option>b</option></select>"
      (rh '(select (option "a") (option "b")))))

  (deftest "button"
    (assert-equal "<button type=\"submit\">go</button>"
      (rh '(button :type "submit" "go"))))

  (deftest "label with for"
    (assert-equal "<label for=\"name\">Name</label>"
      (rh '(label :for "name" "Name"))))

  (deftest "fieldset and legend"
    (assert-equal "<fieldset><legend>group</legend></fieldset>"
      (rh '(fieldset (legend "group"))))))


;; --------------------------------------------------------------------------
;; 14. Media elements
;; --------------------------------------------------------------------------

(defsuite "html-media"
  (deftest "video with src"
    (assert-equal "<video src=\"v.mp4\"></video>"
      (rh '(video :src "v.mp4"))))

  (deftest "audio with controls"
    (assert-equal "<audio controls></audio>"
      (rh '(audio :controls true))))

  (deftest "iframe"
    (assert-equal "<iframe src=\"page.html\"></iframe>"
      (rh '(iframe :src "page.html"))))

  (deftest "canvas"
    (assert-equal "<canvas width=\"100\" height=\"100\"></canvas>"
      (rh '(canvas :width 100 :height 100))))

  (deftest "picture with source and img"
    (assert-equal "<picture><source srcset=\"photo.webp\" /><img src=\"photo.jpg\" /></picture>"
      (rh '(picture (source :srcset "photo.webp") (img :src "photo.jpg"))))))


;; --------------------------------------------------------------------------
;; 15. Semantic elements
;; --------------------------------------------------------------------------

(defsuite "html-semantic"
  (deftest "header" (assert-equal "<header>h</header>" (rh '(header "h"))))
  (deftest "nav" (assert-equal "<nav>n</nav>" (rh '(nav "n"))))
  (deftest "main" (assert-equal "<main>m</main>" (rh '(main "m"))))
  (deftest "section" (assert-equal "<section>s</section>" (rh '(section "s"))))
  (deftest "article" (assert-equal "<article>a</article>" (rh '(article "a"))))
  (deftest "aside" (assert-equal "<aside>a</aside>" (rh '(aside "a"))))
  (deftest "footer" (assert-equal "<footer>f</footer>" (rh '(footer "f"))))
  (deftest "details and summary"
    (assert-equal "<details><summary>more</summary><p>info</p></details>"
      (rh '(details (summary "more") (p "info")))))
  (deftest "figure and figcaption"
    (assert-equal "<figure><img src=\"x.jpg\" /><figcaption>cap</figcaption></figure>"
      (rh '(figure (img :src "x.jpg") (figcaption "cap"))))))


;; --------------------------------------------------------------------------
;; 16. SVG elements
;; --------------------------------------------------------------------------

(defsuite "html-svg"
  (deftest "svg container"
    (assert-equal "<svg viewBox=\"0 0 100 100\"></svg>"
      (rh '(svg :viewBox "0 0 100 100"))))

  (deftest "circle"
    (let ((html (rh '(circle :cx 50 :cy 50 :r 40))))
      (assert-true (string-contains? html "cx=\"50\""))
      (assert-true (string-contains? html "cy=\"50\""))
      (assert-true (string-contains? html "r=\"40\""))))

  (deftest "rect"
    (assert-equal "<rect width=\"100\" height=\"50\"></rect>"
      (rh '(rect :width 100 :height 50))))

  (deftest "path"
    (assert-equal "<path d=\"M0 0 L100 100\"></path>"
      (rh '(path :d "M0 0 L100 100"))))

  (deftest "g with transform"
    (assert-equal "<g transform=\"translate(10,20)\"></g>"
      (rh '(g :transform "translate(10,20)"))))

  (deftest "text element"
    (assert-equal "<text x=\"10\" y=\"20\">label</text>"
      (rh '(text :x 10 :y 20 "label")))))


;; --------------------------------------------------------------------------
;; 17. Control flow in templates
;; --------------------------------------------------------------------------

(defsuite "html-control-flow"
  (deftest "if true renders then-branch"
    (assert-equal "<p>yes</p>"
      (rh '(if true (p "yes") (p "no")))))

  (deftest "if false renders else-branch"
    (assert-equal "<p>no</p>"
      (rh '(if false (p "yes") (p "no")))))

  (deftest "if false without else renders empty"
    (assert-equal "" (rh '(if false (p "x")))))

  (deftest "when true renders body"
    (assert-equal "<p>ok</p>"
      (rh '(when true (p "ok")))))

  (deftest "when false renders empty"
    (assert-equal "" (rh '(when false (p "x")))))

  (deftest "cond renders matching branch"
    (assert-equal "<p>b</p>"
      (rh '(cond false (p "a") true (p "b")))))

  (deftest "cond else branch"
    (assert-equal "<p>c</p>"
      (rh '(cond false (p "a") :else (p "c"))))))


;; --------------------------------------------------------------------------
;; 18. Let bindings in templates
;; --------------------------------------------------------------------------

(defsuite "html-let"
  (deftest "let binding used in template"
    (assert-equal "<p>hello</p>"
      (rh '(let ((x "hello")) (p x)))))

  (deftest "let with multiple bindings"
    (assert-equal "<p>helloworld</p>"
      (rh '(let ((a "hello") (b "world")) (p a b)))))

  (deftest "nested let"
    (assert-equal "<div><p>inner</p></div>"
      (rh '(let ((x "inner")) (div (let ((y x)) (p y))))))))


;; --------------------------------------------------------------------------
;; 19. Map / for-each in templates
;; --------------------------------------------------------------------------

(defsuite "html-iteration"
  (deftest "map over items"
    (assert-equal "<li>a</li><li>b</li><li>c</li>"
      (rh '(map (fn (x) (li x)) (list "a" "b" "c")))))

  (deftest "for-each renders items"
    (assert-equal "<p>1</p><p>2</p>"
      (rh '(for-each (fn (x) (p (str x))) (list 1 2)))))

  (deftest "map-indexed"
    (assert-equal "<li>0: a</li><li>1: b</li>"
      (rh '(map-indexed (fn (i x) (li (str i ": " x))) (list "a" "b"))))))


;; --------------------------------------------------------------------------
;; 20. Components
;; --------------------------------------------------------------------------

(defsuite "html-components"
  (deftest "simple component"
    (let ((env (env-extend (test-env))))
      (eval-expr '(defcomp ~card (&key title) (div :class "card" (h2 title))) env)
      (assert-equal "<div class=\"card\"><h2>hello</h2></div>"
        (rh-env '(~card :title "hello") env))))

  (deftest "component with children"
    (let ((env (env-extend (test-env))))
      (eval-expr '(defcomp ~box (&rest children) (div :class "box" children)) env)
      (assert-equal "<div class=\"box\"><p>inner</p></div>"
        (rh-env '(~box (p "inner")) env))))

  (deftest "component with keyword and children"
    (let ((env (env-extend (test-env))))
      (eval-expr '(defcomp ~panel (&key title &rest children)
                    (section (h2 title) children)) env)
      (assert-equal "<section><h2>Title</h2><p>body</p></section>"
        (rh-env '(~panel :title "Title" (p "body")) env))))

  (deftest "nested components"
    (let ((env (env-extend (test-env))))
      (eval-expr '(defcomp ~inner (&key text) (em text)) env)
      (eval-expr '(defcomp ~outer (&key text) (div (~inner :text text))) env)
      (assert-equal "<div><em>hi</em></div>"
        (rh-env '(~outer :text "hi") env)))))


;; --------------------------------------------------------------------------
;; 21. Macros
;; --------------------------------------------------------------------------

(defsuite "html-macros"
  (deftest "macro expands and renders"
    (let ((env (env-extend (test-env))))
      (eval-expr '(defmacro ~wrap (body)
                    `(div :class "wrapped" ,body)) env)
      (assert-equal "<div class=\"wrapped\"><p>hello</p></div>"
        (rh-env '(~wrap (p "hello")) env)))))


;; --------------------------------------------------------------------------
;; 22. Begin/do — multi-expression body
;; --------------------------------------------------------------------------

(defsuite "html-begin"
  (deftest "do renders all expressions"
    (assert-equal "<p>a</p><p>b</p>"
      (rh '(do (p "a") (p "b")))))

  (deftest "begin renders all expressions"
    (assert-equal "<h1>title</h1><p>body</p>"
      (rh '(begin (h1 "title") (p "body"))))))


;; --------------------------------------------------------------------------
;; 23. Letrec in templates
;; --------------------------------------------------------------------------

(defsuite "html-letrec"
  (deftest "letrec with side-effect rendering"
    (assert-equal "<li>a</li><li>b</li>"
      (rh '(letrec ((items (list "a" "b")))
              (do (map (fn (x) (li x)) items)))))))


;; --------------------------------------------------------------------------
;; 24. Scope/provide in templates
;; --------------------------------------------------------------------------

(defsuite "html-scope"
  (deftest "scope renders body"
    (assert-equal "<ul><li>inside</li></ul>"
      (rh '(scope "items"
              (ul (li "inside"))))))

  (deftest "provide renders body"
    (assert-equal "<div>content</div>"
      (rh '(provide "theme" "dark"
              (div "content"))))))


;; --------------------------------------------------------------------------
;; 25. Other elements
;; --------------------------------------------------------------------------

(defsuite "html-other-elements"
  (deftest "pre preserves structure"
    (assert-equal "<pre>code here</pre>"
      (rh '(pre "code here"))))

  (deftest "code element"
    (assert-equal "<code>x = 1</code>"
      (rh '(code "x = 1"))))

  (deftest "blockquote"
    (assert-equal "<blockquote>quote</blockquote>"
      (rh '(blockquote "quote"))))

  (deftest "abbr with title"
    (assert-equal "<abbr title=\"HyperText Markup Language\">HTML</abbr>"
      (rh '(abbr :title "HyperText Markup Language" "HTML"))))

  (deftest "time with datetime"
    (assert-equal "<time datetime=\"2026-01-01\">New Year</time>"
      (rh '(time :datetime "2026-01-01" "New Year"))))

  (deftest "dialog"
    (assert-equal "<dialog open>content</dialog>"
      (rh '(dialog :open true "content"))))

  (deftest "template"
    (assert-equal "<template>inner</template>"
      (rh '(template "inner"))))

  (deftest "slot with name"
    (assert-equal "<slot name=\"header\"></slot>"
      (rh '(slot :name "header"))))

  (deftest "noscript"
    (assert-equal "<noscript>fallback</noscript>"
      (rh '(noscript "fallback")))))


;; --------------------------------------------------------------------------
;; 26. Islands — defisland with hydration markers
;; --------------------------------------------------------------------------

(defsuite "html-islands"
  (deftest "island renders with data-sx-island attribute"
    (let ((env (env-extend (test-env))))
      (eval-expr '(defisland ~counter (&key start)
                    (span (str "count: " start))) env)
      (let ((html (rh-env '(~counter :start 0) env)))
        (assert-true (string-contains? html "data-sx-island"))
        (assert-true (string-contains? html "count: 0")))))

  (deftest "island name appears in marker"
    (let ((env (env-extend (test-env))))
      (eval-expr '(defisland ~toggle (&key label)
                    (button label)) env)
      (let ((html (rh-env '(~toggle :label "click") env)))
        (assert-true (string-contains? html "toggle"))
        (assert-true (string-contains? html "click")))))

  (deftest "island with children"
    (let ((env (env-extend (test-env))))
      (eval-expr '(defisland ~wrapper (&rest children)
                    (div :class "island" children)) env)
      (let ((html (rh-env '(~wrapper (p "inside")) env)))
        (assert-true (string-contains? html "data-sx-island"))
        (assert-true (string-contains? html "<p>inside</p>"))))))


;; --------------------------------------------------------------------------
;; 27. Lakes — server-morphable slots within islands
;; --------------------------------------------------------------------------

(defsuite "html-lakes"
  (deftest "lake renders with data-sx-lake attribute"
    (let ((env (env-extend (test-env))))
      (let ((html (rh-env '(lake :id "content" (p "hello")) env)))
        (assert-true (string-contains? html "data-sx-lake"))
        (assert-true (string-contains? html "content"))
        (assert-true (string-contains? html "<p>hello</p>")))))

  (deftest "lake with custom tag"
    (let ((env (env-extend (test-env))))
      (let ((html (rh-env '(lake :id "nav" :tag "nav" (a "link")) env)))
        (assert-true (starts-with? html "<nav"))
        (assert-true (string-contains? html "<a>link</a>"))))))


;; --------------------------------------------------------------------------
;; 28. Marshes — reactive server-morphable slots
;; --------------------------------------------------------------------------

(defsuite "html-marshes"
  (deftest "marsh renders with data-sx-marsh attribute"
    (let ((env (env-extend (test-env))))
      (let ((html (rh-env '(marsh :id "feed" (li "item")) env)))
        (assert-true (string-contains? html "data-sx-marsh"))
        (assert-true (string-contains? html "feed"))
        (assert-true (string-contains? html "<li>item</li>")))))

  (deftest "marsh with custom tag"
    (let ((env (env-extend (test-env))))
      (let ((html (rh-env '(marsh :id "list" :tag "ul" (li "a") (li "b")) env)))
        (assert-true (starts-with? html "<ul"))
        (assert-true (string-contains? html "<li>a</li>"))
        (assert-true (string-contains? html "<li>b</li>"))))))


;; --------------------------------------------------------------------------
;; 29. Thread macro in templates
;; --------------------------------------------------------------------------

(defsuite "html-threading"
  (deftest "thread-first in template context"
    (assert-equal "<p>HELLO</p>"
      (rh '(p (-> "hello" upper))))))


;; --------------------------------------------------------------------------
;; 30. Define in templates
;; --------------------------------------------------------------------------

(defsuite "html-define-in-template"
  (deftest "define then use in same template"
    (assert-equal "<p>42</p>"
      (rh '(do (define x 42) (p (str x))))))

  (deftest "defcomp then use"
    (assert-equal "<em>hi</em>"
      (rh '(do (defcomp ~tag (&key text) (em text))
               (~tag :text "hi"))))))
