{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoImplicitPrelude #-}
module Language.PureScript.Ide.ImportsSpec where

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

import qualified Language.PureScript as P
import           Language.PureScript.Ide.Command as Command
import           Language.PureScript.Ide.Error
import           Language.PureScript.Ide.Imports
import           Language.PureScript.Ide.Filter (moduleFilter)
import qualified Language.PureScript.Ide.Test as Test
import           Language.PureScript.Ide.Types
import           System.FilePath
import           Test.Hspec

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)"
  , ""
  , "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 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)
        addKindImport i mn q is =
          prettyPrintImportSection (addExplicitImport' (_idaDeclaration (Test.ideKind i)) 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 for implicit hiding imports" $
      shouldBe
        (addQualifiedImport' hidingFileImports (Test.mn "Data.Map") (Test.mn "Map"))
        [ "import Data.Maybe hiding (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 a kind to an explicit import list" $
      shouldBe
        (addKindImport "Effect" (P.moduleNameFromString "Control.Monad.Eff") Nothing simpleFileImports)
        [ "import Prelude"
        , ""
        , "import Control.Monad.Eff (kind Effect)"
        ]
    it "adds a kind to an explicit qualified import list" $
      shouldBe
        (addKindImport "Effect" (P.moduleNameFromString "Control.Monad.Eff") (qualify "Eff") simpleFileImports)
        [ "import Prelude"
        , ""
        , "import Control.Monad.Eff (kind Effect) as Eff"
        ]
    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)"]
