(ns ajet.chat.test-helpers
  "Shared fixtures, factories, and utilities for integration/e2e tests.

   All connection details are env-var-overridable for concurrent test runs.
   Source scripts/test-env.sh to auto-pick free ports, or use defaults:
     PG:   AJET_TEST_DB_HOST:AJET_TEST_DB_PORT (default localhost:5433)
     NATS: AJET_TEST_NATS_URL               (default nats://localhost:4223)
     MinIO: AJET_TEST_MINIO_ENDPOINT         (default http://localhost:9002)"
  (:require [clojure.data.json :as json]
            [clojure.string :as str]
            [ajet.chat.shared.db :as db]
            [ajet.chat.shared.eventbus :as eventbus]
            [ajet.chat.shared.storage :as storage]
            [ajet.chat.api.middleware :as mw])
  (:import [java.util UUID]))

;;; ---------------------------------------------------------------------------
;;; Test Config
;;; ---------------------------------------------------------------------------

(def test-db-config
  {:host      (or (System/getenv "AJET_TEST_DB_HOST") "localhost")
   :port      (parse-long (or (System/getenv "AJET_TEST_DB_PORT") "5433"))
   :dbname    (or (System/getenv "AJET_TEST_DB_DBNAME") "ajet_chat_test")
   :user      (or (System/getenv "AJET_TEST_DB_USER") "ajet")
   :password  (or (System/getenv "AJET_TEST_DB_PASSWORD") "ajet_test")
   :pool-size 5})

(def test-nats-config
  {:url (or (System/getenv "AJET_TEST_NATS_URL") "nats://localhost:4223")})

(def test-minio-config
  {:endpoint   (or (System/getenv "AJET_TEST_MINIO_ENDPOINT") "http://localhost:9002")
   :access-key (or (System/getenv "AJET_TEST_MINIO_ACCESS_KEY") "minioadmin")
   :secret-key (or (System/getenv "AJET_TEST_MINIO_SECRET_KEY") "minioadmin")
   :bucket     "ajet-chat"})

;;; ---------------------------------------------------------------------------
;;; Dynamic Vars (bound by suite fixtures)
;;; ---------------------------------------------------------------------------

(def ^:dynamic *ds* nil)
(def ^:dynamic *nats* nil)
(def ^:dynamic *s3* nil)

;;; ---------------------------------------------------------------------------
;;; Suite Fixtures (once per test suite)
;;; ---------------------------------------------------------------------------

(defn with-db-suite
  "Kaocha-compatible suite fixture: create datasource, run migrations, close."
  [f]
  (let [ds (db/make-datasource test-db-config)]
    (try
      (db/migrate! ds {:location "migrations"})
      (binding [*ds* ds]
        (f))
      (finally
        (db/close-datasource ds)))))

(defn with-nats-suite
  "Kaocha-compatible suite fixture: connect NATS, close."
  [f]
  (let [conn (eventbus/connect! test-nats-config)]
    (try
      (binding [*nats* conn]
        (f))
      (finally
        (eventbus/close! conn)))))

(defn with-s3-suite
  "Kaocha-compatible suite fixture: create S3 client, ensure bucket, close."
  [f]
  (let [client (storage/make-client test-minio-config)]
    (try
      (storage/ensure-bucket! client (:bucket test-minio-config))
      (binding [*s3* client]
        (f))
      (finally
        (storage/close-client client)))))

;;; ---------------------------------------------------------------------------
;;; Per-Test Fixtures
;;; ---------------------------------------------------------------------------

(def ^:private truncatable-tables
  "All application tables in dependency order (children first)."
  ["notifications" "mentions" "reactions" "attachments" "messages"
   "channel_members" "channels" "channel_categories"
   "community_members" "invites" "webhooks" "api_tokens" "api_users"
   "bans" "mutes" "sessions" "oauth_accounts" "communities" "users"
   "oauth_providers"])

(defn with-clean-db
  "Per-test fixture: truncate all tables between tests.
   Also resets setup_completed to 'false' for wizard tests."
  [f]
  (db/execute-sql! *ds*
    [(str "TRUNCATE " (str/join ", " truncatable-tables) " RESTART IDENTITY CASCADE")])
  (db/execute-sql! *ds*
    ["UPDATE system_settings SET value = 'false' WHERE key = 'setup_completed'"])
  (f))

;;; ---------------------------------------------------------------------------
;;; Helpers
;;; ---------------------------------------------------------------------------

(defn with-exception-handler
  "Wrap a handler call with the API exception handler middleware.
   Returns the Ring response, converting thrown ex-info to error responses."
  [handler-fn request]
  ((mw/wrap-exception-handler handler-fn) request))

(defn uuid
  "Generate a random UUID string."
  []
  (str (UUID/randomUUID)))

(defn response-body
  "Parse a JSON response body string into a Clojure map."
  [response]
  (when-let [body (:body response)]
    (cond
      (string? body) (when-not (str/blank? body)
                       (json/read-str body :key-fn keyword))
      :else          body)))

;;; ---------------------------------------------------------------------------
;;; Mock Request Builder
;;; ---------------------------------------------------------------------------

(defn mock-request
  "Build a Ring request map for handler testing.

   Options:
     :method      - :get, :post, :put, :delete (default :get)
     :uri         - request URI (default \"/\")
     :user-id     - authenticated user ID (adds X-User-Id header)
     :user-role   - user role in community context
     :community-id - community context
     :system      - system map {:ds, :nats, :s3, :bucket}
     :body-params - parsed JSON body
     :path-params - URL path params
     :query-params - URL query params
     :headers     - additional headers"
  [& [{:keys [method uri user-id user-role community-id
              system body-params path-params query-params headers]}]]
  (cond-> {:request-method (or method :get)
           :uri            (or uri "/")
           :headers        (merge {} headers)
           :body-params    (or body-params {})
           :path-params    (or path-params {})
           :query-params   (or query-params {})}
    user-id      (-> (assoc :user-id user-id)
                     (assoc-in [:headers "x-user-id"] user-id))
    user-role    (assoc-in [:headers "x-user-role"] user-role)
    community-id (assoc-in [:headers "x-community-id"] community-id)
    system       (assoc :system system)))

(defn test-system
  "Build a system map for handler testing from current dynamic vars."
  []
  {:ds     *ds*
   :nats   *nats*
   :s3     *s3*
   :bucket (:bucket test-minio-config)})

;;; ---------------------------------------------------------------------------
;;; Factory Functions (create entities directly via DB)
;;; ---------------------------------------------------------------------------

(defn create-user!
  "Insert a test user. Returns the user row."
  [& [{:keys [username display-name email]
       :or   {email ""}}]]
  (let [id       (uuid)
        username (or username (str "user-" (subs id 0 8)))]
    (db/execute-one! *ds*
      {:insert-into :users
       :values [{:id           [:cast id :uuid]
                 :username     username
                 :display-name (or display-name username)
                 :email        email}]
       :returning [:*]})))

(defn create-community!
  "Insert a test community. Returns the community row."
  [& [{:keys [name slug]}]]
  (let [id   (uuid)
        slug (or slug (str "test-" (subs id 0 8)))]
    (db/execute-one! *ds*
      {:insert-into :communities
       :values [{:id   [:cast id :uuid]
                 :name (or name (str "Community " (subs id 0 8)))
                 :slug slug}]
       :returning [:*]})))

(defn add-member!
  "Add a user to a community with a role. Returns the membership row."
  [community-id user-id & [{:keys [role] :or {role "member"}}]]
  (db/execute-one! *ds*
    {:insert-into :community-members
     :values [{:community-id community-id
               :user-id      user-id
               :role         role}]
     :returning [:*]}))

(defn create-channel!
  "Insert a test channel. Returns the channel row."
  [community-id & [{:keys [name type visibility topic]
                    :or   {type "text" visibility "public"}}]]
  (let [id (uuid)]
    (db/execute-one! *ds*
      {:insert-into :channels
       :values [(cond-> {:id           [:cast id :uuid]
                         :community-id community-id
                         :name         (or name (str "ch-" (subs id 0 8)))
                         :type         type
                         :visibility   visibility}
                  topic (assoc :topic topic))]
       :returning [:*]})))

(defn join-channel!
  "Add a user to a channel. Returns the membership row."
  [channel-id user-id]
  (db/execute-one! *ds*
    {:insert-into :channel-members
     :values [{:channel-id channel-id
               :user-id    user-id}]
     :returning [:*]}))

(defn create-message!
  "Insert a test message. Returns the message row."
  [channel-id user-id & [{:keys [body-md parent-id]
                          :or   {body-md "test message"}}]]
  (let [id (uuid)]
    (db/execute-one! *ds*
      {:insert-into :messages
       :values [(cond-> {:id         [:cast id :uuid]
                         :channel-id channel-id
                         :user-id    user-id
                         :body-md    body-md}
                  parent-id (assoc :parent-id parent-id))]
       :returning [:*]})))
