#lang scheme

(require "../private/matcher.rkt"
         (for-syntax syntax/parse)
         errortrace/errortrace-lib
         errortrace/errortrace-key
         racket/runtime-path)
(provide test test-syn-err tests reset-count
         syn-err-test-namespace
         print-tests-passed
         equal/bindings?
         test-contract-violation
         test-runtime-err
         exec-syntax-error-tests
         exec-runtime-error-tests)

(define-runtime-path this-dir ".")

(define syn-err-test-namespace (make-base-namespace))
(parameterize ([current-namespace syn-err-test-namespace])
  (eval '(require redex/reduction-semantics)))

(define (read-syntax-test path)
  (call-with-input-file path
    (λ (port)
      (port-count-lines! port)
      (read-syntax path port))))

(define-syntax (test stx)
  (syntax-case stx ()
    [(_ expected got)
     (with-syntax ([line (syntax-line stx)]
                   [fn (if (path? (syntax-source (syntax got)))
                           (path->string (syntax-source (syntax got)))
                           "<unknown file>")])
       (syntax/loc stx (test/proc (λ () expected) got line fn)))]))

(define (syntax-error-test-setup thunk)
  (parameterize ([current-namespace syn-err-test-namespace])
    (with-handlers ([exn:fail:syntax? 
                     (λ (exn) 
                       (values (exn-message exn)
                               (map source-location (exn:fail:syntax-exprs exn))))])
      (thunk))))
(define (runtime-error-test-setup thunk)
  (parameterize ([current-compile (make-errortrace-compile-handler)])
    (with-handlers ([exn:fail? 
                     (λ (exn) 
                       (values (exn-message exn)
                               (let ([marks (continuation-mark-set->list
                                             (exn-continuation-marks exn)
                                             errortrace-key)])
                                 (if (null? marks) '() (list (cdar marks))))))])
      (thunk))))

(define ((exec-error-tests setup exec) path)
  (for ([test (read-tests (build-path this-dir path))])
       (exec-error-test test exec setup)))
(define exec-syntax-error-tests
  (exec-error-tests syntax-error-test-setup expand))
(define exec-runtime-error-tests
  (exec-error-tests runtime-error-test-setup eval))

(define (exec-error-test spec exec setup)
  (define-values (file line expected-message expected-sources test)
    (make-error-test spec))
  (let-values ([(actual-message actual-sources)
                (setup (λ () (begin (exec test) (values "" '()))))])
    (test/proc (λ () actual-message) expected-message line file)
    (test/proc (λ () actual-sources) expected-sources line file)))

(define (make-error-test spec)
  (syntax-case spec ()
    [(message named-pieces body)
     (make-error-test (syntax/loc spec (message named-pieces () body)))]
    [(message ([loc-name loc-piece] ...) ([non-loc-name non-loc-piece] ...) body)
     (values (syntax-source spec)
             (syntax-line spec)
             (syntax-e #'message)
             (map source-location (syntax->list #'(loc-piece ...)))
             #'(let-syntax ([subst 
                             (λ (stx)
                               (syntax-case stx ()
                                 [(_ loc-name ... non-loc-name ...)
                                  #'body]))])
                 (subst loc-piece ... non-loc-piece ...)
                 (void)))]))

(define (source-location stx)
  (list (syntax-source stx) 
        (syntax-line stx) 
        (syntax-column stx) 
        (syntax-position stx)
        (syntax-span stx)))

(define (read-tests path)
  (call-with-input-file path
    (λ (port)
      (port-count-lines! port)
      (let loop ()
        (define test (read-syntax path port))
        (if (eof-object? test)
            '()
            (cons test (loop)))))))

(define-syntax (test-syn-err stx)
  #'(void))

(define-syntax (test-runtime-err stx)
  #'(void)
  #;
  #`(parameterize ([current-compile (make-errortrace-compile-handler)])
      #,(test-error-location 
         stx
         eval
         #'[exn:fail? 
            (λ (exn) 
              (values (exn-message exn)
                      (let ([marks (continuation-mark-set->list
                                    (exn-continuation-marks exn)
                                    errortrace-key)])
                        (if (null? marks) #f (list (cdar marks))))))])))

(define tests 0)
(define failures 0)
(define (reset-count) 
  (set! tests 0)
  (set! failures 0))

(define (print-tests-passed filename)
  (cond
    [(= 0 failures)
     (printf "~a: all ~a tests passed.\n" filename tests)]
    [else
     (printf "~a: ~a test~a failed.\n" filename failures (if (= 1 failures) "" "s"))]))

(define (test/proc run expected line filename)
  ;(printf "testing line ~s:~s\n" filename line)
  (let ([got (with-handlers ((exn:fail? values)) (run))])
    (set! tests (+ tests 1))
    (unless (and (not (exn? got))
                 (matches? got expected))
      (set! failures (+ 1 failures))
      (fprintf (current-error-port)
               "test: file ~a line ~a:\n     got ~s\nexpected ~s\n\n" 
               filename 
               line
               got
               expected))))

(define (matches? got expected)
  (cond
    [(regexp? expected)
     (and (string? got) (regexp-match expected got) #t)]
    [else
     (equal/bindings? got expected)]))

;; equal/bindings? : any any -> boolean
;; compares two sexps (with embedded bindings) for equality.
;; uses an order-insensitive comparison for the bindings
(define (equal/bindings? fst snd)
  (let loop ([fst fst]
             [snd snd])
    (cond
      [(pair? fst)
       (and (pair? snd) 
            (loop (car fst) (car snd))
            (loop (cdr fst) (cdr snd)))]
      [(mtch? fst)
       (and (mtch? snd)
            (loop (mtch-bindings fst)
                  (mtch-bindings snd))
            (let ([g1 (gensym 'run-match-test-sym)])
              (equal/bindings? (mtch-context fst)
                               (mtch-context snd)))
            (equal/bindings? (mtch-hole fst)
                             (mtch-hole snd)))]
      [(bindings? fst)
       (and (bindings? snd)
            (let ([fst-table (bindings-table fst)]
                  [snd-table (bindings-table snd)])
              (and (= (length fst-table)
                      (length snd-table))
                   (andmap
                    loop
                    (sort fst-table rib-lt)
                    (sort snd-table rib-lt)))))]
      [(and (bind? fst)
            (bind? snd)
            (context? (bind-exp fst))
            (context? (bind-exp snd)))
       (and (equal? (bind-name fst) (bind-name snd))
            (let ([g (gensym 'run-match-test-sym2)])
              (equal/bindings? (bind-exp fst)
                               (bind-exp snd))))]
      [(and (hole? fst)
            (hole? snd))
       #t]
      [else (equal? fst snd)])))

;; rib-lt : rib rib -> boolean
(define (rib-lt r1 r2) (string<=? (format "~s" (bind-name r1))
                                  (format "~s" (bind-name r2))))

(define-syntax (test-contract-violation stx)
  (syntax-parse stx
    [(_ expr
        (~or (~once (~seq #:blaming blaming:expr))
             (~optional (~seq #:message message:expr)
                        #:defaults ([message #'""]))
             (~optional (~seq #:extract extract:expr)
                        #:defaults ([extract #'values])))
        ...)
     #`(test (with-handlers ([(λ (exn)
                                (let ([exn (extract exn)])
                                  (and (exn:fail:contract:blame? exn)
                                       (regexp-match? 
                                        blaming
                                        (format "~a" (blame-positive (exn:fail:contract:blame-object exn)))))))
                              exn-message])
               expr
               (gensym 'no-violation))
             #,(syntax/loc stx (regexp message)))]))
