(in-package #:local-time.test)

(eval-when (:compile-toplevel :load-toplevel :execute)
  (local-time::define-timezone eastern-tz
      (merge-pathnames #p"US/Eastern" local-time::*default-timezone-repository-path*))
  (local-time::define-timezone amsterdam-tz
      (merge-pathnames #p"Europe/Amsterdam" local-time::*default-timezone-repository-path*)))

(deftest test/simple/make-timestamp ()
  (let ((timestamp (make-timestamp :nsec 1 :sec 2 :day 3)))
    (is (= (nsec-of timestamp) 1))
    (is (= (sec-of timestamp) 2))
    (is (= (day-of timestamp) 3))))

(deftest test/simple/read-binary-integer ()
  (let ((tmp-file-path (merge-pathnames (uiop:temporary-directory) "local-time-test")))
    (with-open-file (ouf tmp-file-path
                         :direction :output
                         :element-type 'unsigned-byte
                         :if-exists :supersede)
      (dotimes (i 14)
        (write-byte 200 ouf)))
  (with-open-file (inf tmp-file-path :element-type 'unsigned-byte)
    (is (eql (local-time::%read-binary-integer inf 1) 200))
    (is (eql (local-time::%read-binary-integer inf 1 t) -56))
    (is (eql (local-time::%read-binary-integer inf 2) 51400))
    (is (eql (local-time::%read-binary-integer inf 2 t) -14136))
    (is (eql (local-time::%read-binary-integer inf 4) 3368601800))
    (is (eql (local-time::%read-binary-integer inf 4 t) -926365496)))))

(deftest test/simple/encode-timestamp ()
  (macrolet ((entry ((&rest encode-timestamp-args)
                     day sec nsec)
               `(let ((timestamp (encode-timestamp ,@encode-timestamp-args)))
                  (is (= (day-of timestamp) ,day))
                  (is (= (sec-of timestamp) ,sec))
                  (is (= (nsec-of timestamp) ,nsec)))))
    (entry (0 0 0 0 1 3 2000 :offset 0)
           0 0 0)
    (entry (0 0 0 0 29 2 2000 :offset 0)
           -1 0 0)
    (entry (0 0 0 0 2 3 2000 :offset 0)
           1 0 0)
    (entry (0 0 0 0 1 1 2000 :offset 0)
           -60 0 0)
    (entry (0 0 0 0 1 3 2001 :offset 0)
           365 0 0)))

(defmacro encode-decode-test (args &body body)
  `(let ((timestamp (encode-timestamp ,@(subseq args 0 7) :offset 0)))
    (is (equal '(,@args ,@(let ((stars nil))
                               (dotimes (n (- 7 (length args)))
                                 (push '* stars))
                               stars))
               (multiple-value-list
                (decode-timestamp timestamp :timezone local-time:+utc-zone+))))
    ,@body))

(deftest test/simple/encode-decode-consistency/1 ()
  (encode-decode-test (5 5 5 5 5 5 1990 6 nil 0 "UTC"))
  (encode-decode-test (0 0 0 0 1 3 2001 4 nil 0 "UTC"))
  (encode-decode-test (0 0 0 0 1 3 1998 0 nil 0 "UTC"))
  (encode-decode-test (1 2 3 4 5 6 2008 4 nil 0 "UTC"))
  (encode-decode-test (0 0 0 0 1 1 1 1 nil 0 "UTC")))

(deftest test/simple/encode-decode-consistency/random ()
  (loop :repeat 1000 :do
    (let ((timestamp (make-timestamp :day (- (random 65535) 36767)
                                     :sec (random 86400)
                                     :nsec (random 1000000000))))
      (multiple-value-bind (ns ss mm hh day month year)
          (decode-timestamp timestamp :timezone local-time:+utc-zone+)
        (is (timestamp= timestamp
                        (encode-timestamp ns ss mm hh day month year :offset 0)))))))

;;;;;;
;;; TODO the rest is uncategorized, just simply converted from the old 5am suite

(deftest test/timestamp-conversions ()
  (is (eql 0 (timestamp-to-unix
              (encode-timestamp 0 0 0 0 1 1 1970 :offset 0))))
  (is (equal (values 2 3 4 5 6 2008 3 * *)
             (decode-universal-time
              (timestamp-to-universal
               (encode-timestamp 1 2 3 4 5 6 2008 :offset 0)) 0)))
  (let ((now (now)))
    (setf (nsec-of now) 0)
    (is (timestamp= now
                     (unix-to-timestamp (timestamp-to-unix now)))))
  (let ((now (get-universal-time)))
    (is (equal now
               (timestamp-to-universal (universal-to-timestamp now))))))

(deftest test/year-difference ()
  (let ((a (parse-timestring "2006-01-01T00:00:00"))
        (b (parse-timestring "2001-01-01T00:00:00")))
    (is (= 5 (timestamp-whole-year-difference a b))))

  (let ((a (parse-timestring "2006-01-01T00:00:00"))
        (b (parse-timestring "2001-01-02T00:00:00")))
    (is (= 4 (timestamp-whole-year-difference a b))))

  (let* ((local-time::*default-timezone* amsterdam-tz)
         (a (parse-timestring "1978-10-01")))
    (is (= 0 (timestamp-whole-year-difference a a)))))

(deftest test/adjust-timestamp/day-of-week/basic ()
  (let ((sunday (parse-timestring "2020-09-13T00:00:00"))
        (saturday (parse-timestring "2020-09-19T00:00:00"))
        (base (parse-timestring "2020-09-15T00:00:00")))
    (loop for dow in '(:sunday :monday :tuesday)
       do (let ((modified (adjust-timestamp base (timezone +utc-zone+) (offset :day-of-week dow))))
            (is (and (timestamp>= modified sunday) (timestamp<= modified base)))))
    (loop for dow in '(:wednesday :thursday :friday :saturday)
       do (let ((modified (adjust-timestamp base (timezone +utc-zone+) (offset :day-of-week dow))))
            (is (and (timestamp> modified base) (timestamp<= modified saturday)))))))

(deftest test/adjust-timestamp/bug1 ()
  (let* ((timestamp (parse-timestring "2006-01-01T00:00:00Z"))
         (modified-timestamp (adjust-timestamp timestamp (timezone +utc-zone+) (offset :year 1))))
    (is (timestamp= (parse-timestring "2007-01-01T00:00:00Z") modified-timestamp))))

(deftest test/adjust-timestamp/bug2 ()
  (let* ((timestamp (parse-timestring "2009-03-01T01:00:00.000000+00:00"))
         (modified-timestamp (adjust-timestamp timestamp (timezone +utc-zone+) (offset :month 1))))
    (is (timestamp= (parse-timestring "2009-04-01T01:00:00.000000+00:00") modified-timestamp))))

(deftest test/adjust-timestamp/bug3 ()
  (let* ((timestamp (parse-timestring "2009-03-01T01:00:00.000000+00:00"))
         (modified-timestamp (adjust-timestamp timestamp (timezone +utc-zone+) (offset :day-of-week :monday))))
    (is (timestamp= (parse-timestring "2009-02-23T01:00:00.000000+00:00") modified-timestamp)))
  (let* ((timestamp (parse-timestring "2009-03-04T01:00:00.000000+00:00"))
         (modified-timestamp (adjust-timestamp timestamp (timezone +utc-zone+) (offset :day-of-week :monday))))
    (is (timestamp= (parse-timestring "2009-03-02T01:00:00.000000+00:00") modified-timestamp))))

(deftest test/adjust-timestamp/bug4 ()
  (let* ((timestamp (parse-timestring "2013-04-30T00:00:00.000000+00:00"))
         (modified-timestamp (adjust-timestamp timestamp (timezone +utc-zone+) (offset :day-of-week :wednesday))))
    (is (timestamp= (parse-timestring "2013-05-01T00:00:00.000000+00:00") modified-timestamp)))
  (let* ((timestamp (parse-timestring "2013-12-31T00:00:00.000000+00:00"))
         (modified-timestamp (adjust-timestamp timestamp (timezone +utc-zone+) (offset :day-of-week :wednesday))))
    (is (timestamp= (parse-timestring "2014-01-01T00:00:00.000000+00:00") modified-timestamp))))

#+nil
(deftest test/adjust-days ()
  (let ((sunday (parse-timestring "2006-12-17T01:02:03Z")))
    (is (timestamp= (parse-timestring "2006-12-11T01:02:03Z")
                     (adjust-timestamp sunday (offset :day-of-week :monday))))
    (is (timestamp= (parse-timestring "2006-12-20T01:02:03Z")
                     (adjust-timestamp sunday (offset :day 3))))))

(deftest test/decode-date ()
  (loop
    :for (total-day year month day) :in '((-1 2000 02 29)
                                          (0 2000 03 01)
                                          (1 2000 03 02)
                                          (364 2001 02 28)
                                          (365 2001 03 01)
                                          (366 2001 03 02)
                                          (#.(* 2 365) 2002 03 01)
                                          (#.(* 4 365) 2004 02 29)
                                          (#.(1+ (* 4 365)) 2004 03 01))
    :do (multiple-value-bind (year* month* day*)
            (local-time::%timestamp-decode-date total-day)
          (is (= year year*))
          (is (= month month*))
          (is (= day day*)))))

(deftest test/timestamp-decoding-readers ()
  (let ((*default-timezone* +utc-zone+))
    (dolist (year '(1900 1975 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010))
      (dolist (month '(1 2 3 4 5 6 7 8 9 10 11 12))
        (dolist (day '(1 2 3 27 28 29 30 31))
          (when (valid-date-tuple? year month day)
            (let ((hour (random 24))
                  (min (random 60))
                  (sec (random 60))
                  (nsec (random 1000000000)))
              (let ((time (encode-timestamp nsec sec min hour day month year :offset 0)))
                (is (= (floor year 10) (timestamp-decade time)))
                (is (= year (timestamp-year time)))
                (is (= month (timestamp-month time)))
                (is (= day (timestamp-day time)))
                (is (= hour (timestamp-hour time)))
                (is (= min (timestamp-minute time)))
                (is (= sec (timestamp-second time)))
                (is (= (floor nsec 1000000)
                       (timestamp-millisecond time)))
                (is (= (floor nsec 1000)
                       (timestamp-microsecond time)))))))))))

(deftest test/timestamp-century ()
  (let ((*default-timezone* +utc-zone+))
    (dolist (year-data '((-101 -2)
                         (-100 -1)
                         (-1 -1)
                         (1 1)
                         (100 1)
                         (101 2)
                         (1999 20)
                         (2000 20)
                         (2001 21)))
      (let ((time (encode-timestamp 0 0 0 0 1 1 (first year-data) :offset 0)))
        (is (= (second year-data) (timestamp-century time)))))))

(deftest test/timestamp-millennium ()
  (let ((*default-timezone* +utc-zone+))
    (dolist (year-data '((-101 -1)
                         (-100 -1)
                         (-1 -1)
                         (1 1)
                         (100 1)
                         (101 1)
                         (1001 2)
                         (1999 2)
                         (2000 2)
                         (2001 3)))
      (let ((time (encode-timestamp 0 0 0 0 1 1 (first year-data) :offset 0)))
        (is (= (second year-data) (timestamp-millennium time)))))))

(defun valid-date-tuple? (year month day)
  ;; it works only on the gregorian calendar
  (let ((month-days #(31 28 31 30 31 30 31 31 30 31 30 31)))
    (and (<= 1 month 12)
         (<= 1 day (+ (aref month-days (1- month))
                      (if (and (= month 2)
                               (zerop (mod year 4))
                               (not (zerop (mod year 100)))
                               (zerop (mod year 400)))
                          1
                          0))))))

(deftest test/encode-decode-timestamp ()
  (let ((*default-timezone* +utc-zone+))
    (loop for year :in '(1900 1975 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010) do
          (loop for month :from 1 :to 12 do
                (loop for day :in '(1 2 3 27 28 29 30 31) do
                      (when (valid-date-tuple? year month day)
                        (multiple-value-bind (nsec sec minute hour day* month* year* day-of-week)
                            (decode-timestamp (encode-timestamp 0 0 0 0 day month year :offset 0))
                          (declare (ignore nsec sec minute day-of-week))
                          (is (= hour 0))
                          (is (= year year*))
                          (is (= month month*))
                          (is (= day day*)))))))))

(deftest test/timestamp-maximize-part ()
  (timestamp= (timestamp-maximize-part
               (encode-timestamp 0 49 26 13 9 12 2010 :offset -18000)
               :min)
              (encode-timestamp 999999999 59 59 13 9 12 2010 :offset -18000)))

(deftest test/timestamp-minimize-part ()
  (timestamp= (timestamp-minimize-part
               (encode-timestamp 0 49 26 13 9 12 2010 :offset -18000)
               :min)
              (encode-timestamp 0 0 0 13 9 12 2010 :offset -18000)))

(deftest test/decode-iso-week ()
  (dolist (*default-timezone* (list eastern-tz +utc-zone+ amsterdam-tz))
    (dolist (testcase '((2005 01 01 2004 53 6)
                        (2005 01 02 2004 53 7)
                        (2005 12 31 2005 52 6)
                        (2007 01 01 2007 1 1)
                        (2007 12 30 2007 52 7)
                        (2007 12 31 2008 1 1)
                        (2008 01 01 2008 1 2)
                        (2008 12 28 2008 52 7)
                        (2008 12 29 2009 1 1)
                        (2008 12 30 2009 1 2)
                        (2008 12 31 2009 1 3)
                        (2009 01 01 2009 1 4)
                        (2009 12 31 2009 53 4)
                        (2010 01 01 2009 53 5)
                        (2010 01 02 2009 53 6)
                        (2010 01 03 2009 53 7)
                        (2016 01 04 2016 1 1)))
      (destructuring-bind (year month day iso-year iso-week iso-dow)
          testcase
        (let ((ts (encode-timestamp 0 0 0 12 day month year)))
          (is (equal (list iso-year iso-week iso-dow)
                     (multiple-value-list (local-time::%timestamp-decode-iso-week ts)))))))))
