{-# LANGUAGE ExplicitNamespaces #-}
{-# LANGUAGE LambdaCase #-}

module Test.Method.MonitorSpec where

import Control.Method (TupleLike (fromTuple))
import RIO (newIORef, readIORef, throwString, void, writeIORef)
import Test.Hspec
  ( Spec,
    anyException,
    before,
    describe,
    it,
    shouldBe,
    shouldReturn,
    shouldSatisfy,
    shouldThrow,
  )
import Test.Method.Matcher (ArgsMatcher (args), anything)
import Test.Method.Monitor
  ( call,
    listenEventLog,
    newMonitor,
    times,
    watch,
    watchBy,
    withMonitor_,
  )
import Test.Method.Monitor.Internal
  ( Event (Enter, Leave),
    Monitor (monitorClock),
    Tick (Tick),
    tick,
  )

spec :: Spec
spec = do
  describe "newClock" $ do
    describe "newMonitor" $ do
      it "has empty trace" $ do
        m <- newMonitor
        length <$> listenEventLog m `shouldReturn` 0
      it "has clock initialized with zero" $ do
        m <- newMonitor
        readIORef (monitorClock m) `shouldReturn` Tick 0

    describe "tick" $ do
      it "increments monitor's clock" $ do
        m <- newMonitor
        t <- tick m
        t `shouldBe` Tick 0
        t1 <- tick m
        t1 `shouldBe` Tick 1

    describe "watch'" $ do
      let setup = do
            m <- newMonitor
            let method = watch m method'
            pure (m, method)
          method' :: Int -> IO String
          method' n
            | n == 42 = throwString "error"
            | otherwise = pure $ show n

      before setup $ do
        it "logs single method call" $ \(m, method) -> do
          void $ method 1
          listenEventLog m
            `shouldReturn` [ Enter (Tick 0) (fromTuple 1),
                             Leave (Tick 1) (Tick 0) (Right "1")
                           ]
        it "logs exception thrown" $ \(m, method) -> do
          method 42 `shouldThrow` anyException
          logs <- listenEventLog m
          shouldSatisfy (logs !! 1) $ \case
            Leave (Tick 1) (Tick 0) (Left _) -> True
            _ -> False
    describe "watch" $ do
      let setup = do
            m <- newMonitor
            r <- newIORef (0 :: Int)
            let get :: IO Int
                get = watchBy Left Left m (readIORef r)
                put :: Int -> IO ()
                put = watchBy Right Right m (writeIORef r)
            pure (m, get, put)
      before setup $ do
        it "logs two method call" $ \(m, get, put) -> do
          put 10
          _ <- get
          _ <- get
          listenEventLog m
            `shouldReturn` [ Enter (Tick 0) (Right (fromTuple 10)),
                             Leave (Tick 1) (Tick 0) (Right (Right ())),
                             Enter (Tick 2) (Left (fromTuple ())),
                             Leave (Tick 3) (Tick 2) (Right (Left 10)),
                             Enter (Tick 4) (Left (fromTuple ())),
                             Leave (Tick 5) (Tick 4) (Right (Left 10))
                           ]
    describe "times" $ do
      let setup = withMonitor_ $ \m -> do
            let method :: String -> IO ()
                method = watch m (const $ pure ())
            method "hoge"
            method "piyo"

      before setup $ do
        it "should call method twice" $ \logs -> do
          logs `shouldSatisfy` ((== 2) `times` call anything)

        it "should not call method with \"fuga\"" $ \logs -> do
          logs `shouldSatisfy` ((== 0) `times` call (args (== "fuga")))

        it "should call method with \"hoge\" at least once" $ \logs -> do
          logs `shouldSatisfy` ((>= 1) `times` call (args (== "hoge")))