-- | Hspec expectations for the lens stuff
module Test.Hspec.Expectations.Lens
  ( -- * Expectations
    shouldHave
  , shouldNotHave
  , shouldView
  , shouldPreview
  , shouldList
  , shouldThrow
  , through
  ) where

import Control.Lens
import Control.Monad
import Control.Exception (SomeException)
import Control.Exception.Lens
import Data.Monoid (Any(..), All(..), First(..), Endo(..))
import Test.Hspec.Expectations (Expectation)
import Test.HUnit (assertBool, assertFailure)
import Text.Printf (printf)


infixl 1 `shouldHave`, `shouldNotHave`
infixl 1 `shouldView`, `shouldPreview`, `shouldList`
infixl 1 `shouldThrow`
infixl 1 `through`

-- | @s \`shouldHave\` l@ sets the expectation that 'Fold' @l@ has
-- non-zero number of targets in the structure @s@
--
-- > s `shouldBe` t ≡ s `shouldHave` only t
--
-- @
-- shouldHave :: 'Show' s => s -> 'Getter'     s a -> 'Expectation'
-- shouldHave :: 'Show' s => s -> 'Fold'       s a -> 'Expectation'
-- shouldHave :: 'Show' s => s -> 'Iso''       s a -> 'Expectation'
-- shouldHave :: 'Show' s => s -> 'Lens''      s a -> 'Expectation'
-- shouldHave :: 'Show' s => s -> 'Traversal'' s a -> 'Expectation'
-- shouldHave :: 'Show' s => s -> 'Prism''     s a -> 'Expectation'
-- @
shouldHave :: Show s => s -> Getting Any s a -> Expectation
s `shouldHave` l = assertBool msg (has l s)
 where
  msg = printf "This Fold has zero targets for %s" (show s)

-- | @s \`shouldNotHave\` l@ sets the expectation that 'Fold' @l@
-- has exactly zero targets in the structue @s@
--
-- @
-- shouldNotHave :: 'Show' s => s -> 'Getter'     s a -> 'Expectation'
-- shouldNotHave :: 'Show' s => s -> 'Fold'       s a -> 'Expectation'
-- shouldNotHave :: 'Show' s => s -> 'Iso''       s a -> 'Expectation'
-- shouldNotHave :: 'Show' s => s -> 'Lens''      s a -> 'Expectation'
-- shouldNotHave :: 'Show' s => s -> 'Traversal'' s a -> 'Expectation'
-- shouldNotHave :: 'Show' s => s -> 'Prism''     s a -> 'Expectation'
-- @
shouldNotHave :: Show s => s -> Getting All s a -> Expectation
s `shouldNotHave` l = assertBool msg (hasn't l s)
 where
  msg = printf "This Fold has non-zero targets for %s" (show s)

-- | @s \`shouldView\` t \`through\` l@ sets the expectation that
-- you can see target @t@ in the structure @s@ though a 'Getter' @l@
--
-- @
-- shouldView ::           ('Show' s, 'Show' a, 'Eq' a) => s -> a -> 'Getter'     s a -> 'Expectation'
-- shouldView :: ('Data.Monoid.Monoid' m, 'Show' s, 'Show' a, 'Eq' a) => s -> a -> 'Fold'       s m -> 'Expectation'
-- shouldView ::           ('Show' s, 'Show' a, 'Eq' a) => s -> a -> 'Iso''       s a -> 'Expectation'
-- shouldView ::           ('Show' s, 'Show' a, 'Eq' a) => s -> a -> 'Lens''      s a -> 'Expectation'
-- shouldView :: ('Data.Monoid.Monoid' m, 'Show' s, 'Show' a, 'Eq' a) => s -> a -> 'Traversal'' s m -> 'Expectation'
-- shouldView :: ('Data.Monoid.Monoid' m, 'Show' s, 'Show' a, 'Eq' a) => s -> a -> 'Prism''     s m -> 'Expectation'
-- @
shouldView :: (Show s, Show a, Eq a) => s -> a -> Getting a s a -> Expectation
(s `shouldView` t) l =
  let r = view l s in
    unless (r == t) $
      assertFailure (printf "Resulted in %s, but expected %s" (show r) (show t))

-- | @s \`shouldPreview\` t \`through\` l@ sets the expectation that
-- you @y@ is the first target of the 'Fold' @l@ in @s@
--
-- @
-- shouldPreview :: ('Show' s, 'Show' a, 'Eq' a) => s -> a -> 'Getter'     s a -> 'Expectation'
-- shouldPreview :: ('Show' s, 'Show' a, 'Eq' a) => s -> a -> 'Fold'       s a -> 'Expectation'
-- shouldPreview :: ('Show' s, 'Show' a, 'Eq' a) => s -> a -> 'Lens''      s a -> 'Expectation'
-- shouldPreview :: ('Show' s, 'Show' a, 'Eq' a) => s -> a -> 'Iso''       s a -> 'Expectation'
-- shouldPreview :: ('Show' s, 'Show' a, 'Eq' a) => s -> a -> 'Traversal'' s a -> 'Expectation'
-- shouldPreview :: ('Show' s, 'Show' a, 'Eq' a) => s -> a -> 'Prism''     s a -> 'Expectation'
-- @
shouldPreview :: (Show s, Show a, Eq a) => s -> a -> Getting (First a) s a -> Expectation
(s `shouldPreview` t) l =
  case preview l s of
    Nothing ->
      assertFailure (printf "No targets, but expected %s" (show t))
    Just r  -> unless (r == t) $
      assertFailure (printf "Resulted in %s, but expected %s" (show r) (show t))

-- | @s \`shouldList\` ts \`through\` l@ sets the expectation that
-- @ts@ is a list of the Fold @l@ targets in @x@
--
-- @
-- shouldList :: ('Show' s, 'Show' a, 'Eq' a) => s -> [a] -> 'Getter'     s a -> 'Expectation'
-- shouldList :: ('Show' s, 'Show' a, 'Eq' a) => s -> [a] -> 'Fold'       s a -> 'Expectation'
-- shouldList :: ('Show' s, 'Show' a, 'Eq' a) => s -> [a] -> 'Lens''      s a -> 'Expectation'
-- shouldList :: ('Show' s, 'Show' a, 'Eq' a) => s -> [a] -> 'Iso''       s a -> 'Expectation'
-- shouldList :: ('Show' s, 'Show' a, 'Eq' a) => s -> [a] -> 'Traversal'' s a -> 'Expectation'
-- shouldList :: ('Show' s, 'Show' a, 'Eq' a) => s -> [a] -> 'Prism''     s a -> 'Expectation'
-- @
shouldList :: (Show s, Show a, Eq a) => s -> [a] -> Getting (Endo [a]) s a -> Expectation
(s `shouldList` t) l =
  let r = toListOf l s in
    unless (r == t) $
      assertFailure (printf "Resulted in %s, but expected %s" (show r) (show t))

-- | @a \`shouldThrow\` l@ sets the expectation that
-- @a@ throws an exception that 'Fold' @l@ can catch
--
-- "Test.Hspec" exports 'Test.Hspec.Expectations.shouldThrow' too; it
-- only allows @e -> Bool@ selectors, which is less general and often less convenient
--
-- @
-- shouldThrow :: 'IO' a -> 'Getter'     'SomeException' b -> 'Expectation'
-- shouldThrow :: 'IO' a -> 'Fold'       'SomeException' b -> 'Expectation'
-- shouldThrow :: 'IO' a -> 'Lens''      'SomeException' b -> 'Expectation'
-- shouldThrow :: 'IO' a -> 'Iso''       'SomeException' b -> 'Expectation'
-- shouldThrow :: 'IO' a -> 'Traversal'' 'SomeException' b -> 'Expectation'
-- shouldThrow :: 'IO' a -> 'Prism''     'SomeException' b -> 'Expectation'
-- @
shouldThrow :: IO a -> Getting (First b) SomeException b -> Expectation
x `shouldThrow` l = do
  r <- trying l x
  case r of
    Left  _ -> return ()
    Right _ -> assertFailure "Couldn't catch any exceptions"

-- | `through` fights parentheses
--
-- @
-- through ≡ id
-- @
--
-- @
-- through :: 'Int' -> 'Int'
-- through :: 'Char' -> 'Char'
-- @
through :: a -> a
through = id