{-# LANGUAGE ScopedTypeVariables #-}
module Control.DebounceSpec (spec) where

import Control.Concurrent
import Control.Debounce
import qualified Control.Debounce.Internal as DI
import Control.Monad
import Control.Monad.Catch
import Control.Retry
import Data.IORef
import Test.HUnit.Lang
import Test.Hspec

spec :: Spec
spec = describe "mkDebounce" $ do
    describe "Leading edge" $ do
        it "works for a single event" $ do
            (ref, debounced, baton, returnFromWait) <- getDebounce leadingEdge

            debounced
            waitUntil 5 $ readIORef ref >>= (`shouldBe` 1)

            returnFromWait
            pause
            readIORef ref >>= (`shouldBe` 1)

            -- Try another round
            debounced
            waitUntil 5 $ readIORef ref >>= (`shouldBe` 2)

            returnFromWait
            pause
            readIORef ref >>= (`shouldBe` 2)

        it "works for multiple events" $ do
            (ref, debounced, baton, returnFromWait) <- getDebounce leadingEdge

            debounced
            waitForBatonToBeTaken baton
            debounced
            pause
            waitUntil 5 $ readIORef ref >>= (`shouldBe` 1)

            returnFromWait
            pause
            readIORef ref >>= (`shouldBe` 2)

    describe "Trailing edge" $ do
        it "works for a single event" $ do
            (ref, debounced, baton, returnFromWait) <- getDebounce trailingEdge

            debounced
            pause
            waitUntil 5 $ readIORef ref >>= (`shouldBe` 0)

            returnFromWait
            waitUntil 5 $ readIORef ref >>= (`shouldBe` 1)

            -- Try another round
            debounced
            pause
            waitUntil 5 $ readIORef ref >>= (`shouldBe` 1)

            returnFromWait
            waitUntil 5 $ readIORef ref >>= (`shouldBe` 2)

        it "works for multiple events" $ do
            (ref, debounced, baton, returnFromWait) <- getDebounce trailingEdge

            debounced
            waitForBatonToBeTaken baton
            debounced
            pause
            waitUntil 5 $ readIORef ref >>= (`shouldBe` 0)

            returnFromWait
            waitUntil 5 $ readIORef ref >>= (`shouldBe` 1)


-- | Make a controllable delay function
getWaitAction :: IO (p -> IO (), IO ())
getWaitAction = do
    waitVar <- newEmptyMVar
    let waitAction _ = takeMVar waitVar
    let returnFromWait = putMVar waitVar ()
    return (waitAction, returnFromWait)

-- | Get a debounce system with access to the internals for testing
getDebounce :: DI.DebounceEdge -> IO (IORef Int, IO (), MVar (), IO ())
getDebounce edge = do
  ref :: IORef Int <- newIORef 0
  let action = modifyIORef ref (+ 1)

  (waitAction, returnFromWait) <- getWaitAction

  baton <- newEmptyMVar

  debounced <- DI.mkDebounceInternal baton waitAction defaultDebounceSettings {
    debounceFreq = 5000000 -- unused
    , debounceAction = action
    , debounceEdge = edge
    }

  return (ref, debounced, baton, returnFromWait)

-- | Pause briefly (100ms)
pause :: IO ()
pause = threadDelay 100000

waitForBatonToBeTaken :: MVar () -> IO ()
waitForBatonToBeTaken baton = waitUntil 5 $ tryReadMVar baton >>= (`shouldBe` Nothing)

-- | Wait up to n seconds for an action to complete without throwing an HUnitFailure
waitUntil :: Int -> IO a -> IO ()
waitUntil n action = recovering policy [handler] (\_status -> void action)
  where policy = constantDelay 1000 `mappend` limitRetries (n * 1000) -- 1ms * n * 1000 tries = n seconds
        handler _status = Handler (\(HUnitFailure {}) -> return True)

main :: IO ()
main = hspec spec
