{-# LANGUAGE CPP #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeSynonymInstances #-}

module Test.Hspec.Core.Example (
-- RE-EXPORTED from Test.Hspec.Core.Spec
  Example (..)
#ifndef ENABLE_SPEC_HOOK_ARGS
, Arg
#endif
, Params (..)
, defaultParams
, ActionWith
, Progress
, ProgressCallback
, Result(..)
, ResultStatus (..)
, Location (..)
, FailureReason (..)
, safeEvaluate
, safeEvaluateExample
-- END RE-EXPORTED from Test.Hspec.Core.Spec
, safeEvaluateResultStatus
, exceptionToResultStatus
, toLocation
, hunitFailureToResult
) where

import           Prelude ()
import           Test.Hspec.Core.Compat

import qualified Test.HUnit.Lang as HUnit

import           Data.CallStack (SrcLoc(..))

import           Control.DeepSeq
import qualified Test.QuickCheck as QC
import           Test.Hspec.Expectations (Expectation)

import           Test.Hspec.Core.Util
import           Test.Hspec.Core.QuickCheck.Util (liftHook)
import           Test.Hspec.Core.Example.Location

#ifdef ENABLE_SPEC_HOOK_ARGS
-- | A type class for examples, that is to say, test bodies as used in
-- `Test.Hspec.Core.Spec.it` and similar functions.
class Example e where
  -- | The argument type that is needed to run this `Example`.
  -- If `Arg` is @()@, no argument is required and the `Example` can be run
  -- as-is.
  --
  -- The value of `Arg` is the difference between `Test.Hspec.Core.Spec.Spec`
  -- (aka @`Test.Hspec.Core.Hspec.SpecWith` ()@), which can be executed, and
  -- @`Test.Hspec.Core.Spec.SpecWith` a@, which cannot be executed without
  -- turning it into `Test.Hspec.Core.Spec.Spec` first.
  --
  -- To supply an argument to examples, use the functions in
  -- "Test.Hspec.Core.Hooks" such as `Test.Hspec.Core.Hooks.around',
  -- `Test.Hspec.Core.Hooks.before', `Test.Hspec.Core.Hooks.mapSubject' and
  -- similar.
  type Arg e
  type Arg e = ()
#else
type Arg e = ()

-- | A type class for examples, that is to say, test bodies as used in
-- `Test.Hspec.Core.Spec.it` and similar functions.
class Example e where
#endif

  -- | Evaluates an example.
  --
  -- `evaluateExample` is expected to execute the test body inside the IO action
  -- passed to the hook. It's often necessary to use an `IORef` to pass data
  -- out like whether the test succeeded to the outer IO action so it can be
  -- returned as a `Result`.
  --
  -- Example:
  --
  -- @
  -- newtype MyAction = MyAction (Int -> IO Bool)
  --
  -- instance Example MyAction where
  --   type Arg MyAction = Int
  --
  --   evaluateExample (MyAction act) _params hook _progress = do
  --     result <- newIORef (Result "" Success)
  --     hook $ \arg -> do
  --       -- e.g. determines if arg is 42
  --       ok <- act arg
  --       let result' = Result "" $ if ok then Success else Failure Nothing NoReason
  --       writeIORef result result'
  --     readIORef result
  -- @
  evaluateExample
    :: e
    -- ^ The example being evaluated
    -> Params
    -- ^ QuickCheck/SmallCheck settings
    -> (ActionWith (Arg e) -> IO ())
    -- ^ Hook: takes an @`ActionWith` (`Arg` e)@, namely, the IO action to run
    -- the test body, obtains @`Arg` e@ from somewhere, then executes the test
    -- body (or possibly decides not to execute it!).
    --
    -- This is used to implement `Test.Hspec.Core.Hooks.around` and similar
    -- hook functionality.
    -> ProgressCallback
    -- ^ Callback for composite tests like QuickCheck to report their progress.
    -> IO Result

-- | QuickCheck and SmallCheck related parameters.
data Params = Params {
  paramsQuickCheckArgs  :: QC.Args
, paramsSmallCheckDepth :: Maybe Int
} deriving (Show)

defaultParams :: Params
defaultParams = Params {
  paramsQuickCheckArgs = QC.stdArgs
, paramsSmallCheckDepth = Nothing
}

-- | @(CurrentItem, TotalItems)@ tuple.
type Progress = (Int, Int)
-- | Callback used by composite test items that contain many tests to report
-- their progress towards finishing them all.
--
-- This is used, for example, to report how many QuickCheck examples are finished.
type ProgressCallback = Progress -> IO ()

-- | An `IO` action that expects an argument of type @a@.
--
-- This type is what `Example`s are ultimately unlifted into for execution.
type ActionWith a = a -> IO ()

-- | The result of running an example
data Result = Result {
  resultInfo :: String
, resultStatus :: ResultStatus
} deriving Show

data ResultStatus =
    Success
  | Pending (Maybe Location) (Maybe String)
  | Failure (Maybe Location) FailureReason
  deriving Show

data FailureReason =
    NoReason
  | Reason String
  | ColorizedReason String
  | ExpectedButGot (Maybe String) String String
  | Error (Maybe String) SomeException
  deriving Show

instance NFData FailureReason where
  rnf reason = case reason of
    NoReason -> ()
    Reason r -> r `deepseq` ()
    ColorizedReason r -> r `deepseq` ()
    ExpectedButGot p e a  -> p `deepseq` e `deepseq` a `deepseq` ()
    Error m e -> m `deepseq` show e `deepseq` ()

instance Exception ResultStatus

forceResult :: Result -> Result
forceResult r@(Result info status) = info `deepseq` (forceResultStatus status) `seq` r

forceResultStatus :: ResultStatus -> ResultStatus
forceResultStatus r = case r of
  Success -> r
  Pending _ m -> m `deepseq` r
  Failure _ m -> m `deepseq` r

safeEvaluateExample :: Example e => e -> Params -> (ActionWith (Arg e) -> IO ()) -> ProgressCallback -> IO Result
safeEvaluateExample example params around = safeEvaluate . evaluateExample example params around

safeEvaluate :: IO Result -> IO Result
safeEvaluate action = do
  r <- safeTry $ forceResult <$> action
  case r of
    Left e -> Result "" <$> exceptionToResultStatus e
    Right result -> return result

safeEvaluateResultStatus :: IO ResultStatus -> IO ResultStatus
safeEvaluateResultStatus action = do
  r <- safeTry $ forceResultStatus <$> action
  case r of
    Left e -> exceptionToResultStatus e
    Right status -> return status

exceptionToResultStatus :: SomeException -> IO ResultStatus
exceptionToResultStatus = safeEvaluateResultStatus . pure . toResultStatus
  where
    toResultStatus :: SomeException -> ResultStatus
    toResultStatus e
      | Just result <- fromException e = result
      | Just hunit <- fromException e = hunitFailureToResult Nothing hunit
      | otherwise = Failure Nothing $ Error Nothing e

instance Example Result where
#ifdef ENABLE_SPEC_HOOK_ARGS
  type Arg Result = ()
#endif
  evaluateExample e = evaluateExample (\() -> e)

#ifdef ENABLE_SPEC_HOOK_ARGS
instance Example (a -> Result) where
  type Arg (a -> Result) = a
#else
instance Example (() -> Result) where
#endif
  evaluateExample example _params hook _callback = do
    liftHook (Result "" Success) hook (evaluate . example)

instance Example Bool where
#ifdef ENABLE_SPEC_HOOK_ARGS
  type Arg Bool = ()
#endif
  evaluateExample e = evaluateExample (\() -> e)

#ifdef ENABLE_SPEC_HOOK_ARGS
instance Example (a -> Bool) where
  type Arg (a -> Bool) = a
#else
instance Example (() -> Bool) where
#endif
  evaluateExample p _params hook _callback = do
    liftHook (Result "" Success) hook (evaluate . example)
    where
      example a
        | p a = Result "" Success
        | otherwise = Result "" $ Failure Nothing NoReason

instance Example Expectation where
#ifdef ENABLE_SPEC_HOOK_ARGS
  type Arg Expectation = ()
#endif
  evaluateExample e = evaluateExample (\() -> e)

hunitFailureToResult :: Maybe String -> HUnit.HUnitFailure -> ResultStatus
hunitFailureToResult pre e = case e of
  HUnit.HUnitFailure mLoc err ->
      case err of
        HUnit.Reason reason -> Failure location (Reason $ addPre reason)
        HUnit.ExpectedButGot preface expected actual -> Failure location (ExpectedButGot (addPreMaybe preface) expected actual)
          where
            addPreMaybe :: Maybe String -> Maybe String
            addPreMaybe xs = case (pre, xs) of
              (Just x, Just y) -> Just (x ++ "\n" ++ y)
              _ -> pre <|> xs
    where
      location = case mLoc of
        Nothing -> Nothing
        Just loc -> Just $ toLocation loc
  where
    addPre :: String -> String
    addPre xs = case pre of
      Just x -> x ++ "\n" ++ xs
      Nothing -> xs

toLocation :: SrcLoc -> Location
toLocation loc = Location (srcLocFile loc) (srcLocStartLine loc) (srcLocStartCol loc)

#ifdef ENABLE_SPEC_HOOK_ARGS
instance Example (a -> Expectation) where
  type Arg (a -> Expectation) = a
#else
instance Example (() -> Expectation) where
#endif
  evaluateExample e _params hook _ = hook e >> return (Result "" Success)
