packages feed

seihou-cli-0.4.0.0: test/Seihou/FzfSpec.hs

module Seihou.FzfSpec (tests) where

import Data.Text qualified as T
import Seihou.Core.Module (DiscoveredModule (..), DiscoveredRunnable (..), ModuleSource (..), RunnableKind (..))
import Seihou.Core.Types
import Seihou.Effect.Fzf (selectOne)
import Seihou.Effect.FzfInterp (runFzfPure)
import Seihou.Fzf
  ( Candidate (..),
    FzfOpts,
    FzfResult (..),
    optsToArgs,
    withAnsi,
    withHeader,
    withHeight,
    withNoSort,
    withPreview,
    withPrompt,
  )
import Seihou.Fzf.Selector.Module (formatModuleCandidate, formatRunnableCandidate)
import Seihou.Prelude
import Test.Hspec
import Test.Tasty
import Test.Tasty.Hspec (testSpec)

tests :: IO TestTree
tests = testSpec "Seihou.Fzf" spec

spec :: Spec
spec = do
  describe "optsToArgs" $ do
    it "produces empty args for mempty" $ do
      optsToArgs mempty `shouldBe` []

    it "produces --prompt flag" $ do
      optsToArgs (withPrompt "test> ") `shouldBe` ["--prompt", "test> "]

    it "produces --header flag" $ do
      optsToArgs (withHeader "Pick one") `shouldBe` ["--header", "Pick one"]

    it "produces --height flag" $ do
      optsToArgs (withHeight "40%") `shouldBe` ["--height", "40%"]

    it "produces --ansi flag" $ do
      optsToArgs withAnsi `shouldBe` ["--ansi"]

    it "produces --no-sort flag" $ do
      optsToArgs withNoSort `shouldBe` ["--no-sort"]

    it "produces --preview flag" $ do
      optsToArgs (withPreview "cat {}") `shouldBe` ["--preview", "cat {}"]

    it "combines options with <>" $ do
      let opts = withPrompt "p> " <> withHeight "50%" <> withAnsi
      optsToArgs opts `shouldBe` ["--prompt", "p> ", "--height", "50%", "--ansi"]

  describe "FzfOpts Monoid" $ do
    it "mempty is left identity" $ do
      let opts = withPrompt "test> "
      optsToArgs (mempty <> opts) `shouldBe` optsToArgs opts

    it "mempty is right identity" $ do
      let opts = withPrompt "test> "
      optsToArgs (opts <> mempty) `shouldBe` optsToArgs opts

    it "right-biases Maybe fields" $ do
      let a = withPrompt "first> "
          b = withPrompt "second> "
      optsToArgs (a <> b) `shouldBe` ["--prompt", "second> "]

    it "sticky-true for Bool fields" $ do
      let opts = withAnsi <> mempty
      optsToArgs opts `shouldBe` ["--ansi"]

  describe "runFzfPure" $ do
    it "selects candidate at given index" $ do
      let candidates =
            [ Candidate "alpha" ("a" :: Text),
              Candidate "beta" "b",
              Candidate "gamma" "c"
            ]
      result <- runEff $ runFzfPure 1 $ selectOne mempty candidates
      case result of
        FzfSelected val -> val `shouldBe` "b"
        _ -> expectationFailure "expected FzfSelected"

    it "returns FzfNoMatch for negative index" $ do
      let candidates = [Candidate "alpha" ("a" :: Text)]
      result <- runEff $ runFzfPure (-1) $ selectOne mempty candidates
      case result of
        FzfNoMatch -> pure ()
        _ -> expectationFailure "expected FzfNoMatch"

    it "returns FzfNoMatch for out-of-bounds index" $ do
      let candidates = [Candidate "alpha" ("a" :: Text)]
      result <- runEff $ runFzfPure 5 $ selectOne mempty candidates
      case result of
        FzfNoMatch -> pure ()
        _ -> expectationFailure "expected FzfNoMatch"

    it "returns FzfNoMatch for empty candidates" $ do
      result <- runEff $ runFzfPure 0 $ selectOne mempty ([] :: [Candidate Text])
      case result of
        FzfNoMatch -> pure ()
        _ -> expectationFailure "expected FzfNoMatch"

  describe "formatModuleCandidate" $ do
    it "returns Just for a valid module" $ do
      let dm = validModule "test-mod" "A test module" SourceUser
      case formatModuleCandidate dm of
        Just c -> do
          c.candidateValue `shouldBe` ModuleName "test-mod"
          T.isInfixOf "test-mod" c.candidateDisplay `shouldBe` True
          T.isInfixOf "[user]" c.candidateDisplay `shouldBe` True
        Nothing -> expectationFailure "expected Just"

    it "returns Nothing for a failed module" $ do
      let dm =
            DiscoveredModule
              { discoveredResult = Left (ModuleNotFound (ModuleName "bad") []),
                discoveredSource = SourceProject,
                discoveredDir = "/tmp/bad"
              }
      case formatModuleCandidate dm of
        Nothing -> pure ()
        Just _ -> expectationFailure "expected Nothing"

    it "includes description when present" $ do
      let dm = validModule "mod" "My description" SourceInstalled
      case formatModuleCandidate dm of
        Just c -> T.isInfixOf "My description" c.candidateDisplay `shouldBe` True
        Nothing -> expectationFailure "expected Just"

    it "tags source correctly" $ do
      let dmProject = validModule "m" "d" SourceProject
          dmUser = validModule "m" "d" SourceUser
          dmInstalled = validModule "m" "d" SourceInstalled
      case (formatModuleCandidate dmProject, formatModuleCandidate dmUser, formatModuleCandidate dmInstalled) of
        (Just p, Just u, Just i) -> do
          T.isInfixOf "[project]" p.candidateDisplay `shouldBe` True
          T.isInfixOf "[user]" u.candidateDisplay `shouldBe` True
          T.isInfixOf "[installed]" i.candidateDisplay `shouldBe` True
        _ -> expectationFailure "expected all Just"

  describe "formatRunnableCandidate" $ do
    it "tags prompt candidates with [prompt]" $ do
      let dr =
            DiscoveredRunnable
              { drName = "review-changes",
                drDescription = Just "Review current changes",
                drKind = KindPrompt,
                drSource = SourceProject,
                drDir = "/tmp/review-changes",
                drIsError = False,
                drError = Nothing
              }
      case formatRunnableCandidate dr of
        Just c -> T.isInfixOf "[prompt]" c.candidateDisplay `shouldBe` True
        Nothing -> expectationFailure "expected Just"

-- | Helper to create a valid discovered module for testing.
validModule :: String -> String -> ModuleSource -> DiscoveredModule
validModule name desc src =
  DiscoveredModule
    { discoveredResult =
        Right
          Module
            { name = ModuleName (T.pack name),
              version = Nothing,
              description = Just (T.pack desc),
              vars = [],
              exports = [],
              prompts = [],
              steps = [],
              commands = [],
              dependencies = [],
              removal = Nothing,
              migrations = []
            },
      discoveredSource = src,
      discoveredDir = "/tmp/" ++ name
    }