content: {-# LANGUAGE ScopedTypeVariables #-}
  
  module CoreTests.UtilTests where
  
  import AgentTests.FunctionalAPITests ()
  import Control.Exception (Exception, SomeException, throwIO)
  import Control.Monad.Except
  import Control.Monad.IO.Class
  import Data.IORef
  import Simplex.Messaging.Util
  import Test.Hspec hiding (fit, it)
  import qualified UnliftIO.Exception as UE
  import Util
  
  utilTests :: Spec
  utilTests = do
    describe "problems of lifted try, catch and finally (don't use them)" $ do
      describe "lifted try" $ do
        it "does not catch errors" $ do
          runExceptT (UE.try throwTestError >>= either handleCatch pure) `shouldReturn` Left (TestError "error")
          runExceptT (UE.try throwTestException >>= either handleCatch pure) `shouldThrow` (\(e :: IOError) -> show e == "user error (error)")
        it "with SomeException catches all errors but wraps ExceptT errors" $ do
          runExceptT (UE.try throwTestError >>= either handleException pure) `shouldReturn` Right "caught InternalException {unInternalException = TestError \"error\"}"
          runExceptT (UE.try throwTestException >>= either handleException pure) `shouldReturn` Right "caught user error (error)"
      describe "lifted catch" $ do
        it "does not catch" $ do
          runExceptT (throwTestError `UE.catch` handleCatch) `shouldReturn` Left (TestError "error")
          runExceptT (throwTestException `UE.catch` handleCatch) `shouldThrow` (\(e :: IOError) -> show e == "user error (error)")
        it "with SomeException catches all errors but wraps ExceptT errors" $ do
          runExceptT (throwTestError `UE.catch` handleException) `shouldReturn` Right "caught InternalException {unInternalException = TestError \"error\"}"
          runExceptT (throwTestException `UE.catch` handleException) `shouldReturn` Right "caught user error (error)"
      describe "lifted finally" $ do
        it "with ExceptT error executes final action and stays in ExceptT monad" $ withFinal $ \final ->
          runExceptT (throwTestError `UE.finally` final) `shouldReturn` Left (TestError "error")
        it "with exception executes final action (not always - race condition?) and throws exception" $ withFinal $ \final ->
          runExceptT (throwTestException `UE.finally` final) `shouldThrow` (\(e :: IOError) -> show e == "user error (error)")
    describe "problems of tryError and catchError (don't use them)" $ do
      describe "tryError" $ do
        it "catches ExceptT errors but not Exceptions" $ do
          runExceptT (tryError throwTestError >>= either handleCatch pure) `shouldReturn` Right "caught TestError \"error\""
          runExceptT (tryError throwTestException >>= either handleCatch pure) `shouldThrow` (\(e :: IOError) -> show e == "user error (error)")
      describe "catchError" $ do
        it "catches ExceptT errors but not Exceptions" $ do
          runExceptT (throwTestError `catchError` handleCatch) `shouldReturn` Right "caught TestError \"error\""
          runExceptT (throwTestException `catchError` handleCatch) `shouldThrow` (\(e :: IOError) -> show e == "user error (error)")
    describe "tryAllErrors" $ do
      it "should return ExceptT error as Left" $
        runExceptT (tryAllErrors throwTestError) `shouldReturn` Right (Left (TestError "error"))
      it "should return SomeException as Left" $
        runExceptT (tryAllErrors throwTestException) `shouldReturn` Right (Left (TestException "user error (error)"))
      it "should return no errors as Right" $
        runExceptT (tryAllErrors noErrors) `shouldReturn` Right (Right "no errors")
    describe "catchAllErrors" $ do
      it "should catch ExceptT error" $
        runExceptT (throwTestError `catchAllErrors` handleCatch) `shouldReturn` Right "caught TestError \"error\""
      it "should catch SomeException" $
        runExceptT (throwTestException `catchAllErrors` handleCatch) `shouldReturn` Right "caught TestException \"user error (error)\""
      it "should not throw if there are no errors" $
        runExceptT (noErrors `catchAllErrors` throwError) `shouldReturn` Right "no errors"
    describe "catchThrow" $ do
      it "should re-throw ExceptT error" $
        runExceptT (throwTestError `catchThrow` fromSomeException) `shouldReturn` Left (TestError "error")
      it "should catch SomeException and throw as ExceptT error" $
        runExceptT (throwTestException `catchThrow` fromSomeException) `shouldReturn` Left (TestException "user error (error)")
      it "should not throw if there are no exceptions" $
        runExceptT (noErrors `catchThrow` fromSomeException) `shouldReturn` Right "no errors"
    describe "allFinally should run final action" $ do
      it "then throw ExceptT error" $ withFinal $ \final ->
        runExceptT (throwTestError `allFinally` final) `shouldReturn` Left (TestError "error")
      it "then throw SomeException as ExceptT error" $ withFinal $ \final ->
        runExceptT (throwTestException `allFinally` final) `shouldReturn` Left (TestException "user error (error)")
      it "and should not throw if there are no exceptions" $ withFinal $ \final ->
        runExceptT (noErrors `allFinally` final) `shouldReturn` Right "no errors"
    where
      throwTestError :: ExceptT TestError IO String
      throwTestError = throwError $ TestError "error"
      throwTestException :: ExceptT TestError IO String
      throwTestException = liftIO $ throwIO $ userError "error"
      noErrors :: ExceptT TestError IO String
      noErrors = pure "no errors"
      handleCatch :: TestError -> ExceptT TestError IO String
      handleCatch e = pure $ "caught " <> show e
      handleException :: SomeException -> ExceptT TestError IO String
      handleException e = pure $ "caught " <> show e
      withFinal :: (ExceptT TestError IO String -> IO ()) -> IO ()
      withFinal test = do
        r <- newIORef False
        let final = liftIO $ writeIORef r True >> pure "final"
        test final
        readIORef r `shouldReturn` True
  
  data TestError = TestError String | TestException String
    deriving (Eq, Show)
  
  instance Exception TestError
  
  instance AnyError TestError where
    fromSomeException = TestException . show
  