;; test-htmx.sx — Tests for htmx 4.0 compatibility layer
;;
;; Tests the attribute-to-handler translator: pure parsing functions,
;; swap mode resolution, trigger parsing, and DOM integration via harness.

;; ── Time parsing ────────────────────────────────────────────────

(defsuite
  "htmx-parse-time"
  (deftest "parses milliseconds" (assert= (hx-parse-time "500ms") 500))
  (deftest "parses seconds" (assert= (hx-parse-time "1s") 1000))
  (deftest "parses fractional seconds" (assert= (hx-parse-time "0.5s") 500))
  (deftest "parses minutes" (assert= (hx-parse-time "2m") 120000))
  (deftest "parses bare number" (assert= (hx-parse-time "100") 100))
  (deftest "returns nil for nil" (assert= (hx-parse-time nil) nil)))

;; ── Swap mode normalization (v4 aliases) ────────────────────────

(defsuite
  "htmx-swap-aliases"
  (deftest
    "before → beforebegin"
    (assert= (hx-normalize-swap-mode "before") "beforebegin"))
  (deftest
    "after → afterend"
    (assert= (hx-normalize-swap-mode "after") "afterend"))
  (deftest
    "prepend → afterbegin"
    (assert= (hx-normalize-swap-mode "prepend") "afterbegin"))
  (deftest
    "append → beforeend"
    (assert= (hx-normalize-swap-mode "append") "beforeend"))
  (deftest
    "innerHTML passes through"
    (assert= (hx-normalize-swap-mode "innerHTML") "innerHTML"))
  (deftest
    "outerHTML passes through"
    (assert= (hx-normalize-swap-mode "outerHTML") "outerHTML"))
  (deftest
    "delete passes through"
    (assert= (hx-normalize-swap-mode "delete") "delete"))
  (deftest
    "innerMorph passes through"
    (assert= (hx-normalize-swap-mode "innerMorph") "innerMorph"))
  (deftest
    "outerMorph passes through"
    (assert= (hx-normalize-swap-mode "outerMorph") "outerMorph"))
  (deftest
    "textContent passes through"
    (assert= (hx-normalize-swap-mode "textContent") "textContent")))

;; ── Swap spec parsing ───────────────────────────────────────────

(defsuite
  "htmx-parse-swap-spec"
  (deftest
    "nil defaults to innerHTML"
    (let
      ((spec (hx-parse-swap-spec nil)))
      (assert= (get spec :mode) "innerHTML")))
  (deftest
    "bare mode"
    (let
      ((spec (hx-parse-swap-spec "outerHTML")))
      (assert= (get spec :mode) "outerHTML")
      (assert= (get spec :swap-delay) nil)))
  (deftest
    "mode with swap delay"
    (let
      ((spec (hx-parse-swap-spec "innerHTML swap:100ms")))
      (assert= (get spec :mode) "innerHTML")
      (assert= (get spec :swap-delay) 100)))
  (deftest
    "mode with settle delay"
    (let
      ((spec (hx-parse-swap-spec "innerHTML settle:200ms")))
      (assert= (get spec :settle-delay) 200)))
  (deftest
    "mode with scroll"
    (let
      ((spec (hx-parse-swap-spec "innerHTML scroll:top")))
      (assert= (get spec :scroll) "top")))
  (deftest
    "v4 alias normalized in spec"
    (let
      ((spec (hx-parse-swap-spec "append settle:500ms")))
      (assert= (get spec :mode) "beforeend")
      (assert= (get spec :settle-delay) 500)))
  (deftest
    "full spec with multiple modifiers"
    (let
      ((spec (hx-parse-swap-spec "innerHTML swap:50ms settle:100ms scroll:top")))
      (assert= (get spec :mode) "innerHTML")
      (assert= (get spec :swap-delay) 50)
      (assert= (get spec :settle-delay) 100)
      (assert= (get spec :scroll) "top"))))

;; ── Trigger parsing ─────────────────────────────────────────────

(defsuite
  "htmx-parse-trigger"
  (deftest
    "simple event"
    (let
      ((spec (hx-parse-trigger "click" nil)))
      (assert= (get spec :event) "click")
      (assert= (get spec :delay) nil)
      (assert= (get spec :once) false)))
  (deftest
    "event with delay"
    (let
      ((spec (hx-parse-trigger "keyup delay:500ms" nil)))
      (assert= (get spec :event) "keyup")
      (assert= (get spec :delay) 500)))
  (deftest
    "event with throttle"
    (let
      ((spec (hx-parse-trigger "click throttle:1s" nil)))
      (assert= (get spec :event) "click")
      (assert= (get spec :throttle) 1000)))
  (deftest
    "event with once"
    (let
      ((spec (hx-parse-trigger "click once" nil)))
      (assert= (get spec :event) "click")
      (assert= (get spec :once) true)))
  (deftest
    "event with changed"
    (let
      ((spec (hx-parse-trigger "keyup changed delay:500ms" nil)))
      (assert= (get spec :event) "keyup")
      (assert= (get spec :changed) true)
      (assert= (get spec :delay) 500)))
  (deftest
    "event with from selector"
    (let
      ((spec (hx-parse-trigger "click from:body" nil)))
      (assert= (get spec :event) "click")
      (assert= (get spec :from) "body")))
  (deftest
    "event with filter"
    (let
      ((spec (hx-parse-trigger "keyup [key=='Enter']" nil)))
      (assert= (get spec :event) "keyup")
      (assert= (get spec :filter) "[key=='Enter']")))
  (deftest
    "every trigger"
    (let
      ((spec (hx-parse-trigger "every delay:2s" nil)))
      (assert= (get spec :event) "every")
      (assert= (get spec :delay) 2000))))

;; ── URL encoding ────────────────────────────────────────────────

(defsuite
  "htmx-url-encode"
  (deftest
    "encodes single param"
    (assert= (url-encode-params {:q "search"}) "q=search"))
  (deftest
    "encodes numeric values"
    (assert= (url-encode-params {:page 1}) "page=1")))

(defsuite
  "htmx-status-matches"
  (deftest "exact match" (assert= (hx-status-matches? "404" "404") true))
  (deftest
    "exact non-match"
    (assert= (hx-status-matches? "404" "500") false))
  (deftest
    "1-digit wildcard 5xx matches 503"
    (assert= (hx-status-matches? "503" "5xx") true))
  (deftest
    "1-digit wildcard 4xx does not match 503"
    (assert= (hx-status-matches? "503" "4xx") false))
  (deftest
    "2-digit wildcard 50x matches 503"
    (assert= (hx-status-matches? "503" "50x") true))
  (deftest
    "2-digit wildcard 50x does not match 522"
    (assert= (hx-status-matches? "522" "50x") false))
  (deftest "2xx matches 200" (assert= (hx-status-matches? "200" "2xx") true)))

(defsuite
  "htmx-status-modifiers"
  (deftest
    "parses swap target push"
    (let
      ((m (hx-parse-status-modifiers "swap:innerHTML target:#errors push:false")))
      (assert= (get m :swap) "innerHTML")
      (assert= (get m :target) "#errors")
      (assert= (get m :push) "false")))
  (deftest
    "parses transition"
    (let
      ((m (hx-parse-status-modifiers "swap:none transition:true")))
      (assert= (get m :swap) "none")
      (assert= (get m :transition) "true"))))

(defsuite
  "htmx-match-status"
  (deftest
    "exact match wins over wildcard"
    (let
      ((rules (list {:target nil :transition nil :swap "none" :select nil :push nil :specificity 1 :code "5xx" :replace nil} {:target nil :transition nil :swap "outerHTML" :select nil :push nil :specificity 3 :code "503" :replace nil})))
      (assert= (get (hx-match-status 503 rules) :swap) "outerHTML")))
  (deftest
    "2-digit wildcard wins over 1-digit"
    (let
      ((rules (list {:target nil :transition nil :swap "none" :select nil :push nil :specificity 1 :code "5xx" :replace nil} {:target nil :transition nil :swap "innerHTML" :select nil :push nil :specificity 2 :code "50x" :replace nil})))
      (assert= (get (hx-match-status 501 rules) :swap) "innerHTML")))
  (deftest
    "nil when no match"
    (let
      ((rules (list {:target nil :transition nil :swap "none" :select nil :push nil :specificity 1 :code "5xx" :replace nil})))
      (assert= (hx-match-status 404 rules) nil))))

(defsuite
  "htmx-sync-spec"
  (deftest
    "parses selector:strategy"
    (let
      ((s (hx-parse-sync-spec "closest form:abort")))
      (assert= (get s :selector) "closest form")
      (assert= (get s :strategy) "abort")))
  (deftest
    "parses queue with mode"
    (let
      ((s (hx-parse-sync-spec "this:queue last")))
      (assert= (get s :selector) "this")
      (assert= (get s :strategy) "queue")
      (assert= (get s :queue-mode) "last")))
  (deftest
    "defaults to drop strategy"
    (let
      ((s (hx-parse-sync-spec "this:drop")))
      (assert= (get s :strategy) "drop")))
  (deftest "nil for nil input" (assert= (hx-parse-sync-spec nil) nil)))

(defsuite
  "htmx-sse-swap-parse"
  (deftest
    "parses single spec"
    (let
      ((specs (hx-parse-sse-swap "message:#target")))
      (assert= (len specs) 1)
      (assert= (get (first specs) :event) "message")
      (assert= (get (first specs) :target) "#target")))
  (deftest
    "parses multiple specs with swap mode"
    (let
      ((specs (hx-parse-sse-swap "message:#target,update:#list:outerHTML")))
      (assert= (len specs) 2)
      (assert= (get (nth specs 1) :event) "update")
      (assert= (get (nth specs 1) :swap) "outerHTML")))
  (deftest
    "nil returns empty list"
    (assert= (hx-parse-sse-swap nil) (list))))

(defsuite
  "htmx-swap-spec-v4-modifiers"
  (deftest
    "transition modifier"
    (let
      ((spec (hx-parse-swap-spec "innerHTML transition:true")))
      (assert= (get spec :transition) true)
      (assert= (get spec :mode) "innerHTML")))
  (deftest
    "strip modifier"
    (let
      ((spec (hx-parse-swap-spec "outerHTML strip:true")))
      (assert= (get spec :strip) true)))
  (deftest
    "target override in swap spec"
    (let
      ((spec (hx-parse-swap-spec "innerHTML target:#alt")))
      (assert= (get spec :target) "#alt")))
  (deftest
    "ignoreTitle modifier"
    (let
      ((spec (hx-parse-swap-spec "innerHTML ignoreTitle:true")))
      (assert= (get spec :ignore-title) true)))
  (deftest
    "all modifiers together"
    (let
      ((spec (hx-parse-swap-spec "append swap:50ms settle:100ms scroll:top transition:true strip:true")))
      (assert= (get spec :mode) "beforeend")
      (assert= (get spec :swap-delay) 50)
      (assert= (get spec :settle-delay) 100)
      (assert= (get spec :scroll) "top")
      (assert= (get spec :transition) true)
      (assert= (get spec :strip) true))))