--------------------------------------------------------------------
-- |
-- Module    : Test.Framework.Providers.SmallCheck
-- Copyright : (c) Roman Cheplyaka
-- License   : BSD3
-- Maintainer: Roman Cheplyaka <roma@ro-che.info>
--
-- This module allows to use SmallCheck properties in test-framework.
--------------------------------------------------------------------
{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, FlexibleContexts,
             TypeOperators #-}
module Test.Framework.Providers.SmallCheck
    ( testProperty
    , withDepth
    ) where

import Test.Framework.Providers.API
import qualified Test.SmallCheck as SC
import qualified Test.SmallCheck.Drivers as SC
import Test.SmallCheck.Drivers
import Data.Maybe
import Data.List
import Data.Monoid
import Data.IORef
import qualified Control.Monad.IO.Class as T
import System.Timeout
import Control.Concurrent.Chan
import Control.Applicative

-- | Create a 'Test' for a SmallCheck 'SC.Testable' property
-- testProperty :: TestName -> (forall m . T.MonadIO m => SC.Testable m a) -> Test
testProperty :: SC.Testable IO a => TestName -> a -> Test
testProperty name prop = Test name $ (SC.test prop :: SC.Property IO)

-- | Change the default maximum test depth for a given 'Test'.
--
-- This is a simple wrapper around 'plusTestOptions'.
withDepth :: SC.Depth -> Test -> Test
withDepth d = plusTestOptions mempty { topt_maximum_test_depth = Just d }

data Result
    = Timeout
    | Pass
    | Fail SC.PropertyFailure

instance Show Result where
    show Timeout  = "Timed out"
    show Pass     = "OK"
    show (Fail s) = ppFailure s

instance TestResultlike Int Result where
    testSucceeded Pass = True
    testSucceeded _    = False

instance Testlike Int Result (SC.Property IO) where
  testTypeName _ = "Properties"

  runTest topts prop = do
    let
      timeoutAmount = unK $ topt_timeout topts
      depth = unK $ topt_maximum_test_depth topts

    chan <- newChan

    -- Execute the test, writing () to the channel after completion of each
    -- individual test
    let
      action = do
        mb_result <- timeout (fromMaybe (-1) timeoutAmount) $ smallCheckWithHook depth (const $ writeChan chan (Left ())) prop
        writeChan chan $ Right $
          case mb_result of
            Nothing -> Timeout
            Just Nothing -> Pass
            Just (Just x) -> Fail x

    improving <- reifyListToImproving . accumulate <$> getChanContents chan

    return (improving, action)

accumulate :: [Either () a] -> [Either Int a]
accumulate xs =
  (\f -> snd $ mapAccumL f 0 xs) $
  \n e ->
    case e of
      Left {} ->
        let n' = n+1
        in n' `seq` (n', Left n')
      Right x -> (n, Right x)

-- Copy-pasted from test-framework (because it's not exported)
reifyListToImproving :: [Either i f] -> (i :~> f)
reifyListToImproving (Left improvement:rest) = Improving improvement (reifyListToImproving rest)
reifyListToImproving (Right final:_)         = Finished final
reifyListToImproving []                      = error "reifyListToImproving: list finished before a final value arrived"