{-# 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(..)
  , PropertyName(..)
  , PropertyConfig(..)
  , TestLimit(..)
  , DiscardLimit(..)
  , ShrinkLimit(..)
  , property
  , withTests
  , withDiscards
  , withShrinks

  -- * Group
  , Group(..)
  , GroupName(..)

  -- * Test
  , Test(..)
  , Log(..)
  , Failure(..)
  , Diff(..)
  , forAll
  , forAllWith
  , annotate
  , annotateShow
  , footnote
  , footnoteShow
  , discard
  , failure
  , success
  , assert
  , (===)

  , liftCatch
  , liftCatchIO
  , liftEither
  , liftExceptT

  , withCatch
  , withExceptT
  , 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.Semigroup (Semigroup)
import           Data.String (IsString)

import           Hedgehog.Internal.Distributive
import           Hedgehog.Internal.Exception
import           Hedgehog.Internal.Gen (Gen)
import qualified Hedgehog.Internal.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 property.
--
newtype PropertyName =
  PropertyName {
      unPropertyName :: String
    } deriving (Eq, Ord, Show, IsString, Semigroup)

-- | 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)

-- | A named collection of property tests.
--
data Group =
  Group {
      groupName :: !GroupName
    , groupProperties :: ![(PropertyName, Property)]
    }

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

--
-- 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 =
    Annotation (Maybe Span) String
  | Footnote 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

distributeTest :: Transformer t Test m => Test (t m) a -> t (Test m) a
distributeTest =
  hoist Test .
  distribute .
  hoist distribute .
  hoist (hoist distribute) .
  unTest

instance Distributive Test where
  type Transformer t Test m = (
      Transformer t Gen m
    , Transformer t (WriterT [Log]) (Gen m)
    , Transformer t (ExceptT Failure) (WriterT [Log] (Gen m))
    )

  distribute =
    distributeTest

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, HasCallStack) => Gen m a -> Test m a
forAll gen =
  withFrozenCallStack $ forAllWith showPretty gen

-- | Generates a random input for the test by running the provided generator.
--
--   /This is a the same as 'forAll' but allows the user to provide a custom/
--   /rendering function. This is useful for values which don't have a/
--   /'Show' instance./
--
forAllWith :: (Monad m, HasCallStack) => (a -> String) -> Gen m a -> Test m a
forAllWith render gen = do
  x <- Test . lift $ lift gen
  withFrozenCallStack $ annotate (render x)
  return x

-- | Annotates the source code with a message that might be useful for
--   debugging a test failure.
--
annotate :: (Monad m, HasCallStack) => String -> Test m ()
annotate x = do
  writeLog $ Annotation (getCaller callStack) x

-- | Annotates the source code with a value that might be useful for
--   debugging a test failure.
--
annotateShow :: (Monad m, Show a, HasCallStack) => a -> Test m ()
annotateShow x = do
  withFrozenCallStack $ annotate (showPretty x)

-- | Logs a message to be displayed as additional information in the footer of
--   the failure report.
--
footnote :: Monad m => String -> Test m ()
footnote =
  writeLog . Footnote

-- | Logs a value to be displayed as additional information in the footer of
--   the failure report.
--
footnoteShow :: (Monad m, Show a) => a -> Test m ()
footnoteShow =
  writeLog . Footnote . showPretty

-- | 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
    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 ->
    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 =
  withFrozenCallStack liftEither =<< lift (runExceptT m)

-- | Fails the test if the action throws an exception.
--
--   /The benefit of using this over 'lift' is that the location of the
--   exception will be shown in the output./
--
liftCatch :: (MonadCatch m, HasCallStack) => m a -> Test m a
liftCatch m =
  withFrozenCallStack liftEither =<< lift (tryAll m)

-- | Fails the test if the action throws an exception.
--
--   /The benefit of using this over 'liftIO' is that the location of the
--   exception will be shown in the output./
--
liftCatchIO :: (MonadIO m, HasCallStack) => IO a -> Test m a
liftCatchIO m =
  withFrozenCallStack liftEither =<< liftIO (tryAll m)

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

-- | Fails the test if the action throws an exception.
--
--   /The benefit of using this over simply letting the exception bubble up is
--   that the location of the closest 'withCatch' will be shown in the output./
--
withCatch :: (MonadCatch m, HasCallStack) => Test m a -> Test m a
withCatch m =
  withFrozenCallStack liftEither =<< tryAll 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.