{-# LANGUAGE OverloadedLabels  #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wno-orphans #-}

module Ide.PluginUtilsTest
    ( tests
    ) where

import qualified Data.Aeson                  as A
import qualified Data.Aeson.Types            as A
import           Data.ByteString.Lazy        (ByteString)
import           Data.Function               ((&))
import qualified Data.Set                    as Set
import qualified Data.Text                   as T
import           Ide.Plugin.Properties       (KeyNamePath (..),
                                              definePropertiesProperty,
                                              defineStringProperty,
                                              emptyProperties, toDefaultJSON,
                                              toVSCodeExtensionSchema,
                                              usePropertyByPath,
                                              usePropertyByPathEither)
import qualified Ide.Plugin.RangeMap         as RangeMap
import           Ide.PluginUtils             (extractTextInRange, unescape)
import           Language.LSP.Protocol.Types (Position (..), Range (Range),
                                              UInt, isSubrangeOf)
import           Test.Tasty
import           Test.Tasty.Golden           (goldenVsStringDiff)
import           Test.Tasty.HUnit
import           Test.Tasty.QuickCheck

tests :: TestTree
tests = testGroup "PluginUtils"
    [ unescapeTest
    , extractTextInRangeTest
    , localOption (QuickCheckMaxSize 10000) $
        testProperty "RangeMap-List filtering identical" $
          prop_rangemapListEq @Int
    , propertyTest
    ]

unescapeTest :: TestTree
unescapeTest = testGroup "unescape"
    [ testCase "no double quote" $
        unescape "hello世界" @?= "hello世界"
    , testCase "whole string quoted" $
        unescape "\"hello\\19990\\30028\"" @?= "\"hello世界\""
    , testCase "text before quotes should not be unescaped" $
        unescape "\\19990a\"hello\\30028\"" @?= "\\19990a\"hello界\""
    , testCase "some text after quotes" $
        unescape "\"hello\\19990\\30028\"abc" @?= "\"hello世界\"abc"
    , testCase "many pairs of quote" $
        unescape "oo\"hello\\19990\\30028\"abc\"\1087\1088\1080\1074\1077\1090\"hh" @?= "oo\"hello世界\"abc\"привет\"hh"
    , testCase "double quote itself should not be unescaped" $
        unescape "\"\\\"\\19990o\"" @?= "\"\\\"世o\""
    , testCase "control characters should not be escaped" $
        unescape "\"\\n\\t\"" @?= "\"\\n\\t\""
    ]

extractTextInRangeTest :: TestTree
extractTextInRangeTest = testGroup "extractTextInRange"
    [ testCase "inline range" $
        extractTextInRange
            ( Range (Position 0 3) (Position 3 5) )
            src
        @?= T.intercalate "\n"
                [ "ule Main where"
                , ""
                , "main :: IO ()"
                , "main "
                ]
    , testCase "inline range with empty content" $
        extractTextInRange
            ( Range (Position 0 0) (Position 0 1) )
            emptySrc
        @?= ""
    , testCase "multiline range with empty content" $
        extractTextInRange
            ( Range (Position 0 0) (Position 1 0) )
            emptySrc
        @?= "\n"
    , testCase "multiline range" $
        extractTextInRange
            ( Range (Position 1 0) (Position 4 0) )
            src
        @?= T.unlines
                [ ""
                , "main :: IO ()"
                , "main = do"
                ]
    , testCase "multiline range with end pos at the line below the last line" $
        extractTextInRange
            ( Range (Position 2 0) (Position 5 0) )
            src
        @?= T.unlines
                [ "main :: IO ()"
                , "main = do"
                , "    putStrLn \"hello, world\""
                ]
    ]
    where
        src = T.unlines
                [ "module Main where"
                , ""
                , "main :: IO ()"
                , "main = do"
                , "    putStrLn \"hello, world\""
                ]
        emptySrc = "\n"

genRange :: Gen Range
genRange = oneof [ genRangeInline, genRangeMultiline ]

genRangeInline :: Gen Range
genRangeInline = do
  x1 <- genPosition
  delta <- genRangeLength
  let x2 = x1 { _character = _character x1 + delta }
  pure $ Range x1 x2
  where
    genRangeLength :: Gen UInt
    genRangeLength = uInt (5, 50)

genRangeMultiline :: Gen Range
genRangeMultiline = do
  x1 <- genPosition
  let heightDelta = 1
  secondX <- genSecond
  let x2 = x1 { _line = _line x1 + heightDelta
              , _character = secondX
              }
  pure $ Range x1 x2
  where
    genSecond :: Gen UInt
    genSecond = uInt (0, 10)

genPosition :: Gen Position
genPosition = Position
  <$> uInt (0, 1000)
  <*> uInt (0, 150)

uInt :: (Integer, Integer) -> Gen UInt
uInt (a, b) = fromInteger <$> chooseInteger (a, b)

instance Arbitrary Range where
  arbitrary = genRange

prop_rangemapListEq :: (Show a, Ord a) => Range -> [(Range, a)] -> Property
prop_rangemapListEq r xs =
  let filteredList = (map snd . filter (isSubrangeOf r . fst)) xs
      filteredRangeMap = RangeMap.filterByRange r (RangeMap.fromList' xs)
   in classify (null filteredList) "no matches" $
      cover 5 (length filteredList == 1) "1 match" $
      cover 2 (length filteredList > 1) ">1 matches" $
      Set.fromList filteredList === Set.fromList filteredRangeMap


gitDiff :: FilePath -> FilePath -> [String]
gitDiff fRef fNew = ["git", "-c", "core.fileMode=false", "diff", "--no-ext-diff", "-w", "--no-index", "--text", "--exit-code", fRef, fNew]

goldenGitDiff :: TestName -> FilePath -> IO ByteString -> TestTree
goldenGitDiff name = goldenVsStringDiff name gitDiff

testDir :: FilePath
testDir = "test/testdata/Property"

propertyTest :: TestTree
propertyTest = testGroup "property api tests" [
    goldenGitDiff "property toVSCodeExtensionSchema" (testDir <> "/NestedPropertyVscode.json") (return $ A.encode $ A.object $ toVSCodeExtensionSchema "top." nestedPropertiesExample)
    , goldenGitDiff "property toDefaultJSON" (testDir <> "/NestedPropertyDefault.json") (return $ A.encode $ A.object $ toDefaultJSON nestedPropertiesExample)
    , testCase "parsePropertyPath single key path" $ do
        let obj = A.object (toDefaultJSON nestedPropertiesExample)
        let key1 = A.parseEither (A.withObject "test parsePropertyPath" $ \o -> do
                let key1 = usePropertyByPathEither examplePath1 nestedPropertiesExample o
                return key1) obj
        key1 @?= Right (Right "baz")
    , testCase "parsePropertyPath two key path" $ do
        let obj = A.object (toDefaultJSON nestedPropertiesExample)
        let key1 = A.parseEither (A.withObject "test parsePropertyPath" $ \o -> do
                let key1 = usePropertyByPathEither examplePath2 nestedPropertiesExample o
                return key1) obj
        key1 @?= Right (Right "foo")
    , testCase "parsePropertyPath two key path default" $ do
        let obj = A.object []
        let key1 = A.parseEither (A.withObject "test parsePropertyPath" $ \o -> do
                let key1 = usePropertyByPath examplePath2 nestedPropertiesExample o
                return key1) obj
        key1 @?= Right "foo"
    , testCase "parsePropertyPath two key path not default" $ do
        let obj = A.object (toDefaultJSON nestedPropertiesExample2)
        let key1 = A.parseEither (A.withObject "test parsePropertyPath" $ \o -> do
                let key1 = usePropertyByPathEither examplePath2 nestedPropertiesExample o
                return key1) obj
        key1 @?= Right (Right "xxx")
    ]
    where
    nestedPropertiesExample = emptyProperties
        & definePropertiesProperty #parent "parent" (emptyProperties & defineStringProperty #foo "foo" "foo" & defineStringProperty #boo "boo" "boo")
        & defineStringProperty #baz "baz" "baz"

    nestedPropertiesExample2 = emptyProperties
        & definePropertiesProperty #parent "parent" (emptyProperties & defineStringProperty #foo "foo" "xxx")
        & defineStringProperty #baz "baz" "baz"

    examplePath1 = SingleKey #baz
    examplePath2 = ConsKeysPath #parent (SingleKey #foo)
