module B9.RepositorySpec
  ( spec,
    filterRepoImagesMapReturnsAllAndOnlyImagesSatisfieingTheFilterPredicate,
    filterRepoImagesMapReturnsAllAndOnlyReposSatisfieingTheFilterPredicate,
    allSharedImagesWithRepoReturnsAllNonEmptyRepos,
    allSharedImagesWithRepoReturnsOnlyReposContainedInTheParameter,
    allSharedImagesWithRepoReturnsAllImages,
    allSharedImagesWithRepoReturnsOnlyPairsSuchThatTheImageIsContainedInTheRepository,
    maxSharedImageOfAllReposReturnsNonNothingIfInputHasImages,
    maxSharedImageOfAllReposReturnsTheMaximumImage,
    maxSharedImageOfAllReposReturnsAValidRepoImagePair,
  )
where

import B9.DiskImages
import B9.Repository
import Data.Foldable (any)
import qualified Data.Map as Map
import Data.Maybe
import qualified Data.Set as Set
import Test.Hspec
import Test.QuickCheck

spec :: HasCallStack => Spec
spec =
  describe
    "Repository"
    ( do
        describe
          "dropAllButNLatestSharedImages"
          ( do
              it
                "returns a set that is disjunct with the result of keepNLatestSharedImages"
                ( property
                    ( \(Positive n) sharedImages ->
                        let dropped = dropAllButNLatestSharedImages n sharedImages
                            kept = keepNLatestSharedImages n sharedImages
                         in dropped `Set.disjoint` kept
                    )
                )
              it
                "returns a set with at most n * number of unique names entries, for all n >= 0"
                ( property
                    ( \(Positive n) sharedImages ->
                        let actual = keepNLatestSharedImages n sharedImages
                            noUniqueNames =
                              Set.size (Set.map sharedImageName sharedImages)
                         in Set.size actual <= n * noUniqueNames
                    )
                )
          )
        describe
          "keepNLatestSharedImages"
          ( do
              it
                "returns a set with a plausible number of elements"
                ( property
                    ( \n sharedImages ->
                        let actual = keepNLatestSharedImages n sharedImages
                            noUniqueNames =
                              Set.size (Set.map sharedImageName sharedImages)
                         in label
                              "number of output elements < (max 0 n) * number of unqiue image names"
                              (Set.size actual <= max 0 n * noUniqueNames)
                              .&&. classify
                                (n > 0)
                                "n > 0"
                                ( n > 0
                                    ==> label
                                      "the output contains as many sharedImageNames as the input"
                                      (Set.size (Set.map sharedImageName actual) === noUniqueNames)
                                    .&&. label
                                      "keepNLatestSharedImages with n == 1 returns exactly as many elements as there are unique names"
                                      (Set.size (keepNLatestSharedImages 1 sharedImages) === noUniqueNames)
                                    .&&. label
                                      "the newest entry in the output is the newest entry in the input"
                                      (Set.lookupMax actual === Set.lookupMax sharedImages)
                                )
                    )
                )
              it
                "returns the input unaltered if n >= number of input images"
                ( property
                    ( \(Positive n) sharedImages ->
                        let actual = keepNLatestSharedImages n sharedImages
                         in n >= Set.size sharedImages ==> sharedImages === actual
                    )
                )
              it
                "returns a set with at most n * number of unique names entries, for all n >= 0"
                ( property
                    ( \(Positive n) sharedImages ->
                        let actual = keepNLatestSharedImages n sharedImages
                            noUniqueNames =
                              Set.size (Set.map sharedImageName sharedImages)
                         in Set.size actual <= n * noUniqueNames
                    )
                )
          )
        describe
          "filterRepoImagesMap"
          ( do
              it
                "returns all- and only repos matching the repo predicate"
                (property (withMaxSuccess 20 filterRepoImagesMapReturnsAllAndOnlyReposSatisfieingTheFilterPredicate))
              it
                "returns all- and only images matching the image predicate"
                (property (withMaxSuccess 20 filterRepoImagesMapReturnsAllAndOnlyImagesSatisfieingTheFilterPredicate))
              it "is idempotent" $
                property
                  ( withMaxSuccess
                      20
                      ( \(Fun _ repoPred) (Fun _ p) repoImgMap ->
                          let expected = filterRepoImagesMap repoPred p repoImgMap
                              actual = filterRepoImagesMap repoPred p expected
                           in expected === actual
                      )
                  )
          )
        describe
          "lookupCachedImages"
          ( it
              "returns only shared images that are cached"
              (property lookupCachedImagesReturnsOnlyImagesFromCache)
          )
        describe
          "allSharedImagesWithRepo"
          ( do
              it
                "returns all repositories that are not empty"
                (property allSharedImagesWithRepoReturnsAllNonEmptyRepos)
              it
                "returns only repositories that are in the input"
                (property allSharedImagesWithRepoReturnsOnlyReposContainedInTheParameter)
              it
                "returns all images"
                (property allSharedImagesWithRepoReturnsAllImages)
              it
                "returns only pairs where the image is in the repository"
                (property allSharedImagesWithRepoReturnsOnlyPairsSuchThatTheImageIsContainedInTheRepository)
          )
        describe
          "maxSharedImageOfAllRepos"
          ( do
              it
                "returns a non-Nothing value of the input has any images and Nothing otherwise"
                (property maxSharedImageOfAllReposReturnsNonNothingIfInputHasImages)
              it
                "returns the maximum of all images"
                (property maxSharedImageOfAllReposReturnsTheMaximumImage)
              it
                "returns a pair where the image is in the repository"
                (property maxSharedImageOfAllReposReturnsAValidRepoImagePair)
          )
    )

matchesSomeButNotAll :: Foldable t => (a -> Bool) -> t a -> Bool
matchesSomeButNotAll p xs = any p xs && any (not . p) xs

filterRepoImagesMapReturnsAllAndOnlyImagesSatisfieingTheFilterPredicate ::
  Fun SharedImage Bool -> RepoImagesMap -> Property
filterRepoImagesMapReturnsAllAndOnlyImagesSatisfieingTheFilterPredicate (Fun _ p) t =
  let i' = runCodeUnderTest p
      i'Complement = runCodeUnderTest (not . p)
      i = allSharedImages t
      runCodeUnderTest = allSharedImages . flip (filterRepoImagesMap (const True)) t
   in classify
        (not (p `any` i))
        "predicate matches no image"
        (null i' && i'Complement == i)
        .||. classify
          (p `matchesSomeButNotAll` i)
          "predicate matches some images"
          ( p `all` i'
              && not (p `any` i'Complement)
              && Set.union i' i'Complement == i
              && null (Set.intersection i' i'Complement)
          )
        .||. classify
          (p `all` i)
          "predicate matches all images"
          (null i'Complement && i' == i)

filterRepoImagesMapReturnsAllAndOnlyReposSatisfieingTheFilterPredicate ::
  Fun Repository Bool -> RepoImagesMap -> Property
filterRepoImagesMapReturnsAllAndOnlyReposSatisfieingTheFilterPredicate (Fun _ p) t =
  let i' = runCodeUnderTest p
      i'Complement = runCodeUnderTest (not . p)
      i = allRepositories t
      runCodeUnderTest = allRepositories . flip (flip filterRepoImagesMap (const True)) t
   in classify
        (not (p `any` i))
        "predicate matches no repo"
        (null i' && i'Complement == i)
        .||. classify
          (p `matchesSomeButNotAll` i)
          "predicate matches some repos"
          ( p `all` i'
              && not (p `any` i'Complement)
              && Set.union i' i'Complement == i
              && null (Set.intersection i' i'Complement)
          )
        .||. classify
          (p `all` i)
          "predicate matches all repos"
          (null i'Complement && i' == i)

lookupCachedImagesReturnsOnlyImagesFromCache :: SharedImageName -> RepoImagesMap -> Property
lookupCachedImagesReturnsOnlyImagesFromCache sn table =
  let result = lookupCachedImages sn table
   in Set.intersection
        result
        (fromMaybe (Set.empty) (Map.lookup Cache table))
        === result

allSharedImagesWithRepoReturnsAllNonEmptyRepos :: RepoImagesMap -> Property
allSharedImagesWithRepoReturnsAllNonEmptyRepos t =
  let nonEmptyRepos =
        Map.foldrWithKey
          (\repo imgs acc -> if null imgs then acc else Set.insert repo acc)
          Set.empty
          t
      reposReturned =
        Set.map snd (allSharedImagesWithRepo t)
   in reposReturned === nonEmptyRepos

allSharedImagesWithRepoReturnsOnlyReposContainedInTheParameter ::
  RepoImagesMap -> Bool
allSharedImagesWithRepoReturnsOnlyReposContainedInTheParameter t =
  let allReposReturned = Set.map snd (allSharedImagesWithRepo t)
   in allReposReturned `Set.isSubsetOf` allRepositories t

allSharedImagesWithRepoReturnsAllImages :: RepoImagesMap -> Property
allSharedImagesWithRepoReturnsAllImages t =
  let allImagesReturned = Set.map fst (allSharedImagesWithRepo t)
   in allImagesReturned === allSharedImages t

allSharedImagesWithRepoReturnsOnlyPairsSuchThatTheImageIsContainedInTheRepository ::
  RepoImagesMap -> Bool
allSharedImagesWithRepoReturnsOnlyPairsSuchThatTheImageIsContainedInTheRepository t =
  let validPair (i, r) =
        maybe False (Set.member i) (Map.lookup r t)
   in all validPair (allSharedImagesWithRepo t)

maxSharedImageOfAllReposReturnsNonNothingIfInputHasImages :: RepoImagesMap -> Property
maxSharedImageOfAllReposReturnsNonNothingIfInputHasImages t =
  isJust (maxSharedImageOfAllRepos t) =/= null (allSharedImages t)

maxSharedImageOfAllReposReturnsTheMaximumImage :: RepoImagesMap -> Property
maxSharedImageOfAllReposReturnsTheMaximumImage t =
  not (null (allSharedImages t))
    ==> fmap fst (maxSharedImageOfAllRepos t) === Just (maximum (allSharedImages t))

maxSharedImageOfAllReposReturnsAValidRepoImagePair :: RepoImagesMap -> Property
maxSharedImageOfAllReposReturnsAValidRepoImagePair t =
  case maxSharedImageOfAllRepos t of
    Just (i, r) ->
      label
        "got result"
        (Just i === (Map.lookup r t >>= Set.lookupMax))
    Nothing ->
      label "got no result" True
