(ns lazygitclj.git-test
  "Unit tests for lazygitclj.git namespace"
  (:require [clojure.test :refer [deftest testing is use-fixtures]]
            [lazygitclj.git :as git]
            [clojure.string :as str]
            [babashka.process :refer [shell]]))

;; === Test Fixtures ===

(def ^:dynamic *test-repo* nil)

(defn- sh [& args]
  (try
    (-> (apply shell {:out :string :err :string :dir *test-repo*} args)
        :out
        str/trim-newline)
    (catch Exception _ nil)))

(defn create-test-repo []
  (let [repo-dir (str "/tmp/lazygitclj-unit-test-" (System/currentTimeMillis))]
    (shell "mkdir" "-p" repo-dir)
    (shell {:dir repo-dir} "git" "init" "-b" "main")
    (shell {:dir repo-dir} "git" "config" "user.email" "test@example.com")
    (shell {:dir repo-dir} "git" "config" "user.name" "Test User")
    ;; Create initial files
    (spit (str repo-dir "/README.md") "# Test Project\n")
    (spit (str repo-dir "/file1.txt") "line1\n")
    (shell {:dir repo-dir} "git" "add" ".")
    (shell {:dir repo-dir} "git" "commit" "-m" "Initial commit")
    repo-dir))

(defn delete-test-repo [repo-dir]
  (shell "rm" "-rf" repo-dir))

(defn with-test-repo [f]
  (let [repo-dir (create-test-repo)]
    (try
      (binding [*test-repo* repo-dir]
        ;; Change to test repo for git commands
        (let [original-dir (System/getProperty "user.dir")]
          (System/setProperty "user.dir" repo-dir)
          (try
            (f)
            (finally
              (System/setProperty "user.dir" original-dir)))))
      (finally
        (delete-test-repo repo-dir)))))

(use-fixtures :each with-test-repo)

;; === Status Tests ===

(deftest test-current-branch
  (testing "returns current branch name"
    (is (= "main" (sh "git" "branch" "--show-current")))))

(deftest test-head-sha
  (testing "returns short SHA of HEAD"
    (let [sha (sh "git" "rev-parse" "--short" "HEAD")]
      (is (string? sha))
      (is (>= (count sha) 7)))))

(deftest test-repo-root
  (testing "returns repo root directory"
    (let [root (sh "git" "rev-parse" "--show-toplevel")]
      (is (string? root))
      (is (str/starts-with? root "/tmp/lazygitclj-unit-test-")))))

;; === File Status Tests ===

(deftest test-staged-files
  (testing "returns empty list when nothing staged"
    ;; Modify file but don't stage
    (spit (str *test-repo* "/file1.txt") "modified\n")
    (let [staged (git/staged-files)]
      (is (empty? staged))))

  (testing "returns staged files after git add"
    (spit (str *test-repo* "/file1.txt") "modified\n")
    (sh "git" "add" "file1.txt")
    (let [staged (git/staged-files)]
      (is (= 1 (count staged)))
      (is (= "file1.txt" (:path (first staged)))))))

(deftest test-unstaged-files
  (testing "returns modified unstaged files"
    (spit (str *test-repo* "/file1.txt") "modified\n")
    (let [unstaged (git/unstaged-files)]
      (is (= 1 (count unstaged)))
      (is (= "file1.txt" (:path (first unstaged))))))

  (testing "returns untracked files"
    (spit (str *test-repo* "/newfile.txt") "new content\n")
    (let [unstaged (git/unstaged-files)]
      (is (some #(= "newfile.txt" (:path %)) unstaged)))))

;; === Branch Tests ===

(deftest test-branches
  (testing "returns list of local branches"
    (let [branches (git/branches)]
      (is (vector? branches))
      (is (some #(= "main" %) branches))))

  (testing "includes newly created branches"
    (sh "git" "branch" "feature-branch")
    (let [branches (git/branches)]
      (is (some #(= "feature-branch" %) branches)))))

(deftest test-create-branch
  (testing "creates a new branch and switches to it"
    (git/create-branch "new-feature")
    (is (= "new-feature" (sh "git" "branch" "--show-current")))))

(deftest test-checkout-branch
  (testing "switches to existing branch"
    (sh "git" "branch" "other-branch")
    (git/checkout-branch "other-branch")
    (is (= "other-branch" (sh "git" "branch" "--show-current")))))

(deftest test-delete-branch
  (testing "deletes a branch"
    (sh "git" "branch" "to-delete")
    (git/delete-branch "to-delete")
    (let [branches (git/branches)]
      (is (not (some #(= "to-delete" %) branches))))))

(deftest test-rename-branch
  (testing "renames a branch"
    (sh "git" "branch" "old-name")
    (git/rename-branch "old-name" "new-name")
    (let [branches (git/branches)]
      (is (some #(= "new-name" %) branches))
      (is (not (some #(= "old-name" %) branches))))))

;; === Staging Tests ===

(deftest test-stage-file
  (testing "stages a modified file"
    (spit (str *test-repo* "/file1.txt") "modified\n")
    (git/stage-file "file1.txt")
    (let [staged (git/staged-files)]
      (is (= 1 (count staged))))))

(deftest test-unstage-file
  (testing "unstages a staged file"
    (spit (str *test-repo* "/file1.txt") "modified\n")
    (sh "git" "add" "file1.txt")
    (git/unstage-file "file1.txt")
    (let [staged (git/staged-files)]
      (is (empty? staged)))))

(deftest test-stage-all
  (testing "stages all changed files"
    (spit (str *test-repo* "/file1.txt") "modified\n")
    (spit (str *test-repo* "/newfile.txt") "new\n")
    (git/stage-all)
    (let [staged (git/staged-files)]
      (is (= 2 (count staged))))))

;; === Commit Tests ===

(deftest test-commit
  (testing "creates a commit with message"
    (spit (str *test-repo* "/file1.txt") "modified\n")
    (sh "git" "add" ".")
    (git/commit "Test commit message")
    (let [log (sh "git" "log" "--oneline" "-1")]
      (is (str/includes? log "Test commit message")))))

(deftest test-recent-commits
  (testing "returns recent commits"
    (let [commits (git/recent-commits 5)]
      (is (seq commits))
      (is (= "Initial commit" (:subject (first commits)))))))

;; === Stash Tests ===

(deftest test-stash-all
  (testing "stashes all changes"
    (spit (str *test-repo* "/file1.txt") "modified\n")
    (git/stash-all)
    (let [stashes (git/stash-list)]
      (is (= 1 (count stashes))))
    ;; Working tree should be clean
    (let [unstaged (git/unstaged-files)]
      (is (empty? unstaged)))))

(deftest test-stash-pop
  (testing "pops stash and removes it from list"
    (spit (str *test-repo* "/file1.txt") "modified\n")
    (git/stash-all)
    (git/stash-pop)
    (let [stashes (git/stash-list)
          unstaged (git/unstaged-files)]
      (is (empty? stashes))
      (is (= 1 (count unstaged))))))

(deftest test-stash-apply
  (testing "applies stash but keeps it in list"
    (spit (str *test-repo* "/file1.txt") "modified\n")
    (git/stash-all)
    (git/stash-apply)
    (let [stashes (git/stash-list)
          unstaged (git/unstaged-files)]
      (is (= 1 (count stashes)))
      (is (= 1 (count unstaged))))))

(deftest test-stash-drop
  (testing "drops stash from list"
    (spit (str *test-repo* "/file1.txt") "modified\n")
    (git/stash-all)
    (git/stash-drop)
    (let [stashes (git/stash-list)]
      (is (empty? stashes)))))

;; === Reset Tests ===

(deftest test-discard-file
  (testing "discards unstaged changes to a file"
    (spit (str *test-repo* "/file1.txt") "modified\n")
    (git/discard-file "file1.txt")
    (let [content (slurp (str *test-repo* "/file1.txt"))]
      (is (= "line1\n" content)))))

(deftest test-discard-all-unstaged
  (testing "discards all unstaged changes"
    (spit (str *test-repo* "/file1.txt") "modified\n")
    (git/discard-all-unstaged)
    (let [unstaged (git/unstaged-files)]
      (is (empty? unstaged)))))

(deftest test-reset-staged
  (testing "unstages all staged files"
    (spit (str *test-repo* "/file1.txt") "modified\n")
    (sh "git" "add" ".")
    (git/reset-staged)
    (let [staged (git/staged-files)]
      (is (empty? staged)))))

;; === Tag Tests ===

(deftest test-list-tags
  (testing "returns empty list when no tags"
    (let [tags (git/list-tags)]
      (is (empty? tags))))

  (testing "returns tags after creating one"
    (sh "git" "tag" "v1.0.0")
    (let [tags (git/list-tags)]
      (is (some #(= "v1.0.0" %) tags)))))

(deftest test-create-tag
  (testing "creates a tag"
    (git/create-tag "v2.0.0")
    (let [tags (git/list-tags)]
      (is (some #(= "v2.0.0" %) tags)))))

(deftest test-delete-tag
  (testing "deletes a tag"
    (sh "git" "tag" "to-delete")
    (git/delete-tag "to-delete")
    (let [tags (git/list-tags)]
      (is (not (some #(= "to-delete" %) tags))))))

;; === Reflog Tests ===

(deftest test-reflog
  (testing "returns reflog entries"
    (let [reflog (git/reflog 10)]
      (is (seq reflog))
      (is (every? :sha reflog))
      (is (every? :ref reflog)))))

;; === Cherry-pick and Revert Tests ===

(deftest test-cherry-pick-commit
  (testing "cherry-picks a commit"
    ;; Create a branch with a commit
    (sh "git" "checkout" "-b" "feature")
    (spit (str *test-repo* "/feature.txt") "feature content\n")
    (sh "git" "add" ".")
    (sh "git" "commit" "-m" "Feature commit")
    (let [feature-sha (sh "git" "rev-parse" "--short" "HEAD")]
      ;; Go back to main
      (sh "git" "checkout" "main")
      ;; Cherry-pick
      (git/cherry-pick-commit feature-sha)
      ;; Verify file exists
      (is (.exists (java.io.File. (str *test-repo* "/feature.txt")))))))

(deftest test-revert-commit
  (testing "reverts a commit"
    ;; Make a change and commit
    (spit (str *test-repo* "/file1.txt") "modified\n")
    (sh "git" "add" ".")
    (sh "git" "commit" "-m" "Change to revert")
    (let [sha (sh "git" "rev-parse" "--short" "HEAD")]
      ;; Revert it
      (git/revert-commit sha)
      ;; Verify content is back to original
      (let [content (slurp (str *test-repo* "/file1.txt"))]
        (is (= "line1\n" content))))))

;; === Merge Tests ===

(deftest test-merge-branch
  (testing "merges a branch into current"
    ;; Create feature branch with changes
    (sh "git" "checkout" "-b" "to-merge")
    (spit (str *test-repo* "/merged.txt") "merged content\n")
    (sh "git" "add" ".")
    (sh "git" "commit" "-m" "Merge commit")
    ;; Go back to main
    (sh "git" "checkout" "main")
    ;; Merge
    (git/merge-branch "to-merge")
    ;; Verify file exists on main
    (is (.exists (java.io.File. (str *test-repo* "/merged.txt"))))))

;; Run tests when executed directly
(defn -main [& args]
  (clojure.test/run-tests 'lazygitclj.git-test))
