{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ImplicitParams #-}
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | Golden test management, interactive mode. Runs the tests, and asks
-- the user how to proceed in case of failure or missing golden standard.

module Test.Tasty.Silver.Interactive
  (
  -- * Command line helpers
    defaultMain
  , defaultMain1

  -- * The ingredient
  , interactiveTests
  , Interactive (..)

  -- * Programmatic API
  , runTestsInteractive
  , DisabledTests
  )
  where

import Prelude

import Control.Concurrent.STM.TVar
import Control.Exception
import Control.Monad.Identity
import Control.Monad.Reader
import Control.Monad.STM
import Control.Monad.State

import Data.Char
import Data.Maybe
import Data.Monoid    ( Any(..) )
#if !(MIN_VERSION_base(4,8,0))
import Data.Monoid    ( Monoid(..) )
#endif
import Data.Proxy
#if !(MIN_VERSION_base(4,11,0))
import Data.Semigroup ( Semigroup(..) )
#endif
import Data.Tagged
import Data.Text      ( Text )
import Data.Text.Encoding
import Data.Typeable
import qualified Data.ByteString as BS
import qualified Data.IntMap as IntMap
import qualified Data.Text as T
import qualified Data.Text.IO as TIO

import Options.Applicative hiding (Failure, Success)

import System.Console.ANSI
import System.Directory
import System.Exit
import System.FilePath
import System.IO
import System.IO.Silently (silence)
import System.IO.Temp
import System.Process
import System.Process.ByteString as PS
import qualified System.Process.Text as ProcessText

import Text.Printf

import Test.Tasty hiding (defaultMain)
import Test.Tasty.Options
import Test.Tasty.Providers
import Test.Tasty.Runners
import Test.Tasty.Silver.Filter
import Test.Tasty.Silver.Interactive.Run
import Test.Tasty.Silver.Internal

type DisabledTests = TestPath -> Bool

-- | Like @defaultMain@ from the main tasty package, but also includes the
-- golden test management capabilities.

defaultMain :: TestTree -> IO ()
defaultMain = defaultMain1 []


defaultMain1 :: [RegexFilter] -> TestTree -> IO ()
defaultMain1 filters =
    defaultMainWithIngredients
        [ listingTests
        , interactiveTests (checkRF False filters)
        ]

-- | Option for interactive mode.

newtype Interactive = Interactive Bool
  deriving (Eq, Ord, Typeable)

instance IsOption Interactive where
  defaultValue   = Interactive False
  parseValue     = fmap Interactive . safeRead
  optionName     = return "interactive"
  optionHelp     = return "Run tests in interactive mode."
  optionCLParser = flagCLParser (Just 'i') (Interactive True)

data ResultType = RTSuccess | RTFail | RTIgnore
  deriving (Eq)

data FancyTestException
  = Mismatch GoldenResultI
  | Disabled
  deriving (Show, Typeable)

instance Exception FancyTestException

getResultType :: Result -> ResultType
getResultType (Result { resultOutcome = Success}) = RTSuccess
getResultType (Result { resultOutcome = (Failure (TestThrewException e))}) =
  case fromException e of
    Just Disabled -> RTIgnore
    _ -> RTFail
getResultType (Result { resultOutcome = (Failure _)}) = RTFail


interactiveTests :: DisabledTests
    -> Ingredient
interactiveTests dis = TestManager
    [ Option (Proxy :: Proxy Interactive)
    , Option (Proxy :: Proxy HideSuccesses)
    , Option (Proxy :: Proxy AnsiTricks)
    , Option (Proxy :: Proxy UseColor)
    , Option (Proxy :: Proxy NumThreads)
    , Option (Proxy :: Proxy ExcludeFilters)
    , Option (Proxy :: Proxy IncludeFilters)
    , Option (Proxy :: Proxy AcceptTests)
    ] $
  \opts tree ->
      Just $ runTestsInteractive dis opts (filterWithRegex opts tree)

runSingleTest ::  IsTest t => DisabledTests -> TestPath -> TestName -> OptionSet -> t -> (Progress -> IO ()) -> IO Result
runSingleTest dis tp _ _ _ _ | dis tp =
  return $ (testFailed "")
    { resultOutcome = (Failure $ TestThrewException $ toException Disabled) }
runSingleTest _ _ _ opts t cb = do
  case (cast t :: Maybe Golden) of
    Nothing -> run opts t cb
    Just g -> do
        (r, gr) <- runGolden g

        -- we may be in a different thread here than the main ui.
        -- force evaluation of actual value here, as we have to evaluate it before
        -- leaving this test.
        gr' <- forceGoldenResult gr
        case gr' of
            GREqual -> return r
            grd -> return $ r { resultOutcome = (Failure $ TestThrewException $ toException $ Mismatch grd) }

-- | A simple console UI.
runTestsInteractive :: DisabledTests -> OptionSet -> TestTree -> IO Bool
runTestsInteractive dis opts tests = do
  let tests' = wrapRunTest (runSingleTest dis) tests

  launchTestTree opts tests' $ \smap -> do
    isTerm <- hSupportsANSI stdout

    (\k -> if isTerm
      then (do hideCursor; k) `finally` showCursor
      else k) $ do

      hSetBuffering stdout NoBuffering

      let
        whenColor = lookupOption opts
        HideSuccesses hideSuccesses = lookupOption opts
        AnsiTricks ansiTricks = lookupOption opts

      let
        ?colors = useColor whenColor isTerm

      let
        outp = produceOutput opts tests

      stats <- case () of { _
        | hideSuccesses && isTerm && ansiTricks ->
            consoleOutputHidingSuccesses outp smap
        | hideSuccesses && not isTerm ->
            streamOutputHidingSuccesses outp smap
        | otherwise -> consoleOutput outp smap
      }

      return $ \time -> do
            printStatistics stats time
            return $ statFailures stats == 0


-- | Show diff using available external tools.

printDiff :: TestName -> GDiff -> IO ()
printDiff = showDiff_ False

-- | Like 'printDiff', but uses @less@ if available.

showDiff :: TestName -> GDiff -> IO ()
showDiff n d = do
  useLess <- useLess
  showDiff_ useLess n d

showDiff_ :: Bool -> TestName -> GDiff -> IO ()
showDiff_ _       _ Equal                   = error "Can't show diff for equal values."
showDiff_ True    n (ShowDiffed _ t)        = showInLess n t
showDiff_ False   _ (ShowDiffed _ t)        = TIO.putStrLn t
showDiff_ useLess n (DiffText _ tGold tAct) =
  ifM (doesCmdExist "wdiff" `and2M` haveColorDiff) colorDiff $ {-else-}
  ifM (doesCmdExist "git") gitDiff {-else-} noDiff
  where

  -- Display diff using `git diff`.
  gitDiff = do
    withDiffEnv n tGold tAct $ \ fGold fAct -> do
      -- Unless we use `less`, we simply call `git` directly.
      if not useLess
        then do
          (out, err) <- callGitDiff [ fGold, fAct ]
          TIO.putStrLn err
          TIO.putStrLn out
        else callCommand $ unwords
            [ "git"
            , unwords gitDiffArgs
            , "--color=always"
            , toSlashesFilename fGold
            , toSlashesFilename fAct
            , "| less -r > /dev/tty"
              -- Option -r: display control characters raw (e.g. sound bell instead of printing ^G).
              -- Thus, ANSI escape sequences will be interpreted as that.
              -- /dev/tty is "terminal where process started"  ("CON" on Windows?)
            ]

  -- Display diff using `wdiff | colordiff`.
  colorDiff = do
    withDiffEnv n tGold tAct $ \ fGold fAct -> do
      let cmd = unwords
            [ "wdiff"
            , toSlashesFilename fGold
            , toSlashesFilename fAct
            , "| colordiff"
              -- E.g.
            , if useLess then "| less -r > /dev/tty" else ""
              -- Option -r: display control characters raw (e.g. sound bell instead of printing ^G).
              -- Thus, ANSI escape sequences will be interpreted, e.g. as coloring.
              -- /dev/tty is "terminal where process started"  ("CON" on Windows?)
            ]
      ifM (doesCmdExist "colordiff")
        -- If `colordiff` is treated as executable binary, we do not indirect via `sh`,
        -- but can let the default shell do the piping for us.
        {-then-} (callCommand cmd)
        -- Otherwise, let `sh` do the piping for us.  (Needed e.g. for Cygwin.)
        {-else-} (callProcess "sh" [ "-c", cmd ])

      -- Alt:
      --   -- We have to pipe ourselves; don't use `less` then.
      --   callProcessText "wdiff" [fGold, fAct] T.empty >>=
      --     void . callProcessText "colordiff" []
      --   -- TODO: invoke "colordiff" through callCommand

    -- Newline if we didn't go through less
    unless useLess $ putStrLn ""

  -- No diff tool: Simply print both golden and actual value.
  noDiff = do
    putStrLn "`git diff` not available, cannot produce a diff."
    putStrLn "Golden value:"
    TIO.putStrLn tGold
    putStrLn "Actual value:"
    TIO.putStrLn tAct

-- | Call external tool @"git" 'gitDiffArgs'@ with given extra arguments, returning its output.
--   If @git diff@ prints to @stderr@ or returns a exitcode indicating failure, throw exception.

callGitDiff
  :: [String]
       -- ^ File arguments to @git diff@.
  -> IO (Text, Text)
       -- ^ @stdout@ and @stderr@ produced by the call.
callGitDiff args = do
  ret@(exitcode, stdOut, stdErr) <-
    ProcessText.readProcessWithExitCode
      "git" (gitDiffArgs ++ args) T.empty
  let done = return (stdOut, stdErr)
  case exitcode of
    ExitSuccess   -> done
    -- With option --no-index, exitcode 1 indicates that files are different.
    ExitFailure 1 -> done
    -- Other failure codes indicate that something went wrong.
    ExitFailure _ -> gitFailed $ show ret
  where
  gitFailed msg = fail $ "Call to `git diff` failed: " ++ msg

gitDiffArgs :: [String]
gitDiffArgs = [ "diff", "--no-index", "--text" ]

-- #16: filenames get mangled under Windows, backslashes disappearing.
-- We only use this function on names of tempfiles, which do not contain spaces,
-- so it should be enough to hackily replace backslashes by slashes.
-- | Turn backslashes to slashes, which can also be path separators on Windows.
toSlashesFilename :: String -> String
toSlashesFilename = map $ \ c -> case c of
  '\\' -> '/'
  c    -> c

-- | Look for a command on the PATH.  If @doesCmdExist cmd@, then
--   @callProcess cmd@ should be possible.
--
--   Note that there are OS-specific differences.
--   E.g. on @cygwin@, only binaries (@.exe@) are deemed to exist,
--   not scripts.  The latter also cannot be called directly with
--   @callProcess@, but need indirection via @sh -c@.
--   In particular, @colordiff@, which is a @perl@ script, is not
--   found by @doesCmdExist@ on @cygwin@.
--
--   On @macOS@, there isn't such a distinction, so @colordiff@
--   is both found by @doesCmdExist@ and can be run by @callProcess@.
doesCmdExist :: String -> IO Bool
doesCmdExist cmd = isJust <$> findExecutable cmd

-- | Since @colordiff@ is a script, it may not be found by 'findExecutable'
-- e.g. on Cygwin.  So we try also to find it using @which@.
haveColorDiff :: IO Bool
haveColorDiff = orM
  [ doesCmdExist "colordiff"
  , andM
    [ haveSh
    , silence $ exitCodeToBool <$> rawSystem "which" [ "colordiff" ]
    ]
  ]

exitCodeToBool :: ExitCode -> Bool
exitCodeToBool ExitSuccess   = True
exitCodeToBool ExitFailure{} = False

-- Stores the golden/actual text in two files, so we can use it for git diff.
withDiffEnv :: TestName -> T.Text -> T.Text -> (FilePath -> FilePath -> IO ()) -> IO ()
withDiffEnv n tGold tAct cont = do
  withSystemTempFile (n <.> "golden") $ \ fGold hGold -> do
    withSystemTempFile (n <.> "actual") $ \ fAct hAct -> do
      hSetBinaryMode hGold True
      hSetBinaryMode hAct True
      BS.hPut hGold (encodeUtf8 tGold)
      BS.hPut hAct (encodeUtf8 tAct)
      hClose hGold
      hClose hAct
      cont fGold fAct


printValue :: TestName -> GShow -> IO ()
printValue _ (ShowText t) = TIO.putStrLn t

showValue :: TestName -> GShow -> IO ()
showValue n (ShowText t) = showInLess n t

showInLess :: String -> T.Text -> IO ()
showInLess _ t = do
  ifNotM useLess
    {-then-} (TIO.putStrLn t)
    {-else-} $ do
      ret <- PS.readCreateProcessWithExitCode (shell "less > /dev/tty") $ encodeUtf8 t
      case ret of
        ret@(ExitFailure _, _, _) -> error $ show ret
        _ -> return ()

-- | Should we use external tool @less@ to display diffs and results?
useLess :: IO Bool
useLess = andM [ hIsTerminalDevice stdin, hSupportsANSI stdout, doesCmdExist "less" ]

-- | Is @sh@ available to take care of piping for us?
haveSh :: IO Bool
haveSh = doesCmdExist "sh"

-- | Ask user whether to accept a new golden value, and run action if yes.

tryAccept
  :: String   -- ^ @prefix@ printed at the beginning of each line.
  -> IO ()    -- ^ Action to @update@ golden value.
  -> IO Bool  -- ^ Return decision whether to update the golden value.
tryAccept prefix update = do
  -- Andreas, 2021-09-18
  -- Accepting by default in batch mode is not the right thing,
  -- because CI may then falsely accept broken tests.
  --
  -- --   If terminal is non-interactive, just assume "yes" always.
  -- termIsInteractive <- hIsTerminalDevice stdin
  -- if not termIsInteractive then do
  --   putStr prefix
  --   putStr "Accepting actual value as new golden value."
  --   update
  --   return True
  -- else do
    isANSI <- hSupportsANSI stdout
    when isANSI showCursor
    putStr prefix
    putStr "Accept actual value as new golden value? [yn] "
    let
      done b = do
        when isANSI hideCursor
        putStr prefix
        return b
      loop = do
        ans <- getLine
        case ans of
          "y" -> do update; done True
          "n" -> done False
          _   -> do
            putStr prefix
            putStrLn "Invalid answer."
            loop
    loop


--------------------------------------------------
-- TestOutput base definitions
--------------------------------------------------
-- {{{
-- | 'TestOutput' is an intermediary between output formatting and output
-- printing. It lets us have several different printing modes (normal; print
-- failures only; quiet).
data TestOutput
  = HandleTest
      {- test name, used for golden lookup #-} (TestName)
      {- print test name   -} (IO ())
      {- print test result -} (Result -> IO Statistics)
  | PrintHeading (IO ()) TestOutput
  | Skip
  | Seq TestOutput TestOutput

instance Semigroup TestOutput where
  (<>) = Seq

-- The monoid laws should hold observationally w.r.t. the semantics defined
-- in this module
instance Monoid TestOutput where
  mempty = Skip
  mappend = (<>)

type Level = Int

produceOutput :: (?colors :: Bool) => OptionSet -> TestTree -> TestOutput
produceOutput opts tree =
  let
    -- Do not retain the reference to the tree more than necessary
    !alignment = computeAlignment opts tree
    Interactive isInteractive = lookupOption opts

    handleSingleTest
      :: (IsTest t, ?colors :: Bool)
      => OptionSet -> TestName -> t -> Ap (Reader Level) TestOutput
    handleSingleTest _opts name _test = Ap $ do
      level <- ask

      let
        align = replicate (alignment - indentSize * level - length name) ' '
        pref = indent level ++ replicate (length name) ' ' ++ "  " ++ align
        printTestName =
          printf "%s%s: %s" (indent level) name align
        printResultLine result forceTime = do
          -- use an appropriate printing function
          let
            resTy = getResultType result
            printFn = case resTy of
                RTSuccess -> ok
                RTIgnore -> warn
                RTFail -> failure
          case resTy of
            RTSuccess -> printFn "OK"
            RTIgnore -> printFn "DISABLED"
            RTFail -> printFn "FAIL"
          -- print time only if it's significant
          when (resultTime result >= 0.01 || forceTime) $
            printFn (printf " (%.2fs)" $ resultTime result)
          printFn "\n"


        handleTestResult result = do
          -- non-interactive mode. Uses different order of printing,
          -- as using the interactive layout doesn't go that well
          -- with printing the diffs to stdout.
          --
          printResultLine result True

          rDesc <- formatMessage $ resultDescription result
          when (not $ null rDesc) $ (case getResultType result of
            RTSuccess -> infoOk
            RTIgnore -> infoWarn
            RTFail -> infoFail) $
              printf "%s%s\n" pref (formatDesc (level+1) rDesc)

          stat' <- printTestOutput pref name result

          return stat'

        handleTestResultInteractive result = do
          (result', stat') <- case (resultOutcome result) of
            Failure (TestThrewException e) ->
              case fromException e of
                Just (Mismatch (GRDifferent _ _ _ Nothing)) -> do
                  printResultLine result False
                  s <- printTestOutput pref name result
                  return (testFailed "", s)
                Just (Mismatch (GRNoGolden a shw (Just upd))) -> do
                  printf "Golden value missing. Press <enter> to show actual value.\n"
                  _ <- getLine
                  let a' = runIdentity a
                  shw' <- shw a'
                  showValue name shw'
                  isUpd <- tryAccept pref $ upd a'
                  let r =
                        if isUpd
                        then ( testPassed "Created golden value."
                             , mempty { statCreatedGolden = 1 } )
                        else ( testFailed "Golden value missing."
                             , mempty { statFailures = 1 } )
                  printResultLine (fst r) False
                  return r
                Just (Mismatch (GRDifferent _ a diff (Just upd))) -> do
                  printf "Golden value differs from actual value.\n"
                  showDiff name diff
                  isUpd <- tryAccept pref $ upd a
                  let r =
                        if isUpd
                        then ( testPassed "Updated golden value."
                             , mempty { statUpdatedGolden = 1 } )
                        else ( testFailed "Golden value does not match actual output."
                             , mempty { statFailures = 1 } )
                  printResultLine (fst r) False
                  return r
                Just (Mismatch _) -> error "Impossible case!"
                Just Disabled -> do
                  printResultLine result False
                  return ( result
                         , mempty { statDisabled = 1 } )
                Nothing -> do
                  printResultLine result False
                  return (result, mempty {statFailures = 1})
            Success -> do
              printResultLine result False
              return (result, mempty { statSuccesses = 1 })
            Failure _ -> do
              printResultLine result False
              return (result, mempty { statFailures = 1 })

          let result'' = result' { resultTime = resultTime result }

          rDesc <- formatMessage $ resultDescription result''
          when (not $ null rDesc) $ (case getResultType result'' of
            RTSuccess -> infoOk
            RTIgnore -> infoWarn
            RTFail -> infoFail) $
              printf "%s%s\n" pref (formatDesc (level+1) rDesc)

          return stat'

      let handleTestResult' = (if isInteractive then handleTestResultInteractive else handleTestResult)
      return $ HandleTest name printTestName handleTestResult'

    handleGroup :: OptionSet -> TestName -> Ap (Reader Level) TestOutput -> Ap (Reader Level) TestOutput
    handleGroup _ name grp = Ap $ do
      level <- ask
      let
        printHeading = printf "%s%s\n" (indent level) name
        printBody = runReader (getApp grp) (level + 1)
      return $ PrintHeading printHeading printBody

  in
    flip runReader 0 $ getApp $
      foldTestTree
        trivialFold
          { foldSingle = handleSingleTest
          , foldGroup = handleGroup
          }
          opts tree

printTestOutput :: (?colors :: Bool) => String -> TestName -> Result -> IO Statistics
printTestOutput prefix name result = case resultOutcome result of
  Failure (TestThrewException e) ->
    case fromException e of
      Just (Mismatch (GRNoGolden a shw _)) -> do
        infoFail $ printf "%sActual value is:\n" prefix
        let a' = runIdentity a
        shw' <- shw a'
        hsep
        printValue name shw'
        hsep
        return ( mempty { statFailures = 1 } )
      Just (Mismatch (GRDifferent _ _ diff _)) -> do
        infoFail $ printf "%sDiff between actual and golden value:\n" prefix
        hsep
        printDiff name diff
        hsep
        return ( mempty { statFailures = 1 } )
      Just (Mismatch _) -> error "Impossible case!"
      Just Disabled -> return ( mempty { statDisabled = 1 } )
      Nothing -> return ( mempty { statFailures = 1 } )
  Failure _ -> return ( mempty { statFailures = 1 } )
  Success -> return ( mempty { statSuccesses = 1 } )

hsep :: IO ()
hsep = putStrLn (replicate 40 '=')

foldTestOutput
  :: (?colors :: Bool, Monoid b)
  => (IO () -> IO Result
    -> (Result -> IO Statistics)
    -> b)
  -> (IO () -> b -> b)
  -> TestOutput -> StatusMap -> b
foldTestOutput foldTest foldHeading outputTree smap =
  flip evalState 0 $ getApp $ go outputTree where
  go (HandleTest _ printName handleResult) = Ap $ do
    ix <- get
    put $! ix + 1
    let
      statusVar =
        fromMaybe (error "internal error: index out of bounds") $
        IntMap.lookup ix smap
      readStatusVar = getResultFromTVar statusVar
    return $ foldTest printName readStatusVar handleResult
  go (PrintHeading printName printBody) = Ap $
    foldHeading printName <$> getApp (go printBody)
  go (Seq a b) = mappend (go a) (go b)
  go Skip = mempty

-- }}}

--------------------------------------------------
-- TestOutput modes
--------------------------------------------------
-- {{{
consoleOutput :: (?colors :: Bool) => TestOutput -> StatusMap -> IO Statistics
consoleOutput outp smap =
  getApp . fst $ foldTestOutput foldTest foldHeading outp smap
  where
    foldTest printName getResult handleResult =
      (Ap $ do
        _ <- printName
        r <- getResult
        handleResult r
      , Any True)
    foldHeading printHeading (printBody, Any nonempty) =
      (Ap $ do
        when nonempty $ printHeading
        stats <- getApp printBody
        return stats
      , Any nonempty )

consoleOutputHidingSuccesses :: (?colors :: Bool) => TestOutput -> StatusMap -> IO Statistics
consoleOutputHidingSuccesses outp smap =
  snd <$> (getApp $ foldTestOutput foldTest foldHeading outp smap)
  where
    foldTest printName getResult handleResult =
      Ap $ do
          _ <- printName
          r <- getResult
          if resultSuccessful r
            then do
                clearThisLine
                return (Any False, mempty { statSuccesses = 1 })
            else do
                stats <- handleResult r
                return (Any True, stats)

    foldHeading printHeading printBody =
      Ap $ do
        _ <- printHeading
        b@(Any failed, _) <- getApp printBody
        unless failed clearAboveLine
        return b

    clearAboveLine = do cursorUpLine 1; clearThisLine
    clearThisLine = do clearLine; setCursorColumn 0

streamOutputHidingSuccesses :: (?colors :: Bool) => TestOutput -> StatusMap -> IO Statistics
streamOutputHidingSuccesses outp smap =
  snd <$> (flip evalStateT [] . getApp $
    foldTestOutput foldTest foldHeading outp smap)
  where
    foldTest printName getResult handleResult =
      Ap $ do
          r <- liftIO $ getResult
          if resultSuccessful r
            then return (Any False, mempty { statSuccesses = 1 })
            else do
              stack <- get
              put []

              stats <- liftIO $ do
                sequence_ $ reverse stack
                _ <- printName
                handleResult r

              return (Any True, stats)

    foldHeading printHeading printBody =
      Ap $ do
        modify (printHeading :)
        b@(Any failed, _) <- getApp printBody
        unless failed $
          modify $ \stack ->
            case stack of
              _:rest -> rest
              [] -> [] -- shouldn't happen anyway
        return b

-- }}}

--------------------------------------------------
-- Statistics
--------------------------------------------------
-- {{{

data Statistics = Statistics
  { statSuccesses :: !Int
  , statUpdatedGolden :: !Int
  , statCreatedGolden :: !Int
  , statFailures :: !Int
  , statDisabled :: !Int
  }

instance Semigroup Statistics where
  Statistics a1 b1 c1 d1 e1 <> Statistics a2 b2 c2 d2 e2 = Statistics (a1 + a2) (b1 + b2) (c1 + c2) (d1 + d2) (e1 + e2)


instance Monoid Statistics where
  mempty = Statistics 0 0 0 0 0
  mappend = (<>)

printStatistics :: (?colors :: Bool) => Statistics -> Time -> IO ()
printStatistics st time = do
  printf "\n"

  let total = statFailures st + statUpdatedGolden st + statCreatedGolden st + statSuccesses st

  when (statCreatedGolden st > 0) (printf "Created %d golden values.\n" (statCreatedGolden st))
  when (statUpdatedGolden st > 0) (printf "Updated %d golden values.\n" (statUpdatedGolden st))
  when (statDisabled st > 0) (printf "Ignored %d disabled tests.\n" (statDisabled st))

  case statFailures st of
    0 -> do
      ok $ printf "All %d tests passed (%.2fs)\n" total time

    fs -> do
      failure $ printf "%d out of %d tests failed (%.2fs)\n" fs total time

data FailureStatus
  = Unknown
  | Failed
  | OK

instance Semigroup FailureStatus where
  Failed  <> _      = Failed
  _       <> Failed = Failed
  OK      <> OK     = OK
  _       <> _      = Unknown

instance Monoid FailureStatus where
  mempty = OK
  mappend = (<>)

-- }}}

--------------------------------------------------
-- Console test reporter
--------------------------------------------------

-- | Report only failed tests
newtype HideSuccesses = HideSuccesses Bool
  deriving (Eq, Ord, Typeable)

instance IsOption HideSuccesses where
  defaultValue   = HideSuccesses False
  parseValue     = fmap HideSuccesses . safeRead
  optionName     = return "hide-successes"
  optionHelp     = return "Do not print tests that passed successfully"
  optionCLParser = flagCLParser Nothing (HideSuccesses True)

newtype AnsiTricks = AnsiTricks Bool
   deriving Typeable

instance IsOption AnsiTricks where
  defaultValue = AnsiTricks True
  parseValue   = fmap AnsiTricks . safeReadBool
  optionName   = return "ansi-tricks"
  optionHelp   = return $
    -- Multiline literals don't work because of -XCPP.
    "Enable various ANSI terminal tricks. " ++
    "Can be set to 'true' (default) or 'false'."

-- | When to use color on the output
data UseColor
  = Never | Always | Auto
  deriving (Eq, Ord, Typeable)

-- | Control color output
instance IsOption UseColor where
  defaultValue   = Auto
  parseValue     = parseUseColor
  optionName     = return "color"
  optionHelp     = return "When to use colored output. Options are 'never', 'always' and 'auto' (default: 'auto')"
  optionCLParser =
    option parse
      (  long name
      <> help (untag (optionHelp :: Tagged UseColor String))
      )
    where
      name = untag (optionName :: Tagged UseColor String)
      parse = str >>=
        maybe (readerError $ "Could not parse " ++ name) pure <$> parseValue

-- | @useColor when isTerm@ decides if colors should be used,
--   where @isTerm@ denotes where @stdout@ is a terminal device.
useColor :: UseColor -> Bool -> Bool
useColor cond isTerm =
  case cond of
    Never  -> False
    Always -> True
    Auto   -> isTerm

parseUseColor :: String -> Maybe UseColor
parseUseColor s =
  case map toLower s of
    "never"  -> return Never
    "always" -> return Always
    "auto"   -> return Auto
    _        -> Nothing

-- }}}

--------------------------------------------------
-- Various utilities
--------------------------------------------------
-- {{{

{-getResultWithGolden :: StatusMap -> GoldenStatusMap -> TestName -> Int -> IO (Result, ResultStatus)
getResultWithGolden smap gmap nm ix = do
  r <- getResultFromTVar statusVar

  gr <- atomically $ readTVar gmap
  case nm `M.lookup` gr of
    Just g@(GRDifferent {}) -> return (r, RMismatch g)
    Just g@(GRNoGolden {})  -> return (r, RMismatch g)
    _ | resultSuccessful r  -> return (r, RPass)
    _ | resultOutcome r
    _ | otherwise           -> return (r, RFail)
  where statusVar =
            fromMaybe (error "internal error: index out of bounds") $
            IntMap.lookup ix smap
-}

getResultFromTVar :: TVar Status -> IO Result
getResultFromTVar statusVar = do
  atomically $ do
    status <- readTVar statusVar
    case status of
      Done r -> return r
      _ -> retry



-- }}}

--------------------------------------------------
-- Formatting
--------------------------------------------------
-- {{{

indentSize :: Int
indentSize = 2

indent :: Int -> String
indent n = replicate (indentSize * n) ' '

-- handle multi-line result descriptions properly
formatDesc
  :: Int -- indent
  -> String
  -> String
formatDesc n desc =
  let
    -- remove all trailing linebreaks
    chomped = reverse . dropWhile (== '\n') . reverse $ desc

    multiline = '\n' `elem` chomped

    -- we add a leading linebreak to the description, to start it on a new
    -- line and add an indentation
    paddedDesc = flip concatMap chomped $ \c ->
      if c == '\n'
        then c : indent n
        else [c]
  in
    if multiline
      then paddedDesc
      else chomped

data Maximum a
  = Maximum a
  | MinusInfinity

instance Ord a => Semigroup (Maximum a) where
  Maximum a <> Maximum b = Maximum (a `max` b)
  MinusInfinity <> a = a
  a <> MinusInfinity = a

instance Ord a => Monoid (Maximum a) where
  mempty = MinusInfinity
  mappend = (<>)


-- | Compute the amount of space needed to align "OK"s and "FAIL"s
computeAlignment :: OptionSet -> TestTree -> Int
computeAlignment opts =
  fromMonoid .
  foldTestTree
    trivialFold
      { foldSingle = \_ name _ level -> Maximum (length name + level)
      , foldGroup = \_ _ m -> m . (+ indentSize)
      }
    opts
  where
    fromMonoid m =
      case m 0 of
        MinusInfinity -> 0
        Maximum x -> x

-- (Potentially) colorful output
ok, warn, failure, infoOk, infoWarn, infoFail :: (?colors :: Bool) => String -> IO ()
ok       = output NormalIntensity Dull  Green
warn     = output NormalIntensity Dull  Yellow
failure  = output BoldIntensity   Vivid Red
infoOk   = output NormalIntensity Dull  White
infoWarn = output NormalIntensity Dull  White
infoFail = output NormalIntensity Dull  Red

output
  :: (?colors :: Bool)
  => ConsoleIntensity
  -> ColorIntensity
  -> Color
  -> String
  -> IO ()
output bold intensity color st
  | ?colors =
    (do
      setSGR
        [ SetColor Foreground intensity color
        , SetConsoleIntensity bold
        ]
      putStr st
    ) `finally` setSGR []
  | otherwise = putStr st

-- }}}
