(define-library (srfi 69 test)
  (export run-tests)
  (import (chibi) (srfi 1) (srfi 69) (chibi test))
  (begin
    (define (run-tests)

      (define-syntax test-lset-eq?
        (syntax-rules ()
          ((test-lset= . args)
           (test-equal (lambda (a b) (lset= eq? a b)) . args))))

      (define-syntax test-lset-equal?
        (syntax-rules ()
          ((test-lset-equal? . args)
           (test-equal (lambda (a b) (lset= equal? a b)) . args))))

      (test-begin "srfi-69: hash-tables")

      (let ((ht (make-hash-table eq?)))
        ;; 3 initial elements
        (test 0 (hash-table-size ht))
        (hash-table-set! ht 'cat 'black)
        (hash-table-set! ht 'dog 'white)
        (hash-table-set! ht 'elephant 'pink)
        (test 3 (hash-table-size ht))
        (test-assert (hash-table-exists? ht 'dog))
        (test-assert (hash-table-exists? ht 'cat))
        (test-assert (hash-table-exists? ht 'elephant))
        (test-not (hash-table-exists? ht 'goose))
        (test 'white (hash-table-ref ht 'dog))
        (test 'black (hash-table-ref ht 'cat))
        (test 'pink (hash-table-ref ht 'elephant))
        (test-error (hash-table-ref ht 'goose))
        (test 'grey (hash-table-ref ht 'goose (lambda () 'grey)))
        (test 'grey (hash-table-ref/default ht 'goose 'grey))
        (test-lset-eq? '(cat dog elephant) (hash-table-keys ht))
        (test-lset-eq? '(black white pink) (hash-table-values ht))
        (test-lset-equal? '((cat . black) (dog . white) (elephant . pink))
                          (hash-table->alist ht))

        ;; remove an element
        (hash-table-delete! ht 'dog)
        (test 2 (hash-table-size ht))
        (test-not (hash-table-exists? ht 'dog))
        (test-assert (hash-table-exists? ht 'cat))
        (test-assert (hash-table-exists? ht 'elephant))
        (test-error (hash-table-ref ht 'dog))
        (test 'black (hash-table-ref ht 'cat))
        (test 'pink (hash-table-ref ht 'elephant))
        (test-lset-eq? '(cat elephant) (hash-table-keys ht))
        (test-lset-eq? '(black pink) (hash-table-values ht))
        (test-lset-equal? '((cat . black) (elephant . pink)) (hash-table->alist ht))

        ;; remove a non-existing element
        (hash-table-delete! ht 'dog)
        (test 2 (hash-table-size ht))
        (test-not (hash-table-exists? ht 'dog))

        ;; overwrite an existing element
        (hash-table-set! ht 'cat 'calico)
        (test 2 (hash-table-size ht))
        (test-not (hash-table-exists? ht 'dog))
        (test-assert (hash-table-exists? ht 'cat))
        (test-assert (hash-table-exists? ht 'elephant))
        (test-error (hash-table-ref ht 'dog))
        (test 'calico (hash-table-ref ht 'cat))
        (test 'pink (hash-table-ref ht 'elephant))
        (test-lset-eq? '(cat elephant) (hash-table-keys ht))
        (test-lset-eq? '(calico pink) (hash-table-values ht))
        (test-lset-equal? '((cat . calico) (elephant . pink)) (hash-table->alist ht))

        ;; walk and fold
        (test-lset-equal?
         '((cat . calico) (elephant . pink))
         (let ((a '()))
           (hash-table-walk ht (lambda (k v) (set! a (cons (cons k v) a))))
           a))
        (test-lset-equal? '((cat . calico) (elephant . pink))
                          (hash-table-fold ht (lambda (k v a) (cons (cons k v) a)) '()))

        ;; copy
        (let ((ht2 (hash-table-copy ht)))
          (test 2 (hash-table-size ht2))
          (test-not (hash-table-exists? ht2 'dog))
          (test-assert (hash-table-exists? ht2 'cat))
          (test-assert (hash-table-exists? ht2 'elephant))
          (test-error (hash-table-ref ht2 'dog))
          (test 'calico (hash-table-ref ht2 'cat))
          (test 'pink (hash-table-ref ht2 'elephant))
          (test-lset-eq? '(cat elephant) (hash-table-keys ht2))
          (test-lset-eq? '(calico pink) (hash-table-values ht2))
          (test-lset-equal? '((cat . calico) (elephant . pink))
                            (hash-table->alist ht2)))

        ;; merge
        (let ((ht2 (make-hash-table eq?)))
          (hash-table-set! ht2 'bear 'brown)
          (test 1 (hash-table-size ht2))
          (test-not (hash-table-exists? ht2 'dog))
          (test-assert (hash-table-exists? ht2 'bear))
          (hash-table-merge! ht2 ht)
          (test 3 (hash-table-size ht2))
          (test-assert (hash-table-exists? ht2 'bear))
          (test-assert (hash-table-exists? ht2 'cat))
          (test-assert (hash-table-exists? ht2 'elephant))
          (test-not (hash-table-exists? ht2 'goose))
          (test 'brown (hash-table-ref ht2 'bear))
          (test 'calico (hash-table-ref ht2 'cat))
          (test 'pink (hash-table-ref ht2 'elephant))
          (test-error (hash-table-ref ht2 'goose))
          (test 'grey (hash-table-ref/default ht2 'goose 'grey))
          (test-lset-eq? '(bear cat elephant) (hash-table-keys ht2))
          (test-lset-eq? '(brown calico pink) (hash-table-values ht2))
          (test-lset-equal? '((cat . calico) (bear . brown) (elephant . pink))
                            (hash-table->alist ht2)))

        ;; alist->hash-table
        (test-lset-equal? (hash-table->alist ht)
                          (hash-table->alist
                           (alist->hash-table
                            '((cat . calico) (elephant . pink))))))

      ;; update
      (let ((ht (make-hash-table eq?))
            (add1 (lambda (x) (+ x 1))))
        (hash-table-set! ht 'sheep 0)
        (hash-table-update! ht 'sheep add1)
        (hash-table-update! ht 'sheep add1)
        (test 2 (hash-table-ref ht 'sheep))
        (hash-table-update!/default ht 'crows add1 0)
        (hash-table-update!/default ht 'crows add1 0)
        (hash-table-update!/default ht 'crows add1 0)
        (test 3 (hash-table-ref ht 'crows)))

      ;; string keys
      (let ((ht (make-hash-table equal?)))
        (hash-table-set! ht "cat" 'black)
        (hash-table-set! ht "dog" 'white)
        (hash-table-set! ht "elephant" 'pink)
        (hash-table-ref/default ht "dog" #f)
        (test 'white (hash-table-ref ht "dog"))
        (test 'black (hash-table-ref ht "cat"))
        (test 'pink (hash-table-ref ht "elephant"))
        (test-error (hash-table-ref ht "goose"))
        (test 'grey (hash-table-ref/default ht "goose" 'grey))
        (test-lset-equal? '("cat" "dog" "elephant") (hash-table-keys ht))
        (test-lset-equal? '(black white pink) (hash-table-values ht))
        (test-lset-equal?
         '(("cat" . black) ("dog" . white) ("elephant" . pink))
         (hash-table->alist ht)))

      ;; string-ci keys
      (let ((ht (make-hash-table string-ci=? string-ci-hash)))
        (hash-table-set! ht "cat" 'black)
        (hash-table-set! ht "dog" 'white)
        (hash-table-set! ht "elephant" 'pink)
        (hash-table-ref/default ht "DOG" #f)
        (test 'white (hash-table-ref ht "DOG"))
        (test 'black (hash-table-ref ht "Cat"))
        (test 'pink (hash-table-ref ht "eLePhAnT"))
        (test-error (hash-table-ref ht "goose"))
        (test-lset-equal? '("cat" "dog" "elephant") (hash-table-keys ht))
        (test-lset-equal? '(black white pink) (hash-table-values ht))
        (test-lset-equal?
         '(("cat" . black) ("dog" . white) ("elephant" . pink))
         (hash-table->alist ht)))

      ;; Exception values - this works because the return value from the
      ;; primitives is a cell, and we use the cdr opcode to retrieve the
      ;; cell value.  Thus there is no FFI issue with storing exceptions.
      (let ((ht (make-hash-table)))
        (hash-table-set! ht 'boom (make-exception 'my-exn-type "boom!" '() #f #f))
        (test 'my-exn-type (exception-kind (hash-table-ref ht 'boom))))

      ;; stress test
      (test 625
          (let ((ht (make-hash-table)))
            (do ((i 0 (+ i 1))) ((= i 1000))
              (hash-table-set! ht i (* i i)))
            (hash-table-ref/default ht 25 #f)))

      (test-end))))