-- | This module implements a simplified, pure version of Test.Quickcheck's
-- quickCheck functionality.

-- Author: Bertram Felgenhauer
-- License: MIT

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE Safe #-}
{-# LANGUAGE CPP #-}

module Test.QuickCheck.Safe (
    -- * Checking properties
    quickCheck, quickCheckResult, quickCheckWith, quickCheckWithResult,
    -- * Creating and combining properties
    STestable(),
    (==>), (.||.), (.&&.), (.&.), (===),
    label, shrinking, noShrinking, mapSize,
    forAll, forAllShrink,
    -- * Miscellaneous
    inventQCGen,
    module Test.QuickCheck
) where

import Test.QuickCheck.Safe.Trusted

import Test.QuickCheck hiding (
    Testable(..), Property(..),
    (==>), (.||.), (.&&.), (.&.), (===),
    label, shrinking, noShrinking, mapSize,
    forAll, forAllShrink,
    classify, collect, conjoin, counterexample, cover, disjoin,
    expectFailure, once, printTestCase, verbose, within,
    quickCheck, quickCheckResult, quickCheckWith, quickCheckWithResult)
import Test.QuickCheck.Gen (Gen(..))
import Control.Monad
import qualified Data.Set as S
import qualified Data.Map as M

-- STestable and SProperty are simplified versions of Testable/Property
class STestable prop where
    sProperty :: prop -> SProperty

newtype SProperty = MkSProperty{ unSProperty :: Gen SResult }

data SResult
    = SOk                                -- success
    | SDiscard                           -- discarded sample
    | SFail{                             -- failed sample
        sLabels :: [String],             -- text describing counterexample
        sException :: Maybe AnException, -- caught exception, if any
        sSmaller :: [SResult]            -- results of shrunk examples
    }

instance STestable SProperty where
    sProperty prop = prop

instance STestable prop => STestable (Gen prop) where
    sProperty gen = MkSProperty $ gen >>= unSProperty . sProperty

-- instance STestable Discard where
--     sProperty _ = MkSProperty . return $ SDiscard

instance STestable Bool where
    sProperty b = MkSProperty . return $ case pureEvaluate b of
        Right True -> SOk
        Right _ -> SFail{ sLabels = [], sException = Nothing, sSmaller = [] }
        Left e -> SFail{ sLabels = [], sException = Just e, sSmaller = [] }

instance (Arbitrary a, Show a, STestable prop) => STestable (a -> prop) where
    sProperty = forAllShrink arbitrary shrink

-- | Implication. Cf. 'Test.QuickCheck.==>'.
(==>) :: STestable prop => Bool -> prop -> SProperty
t ==> p = case pureEvaluate t of
    Right True -> sProperty $ p
    Right _ -> MkSProperty . return $ SDiscard
    Left e -> MkSProperty . return $
        SFail{ sLabels = [], sException = Just e, sSmaller = [] }

-- | Equality test. Cf. 'Test.QuickCheck.==='.
(===) :: (Eq a, Show a) => a -> a -> SProperty
a === b = label (show a ++ " /= " ++ show b) $ sProperty (a == b)

-- | Conjunction. Cf. 'Test.QuickCheck..&&.'.
(.&&.) :: (STestable prop2, STestable prop1) => prop1 -> prop2 -> SProperty
prop1 .&&. prop2 = MkSProperty $ do
    res1 <- unSProperty $ label "LHS" $ prop1
    case res1 of
        SOk -> unSProperty $ label "RHS" $ prop2
        _ -> return res1

-- | Disjunction. Cf. 'Test.QuickCheck..||.'.
(.||.) :: (STestable prop2, STestable prop1) => prop1 -> prop2 -> SProperty
prop1 .||. prop2 = MkSProperty $ do
    res1 <- unSProperty . sProperty $ prop1
    res2 <- unSProperty . sProperty $ prop2
    let merge res1@SFail{ sSmaller = shr1 } res2@SFail{ sSmaller = shr2 } =
            SFail{
                sLabels = sLabels res1 ++ sLabels res2,
                sException = sException res1 `mplus` sException res2,
                sSmaller = map (`merge` res2) shr1 ++ map (res1 `merge`) shr2
            }
        merge res1 SFail{} = res1
        merge SFail{} res2 = res2
    return $ res1 `merge` res2

-- | Nondeterministic conjunction. Cf. 'Test.QuickCheck.&.'.
(.&.) :: (STestable prop2, STestable prop1) => prop1 -> prop2 -> SProperty
prop1 .&. prop2 = MkSProperty $ do
    c <- choose (0, 1)
    case c :: Int of
        0 -> unSProperty $ label "LHS" prop1
        1 -> unSProperty $ label "RHS" prop2

-- | Label tests. Cf. 'Test.QuickCheck.label'.
label :: STestable prop => String -> prop -> SProperty
label lab = MkSProperty . fmap (labelSResult lab) . unSProperty . sProperty

labelSResult :: String -> SResult -> SResult
labelSResult lab = mapSResultLabels (lab :)

mapSResultLabels :: ([String] -> [String]) -> SResult -> SResult
mapSResultLabels f res@SFail{} = res{
    sLabels = f (sLabels res),
    sSmaller = map (mapSResultLabels f) (sSmaller res)
 }
mapSResultLabels _ res = res

-- | Shrink counterexamples. Cf. 'Test.QuickCheck.shrinking'.
shrinking :: STestable prop => (a -> [a]) -> a -> (a -> prop) -> SProperty
shrinking shr x f = MkSProperty $ MkGen $ \seed size -> do
    let unfold x = case unGen (unSProperty . sProperty $ f x) seed size of
            res@SFail{ sSmaller = ps } ->
                res{ sSmaller = map unfold (shr x) ++ sSmaller res }
            res -> res
    unfold x

-- | Suppress shrinking of counterexamples. Cf. 'Test.QuickCheck.noShrinking'.
noShrinking :: STestable prop => prop -> SProperty
noShrinking prop = MkSProperty $ do
    res <- unSProperty . sProperty $ prop
    return $ case res of
        SFail{} -> res{ sSmaller = [] }
        _ -> res

-- | Universal quantification with shrinking.
-- Cf. 'Test.QuickCheck.forAllShrink'.
forAllShrink :: (Show a, STestable prop) =>
    Gen a -> (a -> [a]) -> (a -> prop) -> SProperty
forAllShrink gen shr f = MkSProperty $ do
    x <- gen
    unSProperty . label (show x) $ shrinking shr x f

-- | Universal quantification. Cf. 'Test.QuickCheck.forAll'.
forAll :: (Show a, STestable prop) => Gen a -> (a -> prop) -> SProperty
forAll gen = forAllShrink gen (const [])

-- | Adjust testcase sizes. Cf. 'Test.QuickCheck.mapSize'.
mapSize :: STestable prop => (Int -> Int) -> prop -> SProperty
mapSize f = MkSProperty . scale f . unSProperty . sProperty where
    scale f a = sized (\n -> resize (f n) a)

-- Other combinators that may be considered:

-- classify :: STestable prop => Bool -> String -> prop -> SProperty
-- collect :: (Show a, STestable prop) => a -> prop -> SProperty
-- conjoin :: STestable prop => [prop] -> SProperty
-- counterexample :: STestable prop => String -> prop -> SProperty
-- cover :: STestable prop => Bool -> Int -> String -> prop -> SProperty
-- disjoin :: STestable prop => [prop] -> SProperty
-- expectFailure :: STestable prop => prop -> SProperty
-- once :: STestable prop => prop -> SProperty
-- printTestCase :: STestable prop => String -> prop -> SProperty
-- verbose :: STestable prop => prop -> SProperty
-- within :: STestable prop => Int -> prop -> SProperty

-- | Cf. 'Test.QuickCheck.quickCheckWithResult'. Note that in contrast to
-- QuickCheck's function, this one takes an additional 'QCGen' argument.
quickCheckWithResult :: STestable prop => Args -> QCGen -> prop -> Result
quickCheckWithResult args seed prop = unGen (runTests 0 0 sizes) seed' 0 where
    runTests :: Int -> Int -> [Int] -> Gen Result
    runTests pass disc (size : sizes)
        | pass >= maxSuccess args =
            return Success{
                numTests = pass,
#if MIN_VERSION_QuickCheck(2,12,0)
                numDiscarded = disc,
                labels = M.empty,
                classes = M.empty,
                tables = M.empty,
#else
                labels = [],
#endif
                output = "+++ OK, passed " ++ show pass ++ " tests.\n"
             }
        | disc > (maxDiscardRatio args - 1) * maxSuccess args =
            return GaveUp{
                numTests = pass,
#if MIN_VERSION_QuickCheck(2,12,0)
                numDiscarded = disc,
                labels = M.empty,
                classes = M.empty,
                tables = M.empty,
#else
                labels = [],
#endif
                output = "*** Gave up! Passed only " ++ show pass ++ " tests.\n"
             }
        | otherwise = do
            (seed, _) <- MkGen (,)
            res <- resize size (unSProperty . sProperty $ prop)
            case res of
                SOk -> runTests (pass + 1) disc sizes
                SDiscard -> runTests pass (disc + 1) sizes
                SFail{} -> return $ deflate pass disc 0 0 0 seed size res

    deflate :: Int -> Int -> Int -> Int -> Int -> QCGen -> Int -> SResult -> Result
    deflate pass disc !shr !shrT !shrF seed size res@SFail{ sSmaller = [] } =
        Failure{
            numTests = pass,
            numShrinks = shr,
            numShrinkTries = shrT,
            numShrinkFinal = shrF,
            usedSeed = seed,
            usedSize = size,
            reason = reason,
            theException = sException res,
#if MIN_VERSION_QuickCheck(2,10,0)
            failingTestCase = sLabels res,
#endif
#if !MIN_VERSION_QuickCheck(2,12,0)
            labels = map (\x -> (x, 0)) (sLabels res),
#else
            numDiscarded = disc,
            failingLabels = sLabels res,
            failingClasses = S.empty,
#endif
#if MIN_VERSION_QuickCheck(2,16,0)
            witnesses = [],
#endif
            output = "*** Failed! " ++ reason ++
                  " (after " ++ count (pass + 1) "test" ++
                  (if shr > 0 then " and " ++ count shr "shrink" else "") ++
                  "):\n" ++ unlines (sLabels res)
        }
      where
        count i w = show i ++ " " ++ w ++ ['s' | i /= 1]
        reason = maybe "Falsifiable" (\e -> "Exception: '" ++ show e ++ "'") $
            sException res
    deflate pass disc shr shrT shrF seed size res@SFail{ sSmaller = res' : rs } =
        case res' of
            SFail{} -> deflate pass disc (shr + 1) (shrT + shrF) 0 seed size res'
            _ -> deflate pass disc shr shrT (shrF + 1) seed size res{ sSmaller = rs }

    sizes :: [Int]
    sizes = cycle [0..maxSize args]

    seed' :: QCGen
    seed' = maybe seed fst (replay args)

-- | Cf. 'Test.QuickCheck.quickCheckResult'. Note that in contrast to
-- QuickCheck's function, this one takes an additional 'QCGen' argument.
quickCheckResult :: STestable prop => QCGen -> prop -> Result
quickCheckResult = quickCheckWithResult stdArgs

-- | Cf. 'Test.QuickCheck.quickCheckWith'. Note that in contrast to
-- QuickCheck's function, this one takes an additional 'QCGen' argument.
quickCheckWith :: STestable prop => Args -> QCGen -> prop -> String
quickCheckWith args seed = output . quickCheckWithResult args seed

-- | Cf. 'Test.QuickCheck.quickCheck'. Note that in contrast to QuickCheck's
-- function, this one takes an additional 'QCGen' argument.
--
-- >>> putStr $ quickCheck (inventQCGen ()) (\x -> length (x :: [()]) < 10)
-- *** Failed! Falsifiable (after 18 tests and 3 shrinks):
-- [(),(),(),(),(),(),(),(),(),(),(),(),(),(),()]
quickCheck :: STestable prop => QCGen -> prop -> String
quickCheck = quickCheckWith stdArgs

