{-# LANGUAGE MultiWayIf #-}
module HieDbRetry (tests) where

import           Control.Concurrent.Extra     (Var, modifyVar, newVar, readVar,
                                               withVar)
import           Control.Exception            (ErrorCall (ErrorCall), evaluate,
                                               throwIO, tryJust)
import           Control.Monad.IO.Class       (MonadIO (liftIO))
import           Data.Tuple.Extra             (dupe)
import qualified Database.SQLite.Simple       as SQLite
import           Development.IDE.Session      (retryOnException,
                                               retryOnSqliteBusy)
import qualified Development.IDE.Session      as Session
import           Development.IDE.Types.Logger (Recorder (Recorder, logger_),
                                               WithPriority (WithPriority, payload),
                                               cmapWithPrio)
import qualified System.Random                as Random
import           Test.Tasty                   (TestTree, testGroup)
import           Test.Tasty.HUnit             (assertFailure, testCase, (@?=))

data Log
  = LogSession Session.Log
  deriving Show

makeLogger :: Var [Log] -> Recorder (WithPriority Log)
makeLogger msgsVar =
  Recorder {
    logger_ = \WithPriority{ payload = msg } -> liftIO $ modifyVar msgsVar (\msgs -> pure (msg : msgs, ()))
  }

rng :: Random.StdGen
rng = Random.mkStdGen 0

retryOnSqliteBusyForTest :: Recorder (WithPriority Log) -> Int -> IO a -> IO a
retryOnSqliteBusyForTest recorder maxRetryCount = retryOnException isErrorBusy (cmapWithPrio LogSession recorder) 1 1 maxRetryCount rng

isErrorBusy :: SQLite.SQLError -> Maybe SQLite.SQLError
isErrorBusy e
  | SQLite.SQLError { sqlError = SQLite.ErrorBusy } <- e = Just e
  | otherwise = Nothing

errorBusy :: SQLite.SQLError
errorBusy = SQLite.SQLError{ sqlError = SQLite.ErrorBusy, sqlErrorDetails = "", sqlErrorContext = "" }

isErrorCall :: ErrorCall -> Maybe ErrorCall
isErrorCall e
  | ErrorCall _ <- e = Just e
  | otherwise = Nothing

tests :: TestTree
tests = testGroup "RetryHieDb"
  [ testCase "retryOnException throws exception after max retries" $ do
      logMsgsVar <- newVar []
      let logger = makeLogger logMsgsVar
      let maxRetryCount = 1

      result <- tryJust isErrorBusy (retryOnSqliteBusyForTest logger maxRetryCount (throwIO errorBusy))

      case result of
        Left exception -> do
          exception @?= errorBusy
          withVar logMsgsVar $ \logMsgs ->
            length logMsgs @?= 2
            -- uncomment if want to compare log msgs
            -- logMsgs @?= []
        Right _ -> assertFailure "Expected ErrorBusy exception"

   , testCase "retryOnException doesn't throw if given function doesn't throw" $ do
      let expected = 1 :: Int
      let maxRetryCount = 0

      actual <- retryOnSqliteBusyForTest mempty maxRetryCount (pure expected)

      actual @?= expected

   , testCase "retryOnException retries the number of times it should" $ do
      countVar <- newVar 0
      let maxRetryCount = 3
      let incrementThenThrow = modifyVar countVar (\count -> pure (dupe (count + 1))) >> throwIO errorBusy

      _ <- tryJust isErrorBusy (retryOnSqliteBusyForTest mempty maxRetryCount incrementThenThrow)

      withVar countVar $ \count ->
        count @?= maxRetryCount + 1

   , testCase "retryOnException doesn't retry if exception is not ErrorBusy" $ do
      countVar <- newVar (0 :: Int)
      let maxRetryCount = 1

      let throwThenIncrement = do
            count <- readVar countVar
            if count == 0 then
              evaluate (error "dummy exception")
            else
              modifyVar countVar (\count -> pure (dupe (count + 1)))


      _ <- tryJust isErrorCall (retryOnSqliteBusyForTest mempty maxRetryCount throwThenIncrement)

      withVar countVar $ \count ->
        count @?= 0

   , testCase "retryOnSqliteBusy retries on ErrorBusy" $ do
      countVar <- newVar (0 :: Int)

      let incrementThenThrowThenIncrement = do
            count <- readVar countVar
            if count == 0 then
              modifyVar countVar (\count -> pure (dupe (count + 1))) >> throwIO errorBusy
            else
              modifyVar countVar (\count -> pure (dupe (count + 1)))

      _ <- retryOnSqliteBusy mempty rng incrementThenThrowThenIncrement

      withVar countVar $ \count ->
        count @?= 2

    , testCase "retryOnException exponentially backs off" $ do
       logMsgsVar <- newVar ([] :: [Log])

       let maxDelay = 100
       let baseDelay = 1
       let maxRetryCount = 6
       let logger = makeLogger logMsgsVar

       result <- tryJust isErrorBusy (retryOnException isErrorBusy (cmapWithPrio LogSession logger) maxDelay baseDelay maxRetryCount rng (throwIO errorBusy))

       case result of
         Left _ -> do
           withVar logMsgsVar $ \logMsgs ->
             -- uses log messages to check backoff...
             if | (LogSession (Session.LogHieDbRetriesExhausted baseDelay maximumDelay maxRetryCount _) : _) <- logMsgs -> do
                  baseDelay @?= 64
                  maximumDelay @?= 100
                  maxRetryCount @?= 0
                | otherwise -> assertFailure "Expected more than 0 log messages"
         Right _ -> assertFailure "Expected ErrorBusy exception"
  ]
