;; lib/events/tests/booking.sx — capacity-safe booking, cancel, and holds.

(define ev-bk-pass 0)
(define ev-bk-fail 0)
(define ev-bk-failures (list))

(define
  ev-bk-check!
  (fn
    (name got expected)
    (if
      (= got expected)
      (set! ev-bk-pass (+ ev-bk-pass 1))
      (do
        (set! ev-bk-fail (+ ev-bk-fail 1))
        (append!
          ev-bk-failures
          (str name "\n    expected: " expected "\n    got:      " got))))))

;; Take a consistent (roster, last-seq) snapshot of an occurrence's stream.
(define ev-bk-snap (fn (b k) (ev-booked-actors b k)))
(define ev-bk-seq (fn (b k) (persist/last-seq b (ev-booking-stream k))))

(define
  ev-bk-run-all!
  (fn
    ()
    (do
      (let
        ((b (persist/open)))
        (do
          (ev-bk-check!
            "first booking takes seat 1"
            (get (ev/book! b "o1" 3 (quote a)) :seat)
            1)
          (ev-bk-check!
            "second booking takes seat 2"
            (get (ev/book! b "o1" 3 (quote c)) :seat)
            2)
          (ev-bk-check!
            "booked status reported"
            (get (ev/book! b "o1" 3 (quote d)) :status)
            :booked)
          (ev-bk-check!
            "roster is oldest-first"
            (ev/roster b "o1")
            (list (quote a) (quote c) (quote d)))
          (ev-bk-check!
            "seats-left is zero when full"
            (ev/seats-left b "o1" 3)
            0)
          (ev-bk-check!
            "free booking is confirmed state"
            (ev/seat-state b "o1" (quote a))
            :confirmed)))
      (let
        ((b (persist/open)))
        (do
          (ev/book! b "o2" 1 (quote a))
          (ev-bk-check!
            "booking past capacity is refused"
            (get (ev/book! b "o2" 1 (quote c)) :status)
            :full)
          (ev-bk-check!
            "full does not grow the roster"
            (ev/roster b "o2")
            (list (quote a)))
          (ev-bk-check!
            "seats-left zero at capacity"
            (ev/seats-left b "o2" 1)
            0)))
      (let
        ((b (persist/open)))
        (do
          (ev/book! b "o3" 5 (quote a))
          (ev-bk-check!
            "re-booking the same actor is idempotent"
            (get (ev/book! b "o3" 5 (quote a)) :status)
            :already)
          (ev-bk-check!
            "idempotent re-book reports existing seat"
            (get (ev/book! b "o3" 5 (quote a)) :seat)
            1)
          (ev-bk-check!
            "roster unchanged after re-book"
            (ev/roster b "o3")
            (list (quote a)))
          (ev-bk-check!
            "count unchanged after re-book"
            (ev-booking-count b "o3")
            1)))
      (let
        ((b (persist/open)))
        (do
          (ev/book! b "last" 2 (quote x))
          (let
            ((snap (ev-bk-snap b "last")) (exp (ev-bk-seq b "last")))
            (let
              ((ra (ev/book-with-observed b "last" 2 (quote a) snap exp))
                (rb
                  (ev/book-with-observed
                    b
                    "last"
                    2
                    (quote bee)
                    snap
                    exp)))
              (do
                (ev-bk-check!
                  "race winner is booked"
                  (get ra :status)
                  :booked)
                (ev-bk-check!
                  "race winner takes the last seat"
                  (get ra :seat)
                  2)
                (ev-bk-check!
                  "race loser is rejected with a conflict"
                  (get rb :status)
                  :conflict)
                (ev-bk-check!
                  "conflict reports the advanced seq"
                  (get rb :actual)
                  (+ exp 1))
                (ev-bk-check!
                  "no overbooking: exactly two on roster"
                  (ev-booking-count b "last")
                  2)
                (ev-bk-check!
                  "race loser is NOT on the roster"
                  (ev-bk-member? (quote bee) (ev/roster b "last"))
                  false)
                (ev-bk-check!
                  "race loser retrying gets full"
                  (get (ev/book! b "last" 2 (quote bee)) :status)
                  :full))))))
      (let
        ((b (persist/open)))
        (do
          (ev/book! b "room" 3 (quote x))
          (let
            ((snap (ev-bk-snap b "room")) (exp (ev-bk-seq b "room")))
            (let
              ((ra (ev/book-with-observed b "room" 3 (quote a) snap exp))
                (rb
                  (ev/book-with-observed
                    b
                    "room"
                    3
                    (quote bee)
                    snap
                    exp)))
              (do
                (ev-bk-check!
                  "room winner booked seat 2"
                  (get ra :seat)
                  2)
                (ev-bk-check!
                  "room loser first conflicts"
                  (get rb :status)
                  :conflict)
                (ev-bk-check!
                  "room loser retry books seat 3"
                  (get (ev/book! b "room" 3 (quote bee)) :seat)
                  3)
                (ev-bk-check!
                  "room roster is x,a,bee"
                  (ev/roster b "room")
                  (list (quote x) (quote a) (quote bee)))
                (ev-bk-check!
                  "room is now full"
                  (ev/seats-left b "room" 3)
                  0))))))
      (let
        ((b (persist/open)))
        (do
          (ev/book! b "cx" 2 (quote a))
          (ev/book! b "cx" 2 (quote c))
          (ev-bk-check!
            "occupied to capacity before cancel"
            (ev/seats-left b "cx" 2)
            0)
          (ev-bk-check!
            "booking when full (pre-cancel) is refused"
            (get (ev/book! b "cx" 2 (quote d)) :status)
            :full)
          (ev-bk-check!
            "cancel reports cancelled"
            (get (ev/cancel! b "cx" (quote a)) :status)
            :cancelled)
          (ev-bk-check!
            "cancel removes actor from roster"
            (ev/roster b "cx")
            (list (quote c)))
          (ev-bk-check!
            "cancel frees a seat"
            (ev/seats-left b "cx" 2)
            1)
          (ev-bk-check!
            "freed seat is bookable again"
            (get (ev/book! b "cx" 2 (quote d)) :status)
            :booked)
          (ev-bk-check!
            "roster after rebook is c,d"
            (ev/roster b "cx")
            (list (quote c) (quote d)))))
      (let
        ((b (persist/open)))
        (do
          (ev/book! b "ce" 3 (quote a))
          (ev-bk-check!
            "cancelling an unbooked actor is a no-op"
            (get (ev/cancel! b "ce" (quote z)) :status)
            :not-booked)
          (ev-bk-check!
            "no-op cancel leaves roster intact"
            (ev/roster b "ce")
            (list (quote a)))
          (ev/cancel! b "ce" (quote a))
          (ev-bk-check!
            "double cancel is not-booked the second time"
            (get (ev/cancel! b "ce" (quote a)) :status)
            :not-booked)
          (ev-bk-check!
            "empty roster after cancel"
            (ev/roster b "ce")
            (list))
          (ev-bk-check!
            "cancelled actor may re-book"
            (get (ev/book! b "ce" 3 (quote a)) :status)
            :booked)
          (ev-bk-check!
            "re-booked actor back on roster"
            (ev/roster b "ce")
            (list (quote a)))))
      (let
        ((b (persist/open)))
        (do
          (ev/book! b "h" 2 (quote a))
          (ev-bk-check!
            "hold reports held"
            (get (ev/hold! b "h" 2 (quote p)) :status)
            :held)
          (ev-bk-check!
            "held seat is :held state"
            (ev/seat-state b "h" (quote p))
            :held)
          (ev-bk-check!
            "held actor is on the roster"
            (ev/roster b "h")
            (list (quote a) (quote p)))
          (ev-bk-check!
            "held seat blocks the last booking"
            (get (ev/book! b "h" 2 (quote x)) :status)
            :full)
          (ev-bk-check!
            "no seats left with one held"
            (ev/seats-left b "h" 2)
            0)))
      (let
        ((b (persist/open)))
        (do
          (ev/hold! b "hc" 3 (quote p))
          (ev-bk-check!
            "confirm reports confirmed"
            (get (ev/confirm! b "hc" (quote p)) :status)
            :confirmed)
          (ev-bk-check!
            "confirmed seat is :confirmed state"
            (ev/seat-state b "hc" (quote p))
            :confirmed)
          (ev-bk-check!
            "re-confirm is already-confirmed"
            (get (ev/confirm! b "hc" (quote p)) :status)
            :already-confirmed)
          (ev-bk-check!
            "confirming a non-holder is not-held"
            (get (ev/confirm! b "hc" (quote z)) :status)
            :not-held)
          (ev-bk-check!
            "confirmed seat still occupies"
            (ev/seats-left b "hc" 3)
            2)))
      (let
        ((b (persist/open)))
        (do
          (ev/book! b "hr" 2 (quote a))
          (ev/hold! b "hr" 2 (quote p))
          (ev-bk-check!
            "full while hold pending"
            (ev/seats-left b "hr" 2)
            0)
          (ev-bk-check!
            "release reports released"
            (get (ev/release! b "hr" (quote p)) :status)
            :released)
          (ev-bk-check!
            "release frees the held seat"
            (ev/seats-left b "hr" 2)
            1)
          (ev-bk-check!
            "released actor off the roster"
            (ev/roster b "hr")
            (list (quote a)))
          (ev-bk-check!
            "freed seat bookable after release"
            (get (ev/book! b "hr" 2 (quote x)) :status)
            :booked)
          (ev/hold! b "hr2" 1 (quote q))
          (ev/confirm! b "hr2" (quote q))
          (ev-bk-check!
            "release on a confirmed seat is not-held"
            (get (ev/release! b "hr2" (quote q)) :status)
            :not-held)
          (ev-bk-check!
            "cancel frees a confirmed-from-hold seat"
            (get (ev/cancel! b "hr2" (quote q)) :status)
            :cancelled)))
      (let
        ((b (persist/open)))
        (do
          (ev/book! b "hlast" 2 (quote x))
          (let
            ((snap (ev-bk-snap b "hlast")) (exp (ev-bk-seq b "hlast")))
            (let
              ((ra (ev/hold-with-observed b "hlast" 2 (quote p) snap exp))
                (rb
                  (ev/hold-with-observed
                    b
                    "hlast"
                    2
                    (quote q)
                    snap
                    exp)))
              (do
                (ev-bk-check! "hold race winner held" (get ra :status) :held)
                (ev-bk-check!
                  "hold race loser conflicts"
                  (get rb :status)
                  :conflict)
                (ev-bk-check!
                  "no oversell via concurrent holds"
                  (ev-booking-count b "hlast")
                  2)
                (ev-bk-check!
                  "hold loser retry gets full"
                  (get (ev/hold! b "hlast" 2 (quote q)) :status)
                  :full))))))
      (let
        ((b (persist/open)))
        (do
          (ev/hold! b "hi" 4 (quote p))
          (ev-bk-check!
            "re-holding the same actor is idempotent"
            (get (ev/hold! b "hi" 4 (quote p)) :status)
            :already)
          (ev-bk-check!
            "hold idempotency keeps one seat"
            (ev-booking-count b "hi")
            1))))))

;; ---- waitlist ----
(define
  ev-bk-wl-run-all!
  (fn
    ()
    (do
      ;; join the waitlist when full; book directly when a seat is free
      (let
        ((b (persist/open)))
        (do
          (ev-bk-check! "waitlist! books when a seat is free" (get (ev/waitlist! b "w" 2 (quote a)) :status) :booked)
          (ev-bk-check! "second booking still fits" (get (ev/waitlist! b "w" 2 (quote c)) :status) :booked)
          (ev-bk-check! "third joins the waitlist when full" (get (ev/waitlist! b "w" 2 (quote x)) :status) :waitlisted)
          (ev-bk-check! "fourth is next in line" (get (ev/waitlist! b "w" 2 (quote y)) :position) 2)
          (ev-bk-check! "waitlist is FIFO" (ev/waitlist b "w") (list (quote x) (quote y)))
          (ev-bk-check! "seats unaffected by waitlisting" (ev/roster b "w") (list (quote a) (quote c)))
          (ev-bk-check! "waitlist-position reports a queued actor" (ev/waitlist-position b "w" (quote y)) 2)
          (ev-bk-check! "waitlist-position 0 for a seated actor" (ev/waitlist-position b "w" (quote a)) 0)))
      ;; idempotency
      (let
        ((b (persist/open)))
        (do
          (ev/waitlist! b "wi" 1 (quote a))
          (ev/waitlist! b "wi" 1 (quote x))
          (ev-bk-check! "re-joining as a seated actor is :already" (get (ev/waitlist! b "wi" 1 (quote a)) :status) :already)
          (ev-bk-check! "re-joining the queue is :already-waiting" (get (ev/waitlist! b "wi" 1 (quote x)) :status) :already-waiting)
          (ev-bk-check! "queue did not grow on re-join" (ev/waitlist b "wi") (list (quote x)))))
      ;; leaving the waitlist
      (let
        ((b (persist/open)))
        (do
          (ev/waitlist! b "wl" 1 (quote a))
          (ev/waitlist! b "wl" 1 (quote x))
          (ev/waitlist! b "wl" 1 (quote y))
          (ev-bk-check! "leave-waitlist reports left" (get (ev/leave-waitlist! b "wl" (quote x)) :status) :left)
          (ev-bk-check! "leaving removes from the queue" (ev/waitlist b "wl") (list (quote y)))
          (ev-bk-check! "leaving when not queued is not-waiting" (get (ev/leave-waitlist! b "wl" (quote z)) :status) :not-waiting)))
      ;; auto-promotion on cancel
      (let
        ((b (persist/open)))
        (do
          (ev/waitlist! b "wp" 1 (quote a))
          (ev/waitlist! b "wp" 1 (quote x))
          (ev/waitlist! b "wp" 1 (quote y))
          (let
            ((r (ev/cancel-promote! b "wp" 1 (quote a))))
            (do
              (ev-bk-check! "cancel-promote cancels the seat holder" (get r :status) :cancelled)
              (ev-bk-check! "cancel-promote promotes the head of the queue" (get r :promoted) (quote x))))
          (ev-bk-check! "promoted actor now holds the seat" (ev/roster b "wp") (list (quote x)))
          (ev-bk-check! "promoted actor left the queue" (ev/waitlist b "wp") (list (quote y)))
          (ev-bk-check! "promoted seat is confirmed" (ev/seat-state b "wp" (quote x)) :confirmed)
          ;; cancelling with an empty waitlist promotes nobody
          (ev/leave-waitlist! b "wp" (quote y))
          (let
            ((r2 (ev/cancel-promote! b "wp" 1 (quote x))))
            (ev-bk-check! "cancel with empty waitlist promotes nobody" (get r2 :promoted) nil))
          (ev-bk-check! "seat is free after the last cancel" (ev/seats-left b "wp" 1) 1))))))

(define
  ev-booking-tests-run!
  (fn
    ()
    (do
      (set! ev-bk-pass 0)
      (set! ev-bk-fail 0)
      (set! ev-bk-failures (list))
      (ev-bk-run-all!)
      (ev-bk-wl-run-all!)
      {:failures ev-bk-failures :total (+ ev-bk-pass ev-bk-fail) :passed ev-bk-pass :failed ev-bk-fail})))
