{-# LANGUAGE OverloadedStrings #-}
module CodeActionsSpec where

import Test.Hspec
import qualified Data.Text.IO as T
import Haskell.Ide.Engine.Plugin.HsImport
import Haskell.Ide.Engine.Plugin.Generic hiding (Import)
import Haskell.Ide.Engine.Plugin.Package

main :: IO ()
main = hspec spec

spec :: Spec
spec = do
  describe "import code actions" $ do
    it "pick up variable not in scope" $
      let msg =  "Variable not in scope: fromJust :: Maybe Integer -> t"
        in extractImportableTerm msg `shouldBe` Just ("fromJust :: Maybe Integer -> t", Import Symbol)
    it "pick up variable not in scope with 'perhaps you meant'" $
      let msg =  "• Variable not in scope: msgs :: T.Text\n• Perhaps you meant ‘msg’ (line 90)"
        in extractImportableTerm msg `shouldBe` Just ("msgs :: T.Text", Import Symbol)
    it "pick up multi-line variable not in scope" $
      let msg = "Variable not in scope:\nliftIO\n:: IO [FilePath]\n-> GhcMod.Monad.Newtypes.GmT\n                (GhcMod.Monad.Newtypes.GmOutT IdeM) [[t0]]"
        in extractImportableTerm msg `shouldBe` Just ("liftIO :: IO [FilePath] -> GhcMod.Monad.Newtypes.GmT (GhcMod.Monad.Newtypes.GmOutT IdeM) [[t0]]", Import Symbol)
    it "pick up when" $
      let msg = "Variable not in scope: when :: Bool -> IO () -> t"
        in extractImportableTerm msg `shouldBe` Just ("when :: Bool -> IO () -> t", Import Symbol)
    it "pick up data constructors" $
      let msg = "Data constructor not in scope: ExitFailure :: Integer -> t"
        in extractImportableTerm msg `shouldBe` Just ("ExitFailure :: Integer -> t", Import Constructor)
    it "pick up type" $
      let msg = "Not in scope: type constructor or class ‘Text"
        in extractImportableTerm msg `shouldBe` Just ("Text", Import Type)

  describe "rename code actions" $ do
    it "pick up variable not in scope perhaps you meant" $
      let msg = "• Variable not in scope: fromBust\n• Perhaps you meant ‘fromJust’ (imported from Data.Maybe)"
        in extractRenamableTerms msg `shouldBe` ["fromJust"]
    it "pick up multiline perhaps you meant" $
      let msg = "• Variable not in scope: fromBust :: [Char] -> t\n• Perhaps you meant one of these:\n‘fromJust’ (imported from Data.Maybe),\n‘BM.fromList’ (imported from Data.Bimap),\n‘HM.fromList’ (imported from Data.HashMap.Strict)"
        in extractRenamableTerms msg `shouldBe` ["fromJust", "BM.fromList", "HM.fromList"]
    it "doen't pick up irrelvant messages" $
      let msg = "The import of ‘Control.Exception’ is redundant\n      except perhaps to import instances from ‘Control.Exception’"
        in extractRenamableTerms msg `shouldBe` []

    it "picks up variable not in scope with multiple suggestions" $
      let msg = "• Variable not in scope: uri\n• Perhaps you meant one of these:\n‘J.uri’ (imported from Language.Haskell.LSP.Types),\ndata constructor ‘J.Uri’ (imported from Language.Haskell.LSP.Types)"
        in extractRenamableTerms msg `shouldBe` ["J.uri", "J.Uri"]

    it "picks up data constructors" $
      let msg = "• Data constructor not in scope:\n    MarkupContent :: J.MarkupKind -> Maybe T.Text -> t0\n• Perhaps you meant ‘J.MarkupContent’ (imported from Language.Haskell.LSP.Types)"
        in extractRenamableTerms msg `shouldBe` ["J.MarkupContent"]

    it "returns nothing when there's no suggetsions" $
      let msg = "Variable not in scope:\n  fromJust\n    :: Maybe (Maybe (List CommandOrCodeAction)) -> Maybe (List a)"
        in extractRenamableTerms msg `shouldBe` []

    it "picks up variables not in scope on new line" $
      let msg = "• Variable not in scope:\n    forM_ :: [CodeAction] -> (s0 -> Expectation) -> IO a0\n• Perhaps you meant ‘iforM_’ (imported from Control.Lens)"
        in extractRenamableTerms msg `shouldBe` ["iforM_"]

    it "picks up qualified functions" $
      let msg = "    Not in scope: ‘Foo.printResul’\n\
                \    Perhaps you meant one of these:\n\
                \      ‘Foo.printResult’ (imported from Mod.Bar.Foo),\n\
                \      ‘Foo.formatResult’ (imported from Mod.Bar.Foo)\n\
                \    Module ‘Mod.Bar.Foo’ does not export ‘printResul’"
        in extractRenamableTerms msg `shouldBe` ["Foo.printResult", "Foo.formatResult"]

    it "picks up local definitions" $
      let msg = "• Variable not in scope: as :: f Foo\n\
                \    • Perhaps you meant one of these:\n\
                \        ‘ast’ (line 235)"
        in extractRenamableTerms msg `shouldBe` ["ast"]

    it "picks up definitions in same line" $
      let msg = "• Variable not in scope: as :: f Foo\n\
                \    • Perhaps you meant one of these:\n\
                \        ‘ast’ (line 235), ‘abs’ (imported from Prelude)"
        in extractRenamableTerms msg `shouldBe` ["ast", "abs"]

  describe "typed holes" $ do
    it "picks them up" $ do
      msg <- T.readFile "test/testdata/typedHoleDiag.txt"
      let substitutions = ValidSubstitutions [ FunctionSig "Nothing" (TypeDef "forall a. Maybe a")
                                             , FunctionSig "mempty" (TypeDef "forall a. Monoid a => a")
                                             , FunctionSig "undefined" (TypeDef "forall (a :: TYPE r). GHC.Stack.Types.HasCallStack => a")
                                             , FunctionSig "GM.mzero" (TypeDef "forall (m :: * -> *). GM.MonadPlus m => forall a. m a")
                                             ]


          bindings = Bindings [ FunctionSig "diag" (TypeDef "T.Text")
                              , FunctionSig "extractHoles" (TypeDef "T.Text -> Maybe T.Text")
                              ]

          expected = Just (TypeDef "Maybe T.Text", substitutions, bindings)
      extractHoleSubstitutions msg `shouldBe` expected

    it "removes bound at" $ do
      msg <- T.readFile "test/testdata/typedHoleDiag2.txt"
      let substitutions = ValidSubstitutions [FunctionSig "undefined" (TypeDef "forall (a :: TYPE r). GHC.Stack.Types.HasCallStack => a")]

          bindings = Bindings [ FunctionSig "stuff" (TypeDef "A -> A")
                              , FunctionSig "x" (TypeDef "[A]")
                              , FunctionSig "foo2" (TypeDef "[A] -> A")
                              ]

          expected = Just (TypeDef "A", substitutions, bindings)
      extractHoleSubstitutions msg `shouldBe` expected

    it "tolerates long signatures" $ do
      msg <- T.readFile "test/testdata/typedHoleDiag3.txt"
      let substitutions = ValidSubstitutions [ FunctionSig "mempty" (TypeDef "forall a. Monoid a => a")
                                             , FunctionSig "undefined" (TypeDef "forall (a :: TYPE r). GHC.Stack.Types.HasCallStack => a")
                                             , FunctionSig "idm" (TypeDef "forall m. Monoid m => m")
                                             ]
          longSig = "Either Language.Docker.Parser.Error Dockerfile -> Either Language.Docker.Parser.Error [Rules.RuleCheck]"
          longSig2 = "[IgnoreRule] -> t -> IO (Either Language.Docker.Parser.Error [Rules.RuleCheck])"
          bindings = Bindings [ FunctionSig "processedFile" (TypeDef longSig)
                              , FunctionSig "processRules" (TypeDef "Dockerfile -> [Rules.RuleCheck]")
                              , FunctionSig "ignoredRules" (TypeDef "Rules.RuleCheck -> Bool")
                              , FunctionSig "dockerFile" (TypeDef "t")
                              , FunctionSig "ignoreRules" (TypeDef "[IgnoreRule]")
                              , FunctionSig "lintDockerfile" (TypeDef longSig2)
                              ]

          expected = Just (TypeDef "t -> FilePath", substitutions, bindings)
      extractHoleSubstitutions msg `shouldBe` expected

  describe "missing package code actions" $ do
    it "pick up relevant messages" $
      let msg = "Could not find module ‘Foo.Bar’\n      Use -v to see a list of the files searched for."
        in extractModuleName msg `shouldBe` Just "Foo.Bar"
    it "don't pick up irrelevant messages" $
      let msg = "Could not find modulez ‘Foo.Bar’\n      Use -v to see a list of the files searched for."
        in extractModuleName msg `shouldBe` Nothing

  describe "missing signature code actions" $ do
    it "pick up relevant messages" $
      let msg = "    Top-level binding with no type signature: main :: IO ()"
        in extractMissingSignature msg `shouldBe` Just "main :: IO ()"

    it "pick up multiline signatures" $
      let msg = "    Top-level binding with no type signature:\n\
                \      printResultsAndExit :: (Text.Megaparsec.Error.ShowToken t,\n\
                \                              Text.Megaparsec.Error.ShowErrorComponent e, Ord t) =>\n\
                \                             OutputFormat -> Format.Result t e -> IO b"
          expected = "printResultsAndExit :: (Text.Megaparsec.Error.ShowToken t,\n\
                \                              Text.Megaparsec.Error.ShowErrorComponent e, Ord t) =>\n\
                \                             OutputFormat -> Format.Result t e -> IO b"
        in extractMissingSignature msg `shouldBe` Just expected

  describe "unused term code actions" $ do
    it "pick up unused term" $
      let msg = "  Defined but not used: ‘imUnused’"
       in extractUnusedTerm msg `shouldBe` Just "imUnused"
