{-# OPTIONS_HADDOCK show-extensions #-}

module Test.Fluent.Assertions.Core
  ( -- ** Assertion util functions
    assertThat,
    assertThatIO,
    assertThat',
    assertThatIO',
    assertThrown,
    assertThrown',
    assertThrows',
    assertThrows,
  )
where

import Control.Exception (Exception (fromException), throwIO, try)
import Data.Data (typeOf)
import GHC.Stack (HasCallStack, callStack, getCallStack)
import Test.Fluent.Assertions (FluentTestFailure (..), simpleAssertion)
import Test.Fluent.Assertions.Exceptions (ExceptionSelector)
import Test.Fluent.Internal.AssertionConfig
  ( AssertionConfig,
    defaultConfig,
  )
import Test.Fluent.Internal.Assertions
  ( Assertion',
    assertThat,
    assertThat',
    assertThatIO,
    assertThatIO',
    assertThatIO'',
  )

-- |
-- Module      : Test.Fluent.Assertions.Core
-- Description : Set util function to execute assertions against given value
-- Copyright   : (c) Pawel Nosal, 2021
-- License     : MIT
-- Maintainer  : p.nosal1986@gmail.com
-- Stability   : experimental

-- | Verify if given `IO` action throws expected exception.
assertThrows :: (HasCallStack, Exception e) => IO a -> ExceptionSelector e -> IO ()
assertThrows givenIO selector = assertThrown' defaultConfig givenIO selector (simpleAssertion (const True) (const "should not be invoked"))

assertThrows' :: (HasCallStack, Exception e) => AssertionConfig -> IO a -> ExceptionSelector e -> IO ()
assertThrows' config givenIO selector = assertThrown' config givenIO selector (simpleAssertion (const True) (const "should not be invoked"))

-- | Execute assertions against selected exception
assertThrown :: (HasCallStack, Exception e) => IO a -> ExceptionSelector e -> Assertion' e b -> IO ()
assertThrown = assertThrown' defaultConfig

assertThrown' :: (HasCallStack, Exception e) => AssertionConfig -> IO a -> ExceptionSelector e -> Assertion' e b -> IO ()
assertThrown' config givenIO predicate = assertThatIO'' config givenIO $ \io -> do
  res <- try io
  case res of
    Left e -> do
      let thrownException = show e
      case fromException e of
        Just expectedException -> pure expectedException
        Nothing -> throwIO (FluentTestFailure location [("should throw an exception of type " <> expectedExceptionName <> " , but " <> thrownException <> " has been thrown", location)] 1 0)
    _ -> throwIO (FluentTestFailure location [("should throw an exception of type " <> expectedExceptionName <> ", but it doesn't", location)] 1 0)
  where
    expectedExceptionName = show $ typeOf (exceptionName predicate)
    exceptionName :: (a -> a) -> a
    exceptionName _ = error "instance of Typeable is broken"
    location = case reverse (getCallStack callStack) of
      (_, loc) : _ -> Just loc
      [] -> Nothing
