;; lib/guest/tests/hm.sx — exercises lib/guest/hm.sx algebra.

(define ghm-test-pass 0)
(define ghm-test-fail 0)
(define ghm-test-fails (list))

(define
  ghm-test
  (fn (name actual expected)
    (if (= actual expected)
      (set! ghm-test-pass (+ ghm-test-pass 1))
      (begin
        (set! ghm-test-fail (+ ghm-test-fail 1))
        (append! ghm-test-fails {:name name :expected expected :actual actual})))))

;; ── Type constructors ─────────────────────────────────────────────
(ghm-test "tv"           (hm-tv "a") (list :var "a"))
(ghm-test "int"          (hm-int)    (list :ctor "Int" (list)))
(ghm-test "arrow"        (ctor-head (hm-arrow (hm-int) (hm-bool))) "->")
(ghm-test "arrow-args-len" (len (ctor-args (hm-arrow (hm-int) (hm-bool)))) 2)

;; ── Schemes ───────────────────────────────────────────────────────
(ghm-test "scheme-vars"  (hm-scheme-vars (hm-scheme (list "a") (hm-tv "a"))) (list "a"))
(ghm-test "monotype-vars" (hm-scheme-vars (hm-monotype (hm-int))) (list))
(ghm-test "scheme?-yes"  (hm-scheme? (hm-monotype (hm-int))) true)
(ghm-test "scheme?-no"   (hm-scheme? (hm-int)) false)

;; ── Fresh tyvars ──────────────────────────────────────────────────
(ghm-test "fresh-1"
  (let ((c (list 0))) (var-name (hm-fresh-tv c))) "t1")
(ghm-test "fresh-bumps"
  (let ((c (list 5))) (begin (hm-fresh-tv c) (first c))) 6)

;; ── Free type variables ──────────────────────────────────────────
(ghm-test "ftv-int"      (hm-ftv (hm-int)) (list))
(ghm-test "ftv-tv"       (hm-ftv (hm-tv "a")) (list "a"))
(ghm-test "ftv-arrow"
  (len (hm-ftv (hm-arrow (hm-tv "a") (hm-arrow (hm-tv "b") (hm-tv "a"))))) 2)
(ghm-test "ftv-scheme-quantified"
  (hm-ftv-scheme (hm-scheme (list "a") (hm-arrow (hm-tv "a") (hm-tv "b")))) (list "b"))
(ghm-test "ftv-env"
  (let ((env (assoc {} "f" (hm-monotype (hm-arrow (hm-tv "x") (hm-tv "y"))))))
    (len (hm-ftv-env env))) 2)

;; ── Substitution / apply / compose ───────────────────────────────
(ghm-test "apply-tv"
  (hm-apply (assoc {} "a" (hm-int)) (hm-tv "a")) (hm-int))
(ghm-test "apply-arrow"
  (ctor-head
    (hm-apply (assoc {} "a" (hm-int))
              (hm-arrow (hm-tv "a") (hm-tv "b")))) "->")
(ghm-test "compose-1-then-2"
  (var-name
    (hm-apply
      (hm-compose (assoc {} "b" (hm-tv "c")) (assoc {} "a" (hm-tv "b")))
      (hm-tv "a"))) "c")

;; ── Generalize / Instantiate ─────────────────────────────────────
;;   forall a. a -> a   instantiated twice yields fresh vars each time
(ghm-test "generalize-id"
  (len (hm-scheme-vars (hm-generalize (hm-arrow (hm-tv "a") (hm-tv "a")) {}))) 1)

(ghm-test "generalize-skips-env"
  ;; ftv(t)={a,b}, ftv(env)={a}, qs={b}
  (let ((env (assoc {} "x" (hm-monotype (hm-tv "a")))))
    (len (hm-scheme-vars
           (hm-generalize (hm-arrow (hm-tv "a") (hm-tv "b")) env)))) 1)

(ghm-test "instantiate-fresh"
  (let ((s (hm-scheme (list "a") (hm-arrow (hm-tv "a") (hm-tv "a"))))
        (c (list 0)))
    (let ((t1 (hm-instantiate s c)) (t2 (hm-instantiate s c)))
      (not (= (var-name (first (ctor-args t1)))
              (var-name (first (ctor-args t2)))))))
  true)

;; ── Inference (literal only) ─────────────────────────────────────
(ghm-test "infer-int"
  (ctor-head (get (hm-infer-literal (ast-literal 42)) :type)) "Int")
(ghm-test "infer-string"
  (ctor-head (get (hm-infer-literal (ast-literal "hi")) :type)) "String")
(ghm-test "infer-bool"
  (ctor-head (get (hm-infer-literal (ast-literal true)) :type)) "Bool")

(define ghm-tests-run!
  (fn ()
    {:passed ghm-test-pass
     :failed ghm-test-fail
     :total  (+ ghm-test-pass ghm-test-fail)}))
