;; copyright (c) 2019-2021 Sean Corfield, all rights reserved

(ns next.jdbc.connection-test
  "Tests for the main hash map spec to JDBC URL logic and the get-datasource
  and get-connection protocol implementations.

  At some point, the datasource/connection tests should probably be extended
  to accept EDN specs from an external source (environment variables?)."
  (:require [clojure.string :as str]
            [clojure.test :refer [deftest is testing]]
            [next.jdbc.connection :as c]
            [next.jdbc.protocols :as p])
  (:import (com.zaxxer.hikari HikariDataSource)
           (com.mchange.v2.c3p0 ComboPooledDataSource PooledDataSource)))

(set! *warn-on-reflection* true)

(def ^:private db-name "clojure_test")

(deftest test-aliases-and-defaults
  (testing "aliases"
    (is (= (#'c/spec->url+etc {:dbtype "hsql" :dbname db-name})
           (#'c/spec->url+etc {:dbtype "hsqldb" :dbname db-name})))
    (is (= (#'c/spec->url+etc {:dbtype "jtds" :dbname db-name})
           (#'c/spec->url+etc {:dbtype "jtds:sqlserver" :dbname db-name})))
    (is (= (#'c/spec->url+etc {:dbtype "mssql" :dbname db-name})
           (#'c/spec->url+etc {:dbtype "sqlserver" :dbname db-name})))
    (is (= (#'c/spec->url+etc {:dbtype "oracle" :dbname db-name})
           (#'c/spec->url+etc {:dbtype "oracle:thin" :dbname db-name})))
    (is (= (#'c/spec->url+etc {:dbtype "oracle:sid" :dbname db-name})
           (-> (#'c/spec->url+etc {:dbtype "oracle:thin" :dbname db-name})
               ;; oracle:sid uses : before DB name, not /
               (update 0 str/replace (re-pattern (str "/" db-name)) (str ":" db-name)))))
    (is (= (#'c/spec->url+etc {:dbtype "oracle:oci" :dbname db-name})
           (-> (#'c/spec->url+etc {:dbtype "oracle:thin" :dbname db-name})
               ;; oracle:oci and oracle:thin only differ in the protocol
               (update 0 str/replace #":thin" ":oci"))))
    (is (= (#'c/spec->url+etc {:dbtype "postgres" :dbname db-name})
           (#'c/spec->url+etc {:dbtype "postgresql" :dbname db-name}))))
  (testing "default ports"
    (is (= (#'c/spec->url+etc {:dbtype "jtds:sqlserver" :dbname db-name})
           (#'c/spec->url+etc {:dbtype "jtds:sqlserver" :dbname db-name :port 1433})))
    (is (= (#'c/spec->url+etc {:dbtype "mysql" :dbname db-name})
           (#'c/spec->url+etc {:dbtype "mysql" :dbname db-name :port 3306})))
    (is (= (#'c/spec->url+etc {:dbtype "oracle:oci" :dbname db-name})
           (#'c/spec->url+etc {:dbtype "oracle:oci" :dbname db-name :port 1521})))
    (is (= (#'c/spec->url+etc {:dbtype "oracle:sid" :dbname db-name})
           (#'c/spec->url+etc {:dbtype "oracle:sid" :dbname db-name :port 1521})))
    (is (= (#'c/spec->url+etc {:dbtype "oracle:thin" :dbname db-name})
           (#'c/spec->url+etc {:dbtype "oracle:thin" :dbname db-name :port 1521})))
    (is (= (#'c/spec->url+etc {:dbtype "postgresql" :dbname db-name})
           (#'c/spec->url+etc {:dbtype "postgresql" :dbname db-name :port 5432})))
    (is (= (#'c/spec->url+etc {:dbtype "sqlserver" :dbname db-name})
           (#'c/spec->url+etc {:dbtype "sqlserver" :dbname db-name :port 1433})))))

(deftest custom-dbtypes
  (is (= ["jdbc:acme:my-db" {} nil]
         (#'c/spec->url+etc {:dbtype "acme" :classname "java.lang.String"
                             :dbname "my-db" :host :none})))
  (is (= ["jdbc:acme://127.0.0.1/my-db" {} nil]
         (#'c/spec->url+etc {:dbtype "acme" :classname "java.lang.String"
                             :dbname "my-db"})))
  (is (= ["jdbc:acme://12.34.56.70:1234/my-db" {} nil]
         (#'c/spec->url+etc {:dbtype "acme" :classname "java.lang.String"
                             :dbname "my-db" :host "12.34.56.70" :port 1234})))
  (is (= ["jdbc:acme:dsn=my-db" {} nil]
         (#'c/spec->url+etc {:dbtype "acme" :classname "java.lang.String"
                             :dbname "my-db" :host :none
                             :dbname-separator ":dsn="})))
  (is (= ["jdbc:acme:(*)127.0.0.1/my-db" {} nil]
         (#'c/spec->url+etc {:dbtype "acme" :classname "java.lang.String"
                             :dbname "my-db"
                             :host-prefix "(*)"})))
  (is (= ["jdbc:acme:(*)12.34.56.70:1234/my-db" {} nil]
         (#'c/spec->url+etc {:dbtype "acme" :classname "java.lang.String"
                             :dbname "my-db" :host "12.34.56.70" :port 1234
                             :host-prefix "(*)"})))
  (is (= ["jdbc:acme:(*)12.34.56.70/my-db" {} nil]
         (#'c/spec->url+etc {:dbtype "acme" :classname "java.lang.String"
                             :dbname "my-db" :host "12.34.56.70" :port :none
                             :host-prefix "(*)"}))))

(deftest jdbc-url-tests
  (testing "basic URLs work"
    (is (= "jdbc:acme:my-db"
           (c/jdbc-url {:dbtype "acme" :classname "java.lang.String"
                        :dbname "my-db" :host :none})))
    (is (= "jdbc:acme://127.0.0.1/my-db"
           (c/jdbc-url {:dbtype "acme" :classname "java.lang.String"
                        :dbname "my-db"})))
    (is (= "jdbc:acme://12.34.56.70:1234/my-db"
           (c/jdbc-url {:dbtype "acme" :classname "java.lang.String"
                        :dbname "my-db" :host "12.34.56.70" :port 1234})))
    (is (= "jdbc:acme:dsn=my-db"
           (c/jdbc-url {:dbtype "acme" :classname "java.lang.String"
                        :dbname "my-db" :host :none
                        :dbname-separator ":dsn="})))
    (is (= "jdbc:acme:(*)127.0.0.1/my-db"
           (c/jdbc-url {:dbtype "acme" :classname "java.lang.String"
                        :dbname "my-db"
                        :host-prefix "(*)"})))
    (is (= "jdbc:acme:(*)12.34.56.70:1234/my-db"
           (c/jdbc-url {:dbtype "acme" :classname "java.lang.String"
                        :dbname "my-db" :host "12.34.56.70" :port 1234
                        :host-prefix "(*)"}))))
  (testing "URLs with properties work"
    (is (= "jdbc:acme:my-db?useSSL=true"
           (c/jdbc-url {:dbtype "acme" :classname "java.lang.String"
                        :dbname "my-db" :host :none
                        :useSSL true})))
    (is (boolean (#{"jdbc:acme:my-db?useSSL=true&user=dba"
                    "jdbc:acme:my-db?user=dba&useSSL=true"}
                  (c/jdbc-url {:dbtype "acme" :classname "java.lang.String"
                               :dbname "my-db" :host :none
                               :useSSL true :user "dba"}))))

    (is (= "jdbc:jtds:sqlserver:my-db;useSSL=true"
           (c/jdbc-url {:dbtype "jtds"
                        :dbname "my-db" :host :none
                        :useSSL true})))
    (is (boolean (#{"jdbc:jtds:sqlserver:my-db;useSSL=true;user=dba"
                    "jdbc:jtds:sqlserver:my-db;user=dba;useSSL=true"}
                  (c/jdbc-url {:dbtype "jtds"
                               :dbname "my-db" :host :none
                               :useSSL true :user "dba"}))))))

;; these are the 'local' databases that we can always test against
(def test-db-type ["derby" "h2" "h2:mem" "hsqldb" "sqlite"])

(def test-dbs
  (for [db test-db-type]
    (cond-> {:dbtype db :dbname (str db-name "_" (str/replace db #":" "_"))}
      (= "derby" db)
      (assoc :create true))))

(deftest test-sourceable-via-metadata
  (doseq [db test-dbs]
    (let [ds (p/get-datasource
              ^{`p/get-datasource (fn [v] (p/get-datasource (first v)))} [db])]
      (is (instance? javax.sql.DataSource ds)))))

(deftest test-get-connection
  (doseq [db test-dbs]
    (println 'test-get-connection (:dbtype db))
    (testing "datasource via Associative"
      (let [ds (p/get-datasource db)]
        (is (instance? javax.sql.DataSource ds))
        (is (str/index-of (pr-str ds) (str "jdbc:" (:dbtype db))))
        ;; checks get-datasource on a DataSource is identity
        (is (identical? ds (p/get-datasource ds)))
        (with-open [con (p/get-connection ds {})]
          (is (instance? java.sql.Connection con)))))
    (testing "datasource via String"
      (let [[url _] (#'c/spec->url+etc db)
            ds (p/get-datasource url)]
        (is (instance? javax.sql.DataSource ds))
        (is (str/index-of (pr-str ds) url))
        (.setLoginTimeout ds 0)
        (is (= 0 (.getLoginTimeout ds)))
        (with-open [con (p/get-connection ds {})]
          (is (instance? java.sql.Connection con)))))
    (testing "datasource via jdbcUrl"
      (let [[url etc] (#'c/spec->url+etc db)
            ds (p/get-datasource (assoc etc :jdbcUrl url))]
        (if (= "derby" (:dbtype db))
          (is (= {:create true} etc))
          (is (= {} etc)))
        (is (instance? javax.sql.DataSource ds))
        (is (str/index-of (pr-str ds) (str "jdbc:" (:dbtype db))))
        ;; checks get-datasource on a DataSource is identity
        (is (identical? ds (p/get-datasource ds)))
        (.setLoginTimeout ds 1)
        (is (= 1 (.getLoginTimeout ds)))
        (with-open [con (p/get-connection ds {})]
          (is (instance? java.sql.Connection con)))))
    (testing "datasource via HikariCP"
      ;; the type hint is only needed because we want to call .close
      (with-open [^HikariDataSource ds (c/->pool HikariDataSource db)]
        (is (instance? javax.sql.DataSource ds))
        ;; checks get-datasource on a DataSource is identity
        (is (identical? ds (p/get-datasource ds)))
        (with-open [con (p/get-connection ds {})]
          (is (instance? java.sql.Connection con)))))
    (testing "datasource via c3p0"
      ;; the type hint is only needed because we want to call .close
      (with-open [^PooledDataSource ds (c/->pool ComboPooledDataSource db)]
        (is (instance? javax.sql.DataSource ds))
        ;; checks get-datasource on a DataSource is identity
        (is (identical? ds (p/get-datasource ds)))
        (with-open [con (p/get-connection ds {})]
          (is (instance? java.sql.Connection con)))))
    (testing "connection via map (Object)"
      (with-open [con (p/get-connection db {})]
        (is (instance? java.sql.Connection con))))))

(deftest issue-243-uri->db-spec
  (is (= {:dbtype "mysql" :dbname "mydb"
          :host "myserver" :port 1234
          :user "foo" :password "bar"}
         (c/uri->db-spec "mysql://foo:bar@myserver:1234/mydb")))
  (is (= {:dbtype "mysql" :dbname "mydb"
          :host "myserver" :port 1234
          :user "foo" :password "bar"}
         (c/uri->db-spec "jdbc:mysql://myserver:1234/mydb?user=foo&password=bar"))))
