{-# LANGUAGE GeneralizedNewtypeDeriving #-}

module TAP (
    planTests, planNoPlan, planSkipAll,
    is, isnt, like, unlike, pass, fail, ok,
    skip, skipIf, toDo,
    diag, bailOut, runTests
    ) where


import Prelude hiding (fail)
import System.IO
import System.Exit
import Control.Monad.State hiding (fail)
import Control.Exception
import Text.Regex.Posix


data TAPState = TAPState {
  planSet :: Bool,
  noPlan :: Bool,
  skipAll :: Bool,
  testDied :: Bool,
  expectedTests :: Int,
  executedTests :: Int,
  failedTests :: Int,
  toDoReason :: Maybe String,
  exitCode :: Int
} deriving (Show)

initState = TAPState {
  planSet = False,
  noPlan = False,
  skipAll = False,
  testDied = False,
  expectedTests = 0,
  executedTests = 0,
  failedTests = 0,
  toDoReason = Nothing,
  exitCode = 0
}


newtype TAP a = TAP {
        runTAP :: StateT TAPState IO a
    } deriving (Monad, MonadIO, MonadState TAPState)


_assertNotPlanned :: TAP ()
_assertNotPlanned = do
    ts <- get
    when (planSet ts) $ _die "You tried to plan twice!"


_assertPlanned :: TAP ()
_assertPlanned = do
    ts <- get
    when (not $ planSet ts) $ _die $ "You tried to run a test without a plan!"
        ++ "  Gotta have a plan."


_printPlan :: Int -> Maybe String -> IO ()
_printPlan n plan = do
    putStrLn $ "1.." ++ show n ++
        case plan of
           Just plan -> " # " ++ plan
           otherwise -> ""


planTests :: Int -> TAP ()
planTests n = do
    _assertNotPlanned
    when (n == 0) $ _die $ "You said to run 0 tests!"
        ++ " You've got to run something."
    liftIO $ _printPlan n Nothing
    modify (\x -> x {planSet = True, expectedTests = n})


planNoPlan :: TAP ()
planNoPlan = do
    _assertNotPlanned
    modify (\x -> x {planSet = True, noPlan = True})


planSkipAll :: String -> TAP ()
planSkipAll plan = do
    _assertNotPlanned
    liftIO . _printPlan 0 . Just $ "Skip " ++ plan
    modify (\x -> x {planSet = True, skipAll = True})
    _exit $ Just 0
    return ()



_matches :: String -> String -> Bool
_matches "" _ = False
_matches _ "" = False
_matches target pattern = target =~ pattern :: Bool


ok :: Bool -> Maybe String -> TAP Bool
ok result msg = do
    _assertPlanned
    modify (\x -> x {executedTests = executedTests x + 1})

    case msg of
        Just s -> when (_matches s "^[0-9]+$") $ do
            diag $ "    You named your test '" ++ s
                ++ "'.  You shouldn't use numbers for your test names."
            diag $ "    Very confusing."
        otherwise -> return ()

    when (not result) $ do
        liftIO $ putStr "not "
        modify (\x -> x {failedTests = failedTests x + 1})

    ts <- get
    liftIO . putStr $ "ok " ++ (show $ executedTests ts)

    case msg of
        -- TODO: Escape s
        Just s -> liftIO . putStr $ " - " ++ s
        otherwise -> return ()

    case (toDoReason ts) of
        Just r -> liftIO . putStr $ " # TODO " ++ r
        otherwise -> return ()

    when (not result) $ do
        modify (\x -> x {failedTests = failedTests x - 1})

    liftIO $ putStrLn ""

    -- TODO: STACK TRACE?

    return result


is :: (Show a, Eq a) => a -> a -> Maybe String -> TAP Bool
is result expected msg = do
    rc <- ok (result == expected) msg
    when (not rc) $ do
        diag $ "           got: '" ++ (show result) ++ "'"
        diag $ "      expected: '" ++ (show expected) ++ "'"
    return rc


isnt :: (Show a, Eq a) => a -> a -> Maybe String -> TAP Bool
isnt result expected msg = do
    rc <- ok (result /= expected) msg
    when (not rc) $ do
        diag $ "           got: '" ++ (show result) ++ "'"
        diag $ " didn't expect: '" ++ (show expected) ++ "'"
    return rc


like :: String -> String -> Maybe String -> TAP Bool
like target pattern msg = do
    rc <- ok (_matches target pattern) msg
    when (not rc) $ diag $ "    '" ++ target ++ "' doesn't match '"
        ++ pattern ++ "'"
    return rc


unlike :: String -> String -> Maybe String -> TAP Bool
unlike target pattern msg = do
    rc <- ok (not $ _matches target pattern) msg
    when (not rc) $ diag $ "    '" ++ target ++ "' matches '"
        ++ pattern ++ "'"
    return rc


pass :: Maybe String -> TAP Bool
pass s = ok True s


fail :: Maybe String -> TAP Bool
fail s = ok False s


skip :: Int -> String -> TAP ()
skip n reason = do
    forM_ [1 .. n] (\n' -> do
        modify (\x -> x {executedTests = executedTests x + 1})
        ts <- get
        liftIO . putStrLn $ "ok " ++ (show $ executedTests ts)
            ++ " # skip: " ++ reason)
    return ()


skipIf :: Bool -> Int -> String -> TAP a -> TAP ()
skipIf cond n reason tap = do
    if cond
        then skip n reason
        else do
            tap
            return ()


toDo :: String -> TAP a -> TAP ()
toDo reason tap = do
    modify (\x -> x {toDoReason = Just reason})
    a <- tap
    modify (\x -> x {toDoReason = Nothing})
    return ()


diag :: String -> TAP ()
diag s = do
    liftIO . putStrLn $ "# " ++ s


bailOut :: String -> TAP a
bailOut s = do
    liftIO $ hPutStrLn stderr s
    _exit $ Just 255


_die :: String -> TAP a
_die s = do
    liftIO $ hPutStrLn stderr s
    modify (\x -> x {testDied = True})
    _exit $ Just 255


_wrapup :: TAP ()
_wrapup = do
    ts <- get
    let s n = if (n > 1) then "s" else ""
    let err | not $ planSet ts = diag "Looks like your test died before it could output anything." >> return True
            | testDied ts      = diag ("Looks like your test died just after " ++ (show $ executedTests ts)) >> return True
            | otherwise        = return False
    stop <- err
    if stop
        then return ()
        else do
            when ((not $ noPlan ts)&&((expectedTests ts) < (executedTests ts))) $ do
                let extra = (executedTests ts) - (expectedTests ts)
                diag $ "Looks like you planned " ++ (show $ expectedTests ts)
                    ++ " test" ++ (s $ expectedTests ts)
                    ++ " but ran " ++ (show extra) ++ " extra."
                modify (\x -> x {exitCode = -1})

            when ((not $ noPlan ts)&&((expectedTests ts) > (executedTests ts))) $ do
                diag $ "Looks like you planned " ++ (show $ expectedTests ts)
                    ++ " test" ++ (s $ expectedTests ts)
                    ++ " but only ran " ++ (show $ executedTests ts)

            when (failedTests ts > 0) $ do
                diag $ "Looks like you failed " ++ (show $ failedTests ts)
                    ++ " test" ++ (s $ failedTests ts)
                    ++ " of " ++ (show $ executedTests ts)


_exit :: Maybe Int -> TAP a
_exit mrc = do
    case mrc of
        Just rc -> modify (\x -> x {exitCode = rc})
        otherwise -> return ()
    ts <- get
    when (exitCode ts == 0) $ do
        rc <- if ((noPlan ts)||(not $ planSet ts))
                  then return $ failedTests ts
                  else if ((expectedTests ts) < (executedTests ts))
                      then return $ (executedTests ts) - (expectedTests ts)
                      else return $ ((failedTests ts)
                          + ((expectedTests ts) - (executedTests ts)))
        modify (\x -> x {exitCode = rc})

    _wrapup
    ts <- get
    let rc = exitCode ts
    liftIO . exitWith $ if (rc == 0) then ExitSuccess else ExitFailure rc


runTests :: TAP a -> IO (a, TAPState)
-- TODO: Add exception handling here?
runTests s = runStateT (runTAP (s >> _exit Nothing)) initState
