(ns ajet-chat.e2e.helpers
  (:require ["playwright" :as pw]
            [ajet-chat.e2e.config :as config]
            [promesa.core :as p]))

;; ---------------------------------------------------------------------------
;; Browser lifecycle
;; ---------------------------------------------------------------------------

(defn launch-browser!
  "Launch a Chromium browser instance. Returns a promise."
  []
  (.launch pw/chromium
           #js {:headless config/headless?
                :slowMo   config/slow-mo}))

(defn new-context!
  "Create a new browser context (isolated cookies/storage). Returns a promise."
  [browser]
  (.newContext browser))

(defn new-page!
  "Create a new page within a browser context. Returns a promise."
  [context]
  (.newPage context))

(defn close-all!
  "Close browser and all contexts."
  [browser]
  (when browser
    (.close browser)))

;; ---------------------------------------------------------------------------
;; Navigation
;; ---------------------------------------------------------------------------

(defn goto
  "Navigate to a URL (relative to base-url or absolute). Returns a promise."
  ([page path]
   (goto page path {}))
  ([page path opts]
   (let [url (if (re-find #"^https?://" path)
               path
               (str config/base-url path))
         wait-until (or (:wait-until opts) "load")]
     (.goto page url #js {:waitUntil wait-until
                          :timeout   (or (:timeout opts) 30000)}))))

;; ---------------------------------------------------------------------------
;; Interactions
;; ---------------------------------------------------------------------------

(defn click
  "Click an element matching the selector."
  ([page selector]
   (click page selector {}))
  ([page selector opts]
   (.click page selector (clj->js (merge {:timeout config/default-timeout} opts)))))

(defn fill
  "Fill a form field matching the selector."
  [page selector value]
  (.fill page selector value #js {:timeout config/default-timeout}))

(defn type-text
  "Type text character by character (triggers key handlers)."
  ([page selector text]
   (type-text page selector text {}))
  ([page selector text opts]
   (.type page selector text (clj->js (merge {:timeout config/default-timeout} opts)))))

(defn press
  "Press a key (e.g. 'Enter', 'Tab')."
  [page selector key-name]
  (.press page selector key-name #js {:timeout config/default-timeout}))

(defn select-option
  "Select an option in a <select> element."
  [page selector value]
  (.selectOption page selector value #js {:timeout config/default-timeout}))

;; ---------------------------------------------------------------------------
;; Queries
;; ---------------------------------------------------------------------------

(defn text-content
  "Get text content of an element matching the selector."
  [page selector]
  (.textContent page selector #js {:timeout config/default-timeout}))

(defn inner-text
  "Get inner text of an element."
  [page selector]
  (.innerText page selector #js {:timeout config/default-timeout}))

(defn input-value
  "Get the value of an input field."
  [page selector]
  (.inputValue page selector #js {:timeout config/default-timeout}))

(defn is-visible?
  "Check if an element is visible."
  [page selector]
  (.isVisible page selector))

(defn count-elements
  "Count elements matching a selector."
  [page selector]
  (p/let [els (.locator page selector)]
    (.count els)))

;; ---------------------------------------------------------------------------
;; Waiting
;; ---------------------------------------------------------------------------

(defn wait-for-selector
  "Wait for an element matching the selector to appear."
  ([page selector]
   (wait-for-selector page selector {}))
  ([page selector opts]
   (.waitForSelector page selector
                     (clj->js (merge {:timeout config/default-timeout
                                      :state   "visible"}
                                     opts)))))

(defn wait-for-text
  "Wait for specific text to appear on the page."
  ([page text]
   (wait-for-text page text {}))
  ([page text opts]
   (let [escaped (-> text
                     (.replace (js/RegExp. "\"" "g") "\\\""))]
     (.waitForSelector page
                       (str "text=\"" escaped "\"")
                       (clj->js (merge {:timeout config/default-timeout
                                        :state   "visible"}
                                       opts))))))

(defn wait-for-url
  "Wait until the page URL matches a pattern (string or regex)."
  ([page url-pattern]
   (wait-for-url page url-pattern {}))
  ([page url-pattern opts]
   (.waitForURL page url-pattern
                (clj->js (merge {:timeout config/default-timeout} opts)))))

(defn wait-for-navigation
  "Wait for a navigation event."
  ([page]
   (wait-for-navigation page {}))
  ([page opts]
   (.waitForNavigation page
                       (clj->js (merge {:timeout config/default-timeout} opts)))))

(defn wait-for-load
  "Wait for the page to reach a specific load state."
  ([page]
   (wait-for-load page "load"))
  ([page state]
   (.waitForLoadState page state)))

(defn wait-ms
  "Wait for a fixed number of milliseconds."
  [ms]
  (js/Promise. (fn [resolve _] (js/setTimeout resolve ms))))

;; ---------------------------------------------------------------------------
;; File upload
;; ---------------------------------------------------------------------------

(defn set-input-files
  "Set files for a file input element."
  [page selector file-paths]
  (.setInputFiles page selector (clj->js file-paths)))

;; ---------------------------------------------------------------------------
;; Debug
;; ---------------------------------------------------------------------------

(defn screenshot!
  "Take a screenshot for debugging."
  ([page]
   (screenshot! page "debug-screenshot.png"))
  ([page filename]
   (.screenshot page #js {:path     (str "screenshots/" filename)
                          :fullPage true})))

(defn page-url
  "Get the current page URL."
  [page]
  (.url page))

(defn evaluate
  "Evaluate JavaScript in the page context."
  [page js-code]
  (.evaluate page js-code))

(defn send-message!
  "Send a message in the current channel via direct POST to /web/messages.
   Bypasses Datastar signal binding (which Playwright can't reliably trigger).
   Uses the browser's session cookie for auth."
  [page channel-id message-text]
  (evaluate page
    (str "fetch('/web/messages', {"
         "  method: 'POST',"
         "  headers: {"
         "    'Content-Type': 'application/x-www-form-urlencoded',"
         "    'X-Channel-Id': '" channel-id "'"
         "  },"
         "  body: 'messageText=' + encodeURIComponent('" (-> message-text
                                                             (.replace "'" "\\'")
                                                             (.replace "\n" "\\n")) "')"
         "}).then(async r => ({status: r.status, ok: r.ok, body: await r.text().catch(() => '')}))")))

(defn get-active-channel-id
  "Get the active channel ID from the page's Datastar signals or URL."
  [page]
  (evaluate page
    "(() => {
       // Try to get from data-signals attribute
       const el = document.querySelector('[data-signals]');
       if (el) {
         try {
           const signals = JSON.parse(el.getAttribute('data-signals').replace(/'/g, '\"'));
           if (signals.activeChannel) return signals.activeChannel;
         } catch(e) {}
       }
       // Fallback: get from the sidebar active channel
       const active = document.querySelector('[id^=\"sidebar-channel-\"].bg-hover, [id^=\"sidebar-channel-\"].bg-surface1');
       if (active) return active.id.replace('sidebar-channel-', '');
       // Last fallback: first channel in sidebar
       const first = document.querySelector('[id^=\"sidebar-channel-\"]');
       if (first) return first.id.replace('sidebar-channel-', '');
       return null;
     })()"))
