;; Conformance fixture: routing/query-string-coercion
;;
;; Spec 012 §Query strings and fragments locks the per-key coercion
;; cascade: when a route declares a :query Malli :map schema, raw URL
;; values are coerced into the declared types. The first-pass type set is
;; :int, :boolean, and the enum-bounded keyword form `[:enum :a :b ...]`.
;; Strings and unknown types pass through.
;;
;; Per rf2-3k3o7 a bare `:keyword` slot is treated as an unbounded
;; keyword-interning DoS site (JVM keywords intern forever); the URL
;; value is preserved as a string rather than being interned. Authors
;; who want a keyword-typed slot declare an `[:enum :a :b ...]`
;; allowlist — the bounded keyword universe.
;;
;; Per rf2-oyw04 :int coercion is STRICT and host-IDENTICAL: a value
;; coerces to a number only when the WHOLE string is an integer literal
;; (`^-?\d+$`); partial-numeric / radix-prefixed / whitespace-padded input
;; (`12abc`, `0x10`, ` 12`) stays a string on EVERY host. This closes a
;; Spec 011 hydration-mismatch hazard where `Long/parseLong` (strict, JVM)
;; and `js/parseInt` (lenient, CLJS) disagreed on partial-numeric input.
;; The non-coerced string then fails the :int-typed slot, surfacing
;; :validation-failed? true via the layered :query validator.
;;
;; Per [012 §Query strings and fragments]
;; (../../012-Routing.md#query-strings-and-fragments).

{:fixture/id           :routing/query-string-coercion
 :fixture/spec-version "1.0"
 :fixture/capabilities #{:routing/match-url}
 :fixture/doc          "Per-key coercion of query-string values via the route's :query schema. :int parses to a number, an `[:enum :a :b ...]` allowlist interns matching values to keywords (bounded universe — the rf2-3k3o7 keyword-DoS guard), :boolean true/false strings become booleans, :string passes through. :query-defaults populate keys absent from the URL. Optional schema keys without a value also pass through."

 :fixture/registry
 {:route
  {:route/articles
   {:path           "/articles"
    :query          [:map
                     [:q :string]
                     [:page  {:optional true} :int]
                     ;; rf2-3k3o7: bounded keyword universe.
                     [:tag   {:optional true} [:enum :lang :clojure :design]]
                     [:draft? {:optional true} :boolean]]
    :query-defaults {:page 1}}}}

 :fixture/calls
 [;; All four coercion shapes at once.
  {:call :match-url :url "/articles?q=clojure&page=3&tag=lang&draft?=true"
   :expect {:route-id :route/articles
            :params   {}
            :query    {:q "clojure" :page 3 :tag :lang :draft? true}
            :fragment nil
            :validation-failed? false}}

  ;; :boolean false coerces from the literal "false".
  {:call :match-url :url "/articles?q=x&draft?=false"
   :expect {:route-id :route/articles
            :params   {}
            :query    {:q "x" :draft? false :page 1}
            :fragment nil
            :validation-failed? false}}

  ;; :query-defaults populate when the key is absent.
  {:call :match-url :url "/articles?q=y"
   :expect {:route-id :route/articles
            :params   {}
            :query    {:q "y" :page 1}
            :fragment nil
            :validation-failed? false}}

  ;; :int over a non-integer string: coerce-query-value passes through
  ;; on parse failure; rf2-ug2m1 layered validation then catches the
  ;; mismatch against the route's :query schema and surfaces
  ;; :validation-failed? true with a :validation-error explanation.
  ;; Apps that want a soft fallback unregister the validator (Spec 010
  ;; §Non-Malli validators) or register a coerce-and-fix schema.
  {:call :match-url :url "/articles?q=z&page=not-a-number"
   :expect {:route-id :route/articles
            :params   {}
            :query    {:q "z" :page "not-a-number"}
            :fragment nil
            :validation-failed? true}}

  ;; rf2-oyw04 — cross-host :int coercion must be STRICT and IDENTICAL on
  ;; every host. A clean integer literal coerces to a number.
  {:call :match-url :url "/articles?q=p&page=12"
   :expect {:route-id :route/articles
            :params   {}
            :query    {:q "p" :page 12}
            :fragment nil
            :validation-failed? false}}

  ;; rf2-oyw04 — the cross-host hazard case. Partial-numeric input
  ;; (`12abc`) must produce the SAME output server- and client-side. The
  ;; predecessor diverged: `Long/parseLong "12abc"` threw -> string "12abc"
  ;; (JVM) while `js/parseInt "12abc" 10` -> number 12 (CLJS), a Spec 011
  ;; hydration-mismatch class violating Spec 012's "same handler both
  ;; sides" + Spec 000 Goal 2. The canonical semantics: coerce only when
  ;; the whole string is an integer literal (`^-?\d+$`), else string
  ;; passthrough on BOTH hosts. The non-coerced string then fails the
  ;; :int-typed :page slot, surfacing :validation-failed? true — exactly
  ;; like the `not-a-number` case above. Both the JVM
  ;; (conformance_test.clj) and CLJS (conformance_corpus_cljs_test.cljs)
  ;; harnesses run this call, so a host that re-introduced the asymmetry
  ;; fails the corpus.
  {:call :match-url :url "/articles?q=p&page=12abc"
   :expect {:route-id :route/articles
            :params   {}
            :query    {:q "p" :page "12abc"}
            :fragment nil
            :validation-failed? true}}

  ;; route-url's 3-arity emits the query string in input order.
  {:call :route-url :route-id :route/articles :params {}
                    :query {:q "clojure" :page 2}
   :expect "/articles?q=clojure&page=2"}]}
