{-# OPTIONS_GHC -Wall -Werror #-}
{-# LANGUAGE OverloadedStrings #-}

-- | Text-based reporting functionality for reporting either as text,
-- or to the terminal.  This module is an adaptation of code from the
-- original HUnit library.
--
-- Note that the test execution function in this module are included
-- for (a measure of) compatibility with HUnit, but are deprecated in
-- favor of the function in the "Test.HUnitPlus.Main" module.
module Test.HUnitPlus.Text(
       -- * Utilities
       PutText(..),
       putTextToHandle,
       putTextToShowS,
       showCounts,
       -- * Text Reporting
       textReporter,
       runTestText,
       runSuiteText,
       runSuitesText,
       -- * Terminal reporting
       terminalReporter,
       runTestTT,
       runSuiteTT,
       runSuitesTT
       ) where

import Distribution.TestSuite
import Test.HUnitPlus.Base
import Test.HUnitPlus.Execution
import Test.HUnitPlus.Filter
import Test.HUnitPlus.Reporting

import Control.Monad (when)
import System.IO (Handle, stderr)
import Text.Printf(printf)

import qualified Data.HashMap.Strict as HashMap
import qualified Data.Text as Strict
import qualified Data.Text.IO as Strict

-- | The text-based reporters ('textReporter' and 'terminalReporter')
-- construct strings and pass them to the function embodied in a
-- 'PutText'.  This function handles the string in one of several
-- ways.  Two schemes are defined here.  'putTextToHandle' writes
-- report lines to a given handle.  'putTextToShowS' accumulates lines
-- for return as a whole.
--
-- The 'PutText' function is also passed, and returns, an arbitrary state
-- value (called 'st' here).  The initial state value is given in the
-- 'PutText'; the final value is returned by 'runTestText'.

data PutText st = PutText (Strict.Text -> st -> IO st) st

-- | Writes persistent lines to the given handle.
putTextToHandle :: Handle -> PutText ()
putTextToHandle handle = PutText (\line () -> Strict.hPutStr handle line) ()

-- | Accumulates lines for return by 'runTestText'.  The
-- accumulated lines are represented by a @'ShowS' ('String' ->
-- 'String')@ function whose first argument is the string to be
-- appended to the accumulated report lines.
putTextToShowS :: PutText (Strict.Text -> Strict.Text)
putTextToShowS =
  PutText (\line func -> return (\rest -> func (Strict.concat [line, rest]))) id

-- | Create a 'Reporter' that outputs a textual report for
-- non-terminal output.
textReporter :: PutText us
             -- ^ The method for outputting text.
             -> Bool
             -- ^ Whether or not to output verbose text.
             -> Reporter us
textReporter (PutText put initUs) verbose =
  let
    reportProblem prefix msg ss us =
      let
        path = showQualName ss
        line = Strict.concat ["### ", prefix, path, ": ", msg, "\n"]
      in
        put line us

    reportOutput prefix msg ss us =
      let
        path = showQualName ss
        line = Strict.concat ["### ", prefix, path, ": ", msg, "\n"]
      in
        if verbose then put line us else return us

    reportStartSuite ss us =
      let
        line = Strict.concat ["Test suite ", stName ss, " starting\n"]
      in
        if verbose then put line us else return us

    reportEndSuite time ss us =
      let
        timestr = printf "%.6f" time
        line = Strict.concat ["Test suite", stName ss, " completed in ",
                              Strict.pack timestr, " sec\n"]
      in
        if verbose then put line us else return us

    reportStartCase ss us =
      let
        path = showQualName ss
        line = if Strict.null path then "Test case starting\n"
               else Strict.concat ["Test case ", path, " starting\n"]
      in
        if verbose then put line us else return us

    reportEndCase time ss us =
      let
        path = showQualName ss
        timestr = printf "%.6f" time
        line = if Strict.null path
                 then Strict.concat ["Test completed in ",
                                     Strict.pack timestr, " sec\n"]
                 else Strict.concat ["Test ", path, " completed in ",
                                     Strict.pack timestr, " sec\n"]
      in
        if verbose then put line us else return us

    reportEnd time counts us =
      let
        countstr = Strict.concat [showCounts counts, "\n"]
        timestr = printf "%.6f" time
        timeline = Strict.concat ["Tests completed in ",
                                  Strict.pack timestr, " sec\n"]
      in do
        if verbose
          then do
            us' <- put timeline us
            put countstr us'
          else put countstr us
  in
    defaultReporter {
      reporterStart = return initUs,
      reporterEnd = reportEnd,
      reporterStartSuite = reportStartSuite,
      reporterEndSuite = reportEndSuite,
      reporterStartCase = reportStartCase,
      reporterEndCase = reportEndCase,
      reporterSystemOut = reportOutput "STDOUT from ",
      reporterSystemErr = reportOutput "STDERR from ",
      reporterError = reportProblem "Error in ",
      reporterFailure = reportProblem "Failure in "
    }

-- | Execute a test, processing text output according to the given
-- reporting scheme.  The reporting scheme's state is threaded through
-- calls to the reporting scheme's function and finally returned,
-- along with final count values.  The text is output in non-terminal
-- mode.
--
-- This function is deprecated.  The preferred way to run tests is to
-- use the functions in "Test.HUnitPlus.Main".
runTestText :: PutText us
            -- ^ A function which accumulates output.
            -> Bool
            -- ^ Whether or not to run the test in verbose mode.
            -> Test
            -- ^ The test to run
            -> IO (Counts, us)
runTestText puttext @ (PutText put us0) verbose t =
  let
    initState = State { stCounts = zeroCounts, stName = "",
                        stPath = [], stOptions = HashMap.empty,
                        stOptionDescs = [] }

    reporter = textReporter puttext verbose
  in do
    (ss1, us1) <- ((performTest $! reporter) allSelector $! initState) us0 t
    us2 <- put (Strict.concat [showCounts (stCounts ss1), "\n"]) us1
    return (stCounts ss1, us2)

-- | Execute a test suite, processing text output according to the
-- given reporting scheme.  The reporting scheme's state is threaded
-- through calls to the reporting scheme's function and finally
-- returned, along with final count values.  The text is output in
-- non-terminal mode.
--
-- This function is deprecated.  The preferred way to run tests is to
-- use the functions in "Test.HUnitPlus.Main".
runSuiteText :: PutText us
             -- ^ A function which accumulates output.
             -> Bool
             -- ^ Whether or not to run the tests in verbose mode.
             -> TestSuite
             -- ^ The test suite to run.
             -> IO (Counts, us)
runSuiteText puttext @ (PutText put us0) verbose
             suite @ TestSuite { suiteName = sname } =
  let
    selectorMap = HashMap.singleton sname (HashMap.singleton HashMap.empty
                                                             allSelector)
    reporter = textReporter puttext verbose
  in do
    (counts, us1) <- ((performTestSuite $! reporter) $!selectorMap) us0 suite
    us2 <- put (Strict.concat [showCounts counts, "\n"]) us1
    return (counts, us2)

-- | Execute the given test suites, processing text output according
-- to the given reporting scheme.  The reporting scheme's state is
-- threaded through calls to the reporting scheme's function and
-- finally returned, along with final count values.  The text is
-- output in non-terminal mode.
--
-- This function is deprecated.  The preferred way to run tests is to
-- use the functions in "Test.HUnitPlus.Main".
runSuitesText :: PutText us
              -- ^ A function which accumulates output
              -> Bool
              -- ^ Whether or not to run the test in verbose mode.
              -> [TestSuite]
              -- ^ The test to run
              -> IO (Counts, us)
runSuitesText puttext @ (PutText put _) verbose suites =
  let
    suiteNames = map suiteName suites
    selectorMap =
      foldl (\suitemap sname ->
              HashMap.insert sname (HashMap.singleton HashMap.empty
                                                      allSelector) suitemap)
            HashMap.empty suiteNames
    reporter = textReporter puttext verbose
  in do
    (counts, us1) <- ((performTestSuites $! reporter) $! selectorMap) suites
    us2 <- put (Strict.concat [showCounts counts, "\n"]) us1
    return (counts, us2)

-- | Converts test execution counts to a string.
showCounts :: Counts -> Strict.Text
showCounts Counts { cCases = cases, cTried = tried,
                    cErrors = errors, cFailures = failures,
                    cAsserts = asserts, cSkipped = skipped } =
  Strict.concat ["Cases: ", Strict.pack (show cases), "  Tried: ",
                 Strict.pack (show tried), "  Errors: ",
                 Strict.pack (show errors), "  Failures: ",
                 Strict.pack (show failures), "  Assertions: ",
                 Strict.pack (show asserts), "  Skipped: ",
                 Strict.pack (show skipped)]

-- | Terminal output function, used by the run*TT function and
-- terminal reporters.
termPut :: Strict.Text -> Bool -> Int -> IO Int
termPut line pers (-1) =
  do
    when pers (Strict.hPutStrLn stderr line)
    return (-1)
termPut line True cnt =
  do
    Strict.hPutStrLn stderr (Strict.concat [erase cnt, line])
    return 0
termPut line False _ =
  do
    Strict.hPutStr stderr (Strict.concat ["\r", line])
    return (Strict.length line)

-- The "erasing" strategy with a single '\r' relies on the fact that the
-- lengths of successive summary lines are monotonically nondecreasing.
erase :: Int -> Strict.Text
erase cnt =
  if cnt == 0 then "" else Strict.concat ["\r", Strict.replicate cnt " ", "\r"]

-- | A reporter that outputs lines indicating progress to the
-- terminal.  Reporting is made to standard error, and progress
-- reports are included.
terminalReporter :: Reporter Int
terminalReporter =
  let
    reportProblem prefix msg ss us =
      let
        line = Strict.concat ["### ", prefix, path, "\n", msg]
        path = showQualName ss
      in
        termPut line True us
  in
    defaultReporter {
      reporterStart = return 0,
      reporterEnd = \_ _ _ -> do Strict.hPutStr stderr "\n"; return 0,
      reporterEndCase = \_ ss us -> termPut (showCounts (stCounts ss)) False us,
      reporterError = reportProblem "Error in:   ",
      reporterFailure = reportProblem "Failure in: "
    }

-- | Execute a test, processing text output according to the given
-- reporting scheme.  The reporting scheme's state is threaded through
-- calls to the reporting scheme's function and finally returned,
-- along with final count values.  The text is output in terminal
-- mode.
--
-- This function is deprecated.  The preferred way to run tests is to
-- use the functions in "Test.HUnitPlus.Main".
runTestTT :: Test -> IO Counts
runTestTT t =
  let
    initState = State { stCounts = zeroCounts, stName = "",
                        stPath = [], stOptions = HashMap.empty,
                        stOptionDescs = [] }
  in do
    (ss1, us1) <- (performTest terminalReporter allSelector $! initState) 0 t
    0 <- termPut (showCounts (stCounts ss1)) True us1
    return (stCounts ss1)

-- | Execute a test suite, processing text output according to the
-- given reporting scheme.  The reporting scheme's state is threaded
-- through calls to the reporting scheme's function and finally
-- returned, along with final count values.  The text is output in
-- terminal mode.
--
-- This function is deprecated.  The preferred way to run tests is to
-- use the functions in "Test.HUnitPlus.Main".
runSuiteTT :: TestSuite -> IO Counts
runSuiteTT suite @ TestSuite { suiteName = sname } =
  let
    selectorMap = HashMap.singleton sname (HashMap.singleton HashMap.empty
                                                             allSelector)
  in do
    (counts, us) <- (performTestSuite terminalReporter $! selectorMap) 0 suite
    0 <- termPut (Strict.concat [showCounts counts, "\n"]) True us
    return counts

-- | Execute the given test suites, processing text output according
-- to the given reporting scheme.  The reporting scheme's state is
-- threaded through calls to the reporting scheme's function and
-- finally returned, along with final count values.  The text is
-- output in terminal mode.
--
-- This function is deprecated.  The preferred way to run tests is to
-- use the functions in "Test.HUnitPlus.Main".
runSuitesTT :: [TestSuite] -> IO Counts
runSuitesTT suites =
  let
    suiteNames = map suiteName suites
    selectorMap =
      foldl (\suitemap sname ->
              HashMap.insert sname (HashMap.singleton HashMap.empty
                                                      allSelector) suitemap)
            HashMap.empty suiteNames
  in do
    (counts, us) <- (performTestSuites terminalReporter $! selectorMap) suites
    0 <- termPut (Strict.concat [showCounts counts, "\n"]) True us
    return counts