#lang racket/base

(require "../main.rkt")

(require racket/match)
(require racket/set)

(require rackunit)
(require racket/runtime-path)
(require syntax/srcloc)

(define (d bs #:allow-invalid-prefix? [allow-invalid-prefix? #f])
  (for [(i (in-range 1 (- (bytes-length bs) 1)))]
    (define result (bytes->preserve (subbytes bs 0 i)
                                    #:decode-embedded strip-annotations
                                    #:on-short (lambda () 'short) void))
    (when (and (not (eq? result 'short))
               (not (and allow-invalid-prefix? (void? result))))
      (error 'd "~a-byte prefix of ~v does not read as short; result: ~v" i bs result)))
  (bytes->preserve bs
                   #:read-syntax? #t
                   #:decode-embedded strip-annotations
                   #:on-short (lambda () 'short)
                   void))

(define (d-strip bs) (strip-annotations (d bs)))

(struct discard () #:prefab)
(struct capture (detail) #:prefab)
(struct observe (specification) #:prefab)

(struct speak (who what) #:prefab)

(struct date (year month day) #:prefab)
(struct thing (id) #:prefab)
(struct person thing (name date-of-birth) #:prefab)
(struct titled person (title) #:prefab)

(struct asymmetric (forward back))

(define samples-txt-expected
  (hash 'record1 (capture (discard))
        'record2 (observe (speak (discard) (capture (discard))))
        'list4a '(1 2 3 4)
        'list5 '(-2 -1 0 1)
        'string3 "hello"
        'list6 `("hello" there #"world" () ,(set) #t #f)
        'bytes2 #"hello"
        'bytes3 #"ABC"
        'bytes4 #"ABC"
        'bytes5 #"AJN"
        'bytes7 #"corymb"
        'bytes8 #"corymb"
        'bytes9 #"Hi"
        'bytes10 #"Hi"
        'bytes11 #"Hi"
        'value1 #"corymb"
        'value2 #t
        'value3 #t
        'value4 #t
        'value5 #t
        'value6 (list 1 2 3)
        'list0 '()
        'dict0 (hash)
        'string0 ""
        'symbol0 '||
        'set0 (set)
        'set1 (set 1 2 3)
        'set1a (set 1 2 3)
        'string4 "abc\u6c34\u6C34\\/\"\b\f\n\r\txyz"
        'bytes13 #"abc\x6c\x34\xf0\\/\"\b\f\n\r\txyz"
        'string5 "\U0001D11E"
        'record1 (capture (discard))
        'record2 (observe (speak (discard) (capture (discard))))
        'record3 (titled 101 "Blackwell" (date 1821 2 3) "Dr")
        'record4 (asymmetric (record 'discard '()) (discard))
        'record5 (record 7 '(()))
        'record6 (asymmetric (record 'discard '(surprise))
                             '#s(discard surprise))
        'record7 (record "aString" '(3 4))
        'record8 (record (discard) '(3 4))
        'list7 (list 'abc '|...| 'def)
        'dict1 (hash 'a 1
                     "b" #t
                     '(1 2 3) #"c"
                     (hash 'first-name "Elizabeth") (hash 'surname "Blackwell"))
        'rfc8259-example1 (hash "Image"
                                (hash "Width" 800
                                      "Height" 600
                                      "Title" "View from 15th Floor"
                                      "Thumbnail" (hash "Url" "http://www.example.com/image/481989943"
                                                        "Height" 125
                                                        "Width" 100)
                                      "Animated" 'false
                                      "IDs" (list 116 943 234 38793)))
        'rfc8259-example2 (list (hash
                                 "precision" "zip"
                                 "Latitude"  37.7668
                                 "Longitude" -122.3959
                                 "Address"   ""
                                 "City"      "SAN FRANCISCO"
                                 "State"     "CA"
                                 "Zip"       "94107"
                                 "Country"   "US")
                                (hash
                                 "precision" "zip"
                                 "Latitude"  37.371991
                                 "Longitude" -122.026020
                                 "Address"   ""
                                 "City"      "SUNNYVALE"
                                 "State"     "CA"
                                 "Zip"       "94085"
                                 "Country"   "US"))
        'annotation1 (asymmetric (annotate 9 "abc") 9)
        'annotation2 (asymmetric (annotate (list '() (annotate '() "x")) "abc" "def") '(() ()))
        'annotation3 (asymmetric (annotate 5 (annotate 2 1) (annotate 4 3)) 5)
        'annotation4 (asymmetric (hash (annotate 'a 'ak) (annotate 1 'av)
                                       (annotate 'b 'bk) (annotate 2 'bv))
                                 (hash 'a 1 'b 2))
        'annotation5 (asymmetric (annotate `#s(R ,(annotate 'f 'af)) 'ar) `#s(R f))
        'annotation6 (asymmetric (record (annotate 'R 'ar) (list (annotate 'f 'af))) `#s(R f))
        'annotation7 (asymmetric (annotate '() 'a 'b 'c) '())
        ))

(define (run-test-case variety t-name loc binary-form annotated-text-form)
  (define text-form (strip-annotations annotated-text-form))
  (define-values (forward back can-execute-nondet-with-canonicalization?)
    (match (hash-ref samples-txt-expected t-name text-form)
      [(asymmetric f b) (values f b #f)] ;; #f because e.g. annotation4 includes annotations
      [v (values v v #t)]))
  (check-equal? text-form back loc)                      ;; expectation 1
  (check-equal? (d-strip (preserve->bytes #:encode-embedded values text-form))
                back
                loc)                                     ;; expectation 2
  (check-equal? (d-strip (preserve->bytes #:encode-embedded values forward))
                back
                loc)                                     ;; expectation 3
  (check-equal? (d-strip binary-form) back loc)          ;; expectation 4
  (check-equal? (d binary-form) annotated-text-form loc) ;; expectation 5
  (check-equal? (d (preserve->bytes #:encode-embedded values annotated-text-form))
                annotated-text-form
                loc)                                     ;; expectation 6
  (check-equal? (string->preserve #:decode-embedded strip-annotations
                                  (preserve->string #:encode-embedded values text-form))
                back
                loc)                                     ;; expectation 7
  (check-equal? (string->preserve #:decode-embedded strip-annotations
                                  (preserve->string #:encode-embedded values forward))
                back
                loc)                                     ;; expectation 8
  ;; similar to 8:
  (check-equal? (string->preserve #:decode-embedded strip-annotations
                                  (preserve->string #:encode-embedded values
                                                    annotated-text-form)
                                  #:read-syntax? #t)
                annotated-text-form
                loc)
  (when (and (not (memq variety '(decode)))
             (or (not (memq variety '(nondeterministic)))
                 (and can-execute-nondet-with-canonicalization?)))
    ;; expectations 9 and 10
    (check-equal? (preserve->bytes forward
                                   #:encode-embedded values
                                   #:canonicalizing? #t
                                   #:write-annotations? #t)
                  binary-form
                  loc))
  (unless (memq variety '(decode nondeterministic))
    ;; expectation 11
    (check-equal? (preserve->bytes annotated-text-form
                                   #:encode-embedded values
                                   #:write-annotations? #t)
                  binary-form
                  loc)))

(define-runtime-path samples-pr-path "./samples.pr")
(let* ((testfile (call-with-input-file samples-pr-path
                   (lambda (p)
                     (port-count-lines! p)
                     (read-preserve p
                                    #:read-syntax? #t
                                    #:decode-embedded strip-annotations
                                    #:source samples-pr-path)))))
  (match-define (peel-annotations `#s(TestCases ,tests)) testfile)
  (for [((t-name* t*) (in-hash (annotated-item tests)))]
    (define t-name (strip-annotations t-name*))
    (define loc (format "Test case '~a' (~a)" t-name (source-location->string (annotated-srcloc t*))))
    (define (fail-test fmt . args)
      (fail (format "~a: ~a" loc (apply format fmt args))))
    (displayln loc)
    (match (peel-annotations t*)
      [`#s(Test ,(strip-annotations binary-form) ,annotated-text-form)
       (run-test-case 'normal t-name loc binary-form annotated-text-form)]
      [`#s(NondeterministicTest ,(strip-annotations binary-form) ,annotated-text-form)
       (run-test-case 'nondeterministic t-name loc binary-form annotated-text-form)]
      [`#s(DecodeTest ,(strip-annotations binary-form) ,annotated-text-form)
       (run-test-case 'decode t-name loc binary-form annotated-text-form)]
      [`#s(ParseError ,(strip-annotations str))
       (with-handlers [(exn:fail:read:eof?
                        (lambda (e) (fail-test "Unexpected EOF: ~e" e)))
                       (exn:fail:read?
                        (lambda (e) 'ok))
                       ((lambda (e) #t)
                        (lambda (e) (fail-test "Unexpected exception: ~e" e)))]
         (string->preserve str)
         (fail-test "Unexpected success"))]
      [(or `#s(ParseShort ,(strip-annotations str))
           `#s(ParseEOF ,(strip-annotations str)))
       (with-handlers [(exn:fail:read:eof? (lambda (e) 'ok))
                       ((lambda (e) #t)
                        (lambda (e) (fail-test "Unexpected exception: ~e" e)))]
         (string->preserve str)
         (fail-test "Unexpected success"))]
      [(or `#s(DecodeShort ,(strip-annotations bs))
           `#s(DecodeEOF ,(strip-annotations bs)))
       (check-eq? (d bs) 'short loc)]
      [`#s(DecodeError ,(strip-annotations bs))
       (check-true (void? (d bs #:allow-invalid-prefix? #t)) loc)]
      [_
       (write-preserve/text t* #:indent #f)
       (newline)]))
  )
