#lang scheme/base

(provide all-signature-tests)

(require rackunit
	 deinprogramm/define-record-procedures
	 deinprogramm/signature/signature
	 deinprogramm/signature/signature-german
	 deinprogramm/signature/signature-syntax)

(require scheme/promise)

(define integer (make-predicate-signature 'integer integer? 'integer-marker))
(define boolean (make-predicate-signature 'boolean boolean? 'boolean-marker))
(define %a (make-type-variable-signature 'a 'a-marker))
(define %b (make-type-variable-signature 'b 'b-marker))

(define-syntax say-no
  (syntax-rules ()
    ((say-no ?body ...)
     (let/ec exit
       (call-with-signature-violation-proc
	(lambda (obj signature message blame)
	  (exit 'no))
	(lambda ()
	  ?body ...))))))

(define-syntax failed-signature
  (syntax-rules ()
    ((say-no ?body ...)
     (let/ec exit
       (call-with-signature-violation-proc
	(lambda (obj signature message blame)
	  (exit signature))
	(lambda ()
	  ?body ...))))))

(define signature-tests
  (test-suite
   "Tests for signature combinators"

   (test-case
    "flat"
    (check-equal? (say-no (apply-signature integer 5)) 5)
    (check-equal? (say-no (apply-signature integer "foo")) 'no))

   (test-case
    "list"
    (define integer-list (make-list-signature 'integer-list integer #f))
    (check-equal? (say-no (apply-signature integer-list '(1 2 3)))
		  '(1 2 3))
    (check-equal? (say-no (apply-signature integer-list '#f))
		  'no)
    (check-eq? (failed-signature (apply-signature integer-list '(1 #f 3)))
	       integer))

   (test-case
    "list-cached"
    (define integer-list (make-list-signature 'integer-list integer #f))
    (define boolean-list (make-list-signature 'integer-list boolean #f))
    (define l '(1 2 3))
    (define foo "foo")
    (define no '(1 #f 3))
    (define no2 '(1 #f 3))
    (define integer-list->bool (make-procedure-signature 'integer-list->bool (list integer-list) boolean 'int->bool-marker))

    (check-equal? (say-no (apply-signature integer-list l))
		  '(1 2 3))
    (check-equal? (say-no (apply-signature integer-list l))
		  '(1 2 3))
    (check-equal? (say-no (apply-signature boolean-list l))
		  'no)
    (check-equal? (say-no (apply-signature integer-list foo))
		  'no)
    (check-equal? (say-no (apply-signature integer-list foo))
		  'no)
    (check-eq? (failed-signature (apply-signature integer-list no))
	       integer)
    (check-eq? (failed-signature (apply-signature integer-list no))
	       integer)

    (let ((proc (say-no (apply-signature integer-list->bool (lambda (l) (even? (car l)))))))
      (check-equal? (say-no (proc no)) 'no)
      (check-equal? (say-no (proc no)) 'no)
      (check-equal? (say-no (proc no2)) 'no)
      (check-equal? (say-no (proc no2)) 'no))
    )
 
   (test-case
    "vector"
    (define integer-vector (make-vector-signature 'integer-list integer #f))
    (check-equal? (say-no (apply-signature integer-vector '#(1 2 3)))
		  '#(1 2 3))
    (check-equal? (say-no (apply-signature integer-vector '#f))
		  'no)
    (check-eq? (failed-signature (apply-signature integer-vector '#(1 #f 3)))
	       integer))

   (test-case
    "vector/cached"
    (let ((count 0))
      (define counting-integer
	(make-predicate-signature 'counting-integer 
				  (lambda (obj)
				    (set! count (+ 1 count))
				    (integer? obj))
				  'integer-marker))

      (define integer-vector (make-vector-signature 'integer-list counting-integer #f))

      (define v1 '#(1 2 3))

      (check-eq? (say-no (apply-signature integer-vector v1))
		 v1)
      (check-equal? count 3)
      (check-eq? (say-no (apply-signature integer-vector v1))
		 v1)
      (check-equal? count 3)))


   (test-case
    "mixed"
    (define int-or-bool (make-mixed-signature 'int-or-bool
					     (list integer
						   boolean)
					     'int-or-bool-marker))
    (check-equal? (say-no (apply-signature int-or-bool #f))
		  #f)
    (check-equal? (say-no (apply-signature int-or-bool 17))
		  17)
    (check-equal? (say-no (apply-signature int-or-bool "foo"))
		  'no))

   (test-case
    "combined"
    (define octet (make-combined-signature
		   'octet
		   (list
		    integer
		    (make-predicate-signature '<256
					     (delay (lambda (x)
						      (< x 256)))
					     '<256-marker)
		    (make-predicate-signature 'non-negative
					     (delay (lambda (x)
						      (>= x 0)))
					     'non-negative-marker))
		   'octet-marker))
    (check-equal? (say-no (apply-signature octet #f))
		  'no)
    (check-equal? (say-no (apply-signature octet 17))
		  17)
    (check-equal? (say-no (apply-signature octet 0))
		  0)
    (check-equal? (say-no (apply-signature octet -1))
		  'no)
    (check-equal? (say-no (apply-signature octet 255))
		  255)
    (check-equal? (say-no (apply-signature octet 256))
		  'no)
    (check-equal? (say-no (apply-signature octet "foo"))
		  'no))

   (test-case
    "case"
    (define foo-or-bar (make-case-signature 'foo-or-bar '("foo" "bar") equal? 'foo-or-bar-marker))
    (check-equal? (say-no (apply-signature foo-or-bar #f))
		  'no)
    (check-equal? (say-no (apply-signature foo-or-bar "foo"))
		    "foo")
    (check-equal? (say-no (apply-signature foo-or-bar "bar"))
		  "bar"))

   (test-case
    "procedure"
    (define int->bool (make-procedure-signature 'int->bool (list integer) boolean 'int->bool-marker))
    (check-equal? (say-no (apply-signature int->bool #f))
		  'no)
    (check-equal? (say-no (apply-signature int->bool (lambda () "foo")))
		  'no)
    (check-equal? (say-no (apply-signature int->bool (lambda (x y) "foo")))
		  'no)
    (let ((proc (say-no (apply-signature int->bool (lambda (x) (odd? x))))))
      (check-pred procedure? proc)
      (check-equal? (proc 15) #t)
      (check-equal? (proc 16) #f)
      (check-equal? (say-no (proc "foo")) 'no))
    (let ((proc (say-no (apply-signature int->bool (lambda (x) (+ x 1))))))
      (check-equal? (say-no (proc 12)) 'no)))

   (test-case
    "type variable - simple"
    (check-equal? (say-no (apply-signature %a #f)) #f)
    (check-equal? (say-no (apply-signature %a 15)) 15))
   
   (test-case
    "type variable - list"
    (define a-list (make-list-signature 'a-list %a #f))
    (check-equal? (say-no (apply-signature a-list '(1 2 3)))
		  '(1 2 3))
    (check-equal? (say-no (apply-signature a-list '#f))
		  'no)
    (check-equal? (say-no (apply-signature a-list '(#f "foo" 5)))
		  '(#f "foo" 5)))

   (test-case
    "apply-signature/blame"
    (define int->bool (make-procedure-signature 'int->bool (list integer) boolean 'int->bool-marker))
    (let ((proc (say-no (apply-signature/blame int->bool (lambda (x) (odd? x))))))
      (check-pred procedure? proc)
      (check-equal? (proc 15) #t)
      (check-equal? (proc 16) #f)
      (check-equal? (say-no (proc "foo")) 'no))
    (let ((proc (say-no (apply-signature/blame int->bool (lambda (x) x)))))
      (call-with-signature-violation-proc
       (lambda (obj signature message blame)
	 (check-true (syntax? blame)))
       (lambda ()
	 (proc 5)))))
   ))

(define signature-syntax-tests
  (test-suite
   "Tests for signature syntax"

   (test-case
    "predicate"
    (define integer (signature (predicate integer?)))
    (check-equal? (say-no (apply-signature integer 5)) 5)
    (check-equal? (say-no (apply-signature integer "foo")) 'no))

   (test-case
    "list"
    (check-equal? (say-no (apply-signature (signature x (list-of %a)) 5)) 'no)
    (check-equal? (say-no (apply-signature (signature x (list-of %a)) '(1 2 3))) '(1 2 3))
    (check-equal? (say-no (apply-signature (signature x (list-of (predicate integer?))) '(1 2 3))) '(1 2 3))
    (check-equal? (say-no (apply-signature (signature x (list-of (predicate integer?))) '(1 #f 3))) 'no))

   (test-case
    "mixed"
    (define int-or-bool (signature (mixed integer boolean)))
    (check-equal? (say-no (apply-signature int-or-bool #f))
		  #f)
    (check-equal? (say-no (apply-signature int-or-bool 17))
		  17)
    (check-equal? (say-no (apply-signature int-or-bool "foo"))
		  'no))

   (test-case
    "combined"
    (define octet (signature (combined integer
				      (predicate (lambda (x)
						   (< x 256)))
				      (predicate (lambda (x)
						   (>= x 0))))))
    (check-equal? (say-no (apply-signature octet #f))
		  'no)
    (check-equal? (say-no (apply-signature octet 17))
		  17)
    (check-equal? (say-no (apply-signature octet 0))
		  0)
    (check-equal? (say-no (apply-signature octet -1))
		  'no)
    (check-equal? (say-no (apply-signature octet 255))
		  255)
    (check-equal? (say-no (apply-signature octet 256))
		  'no)
    (check-equal? (say-no (apply-signature octet "foo"))
		  'no))

   (test-case
    "procedure"
    (define int->bool (signature int->bool ((predicate integer?) -> (predicate boolean?))))
    (check-equal? (say-no (apply-signature int->bool #f))
		  'no)
    (check-equal? (say-no (apply-signature int->bool (lambda () "foo")))
		  'no)
    (check-equal? (say-no (apply-signature int->bool (lambda (x y) "foo")))
		  'no)
    (let ((proc (say-no (apply-signature int->bool (lambda (x) (odd? x))))))
      (check-pred procedure? proc)
      (check-equal? (proc 15) #t)
      (check-equal? (proc 16) #f)
      (check-equal? (say-no (proc "foo")) 'no))
    (let ((proc (say-no (apply-signature int->bool (lambda (x) (+ x 1))))))
      (check-equal? (say-no (proc 12)) 'no))) 


   (test-case
    "record-wrap"
    (define-record-procedures-parametric pare pare-of kons pare? (kar kdr))
    (define ctr (pare-of integer boolean))
    (let ((obj (apply-signature ctr (kons 1 #t))))
      (check-equal? (kar obj) 1)
      (check-equal? (kdr obj) #t))
    (check-equal? (say-no (apply-signature ctr (kons 1 2))) 'no)
    )

   (test-case
    "record-wrap/lazy"
    (define-struct pare (kar kdr extra)
      #:mutable
      #:property prop:lazy-wrap
      (make-lazy-wrap-info
       (lambda (kar kdr) (kons kar kdr))
       (list (lambda (x) (pare-kar x)) (lambda (x) (pare-kdr x)))
       (list (lambda (x v) (set-pare-kar! x v))
	     (lambda (x v) (set-pare-kdr! x v)))
       (lambda (x) (pare-extra x)) (lambda (x v) (set-pare-extra! x v))))
    (define (kons kar kdr)
      (make-pare kar kdr #f))
    (define (kar p)
      (check-lazy-wraps! struct:pare p)
      (pare-kar p))
    (define (kdr p)
      (check-lazy-wraps! struct:pare p)
      (pare-kdr p))
    (define (pare-of kar-sig kdr-sig)
      (make-lazy-wrap-signature 'pare #f 
				struct:pare
				pare?
				(list kar-sig kdr-sig)
				#f))
    (define ctr (pare-of integer boolean))
    (let ((obj (apply-signature ctr (kons 1 #t))))
      (check-equal? (kar obj) 1)
      (check-equal? (kdr obj) #t))
    (let ((obj (apply-signature ctr (kons 1 2))))
      (check-equal? (say-no (kar obj)) 'no))
    )

   (test-case
    "record-wrap-2"
    (let ((count 0))
      (define counting-integer
	(make-predicate-signature 'counting-integer 
				  (lambda (obj)
				    (set! count (+ 1 count))
				    (integer? obj))
				  'integer-marker))
      (define-record-procedures-parametric pare pare-of kons pare? (kar kdr))
      (define ctr (signature (pare-of counting-integer boolean)))
      (let ((obj (apply-signature ctr (apply-signature ctr (kons 1 #t)))))
	(check-equal? count 1)
	(check-equal? (kar obj) 1)
	(check-equal? count 1)
	(check-equal? (kdr obj) #t)
	(check-equal? count 1))))

   (test-case
    "record-wrap-2/lazy"
    (let ((count 0))
      (define counting-integer
	(make-predicate-signature 'counting-integer 
				  (lambda (obj)
				    (set! count (+ 1 count))
				    (integer? obj))
				  'integer-marker))

      (define-struct pare (kar kdr extra)
	#:mutable
	#:property prop:lazy-wrap
	(make-lazy-wrap-info
	 (lambda (kar kdr) (kons kar kdr))
	 (list (lambda (x) (pare-kar x)) (lambda (x) (pare-kdr x)))
	 (list (lambda (x v) (set-pare-kar! x v))
	       (lambda (x v) (set-pare-kdr! x v)))
	 (lambda (x) (pare-extra x)) (lambda (x v) (set-pare-extra! x v))))
      (define (kons kar kdr)
	(make-pare kar kdr #f))
      (define (kar p)
	(check-lazy-wraps! struct:pare p)
	(pare-kar p))
      (define (kdr p)
	(check-lazy-wraps! struct:pare p)
	(pare-kdr p))
      (define (pare-of kar-sig kdr-sig)
	(make-lazy-wrap-signature 'pare #f 
				  struct:pare
				  pare?
				  (list kar-sig kdr-sig)
				  #f))

      (define ctr (signature (pare-of counting-integer boolean)))
      (let ((obj (apply-signature ctr (apply-signature ctr (kons 1 #t)))))
	(check-equal? count 0)
	(check-equal? (kar obj) 1)
	(check-equal? count 1)
	(check-equal? (kdr obj) #t)
	(check-equal? count 1))))

   (test-case
    "record-wrap-3"
    (let ((count 0))
      (define counting-integer
	(make-predicate-signature 'counting-integer 
				 (lambda (obj)
				   (set! count (+ 1 count))
				   (integer? obj))
				 'integer-marker))

      (define-record-procedures-parametric pare pare-of kons pare? (kar kdr))
      (define ctr (signature (pare-of counting-integer boolean)))
      (let ((obj (apply-signature ctr (apply-signature ctr (kons 1 #t)))))
	(check-equal? count 1)
	(check-equal? (kar obj) 1)
	(check-equal? count 1)
	(check-equal? (kdr obj) #t)
	(check-equal? count 1)
	;; after checking, the system should remember that it did so
	(let ((obj-2 (apply-signature ctr obj)))
	  (check-equal? count 1)
	  (check-equal? (kar obj) 1)
	  (check-equal? count 1)
	  (check-equal? (kdr obj) #t)
	  (check-equal? count 1)))))

   (test-case
    "record-wrap-3/lazy"
    (let ((count 0))
      (define counting-integer
	(make-predicate-signature 'counting-integer 
				 (lambda (obj)
				   (set! count (+ 1 count))
				   (integer? obj))
				 'integer-marker))
      (define-struct pare (kar kdr extra)
	#:mutable
	#:property prop:lazy-wrap
	(make-lazy-wrap-info
	 (lambda (kar kdr) (kons kar kdr))
	 (list (lambda (x) (pare-kar x)) (lambda (x) (pare-kdr x)))
	 (list (lambda (x v) (set-pare-kar! x v))
	       (lambda (x v) (set-pare-kdr! x v)))
	 (lambda (x) (pare-extra x)) (lambda (x v) (set-pare-extra! x v))))
      (define (kons kar kdr)
	(make-pare kar kdr #f))
      (define (kar p)
	(check-lazy-wraps! struct:pare p)
	(pare-kar p))
      (define (kdr p)
	(check-lazy-wraps! struct:pare p)
	(pare-kdr p))
      (define (pare-of kar-sig kdr-sig)
	(make-lazy-wrap-signature 'pare #f 
				  struct:pare
				  pare?
				  (list kar-sig kdr-sig)
				  #f))

      (define ctr (signature (pare-of counting-integer boolean)))
      (let ((obj (apply-signature ctr (apply-signature ctr (kons 1 #t)))))
	(check-equal? count 0)
	(check-equal? (kar obj) 1)
	(check-equal? count 1)
	(check-equal? (kdr obj) #t)
	(check-equal? count 1)
	;; after checking, the system should remember that it did so
	(let ((obj-2 (apply-signature ctr obj)))
	  (check-equal? count 1)
	  (check-equal? (kar obj) 1)
	  (check-equal? count 1)
	  (check-equal? (kdr obj) #t)
	  (check-equal? count 1)))))

   (test-case
    "double-wrap"
    (let ((count 0))
      (define counting-integer
	(make-predicate-signature 'counting-integer 
				 (lambda (obj)
				   (set! count (+ 1 count))
				   (integer? obj))
				 'integer-marker))
      (define-record-procedures-parametric pare pare-of raw-kons pare? (kar kdr))

      (define empty-list (signature (predicate null?)))

      (define my-list-of
	(lambda (x)
	  (signature (mixed empty-list
			    (pare-of x (my-list-of x))))))
      
      (define/signature kons (signature (%a (my-list-of %a) -> (pare-of %a (my-list-of %a))))
	raw-kons)
      
      (define/signature build-list (signature (integer -> (my-list-of counting-integer)))
	(lambda (n)
	  (if (= n 0)
	      '()
	      (kons n (build-list (- n 1))))))

      (define/signature list-length (signature ((my-list-of counting-integer) -> integer))
	(lambda (lis)
	  (cond
	   ((null? lis) 0)
	   ((pare? lis)
	    (+ 1 (list-length (kdr lis)))))))
      
      ;; one wrap each for (my-list-of %a), one for (my-list-of counting-integer)
      (let  ((l1 (build-list 10)))
	(check-equal? count 10)
	(let ((len1 (list-length l1)))
	  (check-equal? count 10)))))

   (test-case
    "double-wrap/lazy"
    (let ((count 0))
      (define counting-integer
	(make-predicate-signature 'counting-integer 
				 (lambda (obj)
				   (set! count (+ 1 count))
				   (integer? obj))
				 'integer-marker))

      (define-struct pare (kar kdr extra)
	#:mutable
	#:property prop:lazy-wrap
	(make-lazy-wrap-info
	 (lambda (kar kdr) (raw-kons kar kdr))
	 (list (lambda (x) (pare-kar x)) (lambda (x) (pare-kdr x)))
	 (list (lambda (x v) (set-pare-kar! x v))
	       (lambda (x v) (set-pare-kdr! x v)))
	 (lambda (x) (pare-extra x)) (lambda (x v) (set-pare-extra! x v))))
      (define (raw-kons kar kdr)
	(make-pare kar kdr #f))
      (define (kar p)
	(check-lazy-wraps! struct:pare p)
	(pare-kar p))
      (define (kdr p)
	(check-lazy-wraps! struct:pare p)
	(pare-kdr p))
      (define (pare-of kar-sig kdr-sig)
	(make-lazy-wrap-signature 'pare #f 
				  struct:pare
				  pare?
				  (list kar-sig kdr-sig)
				  #f))


      (define empty-list (signature (predicate null?)))

      (define my-list-of
	(lambda (x)
	  (signature (mixed empty-list
			    (pare-of x (my-list-of x))))))
      
      (define/signature kons (signature (%a (my-list-of %a) -> (pare-of %a (my-list-of %a))))
	raw-kons)
      
      (define/signature build-list (signature (integer -> (my-list-of counting-integer)))
	(lambda (n)
	  (if (= n 0)
	      '()
	      (kons n (build-list (- n 1))))))

      (define/signature list-length (signature ((my-list-of counting-integer) -> integer))
	(lambda (lis)
	  (cond
	   ((null? lis) 0)
	   ((pare? lis)
	    (+ 1 (list-length (kdr lis)))))))
      
      ;; one wrap each for (my-list-of %a), one for (my-list-of counting-integer)
      (let  ((l1 (build-list 10)))
	(check-equal? count 0)
	(let ((len1 (list-length l1)))
	  (check-equal? count 10)))))

   (test-case
    "mixed wrap"

    (define-struct pare (kar kdr extra)
      #:mutable
      #:property prop:lazy-wrap
      (make-lazy-wrap-info
       (lambda (kar kdr) (raw-kons kar kdr))
       (list (lambda (x) (pare-kar x)) (lambda (x) (pare-kdr x)))
       (list (lambda (x v) (set-pare-kar! x v))
	     (lambda (x v) (set-pare-kdr! x v)))
       (lambda (x) (pare-extra x)) (lambda (x v) (set-pare-extra! x v))))
    (define (raw-kons kar kdr)
      (make-pare kar kdr #f))
    (define (kar p)
      (check-lazy-wraps! struct:pare p)
      (pare-kar p))
    (define (kdr p)
      (check-lazy-wraps! struct:pare p)
      (pare-kdr p))
    (define (pare-of kar-sig kdr-sig)
      (make-lazy-wrap-signature 'pare #f 
				struct:pare
				pare?
				(list kar-sig kdr-sig)
				#f))


    (define sig1 (signature (pare-of integer boolean)))
    (define sig2 (signature (pare-of boolean integer)))
    (define sig (signature (mixed sig1 sig2)))
    (define/signature x sig (raw-kons #t 15))
    (define/signature y sig (raw-kons #t #t))
    (check-equal? (kar x) #t)
    (check-equal? (say-no (kar y)) 'no))

   (test-case
    "wrap equality"
    (define-record-procedures-parametric pare pare-of raw-kons pare? (kar kdr))

    (define empty-list (signature (predicate null?)))
    
    (define my-list-of
      (lambda (x)
	(signature (mixed empty-list
			  (pare-of x (my-list-of x))))))
    
    (define/signature kons (signature (%a (my-list-of %a) -> (pare-of %a (my-list-of %a))))
      raw-kons)

    (check-equal? (raw-kons 1 '()) (raw-kons 1 '()))
    (check-equal? (kons 1 '()) (kons 1 '()))
    (check-equal? (kons 1 '()) (raw-kons 1 '()))
    (check-equal? (raw-kons 1 '()) (kons 1 '())))

   (test-case
    "pair-wrap"
    (define sig (make-pair-signature #f integer boolean))
    (let ((obj (apply-signature sig (cons 1 #t))))
      (check-equal? (checked-car obj) 1)
      (check-equal? (checked-cdr obj) #t))
    (let ((obj (apply-signature sig (cons 1 2))))
      (check-equal? (say-no (checked-car obj)) 'no))
    )

))


(define all-signature-tests
  (test-suite
   "all-signature-tests"
   signature-tests
   signature-syntax-tests))
