(ns ajet-chat.e2e.runner
  "Runs all E2E test suites sequentially, awaiting async completion.
   Each test module exports a `tests` vector of {:name :test-fn} maps
   where :test-fn returns a promise."
  (:require ["child_process" :as cp]
            [ajet-chat.e2e.config :as config]
            [promesa.core :as p]
            [ajet-chat.e2e.tests.setup-test :as setup-test]
            [ajet-chat.e2e.tests.community-test :as community-test]
            [ajet-chat.e2e.tests.channels-test :as channels-test]
            [ajet-chat.e2e.tests.messaging-test :as messaging-test]
            [ajet-chat.e2e.tests.dms-test :as dms-test]
            [ajet-chat.e2e.tests.upload-test :as upload-test]))

;; ---------------------------------------------------------------------------
;; DB reset — ensures a clean DB for each test run
;; ---------------------------------------------------------------------------

(defn- reset-db!
  "Reset the test database by dropping and recreating the public schema.
   Then restart API + auth-gw to re-run migrations and clear caches."
  []
  (js/console.log "[runner] Resetting test database...")
  (let [root-dir (or js/process.env.AJET_ROOT_DIR "..")]
    (try
      ;; Drop and recreate schema
      (cp/execSync
        "docker exec ajet-chat-postgres-test-1 psql -U ajet -d ajet_chat_test -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'"
        #js {:stdio "pipe" :timeout 10000})
      (js/console.log "[runner] Schema dropped and recreated")

      ;; Restart API first (runs migrations), then other services
      (cp/execSync
        (str "docker compose -f " root-dir "/docker-compose.test.yml --profile e2e stop api auth-gw web-sm tui-sm")
        #js {:stdio "pipe" :timeout 30000})
      (cp/execSync
        (str "docker compose -f " root-dir "/docker-compose.test.yml --profile e2e start api")
        #js {:stdio "pipe" :timeout 30000})

      ;; Wait for API to become healthy
      (js/console.log "[runner] Waiting for API to become healthy...")
      (cp/execSync "sleep 15" #js {:stdio "pipe"})

      ;; Start remaining services
      (cp/execSync
        (str "docker compose -f " root-dir "/docker-compose.test.yml --profile e2e start web-sm tui-sm")
        #js {:stdio "pipe" :timeout 30000})
      (cp/execSync "sleep 5" #js {:stdio "pipe"})
      (cp/execSync
        (str "docker compose -f " root-dir "/docker-compose.test.yml --profile e2e start auth-gw")
        #js {:stdio "pipe" :timeout 30000})

      ;; Wait for auth-gw to become healthy
      (js/console.log "[runner] Waiting for auth-gw to become healthy...")
      (cp/execSync "sleep 15" #js {:stdio "pipe"})

      (js/console.log "[runner] All services restarted with fresh DB")
      (catch :default e
        (js/console.error "[runner] DB reset failed:" (.-message e))
        (throw e)))))

;; ---------------------------------------------------------------------------
;; Simple test runner — sequential, promise-aware
;; ---------------------------------------------------------------------------

(def ^:private state (atom {:pass 0 :fail 0 :error 0 :total 0}))

(defn- run-test
  "Run a single test. Returns a promise that always resolves (never rejects)."
  [test-map]
  (let [test-name (:name test-map)
        test-fn   (:test-fn test-map)]
    (p/let [start  (js/Date.now)
            result (-> (test-fn)
                       (.then  (fn [_] :pass))
                       (.catch (fn [err]
                                 (js/console.error (str "  FAIL: " test-name))
                                 (js/console.error "       " (.-message err))
                                 (when (.-stack err)
                                   (js/console.error (.-stack err)))
                                 :fail)))
            elapsed (- (js/Date.now) start)]
      (swap! state update result (fnil inc 0))
      (swap! state update :total inc)
      (let [icon (if (= result :pass) "  PASS" "  FAIL")]
        (js/console.log (str icon " " test-name " (" elapsed "ms)"))))))

(defn- run-suite
  "Run a named test suite (vector of test maps) sequentially."
  [suite-name tests]
  (js/console.log (str "\n--- " suite-name " ---"))
  (reduce (fn [chain test-map]
            (.then chain (fn [_] (run-test test-map))))
          (js/Promise.resolve nil)
          tests))

(defn- run-all-suites
  "Run all test suites in order."
  []
  (-> (js/Promise.resolve nil)
      (.then (fn [_] (run-suite "Setup Wizard" setup-test/tests)))
      (.then (fn [_] (run-suite "Community" community-test/tests)))
      (.then (fn [_] (run-suite "Channels" channels-test/tests)))
      (.then (fn [_] (run-suite "Messaging" messaging-test/tests)))
      (.then (fn [_] (run-suite "DMs" dms-test/tests)))
      (.then (fn [_] (run-suite "File Upload" upload-test/tests)))))

(defn main []
  ;; Prevent unhandled promise rejections from crashing the process
  ;; (Playwright operations may reject when browser is closed during cleanup)
  (.on js/process "unhandledRejection"
    (fn [reason _promise]
      (js/console.error "[runner] Unhandled rejection (suppressed):"
                        (if (.-message reason) (.-message reason) reason))))

  (js/console.log "\n=== ajet-chat Playwright E2E Tests ===\n")

  ;; Reset DB unless SKIP_DB_RESET=true
  (when-not (= "true" js/process.env.SKIP_DB_RESET)
    (reset-db!))

  (-> (run-all-suites)
      (.then (fn [_]
               (let [{:keys [pass fail total]} @state
                     failures (+ fail (:error @state 0))]
                 (js/console.log "\n========================================")
                 (js/console.log (str "Total: " total
                                      " | Pass: " pass
                                      " | Fail: " fail))
                 (js/console.log "========================================")
                 (js/process.exit (if (zero? failures) 0 1)))))
      (.catch (fn [err]
                (js/console.error "\nFatal error:" err)
                (js/process.exit 1)))))

(main)
