(ns ajet.chat.auth-gw.setup-integration-test
  "Integration tests for the admin setup wizard and OAuth provider management.

   Tests the complete first-deployment bootstrap through the Ring handler:
   1. Configure OAuth providers (no auth)
   2. OAuth login (mocked external HTTP) creates first user
   3. Create community (auth required) completes setup

   Uses mock upstream servers for internal API calls and with-redefs
   for external OAuth provider HTTP calls."
  (:require [clojure.test :refer [deftest testing is use-fixtures]]
            [clojure.data.json :as json]
            [clojure.string :as str]
            [babashka.http-client :as http]
            [ajet.chat.auth-gw.auth :as auth]
            [ajet.chat.auth-gw.routes :as routes]
            [ajet.chat.auth-gw.rate-limit :as rl]
            [ajet.chat.shared.db :as db]
            [ajet.chat.test-helpers :as th]
            [ring.util.codec :as codec])
  (:import [java.net ServerSocket]
           [java.io ByteArrayInputStream]))

;;; ---------------------------------------------------------------------------
;;; Mock upstream servers
;;; ---------------------------------------------------------------------------

(defn- find-free-port []
  (with-open [sock (ServerSocket. 0)]
    (.getLocalPort sock)))

(defn- api-mock-handler
  "Mock API server that handles community creation."
  [request]
  (let [uri  (:uri request)
        meth (:request-method request)]
    (if (and (= :post meth) (= "/api/communities" uri))
      (let [body-str (when (:body request) (slurp (:body request)))
            body     (when body-str (json/read-str body-str :key-fn keyword))]
        {:status  201
         :headers {"Content-Type" "application/json"}
         :body    (json/write-str {:id   (str (java.util.UUID/randomUUID))
                                   :name (:name body)
                                   :slug (:slug body)})})
      {:status  200
       :headers {"Content-Type" "application/json"}
       :body    (json/write-str {:method (name meth) :uri uri})})))

(defn- web-mock-handler
  "Mock Web SM that echoes the request URI."
  [request]
  {:status  200
   :headers {"Content-Type" "text/html"}
   :body    (str "<html>Web SM handled: " (:uri request) "</html>")})

(def ^:private ^:dynamic *api-port* nil)
(def ^:private ^:dynamic *web-port* nil)

(defn- start-mock-server! [port handler]
  (org.httpkit.server/run-server handler {:port port :legacy-return-value? true}))

(defn with-mock-upstreams [f]
  (let [api-port (find-free-port)
        web-port (find-free-port)
        api-stop (start-mock-server! api-port api-mock-handler)
        web-stop (start-mock-server! web-port web-mock-handler)]
    (try
      (binding [*api-port* api-port
                *web-port* web-port]
        (f))
      (finally
        (api-stop)
        (web-stop)))))

;;; ---------------------------------------------------------------------------
;;; Fixtures
;;; ---------------------------------------------------------------------------

(use-fixtures :once th/with-db-suite with-mock-upstreams)
(use-fixtures :each th/with-clean-db)

;;; ---------------------------------------------------------------------------
;;; Test config & system builder
;;; ---------------------------------------------------------------------------

(defn- test-config []
  {:services   {:api    {:host "localhost" :port *api-port*}
                :web-sm {:host "localhost" :port *web-port*}
                :tui-sm {:host "localhost" :port *web-port*}}
   :session    {:ttl-days 30 :cookie-name "ajet_session" :cookie-secure false}
   :rate-limit {:enabled false}
   :cors       {:allowed-origins ["http://localhost:3000"]}
   :oauth      {}})

(defn- make-handler
  "Build the auth-gw Ring handler with test config.
   Accepts optional config overrides, providers atom, and setup-complete atom."
  ([] (make-handler {} (atom []) (atom nil)))
  ([config-overrides] (make-handler config-overrides (atom []) (atom nil)))
  ([config-overrides providers-atom setup-atom]
   (let [config  (merge (test-config) config-overrides)
         limiter (rl/make-limiter)
         sys     {:ds                   th/*ds*
                  :config               config
                  :limiter              limiter
                  :oauth-providers-atom providers-atom
                  :setup-complete-atom  setup-atom}]
     (routes/app sys))))

;;; ---------------------------------------------------------------------------
;;; Request helpers
;;; ---------------------------------------------------------------------------

(defn- make-request
  "Build a Ring request map. Splits query string from URI automatically."
  [method uri & [{:keys [cookie headers body content-type]}]]
  (let [[path query] (str/split uri #"\?" 2)]
    (cond-> {:request-method method
             :uri            path
             :query-string   query
             :headers        (merge {} headers)
             :remote-addr    "127.0.0.1"
             :server-name    "localhost"
             :server-port    3000
             :scheme         :http}
      cookie       (assoc-in [:headers "cookie"] cookie)
      body         (assoc :body (ByteArrayInputStream.
                                  (.getBytes ^String body "UTF-8")))
      content-type (assoc-in [:headers "content-type"] content-type))))

(defn- form-request
  "Build a POST request with URL-encoded form body."
  [uri form-params & [{:keys [cookie]}]]
  (make-request :post uri
    {:cookie       cookie
     :body         (codec/form-encode form-params)
     :content-type "application/x-www-form-urlencoded"}))

(defn- extract-location [resp]
  (get-in resp [:headers "Location"]))

(defn- extract-set-cookie
  "Extract a cookie value from Set-Cookie response header(s)."
  [resp cookie-name]
  (let [raw    (get-in resp [:headers "Set-Cookie"])
        values (cond (string? raw)     [raw]
                     (sequential? raw) raw
                     :else             [])]
    (some (fn [c]
            (when (str/starts-with? c (str cookie-name "="))
              (second (re-find (re-pattern (str cookie-name "=([^;]+)")) c))))
          values)))

(defn- insert-test-provider!
  "Insert an OAuth provider directly via DB. Returns the row."
  [& [{:keys [provider-type slug display-name]
       :or   {provider-type "github" slug "github" display-name "GitHub"}}]]
  (auth/insert-oauth-provider! th/*ds*
    {:provider-type provider-type
     :display-name  display-name
     :slug          slug
     :client-id     "test-client-id"
     :client-secret "test-client-secret"}))

(defn- create-session-cookie!
  "Create a user + session. Returns {:user :cookie}."
  []
  (let [user  (th/create-user!)
        token (auth/create-session! th/*ds* (:id user) 30)]
    {:user user :cookie (str "ajet_session=" token)}))

;;; ---------------------------------------------------------------------------
;;; Mock OAuth responses
;;; ---------------------------------------------------------------------------

(defn- github-mock-post
  "Mock HTTP POST for GitHub token exchange."
  [orig-post]
  (fn [url opts]
    (if (str/includes? url "github.com/login/oauth/access_token")
      {:status 200
       :body   (json/write-str {:access_token "mock-access-token"
                                :token_type   "bearer"})}
      (orig-post url opts))))

(defn- github-mock-get
  "Mock HTTP GET for GitHub user profile + emails."
  [orig-get]
  (fn [url opts]
    (cond
      (str/includes? url "api.github.com/user/emails")
      {:status 200
       :body   (json/write-str [{:email    "admin@test.com"
                                  :primary  true
                                  :verified true}])}

      (str/includes? url "api.github.com/user")
      {:status 200
       :body   (json/write-str {:id         12345
                                :login      "adminuser"
                                :name       "Admin User"
                                :avatar_url "https://example.com/avatar.png"})}

      :else (orig-get url opts))))

(defmacro with-github-oauth-mock
  "Execute body with GitHub OAuth HTTP calls mocked."
  [& body]
  `(let [orig-post# http/post
         orig-get#  http/get]
     (with-redefs [http/post (github-mock-post orig-post#)
                   http/get  (github-mock-get orig-get#)]
       ~@body)))

;;; ---------------------------------------------------------------------------
;;; Setup wizard: provider configuration
;;; ---------------------------------------------------------------------------

(deftest setup-shows-provider-config-when-fresh
  (testing "GET /setup with no providers and no users shows the provider config form"
    (let [handler (make-handler)
          resp    (handler (make-request :get "/setup"))]
      (is (= 200 (:status resp)))
      (is (string? (:body resp)))
      (is (re-find #"(?i)configure.*oauth|oauth.*provider" (:body resp)))
      (is (re-find #"/setup/providers" (:body resp))))))

(deftest setup-add-provider-inserts-and-redirects
  (testing "POST /setup/providers adds a GitHub provider and redirects to /setup"
    (let [providers-atom (atom [])
          setup-atom     (atom nil)
          handler        (make-handler {} providers-atom setup-atom)
          resp           (handler (form-request "/setup/providers"
                                   {"provider-type" "github"
                                    "display-name"  "GitHub"
                                    "slug"          "github"
                                    "client-id"     "test-cid"
                                    "client-secret"  "test-csec"}))]
      (is (= 302 (:status resp)))
      (is (= "/setup" (extract-location resp)))
      ;; Verify provider was created in DB
      (let [providers (db/execute! th/*ds* {:select [:*] :from [:oauth-providers]})]
        (is (= 1 (count providers)))
        (is (= "github" (:provider-type (first providers)))))
      ;; Verify atom was reloaded
      (is (= 1 (count @providers-atom))))))

(deftest setup-add-provider-validates-required-fields
  (testing "POST /setup/providers with missing fields shows error"
    (let [handler (make-handler)
          resp    (handler (form-request "/setup/providers"
                             {"provider-type" "github"
                              "display-name"  ""
                              "slug"          ""
                              "client-id"     ""
                              "client-secret" ""}))]
      (is (= 200 (:status resp)))
      (is (re-find #"(?i)required" (:body resp))))))

(deftest setup-add-provider-validates-type
  (testing "POST /setup/providers with invalid type shows error"
    (let [handler (make-handler)
          resp    (handler (form-request "/setup/providers"
                             {"provider-type" "invalid"
                              "display-name"  "Bad"
                              "slug"          "bad"
                              "client-id"     "x"
                              "client-secret" "y"}))]
      (is (= 200 (:status resp)))
      (is (re-find #"(?i)invalid.*provider" (:body resp))))))

(deftest setup-add-gitea-requires-base-url
  (testing "POST /setup/providers for Gitea without base-url shows error"
    (let [handler (make-handler)
          resp    (handler (form-request "/setup/providers"
                             {"provider-type" "gitea"
                              "display-name"  "Gitea"
                              "slug"          "gitea"
                              "client-id"     "x"
                              "client-secret" "y"}))]
      (is (= 200 (:status resp)))
      (is (re-find #"(?i)base.*url" (:body resp))))))

(deftest setup-add-oidc-requires-issuer-url
  (testing "POST /setup/providers for OIDC without issuer-url shows error"
    (let [handler (make-handler)
          resp    (handler (form-request "/setup/providers"
                             {"provider-type" "oidc"
                              "display-name"  "SSO"
                              "slug"          "sso"
                              "client-id"     "x"
                              "client-secret" "y"}))]
      (is (= 200 (:status resp)))
      (is (re-find #"(?i)issuer.*url" (:body resp))))))

(deftest setup-delete-provider-removes-from-db
  (testing "POST /setup/providers/:id/delete removes the provider"
    (let [provider      (insert-test-provider!)
          providers-atom (atom [provider])
          setup-atom     (atom nil)
          handler        (make-handler {} providers-atom setup-atom)
          resp           (handler (make-request :post
                                   (str "/setup/providers/" (:id provider) "/delete")))]
      (is (= 302 (:status resp)))
      (is (= "/setup" (extract-location resp)))
      (let [remaining (db/execute! th/*ds* {:select [:*] :from [:oauth-providers]})]
        (is (zero? (count remaining)))))))

;;; ---------------------------------------------------------------------------
;;; Setup wizard: state-based redirects
;;; ---------------------------------------------------------------------------

(deftest setup-redirects-to-login-when-providers-exist-no-users
  (testing "GET /setup redirects to /auth/login when providers configured but no users"
    (insert-test-provider!)
    (let [providers (auth/list-oauth-providers th/*ds*)
          handler   (make-handler {} (atom providers) (atom nil))
          resp      (handler (make-request :get "/setup"))]
      (is (= 302 (:status resp)))
      (is (= "/auth/login" (extract-location resp))))))

(deftest setup-shows-community-form-when-user-authenticated
  (testing "GET /setup shows community creation form when user exists and setup incomplete"
    (insert-test-provider!)
    (let [{:keys [cookie]} (create-session-cookie!)
          providers        (auth/list-oauth-providers th/*ds*)
          handler          (make-handler {} (atom providers) (atom nil))
          resp             (handler (make-request :get "/setup" {:cookie cookie}))]
      (is (= 200 (:status resp)))
      (is (re-find #"(?i)create.*community" (:body resp))))))

(deftest setup-redirects-home-when-complete
  (testing "GET /setup proxies to Web SM (not wizard) when setup is complete"
    (let [{:keys [cookie]} (create-session-cookie!)
          handler          (make-handler {} (atom []) (atom :true))
          resp             (handler (make-request :get "/setup" {:cookie cookie}))]
      ;; Should proxy to Web SM mock, not render wizard
      (is (= 200 (:status resp)))
      (is (not (re-find #"/setup/providers" (or (:body resp) "")))))))

;;; ---------------------------------------------------------------------------
;;; Login page integration
;;; ---------------------------------------------------------------------------

(deftest login-page-shows-configured-providers
  (testing "GET /auth/login shows OAuth buttons for configured providers"
    (insert-test-provider! {:slug "my-github" :display-name "My GitHub"})
    (let [providers (auth/list-oauth-providers th/*ds*)
          handler   (make-handler {} (atom providers) (atom nil))
          resp      (handler (make-request :get "/auth/login"))]
      (is (= 200 (:status resp)))
      (is (re-find #"My GitHub" (:body resp)))
      (is (re-find #"provider=my-github" (:body resp))))))

(deftest login-page-redirects-to-setup-when-fresh
  (testing "GET /auth/login redirects to /setup when no providers and no users"
    (let [handler (make-handler {} (atom []) (atom nil))
          resp    (handler (make-request :get "/auth/login"))]
      (is (= 302 (:status resp)))
      (is (= "/setup" (extract-location resp))))))

(deftest login-page-redirects-to-github-authorize
  (testing "GET /auth/login?provider=github redirects to GitHub OAuth authorize URL"
    (insert-test-provider! {:slug "github" :display-name "GitHub"})
    (let [providers (auth/list-oauth-providers th/*ds*)
          handler   (make-handler {} (atom providers) (atom nil))
          resp      (handler (make-request :get "/auth/login?provider=github"))]
      (is (= 302 (:status resp)))
      (let [loc (extract-location resp)]
        (is (str/starts-with? loc "https://github.com/login/oauth/authorize"))
        (is (str/includes? loc "client_id=test-client-id"))
        (is (str/includes? loc "state="))))))

;;; ---------------------------------------------------------------------------
;;; OAuth callback (mocked external HTTP)
;;; ---------------------------------------------------------------------------

(deftest oauth-callback-creates-first-user-and-redirects-to-setup
  (testing "OAuth callback for first user creates user+session and redirects to /setup"
    (insert-test-provider! {:slug "github" :display-name "GitHub"})
    (let [providers      (auth/list-oauth-providers th/*ds*)
          providers-atom (atom providers)
          setup-atom     (atom nil)
          handler        (make-handler {} providers-atom setup-atom)

          ;; Initiate OAuth flow to get a valid state parameter
          login-resp (handler (make-request :get "/auth/login?provider=github"))
          location   (extract-location login-resp)
          state      (second (re-find #"state=([^&]+)" location))]

      (is (some? state) "state parameter should be present in redirect URL")

      ;; Simulate callback with mocked GitHub HTTP calls
      (with-github-oauth-mock
        (let [callback-resp (handler (make-request :get
                                       (str "/auth/callback/github?code=mockcode&state=" state)))
              redirect-to   (extract-location callback-resp)]

          ;; First user during setup → redirect to /setup (not /)
          (is (= 302 (:status callback-resp)))
          (is (= "/setup" redirect-to))

          ;; User was created in DB
          (let [users (db/execute! th/*ds* {:select [:*] :from [:users]})]
            (is (= 1 (count users)))
            (is (= "adminuser" (:username (first users)))))

          ;; OAuth account was linked
          (let [accounts (db/execute! th/*ds* {:select [:*] :from [:oauth-accounts]})]
            (is (= 1 (count accounts)))
            (is (= "github" (:provider (first accounts)))))

          ;; Session cookie was set
          (is (some? (extract-set-cookie callback-resp "ajet_session"))))))))

(deftest oauth-callback-rejects-invalid-state
  (testing "OAuth callback with invalid state redirects to login with error"
    (insert-test-provider! {:slug "github" :display-name "GitHub"})
    (let [providers (auth/list-oauth-providers th/*ds*)
          handler   (make-handler {} (atom providers) (atom nil))
          resp      (handler (make-request :get
                               "/auth/callback/github?code=mockcode&state=bogus-state"))]
      (is (= 302 (:status resp)))
      (is (str/includes? (extract-location resp) "/auth/login?error=")))))

(deftest oauth-callback-rejects-missing-code
  (testing "OAuth callback without code redirects to login with error"
    (insert-test-provider! {:slug "github" :display-name "GitHub"})
    (let [providers (auth/list-oauth-providers th/*ds*)
          handler   (make-handler {} (atom providers) (atom nil))
          resp      (handler (make-request :get "/auth/callback/github?state=foo"))]
      (is (= 302 (:status resp)))
      (is (str/includes? (extract-location resp) "/auth/login?error=")))))

(deftest oauth-callback-rejects-unknown-provider
  (testing "OAuth callback for unknown provider slug redirects with error"
    (let [handler (make-handler {} (atom []) (atom nil))
          resp    (handler (make-request :get
                             "/auth/callback/nonexistent?code=a&state=b"))]
      (is (= 302 (:status resp)))
      (is (str/includes? (extract-location resp) "/auth/login?error=")))))

;;; ---------------------------------------------------------------------------
;;; Community creation
;;; ---------------------------------------------------------------------------

(deftest setup-community-creation-completes-wizard
  (testing "POST /setup/create-community creates community and marks setup complete"
    (insert-test-provider!)
    (let [{:keys [cookie]} (create-session-cookie!)
          providers        (auth/list-oauth-providers th/*ds*)
          providers-atom   (atom providers)
          setup-atom       (atom nil)
          handler          (make-handler {} providers-atom setup-atom)
          resp             (handler (form-request "/setup/create-community"
                                     {"name" "My Team" "slug" "my-team"}
                                     {:cookie cookie}))]
      (is (= 302 (:status resp)))
      (is (= "/" (extract-location resp)))
      ;; Setup marked complete in atom and DB
      (is (= :true @setup-atom))
      (is (= "true" (auth/get-system-setting th/*ds* "setup_completed"))))))

(deftest setup-community-creation-requires-auth
  (testing "POST /setup/create-community without session redirects to login"
    (insert-test-provider!)
    (th/create-user!) ;; User exists (step = :create-community)
    (let [providers (auth/list-oauth-providers th/*ds*)
          handler   (make-handler {} (atom providers) (atom nil))
          resp      (handler (form-request "/setup/create-community"
                               {"name" "Test" "slug" "test"}))]
      (is (= 302 (:status resp)))
      (is (= "/auth/login" (extract-location resp))))))

(deftest setup-community-creation-validates-slug
  (testing "POST /setup/create-community rejects invalid slug"
    (insert-test-provider!)
    (let [{:keys [cookie]} (create-session-cookie!)
          providers        (auth/list-oauth-providers th/*ds*)
          handler          (make-handler {} (atom providers) (atom nil))
          resp             (handler (form-request "/setup/create-community"
                                     {"name" "Bad" "slug" "INVALID"}
                                     {:cookie cookie}))]
      (is (= 200 (:status resp)))
      (is (re-find #"(?i)slug" (:body resp))))))

(deftest setup-community-creation-validates-required-fields
  (testing "POST /setup/create-community with empty fields shows error"
    (insert-test-provider!)
    (let [{:keys [cookie]} (create-session-cookie!)
          providers        (auth/list-oauth-providers th/*ds*)
          handler          (make-handler {} (atom providers) (atom nil))
          resp             (handler (form-request "/setup/create-community"
                                     {"name" "" "slug" ""}
                                     {:cookie cookie}))]
      (is (= 200 (:status resp)))
      (is (re-find #"(?i)required" (:body resp))))))

;;; ---------------------------------------------------------------------------
;;; Post-setup: wizard handlers blocked
;;; ---------------------------------------------------------------------------

(deftest setup-providers-blocked-after-completion
  (testing "POST /setup/providers redirects to / when setup is complete"
    (let [handler (make-handler {} (atom []) (atom :true))
          resp    (handler (form-request "/setup/providers"
                             {"provider-type" "github"
                              "display-name"  "GitHub"
                              "slug"          "github2"
                              "client-id"     "x"
                              "client-secret" "y"}))]
      (is (= 302 (:status resp)))
      (is (= "/" (extract-location resp)))
      ;; Provider was NOT inserted
      (is (zero? (count (db/execute! th/*ds*
                          {:select [:*] :from [:oauth-providers]})))))))

(deftest setup-create-community-blocked-after-completion
  (testing "POST /setup/create-community redirects to / when setup is complete"
    (let [{:keys [cookie]} (create-session-cookie!)
          handler          (make-handler {} (atom []) (atom :true))
          resp             (handler (form-request "/setup/create-community"
                                     {"name" "Test" "slug" "test"}
                                     {:cookie cookie}))]
      (is (= 302 (:status resp)))
      (is (= "/" (extract-location resp))))))

;;; ---------------------------------------------------------------------------
;;; Full wizard flow (end-to-end through the Ring handler)
;;; ---------------------------------------------------------------------------

(deftest full-wizard-flow-add-provider-oauth-login-create-community
  (testing "Complete wizard: add provider -> mock OAuth login -> create community"
    (let [providers-atom (atom [])
          setup-atom     (atom nil)
          handler        (make-handler {} providers-atom setup-atom)]

      ;; 1. GET /setup — shows provider config form (no providers)
      (let [resp (handler (make-request :get "/setup"))]
        (is (= 200 (:status resp)) "Step 1: should show provider config page")
        (is (re-find #"/setup/providers" (:body resp))))

      ;; 2. POST /setup/providers — add a GitHub provider
      (let [resp (handler (form-request "/setup/providers"
                            {"provider-type" "github"
                             "display-name"  "GitHub"
                             "slug"          "github"
                             "client-id"     "test-cid"
                             "client-secret" "test-csec"}))]
        (is (= 302 (:status resp)) "Step 2: should redirect after adding provider")
        (is (= "/setup" (extract-location resp)))
        (is (= 1 (count @providers-atom)) "Atom should have 1 provider"))

      ;; 3. GET /setup — redirects to /auth/login (providers exist, no users)
      (let [resp (handler (make-request :get "/setup"))]
        (is (= 302 (:status resp)) "Step 3: should redirect to login")
        (is (= "/auth/login" (extract-location resp))))

      ;; 4. GET /auth/login — shows GitHub button
      (let [resp (handler (make-request :get "/auth/login"))]
        (is (= 200 (:status resp)) "Step 4: should show login page")
        (is (re-find #"GitHub" (:body resp)))
        (is (re-find #"provider=github" (:body resp))))

      ;; 5. GET /auth/login?provider=github — redirect to GitHub
      (let [login-resp (handler (make-request :get "/auth/login?provider=github"))
            location   (extract-location login-resp)
            state      (second (re-find #"state=([^&]+)" location))]
        (is (= 302 (:status login-resp)) "Step 5: should redirect to GitHub")
        (is (str/starts-with? location "https://github.com/login/oauth/authorize"))
        (is (some? state) "State parameter must be present")

        ;; 6. OAuth callback (mocked) — creates first user, redirects to /setup
        (let [session-cookie-val
              (with-github-oauth-mock
                (let [resp (handler (make-request :get
                                      (str "/auth/callback/github?code=abc123&state=" state)))]
                  (is (= 302 (:status resp)) "Step 6: should redirect after OAuth")
                  (is (= "/setup" (extract-location resp))
                      "First user redirected to /setup to create community")
                  ;; Verify user was created
                  (is (= 1 (count (db/execute! th/*ds* {:select [:*] :from [:users]}))))
                  (extract-set-cookie resp "ajet_session")))]

          (is (some? session-cookie-val) "Session cookie must be set after OAuth")

          (let [cookie (str "ajet_session=" session-cookie-val)]

            ;; 7. GET /setup with session — shows community creation form
            (let [resp (handler (make-request :get "/setup" {:cookie cookie}))]
              (is (= 200 (:status resp)) "Step 7: should show community form")
              (is (re-find #"(?i)create.*community" (:body resp))))

            ;; 8. POST /setup/create-community — completes wizard
            (let [resp (handler (form-request "/setup/create-community"
                                  {"name" "Test Team" "slug" "test-team"}
                                  {:cookie cookie}))]
              (is (= 302 (:status resp)) "Step 8: should redirect to / after completion")
              (is (= "/" (extract-location resp)))
              (is (= :true @setup-atom) "Setup should be marked complete in atom")
              (is (= "true" (auth/get-system-setting th/*ds* "setup_completed"))
                  "Setup should be marked complete in DB"))

            ;; 9. Verify wizard is no longer accessible
            (let [resp (handler (make-request :get "/setup" {:cookie cookie}))]
              (is (= 200 (:status resp)) "Step 9: /setup should proxy to Web SM")
              (is (not (re-find #"/setup/providers" (or (:body resp) "")))
                  "Wizard provider form should not be shown"))))))))
