; Phase 2 — fanout via outer product + dedupe. (feed-test name got expected)

; ---------- graph ----------

; edges: (follower followee). bob,carol follow alice; carol,dave follow bob.
(define
  G
  (feed/follow-graph
    (list
      (list "bob" "alice")
      (list "carol" "alice")
      (list "carol" "bob")
      (list "dave" "bob"))))

(feed-test "followers alice" (feed/followers G "alice") (list "bob" "carol"))
(feed-test "followers bob" (feed/followers G "bob") (list "carol" "dave"))
(feed-test "followers unknown" (feed/followers G "zzz") (list))
(feed-test "audience distinct" (feed/audience G) (list "bob" "carol" "dave"))

; ---------- fanout ----------

(define
  S
  (feed/stream
    (list
      (feed/activity "alice" "post" "p1" 10 (list))
      (feed/activity "alice" "post" "p2" 20 (list))
      (feed/activity "bob" "like" "p1" 30 (list)))))

(define IB (feed/fanout S G))

(feed-test "fanout total edges" (feed/count IB) 6)
(feed-test
  "inbox bob count"
  (feed/count (feed/inbox-for IB "bob"))
  2)
(feed-test
  "inbox carol count"
  (feed/count (feed/inbox-for IB "carol"))
  3)
(feed-test
  "inbox dave count"
  (feed/count (feed/inbox-for IB "dave"))
  1)
(feed-test
  "inbox alice (follows none)"
  (feed/count (feed/inbox-for IB "alice"))
  0)
(feed-test
  "recipients order"
  (feed/recipients IB)
  (list "bob" "carol" "dave"))
(feed-test
  "bob inbox objects"
  (map (fn (a) (get a :object)) (feed/inbox-activities IB "bob"))
  (list "p1" "p2"))
(feed-test
  "dave inbox objects"
  (map (fn (a) (get a :object)) (feed/inbox-activities IB "dave"))
  (list "p1"))
(feed-test
  "dave inbox verb"
  (map (fn (a) (get a :verb)) (feed/inbox-activities IB "dave"))
  (list "like"))

; empty graph → no audience → no edges
(feed-test
  "empty graph fanout"
  (feed/count (feed/fanout S {}))
  0)

; actor nobody follows produces no edges
(define
  Sghost
  (feed/stream (list (feed/activity "ghost" "post" "g1" 5 (list)))))
(feed-test
  "unfollowed actor fanout"
  (feed/count (feed/fanout Sghost G))
  0)

; ---------- high fanout (popular actor) ----------

(define
  Gstar
  (feed/follow-graph
    (list
      (list "u1" "star")
      (list "u2" "star")
      (list "u3" "star")
      (list "u4" "star")
      (list "u5" "star"))))
(define
  Sstar
  (feed/stream (list (feed/activity "star" "post" "s1" 1 (list)))))
(feed-test
  "star fanout count"
  (feed/count (feed/fanout Sstar Gstar))
  5)
(feed-test "star audience size" (len (feed/audience Gstar)) 5)

; ---------- mutual follow ----------

(define Gmut (feed/follow-graph (list (list "a" "b") (list "b" "a"))))
(define
  Smut
  (feed/stream
    (list
      (feed/activity "a" "post" "pa" 1 (list))
      (feed/activity "b" "post" "pb" 2 (list)))))
(define IBmut (feed/fanout Smut Gmut))
(feed-test "mutual total" (feed/count IBmut) 2)
(feed-test
  "mutual a gets pb"
  (map (fn (x) (get x :object)) (feed/inbox-activities IBmut "a"))
  (list "pb"))
(feed-test
  "mutual b gets pa"
  (map (fn (x) (get x :object)) (feed/inbox-activities IBmut "b"))
  (list "pa"))

; ---------- dedupe ----------

(define
  Sdup2
  (feed/stream
    (list
      (feed/activity "alice" "post" "p1" 1 (list))
      (feed/activity "alice" "post" "p1" 9 (list))
      (feed/activity "alice" "post" "p2" 2 (list)))))
(feed-test
  "dedupe-activities collapses dup"
  (feed/count (feed/dedupe-activities Sdup2))
  2)
(feed-test
  "dedupe-activities keeps distinct"
  (map
    (fn (a) (get a :object))
    (feed/items (feed/dedupe-activities Sdup2)))
  (list "p1" "p2"))

(define
  Slikes
  (feed/stream
    (list
      (feed/activity "alice" "like" "X" 1 (list))
      (feed/activity "bob" "like" "X" 2 (list))
      (feed/activity "carol" "like" "Y" 3 (list)))))
(feed-test
  "collapse cross-actor likes"
  (feed/count (feed/dedupe-collapse Slikes))
  2)
(feed-test
  "collapse keeps distinct objects"
  (map
    (fn (a) (get a :object))
    (feed/items (feed/dedupe-collapse Slikes)))
  (list "X" "Y"))

(feed-test
  "activity-key shape"
  (feed/activity-key (feed/activity "a" "post" "o" 0 (list)))
  (list "a" "post" "o"))
(feed-test
  "collapse-key shape"
  (feed/collapse-key (feed/activity "a" "like" "o" 0 (list)))
  (list "like" "o"))

; cross-post: alice posts p1 twice → bob's inbox has it twice → dedupe-inbox → once
(define
  Scross
  (feed/stream
    (list
      (feed/activity "alice" "post" "p1" 1 (list))
      (feed/activity "alice" "post" "p1" 5 (list)))))
(define IBcross (feed/fanout Scross G))
(feed-test
  "cross-post raw bob count"
  (feed/count (feed/inbox-for IBcross "bob"))
  2)
(feed-test
  "cross-post deduped bob count"
  (feed/count (feed/inbox-for (feed/dedupe-inbox IBcross) "bob"))
  1)
(feed-test
  "dedupe-inbox keeps distinct receivers"
  (feed/count (feed/dedupe-inbox IBcross))
  2)
