{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE CPP #-}
module UnliftIO.AsyncSpec (spec) where

import Test.Hspec
import Test.Hspec.QuickCheck
import Test.QuickCheck
import UnliftIO
import UnliftIO.Internals.Async
import Data.List (nub)
import Control.Applicative
import Control.Concurrent (myThreadId, threadDelay)
import qualified Control.Exception as CE (ErrorCall(..), try)
import GHC.Conc.Sync (ThreadStatus(..), threadStatus)
import Control.Concurrent.STM (throwSTM)
import Control.Exception (getMaskingState, MaskingState (Unmasked))

data MyExc = MyExc
  deriving (Show, Eq, Typeable)
instance Exception MyExc

spec :: Spec
spec = do
  describe "replicateConcurrently_" $ do
    prop "works" $ \(NonNegative cnt) -> do
      ref <- newIORef (0 :: Int)
      replicateConcurrently_ cnt $ atomicModifyIORef' ref $ \i -> (i + 1, ())
      readIORef ref `shouldReturn` cnt

    it "uses a different thread per replicated action" $
      forAllShrink ((+ 1) . abs <$> arbitrary) (filter (>= 1) . shrink) $ \n -> do
        threadIdsRef <- newIORef []
        let action = myThreadId >>= \tid -> atomicModifyIORef' threadIdsRef (\acc -> (tid:acc, ()))
        replicateConcurrently_ n action
        tids <- readIORef threadIdsRef
        tids `shouldBe` (nub tids)

#if MIN_VERSION_base(4,8,0)
  describe "flatten" $ do
    -- NOTE: cannot make this test a property test given
    -- Flat and Conc cannot have an Eq property
    it "flattens all alternative trees" $ do
      let
        concValue :: Conc IO Int
        concValue =
            conc (pure 1) <|> conc (pure 2) <|> pure 3
            -- Alt (Alt (Action (pure 1)) (Action (pure 2)))
            --     (Pure 3)
      flatConc <- flatten concValue
      case flatConc of
        FlatAlt (FlatAction action1)
                (FlatAction action2)
                [(FlatPure 3)] -> do
          action1 `shouldReturn` 1
          action2 `shouldReturn` 2
        _ -> expectationFailure "expecting flatten to work but didn't"

  describe "conc" $ do
    it "handles sync exceptions" $ do
      runConc (conc (pure ()) *> conc (throwIO MyExc))
        `shouldThrow` (== MyExc)

    it "handles async exceptions" $ do
      tidVar <- newEmptyMVar
      result <- CE.try $ runConc (conc (pure ())
                               *> conc (takeMVar tidVar >>= (`throwTo` (CE.ErrorCall "having error")))
                               *> conc (myThreadId
                                        >>= putMVar tidVar
                                        >> threadDelay 1000100))
      case result of
        Right _ ->
          expectationFailure "Expecting an error, got none"
        Left (SomeAsyncException err) ->
          displayException err `shouldBe` "having error"

    it "has an Unmasked masking state for given subroutines" $
      uninterruptibleMask_ $
        runConc $ conc (threadDelay maxBound) <|>
          conc (getMaskingState `shouldReturn` Unmasked)

-- NOTE: Older versions of GHC have a timeout function that doesn't
-- work on Windows
#if !WINDOWS
    it "allows to kill parent via timeout" $ do
      ref <- newIORef (0 :: Int)
      mres <- timeout 20 $ runConc $
        conc (pure ()) *>
        conc ((writeIORef ref 1 >> threadDelay maxBound >> writeIORef ref 2)
              `finally` writeIORef ref 3)
      mres `shouldBe` Nothing
      res <- readIORef ref
      case res of
        0 -> putStrLn "make timeout longer"
        1 -> error "it's 1"
        2 -> error "it's 2"
        3 -> pure ()
        _ -> error $ "what? " ++ show res
#endif

    it "throws right exception on empty" $
      runConc empty `shouldThrow` (== EmptyWithNoAlternative)

  describe "Conc Applicative instance" $ do
    prop "doesn't fork a new thread on a pure call" $ \i ->
      runConc (pure (i :: Int)) `shouldReturn` i

    it "evaluates all needed sub-routines " $ do
      runConc (conc (pure ()) *> conc (throwIO MyExc))
        `shouldThrow` (== MyExc)

    it "cleanup on brackets work" $ do
      var <- newTVarIO (0 :: Int)
      let worker = conc $ bracket_
            (atomically $ modifyTVar' var (+ 1))
            (atomically $ modifyTVar' var (subtract 1))
            (threadDelay 10000000 >> error "this should never happen")
          count = 10
          killer = conc $ atomically $ do
            count' <- readTVar var
            checkSTM $ count == count'
            throwSTM MyExc
          composed = foldr (*>) killer (replicate count worker)
      runConc composed `shouldThrow` (== MyExc)
      atomically (readTVar var) `shouldReturn` 0

    it "re-throws exception that happened first" $ do
      let composed = conc (throwIO MyExc) *> conc (threadDelay 1000000 >> error "foo")
      runConc composed `shouldThrow` (== MyExc)

  describe "Conc Alternative instance" $ do
    it "is left associative" $ do
      let
        concValue :: Conc IO Int
        concValue =
            conc (pure 1) <|> conc (pure 2) <|> conc (pure 3)
      case concValue of
        Alt (Alt (Action action1) (Action action2)) (Action action3) -> do
          action1 `shouldReturn` 1
          action2 `shouldReturn` 2
          action3 `shouldReturn` 3

        _ -> expectationFailure "expecting Conc Alternative to be left associative, but it wasn't"

    it "executes body of all alternative blocks" $ do
      var <- newEmptyMVar
      runConc $
        conc (takeMVar var) <|>
        conc (threadDelay maxBound) <|>
        conc (threadDelay 100 >> pure ())
      -- if a GC runs at the right time, it's possible that both `takeMVar` and
      -- `runConc` itself will be in a "blocked indefinitely on MVar" situation,
      -- adding line bellow to avoid that
      putMVar var ()

    it "finishes all threads that didn't finish first" $ do
      ref <- newIORef []
      runConc $
        conc (do tid <- myThreadId
                 atomicModifyIORef' ref (\acc -> (tid:acc, ()))
                 -- it is never going to finish
                 threadDelay maxBound) <|>
        conc (do tid <- myThreadId
                 -- it finishes after registering thread id
                 atomicModifyIORef' ref (\acc -> (tid:acc, ()))
                 threadDelay 500) <|>
        conc (do tid <- myThreadId
                 atomicModifyIORef' ref (\acc -> (tid:acc, ()))
                 -- it is never going to finish
                 threadDelay maxBound)
      threads <- readIORef ref
      statusList <- mapM threadStatus threads
      length (filter (== ThreadFinished) statusList) `shouldBe` 3

    it "nesting works" $ do
      var <- newEmptyMVar
      let sillyAlts :: Conc IO a -> Conc IO a
          sillyAlts c = c <|> conc (takeMVar var >> error "shouldn't happen")
      res <- runConc $ sillyAlts $ (+)
        <$> sillyAlts (conc (pure 1))
        <*> sillyAlts (conc (pure 2))
      res `shouldBe` 3
      putMVar var ()

#endif
