{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}

-- | Test suite for Stack.Dot
module Stack.DotSpec
  ( dummyPayload
  , spec
  , sublistOf
  , pkgName
  , stubLoader
  ) where

import           Data.List ((\\))
import qualified Data.Map as Map
import qualified Data.Set as Set
import           Distribution.License ( License (BSD3) )
import qualified RIO.Text as T
import           Stack.DependencyGraph ( pruneGraph, resolveDependencies )
import           Stack.Prelude hiding ( pkgName )
import           Stack.Types.DependencyTree ( DotPayload (..) )
import           Test.Hspec ( Spec, describe, it, shouldBe )
import           Test.Hspec.QuickCheck ( prop )
import           Test.QuickCheck ( Gen, choose, forAll )

dummyPayload :: DotPayload
dummyPayload = DotPayload (parseVersion "0.0.0.0") (Just (Right BSD3)) Nothing

spec :: Spec
spec = do
  let graph =
         Map.mapKeys pkgName
       . fmap (\p -> (Set.map pkgName p, dummyPayload))
       . Map.fromList $ [("one",Set.fromList ["base","free"])
                        ,("two",Set.fromList ["base","free","mtl","transformers","one"])
                        ]
  describe "Stack.Dot" $ do
    it "does nothing if depth is 0" $
      resolveDependencies (Just 0) graph stubLoader `shouldBe` pure graph

    it "with depth 1, more dependencies are resolved" $ do
      let graph' = Map.insert (pkgName "cycle")
                              (Set.singleton (pkgName "cycle"), dummyPayload)
                              graph
          resultGraph = runIdentity (resolveDependencies (Just 0) graph stubLoader)
          resultGraph' = runIdentity (resolveDependencies (Just 1) graph' stubLoader)
      Map.size resultGraph < Map.size resultGraph' `shouldBe` True

    it "cycles are ignored" $ do
       let graph' = Map.insert (pkgName "cycle")
                               (Set.singleton (pkgName "cycle"), dummyPayload)
                                graph
           resultGraph = resolveDependencies Nothing graph stubLoader
           resultGraph' = resolveDependencies Nothing graph' stubLoader
       fmap Map.size resultGraph' `shouldBe` fmap ((+1) . Map.size) resultGraph

    let graphElem e = Set.member e . Set.unions . Map.elems

    prop "requested packages are pruned" $ do
      let resolvedGraph = runIdentity (resolveDependencies Nothing graph stubLoader)
          allPackages g = Map.keysSet g `Set.union` foldMap fst g
      forAll (sublistOf (Set.toList (allPackages resolvedGraph))) $ \toPrune ->
        let pruned = pruneGraph [pkgName "one", pkgName "two"] toPrune resolvedGraph
        in  Set.null (allPackages pruned `Set.intersection` Set.fromList toPrune)

    prop "pruning removes orphans" $ do
      let resolvedGraph = runIdentity (resolveDependencies Nothing graph stubLoader)
          allPackages g = Map.keysSet g `Set.union` foldMap fst g
          orphans g = Map.filterWithKey (\k _ -> not (graphElem k g)) g
      forAll (sublistOf (Set.toList (allPackages resolvedGraph))) $ \toPrune ->
        let pruned = pruneGraph [pkgName "one", pkgName "two"] toPrune resolvedGraph
        in  null (Map.keys (orphans (fmap fst pruned)) \\ [pkgName "one", pkgName "two"])

{- Helper functions below -}
-- Backport from QuickCheck 2.8 to 2.7.6
sublistOf :: [a] -> Gen [a]
sublistOf = filterM (\_ -> choose (False, True))

-- Unsafe internal helper to create a package name
pkgName :: Text -> PackageName
pkgName = fromMaybe failure . parsePackageName . T.unpack
  where
   failure = error "Internal error during package name creation in DotSpec.pkgName"

-- Stub, simulates the function to load package dependencies
stubLoader :: PackageName -> Identity (Set PackageName, DotPayload)
stubLoader name = pure $ (, dummyPayload) . Set.fromList . map pkgName $
  case show name of
    "StateVar" -> ["stm", "transformers"]
    "array" -> []
    "bifunctors" -> ["semigroupoids", "semigroups", "tagged"]
    "binary" -> ["array", "bytestring", "containers"]
    "bytestring" -> ["deepseq", "ghc-prim", "integer-gmp"]
    "comonad" -> [ "containers", "contravariant", "distributive", "semigroups"
                 , "tagged","transformers","transformers-compat"
                 ]
    "cont" -> [ "StateVar", "semigroups", "transformers", "transformers-compat"
              , "void"
              ]
    "containers" -> ["array", "deepseq", "ghc-prim"]
    "deepseq" -> ["array"]
    "distributive" -> [ "ghc-prim", "tagged", "transformers"
                      , "transformers-compat"
                      ]
    "free" -> [ "bifunctors", "comonad", "distributive", "mtl", "prelude-extras"
              , "profunctors", "semigroupoids", "semigroups", "template-haskell"
              , "transformers"
              ]
    "ghc" -> []
    "hashable" -> ["bytestring", "ghc-prim", "integer-gmp", "text"]
    "integer" -> []
    "mtl" -> ["transformers"]
    "nats" -> []
    "one" -> ["free"]
    "prelude" -> []
    "profunctors" -> [ "comonad", "distributive", "semigroupoids", "tagged"
                     , "transformers"
                     ]
    "semigroupoids" -> [ "comonad", "containers", "contravariant"
                       , "distributive", "semigroups", "transformers"
                       , "transformers-compat"
                       ]
    "semigroups" -> [ "bytestring", "containers", "deepseq", "hashable", "nats"
                    , "text", "unordered-containers"
                    ]
    "stm" -> ["array"]
    "tagged" -> ["template-haskell"]
    "template" -> []
    "text" -> [ "array", "binary", "bytestring", "deepseq", "ghc-prim"
              , "integer-gmp"
              ]
    "transformers" -> []
    "two" -> ["free", "mtl", "one", "transformers"]
    "unordered" -> ["deepseq", "hashable"]
    "void" -> ["ghc-prim", "hashable", "semigroups"]
    _ -> []
