(ns slingshot.slingshot-test
  (:require [clojure.test :refer :all]
            [slingshot.slingshot :refer :all]
            [clojure.string :as str])
  (:import java.util.concurrent.ExecutionException))

(defrecord exception-record [error-code duration-ms message])
(defrecord x-failure [message])

(def a-sphere ^{:type ::sphere} {:radius 3})

(def h1 (derive (make-hierarchy) ::square ::shape))
(def a-square ^{:type ::square} {:size 4})

(def exception-1 (Exception. "exceptional"))
(def exception-record-1 (exception-record. 6 1000 "pdf failure"))

(defn mult-func [x y]
  (let [a 7 b 11]
    (if (= x 3)
      (* a b x y)
      (throw+ (x-failure. "x isn't 3... really??")))))

(defmacro mega-try [body]
  `(try+
    ~body

    ;; by class derived from Throwable
    (catch IllegalArgumentException e#
      [:class-iae e#])
    (catch Exception e#
      [:class-exception e#])

    ;; by java class generically
    (catch String e#
      [:class-string e#])

    ;; by clojure record type
    (catch exception-record e#
      [:class-exception-record e#])

    ;; by key-value
    (catch [:a-key 4] e#
      [:key-yields-value e#])

    ;; by multiple-key-value
    (catch [:key1 4 :key2 5] e#
      [:keys-yield-values e#])

    ;; by key present
    (catch (and (set? ~'%) (contains? ~'% :a-key)) e#
      [:key-is-present e#])

    ;; by clojure type, with optional hierarchy
    (catch (isa? (type ~'%) ::sphere) e#
      [:type-sphere (type e#) e#])
    (catch (isa? h1 (type ~'%) ::shape) e#
      [:type-shape-in-h1 (type e#) e#])

    ;; by predicate
    (catch nil? e#
      [:pred-nil e#])
    (catch keyword? e#
      [:pred-keyword e#])
    (catch symbol? e#
      [:pred-symbol e#])
    (catch map? e#
      [:pred-map e# (meta e#)])))

(deftest test-try+
  (testing "catch by class derived from Throwable"
    (testing "treat throwables exactly as throw does, interop with try/throw"
      (is (= [:class-exception exception-1]
             (mega-try (throw+ exception-1))
             (mega-try (throw exception-1))
             (try (throw+ exception-1)
                  (catch Exception e [:class-exception e]))
             (try (throw exception-1)
                  (catch Exception e [:class-exception e])))))
    (testing "IllegalArgumentException thrown by clojure/core"
      (is (= :class-iae (first (mega-try (str/replace "foo" 1 1)))))))

  (testing "catch by java class generically"
    (is (= [:class-string "fail"] (mega-try (throw+ "fail")))))

  (testing "catch by clojure record type"
    (is (= [:class-exception-record exception-record-1]
           (mega-try (throw+ exception-record-1)))))

  (testing "catch by key is present"
    (is (= [:key-is-present #{:a-key}] (mega-try (throw+ #{:a-key})))))

  (testing "catch by keys and values"
    (is (= [:key-yields-value {:a-key 4}] (mega-try (throw+ {:a-key 4}))))
    (is (= [:keys-yield-values {:key1 4 :key2 5}]
           (mega-try (throw+ {:key1 4 :key2 5})))))

  (testing "catch by clojure type with optional hierarchy"
    (is (= [:type-sphere ::sphere a-sphere] (mega-try (throw+ a-sphere))))
    (is (= [:type-shape-in-h1 ::square a-square] (mega-try (throw+ a-square)))))

  (testing "catch by predicate"
    (is (= [:pred-nil nil] (mega-try (throw+ nil))))
    (is (= [:pred-keyword :awesome] (mega-try (throw+ :awesome))))
    (is (= [:pred-symbol 'yuletide] (mega-try (throw+ 'yuletide))))
    (is (= [:pred-map {:error-code 4} nil] (mega-try (throw+ {:error-code 4}))))
    (testing "preservation of metadata"
      (is (= [:pred-map {:error-code 4} {:severity 4}]
             (mega-try (throw+ ^{:severity 4} {:error-code 4})))))))

(deftest test-clauses
  (let [bumps (atom 0)
        bump (fn [] (swap! bumps inc))]
    (is (nil? (try+)))
    (is (nil? (try+ (catch integer? i (inc i)))))
    (is (nil? (try+ (finally (bump)))))
    (is (nil? (try+ (catch integer? i (inc i)) (finally (bump)))))
    (is (nil? (try+ (catch integer? i (inc i)) (catch map? m m)
                    (finally (bump)))))

    (is (= 3 (try+ 3)))
    (is (= 3 (try+ 3 (catch integer? i 4))))
    (is (= 3 (try+ 3 (finally (bump)))))
    (is (= 3 (try+ 3 (catch integer? i 4) (finally (bump)))))
    (is (= 4 (try+ (throw+ 3) (catch integer? i (inc i)) (finally (bump)))))
    (is (= 4 (try+ (throw+ 3) (catch integer? i (inc i)) (catch map? m m)
                   (finally (bump)))))
    (is (= 4 (try+ (throw+ {:sel 4}) (catch integer? i (inc i))
                   (catch map? m (:sel m)) (finally (bump)))))

    (is (= 4 (try+ 3 4)))
    (is (= 4 (try+ 3 4 (catch integer? i 4))))
    (is (= 4 (try+ 3 4 (finally (bump)))))
    (is (= 4 (try+ 3 4 (catch integer? i 4) (finally (bump)))))
    (is (= 5 (try+ (throw+ 4) 4 (catch integer? i (inc i)) (finally (bump)))))
    (is (= 11 @bumps))))

(defn ax [] (throw+ 1))
(defn bx [] (try+ (ax) (catch integer? p (throw+ 2))))
(defn cx [] (try+ (bx) (catch integer? q (throw+ 3))))
(defn dx [] (try+ (cx) (catch integer? r (throw+ 4))))
(defn ex [] (try+ (dx) (catch integer? s (throw+ 5))))
(defn fx [] (try+ (ex) (catch integer? t (throw+ 6))))
(defn gx [] (try+ (fx) (catch integer? u (throw+ 7))))
(defn hx [] (try+ (gx) (catch integer? v (throw+ 8))))
(defn ix [] (try+ (hx) (catch integer? w &throw-context)))

(defn next-context [x]
  (-> x :cause get-throw-context))

(deftest test-throw-context
  (let [context (ix)
        context1 (next-context context)
        context2 (next-context context1)]

    (is (= #{:object :message :cause :stack-trace :wrapper :throwable}
           (set (keys context))
           (set (keys context1))
           (set (keys context2))))
    (is (= 8 (-> context :object)))
    (is (= 7 (-> context1 :object)))
    (is (= 6 (-> context2 :object)))))

(defn e []
  (try+
   (throw (Exception. "uncaught"))
   (catch integer? i i)))

(defn f []
  (try+
   (throw+ 3.2)
   (catch integer? i i)))


(defn g []
  (try+
   (throw+ 3.2 "wasn't caught")
   (catch integer? i i)))

(deftest test-uncaught
  (is (thrown-with-msg? Exception #"^uncaught$" (e)))
  (is (thrown-with-msg? Exception #"^throw\+: .*" (f)))
  (is (thrown-with-msg? Exception #"wasn't caught" (g))))

(defn h []
  (try+
   (try+
    (throw+ 0)
    (catch zero? e
      (throw+)))
   (catch zero? e
     :zero)))

(deftest test-rethrow
  (is (= :zero (h))))

(defn i []
  (try
    (try+
     (doall (map (fn [x] (throw+ (str x))) [1]))
     (catch string? x
       x))
    (catch Throwable x)))

(defn j []
  (try+
   (let [fut (future (throw+ "whoops"))]
     @fut)
   (catch string? e
     e)))

(deftest test-issue-5
  (is (= "1" (i)))
  (is (= "whoops" (j))))

(deftest test-unmacroed-pct
  (is (= :was-eee (try+ (throw+ "eee")
                        (catch (= % "eee") _ :was-eee)
                        (catch string? _ :no!)))))

(deftest test-x-ray-vision
  (let [[val wrapper] (try+
                       (try
                         (try
                           (try
                             (throw+ "x-ray!")
                             (catch Throwable x
                               (throw (RuntimeException. x))))
                           (catch Throwable x
                             (throw (ExecutionException. x))))
                         (catch Throwable x
                           (throw (RuntimeException. x))))
                       (catch string? x
                         [x (:throwable &throw-context)]))]
    (is (= "x-ray!" val))
    (is (= "x-ray!" (get-thrown-object wrapper)))))

(deftest test-catching-wrapper
  (let [e (Exception.)]
    (try
      (try+
       (throw e)
       (catch Exception _
         (throw+ :a "msg: %s" %)))
      (is false)
      (catch Exception s
        (is (= "msg: :a" (.getMessage s)))
        (is (= e (.getCause s)))))))

(deftest test-eval-object-once
  (let [bumps (atom 0)
        bump (fn [] (swap! bumps inc))]
    (try+
     (throw+ (bump) "this is it: %s %s %s" % % %)
     (catch Object _))
    (is (= @bumps 1))))

(deftest test-get-throw-context
  (let [object (Object.)
        exception1 (Exception.)
        exception2 (Exception. "ex1" exception1)
        t1 (try
             (throw+ object)
             (catch Throwable t t))
        t2 (try
             (throw+ exception2)
             (catch Throwable t t))
        t3 (try
             (throw exception2)
             (catch Throwable t t))]
    (is (= #{:object :message :cause :stack-trace :wrapper
             :throwable}
           (-> t1 get-throw-context keys set)))
    (is (= #{:object :message :cause :stack-trace :throwable}
           (-> t2 get-throw-context keys set)))
    (is (= #{:object :message :cause :stack-trace :throwable}
           (-> t3 get-throw-context keys set)))

    (is (identical? object (:object (get-throw-context t1))))
    (is (identical? exception2 (:object (get-throw-context t2))))
    (is (identical? exception2 (:object (get-throw-context t3))))

    (is (identical? exception1 (:cause (get-throw-context t2))))
    (is (identical? exception1 (:cause (get-throw-context t3))))
    (is (= "ex1" (:message (get-throw-context t2))))
    (is (= "ex1" (:message (get-throw-context t3))))))

(deftest test-get-thrown-object
  (let [object (Object.)
        exception (Exception.)
        t1 (try
             (throw+ object)
             (catch Throwable t t))
        t2 (try
             (throw+ exception)
             (catch Throwable t t))
        t3 (try
             (throw exception)
             (catch Throwable t t))]
    (is (identical? object (get-thrown-object t1)))
    (is (identical? exception (get-thrown-object t2)))
    (is (identical? exception (get-thrown-object t3)))))

(deftest test-wrapper-and-throwable
  (let [context (try+
                 (try
                   (throw+ :afp "wrapper-0")
                   (catch Exception e
                     (throw (RuntimeException. "wrapper-1" e))))
                 (catch Object _
                   &throw-context))]
    (is (= "wrapper-0" (.getMessage ^Throwable (:wrapper context))))
    (is (= "wrapper-1" (.getMessage ^Throwable (:throwable context))))))

(deftest test-inline-predicate
  (is (= :not-caught (try+
                      (throw+ {:foo true})
                      (catch #(-> % :foo (= false)) data
                        :caught)
                      (catch Object _
                        :not-caught)))))

(defn gen-body
  [rec-sym throw?]
  (let [body `(swap! ~rec-sym #(conj % :body))]
    (if throw?
      (list 'do body `(throw+ (Exception.)))
      body)))

(defn gen-catch-clause
  [rec-sym]
  `(catch Exception e# (swap! ~rec-sym #(conj % :catch))))

(defn gen-else-clause
  [rec-sym broken?]
  (let [else-body `(swap! ~rec-sym #(conj % :else))]
    (if broken?
      (list 'else (list 'do else-body `(throw+ (Exception.))))
      (list 'else else-body))))

(defn gen-finally-clause
  [rec-sym]
  `(finally (swap! ~rec-sym #(conj % :finally))))

(defn gen-try-else-form
  "Generate variations of (try ... (else ...) ...) forms, which (when eval'd)
  will return a vector describing the sequence in which things were evaluated,
  e.g. [:body :catch :finally]"
  [throw? catch? finally? broken-else?]
  (let [rec-sym (gensym "rec")
        body (gen-body rec-sym throw?)
        catch-clause (if catch? (gen-catch-clause rec-sym))
        else-clause (gen-else-clause rec-sym broken-else?)
        finally-clause (if finally? (gen-finally-clause rec-sym))]
    `(let [~rec-sym (atom [])]
       (try+
        ~(remove nil? `(try+
                        ~body
                        ~catch-clause
                        ~else-clause
                        ~finally-clause))
        (catch Object e#
          ;; if the inner try+ threw, report it as a :bang! in the return vec
          (swap! ~rec-sym #(conj % :bang!))))
       @~rec-sym)))

(deftest test-else
  (doseq [throw? [true false]
          catch? [true false]
          broken-else? [true false]
          finally? [true false]]
    (testing (str "test-else: throw? " throw? " catch? " catch?
                  " broken-else? " broken-else? " finally? " finally?)
      (let [try-else-form (gen-try-else-form throw? catch? finally? broken-else?)
            actual (eval try-else-form)
            expected (vec (remove nil?
                                  [:body
                                   (if (and throw? catch?) :catch)
                                   (if (not throw?) :else)
                                   (if finally? :finally)
                                   ;; expect an escaped exception when either:
                                   ;;  a) the else clause runs, and throws
                                   ;;  b) the body throws, and is not caught
                                   (if (or (and (not throw?) broken-else?)
                                           (and throw? (not catch?))) :bang!)]))]
        (is (= actual expected))))))

(deftest test-reflection
  (try+
   nil
   (catch Exception e
     (.getMessage e))))

(deftest test-ex-info-compatibility
  (let [data {:type :fail :reason :not-found}
        message "oops"
        wrapper (ex-info message data)
        rte1 (RuntimeException. "one" wrapper)
        rte2 (RuntimeException. "two" rte1)
        direct (try+
                (throw wrapper)
                (catch [:type :fail] e
                  &throw-context)
                (catch Object _
                  :whoops))
        cause-chain (try+
                     (throw rte2)
                     (catch [:type :fail] e
                       &throw-context)
                     (catch Object _
                       :whoops))]
    (is (= (:object direct) data))
    (is (= (:object cause-chain) data))
    (is (= (:message direct) message))
    (is (= (:message cause-chain) message))
    (is (= (:wrapper direct) wrapper))
    (is (= (:wrapper cause-chain) wrapper))
    (is (= (:throwable direct) wrapper))
    (is (= (:throwable cause-chain) rte2))))

;; helpers for test-optional-cause

(defmacro caught-result [& body]
  `(try+
    ~@body
    (catch Object ~'o
      [(:cause ~'&throw-context)
       (:message ~'&throw-context)])))

(defmacro caught-result-from-catch [cause & body]
  `(caught-result
    (try+
     (throw+ ~cause)
     (catch Object ~'o
       ~@body))))

(deftest test-optional-cause
  (let [imp (Exception. "I did it implicitly.")
        exp (Exception. "I did it explicitly.")
        def-msg "throw+: 1"
        msg "message two %s"
        fmt "aha! %s"
        fmt-msg "aha! 1"
        fmt2 "%s leading to %s"
        fmt2-msg "1 leading to [1 1]"

        ;; throw from outside catch, no implicit cause

        result1 (caught-result (throw+ 1))
        result2 (caught-result (throw+ 1 msg))
        result3 (caught-result (throw+ 1 fmt %))
        result4 (caught-result (throw+ 1 fmt2 % [% %]))

        result5 (caught-result (throw+ 1 nil))
        result6 (caught-result (throw+ 1 nil msg))
        result7 (caught-result (throw+ 1 nil fmt %))
        result8 (caught-result (throw+ 1 nil fmt2 % [% %]))

        result9 (caught-result (throw+ 1 exp))
        result10 (caught-result (throw+ 1 exp msg))
        result11 (caught-result (throw+ 1 exp fmt %))
        result12 (caught-result (throw+ 1 exp fmt2 % [% %]))

        ;; throw from inside catch, implicit cause available

        result13 (caught-result-from-catch imp (throw+))

        result14 (caught-result-from-catch imp (throw+ 1))
        result15 (caught-result-from-catch imp (throw+ 1 msg))
        result16 (caught-result-from-catch imp (throw+ 1 fmt %))
        result17 (caught-result-from-catch imp (throw+ 1 fmt2 % [% %]))

        result18 (caught-result-from-catch imp (throw+ 1 nil))
        result19 (caught-result-from-catch imp (throw+ 1 nil msg))
        result20 (caught-result-from-catch imp (throw+ 1 nil fmt %))
        result21 (caught-result-from-catch imp (throw+ 1 nil fmt2 % [% %]))

        result22 (caught-result-from-catch imp (throw+ 1 exp))
        result23 (caught-result-from-catch imp (throw+ 1 exp msg))
        result24 (caught-result-from-catch imp (throw+ 1 exp fmt %))
        result25 (caught-result-from-catch imp (throw+ 1 exp fmt2 % [% %]))]

    (testing "outside catch"
      (testing "implicit cause"
        (is (= result1 [nil def-msg]))
        (is (= result2 [nil msg]))
        (is (= result3 [nil fmt-msg]))
        (is (= result4 [nil fmt2-msg])))
      (testing "erased cause"
        (is (= result5 [nil def-msg]))
        (is (= result6 [nil msg]))
        (is (= result7 [nil fmt-msg]))
        (is (= result8 [nil fmt2-msg])))
      (testing "explicit cause"
        (is (= result9 [exp def-msg]))
        (is (= result10 [exp msg]))
        (is (= result11 [exp fmt-msg]))
        (is (= result12 [exp fmt2-msg]))))
    (testing "inside catch"
      (testing "rethrow"
        (is (= result13 [nil "I did it implicitly."])))
      (testing "implicit cause"
        (is (= result14 [imp def-msg]))
        (is (= result15 [imp msg]))
        (is (= result16 [imp fmt-msg]))
        (is (= result17 [imp fmt2-msg])))
      (testing "erased cause"
        (is (= result18 [nil def-msg]))
        (is (= result19 [nil msg]))
        (is (= result20 [nil fmt-msg]))
        (is (= result21 [nil fmt2-msg])))
      (testing "explicit cause"
        (is (= result22 [exp def-msg]))
        (is (= result23 [exp msg]))
        (is (= result24 [exp fmt-msg]))
        (is (= result25 [exp fmt2-msg]))))))
