; Phase 3 — aggregation + ranking. (feed-test name got expected)

; ---------- aggregation ----------

(define
  A
  (feed/stream
    (list
      (feed/activity "alice" "post" "p1" 5 (list))
      (feed/activity "alice" "post" "p2" 15 (list))
      (feed/activity "bob" "post" "p3" 25 (list))
      (feed/activity "alice" "like" "p1" 35 (list)))))

(feed-test "actor-counts" (feed/actor-counts A) {:alice 3 :bob 1})
(feed-test "object-counts" (feed/object-counts A) {:p2 1 :p3 1 :p1 2})
(feed-test
  "group-by actor alice len"
  (len (get (feed/group-by A feed/actor) "alice"))
  3)
(feed-test
  "group-count empty"
  (feed/group-count feed/empty feed/actor)
  {})

; day bucketing
(define
  D
  (feed/stream
    (list
      (feed/activity "alice" "post" "p1" 5 (list))
      (feed/activity "alice" "post" "p2" 8 (list))
      (feed/activity "alice" "post" "p3" 12 (list)))))

(feed-test "feed/day floor" (feed/day 12 10) 1)
(feed-test "feed/day same bucket" (feed/day 8 10) 0)
(feed-test "by-actor-day" (feed/by-actor-day D 10) {:alice#0 2 :alice#1 1})

; ---------- recency ----------

(define rec (feed/recency 100 10))
(feed-test
  "recency at=now -> 1"
  (rec (feed/activity "x" "post" "o" 100 (list)))
  1)
(feed-test
  "recency age=hl -> .5"
  (rec (feed/activity "x" "post" "o" 90 (list)))
  0.5)
(feed-test
  "recency age=2hl -> .25"
  (rec (feed/activity "x" "post" "o" 80 (list)))
  0.25)

; ---------- velocity ----------

(define vel (feed/velocity D 10))
(feed-test
  "velocity burst (at=12)"
  (vel (feed/activity "alice" "post" "z" 12 (list)))
  3)
(feed-test
  "velocity mid (at=8)"
  (vel (feed/activity "alice" "post" "z" 8 (list)))
  2)
(feed-test
  "velocity first (at=5)"
  (vel (feed/activity "alice" "post" "z" 5 (list)))
  1)
(feed-test
  "velocity other actor"
  (vel (feed/activity "bob" "post" "z" 12 (list)))
  0)

; ---------- engagement ----------

(define eng (feed/engagement A))
(feed-test
  "engagement p1"
  (eng (feed/activity "x" "post" "p1" 0 (list)))
  2)
(feed-test
  "engagement p2"
  (eng (feed/activity "x" "post" "p2" 0 (list)))
  1)

; ---------- composite ----------

(define
  cmp1
  (feed/composite (list (list 2 (fn (a) (get a :at))))))
(feed-test
  "composite single part"
  (cmp1 (feed/activity "x" "post" "o" 5 (list)))
  10)
(define
  cmp2
  (feed/composite
    (list
      (list 2 (fn (a) (get a :at)))
      (list 3 (fn (a) 1)))))
(feed-test
  "composite two parts"
  (cmp2 (feed/activity "x" "post" "o" 5 (list)))
  13)

; ---------- ranking ----------

(define
  R
  (feed/stream
    (list
      (feed/activity "u" "post" "oC" 80 (list))
      (feed/activity "u" "post" "oA" 100 (list))
      (feed/activity "u" "post" "oB" 90 (list)))))

(feed-test
  "rank by recency objects"
  (map (fn (a) (get a :object)) (feed/items (feed/rank R rec)))
  (list "oA" "oB" "oC"))
(feed-test
  "top-2 by recency"
  (map (fn (a) (get a :object)) (feed/items (feed/top R rec 2)))
  (list "oA" "oB"))
(feed-test "top-2 count" (feed/count (feed/top R rec 2)) 2)

; constant score -> tiebreak by :at descending
(define
  T
  (feed/stream
    (list
      (feed/activity "u" "post" "f" 10 (list))
      (feed/activity "u" "post" "g" 30 (list))
      (feed/activity "u" "post" "h" 20 (list)))))
(feed-test
  "tiebreak at-desc"
  (map
    (fn (a) (get a :object))
    (feed/items (feed/rank T (fn (a) 0))))
  (list "g" "h" "f"))

; equal score AND equal :at -> stable input order
(define
  E
  (feed/stream
    (list
      (feed/activity "u" "post" "first" 50 (list))
      (feed/activity "u" "post" "second" 50 (list)))))
(feed-test
  "stable equal-key input order"
  (map
    (fn (a) (get a :object))
    (feed/items (feed/rank E (fn (a) 0))))
  (list "first" "second"))

(feed-test
  "with-scores attaches score"
  (get (nth (feed/items (feed/with-scores R rec)) 1) :score)
  1)

(feed-test "rank preserves count" (feed/count (feed/rank A rec)) 4)
