;; lib/dream/tests/session.sx — cookies, store, session round-trip, signed cookies.

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

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

;; ── cookie parsing ─────────────────────────────────────────────────
(define dream-ss-creq (dream-request "GET" "/" {:Cookie "a=1; b=2; dream.session=s9"} ""))
(dream-ss-test "parse cookie a" (dream-cookie dream-ss-creq "a") "1")
(dream-ss-test "parse cookie b" (dream-cookie dream-ss-creq "b") "2")
(dream-ss-test
  "parse session cookie"
  (dream-cookie dream-ss-creq "dream.session")
  "s9")
(dream-ss-test "missing cookie nil" (dream-cookie dream-ss-creq "z") nil)
(dream-ss-test
  "no cookie header"
  (dream-cookie (dream-request "GET" "/" {} "") "a")
  nil)

;; ── cookie building ────────────────────────────────────────────────
(dream-ss-test
  "build basic cookie"
  (dr/build-cookie "k" "v" {})
  "k=v; Path=/")
(dream-ss-test
  "build httponly samesite"
  (dr/build-cookie "sid" "x" {:http-only true :same-site "Lax"})
  "sid=x; Path=/; HttpOnly; SameSite=Lax")
(dream-ss-test
  "build max-age"
  (dr/build-cookie "k" "v" {:max-age 0})
  "k=v; Path=/; Max-Age=0")
(dream-ss-test
  "set-cookie appends"
  (len
    (dream-resp-cookies
      (dream-set-cookie (dream-html "x") "k" "v" {})))
  1)
(dream-ss-test
  "set-cookie two"
  (len
    (dream-resp-cookies
      (dream-set-cookie
        (dream-set-cookie (dream-html "x") "a" "1" {})
        "b"
        "2"
        {})))
  2)
(dream-ss-test
  "drop cookie max-age 0"
  (contains?
    (first (dream-resp-cookies (dream-drop-cookie (dream-html "x") "k")))
    "Max-Age=0")
  true)

;; ── signed cookie values ───────────────────────────────────────────
(dream-ss-test
  "sign/unsign roundtrip"
  (dream-cookie-unsign "k" (dream-cookie-sign "k" "s5"))
  "s5")
(dream-ss-test
  "unsign wrong secret"
  (dream-cookie-unsign "k2" (dream-cookie-sign "k" "s5"))
  nil)
(dream-ss-test "unsign tampered" (dream-cookie-unsign "k" "s5.999") nil)
(dream-ss-test "unsign no dot" (dream-cookie-unsign "k" "s5") nil)
(dream-ss-test "unsign nil" (dream-cookie-unsign "k" nil) nil)

;; ── in-memory store ────────────────────────────────────────────────
(define dream-ss-store (dream-memory-sessions))
(define dream-ss-sid (dream-ss-store {:op "session/create"}))
(dream-ss-test "create returns id" dream-ss-sid "s1")
(dream-ss-test "new session exists" (dream-ss-store {:op "session/exists" :sid "s1"}) true)
(dream-ss-test "absent session not exists" (dream-ss-store {:op "session/exists" :sid "s99"}) false)
(dream-ss-test "get missing key nil" (dream-ss-store {:key "k" :op "session/get" :sid "s1"}) nil)
(dream-ss-store {:val "ada" :key "user" :op "session/set" :sid "s1"})
(dream-ss-test "set then get" (dream-ss-store {:key "user" :op "session/get" :sid "s1"}) "ada")
(dream-ss-store {:val "admin" :key "role" :op "session/set" :sid "s1"})
(dream-ss-test "load all fields" (dream-ss-store {:op "session/load" :sid "s1"}) {:role "admin" :user "ada"})
(dream-ss-test "second create distinct" (dream-ss-store {:op "session/create"}) "s2")
(dream-ss-store {:op "session/clear" :sid "s1"})
(dream-ss-test "clear removes" (dream-ss-store {:op "session/exists" :sid "s1"}) false)

;; ── middleware round-trip ──────────────────────────────────────────
(define dream-ss-backend (dream-memory-sessions))
(define
  dream-ss-counter-h
  (fn
    (req)
    (let
      ((n (or (dream-session-field req "count") 0)))
      (begin
        (dream-set-session-field req "count" (+ n 1))
        (dream-text (str "count=" (+ n 1)))))))
(define dream-ss-app ((dream-sessions dream-ss-backend) dream-ss-counter-h))

(define dream-ss-r1 (dream-ss-app (dream-request "GET" "/" {} "")))
(dream-ss-test "first body count=1" (dream-resp-body dream-ss-r1) "count=1")
(dream-ss-test
  "first sets one cookie"
  (len (dream-resp-cookies dream-ss-r1))
  1)
(dream-ss-test
  "session cookie name+id"
  (contains? (first (dream-resp-cookies dream-ss-r1)) "dream.session=s1")
  true)
(dream-ss-test
  "session cookie httponly"
  (contains? (first (dream-resp-cookies dream-ss-r1)) "HttpOnly")
  true)

(define dream-ss-r2 (dream-ss-app (dream-request "GET" "/" {:Cookie "dream.session=s1"} "")))
(dream-ss-test "second body count=2" (dream-resp-body dream-ss-r2) "count=2")
(dream-ss-test
  "second sets no cookie"
  (len (dream-resp-cookies dream-ss-r2))
  0)

(define dream-ss-r3 (dream-ss-app (dream-request "GET" "/" {:Cookie "dream.session=s1"} "")))
(dream-ss-test "third body count=3" (dream-resp-body dream-ss-r3) "count=3")

(define dream-ss-r4 (dream-ss-app (dream-request "GET" "/" {:Cookie "dream.session=bogus"} "")))
(dream-ss-test
  "bogus id starts fresh"
  (dream-resp-body dream-ss-r4)
  "count=1")
(dream-ss-test
  "bogus id gets new cookie"
  (len (dream-resp-cookies dream-ss-r4))
  1)

;; ── session-all + invalidate via middleware ────────────────────────
(dream-ss-test
  "session-all shows count"
  (dream-session-all
    (assoc (dream-request "GET" "/" {} "") :dream-session {:io dream-ss-backend :sid "s1"}))
  {:count 3})

(define
  dream-ss-invalidate-h
  (fn (req) (begin (dream-invalidate-session req) (dream-text "bye"))))
(define
  dream-ss-app3
  ((dream-sessions dream-ss-backend) dream-ss-invalidate-h))
(dream-ss-app3 (dream-request "GET" "/" {:Cookie "dream.session=s1"} ""))
(dream-ss-test "invalidate clears store" (dream-ss-backend {:op "session/exists" :sid "s1"}) false)

;; ── signed session middleware ──────────────────────────────────────
(define dream-ss-sbackend (dream-memory-sessions))
(define
  dream-ss-sapp
  ((dream-sessions-signed dream-ss-sbackend "topsecret")
    (fn (req) (dream-text (dream-session-id req)))))

(define dream-ss-sr1 (dream-ss-sapp (dream-request "GET" "/" {} "")))
(dream-ss-test "signed first sid" (dream-resp-body dream-ss-sr1) "s1")
(dream-ss-test
  "signed cookie is signed"
  (contains? (first (dream-resp-cookies dream-ss-sr1)) "dream.session=s1.")
  true)

;; forged plaintext sid (no signature) is rejected -> a fresh session is made
(dream-ss-test
  "forged plaintext rejected -> new session"
  (dream-resp-body (dream-ss-sapp (dream-request "GET" "/" {:Cookie "dream.session=s1"} "")))
  "s2")

;; a validly-signed cookie reuses the session
(define dream-ss-signed-val (dream-cookie-sign "topsecret" "s1"))
(define dream-ss-sr3 (dream-ss-sapp (dream-request "GET" "/" {:Cookie (str "dream.session=" dream-ss-signed-val)} "")))
(dream-ss-test "valid signed reuses s1" (dream-resp-body dream-ss-sr3) "s1")
(dream-ss-test
  "valid signed sets no new cookie"
  (len (dream-resp-cookies dream-ss-sr3))
  0)

;; a cookie signed with the wrong secret is rejected
(dream-ss-test
  "wrong-secret signed rejected"
  (=
    (dream-resp-body (dream-ss-sapp (dream-request "GET" "/" {:Cookie (str "dream.session=" (dream-cookie-sign "other" "s1"))} "")))
    "s1")
  false)

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