{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DoAndIfThenElse #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-} -- MonadBase
module Hedgehog.Internal.Property (
  -- * Property
    Property(..)
  , GroupName(..)
  , PropertyName(..)
  , PropertyConfig(..)
  , TestLimit(..)
  , DiscardLimit(..)
  , ShrinkLimit(..)
  , property
  , withTests
  , withDiscards
  , withShrinks

  -- * Test
  , Test(..)
  , Log(..)
  , Failure(..)
  , Diff(..)
  , forAll
  , info
  , discard
  , failure
  , success
  , assert
  , (===)

  , liftEither
  , liftExceptT
  , withResourceT

  -- * Internal
  -- $internal
  , defaultConfig
  , mapConfig
  , failWith
  , writeLog
  , runTest
  ) where

import           Control.Applicative (Alternative(..))
import           Control.Monad (MonadPlus(..))
import           Control.Monad.Base (MonadBase(..))
import           Control.Monad.Catch (MonadThrow(..), MonadCatch(..))
import           Control.Monad.Error.Class (MonadError(..))
import           Control.Monad.IO.Class (MonadIO(..))
import           Control.Monad.Morph (MFunctor(..))
import           Control.Monad.Primitive (PrimMonad(..))
import           Control.Monad.Reader.Class (MonadReader(..))
import           Control.Monad.State.Class (MonadState(..))
import           Control.Monad.Trans.Class (MonadTrans(..))
import           Control.Monad.Trans.Except (ExceptT(..), runExceptT)
import           Control.Monad.Trans.Resource (MonadResource(..), MonadResourceBase)
import           Control.Monad.Trans.Resource (ResourceT, runResourceT)
import           Control.Monad.Trans.Writer.Lazy (WriterT(..))
import           Control.Monad.Writer.Class (MonadWriter(..))

import           Data.Typeable (Typeable, TypeRep, typeOf)

import           Hedgehog.Gen (Gen)
import qualified Hedgehog.Gen as Gen
import           Hedgehog.Internal.Show
import           Hedgehog.Internal.Source

import           Language.Haskell.TH.Lift (deriveLift)

------------------------------------------------------------------------

-- | A property test to check, along with some configurable limits like how
--   many times to run the test.
--
data Property =
  Property {
      propertyConfig :: !PropertyConfig
    , propertyTest :: Test IO ()
    }

-- | A property test.
--
newtype Test m a =
  Test {
      unTest :: ExceptT Failure (WriterT [Log] (Gen m)) a
    } deriving (Functor, Applicative)

-- | The name of a group of properties.
--
newtype GroupName =
  GroupName {
      unGroupName :: String
    } deriving (Eq, Ord, Show)

-- | The name of a property.
--
newtype PropertyName =
  PropertyName {
      unPropertyName :: String
    } deriving (Eq, Ord, Show)

-- | Configuration for a property test.
--
data PropertyConfig =
  PropertyConfig {
      propertyTestLimit :: !TestLimit
    , propertyDiscardLimit :: !DiscardLimit
    , propertyShrinkLimit :: !ShrinkLimit
    } deriving (Eq, Ord, Show)

-- | The number of successful tests that need to be run before a property test
--   is considered successful.
--
newtype TestLimit =
  TestLimit Int
  deriving (Eq, Ord, Show, Num, Enum, Real, Integral)

-- | The number of shrinks to try before giving up on shrinking.
--
newtype ShrinkLimit =
  ShrinkLimit Int
  deriving (Eq, Ord, Show, Num, Enum, Real, Integral)

-- | The number of discards to allow before giving up.
--
newtype DiscardLimit =
  DiscardLimit Int
  deriving (Eq, Ord, Show, Num, Enum, Real, Integral)

--
-- FIXME This whole Log/Failure thing could be a lot more structured to allow
-- FIXME for richer user controlled error messages, think Doc. Ideally we'd
-- FIXME allow user's to create their own diffs anywhere.
--

-- | Log messages which are recorded during a test run.
--
data Log =
    Info String
  | Input (Maybe Span) TypeRep String
    deriving (Eq, Show)

-- | Details on where and why a test failed.
--
data Failure =
  Failure (Maybe Span) String (Maybe Diff)
  deriving (Eq, Show)

-- | The difference between some expected and actual value.
--
data Diff =
  Diff {
      diffPrefix :: String
    , diffRemoved :: String
    , diffInfix :: String
    , diffAdded :: String
    , diffSuffix :: String
    , diffValue :: ValueDiff
    } deriving (Eq, Show)

instance Monad m => Monad (Test m) where
  return =
    Test . return

  (>>=) m k =
    Test $
      unTest m >>=
      unTest . k

  fail err =
    Test . ExceptT . pure . Left $ Failure Nothing err Nothing

instance Monad m => MonadPlus (Test m) where
  mzero =
    discard

  mplus x y =
    Test . ExceptT . WriterT $
      mplus (runTest x) (runTest y)

instance Monad m => Alternative (Test m) where
  empty =
    mzero
  (<|>) =
    mplus

instance MonadTrans Test where
  lift =
    Test . lift . lift . lift

instance MFunctor Test where
  hoist f =
    Test . hoist (hoist (hoist f)) . unTest

instance PrimMonad m => PrimMonad (Test m) where
  type PrimState (Test m) =
    PrimState m
  primitive =
    lift . primitive

instance MonadIO m => MonadIO (Test m) where
  liftIO =
    lift . liftIO

instance MonadBase b m => MonadBase b (Test m) where
  liftBase =
    lift . liftBase

instance MonadThrow m => MonadThrow (Test m) where
  throwM =
    lift . throwM

instance MonadCatch m => MonadCatch (Test m) where
  catch m onErr =
    Test $
      (unTest m) `catch`
      (unTest . onErr)

instance MonadReader r m => MonadReader r (Test m) where
  ask =
    lift ask
  local f m =
    Test $
      local f (unTest m)

instance MonadState s m => MonadState s (Test m) where
  get =
    lift get
  put =
    lift . put
  state =
    lift . state

-- FIXME instance MonadWriter Test

instance MonadError e m => MonadError e (Test m) where
  throwError =
    lift . throwError
  catchError m onErr =
    Test . ExceptT $
      (runExceptT $ unTest m) `catchError`
      (runExceptT . unTest . onErr)

instance MonadResource m => MonadResource (Test m) where
  liftResourceT =
    lift . liftResourceT

------------------------------------------------------------------------
-- Property

-- | The default configuration for a property test.
--
defaultConfig :: PropertyConfig
defaultConfig =
  PropertyConfig {
      propertyTestLimit =
        100
    , propertyDiscardLimit =
        100
    , propertyShrinkLimit =
        1000
    }

-- | Map a config modification function over a property.
--
mapConfig :: (PropertyConfig -> PropertyConfig) -> Property -> Property
mapConfig f (Property cfg t) =
  Property (f cfg) t

-- | Set the number times a property should be executed before it is considered
--   successful.
--
withTests :: TestLimit -> Property -> Property
withTests n =
  mapConfig $ \config -> config { propertyTestLimit = n }

-- | Set the number times a property is allowed to discard before the test
--   runner gives up.
--
withDiscards :: DiscardLimit -> Property -> Property
withDiscards n =
  mapConfig $ \config -> config { propertyDiscardLimit = n }

-- | Set the number times a property is allowed to shrink before the test
--   runner gives up and prints the counterexample.
--
withShrinks :: ShrinkLimit -> Property -> Property
withShrinks n =
  mapConfig $ \config -> config { propertyShrinkLimit = n }

-- | Creates a property to check.
--
property :: Test IO () -> Property
property =
  Property defaultConfig

------------------------------------------------------------------------
-- Test

runTest :: Test m a -> Gen m (Either Failure a, [Log])
runTest =
  runWriterT . runExceptT . unTest

writeLog :: Monad m => Log -> Test m ()
writeLog =
  Test . lift . tell . pure

-- | Generates a random input for the test by running the provided generator.
--
forAll :: (Monad m, Show a, Typeable a, HasCallStack) => Gen m a -> Test m a
forAll gen = do
  x <- Test . lift $ lift gen
  writeLog $ Input (getCaller callStack) (typeOf x) (showPretty x)
  return x

-- | Logs an information message to be displayed if the test fails.
--
info :: Monad m => String -> Test m ()
info =
  writeLog . Info

-- | Discards a test entirely.
--
discard :: Monad m => Test m a
discard =
  Test . lift $ lift Gen.discard

-- | Fail with an error message, useful for building other failure combinators.
--
failWith :: (Monad m, HasCallStack) => Maybe Diff -> String -> Test m a
failWith diff msg =
  Test . ExceptT . pure . Left $ Failure (getCaller callStack) msg diff

-- | Causes a test to fail.
--
failure :: (Monad m, HasCallStack) => Test m a
failure =
  withFrozenCallStack $ failWith Nothing ""

-- | Another name for @pure ()@.
--
success :: Monad m => Test m ()
success =
  Test $ pure ()

-- | Fails the test if the condition provided is 'False'.
--
assert :: (Monad m, HasCallStack) => Bool -> Test m ()
assert b =
  if b then
    success
  else do
    withFrozenCallStack failure

infix 4 ===

-- | Fails the test if the two arguments provided are not equal.
--
(===) :: (Monad m, Eq a, Show a, HasCallStack) => a -> a -> Test m ()
(===) x y =
  if x == y then
    success
  else
    case valueDiff <$> mkValue x <*> mkValue y of
      Nothing ->
        withFrozenCallStack $
          failWith Nothing $ unlines [
              "━━━ Not Equal ━━━"
            , showPretty x
            , showPretty y
            ]
      Just diff ->
        withFrozenCallStack $
          failWith (Just $ Diff "Failed (" "- lhs" "=/=" "+ rhs" ")" diff) ""

-- | Fails the test if the 'Either' is 'Left', otherwise returns the value in
--   the 'Right'.
--
liftEither :: (Monad m, Show x, HasCallStack) => Either x a -> Test m a
liftEither = \case
  Left x -> do
    withFrozenCallStack $ failWith Nothing $ showPretty x
  Right x ->
    pure x

-- | Fails the test if the 'ExceptT' is 'Left', otherwise returns the value in
--   the 'Right'.
--
liftExceptT :: (Monad m, Show x, HasCallStack) => ExceptT x m a -> Test m a
liftExceptT m = do
  withFrozenCallStack liftEither =<< lift (runExceptT m)

-- | Run a computation which requires resource acquisition / release.
--
--   /Note that if you 'Control.Monad.Trans.Resource.allocate' anything before/
--   /a 'forAll' you will likely encounter unexpected behaviour, due to the way/
--   /'ResourceT' interacts with the control flow introduced by shrinking./
--
withResourceT :: MonadResourceBase m => Test (ResourceT m) a -> Test m a
withResourceT =
  hoist runResourceT

------------------------------------------------------------------------
-- FIXME Replace with DeriveLift when we drop 7.10 support.

$(deriveLift ''GroupName)
$(deriveLift ''PropertyName)
$(deriveLift ''PropertyConfig)
$(deriveLift ''TestLimit)
$(deriveLift ''ShrinkLimit)
$(deriveLift ''DiscardLimit)

------------------------------------------------------------------------
-- Internal

-- $internal
--
-- These functions are exported in case you need them in a pinch, but are not
-- part of the public API and may change at any time, even as part of a minor
-- update.