{-# LANGUAGE ScopedTypeVariables #-}

-- TODO This module was originally based on the PackageTests.PackageTester
-- module in Cabal, however it has a few differences. I suspect that as
-- this module ages the two modules will diverge further. As such, I have
-- not attempted to merge them into a single module nor to extract a common
-- module from them.  Refactor this module and/or Cabal's
-- PackageTests.PackageTester to remove commonality.
--   2014-05-15 Ben Armston

-- | Routines for black-box testing cabal-install.
--
-- Instead of driving the tests by making library calls into
-- Distribution.Simple.* or Distribution.Client.* this module only every
-- executes the `cabal-install` binary.
--
-- You can set the following VERBOSE environment variable to control
-- the verbosity of the output generated by this module.
module PackageTests.PackageTester
    ( TestsPaths(..)
    , Result(..)

    , packageTestsDirectory
    , packageTestsConfigFile

    -- * Running cabal commands
    , cabal_clean
    , cabal_exec
    , cabal_freeze
    , cabal_install
    , cabal_sandbox
    , run

    -- * Test helpers
    , assertCleanSucceeded
    , assertExecFailed
    , assertExecSucceeded
    , assertFreezeSucceeded
    , assertInstallSucceeded
    , assertSandboxSucceeded
    ) where

import qualified Control.Exception.Extensible as E
import Control.Monad (when, unless)
import Data.Maybe (fromMaybe)
import System.Directory (canonicalizePath, doesFileExist)
import System.Environment (getEnv)
import System.Exit (ExitCode(ExitSuccess))
import System.FilePath ( (<.>)  )
import System.IO (hClose, hGetChar, hIsEOF)
import System.IO.Error (isDoesNotExistError)
import System.Process (runProcess, waitForProcess)
import Test.HUnit (Assertion, assertFailure)

import Distribution.Simple.BuildPaths (exeExtension)
import Distribution.Simple.Utils (printRawCommandAndArgs)
import Distribution.Compat.CreatePipe (createPipe)
import Distribution.ReadE (readEOrFail)
import Distribution.Verbosity (Verbosity, flagToVerbosity, normal)

data Success = Failure
             -- | ConfigureSuccess
             -- | BuildSuccess
             -- | TestSuccess
             -- | BenchSuccess
             | CleanSuccess
             | ExecSuccess
             | FreezeSuccess
             | InstallSuccess
             | SandboxSuccess
             deriving (Eq, Show)

data TestsPaths = TestsPaths
    { cabalPath  :: FilePath -- ^ absolute path to cabal executable.
    , ghcPkgPath :: FilePath -- ^ absolute path to ghc-pkg executable.
    , configPath :: FilePath -- ^ absolute path of the default config file
                             --   to use for tests (tests are free to use
                             --   a different one).
    }

data Result = Result
    { successful :: Bool
    , success    :: Success
    , outputText :: String
    } deriving Show

nullResult :: Result
nullResult = Result True Failure ""

------------------------------------------------------------------------
-- * Config

packageTestsDirectory :: FilePath
packageTestsDirectory = "PackageTests"

packageTestsConfigFile :: FilePath
packageTestsConfigFile = "cabal-config"

------------------------------------------------------------------------
-- * Running cabal commands

recordRun :: (String, ExitCode, String) -> Success -> Result -> Result
recordRun (cmd, exitCode, exeOutput) thisSucc res =
    res { successful = successful res && exitCode == ExitSuccess
        , success    = if exitCode == ExitSuccess then thisSucc
                       else success res
        , outputText =
            (if null $ outputText res then "" else outputText res ++ "\n") ++
            cmd ++ "\n" ++ exeOutput
        }

-- | Run the clean command and return its result.
cabal_clean :: TestsPaths -> FilePath -> [String] -> IO Result
cabal_clean paths dir args = do
    res <- cabal paths dir (["clean"] ++ args)
    return $ recordRun res CleanSuccess nullResult

-- | Run the exec command and return its result.
cabal_exec :: TestsPaths -> FilePath -> [String] -> IO Result
cabal_exec paths dir args = do
    res <- cabal paths dir (["exec"] ++ args)
    return $ recordRun res ExecSuccess nullResult

-- | Run the freeze command and return its result.
cabal_freeze :: TestsPaths -> FilePath -> [String] -> IO Result
cabal_freeze paths dir args = do
    res <- cabal paths dir (["freeze"] ++ args)
    return $ recordRun res FreezeSuccess nullResult

-- | Run the install command and return its result.
cabal_install :: TestsPaths -> FilePath -> [String] -> IO Result
cabal_install paths dir args = do
    res <- cabal paths dir (["install"] ++ args)
    return $ recordRun res InstallSuccess nullResult

-- | Run the sandbox command and return its result.
cabal_sandbox :: TestsPaths -> FilePath -> [String] -> IO Result
cabal_sandbox paths dir args = do
    res <- cabal paths dir (["sandbox"] ++ args)
    return $ recordRun res SandboxSuccess nullResult

-- | Returns the command that was issued, the return code, and the output text.
cabal :: TestsPaths -> FilePath -> [String] -> IO (String, ExitCode, String)
cabal paths dir cabalArgs = do
    run (Just dir) (cabalPath paths) args
  where
    args = configFileArg : cabalArgs
    configFileArg = "--config-file=" ++ configPath paths

-- | Returns the command that was issued, the return code, and the output text
run :: Maybe FilePath -> String -> [String] -> IO (String, ExitCode, String)
run cwd path args = do
    verbosity <- getVerbosity
    -- path is relative to the current directory; canonicalizePath makes it
    -- absolute, so that runProcess will find it even when changing directory.
    path' <- do pathExists <- doesFileExist path
                canonicalizePath (if pathExists then path else path <.> exeExtension)
    printRawCommandAndArgs verbosity path' args
    (readh, writeh) <- createPipe
    pid <- runProcess path' args cwd Nothing Nothing (Just writeh) (Just writeh)

    -- fork off a thread to start consuming the output
    out <- suckH [] readh
    hClose readh

    -- wait for the program to terminate
    exitcode <- waitForProcess pid
    let fullCmd = unwords (path' : args)
    return ("\"" ++ fullCmd ++ "\" in " ++ fromMaybe "" cwd, exitcode, out)
  where
    suckH output h = do
        eof <- hIsEOF h
        if eof
            then return (reverse output)
            else do
                c <- hGetChar h
                suckH (c:output) h

------------------------------------------------------------------------
-- * Test helpers

assertCleanSucceeded :: Result -> Assertion
assertCleanSucceeded result = unless (successful result) $
    assertFailure $
    "expected: \'cabal clean\' should succeed\n" ++
    "  output: " ++ outputText result

assertExecSucceeded :: Result -> Assertion
assertExecSucceeded result = unless (successful result) $
    assertFailure $
    "expected: \'cabal exec\' should succeed\n" ++
    "  output: " ++ outputText result

assertExecFailed :: Result -> Assertion
assertExecFailed result = when (successful result) $
    assertFailure $
    "expected: \'cabal exec\' should fail\n" ++
    "  output: " ++ outputText result

assertFreezeSucceeded :: Result -> Assertion
assertFreezeSucceeded result = unless (successful result) $
    assertFailure $
    "expected: \'cabal freeze\' should succeed\n" ++
    "  output: " ++ outputText result

assertInstallSucceeded :: Result -> Assertion
assertInstallSucceeded result = unless (successful result) $
    assertFailure $
    "expected: \'cabal install\' should succeed\n" ++
    "  output: " ++ outputText result

assertSandboxSucceeded :: Result -> Assertion
assertSandboxSucceeded result = unless (successful result) $
    assertFailure $
    "expected: \'cabal sandbox\' should succeed\n" ++
    "  output: " ++ outputText result

------------------------------------------------------------------------
-- Verbosity

lookupEnv :: String -> IO (Maybe String)
lookupEnv name =
    (fmap Just $ getEnv name)
    `E.catch` \ (e :: IOError) ->
        if isDoesNotExistError e
        then return Nothing
        else E.throw e

-- TODO: Convert to a "-v" flag instead.
getVerbosity :: IO Verbosity
getVerbosity = do
    maybe normal (readEOrFail flagToVerbosity) `fmap` lookupEnv "VERBOSE"
