{-# LANGUAGE ScopedTypeVariables #-}
module Main (main) where

import Data.Functor.Compose                 (Compose (..))
import Data.Functor.Identity                (Identity (..))
import Data.Functor.Product                 (Product (..))
import Data.Functor.Sum                     (Sum (..))
import Data.List.NonEmpty                   (NonEmpty (..))
import Data.Semigroup
       (First (..), Last (..), Max (..), Min (..), Semigroup (..))
import Data.Tree                            (Tree (..))
import Test.Framework.Providers.API         (Test, TestName, testGroup)
import Test.Framework.Providers.QuickCheck2 (testProperty)
import Test.Framework.Runners.Console       (defaultMain)
import Test.QuickCheck
       (Arbitrary, Fun, Property, Testable, applyFun, applyFun2,
       counterexample, mapSize, (===))
import Test.QuickCheck.Poly                 (A, B, OrdA)

import Test.QuickCheck.Instances ()

import Data.Foldable     (Foldable (foldMap), toList)
import Data.Semifoldable

main :: IO ()
main = defaultMain
    [ foldable1tests "NonEmpty" (P1 :: P1 NonEmpty)
    , foldable1tests "Tree"     (P1 :: P1 Tree)
    , foldable1tests "Identity" (P1 :: P1 Identity)
    , foldable1tests "Compose"  (P1 :: P1 (Compose NonEmpty NonEmpty))
    , foldable1tests "Product"  (P1 :: P1 (Product NonEmpty NonEmpty))
    , foldable1tests "Sum"      (P1 :: P1 (Sum NonEmpty NonEmpty))
    ]

-------------------------------------------------------------------------------
-- tests
-------------------------------------------------------------------------------

foldable1tests
    :: forall f.
      ( Semifoldable f
      , Arbitrary (f A), Show (f A)
      , Arbitrary (f OrdA), Show (f OrdA)
      , Arbitrary (f [B]), Show (f [B])
      )
    => TestName
    -> P1 f
    -> Test
foldable1tests name _p = testGroup name
    [ testProperty "semifoldMap ~= foldMap" coherentFoldMap
    , testProperty "toList . toNonEmpty ~= toList" coherentToNonEmpty

    , testProperty "semifoldl non/strict" $ smaller strictFoldl1
    , testProperty "semifoldr non/strict" $ smaller strictFoldr1
    , testProperty "semifoldlMap non/strict" $ smaller semistrictFoldlMap
    , testProperty "semifoldrMap non/strict" $ smaller semistrictFoldrMap

    -- test against default implementations
    , testProperty "semifoldMap default" defaultFoldMap
    , testProperty "semifoldrMap default" $ smaller semidefaultFoldrMap
    , testProperty "semifoldlMap default" $ smaller semidefaultFoldlMap
    , testProperty "toNonEmpty default" defaultToNonEmpty

    , testProperty "semihead default" defaultHead1
    , testProperty "semilast default" defaultLast1
    , testProperty "semiminimum default" defaultMinimum1
    , testProperty "semimaximum default" defaultMaximum1

    -- if we first convert to nonEmpty it should be the same
    , testProperty "foldMap via toNonEmpty" viaFoldMap
    , testProperty "semifoldr via toNonEmpty" $ smaller viaFoldr1
    , testProperty "semifoldl via toNonEmpty" $ smaller viaFoldl1
    , testProperty "semifoldr' via toNonEmpty" $ smaller viaFoldr1'
    , testProperty "semifoldl' via toNonEmpty" $ smaller viaFoldl1'
    , testProperty "semihead via toNonEmpty" viaHead1
    , testProperty "semilast via toNonEmpty" viaLast1
    , testProperty "semiminimum via toNonEmpty" viaMinimum1
    , testProperty "semimaximum via toNonEmpty" viaMaximum1
    ]
  where
    -- Things like Compose NonEmpty NonEmpty are big
    smaller :: Testable prop => prop -> Property
    smaller = mapSize (`div` 3)

    coherentFoldMap :: f A -> Fun A [B] -> Property
    coherentFoldMap xs f' = foldMap f xs === semifoldMap f xs where
        f = applyFun f'

    coherentToNonEmpty :: f A -> Property
    coherentToNonEmpty xs = toList (toNonEmpty xs) === toList xs

    strictFoldr1 :: f [B] -> Fun ([B], [B]) [B] -> Property
    strictFoldr1 xs g' = semifoldr g xs === semifoldr' g xs where
        g = applyFun2 g'

    strictFoldl1 :: f [B] -> Fun ([B], [B]) [B] -> Property
    strictFoldl1 xs g' = semifoldl g xs === semifoldl' g xs where
        g = applyFun2 g'

    semistrictFoldrMap :: f A -> Fun A [B] -> Fun ([B], [B]) [B] -> Property
    semistrictFoldrMap xs f' g' = semifoldrMap f g xs === semifoldr'Map f g xs where
        f = applyFun f'
        g = applyFun2 g'

    semistrictFoldlMap :: f A -> Fun A [B] -> Fun ([B], [B]) [B] -> Property
    semistrictFoldlMap xs f' g' = semifoldlMap f g xs === semifoldl'Map f g xs where
        f = applyFun f'
        g = applyFun2 g'

    defaultFoldMap :: f A -> Fun A [B] -> Property
    defaultFoldMap xs f' = foldMap f xs === semifoldrMap f (Data.Semigroup.<>) xs where
        f = applyFun f'

    semidefaultFoldrMap :: f A -> Fun A [B] -> Fun ([B], [B]) [B] -> Property
    semidefaultFoldrMap xs f' g'
        = counterexample ("NE: " ++ show ys)
        $ semifoldrMap f g xs === semifoldrMap f g ys
      where
        f = applyFun f'
        g = applyFun2 g'
        ys = toNonEmpty xs

    semidefaultFoldlMap :: f A -> Fun A [B] -> Fun ([B], [B]) [B] -> Property
    semidefaultFoldlMap xs f' g'
        = counterexample ("NE: " ++ show ys)
        $ semifoldlMap f g xs === semifoldlMap f g ys
      where
        f = applyFun f'
        g = applyFun2 g'
        ys = toNonEmpty xs

    defaultToNonEmpty :: f A -> Property
    defaultToNonEmpty xs = toNonEmpty xs === semifoldMap (:|[]) xs

    defaultHead1 :: f A -> Property
    defaultHead1 xs = semihead xs === getFirst (semifoldMap First xs)

    defaultLast1 :: f A -> Property
    defaultLast1 xs = semilast xs === getLast (semifoldMap Last xs)

    defaultMinimum1 :: f OrdA -> Property
    defaultMinimum1 xs = semiminimum xs === getMin (semifoldMap Min xs)

    defaultMaximum1 :: f OrdA -> Property
    defaultMaximum1 xs = semimaximum xs === getMax (semifoldMap Max xs)

    viaFoldMap :: f A -> Fun A [B] -> Property
    viaFoldMap xs f' = foldMap f xs === foldMap f (toNonEmpty xs) where
        f = applyFun f'

    viaFoldr1 :: f [B] -> Fun ([B],[B]) [B] -> Property
    viaFoldr1 xs g' = semifoldr g xs === semifoldr g (toNonEmpty xs) where
        g = applyFun2 g'

    viaFoldr1' :: f [B] -> Fun ([B],[B]) [B] -> Property
    viaFoldr1' xs g' = semifoldr' g xs === semifoldr' g (toNonEmpty xs) where
        g = applyFun2 g'

    viaFoldl1 :: f [B] -> Fun ([B],[B]) [B] -> Property
    viaFoldl1 xs g' = semifoldl g xs === semifoldl g (toNonEmpty xs) where
        g = applyFun2 g'

    viaFoldl1' :: f [B] -> Fun ([B],[B]) [B] -> Property
    viaFoldl1' xs g' = semifoldl' g xs === semifoldl' g (toNonEmpty xs) where
        g = applyFun2 g'

    viaHead1 :: f A -> Property
    viaHead1 xs = semihead xs === semihead (toNonEmpty xs)

    viaLast1 :: f A -> Property
    viaLast1 xs = semilast xs === semilast (toNonEmpty xs)

    viaMinimum1 :: f OrdA -> Property
    viaMinimum1 xs = semiminimum xs === semiminimum (toNonEmpty xs)

    viaMaximum1 :: f OrdA -> Property
    viaMaximum1 xs = semimaximum xs === semimaximum (toNonEmpty xs)

-------------------------------------------------------------------------------
-- utilities
-------------------------------------------------------------------------------

-- Proxy of right kind
data P1 f
    = P1
    | Unused (f Int)
