;; lib/prolog/tests/solve.sx — DFS solver unit tests

(define pl-s-test-count 0)
(define pl-s-test-pass 0)
(define pl-s-test-fail 0)
(define pl-s-test-failures (list))

(define
  pl-s-test!
  (fn
    (name got expected)
    (begin
      (set! pl-s-test-count (+ pl-s-test-count 1))
      (if
        (= got expected)
        (set! pl-s-test-pass (+ pl-s-test-pass 1))
        (begin
          (set! pl-s-test-fail (+ pl-s-test-fail 1))
          (append!
            pl-s-test-failures
            (str name "\n    expected: " expected "\n    got:      " got)))))))

(define
  pl-s-goal
  (fn
    (src env)
    (pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))

(define pl-s-empty-db (pl-mk-db))

(pl-s-test!
  "true succeeds"
  (pl-solve-once! pl-s-empty-db (pl-s-goal "true" {}) (pl-mk-trail))
  true)

(pl-s-test!
  "fail fails"
  (pl-solve-once! pl-s-empty-db (pl-s-goal "fail" {}) (pl-mk-trail))
  false)

(pl-s-test!
  "= identical atoms"
  (pl-solve-once!
    pl-s-empty-db
    (pl-s-goal "=(a, a)" {})
    (pl-mk-trail))
  true)

(pl-s-test!
  "= different atoms"
  (pl-solve-once!
    pl-s-empty-db
    (pl-s-goal "=(a, b)" {})
    (pl-mk-trail))
  false)

(pl-s-test!
  "= var to atom"
  (pl-solve-once!
    pl-s-empty-db
    (pl-s-goal "=(X, foo)" {})
    (pl-mk-trail))
  true)

(define pl-s-env-bind {})
(define pl-s-trail-bind (pl-mk-trail))
(define pl-s-goal-bind (pl-s-goal "=(X, foo)" pl-s-env-bind))
(pl-solve-once! pl-s-empty-db pl-s-goal-bind pl-s-trail-bind)

(pl-s-test!
  "X bound to foo after =(X, foo)"
  (pl-atom-name (pl-walk-deep (dict-get pl-s-env-bind "X")))
  "foo")

(pl-s-test!
  "true , true succeeds"
  (pl-solve-once!
    pl-s-empty-db
    (pl-s-goal "true, true" {})
    (pl-mk-trail))
  true)

(pl-s-test!
  "true , fail fails"
  (pl-solve-once!
    pl-s-empty-db
    (pl-s-goal "true, fail" {})
    (pl-mk-trail))
  false)

(pl-s-test!
  "consistent X bindings succeed"
  (pl-solve-once!
    pl-s-empty-db
    (pl-s-goal "=(X, a), =(X, a)" {})
    (pl-mk-trail))
  true)

(pl-s-test!
  "conflicting X bindings fail"
  (pl-solve-once!
    pl-s-empty-db
    (pl-s-goal "=(X, a), =(X, b)" {})
    (pl-mk-trail))
  false)

(define pl-s-db1 (pl-mk-db))
(pl-db-load!
  pl-s-db1
  (pl-parse "parent(tom, bob). parent(bob, liz). parent(bob, ann)."))

(pl-s-test!
  "fact lookup hit"
  (pl-solve-once!
    pl-s-db1
    (pl-s-goal "parent(tom, bob)" {})
    (pl-mk-trail))
  true)

(pl-s-test!
  "fact lookup miss"
  (pl-solve-once!
    pl-s-db1
    (pl-s-goal "parent(tom, liz)" {})
    (pl-mk-trail))
  false)

(pl-s-test!
  "all parent solutions"
  (pl-solve-count!
    pl-s-db1
    (pl-s-goal "parent(X, Y)" {})
    (pl-mk-trail))
  3)

(pl-s-test!
  "fixed first arg solutions"
  (pl-solve-count!
    pl-s-db1
    (pl-s-goal "parent(bob, Y)" {})
    (pl-mk-trail))
  2)

(define pl-s-db2 (pl-mk-db))
(pl-db-load!
  pl-s-db2
  (pl-parse
    "parent(tom, bob). parent(bob, ann). ancestor(X, Y) :- parent(X, Y). ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z)."))

(pl-s-test!
  "rule direct ancestor"
  (pl-solve-once!
    pl-s-db2
    (pl-s-goal "ancestor(tom, bob)" {})
    (pl-mk-trail))
  true)

(pl-s-test!
  "rule transitive ancestor"
  (pl-solve-once!
    pl-s-db2
    (pl-s-goal "ancestor(tom, ann)" {})
    (pl-mk-trail))
  true)

(pl-s-test!
  "rule no path"
  (pl-solve-once!
    pl-s-db2
    (pl-s-goal "ancestor(ann, tom)" {})
    (pl-mk-trail))
  false)

(define pl-s-env-undo {})
(define pl-s-trail-undo (pl-mk-trail))
(define pl-s-goal-undo (pl-s-goal "=(X, a), fail" pl-s-env-undo))
(pl-solve-once! pl-s-empty-db pl-s-goal-undo pl-s-trail-undo)

(pl-s-test!
  "trail undone after failure leaves X unbound"
  (pl-var-bound? (dict-get pl-s-env-undo "X"))
  false)

(define pl-s-db-cut1 (pl-mk-db))

(pl-db-load! pl-s-db-cut1 (pl-parse "g :- !. g :- true."))

(pl-s-test!
  "bare cut succeeds"
  (pl-solve-once! pl-s-db-cut1 (pl-s-goal "g" {}) (pl-mk-trail))
  true)

(pl-s-test!
  "cut commits to first matching clause"
  (pl-solve-count! pl-s-db-cut1 (pl-s-goal "g" {}) (pl-mk-trail))
  1)

(define pl-s-db-cut2 (pl-mk-db))

(pl-db-load! pl-s-db-cut2 (pl-parse "a(1). a(2). g(X) :- a(X), !."))

(pl-s-test!
  "cut commits to first a solution"
  (pl-solve-count! pl-s-db-cut2 (pl-s-goal "g(X)" {}) (pl-mk-trail))
  1)

(define pl-s-db-cut3 (pl-mk-db))

(pl-db-load!
  pl-s-db-cut3
  (pl-parse "a(1). a(2). g(X) :- a(X), !, fail. g(99)."))

(pl-s-test!
  "cut then fail blocks alt clauses"
  (pl-solve-count! pl-s-db-cut3 (pl-s-goal "g(X)" {}) (pl-mk-trail))
  0)

(define pl-s-db-cut4 (pl-mk-db))

(pl-db-load!
  pl-s-db-cut4
  (pl-parse "a(1). b(10). b(20). g(X, Y) :- a(X), !, b(Y)."))

(pl-s-test!
  "post-cut goal backtracks freely"
  (pl-solve-count!
    pl-s-db-cut4
    (pl-s-goal "g(X, Y)" {})
    (pl-mk-trail))
  2)

(define pl-s-db-cut5 (pl-mk-db))

(pl-db-load!
  pl-s-db-cut5
  (pl-parse "r(1). r(2). q :- r(X), !. p :- q. p :- true."))

(pl-s-test!
  "inner cut does not commit outer predicate"
  (pl-solve-count! pl-s-db-cut5 (pl-s-goal "p" {}) (pl-mk-trail))
  2)

(pl-s-test!
  "\\= different atoms succeeds"
  (pl-solve-once!
    pl-s-empty-db
    (pl-s-goal "\\=(a, b)" {})
    (pl-mk-trail))
  true)

(pl-s-test!
  "\\= same atoms fails"
  (pl-solve-once!
    pl-s-empty-db
    (pl-s-goal "\\=(a, a)" {})
    (pl-mk-trail))
  false)

(pl-s-test!
  "\\= var-vs-atom would unify so fails"
  (pl-solve-once!
    pl-s-empty-db
    (pl-s-goal "\\=(X, a)" {})
    (pl-mk-trail))
  false)

(define pl-s-env-ne {})

(define pl-s-trail-ne (pl-mk-trail))

(define pl-s-goal-ne (pl-s-goal "\\=(X, a)" pl-s-env-ne))

(pl-solve-once! pl-s-empty-db pl-s-goal-ne pl-s-trail-ne)

(pl-s-test!
  "\\= leaves no bindings"
  (pl-var-bound? (dict-get pl-s-env-ne "X"))
  false)

(pl-s-test!
  "; left succeeds"
  (pl-solve-once!
    pl-s-empty-db
    (pl-s-goal ";(true, fail)" {})
    (pl-mk-trail))
  true)

(pl-s-test!
  "; right succeeds when left fails"
  (pl-solve-once!
    pl-s-empty-db
    (pl-s-goal ";(fail, true)" {})
    (pl-mk-trail))
  true)

(pl-s-test!
  "; both fail"
  (pl-solve-once!
    pl-s-empty-db
    (pl-s-goal ";(fail, fail)" {})
    (pl-mk-trail))
  false)

(pl-s-test!
  "; both branches counted"
  (pl-solve-count!
    pl-s-empty-db
    (pl-s-goal ";(true, true)" {})
    (pl-mk-trail))
  2)

(define pl-s-db-call (pl-mk-db))

(pl-db-load! pl-s-db-call (pl-parse "p(1). p(2)."))

(pl-s-test!
  "call(true) succeeds"
  (pl-solve-once!
    pl-s-db-call
    (pl-s-goal "call(true)" {})
    (pl-mk-trail))
  true)

(pl-s-test!
  "call(p(X)) yields all solutions"
  (pl-solve-count!
    pl-s-db-call
    (pl-s-goal "call(p(X))" {})
    (pl-mk-trail))
  2)

(pl-s-test!
  "call of bound goal var resolves"
  (pl-solve-once!
    pl-s-db-call
    (pl-s-goal "=(G, true), call(G)" {})
    (pl-mk-trail))
  true)

(define pl-s-db-ite (pl-mk-db))

(pl-db-load! pl-s-db-ite (pl-parse "p(1). p(2). q(yes). q(no)."))

(pl-s-test!
  "if-then-else: cond true → then runs"
  (pl-solve-once!
    pl-s-db-ite
    (pl-s-goal ";(->(true, =(X, ok)), =(X, fallback))" {})
    (pl-mk-trail))
  true)

(define pl-s-env-ite1 {})

(pl-solve-once!
  pl-s-db-ite
  (pl-s-goal ";(->(true, =(X, ok)), =(X, fallback))" pl-s-env-ite1)
  (pl-mk-trail))

(pl-s-test!
  "if-then-else: cond true binds via then"
  (pl-atom-name (pl-walk-deep (dict-get pl-s-env-ite1 "X")))
  "ok")

(pl-s-test!
  "if-then-else: cond false → else"
  (pl-solve-once!
    pl-s-db-ite
    (pl-s-goal ";(->(fail, =(X, ok)), =(X, fallback))" {})
    (pl-mk-trail))
  true)

(define pl-s-env-ite2 {})

(pl-solve-once!
  pl-s-db-ite
  (pl-s-goal ";(->(fail, =(X, ok)), =(X, fallback))" pl-s-env-ite2)
  (pl-mk-trail))

(pl-s-test!
  "if-then-else: cond false binds via else"
  (pl-atom-name (pl-walk-deep (dict-get pl-s-env-ite2 "X")))
  "fallback")

(pl-s-test!
  "if-then-else: cond commits to first solution (count = 1)"
  (pl-solve-count!
    pl-s-db-ite
    (pl-s-goal ";(->(p(X), =(Y, found)), =(Y, none))" {})
    (pl-mk-trail))
  1)

(pl-s-test!
  "if-then-else: then can backtrack"
  (pl-solve-count!
    pl-s-db-ite
    (pl-s-goal ";(->(true, p(X)), =(X, none))" {})
    (pl-mk-trail))
  2)

(pl-s-test!
  "if-then-else: else can backtrack"
  (pl-solve-count!
    pl-s-db-ite
    (pl-s-goal ";(->(fail, =(X, ignored)), p(X))" {})
    (pl-mk-trail))
  2)

(pl-s-test!
  "standalone -> with true cond succeeds"
  (pl-solve-once!
    pl-s-db-ite
    (pl-s-goal "->(true, =(X, hi))" {})
    (pl-mk-trail))
  true)

(pl-s-test!
  "standalone -> with false cond fails"
  (pl-solve-once!
    pl-s-db-ite
    (pl-s-goal "->(fail, =(X, hi))" {})
    (pl-mk-trail))
  false)

(pl-s-test!
  "write(hello)"
  (begin
    (pl-output-clear!)
    (pl-solve-once!
      pl-s-empty-db
      (pl-s-goal "write(hello)" {})
      (pl-mk-trail))
    pl-output-buffer)
  "hello")

(pl-s-test!
  "nl outputs newline"
  (begin
    (pl-output-clear!)
    (pl-solve-once! pl-s-empty-db (pl-s-goal "nl" {}) (pl-mk-trail))
    pl-output-buffer)
  "\n")

(pl-s-test!
  "write(42) outputs digits"
  (begin
    (pl-output-clear!)
    (pl-solve-once!
      pl-s-empty-db
      (pl-s-goal "write(42)" {})
      (pl-mk-trail))
    pl-output-buffer)
  "42")

(pl-s-test!
  "write(foo(a, b)) formats compound"
  (begin
    (pl-output-clear!)
    (pl-solve-once!
      pl-s-empty-db
      (pl-s-goal "write(foo(a, b))" {})
      (pl-mk-trail))
    pl-output-buffer)
  "foo(a, b)")

(pl-s-test!
  "write conjunction"
  (begin
    (pl-output-clear!)
    (pl-solve-once!
      pl-s-empty-db
      (pl-s-goal "write(a), write(b)" {})
      (pl-mk-trail))
    pl-output-buffer)
  "ab")

(pl-s-test!
  "write of bound var walks binding"
  (begin
    (pl-output-clear!)
    (pl-solve-once!
      pl-s-empty-db
      (pl-s-goal "=(X, hello), write(X)" {})
      (pl-mk-trail))
    pl-output-buffer)
  "hello")

(pl-s-test!
  "write then nl"
  (begin
    (pl-output-clear!)
    (pl-solve-once!
      pl-s-empty-db
      (pl-s-goal "write(hi), nl" {})
      (pl-mk-trail))
    pl-output-buffer)
  "hi\n")

(define pl-s-env-arith1 {})

(pl-solve-once!
  pl-s-empty-db
  (pl-s-goal "is(X, 42)" pl-s-env-arith1)
  (pl-mk-trail))

(pl-s-test!
  "is(X, 42) binds X to 42"
  (pl-num-val (pl-walk-deep (dict-get pl-s-env-arith1 "X")))
  42)

(define pl-s-env-arith2 {})

(pl-solve-once!
  pl-s-empty-db
  (pl-s-goal "is(X, +(2, 3))" pl-s-env-arith2)
  (pl-mk-trail))

(pl-s-test!
  "is(X, +(2, 3)) binds X to 5"
  (pl-num-val (pl-walk-deep (dict-get pl-s-env-arith2 "X")))
  5)

(define pl-s-env-arith3 {})

(pl-solve-once!
  pl-s-empty-db
  (pl-s-goal "is(X, *(2, 3))" pl-s-env-arith3)
  (pl-mk-trail))

(pl-s-test!
  "is(X, *(2, 3)) binds X to 6"
  (pl-num-val (pl-walk-deep (dict-get pl-s-env-arith3 "X")))
  6)

(define pl-s-env-arith4 {})

(pl-solve-once!
  pl-s-empty-db
  (pl-s-goal "is(X, -(10, 3))" pl-s-env-arith4)
  (pl-mk-trail))

(pl-s-test!
  "is(X, -(10, 3)) binds X to 7"
  (pl-num-val (pl-walk-deep (dict-get pl-s-env-arith4 "X")))
  7)

(define pl-s-env-arith5 {})

(pl-solve-once!
  pl-s-empty-db
  (pl-s-goal "is(X, /(10, 2))" pl-s-env-arith5)
  (pl-mk-trail))

(pl-s-test!
  "is(X, /(10, 2)) binds X to 5"
  (pl-num-val (pl-walk-deep (dict-get pl-s-env-arith5 "X")))
  5)

(define pl-s-env-arith6 {})

(pl-solve-once!
  pl-s-empty-db
  (pl-s-goal "is(X, mod(10, 3))" pl-s-env-arith6)
  (pl-mk-trail))

(pl-s-test!
  "is(X, mod(10, 3)) binds X to 1"
  (pl-num-val (pl-walk-deep (dict-get pl-s-env-arith6 "X")))
  1)

(define pl-s-env-arith7 {})

(pl-solve-once!
  pl-s-empty-db
  (pl-s-goal "is(X, abs(-(0, 5)))" pl-s-env-arith7)
  (pl-mk-trail))

(pl-s-test!
  "is(X, abs(-(0, 5))) binds X to 5"
  (pl-num-val (pl-walk-deep (dict-get pl-s-env-arith7 "X")))
  5)

(define pl-s-env-arith8 {})

(pl-solve-once!
  pl-s-empty-db
  (pl-s-goal "is(X, +(2, *(3, 4)))" pl-s-env-arith8)
  (pl-mk-trail))

(pl-s-test!
  "is(X, +(2, *(3, 4))) binds X to 14 (nested)"
  (pl-num-val (pl-walk-deep (dict-get pl-s-env-arith8 "X")))
  14)

(pl-s-test!
  "is(5, +(2, 3)) succeeds (LHS num matches)"
  (pl-solve-once!
    pl-s-empty-db
    (pl-s-goal "is(5, +(2, 3))" {})
    (pl-mk-trail))
  true)

(pl-s-test!
  "is(6, +(2, 3)) fails (LHS num mismatch)"
  (pl-solve-once!
    pl-s-empty-db
    (pl-s-goal "is(6, +(2, 3))" {})
    (pl-mk-trail))
  false)

(pl-s-test!
  "is propagates bound vars on RHS"
  (pl-solve-once!
    pl-s-empty-db
    (pl-s-goal "=(Y, 4), is(X, +(Y, 1)), =(X, 5)" {})
    (pl-mk-trail))
  true)

(define pl-solve-tests-run! (fn () {:failed pl-s-test-fail :passed pl-s-test-pass :total pl-s-test-count :failures pl-s-test-failures}))
