(ns jepsen.antietcd
  (:require
    [clojure.tools.logging :refer :all]
    [clojure.string :as str]
    [jepsen
      [checker :as checker]
      [cli :as cli]
      [client :as client]
      [control :as c]
      [db :as db]
      [generator :as gen]
      [nemesis :as nemesis]
      [tests :as tests]
    ]
    [jepsen.nemesis.combined :as nc]
    [jepsen.antietcd
      [append :as append]
      [client :as anticli]
      [set :as set]
      [register :as register]
      [watch :as watch]
      [wr :as wr]
    ]
    [jepsen.control.util :as cu]
    [jepsen.os.debian :as debian]
    [org.httpkit.client :as http]
    [clojure.data.json :as json]
  )
)

(def workloads
  "A map of workload names to functions that construct workloads, given opts."
  {
    :append         append/workload
    :set            set/workload
    :register       register/workload
    :watch          watch/workload
    :wr             wr/workload
  }
)

(def nemeses
  "All nemeses"
  #{:partition :packet :kill :pause :clock}
)

(def dir "/opt/antietcd")
(def nodejs "/usr/bin/node")
(def logfile (str dir "/antietcd.log"))
(def pidfile (str dir "/antietcd.pid"))

(defn initial-cluster
  "Constructs an initial cluster string for a test, like
  \"foo=foo:2379,bar=bar:2379,...\""
  [test]
  (->> (:nodes test)
    (map (fn [node] (str node "=" (anticli/node-url node))))
    (str/join ",")
  )
)

(defn db
  "Antietcd DB for a particular version."
  [opts]
  (let
    [
      version (:version opts)
      git (:git opts)
    ]
    (reify
      db/DB
      (setup! [db test node]
        (info node "installing antietcd" version)
        (c/su
          (let [pkg (str "antietcd@" version)]
            (debian/install [:nodejs :npm :sudo])
            (if git
              (cu/install-archive! (str "https://git.yourcmc.ru/vitalif/antietcd/archive/" version ".tar.gz") dir)
              (c/exec :mkdir dir)
            )
            (c/cd dir
              (if git
                (c/exec :npm :install)
                (c/exec :npm :install pkg)
              )
            )
          )
          (db/start! db test node)
          (Thread/sleep 10000)
        )
      )

      (teardown! [db test node]
        (info node "tearing down antietcd")
        (db/kill! db test node)
        (c/su (c/exec :rm :-rf dir))
      )

      db/LogFiles
      (log-files [_ test node]
        [logfile]
      )

      db/Primary
      (setup-primary! [_ test node])

      (primaries [_ test]
        (let
          [prim (anticli/get_primary! (:nodes test))]
          (if prim (list prim) ())
        )
      )

      db/Process
      (start! [_ test node]
        (cu/start-daemon!
          {
            :logfile logfile
            :pidfile pidfile
            :chdir   dir
          }
          nodejs
          (if (:git test)
            "/opt/antietcd/antietcd-app.js"
            "/opt/antietcd/node_modules/antietcd/antietcd-app.js"
          )
          :--node_id     (name node)
          :--cluster     (initial-cluster test)
          :--cluster_key :jepsen_test
          :--port        :2379
          :--logs        (if (:access-log test) "access,cluster" "cluster")
          :--stale_read  (str (:stale-read test))
          :--use_base64  :false
        )
      )

      (kill! [_ test node]
        (c/su (cu/stop-daemon! nodejs pidfile))
      )

      db/Pause
      (pause!  [_ test node] (c/su (cu/grepkill! :stop "antietcd")))
      (resume! [_ test node] (c/su (cu/grepkill! :cont "antietcd")))
    )
  )
)

(defn antietcd-test
  "Given an options map from the command line runner (e.g. :nodes, :ssh,
  :concurrency, ...), constructs a test map."
  [opts]
  (let [
      workload-name (:workload opts)
      workload ((workloads workload-name) opts)
      db (db opts)
      nemesis (nc/nemesis-package {
        :db db
        :nodes (:nodes opts)
        :faults (:nemesis opts)
        :partitions {:targets [:primaries :majority :majorities-ring]}
        :clock {:targets [:one :minority]}
        :pause {:targets [:primaries]}
        :kill {:targets [:primaries]}
        :interval (:nemesis-interval opts)
      })
    ]
    (merge tests/noop-test
      opts
      {
        :name (str "antietcd" workload-name)
        :os debian/os
        :db db
        :nemesis (:nemesis nemesis)
        :pure-generators true
        :client (:client workload)
        :checker (:checker workload)
        :generator (gen/nemesis
          (gen/phases
            (gen/sleep 5)
            (gen/time-limit (- (:time-limit opts) 5)
              (:generator nemesis)
            )
            (:final-generator nemesis)
          )
          (gen/phases
            (gen/time-limit (:time-limit opts)
              (gen/stagger (/ (:rate opts))
                (:generator workload)
              )
            )
            (gen/log "Waiting for recovery")
            (gen/sleep 10)
            (gen/clients (:final-generator workload))
          )
        )
      }
    )
  )
)

(def cli-opts
  "Additional command line options."
  [
    ["-w" "--workload NAME" (str "What workload should we run? " (cli/one-of workloads))
      :default  :append
      :parse-fn keyword
      :validate [workloads (cli/one-of workloads)]
    ]
    ["-r" "--rate HZ" "Approximate number of requests per second, per thread."
      :default  10
      :parse-fn read-string
      :validate [#(and (number? %) (pos? %)) "Must be a positive number"]
    ]
    [nil "--ops-per-key NUM" "Maximum number of operations on any given key."
      :default  100
      :parse-fn parse-long
      :validate [pos? "Must be a positive integer."]
    ]
    [nil "--nemesis FAULTS" "A comma-separated list of nemesis faults to enable"
      :default  (vec nemeses)
      :parse-fn #(map keyword (str/split % #","))
      :validate [(partial every? (fn [nem] (nemeses nem))) (cli/one-of nemeses)]
    ]
    [nil "--nemesis-interval SECONDS" "How long between nemesis operations for each class of fault"
      :default  5
      :parse-fn read-string
      :validate [pos? "Must be positive"]
    ]
    [nil "--stale-read true/false" "Enable/disable stale reads (default disabled)"
      :default  false
      :parse-fn boolean
    ]
    [nil "--version NAME" "Antietcd version/branch to test (default master)"
      :default  "master"
      :validate [(partial re-find #"^[a-z0-9_\.\-]+$") "Must consist of alphanumeric characters, _, . and -"]
    ]
    [nil "--git true/false" "Take Antietcd to test from its git (true) or from npm (default true)"
      :default  true
      :parse-fn boolean
    ]
    [nil "--access-log true/false" "Enable Antietcd access logs (sometimes it may prevent reproduction of bugs)"
      :default  false
      :parse-fn boolean
    ]
  ]
)

(defn -main
  "Handles command line arguments. Can either run a test, or a web server for
  browsing results."
  [& args]
  (cli/run!
    (
      merge
      (cli/single-test-cmd {:test-fn antietcd-test :opt-spec cli-opts})
      (cli/serve-cmd)
    )
    args
  )
)
