;; lib/events/tests/timezone.sx — timezones + DST.

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

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

;; Wall-clock (civil + minute-of-day) an occurrence's UTC start maps to in a tz.
(define
  ev-tz-local-of
  (fn
    (tz utc-dt)
    (let
      ((l (ev-tz-utc->local tz utc-dt)))
      (list (ev-dt->civil l) (ev-dt-tod l)))))

(define
  ev-tz-run-all!
  (fn
    ()
    (do
      (let
        ((nyc (ev-tz-fixed "EST" -300)))
        (do
          (ev-tz-check!
            "fixed zone: utc -> local subtracts 5h"
            (ev-tz-utc->local
              nyc
              (ev-dt 2026 1 1 17 0))
            (ev-dt 2026 1 1 12 0))
          (ev-tz-check!
            "fixed zone: local -> utc adds 5h back"
            (ev-tz-local->utc
              nyc
              (ev-dt 2026 1 1 12 0))
            (ev-dt 2026 1 1 17 0))
          (ev-tz-check!
            "UTC zone is identity"
            (ev-tz-local->utc
              ev-tz-utc
              (ev-dt 2026 6 1 9 0))
            (ev-dt 2026 6 1 9 0))))
      (ev-tz-check!
        "London winter offset is 0 (GMT)"
        (ev-tz-offset
          ev-tz-london
          (ev-dt 2026 1 15 12 0))
        0)
      (ev-tz-check!
        "London summer offset is 60 (BST)"
        (ev-tz-offset
          ev-tz-london
          (ev-dt 2026 7 15 12 0))
        60)
      (ev-tz-check!
        "Paris winter offset is 60 (CET)"
        (ev-tz-offset
          ev-tz-paris
          (ev-dt 2026 1 15 12 0))
        60)
      (ev-tz-check!
        "Paris summer offset is 120 (CEST)"
        (ev-tz-offset
          ev-tz-paris
          (ev-dt 2026 7 15 12 0))
        120)
      (ev-tz-check!
        "DST starts last Sunday of March"
        (ev-dt->civil
          (ev-tz-transition
            2026
            (ev-tz-rule 3 -1 6 60)))
        (list 2026 3 29))
      (ev-tz-check!
        "DST ends last Sunday of October"
        (ev-dt->civil
          (ev-tz-transition
            2026
            (ev-tz-rule 10 -1 6 60)))
        (list 2026 10 25))
      (ev-tz-check!
        "09:00 London in winter is 09:00 UTC"
        (ev-tz-local->utc
          ev-tz-london
          (ev-dt 2026 1 15 9 0))
        (ev-dt 2026 1 15 9 0))
      (ev-tz-check!
        "09:00 London in summer is 08:00 UTC"
        (ev-tz-local->utc
          ev-tz-london
          (ev-dt 2026 7 15 9 0))
        (ev-dt 2026 7 15 8 0))
      (ev-tz-check!
        "round trip utc -> local -> utc"
        (ev-tz-local->utc
          ev-tz-london
          (ev-tz-utc->local
            ev-tz-london
            (ev-dt 2026 7 15 8 0)))
        (ev-dt 2026 7 15 8 0))
      (let
        ((ev (ev-event-tz (quote standup) (ev-dt 2026 3 27 9 0) 60 {:freq :daily :count 5} 10 ev-tz-london)))
        (let
          ((occs (ev-expand ev (ev-date 2026 3 1) (ev-date 2026 4 5))))
          (do
            (ev-tz-check!
              "daily occurrences shift in UTC across the DST boundary"
              (map (fn (o) (ev-dt-tod (get o :start))) occs)
              (list 540 540 480 480 480))
            (ev-tz-check!
              "but every occurrence stays 09:00 local wall-clock"
              (map
                (fn
                  (o)
                  (first
                    (rest (ev-tz-local-of ev-tz-london (get o :start)))))
                occs)
              (list 540 540 540 540 540))
            (ev-tz-check!
              "occurrence dates are stable in local time"
              (map
                (fn
                  (o)
                  (ev-civ-d
                    (first (ev-tz-local-of ev-tz-london (get o :start)))))
                occs)
              (list 27 28 29 30 31)))))
      (let
        ((wk (ev-event-tz (quote class) (ev-dt 2026 3 23 18 0) 90 {:freq :weekly :byday (list 0)} 5 ev-tz-london)))
        (let
          ((occs (ev-expand wk (ev-date 2026 3 1) (ev-date 2026 4 20))))
          (ev-tz-check!
            "weekly Monday 18:00 London stays 18:00 local each week"
            (map
              (fn
                (o)
                (first (rest (ev-tz-local-of ev-tz-london (get o :start)))))
              occs)
            (list 1080 1080 1080 1080))))
      (let
        ((plain (ev-event (quote p) (ev-dt 2026 3 27 9 0) 60 {:freq :daily :count 3} 1)))
        (ev-tz-check!
          "plain event expands naively (no UTC shift)"
          (map
            (fn (o) (ev-dt-tod (get o :start)))
            (ev-expand
              plain
              (ev-date 2026 3 1)
              (ev-date 2026 4 5)))
          (list 540 540 540))))))

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