    {-# 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