-- | The entry point for mucheck
module Test.MuCheck.Interpreter (evaluateMutants) where

import qualified Language.Haskell.Interpreter as I
import Control.Monad.Trans (liftIO)
import Control.Monad (liftM)
import Data.Typeable
import Test.MuCheck.Utils.Print (showA, showAS, (./.), catchOutput)
import Data.Either (partitionEithers, rights)
import Data.List(groupBy, sortBy)
import Data.Function (on)
import System.Directory (createDirectoryIfMissing)
import Data.Hash.MD5 (md5s, Str(..))

import Test.MuCheck.TestAdapter

-- | Given the list of tests suites to check, run one test suite at a time on
-- all mutants.
evaluateMutants :: (Summarizable a, Show a) => ([Mutant] -> [InterpreterOutput a] -> Summary) -> [Mutant] -> [String] -> FilePath -> IO ()
evaluateMutants testSummaryFn mutantFiles evalSrcLst logFile  = do
  results <- mapM (runCodeOnMutants mutantFiles) evalSrcLst
  let singleTestSummaries = zip evalSrcLst $ map (mySummaryFn testSummaryFn mutantFiles) results
      tssum  = multipleCheckSummary (isSuccess . snd) results
  -- print results to terminal
  putStrLn $ delim ++ "Overall Results:"
  putStrLn $ terminalSummary tssum
  putStrLn $ showAS $ map showBrief singleTestSummaries
  putStr delim
  -- print results to logfile
  appendFile logFile $ "OVERALL RESULTS:\n" ++ tssumLog tssum ++ showAS (map showDetail singleTestSummaries)
  return ()
  where showDetail (method, msum) = delim ++ showBrief (method, msum) ++ "\n" ++ tsumLog msum
        showBrief (method, msum) = showAS [method,
           "\tTotal number of mutants:\t" ++ show (tsumNumMutants msum),
           "\tFailed to Load:\t" ++ cpx tsumLoadError,
           "\tNot Killed:\t" ++ cpx tsumNotKilled,
           "\tKilled:\t" ++ cpx tsumKilled,
           "\tOthers:\t" ++ cpx tsumOthers,
           ""]
           where cpx fn = show (fn msum) ++ " " ++ fn msum ./. tsumNumMutants msum
        terminalSummary tssum = showAS [
          "Total number of mutants:\t" ++ show (tssumNumMutants tssum),
          "Total number of alive mutants:\t" ++ cpx tssumAlive,
          "Total number of load errors:\t" ++ cpx tssumErrors,
          ""]
           where cpx fn = show (fn tssum) ++ " " ++ fn tssum ./. tssumNumMutants tssum
        delim = "\n" ++ replicate 25 '=' ++ "\n"


data TSum = TSum {tsumNumMutants::Int,
                  tsumLoadError::Int,
                  tsumNotKilled::Int,
                  tsumKilled::Int,
                  tsumOthers::Int,
                  tsumLog::String}

mySummaryFn :: Summarizable a => ([Mutant] -> [InterpreterOutput a] -> Summary) -> [Mutant] -> [InterpreterOutput a] -> TSum
mySummaryFn testSummaryFn mutantFiles results = TSum {
    tsumNumMutants = r,
    tsumLoadError = l,
    tsumNotKilled = s,
    tsumKilled = f,
    tsumOthers = g,
    tsumLog = logStr}
  where (errorCases, executedCases) = partitionEithers results
        [successCases, failureCases, otherCases] = map (\c -> filter (c . snd) executedCases) [isSuccess, isFailure, isOther]
        r = length results
        l = length errorCases
        [s,f,g] = map length [successCases, failureCases, otherCases]
        -- errorFiles = mutantFiles \\ map fst executedCases
        Summary logStr = testSummaryFn mutantFiles results


-- | Run one test suite on all mutants
runCodeOnMutants :: Typeable t => [Mutant] -> String -> IO [InterpreterOutput t]
runCodeOnMutants mutantFiles evalStr = mapM (evalMyStr evalStr) mutantFiles
  where evalMyStr eStr m = liftM fst $ do
          createDirectoryIfMissing True ".mutants"
          let fPath = ".mutants/" ++ md5s (Str m) ++ ".hs"
          -- Hint does not provide us a way to evaluate the module without
          -- writing it to disk first, so we go for this hack.
          -- We write the temporary file to disk, run interpreter on it, get
          -- the result and remove the file
          writeFile fPath m
          putStrLn $ "> " ++ fPath ++ " " ++ evalStr
          catchOutput (I.runInterpreter (evalMethod fPath eStr))

-- | Given the filename, modulename, test to evaluate, evaluate, and return result as a pair.
--
-- > t = I.runInterpreter (evalMethod
-- >        "Examples/QuickCheckTest.hs"
-- >        "quickCheckResult idEmp")
evalMethod :: (I.MonadInterpreter m, Typeable t) => String -> String -> m (String, t)
evalMethod fileName evalStr = do
  I.loadModules [fileName]
  ms <- I.getLoadedModules
  I.setTopLevelModules ms
  result <- I.interpret evalStr (I.as :: (Typeable a => IO a)) >>= liftIO
  return (fileName, result)

-- | Datatype to hold results of the entire run
data TSSum = TSSum {tssumNumMutants::Int,
                    tssumAlive::Int,
                    tssumErrors::Int,
                    tssumLog::String}

-- | Summarize the entire run
multipleCheckSummary :: Show b => ((String, b) -> Bool) -> [[InterpreterOutput b]] -> TSSum
multipleCheckSummary isSuccessFunction results
  -- we assume that checking each prop results in the same number of errorCases and executedCases
  | not (checkLength results) = error "Output lengths differ for some properties."
  | otherwise = TSSum {tssumNumMutants = countMutants,
                       tssumAlive = countAlive,
                       tssumErrors= countErrors,
                       tssumLog = logMsg}
  where executedCases = groupBy ((==) `on` fst) . sortBy (compare `on` fst) . rights $ concat results
        allSuccesses = [rs | rs <- executedCases, length rs == length results, all isSuccessFunction rs]
        countAlive = length allSuccesses
        countErrors = countMutants - length executedCases
        logMsg = showA allSuccesses
        checkLength res = and $ map ((==countMutants) . length) res ++ map ((==countExecutedCases) . length) executedCases
        countExecutedCases = length . head $ executedCases
        countMutants = length . head $ results