;; lib/relations/tests/reach.sx — Phase 2: reachability, roots/leaves, cycles.

(define relations-rt-pass 0)
(define relations-rt-fail 0)
(define relations-rt-failures (list))

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

(define
  relations-rt-subset?
  (fn
    (xs ys)
    (cond
      ((= (len xs) 0) true)
      ((relations-member? (first xs) ys)
        (relations-rt-subset? (rest xs) ys))
      (else false))))

(define
  relations-rt-set=?
  (fn
    (xs ys)
    (and
      (= (len xs) (len ys))
      (relations-rt-subset? xs ys)
      (relations-rt-subset? ys xs))))

;; Diamond + a disconnected pair under parent, plus a reply cross-edge.
;;   parent: a->b, a->c, b->d, c->d ; e->f
;;   reply:  b->z
(define
  relations-rt-fixture
  (fn
    ()
    (relations-build-db
      (list
        (relations-rel (quote a) (quote b) (quote parent))
        (relations-rel (quote a) (quote c) (quote parent))
        (relations-rel (quote b) (quote d) (quote parent))
        (relations-rel (quote c) (quote d) (quote parent))
        (relations-rel (quote e) (quote f) (quote parent))
        (relations-rel (quote b) (quote z) (quote reply))))))

;; Cycles: c1<->c2, self-loop s->s, plus acyclic t->u, all under parent.
(define
  relations-rt-cyc-fixture
  (fn
    ()
    (relations-build-db
      (list
        (relations-rel (quote c1) (quote c2) (quote parent))
        (relations-rel (quote c2) (quote c1) (quote parent))
        (relations-rel (quote s) (quote s) (quote parent))
        (relations-rel (quote t) (quote u) (quote parent))))))

(define
  relations-rt-run-all!
  (fn
    ()
    (let
      ((db (relations-rt-fixture)) (cyc (relations-rt-cyc-fixture)))
      (do
        (relations-rt-check!
          "descendants of a (diamond)"
          (relations-rt-set=?
            (relations-descendants db (quote a) (quote parent))
            (list (quote b) (quote c) (quote d)))
          true)
        (relations-rt-check!
          "ancestors of d (diamond)"
          (relations-rt-set=?
            (relations-ancestors db (quote d) (quote parent))
            (list (quote a) (quote b) (quote c)))
          true)
        (relations-rt-check!
          "reachable a->d"
          (relations-reachable? db (quote a) (quote d) (quote parent))
          true)
        (relations-rt-check!
          "not reachable d->a"
          (relations-reachable? db (quote d) (quote a) (quote parent))
          false)
        (relations-rt-check!
          "disconnected components"
          (relations-reachable? db (quote a) (quote f) (quote parent))
          false)
        (relations-rt-check!
          "leaf has no descendants"
          (relations-descendants db (quote d) (quote parent))
          (list))
        (relations-rt-check!
          "root has no ancestors"
          (relations-ancestors db (quote a) (quote parent))
          (list))
        (relations-rt-check!
          "roots under parent"
          (relations-rt-set=?
            (relations-roots db (quote parent))
            (list (quote a) (quote e)))
          true)
        (relations-rt-check!
          "leaves under parent"
          (relations-rt-set=?
            (relations-leaves db (quote parent))
            (list (quote d) (quote f)))
          true)
        (relations-rt-check!
          "parent descendants exclude reply target"
          (relations-member?
            (quote z)
            (relations-descendants db (quote a) (quote parent)))
          false)
        (relations-rt-check!
          "reply reachable b->z"
          (relations-reachable? db (quote b) (quote z) (quote reply))
          true)
        (relations-rt-check!
          "parent unreachable a->z"
          (relations-reachable? db (quote a) (quote z) (quote parent))
          false)
        (relations-rt-check!
          "diamond is acyclic"
          (relations-acyclic? db (quote parent))
          true)
        (relations-rt-check!
          "no node cycles in diamond"
          (relations-cycle? db (quote a) (quote parent))
          false)
        (relations-rt-check!
          "c1 is on a cycle"
          (relations-cycle? cyc (quote c1) (quote parent))
          true)
        (relations-rt-check!
          "self-loop counts as cycle"
          (relations-cycle? cyc (quote s) (quote parent))
          true)
        (relations-rt-check!
          "acyclic node t not on cycle"
          (relations-cycle? cyc (quote t) (quote parent))
          false)
        (relations-rt-check!
          "kind with a cycle is not acyclic"
          (relations-acyclic? cyc (quote parent))
          false)
        (relations-rt-check!
          "cycle reachable both ways"
          (and
            (relations-reachable? cyc (quote c1) (quote c2) (quote parent))
            (relations-reachable? cyc (quote c2) (quote c1) (quote parent)))
          true)
        (relations-rt-check!
          "node in cycle reaches itself"
          (relations-member?
            (quote c1)
            (relations-descendants cyc (quote c1) (quote parent)))
          true)
        (do
          (relations/load!
            (list
              (relations-rel (quote r) (quote m) (quote parent))
              (relations-rel (quote m) (quote n) (quote parent))))
          (relations-rt-check!
            "api descendants"
            (relations-rt-set=?
              (relations/descendants (quote r) (quote parent))
              (list (quote m) (quote n)))
            true)
          (relations-rt-check!
            "api reachable"
            (relations/reachable? (quote r) (quote n) (quote parent))
            true)
          (relations-rt-check!
            "api roots"
            (relations-rt-set=?
              (relations/roots (quote parent))
              (list (quote r)))
            true)
          (relations-rt-check!
            "api acyclic"
            (relations/acyclic? (quote parent))
            true)
          (relations/load! (list)))))))

(define
  relations-reach-tests-run!
  (fn
    ()
    (do
      (set! relations-rt-pass 0)
      (set! relations-rt-fail 0)
      (set! relations-rt-failures (list))
      (relations-rt-run-all!)
      {:failures relations-rt-failures :total (+ relations-rt-pass relations-rt-fail) :passed relations-rt-pass :failed relations-rt-fail})))
