module BracketSpec where

import Control.Monad
import Polysemy
import Polysemy.Error
import Polysemy.Output
import Polysemy.Resource
import Polysemy.State
import Polysemy.Trace
import Test.Hspec
import Unsafe.Coerce



spec :: Spec
spec = parallel $ do
  testAllThree "persist state and call the finalizer"
      (\(ts, (s, e)) -> do
        s `shouldBe` "finalized"
        e `shouldBe` Left ()
        ts `shouldBe` ["allocated", "starting block"]
      ) $ do
    bracket
      (put "allocated" >> pure ())
      (\() -> do
        get >>= trace
        put "finalized"
      )
      (\() -> do
        get >>= trace
        put "starting block"
        _ <- throw ()
        put "don't get here"
      )

  testAllThree "persist state and call the finalizer with bracketOnError"
      (\(ts, (s, e)) -> do
        ts `shouldContain` ["allocated"]
        ts `shouldContain` ["starting block"]
        s `shouldBe` "finalized"
        e `shouldBe` Left ()
      ) $ do
    bracketOnError
      (put "allocated" >> pure ())
      (\() -> do
        get >>= trace
        put "finalized"
      )
      (\() -> do
        get >>= trace
        put "starting block"
        _ <- throw ()
        put "don't get here"
      )

  testAllThree "should not call the finalizer if there no error"
      (\(ts, (s, e)) -> do
        ts `shouldContain` ["allocated"]
        ts `shouldNotContain` ["starting block"]
        s `shouldBe` "don't get here"
        e `shouldBe` Right ()
      ) $ do
    bracketOnError
      (put "allocated" >> pure ())
      (\() -> do
        get >>= trace
        put "finalized"
      )
      (\() -> do
        get >>= trace
        put "starting block"
        put "don't get here"
      )

  testAllThree "should call the finalizer on Error"
      (\(ts, (s, e)) -> do
        ts `shouldContain` ["beginning transaction"]
        ts `shouldContain` ["rolling back transaction"]
        s `shouldBe` ""
        e `shouldBe` Left ()
      ) $ do
    withTransaction $ do
      void $ throw ()
      pure "hello"

  testTheIOTwo "io dispatched bracket"
      (\(ts, (s, e)) -> do
        ts `shouldContain` ["allocated"]
        ts `shouldContain` ["starting block"]
        s `shouldBe` "finalized"
        e `shouldBe` Left ()
      ) $ do
    bracket
      (put "allocated" >> pure ())
      (\() -> do
        get >>= trace
        put "finalized"
      )
      (\() -> do
        get >>= trace
        put "starting block"
        _ <- throw ()
        put "don't get here"
      )

  testTheIOTwo "should not lock when done recursively"
      (\(ts, (s, e)) -> do
        ts `shouldContain` [ "hello 1"
                           , "hello 2"
                           , "RUNNING"
                           , "goodbye 2"
                           ]
        s `shouldBe` "finished"
        e `shouldBe` Left ()
      ) $ do
    bracket
      (put "hello 1")
      (\() -> do
        get >>= trace
        put "finished"
      )
      (\() -> do
        get >>= trace
        void $
          bracket (put "hello 2")
                  (const $ do
                    get >>= trace
                    put "goodbye 2"
                  )
                  (const $ do
                    get >>= trace
                    put "RUNNING"
                    throw ()
                  )
        -- This doesn't run due to the thrown error above
        get >>= trace
        put "goodbye 1"
      )


------------------------------------------------------------------------------


runTest
  :: Sem '[Error (), Resource, State [Char], Trace] a
  -> IO ([String], ([Char], Either () a))
runTest = pure
        . run
        . runTraceList
        . runState ""
        . runResource
        . runError @()

runTest2
  :: Sem '[Error (), Resource, State [Char], Trace, Output String, Embed IO] a
  -> IO ([String], ([Char], Either () a))
runTest2 = runM
         . ignoreOutput
         . runTraceList
         . runState ""
         . resourceToIO
         . runError @()

runTest3
  :: Sem '[Error (), Resource, State [Char], Trace, Output String, Embed IO, Final IO] a
  -> IO ([String], ([Char], Either () a))
runTest3 = runFinal
         . embedToFinal
         . outputToIOMonoid (:[])
         . traceToOutput
         . stateToIO ""
         . resourceToIOFinal
         . runError @()


testAllThree
    :: String
    -> (([String], ([Char], Either () a)) -> Expectation)
    -> (Sem '[Error (), Resource, State [Char], Trace] a)
    -> Spec
testAllThree name k m = do
  describe name $ do
    it "via runResource" $ do
      z <- runTest m
      k z
    -- NOTE(sandy): These unsafeCoerces are safe, because we're just weakening
    -- the end of the union
    it "via resourceToIO" $ do
      z <- runTest2 $ unsafeCoerce m
      k z
    it "via resourceToIOFinal" $ do
      z <- runTest3 $ unsafeCoerce m
      k z


testTheIOTwo
    :: String
    -> (([String], ([Char], Either () a)) -> Expectation)
    -> (Sem '[Error (), Resource, State [Char], Trace, Output String, Embed IO] a)
    -> Spec
testTheIOTwo name k m = do
  describe name $ do
    it "via resourceToIO" $ do
      z <- runTest2 m
      k z
    -- NOTE(sandy): This unsafeCoerces are safe, because we're just weakening
    -- the end of the union
    it "via resourceToIOFinal" $ do
      z <- runTest3 $ unsafeCoerce m
      k z


withTransaction :: (Member Resource r, Member Trace r) => Sem r a -> Sem r a
withTransaction m =
  bracketOnError
    (trace "beginning transaction")
    (const $ trace "rolling back transaction")
    (const $ m <* trace "committing transaction")