--------------------------------------------------------------------------------
-- Test Suite for Curry Base
--------------------------------------------------------------------------------
-- 
-- This Test Suite supports three kinds of tests:
-- 
-- 1) tests which should pass
-- 2) tests which should pass with a specific warning
-- 3) tests which should fail yielding a specific error message
-- 
-- In order to add a test to this suite, proceed as follows:
-- 
-- 1) Store your test code in a file (please use descriptive names) and put it
--    in the corresponding subfolder (i.e. test/pass for passing tests,
--    test/fail for failing tests and test/warning for passing tests producing
--    warnings)
-- 2) Extend the corresponding test information list (there is one for each test
--    group at the end of this file) with the required information (i.e. name of
--    the Curry module to be tested and expected warning/failure message(s))
-- 3) Run 'cabal test'

{-# LANGUAGE CPP #-}
module TestBase (tests) where

#if __GLASGOW_HASKELL__ < 710
import Control.Applicative    ((<$>))
#endif

import Control.Monad.Trans    (lift)
import Data.List              (isInfixOf, sort)
import Distribution.TestSuite
import System.FilePath        (FilePath, (</>), (<.>))

import Curry.Base.Message     (Message, ppMessages, ppError)
import Curry.Base.Monad       (CYIO, runCYIO, liftCYM)
import Curry.Files.PathUtils  (readModule)
import Curry.Syntax           (parseModule, unlit)

tests :: IO [Test]
tests = return [passingTests, warningTests, failingTests]

-- Call the Curry parser
parseCurry :: FilePath -> CYIO ()
parseCurry file = do
  msrc <- lift $ lift $ readModule file
  case msrc of Nothing  -> error $ "Missing file " ++ file
               Just src -> liftCYM $ do ul <- unlit file src
                                        parseModule file ul
                                        return ()

-- Execute a test by calling cymake
runTest :: String -> [String] -> IO Progress
runTest test [] = runCYIO (parseCurry test) >>= passOrFail
 where
  passOrFail = (Finished <$>) . either fail pass
  fail msgs
    | null msgs = return Pass
    | otherwise = let errorStr = showMessages msgs
                  in return $ Fail $ "An unexpected failure occurred: " ++ errorStr
  pass _     = return Pass
runTest test errorMsgs = runCYIO (parseCurry test) >>= catchE
 where
   catchE    = (Finished <$>) . either pass fail
   pass msgs = let errorStr = showMessages msgs
               in if all (`isInfixOf` errorStr) errorMsgs
                    then return Pass
                    else return $ Fail $ "Expected warning/failure did not occur: " ++ errorStr
   fail      = pass . snd

showMessages :: [Message] -> String
showMessages = show . ppMessages ppError . sort

-- group of tests which should pass
passingTests :: Test
passingTests = Group { groupName    = "Passing Tests"
                     , concurrently = False
                     , groupTests   = map (mkTest "test/pass/") passInfos
                     }

-- group of test which should fail yielding a specific error message
failingTests :: Test
failingTests = Group { groupName    = "Failing Tests"
                     , concurrently = False
                     , groupTests   = map (mkTest "test/fail/") failInfos
                     }

-- group of tests which should pass producing a specific warning message
warningTests :: Test
warningTests = Group { groupName    = "Warning Tests"
                     , concurrently = False
                     , groupTests   = map (mkTest "test/warning/") warnInfos
                     }

-- create a new test
mkTest :: FilePath -> TestInfo -> Test
mkTest path (testName, testTags, testOpts, mSetOpts, errorMsgs) =
  let file = path </> testName <.> "curry"
      test = TestInstance
        { run       = runTest file errorMsgs
        , name      = testName
        , tags      = testTags
        , options   = testOpts
        , setOption = maybe (\_ _ -> Right test) id mSetOpts
        }
  in Test test

-- Information for a test instance:
-- * name of test
-- * tags to classify a test
-- * options
-- * function to set options
-- * optional warning/error message which should be thrown on execution of test
type TestInfo = (String, [String], [OptionDescr], Maybe SetOption, [String])

type SetOption = String -> String -> Either String TestInstance

--------------------------------------------------------------------------------
-- Definition of passing tests
--------------------------------------------------------------------------------

-- generate a simple passing test
mkPassTest :: String -> TestInfo
mkPassTest name = (name, [], [], Nothing, [])

-- To add a passing test to the test suite simply add the module name of the
-- test code to the following list
-- TODO: add test cases
passInfos :: [TestInfo]
passInfos = map mkPassTest []

--------------------------------------------------------------------------------
-- Definition of failing tests
--------------------------------------------------------------------------------

-- generate a simple failing test
mkFailTest :: String -> [String] -> TestInfo
mkFailTest name errorMsgs = (name, [], [], Nothing, errorMsgs)

-- To add a failing test to the test suite simply add the module name of the
-- test code and the expected error message(s) to the following list
-- TODO: add test cases
failInfos :: [TestInfo]
failInfos = map (uncurry mkFailTest) []

--------------------------------------------------------------------------------
-- Definition of warning tests
--------------------------------------------------------------------------------

-- To add a warning test to the test suite simply add the module name of the
-- test code and the expected warning message(s) to the following list
-- TODO: add test cases
warnInfos :: [TestInfo]
warnInfos = map (uncurry mkFailTest) []
