module Language.PureScript.Ide.ImportsSpec where

import Protolude hiding (moduleName)
import Data.Maybe (fromJust)
import Data.Set qualified as Set

import Language.PureScript qualified as P
import Language.PureScript.Ide.Command as Command
import Language.PureScript.Ide.Error (IdeError)
import Language.PureScript.Ide.Imports (Import, parseImport, prettyPrintImport', prettyPrintImportSection, sliceImportSection)
import Language.PureScript.Ide.Imports.Actions (addExplicitImport', addImplicitImport', addQualifiedImport')
import Language.PureScript.Ide.Filter (moduleFilter)
import Language.PureScript.Ide.Test qualified as Test
import Language.PureScript.Ide.Types (IdeDeclarationAnn(..), Success(..))
import System.FilePath ((</>))
import Test.Hspec (Expectation, Spec, describe, it, shouldBe, shouldSatisfy)

noImportsFile :: [Text]
noImportsFile =
  [ "module Main where"
  , ""
  , "myFunc x y = x + y"
  ]

simpleFile :: [Text]
simpleFile =
  [ "module Main where"
  , "import Prelude"
  , ""
  , "myFunc x y = x + y"
  ]

hidingFile :: [Text]
hidingFile =
  [ "module Main where"
  , "import Prelude"
  , "import Data.Maybe hiding (maybe, maybe')"
  , ""
  , "myFunc x y = x + y"
  ]

syntaxErrorFile :: [Text]
syntaxErrorFile =
  [ "module Main where"
  , "import Prelude"
  , ""
  , "myFunc ="
  ]

testSliceImportSection :: [Text] -> (P.ModuleName, [Text], [Import], [Text])
testSliceImportSection = unsafeFromRight . sliceImportSection
  where
    unsafeFromRight = fromJust . rightToMaybe

withImports :: [Text] -> [Text]
withImports is =
  take 2 simpleFile ++ [""] ++ is ++ drop 2 simpleFile

testParseImport :: Text -> Import
testParseImport = fromJust . parseImport

preludeImport, arrayImport, listImport, consoleImport, maybeImport :: Import
preludeImport = testParseImport "import Prelude"
arrayImport = testParseImport "import Data.Array (head, cons)"
listImport = testParseImport "import Data.List as List"
consoleImport = testParseImport "import Effect.Console (log) as Console"
maybeImport = testParseImport "import Data.Maybe (Maybe(Just))"

spec :: Spec
spec = do
  describe "determining the importsection" $ do
    let moduleSkeleton imports =
          Right (P.moduleNameFromString "Main", take 1 simpleFile, imports, drop 2 simpleFile)
    it "slices a file without imports" $
      shouldBe (sliceImportSection noImportsFile)
          (Right (P.moduleNameFromString "Main", take 1 noImportsFile, [], drop 1 noImportsFile))

    it "handles a file with syntax errors just fine" $
      shouldBe (sliceImportSection syntaxErrorFile)
      (Right (P.moduleNameFromString "Main", take 1 syntaxErrorFile, [preludeImport], drop 2 syntaxErrorFile))

    it "finds a simple import" $
      shouldBe (sliceImportSection simpleFile) (moduleSkeleton [preludeImport])

    it "allows multiline import statements" $
      shouldBe
        (sliceImportSection (withImports [ "import Data.Array (head,"
                                         , "                   cons)"
                                         ]))
        (moduleSkeleton [preludeImport, arrayImport])
    it "allows multiline import statements with hanging parens" $
      shouldBe
        (sliceImportSection (withImports [ "import Data.Array ("
                                         , "  head,"
                                         , "  cons"
                                         , ")"
                                         ]))
        (moduleSkeleton [preludeImport, arrayImport])
  describe "pretty printing imports" $ do
    it "pretty prints a simple import" $
      shouldBe (prettyPrintImport' preludeImport) "import Prelude"
    it "pretty prints an explicit import" $
      shouldBe (prettyPrintImport' arrayImport) "import Data.Array (head, cons)"
    it "pretty prints a qualified import" $
      shouldBe (prettyPrintImport' listImport) "import Data.List as List"
    it "pretty prints a qualified explicit import" $
      shouldBe (prettyPrintImport' consoleImport) "import Effect.Console (log) as Console"
    it "pretty prints an import with a datatype (and PositionedRef's for the dtors)" $
      shouldBe (prettyPrintImport' maybeImport) "import Data.Maybe (Maybe(Just))"

  describe "import commands" $ do
    let simpleFileImports = let (_, _, i, _) = testSliceImportSection simpleFile in i
        hidingFileImports = let (_, _, i, _) = testSliceImportSection hidingFile in i
        addValueImport i mn q is =
          prettyPrintImportSection (addExplicitImport' (_idaDeclaration (Test.ideValue i Nothing)) mn q is)
        addOpImport op mn q is =
          prettyPrintImportSection (addExplicitImport' (_idaDeclaration (Test.ideValueOp op (P.Qualified (P.byMaybeModuleName q) (Left "")) 2 Nothing Nothing)) mn q is)
        addDtorImport i t mn q is =
          prettyPrintImportSection (addExplicitImport' (_idaDeclaration (Test.ideDtor i t Nothing)) mn q is)
        addTypeImport i mn q is =
          prettyPrintImportSection (addExplicitImport' (_idaDeclaration (Test.ideType i Nothing [])) mn q is)
        qualify s = Just (Test.mn s)
    it "adds an implicit unqualified import to a file without any imports" $
      shouldBe
        (addImplicitImport' [] (P.moduleNameFromString "Data.Map"))
        ["import Data.Map"]
    it "adds an implicit unqualified import" $
      shouldBe
        (addImplicitImport' simpleFileImports (P.moduleNameFromString "Data.Map"))
        [ "import Data.Map"
        , "import Prelude"
        ]
    it "adds a qualified import" $
      shouldBe
        (addQualifiedImport' simpleFileImports (Test.mn "Data.Map") (Test.mn "Map"))
        [ "import Prelude"
        , ""
        , "import Data.Map as Map"
        ]
    it "adds a qualified import and maintains proper grouping and formatting for implicit hiding imports" $
      shouldBe
        (addQualifiedImport' hidingFileImports (Test.mn "Data.Map") (Test.mn "Map"))
        [ "import Data.Maybe hiding (maybe, maybe')"
        , "import Prelude"
        , ""
        , "import Data.Map as Map"
        ]
    it "adds an explicit unqualified import to a file without any imports" $
      shouldBe
        (addValueImport "head" (P.moduleNameFromString "Data.Array") Nothing [])
        ["import Data.Array (head)"]
    it "adds an explicit qualified import to a file without any imports" $
      shouldBe
        (addValueImport "head" (P.moduleNameFromString "Data.Array") (qualify "Array") [])
        ["import Data.Array (head) as Array"]
    it "adds an explicit unqualified import" $
      shouldBe
        (addValueImport "head" (P.moduleNameFromString "Data.Array") Nothing simpleFileImports)
        [ "import Prelude"
        , ""
        , "import Data.Array (head)"
        ]
    it "adds an explicit qualified import" $
      shouldBe
        (addValueImport "head" (P.moduleNameFromString "Data.Array") (qualify "Array") simpleFileImports)
        [ "import Prelude"
        , ""
        , "import Data.Array (head) as Array"
        ]
    it "doesn't add an import if the containing module is imported implicitly" $
      shouldBe
      (addValueImport "const" (P.moduleNameFromString "Prelude") Nothing simpleFileImports)
      ["import Prelude"]
    let Right (_, _, qualifiedImports, _) = sliceImportSection (withImports ["import Data.Array as Array"])
    it "doesn't add a qualified explicit import if the containing module is imported qualified" $
      shouldBe
      (addValueImport "length" (P.moduleNameFromString "Data.Array") (qualify "Array") qualifiedImports)
      ["import Prelude"
      , ""
      , "import Data.Array as Array"]
    let Right (_, _, explicitImports, _) = sliceImportSection (withImports ["import Data.Array (tail)"])
    it "adds an identifier to an explicit import list" $
      shouldBe
        (addValueImport "head" (P.moduleNameFromString "Data.Array") Nothing explicitImports)
        [ "import Prelude"
        , ""
        , "import Data.Array (head, tail)"
        ]
    let Right (_, _, explicitQualImports, _) = sliceImportSection (withImports ["import Data.Array (tail) as Array"])
    it "adds an identifier to an explicit qualified import list" $
      shouldBe
        (addValueImport "head" (P.moduleNameFromString "Data.Array") (qualify "Array") explicitQualImports)
        [ "import Prelude"
        , ""
        , "import Data.Array (head, tail) as Array"
        ]
    it "adds an operator to an explicit import list" $
      shouldBe
        (addOpImport "<~>" (P.moduleNameFromString "Data.Array") Nothing explicitImports)
        [ "import Prelude"
        , ""
        , "import Data.Array (tail, (<~>))"
        ]
    it "adds an operator to an explicit qualified import list" $
      shouldBe
        (addOpImport "<~>" (P.moduleNameFromString "Data.Array") (qualify "Array") explicitQualImports)
        [ "import Prelude"
        , ""
        , "import Data.Array (tail, (<~>)) as Array"
        ]
    it "adds a type with constructors without automatically adding an open import of said constructors " $
        shouldBe
          (addTypeImport "Maybe" (P.moduleNameFromString "Data.Maybe") Nothing simpleFileImports)
          [ "import Prelude"
          , ""
          , "import Data.Maybe (Maybe)"
          ]
    it "adds the type for a given DataConstructor" $
        shouldBe
          (addDtorImport "Just" "Maybe" (P.moduleNameFromString "Data.Maybe") Nothing simpleFileImports)
          [ "import Prelude"
          , ""
          , "import Data.Maybe (Maybe(..))"
          ]
    it "adds the type for a given DataConstructor qualified" $
        shouldBe
          (addDtorImport "Just" "Maybe" (P.moduleNameFromString "Data.Maybe") (qualify "M") simpleFileImports)
          [ "import Prelude"
          , ""
          , "import Data.Maybe (Maybe(..)) as M"
          ]
    it "adds a dataconstructor to an existing type import" $ do
      let Right (_, _, typeImports, _) = sliceImportSection (withImports ["import Data.Maybe (Maybe)"])
      shouldBe
        (addDtorImport "Just" "Maybe" (P.moduleNameFromString "Data.Maybe") Nothing typeImports)
        [ "import Prelude"
        , ""
        , "import Data.Maybe (Maybe(..))"
        ]
    it "adding a type to an existing import of that type is noop" $ do
        let Right (_, _, typeImports, _) = sliceImportSection (withImports ["import Data.Maybe (Maybe)"])
        shouldBe
          (addTypeImport "Maybe" (P.moduleNameFromString "Data.Maybe") Nothing typeImports)
          [ "import Prelude"
          , ""
          , "import Data.Maybe (Maybe)"
          ]
    it "adding a type to an existing import of that type with its constructors is noop" $ do
      let Right (_, _, typeImports, _) = sliceImportSection (withImports ["import Data.Maybe (Maybe (..))"])
      shouldBe
        (addTypeImport "Maybe" (P.moduleNameFromString "Data.Maybe") Nothing typeImports)
        [ "import Prelude"
        , ""
        , "import Data.Maybe (Maybe(..))"
        ]
    it "adds a dataconstructor to an existing qualified type import" $ do
      let Right (_, _, typeImports, _) = sliceImportSection (withImports ["import Data.Maybe (Maybe) as M"])
      shouldBe
        (addDtorImport "Just" "Maybe" (P.moduleNameFromString "Data.Maybe") (qualify "M") typeImports)
        [ "import Prelude"
        , ""
        , "import Data.Maybe (Maybe(..)) as M"
        ]
    it "doesn't add a dataconstructor to an existing type import with open dtors" $ do
      let Right (_, _, typeImports, _) = sliceImportSection (withImports ["import Data.Maybe (Maybe(..))"])
      shouldBe
        (addDtorImport "Just" "Maybe" (P.moduleNameFromString "Data.Maybe") Nothing typeImports)
        [ "import Prelude"
        , ""
        , "import Data.Maybe (Maybe(..))"
        ]
    it "doesn't add an identifier to an explicit import list if it's already imported" $
      shouldBe
      (addValueImport "tail" (P.moduleNameFromString "Data.Array") Nothing explicitImports)
      [ "import Prelude"
      , ""
      , "import Data.Array (tail)"
      ]
    it "doesn't add an identifier to an explicit qualified import list if it's already imported qualified" $
      shouldBe
      (addValueImport "tail" (P.moduleNameFromString "Data.Array") (qualify "Array") explicitQualImports)
      [ "import Prelude"
      , ""
      , "import Data.Array (tail) as Array"
      ]

  describe "explicit import sorting" $ do
    -- given some basic import skeleton
    let Right (_, _, baseImports, _) = sliceImportSection $ withImports ["import Control.Monad (ap)"]
        moduleName = P.moduleNameFromString "Control.Monad"
        addImport imports import' = addExplicitImport' import' moduleName Nothing imports
        valueImport ident = _idaDeclaration (Test.ideValue ident Nothing)
        typeImport name = _idaDeclaration (Test.ideType name Nothing [])
        classImport name = _idaDeclaration (Test.ideTypeClass name P.kindType [])
        dtorImport name typeName = _idaDeclaration (Test.ideDtor name typeName Nothing)
        -- expect any list of provided identifiers, when imported, to come out as specified
        expectSorted imports expected = shouldBe
          (ordNub $ map
            (prettyPrintImportSection . foldl addImport baseImports)
            (permutations imports))
          [expected]
    it "sorts class" $
      expectSorted (map classImport ["Applicative", "Bind"])
        ["import Prelude", "", "import Control.Monad (class Applicative, class Bind, ap)"]
    it "sorts value" $
      expectSorted (map valueImport ["unless", "where"])
        ["import Prelude", "", "import Control.Monad (ap, unless, where)"]
    it "sorts type, value" $
      expectSorted
        (map valueImport ["unless", "where"] ++ map typeImport ["Foo", "Bar"])
        ["import Prelude", "", "import Control.Monad (Bar, Foo, ap, unless, where)"]
    it "sorts class, type, value" $
      expectSorted
        (map valueImport ["unless", "where"] ++ map typeImport ["Foo", "Bar"] ++ map classImport ["Applicative", "Bind"])
        ["import Prelude", "", "import Control.Monad (class Applicative, class Bind, Bar, Foo, ap, unless, where)"]
    it "sorts types with constructors, using open imports for the constructors" $
      expectSorted
        -- the imported names don't actually have to exist!
        (map (uncurry dtorImport) [("Just", "Maybe"), ("Nothing", "Maybe"), ("SomeOtherConstructor", "SomeDataType")])
        ["import Prelude", "", "import Control.Monad (Maybe(..), SomeDataType(..), ap)"]
  describe "importing from a loaded IdeState" importFromIdeState

implImport :: Text -> Command
implImport mn =
  Command.Import ("src" </> "ImportsSpec.purs") Nothing [] (Command.AddImplicitImport (Test.mn mn))

addExplicitImport :: Text -> Command
addExplicitImport i =
  Command.Import ("src" </> "ImportsSpec.purs") Nothing [] (Command.AddImportForIdentifier i Nothing)

addExplicitImportFiltered :: Text -> [P.ModuleName] -> Command
addExplicitImportFiltered i ms =
  Command.Import ("src" </> "ImportsSpec.purs") Nothing [moduleFilter (Set.fromList ms)] (Command.AddImportForIdentifier i Nothing)

importShouldBe :: [Text] -> [Text] -> Expectation
importShouldBe res importSection =
  res `shouldBe`
    [ "module ImportsSpec where" ]
    ++ (if null importSection then [] else "" : importSection)
    ++ [ ""
       , "myId x = x"
       ]

runIdeLoaded :: Command -> IO (Either IdeError Success)
runIdeLoaded c = do
  ([_, result], _) <- Test.inProject $ Test.runIde [Command.LoadSync [] , c]
  pure result

importFromIdeState :: Spec
importFromIdeState = do
  it "adds an implicit import" $ do
    Right (MultilineTextResult result) <-
      runIdeLoaded (implImport "ImportsSpec1")
    result `importShouldBe` [ "import ImportsSpec1" ]
  it "adds an explicit unqualified import" $ do
    Right (MultilineTextResult result) <- runIdeLoaded (addExplicitImport "exportedFunction")
    result `importShouldBe` [ "import ImportsSpec1 (exportedFunction)" ]
  it "adds an explicit unqualified import (type)" $ do
    Right (MultilineTextResult result) <- runIdeLoaded (addExplicitImport "MyType")
    result `importShouldBe` [ "import ImportsSpec1 (MyType)" ]
  it "adds an explicit unqualified import (parameterized type)" $ do
    Right (MultilineTextResult result) <- runIdeLoaded (addExplicitImport "MyParamType")
    result `importShouldBe` [ "import ImportsSpec1 (MyParamType)" ]
  it "adds an explicit unqualified import (typeclass)" $ do
    Right (MultilineTextResult result) <- runIdeLoaded (addExplicitImport "ATypeClass")
    result `importShouldBe` [ "import ImportsSpec1 (class ATypeClass)" ]
  it "adds an explicit unqualified import (dataconstructor)" $ do
    Right (MultilineTextResult result) <- runIdeLoaded (addExplicitImport "MyJust")
    result `importShouldBe` [ "import ImportsSpec1 (MyMaybe(..))" ]
  it "adds an explicit unqualified import (newtype)" $ do
    Right (MultilineTextResult result) <- runIdeLoaded (addExplicitImport "MyNewtype")
    result `importShouldBe` [ "import ImportsSpec1 (MyNewtype(..))" ]
  it "adds an explicit unqualified import (typeclass member function)" $ do
    Right (MultilineTextResult result) <- runIdeLoaded (addExplicitImport "typeClassFun")
    result `importShouldBe` [ "import ImportsSpec1 (typeClassFun)" ]
  it "doesn't add a newtypes constructor if only the type is exported" $ do
    Right (MultilineTextResult result) <-
      runIdeLoaded (addExplicitImport "OnlyTypeExported")
    result `importShouldBe` [ "import ImportsSpec1 (OnlyTypeExported)" ]
  it "doesn't add an import if the identifier is defined in the module itself" $ do
    Right (MultilineTextResult result) <- runIdeLoaded (addExplicitImport "myId")
    result `importShouldBe` []
  it "responds with an error if it's undecidable whether we want a type or constructor" $ do
    result <- runIdeLoaded (addExplicitImport "SpecialCase")
    result `shouldSatisfy` isLeft
  it "responds with an error if the identifier cannot be found and doesn't \
     \write to the output file" $ do
    result <- runIdeLoaded (addExplicitImport "doesnExist")
    result `shouldSatisfy` isLeft
  it "doesn't import things from the Prim modules" $ do
    Right (MultilineTextResult result) <- runIdeLoaded (addExplicitImport "String")
    result `importShouldBe` []
  it "imports classes from Prim.* modules" $ do
    Right (MultilineTextResult result) <- runIdeLoaded (addExplicitImportFiltered "Cons" [Test.mn "Prim.Row"])
    result `importShouldBe` ["import Prim.Row (class Cons)"]
  it "imports types from Prim.* modules" $ do
    Right (MultilineTextResult result) <- runIdeLoaded (addExplicitImportFiltered "Cons" [Test.mn "Prim.RowList"])
    result `importShouldBe` ["import Prim.RowList (Cons)"]
