;; lib/prolog/tests/meta_predicates.sx — \+/1, not/1, once/1, ignore/1, ground/1, sort/2, msort/2, atom_number/2, number_string/2

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

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

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

(define pl-mp-db (pl-mk-db))
(pl-db-load!
  pl-mp-db
  (pl-parse "member(X, [X|_]). member(X, [_|T]) :- member(X, T)."))

;; -- \+/1 --

(pl-mp-test!
  "\\+(fail) succeeds"
  (pl-solve-once! pl-mp-db (pl-mp-goal "\\+(fail)" {}) (pl-mk-trail))
  true)

(pl-mp-test!
  "\\+(true) fails"
  (pl-solve-once! pl-mp-db (pl-mp-goal "\\+(true)" {}) (pl-mk-trail))
  false)

(pl-mp-test!
  "\\+(member(d, [a,b,c])) succeeds"
  (pl-solve-once!
    pl-mp-db
    (pl-mp-goal "\\+(member(d, [a,b,c]))" {})
    (pl-mk-trail))
  true)

(pl-mp-test!
  "\\+(member(a, [a,b,c])) fails"
  (pl-solve-once!
    pl-mp-db
    (pl-mp-goal "\\+(member(a, [a,b,c]))" {})
    (pl-mk-trail))
  false)

(define pl-mp-env-neg {})
(pl-solve-once!
  pl-mp-db
  (pl-mp-goal "\\+(X = 5)" pl-mp-env-neg)
  (pl-mk-trail))
(pl-mp-test!
  "\\+(X=5) fails, X stays unbound (bindings undone)"
  (nil? (pl-var-binding (dict-get pl-mp-env-neg "X")))
  true)

;; -- not/1 --

(pl-mp-test!
  "not(fail) succeeds"
  (pl-solve-once! pl-mp-db (pl-mp-goal "not(fail)" {}) (pl-mk-trail))
  true)

(pl-mp-test!
  "not(true) fails"
  (pl-solve-once! pl-mp-db (pl-mp-goal "not(true)" {}) (pl-mk-trail))
  false)

;; -- once/1 --

(pl-mp-test!
  "once(member(X,[1,2,3])) succeeds once"
  (pl-solve-count!
    pl-mp-db
    (pl-mp-goal "once(member(X,[1,2,3]))" {})
    (pl-mk-trail))
  1)

(define pl-mp-env-once {})
(pl-solve-once!
  pl-mp-db
  (pl-mp-goal "once(member(X,[1,2,3]))" pl-mp-env-once)
  (pl-mk-trail))
(pl-mp-test!
  "once(member(X,[1,2,3])): X=1 (first solution)"
  (pl-num-val (pl-walk-deep (dict-get pl-mp-env-once "X")))
  1)

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

;; -- ignore/1 --

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

(pl-mp-test!
  "ignore(fail) still succeeds"
  (pl-solve-once!
    pl-mp-db
    (pl-mp-goal "ignore(fail)" {})
    (pl-mk-trail))
  true)

;; -- ground/1 --

(pl-mp-test!
  "ground(foo(1, a)) succeeds"
  (pl-solve-once!
    pl-mp-db
    (pl-mp-goal "ground(foo(1, a))" {})
    (pl-mk-trail))
  true)

(pl-mp-test!
  "ground(foo(X, a)) fails (X unbound)"
  (pl-solve-once!
    pl-mp-db
    (pl-mp-goal "ground(foo(X, a))" {})
    (pl-mk-trail))
  false)

(pl-mp-test!
  "ground(42) succeeds"
  (pl-solve-once!
    pl-mp-db
    (pl-mp-goal "ground(42)" {})
    (pl-mk-trail))
  true)

;; -- sort/2 --

(pl-mp-test!
  "sort([b,a,c], [a,b,c])"
  (pl-solve-once!
    pl-mp-db
    (pl-mp-goal "sort([b,a,c], [a,b,c])" {})
    (pl-mk-trail))
  true)

(pl-mp-test!
  "sort([b,a,a,c], [a,b,c]) (removes duplicates)"
  (pl-solve-once!
    pl-mp-db
    (pl-mp-goal "sort([b,a,a,c], [a,b,c])" {})
    (pl-mk-trail))
  true)

(pl-mp-test!
  "sort([], [])"
  (pl-solve-once!
    pl-mp-db
    (pl-mp-goal "sort([], [])" {})
    (pl-mk-trail))
  true)

;; -- msort/2 --

(pl-mp-test!
  "msort([b,a,a,c], [a,a,b,c]) (keeps duplicates)"
  (pl-solve-once!
    pl-mp-db
    (pl-mp-goal "msort([b,a,a,c], [a,a,b,c])" {})
    (pl-mk-trail))
  true)

(pl-mp-test!
  "msort([3,1,2,1], [1,1,2,3])"
  (pl-solve-once!
    pl-mp-db
    (pl-mp-goal "msort([3,1,2,1], [1,1,2,3])" {})
    (pl-mk-trail))
  true)

;; -- atom_number/2 --

(define pl-mp-env-an1 {})
(pl-solve-once!
  pl-mp-db
  (pl-mp-goal "atom_number('42', N)" pl-mp-env-an1)
  (pl-mk-trail))
(pl-mp-test!
  "atom_number('42', N) -> N=42"
  (pl-num-val (pl-walk-deep (dict-get pl-mp-env-an1 "N")))
  42)

(define pl-mp-env-an2 {})
(pl-solve-once!
  pl-mp-db
  (pl-mp-goal "atom_number(A, 7)" pl-mp-env-an2)
  (pl-mk-trail))
(pl-mp-test!
  "atom_number(A, 7) -> A='7'"
  (pl-atom-name (pl-walk-deep (dict-get pl-mp-env-an2 "A")))
  "7")

(pl-mp-test!
  "atom_number(foo, N) fails (not a number)"
  (pl-solve-once!
    pl-mp-db
    (pl-mp-goal "atom_number(foo, N)" {})
    (pl-mk-trail))
  false)

;; -- number_string/2 --

(define pl-mp-env-ns1 {})
(pl-solve-once!
  pl-mp-db
  (pl-mp-goal "number_string(42, S)" pl-mp-env-ns1)
  (pl-mk-trail))
(pl-mp-test!
  "number_string(42, S) -> S='42'"
  (pl-atom-name (pl-walk-deep (dict-get pl-mp-env-ns1 "S")))
  "42")

(define pl-mp-env-ns2 {})
(pl-solve-once!
  pl-mp-db
  (pl-mp-goal "number_string(N, '3.14')" pl-mp-env-ns2)
  (pl-mk-trail))
(pl-mp-test!
  "number_string(N, '3.14') -> N=3.14"
  (pl-num-val (pl-walk-deep (dict-get pl-mp-env-ns2 "N")))
  3.14)

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