;; Title: Miniforge.ai
;; Subtitle: An agentic SDLC / fleet-control platform
;; Author: Christopher Lester
;; Line: Founder, Miniforge.ai (project)
;; Copyright 2025-2026 Christopher Lester (christopher@miniforge.ai)
;;
;; Licensed under the Apache License, Version 2.0 (the "License");
;; you may not use this file except in compliance with the License.
;; You may obtain a copy of the License at
;;
;;     http://www.apache.org/licenses/LICENSE-2.0
;;
;; Unless required by applicable law or agreed to in writing, software
;; distributed under the License is distributed on an "AS IS" BASIS,
;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
;; See the License for the specific language governing permissions and
;; limitations under the License.

(ns ai.miniforge.phase-software-factory.review-artifact-resolution-test
  "Tests for resolve-implement-artifact in the review phase.

   Validates artifact resolution across direct serialized artifacts,
   environment-promotion metadata, and worktree fallbacks."
  (:require
   [clojure.test :refer [deftest is testing]]
   [ai.miniforge.agent.interface :as agent]
   [ai.miniforge.phase-software-factory.review]))

;------------------------------------------------------------------------------ Layer 0
;; Factories

(def ^:private resolve-implement-artifact
  "Direct var reference to the private function under test."
  #'ai.miniforge.phase-software-factory.review/resolve-implement-artifact)

(defn- make-serialized-artifact
  "Factory: implement result with an explicit :artifact key (strategy 2)."
  []
  {:code/files [{:path "src/core.clj"
                 :content "(ns core)"
                 :action :create}]
   :code/summary "serialized artifact"})

(defn- make-implement-result-with-artifact
  "Factory: implement result carrying a serialized :artifact."
  []
  {:artifact (make-serialized-artifact)
   :some-other-key "metadata"})

(defn- make-code-files-result
  "Factory: implement result that IS the artifact (has :code/files, strategy 4)."
  []
  {:code/files [{:path "src/widget.clj"
                 :content "(ns widget)"
                 :action :create}]
   :code/summary "code files result"})

(defn- make-bare-result
  "Factory: implement result with no artifact and no :code/files (strategy 7 fallback)."
  []
  {:status :success
   :metrics {:tokens 500}})

(defn- make-ctx
  "Factory: minimal execution context.
   Accepts optional overrides for :execution/worktree-path and :worktree-path."
  ([]
   (make-ctx {}))
  ([overrides]
   (merge {:execution/worktree-path "/tmp/test-worktree"} overrides)))

;------------------------------------------------------------------------------ Layer 1
;; Tests

(deftest resolve-serialized-artifact-test
  (testing "Strategy 2: when implement-result has :artifact key, returns that artifact"
    (let [impl-result (make-implement-result-with-artifact)
          ctx (make-ctx)
          resolved (resolve-implement-artifact impl-result ctx)]
      (is (some? resolved)
          "Should resolve an artifact")
      (is (= (make-serialized-artifact) resolved)
          "Should return the value of the :artifact key")
      (is (= "serialized artifact" (:code/summary resolved))
          "Should preserve the artifact's summary"))))

(deftest resolve-outer-phase-artifact-test
  (testing "metadata-only outer phase artifact rehydrates file contents from the worktree"
    (let [impl-phase-result {:status :completed
                             :artifact {:code/summary "serialized artifact"
                                        :code/scope-deviations ["docs/out-of-scope.md"]
                                        :code/file-paths ["src/core.clj"]}}
          worktree-artifact (make-serialized-artifact)
          ctx (make-ctx)
          resolved (with-redefs [agent/collect-written-files
                                 (fn [_snapshot working-dir]
                                   (is (= "/tmp/test-worktree" working-dir))
                                   worktree-artifact)]
                     (resolve-implement-artifact impl-phase-result ctx))]
      (is (= "serialized artifact" (:code/summary resolved)))
      (is (= ["docs/out-of-scope.md"] (:code/scope-deviations resolved)))
      (is (= 1 (count (:code/files resolved)))))))

(deftest resolve-inner-output-artifact-test
  (testing "environment-model implement phase can carry artifact under [:result :output]"
    (let [artifact (make-code-files-result)
          impl-phase-result {:status :completed
                             :result {:status :success
                                      :output artifact}}
          ctx (make-ctx)
          resolved (resolve-implement-artifact impl-phase-result ctx)]
      (is (= artifact resolved)))))

(deftest resolve-code-files-artifact-test
  (testing "Strategy 4: when implement-result has :code/files, returns the result itself"
    (let [impl-result (make-code-files-result)
          ctx (make-ctx)
          resolved (resolve-implement-artifact impl-result ctx)]
      (is (some? resolved)
          "Should resolve an artifact")
      (is (identical? impl-result resolved)
          "Should return the implement-result itself (not a copy)")
      (is (= "code files result" (:code/summary resolved))
          "Should preserve the original summary"))))

(deftest resolve-strategy-2-takes-precedence-over-strategy-4-test
  (testing "Strategy 2 wins over strategy 4 when both :artifact and :code/files present"
    (let [inner-artifact {:code/files [{:path "inner.clj" :content "inner" :action :create}]
                          :code/summary "inner"}
          impl-result {:artifact inner-artifact
                       :code/files [{:path "outer.clj" :content "outer" :action :create}]
                       :code/summary "outer"}
          ctx (make-ctx)
          resolved (resolve-implement-artifact impl-result ctx)]
      (is (= inner-artifact resolved)
          ":artifact key should take precedence over :code/files on result"))))

(deftest resolve-worktree-fallback-test
  (testing "Strategy 7: when no artifact source has files, falls back to worktree diff"
    (let [fake-artifact {:code/files [{:path "src/new.clj"
                                       :content "(ns new)"
                                       :action :create}]
                         :code/summary "1 files collected from agent working directory (no MCP submit)"}
          impl-result (make-bare-result)
          ctx (make-ctx)]
      (with-redefs [agent/collect-written-files
                    (fn [_snapshot working-dir]
                      (is (= "/tmp/test-worktree" working-dir)
                          "Should pass worktree-path to collect-written-files")
                      fake-artifact)]
        (let [resolved (resolve-implement-artifact impl-result ctx)]
          (is (some? resolved)
              "Should resolve via worktree fallback")
          (is (= fake-artifact resolved)
              "Should return the artifact from collect-written-files"))))))

(deftest resolve-worktree-fallback-uses-worktree-path-key-test
  (testing "Strategy 7 falls back to :worktree-path when :execution/worktree-path is nil"
    (let [fake-artifact {:code/files [] :code/summary "alt path"}
          impl-result (make-bare-result)
          ctx (make-ctx {:execution/worktree-path nil
                         :worktree-path "/tmp/alt-worktree"})]
      (with-redefs [agent/collect-written-files
                    (fn [_snapshot working-dir]
                      (is (= "/tmp/alt-worktree" working-dir)
                          "Should use :worktree-path as fallback")
                      fake-artifact)]
        (let [resolved (resolve-implement-artifact impl-result ctx)]
          (is (some? resolved)
              "Should resolve via :worktree-path fallback"))))))

(deftest resolve-nil-everything-test
  (testing "Returns nil when no strategy succeeds"
    ;; No worktree path means strategy 7 won't even call collect-written-files
    (let [impl-result (make-bare-result)
          ctx (make-ctx {:execution/worktree-path nil
                         :worktree-path nil})
          resolved (resolve-implement-artifact impl-result ctx)]
      (is (nil? resolved)
          "Should return nil when no artifact source is available"))))

(deftest resolve-nil-implement-result-test
  (testing "Handles nil implement-result gracefully"
    (let [ctx (make-ctx {:execution/worktree-path nil})
          resolved (resolve-implement-artifact nil ctx)]
      (is (nil? resolved)
          "Should return nil when implement-result is nil"))))

(deftest resolve-rehydrate-from-paths-test
  (testing "Strategy 1: paths-only outer artifact rehydrates content from worktree"
    (let [outer-artifact {:code/summary "lightweight artifact"
                          :code/file-paths ["src/core.clj" "src/util.clj"]
                          :code/file-actions [:create :modify]
                          :code/file-count 2}
          stale-inner-output {:code/files [{:path "src/core.clj"
                                            :content "(ns stale)"
                                            :action :modify}]
                              :code/summary "stale agent output"}
          impl-result {:status :completed
                       :artifact outer-artifact
                       :result {:output stale-inner-output}}
          ctx (make-ctx)
          rehydrated [{:path "src/core.clj" :content "(ns core)"  :action :create}
                      {:path "src/util.clj" :content "(ns util)" :action :modify}]
          resolved (with-redefs [agent/rehydrate-files
                                 (fn [working-dir paths actions]
                                   (is (= "/tmp/test-worktree" working-dir))
                                   (is (= ["src/core.clj" "src/util.clj"] paths))
                                   (is (= [:create :modify] actions))
                                   rehydrated)
                                 agent/collect-written-files
                                 (fn [_ _]
                                   {:code/files [{:path "should-not-win.clj"
                                                  :content "x" :action :create}]
                                    :code/summary "should-not-win"})]
                     (resolve-implement-artifact impl-result ctx))]
      (is (= rehydrated (:code/files resolved)))
      (is (not= stale-inner-output resolved)
          "Rehydrated promoted worktree content beats stale serialized agent output")
      (is (= "lightweight artifact" (:code/summary resolved)))
      (is (= ["src/core.clj" "src/util.clj"] (:code/file-paths resolved))
          "Outer-artifact metadata is preserved alongside the rehydrated files")))

  (testing "Strategy 1 falls through to strategy 6/7 when rehydration finds no files"
    (let [outer-artifact {:code/summary "lightweight artifact"
                          :code/file-paths ["src/missing.clj"]}
          impl-result {:status :completed :artifact outer-artifact}
          fallback-artifact {:code/files [{:path "src/picked.clj"
                                           :content "(ns picked)"
                                           :action :create}]
                             :code/summary "from worktree"}
          resolved (with-redefs [agent/rehydrate-files (fn [_ _ _] [])
                                 agent/collect-written-files (fn [_ _] fallback-artifact)]
                     (resolve-implement-artifact impl-result (make-ctx)))]
      (is (= [{:path "src/picked.clj" :content "(ns picked)" :action :create}]
             (:code/files resolved))
          "Strategy 6 (worktree merge) takes over when strategy 1 returns nothing")
      (is (= "lightweight artifact" (:code/summary resolved))
          "Outer-artifact metadata still wins on the merge"))))

;------------------------------------------------------------------------------ Rich Comment
(comment
  (clojure.test/run-tests 'ai.miniforge.phase-software-factory.review-artifact-resolution-test)
  :leave-this-here)
