-- |This module defines an api for matchers: rules that can pass or fail,
-- and describe their failure and success conditions for humans to read.
--
-- This module also exports some useful matchers for things in the "Prelude",
-- and some combinators that are useful for combining several matchers into one.
module Control.Rematch(
    Matcher(..)
  -- ** Useful functions for running matchers
  , runMatch
  -- ** Basic Matchers
  , is
  , equalTo
  -- ** Matchers on lists
  , isEmpty
  , hasSize
  , everyItem
  , hasItem
  -- ** Matchers on Ord
  , greaterThan
  , greaterThanOrEqual
  , lessThan
  , lessThanOrEqual
  -- ** Matchers on Maybe
  , isJust
  , hasJust
  , isNothing
  -- ** Matchers on Either
  , isRight
  , hasRight
  , isLeft
  , hasLeft
  -- ** Matcher combinators
  , isNot
  , allOf
  , anyOf
  , on
  -- ** Utility functions for writing your own matchers
  , matcherOn
  , matchList
  , standardMismatch
  ) where
import qualified Data.Maybe as M
import Control.Rematch.Run
import Control.Rematch.Formatting

-- |The basic api for a matcher
data Matcher a = Matcher {
    match :: a -> Bool
  -- ^ A function that returns True if the matcher should pass, False if it should fail
  , description :: String
  -- ^ A description of the matcher (usually of its success conditions)
  , describeMismatch :: a -> String
  -- ^ A description to be shown if the match fails.
  }

-- |Inverts a matcher, so success becomes failure, and failure
-- becomes success
isNot :: Matcher a -> Matcher a
isNot (Matcher m desc mismatch) = Matcher (not . m) ("isNot " ++ desc) mismatch

-- |Run a matcher, producing a Match with a good error string
runMatch :: Matcher a -> a -> Match
runMatch m a = if match m a
  then MatchSuccess
  else MatchFailure $ "\nExpected: " ++ description m ++ "\n     but: " ++ describeMismatch m a

-- |Matcher on equality
is :: (Show a, Eq a) => a -> Matcher a
is a = Matcher (a == ) ("equalTo " ++ show a) standardMismatch

-- |Matcher on equality
equalTo :: (Show a, Eq a) => a -> Matcher a
equalTo = is

-- |Matches if all of a list of matchers pass
allOf :: [Matcher a] -> Matcher a
allOf [] = Matcher (const False) "allOf" (const "was: no matchers supplied")
allOf matchers = Matcher {
    match = and . matchList matchers
  , description = describeList "all" $ map description matchers
  , describeMismatch = \a -> describeList "" (map (`describeMismatch` a) (filter (\m -> not $ match m a) matchers))
  }

-- |Matches if any of a list of matchers pass
anyOf :: [Matcher a] -> Matcher a
anyOf [] = Matcher (const False) "anyOf" (const "was: no matchers supplied")
anyOf matchers = Matcher {
    match = or . matchList matchers
  , description = describeList "or" $ map description matchers
  , describeMismatch = \a -> describeList "" (map (`describeMismatch` a) matchers)
  }

-- |A combinator that translates Matcher a to Matcher b using
-- a function :: (a -> b)
-- Takes a name of the function for better error messages
--
-- Using this as an infix operator gets you some nice syntax:
-- expect ((is 1) `on` (length, "length")) []
on :: Matcher b -> ((a -> b), String) -> Matcher a
on m (f, name) = Matcher {
    match = match m . f
  , description = name ++ " " ++ (description m)
  , describeMismatch = describeMismatch m  . f
  }

-- |Matches if every item in the input list passes a matcher
everyItem :: Matcher a -> Matcher [a]
everyItem m = Matcher {
    match = all (match m)
  , description = "everyItem(" ++ description m ++ ")"
  , describeMismatch = describeList "" . map (describeMismatch m) . filter (not . match m)
  }

-- |Matches if any of the items in the input list passes the provided matcher
hasItem :: Matcher a -> Matcher [a]
hasItem m = Matcher {
    match = any (match m)
  , description = "hasItem(" ++ description m ++ ")"
  , describeMismatch = go
  }
  where go [] = "got an empty list: []"
        go as = describeList "" (map (describeMismatch m) as)

-- |Matches if the input list is empty
isEmpty :: (Show a) => Matcher [a]
isEmpty = Matcher {
    match = null
  , description = "isEmpty"
  , describeMismatch = standardMismatch
  }

-- |Matches if the input list has the required size
hasSize :: (Show a) => Int -> Matcher [a]
hasSize n = Matcher {
    match = ((== n) . length)
  , description = "hasSize(" ++ show n ++ ")"
  , describeMismatch = standardMismatch
  }

-- |Builds a Matcher a out of a name and a function from (a -> a -> Bool)
-- Succeeds if the function returns true, fails if the function returns false
matcherOn :: (Show a) => String -> (a -> a -> Bool) -> a -> Matcher a
matcherOn name comp a = Matcher {
    match = comp a
  , description = name ++ "(" ++ show a ++ ")"
  , describeMismatch = standardMismatch
  }

-- |Matches if the input is greater than the required number
greaterThan :: (Ord a, Show a) => a -> Matcher a
greaterThan = matcherOn "greaterThan" (<)

-- |Matches if the input is greater than or equal to the required number
greaterThanOrEqual :: (Ord a, Show a) => a -> Matcher a
greaterThanOrEqual = matcherOn "greaterThanOrEqual" (<=)

-- |Matches if the input is less than the required number
lessThan :: (Ord a, Show a) => a -> Matcher a
lessThan = matcherOn "lessThan" (>)

-- |Matches if the input is less than or equal to the required number
lessThanOrEqual :: (Ord a, Show a) => a -> Matcher a
lessThanOrEqual = matcherOn "lessThanOrEqual" (>=)

-- |Matches if the input is (Just a)
isJust :: (Show a) => Matcher (Maybe a)
isJust = Matcher {
    match = M.isJust
  , description = "isJust"
  , describeMismatch = standardMismatch
  }

-- |Matcher combinator, turns Matcher a to Matcher (Maybe a)
-- Fails if the Maybe is Nothing, otherwise tries the original
-- matcher on the content of the Maybe
hasJust :: Matcher a -> Matcher (Maybe a)
hasJust matcher = Matcher {
    match = (\a -> M.isJust a && (match matcher (M.fromJust a)))
  , description = "hasJust(" ++ description matcher ++ ")"
  , describeMismatch = mismatchDescription
  }
  where mismatchDescription (Just x) = matcher `describeMismatch` x
        mismatchDescription Nothing  = "but was Nothing"

-- |Matches if the input is Nothing
isNothing :: (Show a) => Matcher (Maybe a)
isNothing = Matcher {
    match = M.isNothing
  , description = "isNothing"
  , describeMismatch = standardMismatch
  }

-- |Matches if an Either is Right
isRight :: (Show a, Show b) => Matcher (Either a b)
isRight = Matcher {
    match = go
  , description = "isRight"
  , describeMismatch = standardMismatch
  }
  where go (Right _) = True
        go (Left _) = False

-- |Matcher combinator: turns a Matcher b into a Matcher on the
-- Right side of an Either a b
hasRight :: (Show a, Show b) => Matcher b -> Matcher (Either a b)
hasRight matcher = Matcher {
    match = (\e -> case e of
                (Right a) -> match matcher a
                (Left _) -> False)
  , description = "hasRight(" ++ description matcher ++ ")"
  , describeMismatch = standardMismatch
  }

-- |Matches if an Either is Left
isLeft :: (Show a, Show b) => Matcher (Either a b)
isLeft = Matcher {
    match = go
  , description = "isLeft"
  , describeMismatch = standardMismatch
  }
  where go (Left _) = True
        go (Right _) = False

-- |Matcher combinator: turns a Matcher a into a Matcher on the
-- Left side of an Either a b
hasLeft :: (Show a, Show b) => Matcher a -> Matcher (Either a b)
hasLeft matcher = Matcher {
    match = (\e -> case e of
                (Left a) -> match matcher a
                (Right _) -> False)
  , description = "hasRight(" ++ description matcher ++ ")"
  , describeMismatch = standardMismatch
  }


-- |Utility function for running a list of matchers
matchList :: [Matcher a] -> a -> [Bool]
matchList matchers a = map (`match` a) matchers

-- |A standard mismatch description on (Show a):
-- standardMismatch 1 == "was 1"
standardMismatch :: (Show a) => a -> String
standardMismatch a = "was " ++ show a