{-# LANGUAGE CPP #-}
module Main where

import Control.Monad
import Data.Char
import Data.List
import Data.Maybe
import qualified Data.Set as S
import Data.Time.Clock
import System.Directory
import System.Environment
import System.FilePath
import System.Exit
import System.Info
import System.IO
import System.Process

-- Because GHC earlier than 7.8 lacks setEnv
-- Install the setenv package on Windows.
#if __GLASGOW_HASKELL__ < 708
#ifndef mingw32_HOST_OS
import qualified System.Posix.Env as PE(setEnv)

setEnv k v = PE.setEnv k v True
#else
import System.SetEnv(setEnv)
#endif
#endif

data Flag = Update | Diff | ShowOutput | Quiet | Time deriving (Eq, Show, Ord)

type Flags = S.Set Flag

data Status = Success | Failure | Updated deriving (Eq, Show)

data Config = Config {
    flags :: Flags,
    idrOpts :: [String],
    tests :: [String]
} deriving (Show, Eq)

isQuiet conf = Quiet `S.member` (flags conf)
showOutput conf = ShowOutput `S.member` (flags conf)
showTime conf = Time  `S.member` (flags conf)
showDiff conf = Diff `S.member` (flags conf)
doUpdate conf = Update  `S.member` (flags conf)

checkTestName :: String -> Bool
checkTestName d = (all isDigit $ take 3 $ reverse d)
                    && (not $ isInfixOf "disabled" d)

enumTests :: IO [String]
enumTests = do
    cwd <- getCurrentDirectory
    dirs <- getDirectoryContents cwd
    return $ sort $ filter checkTestName dirs

parseFlag :: String -> Maybe Flag
parseFlag s = case s of
    "-u" -> Just Update
    "-d" -> Just Diff
    "-s" -> Just ShowOutput
    "-t" -> Just Time
    "-q" -> Just Quiet
    _    -> Nothing

parseFlags :: [String] -> (S.Set Flag, [String])
parseFlags xs = (S.fromList f, i)
    where
        f = catMaybes $ map parseFlag fl
        (fl, i) = partition (\s -> parseFlag s /= Nothing) xs

parseArgs :: [String] -> IO Config
parseArgs args = do
    (tests, rest) <- case args of
        ("all":xs) -> do
            et <- enumTests
            return (et, xs)
        ("without":xs) -> do
            t <- enumTests
            (blacklist, ys) <- return $ break (== "opts") xs
            return (t \\ blacklist, ys \\ ["opts"])
        (x:xs) -> do
            exists <- doesDirectoryExist x
            return (if checkTestName x && exists then [x] else [], xs)
        [] -> do
            et <- enumTests
            return (et, [])
    let (testOpts, idOpts) = parseFlags rest
    return $ Config testOpts idOpts tests

-- "bash" needed because Haskell has cmd as the default shell on windows, and
-- we also want to run the process with another current directory, so we get
-- this thing.
runInShell :: String -> [String] -> IO (ExitCode, String)
runInShell test opts = do
    (ec, output, _) <- readCreateProcessWithExitCode
                            ((proc "bash" ("run":opts)) { cwd = Just test,
                                                          std_out = CreatePipe })
                            ""
    return (ec, output)

runTest :: Config -> String -> IO Status
runTest conf test = do
    -- don't touch the current directory as we want to run these things
    -- in parallel in the future
    let inTest s = test ++ "/" ++ s
    t1 <- getCurrentTime
    (exitCode, output) <- runInShell test (idrOpts conf)
    t2 <- getCurrentTime
    expected <- readFile $ inTest "expected"
    writeFile (inTest "output") output
    res <- if (norm output == norm expected)
                then do putStrLn $ test ++ " finished...success"
                        return Success
                else if doUpdate conf
                    then do putStrLn $ test ++ " finished...UPDATE"
                            writeFile (inTest "expected") output
                            return Updated
                    else do putStrLn $ test ++ " finished...FAILURE"
                            _ <- rawSystem "diff" [inTest "output", inTest "expected"]
                            return Failure
    when (showTime conf) $ do
        let dt = diffUTCTime t2 t1
        putStrLn $ "Duration of " ++ test ++ " was " ++ show dt
    return res
  where 
    -- just pretend that backslashes are slashes for comparison
    -- purposes to avoid path problems, so don't write any tests
    -- that depend on that distinction in other contexts.
    -- Also rewrite newlines for consistency.
       norm ('\r':'\n':xs) = '\n' : norm xs
       norm ('\\':xs) = '/' : norm xs
       norm (x : xs) = x : norm xs
       norm [] = []

printStats :: Config -> [Status] -> IO ()
printStats conf stats = do
    let total = length stats
    let successful = length $ filter (== Success) stats
    let failures = length $ filter (== Failure) stats
    let updates = length $ filter (== Updated) stats
    putStrLn "\n----"
    putStrLn $ show total ++ " tests run: " ++ show successful ++ " succesful, "
                ++ show failures ++ " failed, " ++ show updates ++ " updated."
    let failed = map fst $ filter ((== Failure) . snd) $ zip (tests conf) stats
    when (failed /= []) $ do
        putStrLn "\nFailed tests:"
        mapM_ putStrLn failed
        putStrLn ""

runTests :: Config -> IO Bool
runTests conf = do
    stats <- mapM (runTest conf) (tests conf)
    unless (isQuiet conf) $ printStats conf stats
    return $ all (== Success) stats

runShow :: Config -> IO Bool
runShow conf = do
    mapM_ (\t -> callProcess "cat" [t++"/output"]) (tests conf)
    return True

runDiff :: Config -> IO Bool
runDiff conf = do
    mapM_ (\t -> do putStrLn $ "Differences in " ++ t ++ ":"
                    ec <- rawSystem "diff" [t++"/output", t++"/expected"]
                    when (ec == ExitSuccess) $ putStrLn "No differences found.")
          (tests conf)
    return True

whisper :: Config -> String -> IO ()
whisper conf s = do unless (isQuiet conf) $ putStrLn s

isWindows :: Bool
isWindows = os `elem` ["win32", "mingw32", "cygwin32"]

setPath :: Config -> IO ()
setPath conf = do
    maybeEnv <- lookupEnv "IDRIS"
    idrisExists <- case maybeEnv of
        Just idrisExe -> do
            let exeExtension = if isWindows then ".exe" else ""
            doesFileExist (idrisExe ++ exeExtension)
        Nothing -> return False
    if (idrisExists)
        then do
            idrisAbs <- makeAbsolute $ fromMaybe "" maybeEnv
            setEnv "IDRIS" idrisAbs
            whisper conf $ "Using " ++ idrisAbs
        else do
            path <- getEnv "PATH"
            setEnv "IDRIS" ""
            let sandbox = "../.cabal-sandbox/bin"
            hasBox <- doesDirectoryExist sandbox
            bindir <- if hasBox
                            then do
                                whisper conf $ "Using Cabal sandbox at " ++ sandbox
                                makeAbsolute sandbox
                            else do
                                stackExe <- findExecutable "stack"
                                case stackExe of
                                    Just stack -> do
                                        out <- readProcess stack ["path", "--dist-dir"] []
                                        stackDistDir <- return $ takeWhile (/= '\n') out
                                        let stackDir = "../" ++ stackDistDir ++ "/build/idris"
                                        whisper conf $ "Using stack work dir at " ++ stackDir
                                        makeAbsolute stackDir
                                    Nothing -> return ""
            when (bindir /= "") $ setEnv "PATH" (bindir ++ [searchPathSeparator] ++ path)

main = do
    hSetBuffering stdout LineBuffering
    withCabal <- doesDirectoryExist "test"
    when withCabal $ do
      setCurrentDirectory "test"
    args <- getArgs
    conf <- parseArgs args
    setPath conf
    t1 <- getCurrentTime
    res <- case tests conf of
                [] -> return True
                xs | showOutput conf -> runShow conf
                xs | showDiff conf -> runDiff conf
                xs -> runTests conf
    t2 <- getCurrentTime
    when (showTime conf) $ do
        let dt = diffUTCTime t2 t1
        putStrLn $ "Duration of Entire Test Suite was " ++ show dt
    unless res exitFailure