(ns ajet-chat.e2e.setup
  (:require [ajet-chat.e2e.config :as config]
            [ajet-chat.e2e.helpers :as h]
            [promesa.core :as p]))

;; ---------------------------------------------------------------------------
;; HTTP helpers (fetch-based, runs in Node.js)
;; ---------------------------------------------------------------------------

(defn- fetch-json
  "Make an HTTP request and return parsed JSON."
  ([url opts]
   (p/let [resp (js/fetch url (clj->js opts))
           text (.text resp)]
     (try
       {:status (.-status resp)
        :ok?    (.-ok resp)
        :body   (js/JSON.parse text)}
       (catch :default _
         {:status (.-status resp)
          :ok?    (.-ok resp)
          :body   text})))))

(defn- gitea-api
  "Call Gitea API with admin auth."
  ([method path]
   (gitea-api method path nil))
  ([method path body]
   (let [url  (str config/gitea-url "/api/v1" path)
         opts (cond-> {:method  method
                       :headers {"Content-Type"  "application/json"
                                 "Authorization" (str "Basic "
                                                      (js/btoa (str (:username config/gitea-admin)
                                                                    ":"
                                                                    (:password config/gitea-admin))))}}
                body (assoc :body (js/JSON.stringify (clj->js body))))]
     (fetch-json url opts))))

;; ---------------------------------------------------------------------------
;; Gitea setup — idempotent operations
;; ---------------------------------------------------------------------------

(defn ensure-gitea-admin!
  "Ensure the Gitea admin user exists (created by docker init, but verify)."
  []
  (p/let [resp (gitea-api "GET" "/user")]
    (when-not (:ok? resp)
      (js/console.warn "Gitea admin check failed — admin may not exist yet"
                       (:status resp)))
    resp))

(defn create-gitea-user!
  "Create a user in Gitea. Idempotent (ignores 422 if user exists)."
  [{:keys [username password email]}]
  (gitea-api "POST" "/admin/users"
             {:username            username
              :password            password
              :email               (or email (str username "@test.local"))
              :must_change_password false
              :visibility          "public"}))

(defn create-gitea-oauth-app!
  "Create an OAuth2 application in Gitea. Returns {:client-id :client-secret}.
   Accepts a vector of redirect URIs."
  [redirect-uris]
  (p/let [resp (gitea-api "POST" "/user/applications/oauth2"
                           {:name                "ajet-chat-e2e"
                            :redirect_uris       redirect-uris
                            :confidential_client true})]
    (when (:ok? resp)
      (let [body (:body resp)]
        {:client-id     (aget body "client_id")
         :client-secret (aget body "client_secret")}))))

(defn get-gitea-oauth-apps!
  "List existing OAuth2 applications."
  []
  (gitea-api "GET" "/user/applications/oauth2"))

(defn delete-gitea-oauth-app!
  "Delete an OAuth2 application by ID."
  [app-id]
  (gitea-api "DELETE" (str "/user/applications/oauth2/" app-id)))

(defn ensure-gitea-oauth-app!
  "Ensure an OAuth app exists for ajet-chat-e2e with known credentials.
   Gitea doesn't return client_secret on listing, so we delete any existing
   app and create fresh to get the secret."
  [redirect-uris]
  (p/let [resp (get-gitea-oauth-apps!)
          apps (when (:ok? resp) (js->clj (:body resp) :keywordize-keys true))
          existing (filter #(= (:name %) "ajet-chat-e2e") apps)
          ;; Delete all existing apps with this name
          _ (reduce (fn [chain app]
                      (.then chain (fn [_] (delete-gitea-oauth-app! (:id app)))))
                    (js/Promise.resolve nil)
                    existing)]
    ;; Create fresh to get the secret
    (create-gitea-oauth-app! redirect-uris)))

;; ---------------------------------------------------------------------------
;; ajet-chat setup via browser — setup wizard
;; ---------------------------------------------------------------------------

(defn setup-provider-via-ui!
  "Navigate to setup wizard and add the Gitea OAuth provider via the form."
  [page {:keys [client-id client-secret]}]
  (p/do
    (h/goto page "/setup")
    (h/wait-for-selector page "select[name='provider-type']")
    (h/select-option page "select[name='provider-type']" "gitea")
    (h/fill page "input[name='display-name']" "Gitea Test")
    (h/fill page "input[name='slug']" "gitea-test")
    (h/fill page "input[name='client-id']" client-id)
    (h/fill page "input[name='client-secret']" client-secret)
    (h/fill page "input[name='base-url']" config/gitea-url)
    (h/click page "button[type='submit']")
    (h/wait-ms 1000)))

(defn login-via-gitea!
  "Perform full OAuth login through Gitea. Starts from the login page.
   Uses localhost:4080 for Gitea (exposed via Docker port mapping).
   Handles three scenarios: login form, authorize page, or auto-redirect.
   Returns when the browser is redirected back to ajet-chat."
  [page {:keys [username password]}]
  (p/do
    ;; Click the Gitea provider button on login page
    (h/goto page "/auth/login")
    (h/wait-for-selector page "a[href*='provider=gitea-test']")
    (h/click page "a[href*='provider=gitea-test']")

    ;; After clicking, we might land on:
    ;; 1. Gitea login form (first time for this browser context)
    ;; 2. Gitea authorize page (already logged in, new OAuth grant)
    ;; 3. Auto-redirect back to ajet-chat (already authorized)
    (p/let [login-form (p/catch
                         (.waitForSelector page "#user_name"
                                           #js {:timeout 5000})
                         (fn [_] nil))]
      (when login-form
        (p/do
          (h/fill page "#user_name" username)
          (h/fill page "#password" password)
          (h/click page "button:has-text('Sign In')"))))

    ;; Handle Gitea authorization page if shown
    (p/let [authorize-btn (p/catch
                            (.waitForSelector page
                                              "button:has-text('Authorize Application'), button.ui.green.button"
                                              #js {:timeout 5000})
                            (fn [_] nil))]
      (when authorize-btn
        (.click authorize-btn)))

    ;; Wait for redirect back to ajet-chat (localhost:4000)
    (h/wait-for-url page (js/RegExp. (str config/base-url ".*")) {:timeout 30000})))

(defn create-community-via-ui!
  "Fill in the community creation form.
   Works with both Auth-GW HTML form (initial setup) and Web-SM Datastar form.
   Uses type-text (char-by-char) to ensure Datastar data-bind signals update."
  [page community-name community-slug]
  (p/do
    (h/wait-for-selector page "input[name='name']")
    ;; Wait for Datastar SDK to initialize (loaded as ES module from CDN)
    (h/wait-ms 1500)
    ;; Use type-text to trigger Datastar data-bind event handlers
    (h/type-text page "input[name='name']" community-name)
    (h/type-text page "input[name='slug']" community-slug)
    (h/click page "button[type='submit']")
    ;; Wait for form processing (HTTP redirect or Datastar SSE redirect)
    (h/wait-ms 2000)))

;; ---------------------------------------------------------------------------
;; Direct DB / API setup shortcuts (skip OAuth for most tests)
;; ---------------------------------------------------------------------------

(defn api-call
  "Make authenticated API call to ajet-chat API.
   Uses direct API URL (bypasses auth-gw) with injected X-User-Id header."
  ([method path user-id]
   (api-call method path user-id nil))
  ([method path user-id body]
   (let [url  (str config/api-url "/api" path)
         opts (cond-> {:method  method
                       :headers {"Content-Type" "application/json"
                                 "X-User-Id"    user-id}}
                body (assoc :body (js/JSON.stringify (clj->js body))))]
     (fetch-json url opts))))

(defn create-user-direct!
  "Create a user directly via the API (bypasses OAuth).
   Returns the user object."
  [{:keys [username display-name email]}]
  ;; Use the admin-level internal API endpoint
  ;; For tests, we create users by posting to the API with a synthetic X-User-Id
  ;; The API trusts the X-User-Id header since auth-gw normally handles auth
  ;; For creating test users, we use a special bootstrap approach:
  ;; directly insert via SQL or use the first-user setup flow
  (p/let [url  (str config/api-url "/api/health")
          resp (fetch-json url {:method "GET"})]
    ;; If API is healthy, we can proceed
    (when (:ok? resp)
      {:username     username
       :display-name (or display-name username)
       :email        (or email (str username "@test.local"))})))

(defn create-community-api!
  "Create a community via API."
  [user-id name slug]
  (api-call "POST" "/communities" user-id {:name name :slug slug}))

(defn create-channel-api!
  "Create a channel in a community via API."
  [user-id community-id name]
  (api-call "POST" (str "/communities/" community-id "/channels")
            user-id {:name name :type "text"}))

(defn send-message-api!
  "Send a message to a channel via API."
  [user-id channel-id body-md]
  (api-call "POST" (str "/channels/" channel-id "/messages")
            user-id {:body-md body-md}))

(defn create-dm-api!
  "Create a DM between two users via API."
  [user-id target-user-id]
  (api-call "POST" "/dms" user-id {:user-id target-user-id}))

(defn set-session-cookie!
  "Set a session cookie on a browser page context.
   Useful for skipping OAuth login in most tests."
  [context cookie-value]
  (let [url-obj (js/URL. config/base-url)]
    (.addCookies context
                 (clj->js [{:name     "ajet_session"
                             :value    cookie-value
                             :domain   (.-hostname url-obj)
                             :path     "/"
                             :httpOnly true
                             :sameSite "Lax"}]))))

;; ---------------------------------------------------------------------------
;; Browser-context API helpers (use session cookie from logged-in page)
;; ---------------------------------------------------------------------------

(defn get-user-id-from-page
  "Get the current user's ID from a logged-in page via /api/me."
  [page]
  (h/evaluate page "fetch('/api/me').then(r => r.json()).then(d => d.id)"))

(defn get-first-community-id
  "Get the first community's ID from a logged-in page."
  [page]
  (h/evaluate page "fetch('/api/communities').then(r => r.json()).then(d => d[0] && d[0].id)"))

(defn create-invite-from-page!
  "Create an invite link for a community from a logged-in page. Returns invite code."
  [page community-id]
  (h/evaluate page
    (str "fetch('/api/communities/" community-id "/invites', {"
         "method: 'POST', "
         "headers: {'Content-Type': 'application/json'}, "
         "body: JSON.stringify({max_uses: 10, expires_in_hours: 24})"
         "}).then(r => r.json()).then(d => d.code)")))

(defn accept-invite-from-page!
  "Accept an invite from a logged-in page. Returns response body."
  [page invite-code]
  (h/evaluate page
    (str "fetch('/api/invites/" invite-code "/accept', {"
         "method: 'POST', "
         "headers: {'Content-Type': 'application/json'}"
         "}).then(r => r.json())")))

(defn ensure-user-in-community!
  "Ensure user B joins user A's community via invite flow.
   page-a must be logged in as User A with a community.
   page-b must be logged in as User B.
   Returns the community ID."
  [page-a page-b]
  (p/let [community-id (get-first-community-id page-a)
          invite-code  (create-invite-from-page! page-a community-id)
          _            (accept-invite-from-page! page-b invite-code)]
    (js/console.log "  [setup] User B joined community:" community-id
                    "via invite:" invite-code)
    community-id))

(defn create-dm-from-page!
  "Create a DM between the page's current user and target user.
   Returns the DM channel object. API expects user_id (underscore)."
  [page target-user-id]
  (h/evaluate page
    (str "fetch('/api/dms', {"
         "method: 'POST', "
         "headers: {'Content-Type': 'application/json'}, "
         "body: JSON.stringify({user_id: '" target-user-id "'})"
         "}).then(r => r.json())")))

(defn create-community-from-page!
  "Create a community via API from a logged-in browser page.
   Returns the community object."
  [page community-name community-slug]
  (h/evaluate page
    (str "fetch('/api/communities', {"
         "method: 'POST', "
         "headers: {'Content-Type': 'application/json'}, "
         "body: JSON.stringify({name: '" community-name "', slug: '" community-slug "'})"
         "}).then(r => r.json())")))

;; ---------------------------------------------------------------------------
;; Login + setup helper for multi-user tests
;; ---------------------------------------------------------------------------

(defn login-and-setup!
  "Login a user via OAuth, create community if on /setup, return to /app.
   Returns the page (already navigated to /app)."
  [page user-creds community-name community-slug]
  (p/do
    (login-via-gitea! page user-creds)
    (h/wait-for-url page #"/(app|setup)" {:timeout 30000})
    (p/let [url (h/page-url page)]
      (when (re-find #"/setup" url)
        (p/do
          (create-community-via-ui! page community-name community-slug)
          (h/wait-for-url page #"/app" {:timeout 15000}))))))

;; ---------------------------------------------------------------------------
;; Full E2E setup orchestration
;; ---------------------------------------------------------------------------

;; Cache OAuth creds across tests — the Gitea OAuth app must match what's in
;; the ajet-chat DB. Recreating it invalidates the DB provider's client_id.
(def ^:private cached-oauth-creds (atom nil))

(defn setup-gitea-and-provider!
  "Complete Gitea + OAuth provider setup for E2E tests.
   1. Verify Gitea admin exists
   2. Ensure test users exist
   3. Create/find OAuth app (cached — only created once per test run)
   Returns {:client-id :client-secret}"
  []
  (if-let [cached @cached-oauth-creds]
    (do (js/console.log "[setup] Gitea OAuth app (cached):" (clj->js cached))
        (js/Promise.resolve cached))
    (p/let [_ (ensure-gitea-admin!)
            _ (create-gitea-user! config/test-user-a)
            _ (create-gitea-user! config/test-user-b)
            ;; Register the browser-facing redirect URI (localhost:4000 = auth-gw)
            redirect-uris [(str config/base-url "/auth/callback/gitea-test")]
            oauth-creds   (ensure-gitea-oauth-app! redirect-uris)]
      (reset! cached-oauth-creds oauth-creds)
      (js/console.log "[setup] Gitea OAuth app:" (clj->js oauth-creds))
      oauth-creds)))
