;;; test-ellama-transient.el --- Ellama transient tests -*- lexical-binding: t; package-lint-main-file: "../ellama.el"; -*-

;; Copyright (C) 2023-2026  Free Software Foundation, Inc.

;; Author: Sergey Kostyaev <sskostyaev@gmail.com>

;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.

;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:
;;
;; Ellama transient tests.
;;

;;; Code:

(add-to-list 'load-path default-directory)

(require 'cl-lib)
(require 'ellama)
(require 'ellama-context)
(require 'ellama-transient)
(require 'ert)
(require 'llm-ollama)
(require 'llm-openai)

(ert-deftest test-ellama-fill-transient-ollama-model-populates-fields ()
  (let ((provider (make-llm-ollama
                   :chat-model "test-model"
                   :default-chat-temperature 0.2
                   :host "example.org"
                   :port 11000
                   :default-chat-non-standard-params
                   '(("num_ctx" . 8192))))
        (ellama-transient-model-name "")
        (ellama-transient-temperature 0.7)
        (ellama-transient-context-length 4096)
        (ellama-transient-host "localhost")
        (ellama-transient-port 11434))
    (ellama-fill-transient-model provider)
    (should (eq ellama-transient-provider provider))
    (should (equal ellama-transient-model-name "test-model"))
    (should (= ellama-transient-temperature 0.2))
    (should (= ellama-transient-context-length 8192))
    (should (equal ellama-transient-host "example.org"))
    (should (= ellama-transient-port 11000))))

(ert-deftest test-ellama-fill-transient-ollama-model-defaults ()
  (let ((provider (make-llm-ollama
                   :chat-model "model"
                   :default-chat-temperature nil
                   :host "localhost"
                   :port 11434
                   :default-chat-non-standard-params nil))
        (ellama-transient-temperature 0.3)
        (ellama-transient-context-length 123))
    (ellama-fill-transient-model provider)
    (should-not ellama-transient-temperature)
    (should-not ellama-transient-context-length)))

(ert-deftest test-ellama-fill-transient-ollama-model-noop-for-non-ollama ()
  (let ((ellama-transient-model-name "keep-model")
        (ellama-transient-temperature 0.9)
        (ellama-transient-context-length 2222)
        (ellama-transient-host "keep-host")
        (ellama-transient-port 22000))
    (ellama-fill-transient-model :not-ollama-provider)
    (should (equal ellama-transient-model-name "keep-model"))
    (should-not ellama-transient-temperature)
    (should (= ellama-transient-context-length 2222))
    (should (equal ellama-transient-host "keep-host"))
    (should (= ellama-transient-port 22000))))

(ert-deftest test-ellama-fill-transient-model-openai-compatible ()
  (let ((provider (make-llm-openai-compatible
                   :url "http://127.0.0.1:8000/v1"
                   :chat-model "LFM2.5"
                   :key "secret"
                   :default-chat-temperature 0.4))
        (ellama-transient-model-name "")
        (ellama-transient-temperature 0.7)
        (ellama-transient-url nil))
    (ellama-fill-transient-model provider)
    (should (eq ellama-transient-provider provider))
    (should (equal ellama-transient-model-name "LFM2.5"))
    (should (= ellama-transient-temperature 0.4))
    (should (equal ellama-transient-url "http://127.0.0.1:8000/v1"))))

(ert-deftest test-ellama-transient-reset-model-fields-and-descriptions ()
  (let ((ellama-transient-model-name "model")
        (ellama-transient-temperature 0.4)
        (ellama-transient-context-length 8192)
        (ellama-max-tokens 256))
    (ellama-transient-reset-model-fields)
    (should-not ellama-transient-model-name)
    (should-not ellama-transient-temperature)
    (should-not ellama-transient-context-length)
    (should-not ellama-max-tokens)
    (should (equal (ellama-transient-model-description)
                   "Model (default)"))
    (should (equal (ellama-transient-temperature-description)
                   "Temperature (default)"))
    (should (equal (ellama-transient-context-length-description)
                   "Context Length (default)"))
    (should (equal (ellama-transient-max-tokens-description)
                   "Max Tokens (default)"))))

(ert-deftest test-ellama-transient-set-max-tokens ()
  (let ((ellama-max-tokens nil))
    (cl-letf (((symbol-function 'read-string)
               (lambda (&rest _args)
                 "42")))
      (ellama-transient-set-max-tokens)
      (should (= ellama-max-tokens 42))
      (should (equal (ellama-transient-max-tokens-description)
                     "Max Tokens (42)")))
    (cl-letf (((symbol-function 'read-string)
               (lambda (&rest _args)
                 "")))
      (ellama-transient-set-max-tokens)
      (should-not ellama-max-tokens))))

(ert-deftest test-ellama-transient-set-model-keeps-reset-temperature ()
  (let ((ellama-transient-provider :provider)
        (ellama-transient-model-name nil)
        (ellama-transient-temperature nil)
        (ellama-transient-context-length nil))
    (cl-letf (((symbol-function 'ellama-transient-read-model-name)
               (lambda (&rest _args)
                 "new-model")))
      (ellama-transient-set-model)
      (should (equal ellama-transient-model-name "new-model"))
      (should-not ellama-transient-temperature)
      (should-not ellama-transient-context-length)
      (should (equal (ellama-transient-temperature-description)
                     "Temperature (default)")))))

(ert-deftest
    test-ellama-construct-ollama-provider-from-transient-passes-all-params ()
  (let ((ellama-transient-model-name "model-x")
        (ellama-transient-temperature 0.61)
        (ellama-transient-host "localhost")
        (ellama-transient-port 12000)
        (ellama-transient-context-length 16384)
        (ellama-transient-provider nil))
    (let* ((provider (ellama-construct-ollama-provider-from-transient))
           (params
            (seq--into-list
             (llm-ollama-default-chat-non-standard-params provider))))
      (should (llm-ollama-p provider))
      (should (equal (llm-ollama-chat-model provider) "model-x"))
      (should (= (llm-ollama-default-chat-temperature provider) 0.61))
      (should (equal (llm-ollama-host provider) "localhost"))
      (should (= (llm-ollama-port provider) 12000))
      (should (equal params '(("num_ctx" . 16384)))))))

(ert-deftest test-ellama-construct-provider-reset-ollama-model-fields ()
  (let ((base (make-llm-ollama
               :chat-model "old-model"
               :default-chat-temperature 0.9
               :host "example.org"
               :port 11000
               :default-chat-non-standard-params
               '(("num_ctx" . 8192) ("keep_alive" . "5m"))))
        (ellama-transient-model-name nil)
        (ellama-transient-temperature nil)
        (ellama-transient-context-length nil)
        (ellama-transient-host "example.org")
        (ellama-transient-port 11000)
        (ellama-transient-provider nil))
    (let* ((provider (ellama-construct-provider-from-transient base))
           (params
            (seq--into-list
             (llm-ollama-default-chat-non-standard-params provider))))
      (should-not (llm-ollama-chat-model provider))
      (should-not (llm-ollama-default-chat-temperature provider))
      (should-not (assoc "num_ctx" params))
      (should (equal (assoc "keep_alive" params)
                     '("keep_alive" . "5m")))
      (should (equal (llm-ollama-host provider) "example.org"))
      (should (= (llm-ollama-port provider) 11000)))))

(ert-deftest
    test-ellama-construct-provider-from-transient-openai-compatible ()
  (let ((base (make-llm-openai-compatible
               :url "http://old/v1"
               :chat-model "old-model"
               :key "secret"
               :default-chat-temperature 0.1))
        (ellama-transient-model-name "new-model")
        (ellama-transient-temperature 0.8)
        (ellama-transient-url "http://new/v1")
        (ellama-transient-provider nil))
    (let ((provider (ellama-construct-provider-from-transient base)))
      (should (llm-openai-compatible-p provider))
      (should-not (eq provider base))
      (should (equal (llm-openai-compatible-chat-model provider)
                     "new-model"))
      (should (equal (llm-openai-compatible-url provider)
                     "http://new/v1"))
      (should (let ((key (llm-openai-compatible-key provider)))
                (equal (if (functionp key) (funcall key) key) "secret")))
      (should (= (llm-openai-compatible-default-chat-temperature provider)
                 0.8)))))

(ert-deftest
    test-ellama-construct-provider-reset-openai-compatible-model-fields ()
  (let ((base (make-llm-openai-compatible
               :url "http://old/v1"
               :chat-model "old-model"
               :key "secret"
               :default-chat-temperature 0.1))
        (ellama-transient-model-name nil)
        (ellama-transient-temperature nil)
        (ellama-transient-url nil)
        (ellama-transient-provider nil))
    (let ((provider (ellama-construct-provider-from-transient base)))
      (should (equal (llm-openai-compatible-chat-model provider)
                     (llm-openai-compatible-chat-model
                      (make-llm-openai-compatible))))
      (should-not
       (llm-openai-compatible-default-chat-temperature provider))
      (should (equal (llm-openai-compatible-url provider)
                     "http://old/v1"))
      (should (let ((key (llm-openai-compatible-key provider)))
                (equal (if (functionp key) (funcall key) key) "secret"))))))

(ert-deftest test-ellama-transient-provider-candidates-hide-provider-values ()
  (let* ((secret "transient-secret")
         (ellama-provider (make-llm-openai-compatible
                           :url "http://127.0.0.1:8000/v1"
                           :chat-model "model"
                           :key secret))
         (ellama-provider-list '(ellama-provider))
         captured)
    (cl-letf (((symbol-function 'completing-read)
               (lambda (_prompt collection &rest _args)
                 (setq captured collection)
                 (caar collection))))
      (should (eq (ellama-transient--read-provider-symbol) 'ellama-provider))
      (should-not (string-match-p (regexp-quote secret)
                                  (prin1-to-string captured))))))

(ert-deftest test-ellama-transient-set-provider-does-not-pass-provider-arg ()
  (let ((ellama-provider :old-default)
        (received-args :unset))
    (cl-letf (((symbol-function 'completing-read)
               (lambda (&rest _args)
                 (ellama-transient--provider-label 'ellama-provider)))
              ((symbol-function 'ellama-construct-provider-from-transient)
               (lambda (&rest args)
                 (setq received-args args)
                 :new-provider)))
      (ellama-transient-set-provider)
      (should-not received-args)
      (should (eq ellama-provider :new-provider)))))

(ert-deftest test-ellama-transient-set-provider-updates-selected-symbol ()
  (let ((ellama-provider :old-default)
        (ellama-coding-provider :old-coding))
    (cl-letf (((symbol-function 'completing-read)
               (lambda (&rest _args)
                 (ellama-transient--provider-label 'ellama-coding-provider)))
              ((symbol-function 'ellama-construct-provider-from-transient)
               (lambda (&rest _args)
                 :new-provider)))
      (ellama-transient-set-provider)
      (should (eq ellama-coding-provider :new-provider))
      (should (eq ellama-provider :old-default)))))

(ert-deftest
    test-ellama-transient-set-provider-resets-session-only-for-default-provider ()
  (let ((ellama-provider :default-provider)
        (ellama-coding-provider :coding-provider)
        (ellama--current-session-id "session-1")
        (ellama--current-session-uid "uid-1")
        (providers (list (ellama-transient--provider-label 'ellama-provider)
                         (ellama-transient--provider-label
                          'ellama-coding-provider)))
        (values '(:new-default :new-coding)))
    (cl-letf (((symbol-function 'completing-read)
               (lambda (&rest _args)
                 (prog1 (car providers)
                   (setq providers (cdr providers)))))
              ((symbol-function 'ellama-construct-provider-from-transient)
               (lambda (&rest _args)
                 (prog1 (car values)
                   (setq values (cdr values))))))
      (ellama-transient-set-provider)
      (should (eq ellama-provider :new-default))
      (should-not ellama--current-session-id)
      (should-not ellama--current-session-uid)
      (setq ellama--current-session-id "session-2")
      (setq ellama--current-session-uid "uid-2")
      (ellama-transient-set-provider)
      (should (eq ellama-coding-provider :new-coding))
      (should (equal ellama--current-session-id "session-2"))
      (should (equal ellama--current-session-uid "uid-2")))))

(ert-deftest
    test-ellama-transient-model-get-from-current-session-guard-and-provider-flow
    ()
  (let ((called 0))
    (cl-letf (((symbol-function 'ellama-get-current-session)
               (lambda () nil))
              ((symbol-function 'ellama-fill-transient-model)
               (lambda (&rest _args)
                 (cl-incf called))))
      (ellama-transient-model-get-from-current-session)
      (should (= called 0))))
  (let ((session (make-ellama-session :provider :session-provider))
        provided-session
        provided-provider)
    (cl-letf (((symbol-function 'ellama-get-current-session)
               (lambda ()
                 (setq provided-session session)
                 session))
              ((symbol-function 'ellama-fill-transient-model)
               (lambda (provider)
                 (setq provided-provider provider))))
      (ellama-transient-model-get-from-current-session)
      (should (eq provided-session session))
      (should (eq provided-provider :session-provider)))))

(ert-deftest test-ellama-transient-set-system-region-and-prompt-paths ()
  (let ((ellama-global-system "old"))
    (with-temp-buffer
      (insert "alpha beta")
      (goto-char (point-min))
      (set-mark (point))
      (goto-char (+ (point-min) 5))
      (activate-mark)
      (ellama-transient-set-system)
      (should (equal ellama-global-system "alpha"))))
  (let ((ellama-global-system "old"))
    (cl-letf (((symbol-function 'read-string)
               (lambda (&rest _args)
                 "from prompt")))
      (ellama-transient-set-system)
      (should (equal ellama-global-system "from prompt"))))
  (let ((ellama-global-system "keep"))
    (cl-letf (((symbol-function 'read-string)
               (lambda (&rest _args)
                 "")))
      (ellama-transient-set-system)
      (should-not ellama-global-system))))

(ert-deftest test-ellama-transient-system-show-uses-first-line-and-limit ()
  (let ((ellama-global-system "abcdefghij\nsecond line")
        (ellama-transient-system-show-limit 5))
    (should (equal (ellama-transient-system-show)
                   "System message (abcde)"))))

(ert-deftest test-ellama-context-summary-counts-and-marks-quoted-elements ()
  (let ((ellama-context-global '(:global))
        (ellama-context-ephemeral '(:ephemeral)))
    (cl-letf (((symbol-function 'ellama-context-element-extract)
               (lambda (element)
                 (if (eq element :global)
                     "hello"
                   "cat")))
              ((symbol-function 'ellama-context-element-display)
               (lambda (element)
                 (if (eq element :global)
                     "Global file"
                   "Selection")))
              ((symbol-function 'ellama-context-element-quote-p)
               (lambda (element)
                 (eq element :ephemeral))))
      (let ((summary (ellama--context-summary)))
        (should (string-match-p (regexp-quote "Global file") summary))
        (should (string-match-p
                 (regexp-quote "Selection (3 chars region)")
                 summary))
        (should (string-match-p
                 (regexp-quote "(total 8 chars)")
                 summary))))))

(ert-deftest test-ellama-transient-arg-forwarding ()
  (let ((args '("--new-session" "--ephemeral"))
        code-review-call
        ask-line-call
        ask-selection-call
        ask-about-call
        ask-image-call
        chat-image-call
        chat-calls)
    (cl-letf (((symbol-function 'ellama-code-review)
               (lambda (new-session &rest rest)
                 (setq code-review-call (list new-session rest))))
              ((symbol-function 'ellama-ask-line)
               (lambda (new-session &rest rest)
                 (setq ask-line-call (list new-session rest))))
              ((symbol-function 'ellama-ask-selection)
               (lambda (new-session &rest rest)
                 (setq ask-selection-call (list new-session rest))))
              ((symbol-function 'ellama-ask-about)
               (lambda (new-session &rest rest)
                 (setq ask-about-call (list new-session rest))))
              ((symbol-function 'ellama-ask-image)
               (lambda (image prompt new-session &rest rest)
                 (setq ask-image-call (list image prompt new-session rest))))
              ((symbol-function 'ellama-chat-with-image)
               (lambda (image prompt new-session &rest rest)
                 (setq chat-image-call (list image prompt new-session rest))))
              ((symbol-function 'ellama-chat)
               (lambda (prompt new-session &rest rest)
                 (push (list prompt new-session rest) chat-calls)))
              ((symbol-function 'read-string)
               (lambda (&rest _args)
                 "Ask me"))
              ((symbol-function 'read-file-name)
               (lambda (&rest _args)
                 "/tmp/image.png")))
      (ellama-transient-code-review args)
      (ellama-transient-ask-line args)
      (ellama-transient-ask-selection args)
      (ellama-transient-ask-about args)
      (ellama-transient-ask-image args)
      (ellama-transient-chat args)
      (ellama-transient-chat-with-image args))
    (should (equal code-review-call '(t (:ephemeral t))))
    (should (equal ask-line-call '(t (:ephemeral t))))
    (should (equal ask-selection-call '(t (:ephemeral t))))
    (should (equal ask-about-call '(t (:ephemeral t))))
    (should (equal ask-image-call
                   '("/tmp/image.png" "Ask me" t (:ephemeral t))))
    (should (equal chat-image-call
                   '("/tmp/image.png" "Ask me" t (:ephemeral t))))
    (should (equal (nreverse chat-calls)
                   '(("Ask me" t (:ephemeral t)))))))

(ert-deftest test-ellama-transient-add-image-uses-ephemeral-flag ()
  (let (calls)
    (cl-letf (((symbol-function 'ellama-context-add-image)
               (lambda (&optional scope)
                 (push scope calls))))
      (ellama-transient-add-image nil)
      (ellama-transient-add-image '("--ephemeral")))
    (should (equal (nreverse calls) '(nil ephemeral)))))

(ert-deftest
    test-ellama-transient-main-menu-initializes-model-only-when-empty ()
  (let ((ellama-provider :provider)
        (ellama-transient-model-name "")
        (ellama-transient-provider nil)
        (fill-calls 0)
        fill-provider)
    (cl-letf (((symbol-function 'transient-setup)
               (lambda (&rest _args) nil))
              ((symbol-function 'ellama-fill-transient-model)
               (lambda (provider)
                 (cl-incf fill-calls)
                 (setq fill-provider provider))))
      (ellama-transient-main-menu)
      (should (= fill-calls 1))
      (should (eq fill-provider :provider))
      (setq ellama-transient-model-name "model-already-set")
      (ellama-transient-main-menu)
      (should (= fill-calls 1)))))

(provide 'test-ellama-transient)

;;; test-ellama-transient.el ends here
