;; lib/dream/tests/router.sx — routing dispatch, path params, scopes, 405/HEAD.

(define dream-rt-pass 0)
(define dream-rt-fail 0)
(define dream-rt-fails (list))

(define
  dream-rt-test
  (fn
    (name actual expected)
    (if
      (= actual expected)
      (set! dream-rt-pass (+ dream-rt-pass 1))
      (begin
        (set! dream-rt-fail (+ dream-rt-fail 1))
        (append! dream-rt-fails {:name name :actual actual :expected expected})))))

(define
  dream-rt-req
  (fn (method target) (dream-request method target {} "")))

;; ── basic dispatch ─────────────────────────────────────────────────
(define
  dream-rt-app
  (dream-router
    (list
      (dream-get "/" (fn (req) (dream-text "home")))
      (dream-get "/about" (fn (req) (dream-text "about")))
      (dream-post "/submit" (fn (req) (dream-text "posted"))))))

(dream-rt-test
  "GET / -> home"
  (dream-resp-body (dream-rt-app (dream-rt-req "GET" "/")))
  "home")
(dream-rt-test
  "GET /about"
  (dream-resp-body (dream-rt-app (dream-rt-req "GET" "/about")))
  "about")
(dream-rt-test
  "POST /submit"
  (dream-resp-body (dream-rt-app (dream-rt-req "POST" "/submit")))
  "posted")
(dream-rt-test
  "unknown path 404"
  (dream-status (dream-rt-app (dream-rt-req "GET" "/nope")))
  404)
(dream-rt-test
  "wrong method 405"
  (dream-status (dream-rt-app (dream-rt-req "GET" "/submit")))
  405)
(dream-rt-test
  "trailing slash equiv"
  (dream-resp-body (dream-rt-app (dream-rt-req "GET" "/about/")))
  "about")
(dream-rt-test
  "query ignored for routing"
  (dream-resp-body (dream-rt-app (dream-rt-req "GET" "/about?x=1")))
  "about")

;; ── path params ────────────────────────────────────────────────────
(define
  dream-rt-papp
  (dream-router
    (list
      (dream-get
        "/users/:id"
        (fn (req) (dream-text (dream-param req "id"))))
      (dream-get
        "/users/:id/posts/:pid"
        (fn
          (req)
          (dream-text
            (str (dream-param req "id") "-" (dream-param req "pid")))))
      (dream-get
        "/files/**"
        (fn (req) (dream-text (dream-param req "**")))))))

(dream-rt-test
  "single param"
  (dream-resp-body (dream-rt-papp (dream-rt-req "GET" "/users/42")))
  "42")
(dream-rt-test
  "two params"
  (dream-resp-body (dream-rt-papp (dream-rt-req "GET" "/users/7/posts/9")))
  "7-9")
(dream-rt-test
  "param no over-match"
  (dream-status (dream-rt-papp (dream-rt-req "GET" "/users/7/extra")))
  404)
(dream-rt-test
  "catch-all captures rest"
  (dream-resp-body (dream-rt-papp (dream-rt-req "GET" "/files/a/b/c.txt")))
  "a/b/c.txt")
(dream-rt-test
  "catch-all empty rest"
  (dream-resp-body (dream-rt-papp (dream-rt-req "GET" "/files/")))
  "")

;; ── route order: first match wins ──────────────────────────────────
(define
  dream-rt-order
  (dream-router
    (list
      (dream-get "/x/specific" (fn (req) (dream-text "specific")))
      (dream-get "/x/:slug" (fn (req) (dream-text "generic"))))))
(dream-rt-test
  "first match wins"
  (dream-resp-body (dream-rt-order (dream-rt-req "GET" "/x/specific")))
  "specific")
(dream-rt-test
  "fallthrough to param"
  (dream-resp-body (dream-rt-order (dream-rt-req "GET" "/x/other")))
  "generic")

;; ── ANY method ─────────────────────────────────────────────────────
(define
  dream-rt-any
  (dream-router
    (list (dream-any "/ping" (fn (req) (dream-text (dream-method req)))))))
(dream-rt-test
  "ANY matches GET"
  (dream-resp-body (dream-rt-any (dream-rt-req "GET" "/ping")))
  "GET")
(dream-rt-test
  "ANY matches DELETE"
  (dream-resp-body (dream-rt-any (dream-rt-req "DELETE" "/ping")))
  "DELETE")

;; ── handler returns bare string (coerced) ──────────────────────────
(define
  dream-rt-coerce
  (dream-router (list (dream-get "/s" (fn (req) "bare")))))
(dream-rt-test
  "string coerced to 200"
  (dream-status (dream-rt-coerce (dream-rt-req "GET" "/s")))
  200)
(dream-rt-test
  "string coerced body"
  (dream-resp-body (dream-rt-coerce (dream-rt-req "GET" "/s")))
  "bare")

;; ── scope: prefix mount ────────────────────────────────────────────
(define
  dream-rt-scoped
  (dream-router
    (list
      (dream-get "/" (fn (req) (dream-text "root")))
      (dream-scope
        "/api"
        (list)
        (list
          (dream-get "/users" (fn (req) (dream-text "api-users")))
          (dream-get
            "/users/:id"
            (fn
              (req)
              (dream-text (str "api-user-" (dream-param req "id"))))))))))
(dream-rt-test
  "scope root still works"
  (dream-resp-body (dream-rt-scoped (dream-rt-req "GET" "/")))
  "root")
(dream-rt-test
  "scope prefix path"
  (dream-resp-body (dream-rt-scoped (dream-rt-req "GET" "/api/users")))
  "api-users")
(dream-rt-test
  "scope prefix param"
  (dream-resp-body (dream-rt-scoped (dream-rt-req "GET" "/api/users/5")))
  "api-user-5")
(dream-rt-test
  "scope unprefixed 404"
  (dream-status (dream-rt-scoped (dream-rt-req "GET" "/users")))
  404)

;; ── scope: middleware applied to all routes ────────────────────────
(define
  dream-rt-mw
  (fn (next) (fn (req) (dream-add-header (next req) "X-Scope" "on"))))
(define
  dream-rt-mwapp
  (dream-router
    (list
      (dream-scope
        "/v1"
        (list dream-rt-mw)
        (list (dream-get "/a" (fn (req) (dream-text "a"))))))))
(dream-rt-test
  "scope mw header"
  (dream-resp-header (dream-rt-mwapp (dream-rt-req "GET" "/v1/a")) "x-scope")
  "on")
(dream-rt-test
  "scope mw body intact"
  (dream-resp-body (dream-rt-mwapp (dream-rt-req "GET" "/v1/a")))
  "a")

;; ── nested scopes ──────────────────────────────────────────────────
(define
  dream-rt-outer
  (fn (next) (fn (req) (dream-add-header (next req) "X-Outer" "1"))))
(define
  dream-rt-inner
  (fn (next) (fn (req) (dream-add-header (next req) "X-Inner" "1"))))
(define
  dream-rt-nested
  (dream-router
    (list
      (dream-scope
        "/api"
        (list dream-rt-outer)
        (list
          (dream-scope
            "/v2"
            (list dream-rt-inner)
            (list (dream-get "/thing" (fn (req) (dream-text "thing"))))))))))
(dream-rt-test
  "nested path"
  (dream-resp-body (dream-rt-nested (dream-rt-req "GET" "/api/v2/thing")))
  "thing")
(dream-rt-test
  "nested outer mw"
  (dream-resp-header
    (dream-rt-nested (dream-rt-req "GET" "/api/v2/thing"))
    "x-outer")
  "1")
(dream-rt-test
  "nested inner mw"
  (dream-resp-header
    (dream-rt-nested (dream-rt-req "GET" "/api/v2/thing"))
    "x-inner")
  "1")

;; ── 405 Method Not Allowed + Allow ─────────────────────────────────
(define
  dream-rt-mapp
  (dream-router
    (list
      (dream-get "/r" (fn (req) (dream-text "get")))
      (dream-post "/r" (fn (req) (dream-text "post")))
      (dream-get "/only" (fn (req) (dream-html "<p>hi</p>"))))))
(define dream-rt-405 (dream-rt-mapp (dream-rt-req "DELETE" "/r")))
(dream-rt-test "405 status" (dream-status dream-rt-405) 405)
(dream-rt-test
  "405 Allow has GET"
  (contains? (dream-resp-header dream-rt-405 "allow") "GET")
  true)
(dream-rt-test
  "405 Allow has POST"
  (contains? (dream-resp-header dream-rt-405 "allow") "POST")
  true)
(dream-rt-test
  "matching method still works"
  (dream-resp-body (dream-rt-mapp (dream-rt-req "POST" "/r")))
  "post")
(dream-rt-test
  "no path is 404 not 405"
  (dream-status (dream-rt-mapp (dream-rt-req "DELETE" "/absent")))
  404)

;; ── automatic HEAD (serve GET, empty body) ─────────────────────────
(define dream-rt-head (dream-rt-mapp (dream-rt-req "HEAD" "/only")))
(dream-rt-test "HEAD status 200" (dream-status dream-rt-head) 200)
(dream-rt-test "HEAD empty body" (dream-resp-body dream-rt-head) "")
(dream-rt-test
  "HEAD keeps content-type"
  (dream-resp-header dream-rt-head "content-type")
  "text/html; charset=utf-8")
(dream-rt-test
  "HEAD on missing path 404"
  (dream-status (dream-rt-mapp (dream-rt-req "HEAD" "/none")))
  404)

(define dream-rt-tests-run! (fn () {:total (+ dream-rt-pass dream-rt-fail) :passed dream-rt-pass :failed dream-rt-fail :fails dream-rt-fails}))
