#lang racket/base

(require "test-utils.rkt"
         racket/list racket/format rackunit
         (static-contracts instantiate optimize combinators structures)
         (for-syntax racket/base syntax/parse))

(provide tests)
(gen-test-main)

(define-syntax (check-optimize stx)
  (syntax-parse stx
    [(_ argument* #:pos positive-expected* #:neg negative-expected*)
     #'(test-suite (~a 'argument*)
         (test-case "Trusted Positive"
           (check-optimize-helper argument* positive-expected* #t #f))
         (test-case "Trusted Negative"
           (check-optimize-helper argument* negative-expected* #f #t)))]))

(define (check-optimize-helper argument expected trusted-positive trusted-negative)
  (define trusted-side
    (cond
      [(and trusted-positive trusted-negative) 'both]
      [trusted-positive 'positive]
      [trusted-negative 'negative]
      [else 'neither]))
  (with-check-info*
    (list (make-check-info 'original argument)
          (make-check-info 'trusted trusted-side)
          (make-check-expected expected))
    (λ ()
      (let ([opt (optimize argument
                   #:trusted-positive trusted-positive
                   #:trusted-negative trusted-negative)])
        (with-check-info* (list (make-check-actual opt))
          (lambda ()
            (unless (equal? opt expected)
              (fail-check))))))))

(define-syntax (check-syntax stx)
  (syntax-parse stx
    [(_ argument* expected*)
     #'(test-case (~a 'argument*)
         (define argument argument*)
         (define expected expected*)
         (with-check-info*
           (list (make-check-info 'original argument)
                 (make-check-expected expected))
           (λ ()
             (let ([ctc (syntax->datum
                         (cadr
                          (instantiate
                            (optimize argument #:trusted-positive #t)
                            (λ (#:reason [reason #f]) (error 'nyi))
                            'impersonator)))])
               (with-check-info* (list (make-check-actual ctc))
                 (λ ()
                   (unless (equal? ctc expected)
                     (fail-check))))))))]))

;; Ids with unique identity so that equals works
(define foo-id #'foo)
(define bar-id #'bar)

(define syntax-tests
  (test-suite "Optimized Syntax Tests"
    (check-syntax list?/sc
      'any/c)
    (check-syntax (arr/sc null #f (list list?/sc))
      '(-> any))
    (check-syntax (hash/sc list?/sc list?/sc)
      '(hash/c list? list?))

    ))


(define optimizer-tests
  (test-suite "Optimizer Tests"
    ;; Lists
    (check-optimize (listof/sc any/sc)
      #:pos any/sc
      #:neg list?/sc)
    (check-optimize (listof/sc none/sc)
      #:pos any/sc
      #:neg empty-list/sc)

    ;; Heterogeneous Lists
    ;; TODO fix ability to test equality here
    #;
    (check-optimize (list/sc any/sc)
      #:pos any/sc
      #:neg (list-length/sc 1))
    (check-optimize (list/sc none/sc)
      #:pos any/sc
      #:neg none/sc)
    (check-optimize (list/sc)
      #:pos any/sc
      #:neg empty-list/sc)

    ;; Sets
    (check-optimize (set/sc any/sc)
      #:pos any/sc
      #:neg set?/sc)
    (check-optimize (set/sc none/sc)
      #:pos any/sc
      #:neg empty-set/sc)


    ;; Vectors
    (check-optimize (vectorof/sc any/sc)
      #:pos any/sc
      #:neg vector?/sc)
    (check-optimize (vectorof/sc none/sc)
      #:pos (vectorof/sc none/sc)
      #:neg (vectorof/sc none/sc))

    ;; Heterogeneous Vectors
    ;; TODO fix ability to test equality here
    #;
    (check-optimize (vector/sc any/sc)
      #:pos any/sc
      #:neg (vector-length/sc 1))
    (check-optimize (vector/sc none/sc)
      #:pos (vector/sc none/sc)
      #:neg (vector/sc none/sc))
    ;; TODO fix ability to test equality here
    #;
    (check-optimize (vector/sc)
      #:pos any/sc
      #:neg empty-vector/sc)
    (check-optimize (vector/sc set?/sc)
      #:pos (vector/sc set?/sc)
      #:neg (vector/sc set?/sc))

    ;; HashTables
    (check-optimize (hash/sc any/sc any/sc)
      #:pos any/sc
      #:neg hash?/sc)
    (check-optimize (hash/sc none/sc any/sc)
      #:pos (hash/sc none/sc any/sc)
      #:neg (hash/sc none/sc any/sc))
    (check-optimize (hash/sc any/sc none/sc)
      #:pos (hash/sc any/sc none/sc)
      #:neg (hash/sc any/sc none/sc))

    ;; And
    (check-optimize (and/sc set?/sc)
      #:pos any/sc
      #:neg set?/sc)
    (check-optimize (and/sc set?/sc any/sc)
      #:pos any/sc
      #:neg set?/sc)
    (check-optimize (and/sc set?/sc none/sc)
      #:pos any/sc
      #:neg none/sc)
    (check-optimize (and/sc)
      #:pos any/sc
      #:neg any/sc)
    (check-optimize (and/sc any/sc any/sc)
      #:pos any/sc
      #:neg any/sc)


    ;; Or
    (check-optimize (or/sc set?/sc)
      #:pos any/sc
      #:neg set?/sc)
    (check-optimize (or/sc set?/sc any/sc)
      #:pos any/sc
      #:neg any/sc)
    (check-optimize (or/sc set?/sc none/sc)
      #:pos any/sc
      #:neg set?/sc)
    (check-optimize (or/sc)
      #:pos any/sc
      #:neg none/sc)
    (check-optimize (or/sc none/sc none/sc)
      #:pos any/sc
      #:neg none/sc)

    ;; None
    (check-optimize none/sc
      #:pos any/sc
      #:neg none/sc)

    ;; Boxes
    (check-optimize (box/sc any/sc)
      #:pos any/sc
      #:neg box?/sc)
    (check-optimize (box/sc none/sc)
      #:pos (box/sc none/sc)
      #:neg (box/sc none/sc))
    (check-optimize (box/sc set?/sc)
      #:pos (box/sc set?/sc)
      #:neg (box/sc set?/sc))

    ;; Syntax Objects
    (check-optimize (syntax/sc any/sc)
      #:pos any/sc
      #:neg syntax?/sc)
    (check-optimize (syntax/sc none/sc)
      #:pos any/sc
      #:neg none/sc)
    (check-optimize (syntax/sc set?/sc)
      #:pos any/sc
      #:neg (syntax/sc set?/sc))

    ;; Promises
    (check-optimize (promise/sc any/sc)
      #:pos any/sc
      #:neg promise?/sc)
    (check-optimize (promise/sc none/sc)
      #:pos any/sc
      #:neg (promise/sc none/sc))
    (check-optimize (promise/sc set?/sc)
      #:pos any/sc
      #:neg (promise/sc set?/sc))
    (check-optimize (promise/sc (box/sc set?/sc))
      #:pos (promise/sc (box/sc set?/sc))
      #:neg (promise/sc (box/sc set?/sc)))

    (check-optimize
      (function/sc (list (listof/sc any/sc))
                   (list)
                   (list)
                   (list)
                   #f
                   (list (listof/sc any/sc)))
      #:pos
      (function/sc (list list?/sc)
                   (list)
                   (list)
                   (list)
                   #f
                   #f)
      #:neg
      (function/sc (list any/sc)
                   (list)
                   (list)
                   (list)
                   #f
                   (list list?/sc)))
    (check-optimize
      (function/sc (list (listof/sc any/sc))
                   (list)
                   (list)
                   (list)
                   #f
                   (list any/sc))
      #:pos
      (function/sc (list list?/sc)
                   (list)
                   (list)
                   (list)
                   #f
                   #f)
      #:neg
      (function/sc (list any/sc)
                   (list)
                   (list)
                   (list)
                   #f
                   (list any/sc)))

    (check-optimize (case->/sc empty)
      #:pos (case->/sc empty)
      #:neg (case->/sc empty))
    (check-optimize (parameter/sc list?/sc set?/sc)
      #:pos (parameter/sc list?/sc any/sc)
      #:neg (parameter/sc any/sc set?/sc))

    (check-optimize
      (case->/sc (list (arr/sc (list (listof/sc any/sc)) (listof/sc (set/sc any/sc)) (list (listof/sc any/sc)))))
      #:pos (case->/sc (list (arr/sc (list list?/sc) (listof/sc set?/sc) #f)))
      #:neg (case->/sc (list (arr/sc (list any/sc) any/sc (list list?/sc)))))

    (check-optimize
      (object/sc #t (list (member-spec 'field 'x (listof/sc any/sc))))
      #:pos (object/sc #t (list (member-spec 'field 'x list?/sc)))
      #:neg (object/sc #t (list (member-spec 'field 'x list?/sc))))

    (check-optimize
      (object/sc #f (list (member-spec 'field 'x (listof/sc any/sc))))
      #:pos (object/sc #f (list (member-spec 'field 'x list?/sc)))
      #:neg (object/sc #f (list (member-spec 'field 'x list?/sc))))

    (check-optimize
      (class/sc #t (list (member-spec 'field 'x (listof/sc any/sc))) null)
      #:pos (class/sc #t (list (member-spec 'field 'x list?/sc)) null)
      #:neg (class/sc #t (list (member-spec 'field 'x list?/sc)) null))

    (check-optimize
      (class/sc #f (list (member-spec 'field 'x (listof/sc any/sc))) null)
      #:pos (class/sc #f (list (member-spec 'field 'x list?/sc)) null)
      #:neg (class/sc #f (list (member-spec 'field 'x list?/sc)) null))

    (check-optimize
      (recursive-sc (list foo-id bar-id)
                    (list (listof/sc (recursive-sc-use foo-id))
                          (listof/sc (recursive-sc-use bar-id)))
                    (recursive-sc-use foo-id))
      #:pos (recursive-sc (list foo-id)
                          (list (listof/sc (recursive-sc-use foo-id)))
                          (recursive-sc-use foo-id))
      #:neg (recursive-sc (list foo-id)
                          (list (listof/sc (recursive-sc-use foo-id)))
                          (recursive-sc-use foo-id)))

    (check-optimize
      (recursive-sc (list foo-id bar-id)
                    (list (listof/sc any/sc )
                          (listof/sc any/sc))
                    (recursive-sc-use foo-id))
      #:pos any/sc
      #:neg list?/sc)

    (check-optimize (cons/sc any/sc list?/sc)
      #:pos any/sc
      #:neg (cons/sc any/sc list?/sc))

    (check-optimize
      (case->/sc
        (list
          (arr/sc empty #f (list set?/sc))
          (arr/sc (list identifier?/sc) #f (list (listof/sc set?/sc)))))
      #:pos  (function/sc (list)
                          (list identifier?/sc)
                          (list)
                          (list)
                          #f
                          #f)
      #:neg (case->/sc
              (list
                (arr/sc empty #f (list set?/sc))
                (arr/sc (list any/sc) #f (list (listof/sc set?/sc))))))

    ))

(define tests
  (test-suite "Static Contracts"
    syntax-tests
    optimizer-tests))
