(ns auto-ap.integration.graphql.transactions
  (:require
   [auto-ap.datomic :refer [conn]]
   [auto-ap.graphql.transactions :as sut]
   [auto-ap.integration.util
    :refer [admin-token
            setup-test-data
            test-bank-account
            test-client
            test-payment
            test-transaction
            test-transaction-rule
            test-invoice
            user-token
            wrap-setup]]
   [clojure.test :as t :refer [deftest is testing use-fixtures]]
   [datomic.api :as dc]))

(use-fixtures :each wrap-setup)

(deftest get-transaction-page
  (testing "Should list transactions"
    (let [{:strs [transaction-id
                  test-client-id]} (setup-test-data [(test-transaction :db/id "transaction-id"
                                                                        :transaction/client "test-client-id"
                                                                        :transaction/bank-account "test-bank-account-id")])]
      (is (= 1 (:total (sut/get-transaction-page {:id (admin-token)} {} nil))))
      (is (= transaction-id (:id (first (:data (sut/get-transaction-page {:id (admin-token)} {} nil))))))
      (testing "Should only show transactions you have access to"
        (is (= 1 (:total (sut/get-transaction-page {:id (admin-token)} {} nil))))
        (is (= 1 (:total (sut/get-transaction-page {:id (user-token test-client-id)} {} nil))))
        (is (= 0 (:total (sut/get-transaction-page {:id (user-token 1)} {} nil))))

        (is (= 1 (:total (sut/get-transaction-page {:id (admin-token)} {:filters {:client_id test-client-id}} nil))))
        (is (= 0 (:total (sut/get-transaction-page {:id (admin-token)} {:filters {:client_id 1}} nil))))
        (is (= 1 (:total (sut/get-transaction-page {:id (user-token test-client-id)} {:filters {:client_id test-client-id}} nil))))
        (is (= 0 (:total (sut/get-transaction-page {:id (user-token 1)} {:filters {:client_id test-client-id}} nil)))))

      (testing "Should only show potential duplicates if filtered enough"
        (is (thrown? Exception (:total (sut/get-transaction-page {:id (admin-token)} {:filters {:potential_duplicates true}} nil))))))))


(deftest bulk-change-status
  (testing "Should change status of multiple transactions"
    (let [{:strs [transaction-id
                  test-client-id]} (setup-test-data [(test-transaction :db/id "transaction-id"
                                                                        :transaction/client "test-client-id"
                                                                        :transaction/approval-status :transaction-approval-status/approved
                                                                        :transaction/bank-account "test-bank-account-id")])]
      (is (= "Succesfully changed 1 transactions to be unapproved."
             (:message (sut/bulk-change-status {:id (admin-token)
                                                :clients [{:db/id test-client-id}]} {:filters {}
                                                                    :status  :unapproved} nil))))
      (is (= :transaction-approval-status/unapproved
             (:db/ident (:transaction/approval-status (dc/pull (dc/db conn) '[{:transaction/approval-status [:db/ident]}] transaction-id)))))

      (testing "Only admins should be able to change the status"
        (is (thrown? Exception (sut/bulk-change-status {:id (user-token test-client-id)}
                                                       {:filters {:client_id test-client-id}
                                                        :status  :unapproved} nil)))))))

(deftest bulk-code-transactions
  (testing "Should code transactions"
    (let [{:strs [transaction-id
                  test-client-id
                  test-account-id
                  test-vendor-id]} (setup-test-data [(test-transaction :db/id "transaction-id"
                                                                  :transaction/client "test-client-id"
                                                                  :transaction/bank-account "test-bank-account-id"
                                                                  :transaction/amount 40.0)])]
      (is (= "Successfully coded 1 transactions."
             (:message (sut/bulk-code-transactions {:id (admin-token)
                                                    :clients [{:db/id test-client-id}]}
                                                   {:filters         {:client_id test-client-id}
                                                    :vendor          test-vendor-id
                                                    :approval_status :unapproved
                                                    :accounts        [{:account_id test-account-id
                                                                       :location   "DT"
                                                                       :percentage 1.0}]} nil))))

      (is (= #:transaction{:vendor          {:db/id test-vendor-id}
                           :approval-status {:db/ident :transaction-approval-status/unapproved}
                           :accounts        [#:transaction-account{:account  {:db/id test-account-id}
                                                                   :location "DT"
                                                                   :amount   40.0}]}
             (dc/pull (dc/db conn) '[:transaction/vendor
                                     {:transaction/approval-status [:db/ident]
                                      :transaction/accounts        [:transaction-account/account
                                                                    :transaction-account/location
                                                                    :transaction-account/amount]}]
                      transaction-id)))

      (testing "for more than one client"
        (let [{:strs [transaction-id-1
                      transaction-id-2
                      test-client-id-2
                      test-client-id]} (setup-test-data [
                                                           (test-transaction :db/id "transaction-id-1"
                                                                             :transaction/client "test-client-id"
                                                                             :transaction/bank-account "test-bank-account-id"
                                                                             :transaction/amount 40.0)
                                                           (test-transaction :db/id "transaction-id-2"
                                                                            :transaction/client "test-client-id-2"
                                                                            :transaction/bank-account "test-bank-account-id-2"
                                                                            :transaction/amount 40.0)
                                                           (test-client :db/id "test-client-id-2"
                                                                        :client/locations ["GR"])
                                                           (test-bank-account :db/id "test-bank-account-id-2")])]
          (is (= "Successfully coded 2 transactions."
                 (:message (sut/bulk-code-transactions {:id      (admin-token)
                                                        :clients [{:db/id test-client-id}
                                                                  {:db/id test-client-id-2}]}
                                                       {:filters         {}
                                                        :vendor          test-vendor-id
                                                        :approval_status :unapproved
                                                        :accounts        [{:account_id test-account-id
                                                                           :location   "Shared"
                                                                           :percentage 1.0}]} nil))))
          
          (is (= #:transaction{:vendor          {:db/id test-vendor-id}
                               :approval-status {:db/ident :transaction-approval-status/unapproved}
                               :accounts        [#:transaction-account{:account  {:db/id test-account-id}
                                                                       :location "DT"
                                                                       :amount   40.0}]}
                 (dc/pull (dc/db conn) '[:transaction/vendor
                                         {:transaction/approval-status [:db/ident]
                                          :transaction/accounts        [:transaction-account/account
                                                                        :transaction-account/location
                                                                        :transaction-account/amount]}]
                          transaction-id-1)))

          (is (= #:transaction{:vendor          {:db/id test-vendor-id}
                               :approval-status {:db/ident :transaction-approval-status/unapproved}
                               :accounts        [#:transaction-account{:account  {:db/id test-account-id}
                                                                       :location "GR"
                                                                       :amount   40.0}]}
                 (dc/pull (dc/db conn) '[:transaction/vendor
                                         {:transaction/approval-status [:db/ident]
                                          :transaction/accounts        [:transaction-account/account
                                                                        :transaction-account/location
                                                                        :transaction-account/amount]}]
                          transaction-id-2))))

        (testing "should reject a location that doesnt exist"
          (let [{:strs [test-client-id-1
                        test-client-id-2]} (setup-test-data [
                                                             (test-transaction :db/id "transaction-id-1"
                                                                               :transaction/client "test-client-id-1"
                                                                               :transaction/bank-account "test-bank-account-id"
                                                                               :transaction/amount 40.0)
                                                             (test-transaction :db/id "transaction-id-2"
                                                                               :transaction/client "test-client-id-2"
                                                                               :transaction/bank-account "test-bank-account-id-2"
                                                                               :transaction/amount 40.0)
                                                             (test-client :db/id "test-client-id-1"
                                                                          :client/locations ["DT" "BOTH"])
                                                             (test-client :db/id "test-client-id-2"
                                                                          :client/locations ["GR" "BOTH"])
                                                             (test-bank-account :db/id "test-bank-account-id-2")])]
            (is (thrown? Exception (sut/bulk-code-transactions {:id      (admin-token)
                                                      :clients [{:db/id test-client-id}
                                                                {:db/id test-client-id-2}]}
                                                     {:filters         {}
                                                      :vendor          test-vendor-id
                                                      :approval_status :unapproved
                                                      :accounts        [{:account_id test-account-id
                                                                         :location   "OG"
                                                                         :percentage 1.0}]} nil)))
            (is (thrown? Exception (sut/bulk-code-transactions {:id      (admin-token)
                                                                :clients [{:db/id test-client-id}
                                                                          {:db/id test-client-id-2}]}
                                                               {:filters         {}
                                                                :vendor          test-vendor-id
                                                                :approval_status :unapproved
                                                                :accounts        [{:account_id test-account-id
                                                                                   :location   "DT"
                                                                                   :percentage 1.0}]} nil)))
            (is (sut/bulk-code-transactions {:id      (admin-token)
                                             :clients [{:db/id test-client-id-1}
                                                       {:db/id test-client-id-2}]}
                                            {:filters         {}
                                             :vendor          test-vendor-id
                                             :approval_status :unapproved
                                             :accounts        [{:account_id test-account-id
                                                                :location   "BOTH"
                                                                :percentage 1.0}]} nil))))))))

(deftest edit-transactions
  (testing "Should edit transactions"
    (let [{:strs [transaction-id
                  test-account-id
                  test-vendor-id]} (setup-test-data [(test-transaction :db/id "transaction-id"
                                                                       :transaction/client "test-client-id"
                                                                       :transaction/bank-account "test-bank-account-id"
                                                                       :transaction/amount 40.0)])]
      (sut/edit-transaction {:id (admin-token)}
                            {:transaction {:id              transaction-id
                                           :vendor_id          test-vendor-id
                                           :approval_status :approved
                                           :accounts        [{:account_id test-account-id
                                                              :location   "DT"
                                                              :amount     40.0}]}} nil)

      (is (= #:transaction{:vendor          {:db/id test-vendor-id}
                           :approval-status {:db/ident :transaction-approval-status/approved}
                           :accounts        [#:transaction-account{:account  {:db/id test-account-id}
                                                                   :location "DT"
                                                                   :amount   40.0}]}
             (dc/pull (dc/db conn) '[:transaction/vendor
                                     {:transaction/approval-status [:db/ident]
                                      :transaction/accounts        [:transaction-account/account
                                                                    :transaction-account/location
                                                                    :transaction-account/amount]}]
                      transaction-id)))

      (testing "Should prevent saves with bad accounts"
        (is (thrown? Exception
                     (sut/edit-transaction {:id (admin-token)}
                                           {:transaction {:id              transaction-id
                                                          :vendor_id       test-vendor-id
                                                          :approval_status :approved
                                                          :accounts        [{:account_id test-account-id
                                                                             :location   "DT"
                                                                             :amount     20.0}]}} nil)))))))


(deftest match-transaction
  (testing "Should link a transaction to a payment, mark it as accounts payable"
    (let [{:strs [transaction-id
                  test-vendor-id
                  accounts-payable-id
                  payment-id]} (setup-test-data [(test-transaction :db/id "transaction-id"
                                                                   :transaction/client "test-client-id"
                                                                   :transaction/bank-account "test-bank-account-id"
                                                                   :transaction/amount -50.0)
                                                 (test-payment :db/id "payment-id"
                                                               :payment/client "test-client-id"
                                                               :payment/vendor "test-vendor-id"
                                                               :payment/bank-account "test-bank-account-id"
                                                               :payment/amount 50.0)])]
      (sut/match-transaction {:id (admin-token)} {:transaction_id transaction-id :payment_id payment-id} nil)
      (is (= #:transaction{:vendor          {:db/id test-vendor-id}
                           :approval-status {:db/ident :transaction-approval-status/approved}
                           :payment {:db/id payment-id}
                           :accounts        [#:transaction-account{:account  {:account/name "Accounts Payable"}
                                                                   :location "A"
                                                                   :amount   50.0}]}
             (dc/pull (dc/db conn) '[:transaction/vendor
                                     :transaction/payment
                                     {:transaction/approval-status [:db/ident]
                                      :transaction/accounts        [{:transaction-account/account [:account/name]}
                                                                    :transaction-account/location
                                                                    :transaction-account/amount]}]
                      transaction-id)))))

  (testing "Should prevent linking a payment if they don't match"
    (let [{:strs [transaction-id
                  mismatched-amount-payment-id
                  mismatched-bank-account-payment-id]}
          (setup-test-data [(test-transaction :db/id "transaction-id"
                                              :transaction/client "test-client-id"
                                              :transaction/bank-account "test-bank-account-id"
                                              :transaction/amount -50.0)
                            (test-payment :db/id "mismatched-amount-payment-id"
                                          :payment/client "test-client-id"
                                          :payment/vendor "test-vendor-id"
                                          :payment/bank-account "test-bank-account-id"
                                          :payment/amount 30.0)

                            (test-client :db/id "mismatched-client-id"
                                         :client/bank-accounts [(test-bank-account :db/id "mismatched-bank-account-id")])
                            (test-payment :db/id "mismatched-bank-account-payment-id"
                                          :payment/client "mismatched-client-id"
                                          :payment/vendor "test-vendor-id"
                                          :payment/bank-account "mismatched-bank-account-id"
                                          :payment/amount 50.0)])]
      (is (thrown? Exception (sut/match-transaction {:id (admin-token)} {:transaction_id transaction-id :payment_id mismatched-amount-payment-id} nil)))
      (is (thrown? Exception (sut/match-transaction {:id (admin-token)} {:transaction_id transaction-id :payment_id mismatched-bank-account-payment-id} nil)))
      )))

(deftest match-transaction-autopay-invoices
  (testing "Should link transaction to a set of autopaid invoices"
    (let [{:strs [transaction-id
                  test-vendor-id
                  invoice-1
                  invoice-2
                  ]} (setup-test-data [(test-transaction :db/id "transaction-id"
                                                                   :transaction/amount -50.0)
                                                 (test-invoice :db/id "invoice-1"
                                                               :invoice/total 30.0)
                                                 (test-invoice :db/id "invoice-2"
                                                               :invoice/total 20.0)])]
      (sut/match-transaction-autopay-invoices {:id (admin-token)} {:transaction_id transaction-id :autopay_invoice_ids [invoice-1 invoice-2]} nil)
      (let [result (dc/pull (dc/db conn) '[:transaction/vendor
                                           {:transaction/payment [:db/id {:payment/status [:db/ident]}]}
                                           {:transaction/approval-status [:db/ident]
                                            :transaction/accounts        [:transaction-account/account
                                                                          :transaction-account/location
                                                                          :transaction-account/amount]}
                                           ]
                            transaction-id)]
        (testing "should have created a payment"
          (is (some? (:transaction/payment result)))
          (is (= :payment-status/cleared (-> result
                                            :transaction/payment
                                            :payment/status
                                            :db/ident)))
          (is (= :transaction-approval-status/approved (-> result :transaction/approval-status :db/ident))))
        (testing "Should have completed the invoice"
          (is (= :invoice-status/paid (->> invoice-1
                                           (dc/pull (dc/db conn) '[{:invoice/status [:db/ident]}])
                                           :invoice/status
                                           :db/ident)))
          (is (= :invoice-status/paid (->> invoice-2
                                           (dc/pull (dc/db conn) '[{:invoice/status [:db/ident]}])
                                           :invoice/status
                                           :db/ident)))))))

  (testing "Should prevent a transaction from linking to an incorrectly balanced invoice"
    (let [{:strs [transaction-id
                  test-vendor-id
                  invoice-1
                  invoice-2
                  ]} (setup-test-data [(test-transaction :db/id "transaction-id"
                                                                   :transaction/amount -50.0)
                                                 (test-invoice :db/id "invoice-1"
                                                               :invoice/total 30.0)])]
      (is (thrown? Exception (sut/match-transaction-autopay-invoices {:id (admin-token)} {:transaction_id transaction-id :autopay_invoice_ids [invoice-1 invoice-2]} nil))))))

(deftest match-transaction-unpaid-invoices
  (testing "TODO exact same as above, no need for two endpaints"
    (let [{:strs [transaction-id
                  test-vendor-id
                  invoice-1
                  invoice-2
                  ]} (setup-test-data [(test-transaction :db/id "transaction-id"
                                                                   :transaction/amount -50.0)
                                                 (test-invoice :db/id "invoice-1"
                                                               :invoice/outstanding-balance 30.0 ;; TODO this part is a little different
                                                               :invoice/total 30.0)
                                                 (test-invoice :db/id "invoice-2"
                                                               :invoice/outstanding-balance 20.0 ;; TODO this part is a little different
                                                               :invoice/total 20.0)])]
      (sut/match-transaction-unpaid-invoices {:id (admin-token)} {:transaction_id transaction-id :unpaid_invoice_ids [invoice-1 invoice-2]} nil)
      (let [result (dc/pull (dc/db conn) '[:transaction/vendor
                                           {:transaction/payment [:db/id {:payment/status [:db/ident]}]}
                                           {:transaction/approval-status [:db/ident]
                                            :transaction/accounts        [:transaction-account/account
                                                                          :transaction-account/location
                                                                          :transaction-account/amount]}
                                           ]
                            transaction-id)]
        (testing "should have created a payment"
          (is (some? (:transaction/payment result)))
          (is (= :payment-status/cleared (-> result
                                            :transaction/payment
                                            :payment/status
                                            :db/ident)))
          (is (= :transaction-approval-status/approved (-> result :transaction/approval-status :db/ident))))
        (testing "Should have completed the invoice"
          (is (= :invoice-status/paid (->> invoice-1
                                           (dc/pull (dc/db conn) '[{:invoice/status [:db/ident]}])
                                           :invoice/status
                                           :db/ident)))
          (is (= :invoice-status/paid (->> invoice-2
                                           (dc/pull (dc/db conn) '[{:invoice/status [:db/ident]}])
                                           :invoice/status
                                           :db/ident)))))))

  (testing "Should prevent a transaction from linking to an incorrectly balanced invoice"
    (let [{:strs [transaction-id
                  test-vendor-id
                  invoice-1
                  invoice-2
                  ]} (setup-test-data [(test-transaction :db/id "transaction-id"
                                                                   :transaction/amount -50.0)
                                                 (test-invoice :db/id "invoice-1"
                                                               :invoice/total 30.0)])]
      (is (thrown? Exception (sut/match-transaction-autopay-invoices {:id (admin-token)} {:transaction_id transaction-id :autopay_invoice_ids [invoice-1 invoice-2]} nil))))))


(deftest match-transaction-rules
  (testing "Should match transactions without linked payments"
    (let [{:strs [transaction-id
                  transaction-rule-id
                  ]} (setup-test-data [(test-transaction :db/id "transaction-id"
                                                         :transaction/amount -50.0)
                                       (test-transaction-rule :db/id "transaction-rule-id"
                                                              :transaction-rule/client "test-client-id"
                                                              :transaction-rule/transaction-approval-status :transaction-approval-status/excluded
                                                              :transaction-rule/description ".*"
                                                     )])]
      (is (= transaction-rule-id (-> (sut/match-transaction-rules {:id (admin-token)} {:transaction_ids [transaction-id] :transaction_rule_id transaction-rule-id} nil)
                    first
                    :matched_rule
                    :id)))

      (testing "Should apply statuses"
        (is (= :excluded
               (-> (sut/match-transaction-rules {:id (admin-token)}
                                                {:transaction_ids [transaction-id] :transaction_rule_id transaction-rule-id}
                                                nil)
                   first
                   :approval_status
                   ))))))

  (testing "Should not apply to transactions if they don't match"
    (let [{:strs [transaction-id
                  transaction-rule-id]}
          (setup-test-data [(test-transaction :db/id "transaction-id"
                                              :transaction/amount -50.0)
                            (test-transaction-rule :db/id "transaction-rule-id"
                                                   :transaction-rule/description "NOMATCH"
                                                   )])]
      (is (thrown? Exception (-> (sut/match-transaction-rules {:id (admin-token)} {:transaction_ids [transaction-id] :transaction_rule_id transaction-rule-id} nil)
                                     first
                                     :matched_rule
                                     :id)))))
  (testing "Should not apply to transactions if they are already matched"
    (let [{:strs [transaction-id
                  transaction-rule-id]}
          (setup-test-data [(test-payment :db/id "extant-payment-id")
                            (test-transaction :db/id "transaction-id"
                                              :transaction/payment {:db/id "extant-payment-id"}
                                              :transaction/amount -50.0)
                            (test-transaction-rule :db/id "transaction-rule-id"
                                                   :transaction-rule/description ".*"
                                                   )])]
      (is (thrown? Exception (-> (sut/match-transaction-rules {:id (admin-token)} {:transaction_ids [transaction-id] :transaction_rule_id transaction-rule-id} nil)
                                 first
                                 :matched_rule
                                 :id)))))

  (testing "Should apply to all transactions even without a transaction id"
    (let [{:strs [transaction-id
                  transaction-rule-id]}
          (setup-test-data [(test-transaction :db/id "transaction-id"
                                              :transaction/description-original "MATCH"
                                              :transaction/amount -50.0)
                            (test-transaction-rule :db/id "transaction-rule-id"
                                                   :transaction-rule/description ".*"
                                                   )])]
      (sut/match-transaction-rules {:id (admin-token)} {:all                 true
                                                        :transaction_rule_id transaction-rule-id} nil)
      (= {:transaction/matched-rule {:db/id transaction-rule-id}}
         (dc/pull (dc/db conn) '[:transaction/matched-rule] transaction-id)))))

