{-# OPTIONS -XFlexibleInstances #-}

module Test.Hspec.Internal where

import System.IO
import System.Exit
import Data.List (mapAccumL, groupBy, intersperse)
import System.CPUTime (getCPUTime)
import Text.Printf
import Control.Exception
import Control.Monad (liftM)

-- | The result of running an example.
data Result = Success | Fail String | Pending String
  deriving Eq


-- | Everything needed to specify and show a specific behavior.
data Spec = Spec {
                 -- | What is being tested, usually the name of a type.
                 name::String,
                 -- | The specific behavior being tested.
                 requirement::String,
                 -- | The status of this behavior.
                 result::Result }


-- | Create a set of specifications for a specific type being described.
-- Once you know what you want specs for, use this.
--
-- > describe "abs" [
-- >   it "returns a positive number given a negative number"
-- >     (abs (-1) == 1)
-- >   ]
--
describe :: String                -- ^ The name of what is being described, usually a function or type.
         -> [IO (String, Result)] -- ^ A list of behaviors and examples, created by a list of 'it'.
         -> IO [Spec]
describe n ss = do
  ss' <- sequence ss
  return $ map (\ (req, res) -> Spec n req res) ss'

-- | Combine a list of descriptions.
descriptions :: [IO [Spec]] -> IO [Spec]
descriptions = liftM concat . sequence

-- | Evaluate a Result. Any exceptions (undefined, etc.) are treated as failures.
safely :: Result -> IO Result
safely f = Control.Exception.catch ok failed
  where ok = f `seq` return f
        failed e = return $ Fail (show (e :: SomeException))


-- | Anything that can be used as an example of a behavior.
class SpecVerifier a where
  -- | Create a description and example of a behavior, a list of these
  -- is used by 'describe'. Once you know what you want to specify, use this.
  --
  -- > describe "closeEnough" [
  -- >   it "is true if two numbers are almost the same"
  -- >     (1.001 `closeEnough` 1.002),
  -- >
  -- >   it "is false if two numbers are not almost the same"
  -- >     (not $ 1.001 `closeEnough` 1.003)
  -- >   ]
  --
  it :: String           -- ^ A description of this behavior.
     -> a                -- ^ An example for this behavior.
     -> IO (String, Result)


instance SpecVerifier Bool where
  it description example = do
    r <- safely (if example then Success else Fail "")
    return (description, r)

instance SpecVerifier Result where
  it description example = return (description, example)


-- | Declare an example as not successful or failing but pending some other work.
-- If you want to report on a behavior but don't have an example yet, use this.
--
-- > describe "fancyFormatter" [
-- >   it "can format text in a way that everyone likes"
-- >     (pending "waiting for clarification from the designers")
-- >   ]
--
pending :: String  -- ^ An explanation for why this behavior is pending.
        -> Result
pending = Pending


-- | Create a document of the given specs.
documentSpecs :: [Spec] -> [String]
documentSpecs specs = lines $ unlines $ concat report ++ [""] ++ intersperse "" errors
  where organize = groupBy (\ a b -> name a == name b)
        (errors, report) = mapAccumL documentGroup [] $ organize specs

-- | Create a document of the given group of specs.
documentGroup :: [String] -> [Spec] -> ([String], [String])
documentGroup errors specGroup = (errors', "" : name (head specGroup) : report)
  where (errors', report) = mapAccumL documentSpec errors specGroup

-- | Create a document of the given spec.
documentSpec :: [String] -> Spec -> ([String], String)
documentSpec errors spec = case result spec of
                Success    -> (errors, " - " ++ requirement spec)
                Fail s     -> (errors ++ [errorDetails s], " x " ++ requirement spec ++ errorTag)
                Pending s  -> (errors, " - " ++ requirement spec ++ "\n     # " ++ s)
    where errorTag = " [" ++ (show $ length errors + 1) ++ "]"
          errorDetails s  = concat [ show (length errors + 1), ") ", name spec,
                                     " ",  requirement spec, " FAILED",
                                     if null s then "" else "\n" ++ s ]


-- | Create a summary of how long it took to run the examples.
timingSummary :: Double -> String
timingSummary t = printf "Finished in %1.4f seconds" (t / (10.0^(12::Integer)) :: Double)

failedCount :: [Spec] -> Int
failedCount ss = length $ filter (isFailure.result) ss
  where
    isFailure (Fail _) = True
    isFailure _        = False

-- | Create a summary of how many specs exist and how many examples failed.
successSummary :: [Spec] -> String
successSummary ss = quantify (length ss) "example" ++ ", " ++ quantify (failedCount ss) "failure"


-- | Create a document of the given specs.
-- This does not track how much time it took to check the examples. If you want
-- a description of each spec and don't need to know how long it tacks to check,
-- use this.
pureHspec :: [Spec]   -- ^ The specs you are interested in.
          -> [String]
pureHspec = fst . pureHspecB


pureHspecB :: [Spec]   -- ^ The specs you are interested in.
          -> ([String], Bool)
pureHspecB ss = (report, failedCount ss == 0)
  where report = documentSpecs ss ++ [ "", timingSummary 0, "", successSummary ss]


-- | Create a document of the given specs and write it to stdout.
-- This does track how much time it took to check the examples. Use this if
-- you want a description of each spec and do need to know how long it tacks
-- to check the examples or want to write to stdout.
hspec :: IO [Spec] -> IO ()
hspec ss = hspecB ss >> return ()

-- | Same as 'hspec' except it returns a bool indicating if all examples ran without failures
hspecB :: IO [Spec] -> IO Bool
hspecB = hHspec stdout

-- | Same as 'hspec' except the program exits successfull if all examples ran without failures or
-- with an errorcode of 1 if any examples failed.
hspecX :: IO [Spec] -> IO a
hspecX ss = hspecB ss >>= exitWith . toExitCode

toExitCode :: Bool -> ExitCode
toExitCode True  = ExitSuccess
toExitCode False = ExitFailure 1


-- | Create a document of the given specs and write it to the given handle.
-- This does track how much time it took to check the examples. Use this if
-- you want a description of each spec and do need to know how long it tacks
-- to check the examples or want to write to a file or other handle.
--
-- > writeReport filename specs = withFile filename WriteMode (\ h -> hHspec h specs)
--
hHspec :: Handle     -- ^ A handle for the stream you want to write to.
       -> IO [Spec]  -- ^ The specs you are interested in.
       -> IO Bool
hHspec h ss = do
  t0 <- getCPUTime
  ss' <- ss
  mapM_ (hPutStrLn h) $ documentSpecs ss'
  t1 <- getCPUTime
  mapM_ (hPutStrLn h) [ "", timingSummary (fromIntegral $ t1 - t0), "", successSummary ss']
  return $ failedCount ss' == 0


-- | Create a more readable display of a quantity of something.
quantify :: Num a => a -> String -> String
quantify 1 s = "1 " ++ s
quantify n s = show n ++ " " ++ s ++ "s"