{-# LANGUAGE OverloadedStrings #-}

module Definition (
    gotoDefinitionTests,
) where

import           Control.Lens                ((^.))
import           Data.List.Extra             (isSuffixOf)
import qualified Data.Text                   as T
import           Ide.Plugin.Cabal.Definition (toHaskellFile)
import qualified Language.LSP.Protocol.Lens  as L
import qualified Language.LSP.Protocol.Types as LSP
import           System.FilePath
import           Test.Hls
import           Utils


gotoDefinitionTests :: TestTree
gotoDefinitionTests = testGroup "Goto Definition"
    [ gotoCommonSectionDefinitionTests
    , gotoModuleDefinitionTests
    ]

gotoModuleDefinitionTests :: TestTree
gotoModuleDefinitionTests = testGroup "Goto Module Definition"
    [ testGoToDefinitionLink "simple cabal test" "simple-cabal" "simple-cabal.cabal"
                             (Position 8 23) (toTestHaskellPath "" "A")

    , testGoToDefinitionLink "library start of exposed-modules" ("goto-definition" </> "modules") "module-examples.cabal"
                             (Position 6 22) (toTestHaskellPath "src" "Library.Lib")
    , testGoToDefinitionLink "library middle of exposed-modules" ("goto-definition" </> "modules") "module-examples.cabal"
                             (Position 6 29) (toTestHaskellPath "src" "Library.Lib")
    , testGoToDefinitionLink "library end of exposed-modules" ("goto-definition" </> "modules") "module-examples.cabal"
                             (Position 6 33) (toTestHaskellPath "src" "Library.Lib")
    , testGoToDefinitionLink "library start of other-modules" ("goto-definition" </> "modules") "module-examples.cabal"
                             (Position 9 22) (toTestHaskellPath "src" "Library.Other.OtherLib")
    , testGoToDefinitionLink "library end of other-modules" ("goto-definition" </> "modules") "module-examples.cabal"
                             (Position 9 44) (toTestHaskellPath "src" "Library.Other.OtherLib")

    , testGoToDefinitionLink "executable other-modules" ("goto-definition" </> "modules") "module-examples.cabal"
                             (Position 22 10) (toTestHaskellPath ("src" </> "exe") "Config")

    , testGoToDefinitionLink "test-suite other-modules Config" ("goto-definition" </> "modules") "module-examples.cabal"
                             (Position 31 10) (toTestHaskellPath ("src" </> "test") "Config")
    , testGoToDefinitionLink "test-suite other-modules Library" ("goto-definition" </> "modules") "module-examples.cabal"
                             (Position 34 10) (toTestHaskellPath ("src" </> "test") "Library")

    , testGoToDefinitionLink "benchmark other-modules Config" ("goto-definition" </> "modules") "module-examples.cabal"
                             (Position 45 30) (toTestHaskellPath ("src" </> "bench") "Config")

    , testGoToDefinitionLinkNoLocation "not existent module" ("goto-definition" </> "modules") "module-examples.cabal" (Position 48 25)
    , testGoToDefinitionLinkNoLocation "behind module" ("goto-definition" </> "modules") "module-examples.cabal" (Position 9 20)
    , testGoToDefinitionLinkNoLocation "after module" ("goto-definition" </> "modules") "module-examples.cabal" (Position 9 50)
    ]
    where
        toTestHaskellPath :: FilePath -> T.Text -> FilePath
        toTestHaskellPath dir moduleName = dir </> toHaskellFile moduleName

        getUriFromDefinition :: Show b => (Definition |? b) -> Uri
        getUriFromDefinition (InL (Definition (InL loc))) = loc^.L.uri
        getUriFromDefinition unk = error $ "Unexpected pattern '" ++ show unk ++ "' , expected '(InL (Definition (InL loc))'"

        testGoToDefinitionLink :: TestName -> FilePath -> FilePath -> Position -> FilePath -> TestTree
        testGoToDefinitionLink testName testDir cabalFile cursorPos expectedFilePath =
            runCabalTestCaseSession testName testDir $ do
                doc <- openDoc cabalFile "cabal"
                definitions <- getDefinitions doc cursorPos
                let uri = getUriFromDefinition definitions
                    mFilePath = (testDir </>) <$> uriToFilePath uri
                case mFilePath of
                    Nothing -> error $ "Not possible to convert Uri " <> show uri <> " to FilePath"
                    Just filePath -> do
                        let filePathWithDir = testDir </> expectedFilePath
                            isCorrectPath = filePathWithDir `isSuffixOf` filePath
                        liftIO $ isCorrectPath @? ("Absolute path expected to end on " <> filePathWithDir <>
                                                   " but " <> filePath <> " was given.")

        testGoToDefinitionLinkNoLocation :: TestName -> FilePath -> FilePath -> Position -> TestTree
        testGoToDefinitionLinkNoLocation testName testDir cabalFile cursorPos =
            runCabalTestCaseSession testName testDir $ do
                doc <- openDoc cabalFile "cabal"
                empty <- getDefinitions doc cursorPos
                liftIO $ empty @?= (InR $ InR LSP.Null)

gotoCommonSectionDefinitionTests :: TestTree
gotoCommonSectionDefinitionTests = testGroup "Goto Common Section Definition"
    [ positiveTest "middle of identifier"            (Position 27 16) (mkRange  6 0  7 22)
    , positiveTest "left of identifier"              (Position 30 12) (mkRange 10 0 17 40)
    , positiveTest "right of identifier"             (Position 33 22) (mkRange 20 0 23 34)
    , positiveTest "left of '-' in identifier"       (Position 36 20) (mkRange  6 0  7 22)
    , positiveTest "right of '-' in identifier"      (Position 39 19) (mkRange 10 0 17 40)
    , positiveTest "identifier in identifier list"   (Position 42 16) (mkRange 20 0 23 34)
    , positiveTest "left of ',' right of identifier" (Position 45 33) (mkRange 10 0 17 40)
    , positiveTest "right of ',' left of identifier" (Position 48 34) (mkRange  6 0  7 22)

    , negativeTest "right of ',' left of space"      (Position 51 23)
    , negativeTest "right of ':' left of space"      (Position 54 11)
    , negativeTest "not a definition"                (Position 57 8)
    , negativeTest "empty space"                     (Position 59 7)
    ]
    where
        getRangeFromDefinition :: Show b => (Definition |? b) -> Range
        getRangeFromDefinition (InL (Definition (InL loc))) = loc^.L.range
        getRangeFromDefinition unk = error $ "Unexpected pattern '" ++ show unk ++ "' , expected '(InL (Definition (InL loc))'"

        -- A positive test checks if the provided range is equal
        -- to the expected range from the definition in the test file.
        -- The test emulates a goto-definition request of an actual definition.
        positiveTest :: TestName -> Position -> Range -> TestTree
        positiveTest testName cursorPos expectedRange =
            runCabalTestCaseSession testName ("goto-definition" </> "common-section") $ do
                doc <- openDoc "simple-with-common.cabal" "cabal"
                definitions <- getDefinitions doc cursorPos
                let range = getRangeFromDefinition definitions
                liftIO $ range @?= expectedRange

        -- A negative test checks if the request failed and
        -- the provided result is empty, i.e. `InR $ InR Null`.
        -- The test emulates a goto-definition request of anything but an
        -- actual definition.
        negativeTest :: TestName -> Position -> TestTree
        negativeTest testName cursorPos =
            runCabalTestCaseSession testName ("goto-definition" </> "common-section") $ do
                doc <- openDoc "simple-with-common.cabal" "cabal"
                empty <- getDefinitions doc cursorPos
                liftIO $ empty @?= (InR $ InR LSP.Null)