{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE AllowAmbiguousTypes #-}

-- | Monoid properties
--
-- You will need @TypeApplications@ to use these.
module Test.Validity.Monoid
    ( monoidSpecOnValid
    , monoidSpec
    , monoidSpecOnArbitrary
    , monoidSpecOnGen
    ) where

import Data.Data

import Data.GenValidity

import Test.Hspec
import Test.QuickCheck

import Test.Validity.Functions
import Test.Validity.Operations
import Test.Validity.Utils

memptyTypeStr ::
       forall a. Typeable a
    => String
memptyTypeStr = unwords ["mempty", "::", nameOf @a]

mappendTypeStr ::
       forall a. Typeable a
    => String
mappendTypeStr = unwords ["mappend", "::", an, "->", an, "->", an]
  where
    an = nameOf @a

mconcatTypeStr ::
       forall a. Typeable a
    => String
mconcatTypeStr = unwords ["mconcat", "::", "[" ++ an ++ "]", "->", an]
  where
    an = nameOf @a

-- | Standard test spec for properties of 'Monoid' instances for valid values
--
-- Example usage:
--
-- > monoidSpecOnValid @[Double]
monoidSpecOnValid ::
       forall a. (Show a, Eq a, Monoid a, Typeable a, GenValid a)
    => Spec
monoidSpecOnValid = monoidSpecOnGen @a genValid "valid" shrinkValid

-- | Standard test spec for properties of 'Monoid' instances for unchecked values
--
-- Example usage:
--
-- > monoidSpec @[Int]
monoidSpec ::
       forall a. (Show a, Eq a, Monoid a, Typeable a, GenUnchecked a)
    => Spec
monoidSpec = monoidSpecOnGen @a genUnchecked "unchecked" shrinkUnchecked

-- | Standard test spec for properties of 'Monoid' instances for arbitrary values
--
-- Example usage:
--
-- > monoidSpecOnArbitrary @[Int]
monoidSpecOnArbitrary ::
       forall a. (Show a, Eq a, Monoid a, Typeable a, Arbitrary a)
    => Spec
monoidSpecOnArbitrary = monoidSpecOnGen @a arbitrary "arbitrary" shrink

-- | Standard test spec for properties of Monoid instances for values generated by a given generator (and name for that generator).
--
-- Example usage:
--
-- > monoidSpecOnGen (pure "a") "singleton list of 'a'"
monoidSpecOnGen ::
       forall a. (Show a, Eq a, Monoid a, Typeable a)
    => Gen a
    -> String
    -> (a -> [a])
    -> Spec
monoidSpecOnGen gen genname s =
    parallel $ do
        let name = nameOf @a
            memptystr = memptyTypeStr @a
            mappendstr = mappendTypeStr @a
            mconcatstr = mconcatTypeStr @a
            gen3 = (,,) <$> gen <*> gen <*> gen
            s3 (a, b, c) = (,,) <$> s a <*> s b <*> s c
            genl = genListOf gen
            sl = shrinkList s
        describe ("Monoid " ++ name) $ do
            let mem = mempty @a
                mapp = mappend @a
                mcon = mconcat @a
            describe memptystr $
                it
                    (unwords
                         [ "is the identity for"
                         , mappendstr
                         , "for"
                         , genDescr @a genname
                         ]) $
                identityOnGen mapp mem gen s
            describe mappendstr $
                it
                    (unwords
                         [ "is an associative operation for"
                         , genDescr @(a, a, a) genname
                         ]) $
                associativeOnGens mapp gen3 s3
            describe mconcatstr $
                it
                    (unwords
                         [ "is equivalent to its default implementation for"
                         , genDescr @[a] genname
                         ]) $
                equivalentOnGen mcon (foldr mapp mem) genl sl
