;; lib/prolog/tests/unify.sx — unification + trail unit tests
;;
;; Run via MCP: (pl-unify-tests-run!)
;;
;; Covers: atoms, vars, numbers, strings, compounds, nested compounds,
;; cons-cell lists, trail undo, occurs-check, deep walks.

(define pl-u-count 0)
(define pl-u-pass 0)
(define pl-u-fail 0)
(define pl-u-failures (list))

(define
  pl-u-test!
  (fn
    (name thunk expected)
    (set! pl-u-count (+ pl-u-count 1))
    (let
      ((got (thunk)))
      (if
        (= got expected)
        (set! pl-u-pass (+ pl-u-pass 1))
        (do
          (set! pl-u-fail (+ pl-u-fail 1))
          (set!
            pl-u-failures
            (cons (list name :expected expected :got got) pl-u-failures)))))))

;; Shortcuts
(define pl-a (fn (n) (list "atom" n)))
(define pl-n (fn (v) (list "num" v)))
(define pl-s (fn (v) (list "str" v)))
(define pl-c (fn (f args) (list "compound" f args)))

;; ── Primitive predicates ──────────────────────────────────────────
(pl-u-test! "var? on fresh var" (fn () (pl-var? (pl-mk-rt-var "X"))) true)

(pl-u-test! "var? on atom" (fn () (pl-var? (pl-a "foo"))) false)

(pl-u-test! "atom? on atom" (fn () (pl-atom? (pl-a "foo"))) true)

(pl-u-test! "atom? on var" (fn () (pl-atom? (pl-mk-rt-var "X"))) false)

(pl-u-test!
  "compound? on compound"
  (fn () (pl-compound? (pl-c "p" (list (pl-a "a")))))
  true)

(pl-u-test! "num? on num" (fn () (pl-num? (pl-n 42))) true)

;; ── Fresh var ids ─────────────────────────────────────────────────
(pl-u-test!
  "fresh vars get distinct ids"
  (fn
    ()
    (let
      ((a (pl-mk-rt-var "X")) (b (pl-mk-rt-var "X")))
      (not (= (pl-var-id a) (pl-var-id b)))))
  true)

;; ── Walk ───────────────────────────────────────────────────────────
(pl-u-test!
  "walk returns unbound var unchanged"
  (fn
    ()
    (let
      ((v (pl-mk-rt-var "X")))
      (= (pl-var-id (pl-walk v)) (pl-var-id v))))
  true)

(pl-u-test!
  "walk follows single binding"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (v (pl-mk-rt-var "X")))
      (pl-bind! v (pl-a "hello") t)
      (pl-walk v)))
  (list "atom" "hello"))

(pl-u-test!
  "walk follows chain var→var→atom"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (a (pl-mk-rt-var "A")) (b (pl-mk-rt-var "B")))
      (pl-bind! a b t)
      (pl-bind! b (pl-a "end") t)
      (pl-walk a)))
  (list "atom" "end"))

;; ── Unify: atoms ──────────────────────────────────────────────────
(pl-u-test!
  "unify same atom"
  (fn () (pl-unify! (pl-a "foo") (pl-a "foo") (pl-mk-trail)))
  true)

(pl-u-test!
  "unify different atoms"
  (fn () (pl-unify! (pl-a "foo") (pl-a "bar") (pl-mk-trail)))
  false)

;; ── Unify: numbers ────────────────────────────────────────────────
(pl-u-test!
  "unify equal nums"
  (fn () (pl-unify! (pl-n 5) (pl-n 5) (pl-mk-trail)))
  true)

(pl-u-test!
  "unify different nums"
  (fn () (pl-unify! (pl-n 5) (pl-n 6) (pl-mk-trail)))
  false)

(pl-u-test!
  "atom vs num fails"
  (fn () (pl-unify! (pl-a "5") (pl-n 5) (pl-mk-trail)))
  false)

;; ── Unify: strings ────────────────────────────────────────────────
(pl-u-test!
  "unify equal strings"
  (fn () (pl-unify! (pl-s "hi") (pl-s "hi") (pl-mk-trail)))
  true)

(pl-u-test!
  "unify different strings"
  (fn () (pl-unify! (pl-s "hi") (pl-s "bye") (pl-mk-trail)))
  false)

;; ── Unify: variables ──────────────────────────────────────────────
(pl-u-test!
  "unify var with atom binds"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (x (pl-mk-rt-var "X")))
      (pl-unify! x (pl-a "foo") t)
      (pl-walk x)))
  (list "atom" "foo"))

(pl-u-test!
  "unify atom with var binds"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (x (pl-mk-rt-var "X")))
      (pl-unify! (pl-a "foo") x t)
      (pl-walk x)))
  (list "atom" "foo"))

(pl-u-test!
  "unify var = var binds one to the other"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (x (pl-mk-rt-var "X")) (y (pl-mk-rt-var "Y")))
      (pl-unify! x y t)
      (pl-unify! y (pl-a "bound") t)
      (pl-walk x)))
  (list "atom" "bound"))

(pl-u-test!
  "unify same var succeeds without binding"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (x (pl-mk-rt-var "X")))
      (pl-unify! x x t)
      (list (pl-var-bound? x) (= (dict-get t :len) 0))))
  (list false true))

(pl-u-test!
  "bound-var vs atom uses binding"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (x (pl-mk-rt-var "X")))
      (pl-bind! x (pl-a "a") t)
      (pl-unify! x (pl-a "a") t)))
  true)

(pl-u-test!
  "bound-var vs different atom fails"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (x (pl-mk-rt-var "X")))
      (pl-bind! x (pl-a "a") t)
      (pl-unify! x (pl-a "b") t)))
  false)

;; ── Unify: compounds ──────────────────────────────────────────────
(pl-u-test!
  "unify p(a) with p(a)"
  (fn
    ()
    (pl-unify!
      (pl-c "p" (list (pl-a "a")))
      (pl-c "p" (list (pl-a "a")))
      (pl-mk-trail)))
  true)

(pl-u-test!
  "unify p(a) with p(b)"
  (fn
    ()
    (pl-unify!
      (pl-c "p" (list (pl-a "a")))
      (pl-c "p" (list (pl-a "b")))
      (pl-mk-trail)))
  false)

(pl-u-test!
  "unify p(a) with q(a) — functor mismatch"
  (fn
    ()
    (pl-unify!
      (pl-c "p" (list (pl-a "a")))
      (pl-c "q" (list (pl-a "a")))
      (pl-mk-trail)))
  false)

(pl-u-test!
  "unify p(a) with p(a,b) — arity mismatch"
  (fn
    ()
    (pl-unify!
      (pl-c "p" (list (pl-a "a")))
      (pl-c "p" (list (pl-a "a") (pl-a "b")))
      (pl-mk-trail)))
  false)

(pl-u-test!
  "unify p(X) with p(foo) binds X"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (x (pl-mk-rt-var "X")))
      (pl-unify! (pl-c "p" (list x)) (pl-c "p" (list (pl-a "foo"))) t)
      (pl-walk x)))
  (list "atom" "foo"))

(pl-u-test!
  "unify p(X,Y) with p(1,2) binds both"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (x (pl-mk-rt-var "X")) (y (pl-mk-rt-var "Y")))
      (pl-unify!
        (pl-c "p" (list x y))
        (pl-c "p" (list (pl-n 1) (pl-n 2)))
        t)
      (list (pl-walk x) (pl-walk y))))
  (list (list "num" 1) (list "num" 2)))

(pl-u-test!
  "unify p(X,X) with p(a,a)"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (x (pl-mk-rt-var "X")))
      (pl-unify!
        (pl-c "p" (list x x))
        (pl-c "p" (list (pl-a "a") (pl-a "a")))
        t)))
  true)

(pl-u-test!
  "unify p(X,X) with p(a,b) fails"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (x (pl-mk-rt-var "X")))
      (pl-unify!
        (pl-c "p" (list x x))
        (pl-c "p" (list (pl-a "a") (pl-a "b")))
        t)))
  false)

;; ── Nested compounds ──────────────────────────────────────────────
(pl-u-test!
  "unify f(g(X)) with f(g(foo))"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (x (pl-mk-rt-var "X")))
      (pl-unify!
        (pl-c "f" (list (pl-c "g" (list x))))
        (pl-c "f" (list (pl-c "g" (list (pl-a "foo")))))
        t)
      (pl-walk x)))
  (list "atom" "foo"))

;; ── Cons-cell lists ──────────────────────────────────────────────
(define pl-nil (pl-a "[]"))
(define pl-cons (fn (h t) (pl-c "." (list h t))))

(pl-u-test!
  "unify [1,2,3] with [X|T] binds X and T"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (x (pl-mk-rt-var "X")) (tl (pl-mk-rt-var "T")))
      (pl-unify!
        (pl-cons (pl-n 1) (pl-cons (pl-n 2) (pl-cons (pl-n 3) pl-nil)))
        (pl-cons x tl)
        t)
      (list (pl-walk x) (pl-walk-deep tl))))
  (list
    (list "num" 1)
    (list
      "compound"
      "."
      (list
        (list "num" 2)
        (list "compound" "." (list (list "num" 3) (list "atom" "[]")))))))

;; ── Trail: mark + undo ───────────────────────────────────────────
(pl-u-test!
  "trail-undo restores unbound vars"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (x (pl-mk-rt-var "X")) (y (pl-mk-rt-var "Y")))
      (let
        ((mark (pl-trail-mark t)))
        (pl-unify! x (pl-a "a") t)
        (pl-unify! y (pl-a "b") t)
        (pl-trail-undo-to! t mark)
        (list (pl-var-bound? x) (pl-var-bound? y) (dict-get t :len)))))
  (list false false 0))

(pl-u-test!
  "partial undo preserves earlier bindings"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (x (pl-mk-rt-var "X")) (y (pl-mk-rt-var "Y")))
      (pl-unify! x (pl-a "a") t)
      (let
        ((mark (pl-trail-mark t)))
        (pl-unify! y (pl-a "b") t)
        (pl-trail-undo-to! t mark)
        (list (pl-var-bound? x) (pl-var-bound? y)))))
  (list true false))

(pl-u-test!
  "try-unify undoes on failure"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (x (pl-mk-rt-var "X")) (y (pl-mk-rt-var "Y")))
      (pl-try-unify!
        (pl-c "p" (list x y (pl-a "a")))
        (pl-c "p" (list (pl-n 1) (pl-n 2) (pl-a "b")))
        t)
      (list (pl-var-bound? x) (pl-var-bound? y))))
  (list false false))

(pl-u-test!
  "try-unify success keeps bindings"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (x (pl-mk-rt-var "X")) (y (pl-mk-rt-var "Y")))
      (pl-try-unify!
        (pl-c "p" (list x y))
        (pl-c "p" (list (pl-n 1) (pl-n 2)))
        t)
      (list (pl-walk x) (pl-walk y))))
  (list (list "num" 1) (list "num" 2)))

;; ── Occurs check ──────────────────────────────────────────────────
(pl-u-test!
  "no occurs check: X = f(X) succeeds"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (x (pl-mk-rt-var "X")))
      (pl-unify! x (pl-c "f" (list x)) t)))
  true)

(pl-u-test!
  "occurs check: X = f(X) fails"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (x (pl-mk-rt-var "X")))
      (dict-set! t :occurs-check true)
      (pl-unify! x (pl-c "f" (list x)) t)))
  false)

(pl-u-test!
  "occurs check: X = Y, Y = f(X) fails"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (x (pl-mk-rt-var "X")) (y (pl-mk-rt-var "Y")))
      (dict-set! t :occurs-check true)
      (pl-unify! x y t)
      (pl-unify! y (pl-c "f" (list x)) t)))
  false)

(pl-u-test!
  "occurs check: deep occurrence in compound"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (x (pl-mk-rt-var "X")))
      (dict-set! t :occurs-check true)
      (pl-unify! x (pl-c "f" (list (pl-c "g" (list x)))) t)))
  false)

(pl-u-test!
  "occurs check: X = Y (both unbound) succeeds"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (x (pl-mk-rt-var "X")) (y (pl-mk-rt-var "Y")))
      (dict-set! t :occurs-check true)
      (pl-unify! x y t)))
  true)

;; ── Parse AST → runtime term ──────────────────────────────────────
(pl-u-test!
  "instantiate replaces (var X) with runtime var"
  (fn
    ()
    (let ((ast (list "var" "X"))) (pl-var? (pl-instantiate-fresh ast))))
  true)

(pl-u-test!
  "instantiate shares vars within clause"
  (fn
    ()
    (let
      ((env {}))
      (let
        ((v1 (pl-instantiate (list "var" "X") env))
          (v2 (pl-instantiate (list "var" "X") env)))
        (= (pl-var-id v1) (pl-var-id v2)))))
  true)

(pl-u-test!
  "instantiate makes distinct vars for _"
  (fn
    ()
    (let
      ((env {}))
      (let
        ((v1 (pl-instantiate (list "var" "_") env))
          (v2 (pl-instantiate (list "var" "_") env)))
        (not (= (pl-var-id v1) (pl-var-id v2))))))
  true)

(pl-u-test!
  "instantiate compound recurses"
  (fn
    ()
    (let
      ((env {}))
      (let
        ((inst (pl-instantiate (list "compound" "p" (list (list "var" "X") (list "atom" "a"))) env))
          (x (dict-get env "X")))
        (pl-unify!
          inst
          (pl-c "p" (list (pl-a "foo") (pl-a "a")))
          (pl-mk-trail))
        (pl-walk x))))
  (list "atom" "foo"))

(pl-u-test!
  "deep-walk resolves nested vars"
  (fn
    ()
    (let
      ((t (pl-mk-trail)) (x (pl-mk-rt-var "X")) (y (pl-mk-rt-var "Y")))
      (pl-unify! x (pl-c "f" (list y (pl-a "b"))) t)
      (pl-unify! y (pl-a "a") t)
      (pl-walk-deep x)))
  (list "compound" "f" (list (list "atom" "a") (list "atom" "b"))))

;; ── Runner ────────────────────────────────────────────────────────
(define pl-unify-tests-run! (fn () {:failed pl-u-fail :passed pl-u-pass :total pl-u-count :failures pl-u-failures}))
