{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ConstraintKinds #-}
-- |
-- Stability: unstable
--
-- This module provides access to Hspec's internals.  It is less stable than
-- other parts of the API. For most users @Test.Hspec@ is more suitable!
module Test.Hspec.Core.Spec (

-- * Defining a spec
  it
, specify
, describe
, context
, pending
, pendingWith
, xit
, xspecify
, xdescribe
, xcontext

-- * Focused spec items #focus#
-- |
-- During a test run, when a spec contains /focused/ spec items, all other spec
-- items are ignored.
, focus
, fit
, fspecify
, fdescribe
, fcontext

, parallel
, sequential

-- * The @SpecM@ monad
, Test.Hspec.Core.Spec.Monad.Spec
, Test.Hspec.Core.Spec.Monad.SpecWith
, Test.Hspec.Core.Spec.Monad.SpecM(..)
, Test.Hspec.Core.Spec.Monad.runSpecM
, Test.Hspec.Core.Spec.Monad.fromSpecList
, Test.Hspec.Core.Spec.Monad.runIO
, Test.Hspec.Core.Spec.Monad.mapSpecForest
, Test.Hspec.Core.Spec.Monad.mapSpecItem
, Test.Hspec.Core.Spec.Monad.mapSpecItem_
, Test.Hspec.Core.Spec.Monad.modifyParams
, Test.Hspec.Core.Spec.Monad.modifyConfig
, getSpecDescriptionPath

-- * A type class for examples
, Test.Hspec.Core.Example.Example (..)
#ifndef ENABLE_SPEC_HOOK_ARGS
, Test.Hspec.Core.Example.Arg
#endif
, Test.Hspec.Core.Example.Params (..)
, Test.Hspec.Core.Example.defaultParams
, Test.Hspec.Core.Example.ActionWith
, Test.Hspec.Core.Example.Progress
, Test.Hspec.Core.Example.ProgressCallback
, Test.Hspec.Core.Example.Result(..)
, Test.Hspec.Core.Example.ResultStatus (..)
, Test.Hspec.Core.Example.Location (..)
, Test.Hspec.Core.Example.FailureReason (..)
, Test.Hspec.Core.Example.safeEvaluate
, Test.Hspec.Core.Example.safeEvaluateExample

-- * Internal representation of a spec tree
, Test.Hspec.Core.Tree.SpecTree
, Test.Hspec.Core.Tree.Tree (..)
, Test.Hspec.Core.Tree.Item (..)
, Test.Hspec.Core.Tree.specGroup
, Test.Hspec.Core.Tree.specItem
, Test.Hspec.Core.Tree.bimapTree
, Test.Hspec.Core.Tree.bimapForest
, Test.Hspec.Core.Tree.filterTree
, Test.Hspec.Core.Tree.filterForest
, Test.Hspec.Core.Tree.filterTreeWithLabels
, Test.Hspec.Core.Tree.filterForestWithLabels
, Test.Hspec.Core.Tree.pruneTree -- unused
, Test.Hspec.Core.Tree.pruneForest -- unused
, Test.Hspec.Core.Tree.location

, focusForest

-- * Re-exports
, HasCallStack
, Expectation
) where

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

import           Control.Monad.Trans.Class (lift)
import           Control.Monad.Trans.Reader (asks)

import           Test.Hspec.Expectations (Expectation)

import           Test.Hspec.Core.Example
#ifdef ENABLE_SPEC_HOOKS
import           Test.Hspec.Core.Hooks
#endif
import           Test.Hspec.Core.Tree
import           Test.Hspec.Core.Spec.Monad
import           Test.Hspec.Core.QuickCheck ()

-- | The @describe@ function combines a list of specs into a larger spec.
describe :: HasCallStack => String -> SpecWith a -> SpecWith a
describe label = withEnv pushLabel . mapSpecForest (return . specGroup label)
  where
    pushLabel (Env labels) = Env $ label : labels

-- | @context@ is an alias for `describe`.
context :: HasCallStack => String -> SpecWith a -> SpecWith a
context = describe

-- |
-- Changing `describe` to `xdescribe` marks all spec items of the corresponding subtree as pending.
--
-- This can be used to temporarily disable spec items.
xdescribe :: HasCallStack => String -> SpecWith a -> SpecWith a
#ifdef ENABLE_SPEC_HOOKS
xdescribe label spec = before_ pending_ $ describe label spec
#else
xdescribe = describe
#endif

-- | @xcontext@ is an alias for `xdescribe`.
xcontext :: HasCallStack => String -> SpecWith a -> SpecWith a
xcontext = xdescribe

-- | The @it@ function creates a spec item.
--
-- A spec item consists of:
--
-- * a textual description of a desired behavior
--
-- * an example for that behavior
--
-- > describe "absolute" $ do
-- >   it "returns a positive number when given a negative number" $
-- >     absolute (-1) == 1
--
-- @`Example` a@ optionally accepts an argument @`Arg` a@, which is then given
-- to the test body. This is useful for provisioning resources for a test which
-- are created and cleaned up outside the test itself. See `Arg` for details.
--
-- Note that this function is often on the scene of nasty type errors due to GHC failing
-- to infer the type of @do@ notation in the test body.
-- It can be helpful to use
-- [TypeApplications](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/type_applications.html)
-- to explicitly specify the intended `Example` type.
it :: (HasCallStack, Example a) => String -> a -> SpecWith (Arg a)
it label action = fromSpecList [specItem label action]

-- | @specify@ is an alias for `it`.
specify :: (HasCallStack, Example a) => String -> a -> SpecWith (Arg a)
specify = it

-- |
-- Changing `it` to `xit` marks the corresponding spec item as pending.
--
-- This can be used to temporarily disable a spec item.
xit :: (HasCallStack, Example a) => String -> a -> SpecWith (Arg a)
#ifdef ENABLE_SPEC_HOOKS
xit label action = before_ pending_ $ it label action
#else
xit = it
#endif

-- | @xspecify@ is an alias for `xit`.
xspecify :: (HasCallStack, Example a) => String -> a -> SpecWith (Arg a)
xspecify = xit

-- | `focus` focuses all spec items of the given spec.
--
-- Applying `focus` to a spec with focused spec items has no effect.
focus :: SpecWith a -> SpecWith a
focus = mapSpecForest focusForest

-- | Marks an entire spec forest as focused if nothing in it is already focused.
focusForest :: [SpecTree a] -> [SpecTree a]
focusForest xs
  | any (any itemIsFocused) xs = xs
  | otherwise = bimapForest id (\ item -> item {itemIsFocused = True}) xs

-- | @fit@ is an alias for @fmap focus . it@
fit :: (HasCallStack, Example a) => String -> a -> SpecWith (Arg a)
fit = fmap focus . it

-- | @fspecify@ is an alias for `fit`.
fspecify :: (HasCallStack, Example a) => String -> a -> SpecWith (Arg a)
fspecify = fit

-- | @fdescribe@ is an alias for @fmap focus . describe@
fdescribe :: HasCallStack => String -> SpecWith a -> SpecWith a
fdescribe = fmap focus . describe

-- | @fcontext@ is an alias for `fdescribe`.
fcontext :: HasCallStack => String -> SpecWith a -> SpecWith a
fcontext = fdescribe

-- | `parallel` marks all spec items of the given spec to be safe for parallel
-- evaluation.
parallel :: SpecWith a -> SpecWith a
parallel = mapSpecItem_ (setParallelizable True)

-- | `sequential` marks all spec items of the given spec to be evaluated sequentially.
sequential :: SpecWith a -> SpecWith a
sequential = mapSpecItem_ (setParallelizable False)

setParallelizable :: Bool -> Item a -> Item a
setParallelizable value item = item {itemIsParallelizable = itemIsParallelizable item <|> Just value}

-- | `pending` can be used to mark a spec item as pending.
--
-- If you want to textually specify a behavior but do not have an example yet,
-- use this:
--
-- > describe "fancyFormatter" $ do
-- >   it "can format text in a way that everyone likes" $
-- >     pending
pending :: HasCallStack => Expectation
pending = throwIO (Pending location Nothing)

#ifdef ENABLE_SPEC_HOOKS
pending_ :: Expectation
pending_ = (throwIO (Pending Nothing Nothing))
#endif

-- |
-- `pendingWith` is similar to `pending`, but it takes an additional string
-- argument that can be used to specify the reason for why the spec item is pending.
pendingWith :: HasCallStack => String -> Expectation
pendingWith = throwIO . Pending location . Just

-- | Get the path of `describe` labels, from the root all the way in to the
-- call-site of this function.
--
-- ==== __Example__
-- >>> :{
-- runSpecM $ do
--   describe "foo" $ do
--     describe "bar" $ do
--       getSpecDescriptionPath >>= runIO . print
-- :}
-- ["foo","bar"]
--
-- @since 2.10.0
getSpecDescriptionPath :: SpecM a [String]
getSpecDescriptionPath = SpecM $ lift $ reverse <$> asks envSpecDescriptionPath
