module Main where

#if ! MIN_VERSION_base(4,6,0)
import Prelude hiding (catch)
#endif
import Control.Monad (forever)
import Control.Concurrent.MVar
  ( newEmptyMVar
  , putMVar
  , takeMVar
  , withMVar
  )
import qualified Network.Transport as NT (Transport)
import Network.Transport.TCP()
import Control.DeepSeq (NFData)
import Control.Distributed.Process
import Control.Distributed.Process.Node
import Control.Distributed.Process.Serializable()
import Control.Distributed.Process.Extras.Time
import Control.Distributed.Process.Extras.Timer
import Control.Distributed.Process.SysTest.Utils

import Test.Framework (Test, testGroup, defaultMain)
import Test.Framework.Providers.HUnit (testCase)
import Network.Transport.TCP
import qualified Network.Transport as NT

import GHC.Generics

instance NFData Ping where

testSendAfter :: TestResult Bool -> Process ()
testSendAfter result =
  let delay = seconds 1 in do
  sleep $ seconds 10
  pid <- getSelfPid
  _ <- sendAfter delay pid Ping
  hdInbox <- receiveTimeout (asTimeout (seconds 2)) [
                      match (\m@(Ping) -> return m)
                    ]
  case hdInbox of
      Just Ping -> stash result True
      Nothing   -> stash result False

testRunAfter :: TestResult Bool -> Process ()
testRunAfter result =
  let delay = seconds 2 in do

  parentPid <- getSelfPid
  _ <- spawnLocal $ do
    _ <- runAfter delay $ send parentPid Ping
    return ()

  msg <- expectTimeout ((asTimeout delay) * 4)
  case msg of
      Just Ping -> stash result True
      Nothing   -> stash result False
  return ()

testCancelTimer :: TestResult Bool -> Process ()
testCancelTimer result = do
  let delay = milliSeconds 50
  pid <- periodically delay noop
  ref <- monitor pid
  _ <- getProcessInfo pid

  cancelTimer pid

  _ <- receiveWait [
          match (\(ProcessMonitorNotification ref' pid' _) ->
                  stash result $ ref == ref' && pid == pid')
        ]

  return ()

testPeriodicSend :: TestResult Bool -> Process ()
testPeriodicSend result = do
  let delay = milliSeconds 100
  self <- getSelfPid
  ref <- ticker delay self
  listener 0 ref
  liftIO $ putMVar result True
  where listener :: Int -> TimerRef -> Process ()
        listener n tRef | n > 10    = cancelTimer tRef
                        | otherwise = waitOne >> listener (n + 1) tRef
        -- get a single tick, blocking indefinitely
        waitOne :: Process ()
        waitOne = do
            Tick <- expect
            return ()

testTimerReset :: TestResult Int -> Process ()
testTimerReset result = do
  let delay = seconds 10
  counter <- liftIO $ newEmptyMVar

  listenerPid <- spawnLocal $ do
      stash counter 0
      -- we continually listen for 'ticks' and increment counter for each
      forever $ do
        Tick <- expect
        liftIO $ withMVar counter (\n -> (return (n + 1)))

  -- this ticker will 'fire' every 10 seconds
  ref <- ticker delay listenerPid

  sleep $ seconds 2
  resetTimer ref

  -- at this point, the timer should be back to roughly a 5 second count down
  -- so our few remaining cycles no ticks ought to make it to the listener
  -- therefore we kill off the timer and the listener now and take the count
  cancelTimer ref
  kill listenerPid "stop!"

  -- how many 'ticks' did the listener observer? (hopefully none!)
  count <- liftIO $ takeMVar counter
  liftIO $ putMVar result count

testTimerFlush :: TestResult Bool -> Process ()
testTimerFlush result = do
  let delay = seconds 1
  self <- getSelfPid
  ref  <- ticker delay self

  -- sleep so we *should* have a message in our 'mailbox'
  sleep $ milliSeconds 2

  -- flush it out if it's there
  flushTimer ref Tick (Delay $ seconds 3)

  m <- expectTimeout 10
  case m of
      Nothing   -> stash result True
      Just Tick -> stash result False

testSleep :: TestResult Bool -> Process ()
testSleep r = do
  sleep $ seconds 20
  stash r True

--------------------------------------------------------------------------------
-- Utilities and Plumbing                                                     --
--------------------------------------------------------------------------------

tests :: LocalNode  -> [Test]
tests localNode = [
    testGroup "Timer Tests" [
        testCase "testSendAfter"
                 (delayedAssertion
                  "expected Ping within 1 second"
                  localNode True testSendAfter)
      , testCase "testRunAfter"
                 (delayedAssertion
                  "expecting run (which pings parent) within 2 seconds"
                  localNode True testRunAfter)
      , testCase "testCancelTimer"
                 (delayedAssertion
                  "expected cancelTimer to exit the timer process normally"
                  localNode True testCancelTimer)
      , testCase "testPeriodicSend"
                 (delayedAssertion
                  "expected ten Ticks to have been sent before exiting"
                  localNode True testPeriodicSend)
      , testCase "testTimerReset"
                 (delayedAssertion
                  "expected no Ticks to have been sent before resetting"
                  localNode 0 testTimerReset)
      , testCase "testTimerFlush"
                 (delayedAssertion
                  "expected all Ticks to have been flushed"
                  localNode True testTimerFlush)
      , testCase "testSleep"
                 (delayedAssertion
                  "why am I not seeing a delay!?"
                  localNode True testTimerFlush)
      ]
  ]

timerTests :: NT.Transport -> IO [Test]
timerTests transport = do
  localNode <- newLocalNode transport initRemoteTable
  let testData = tests localNode
  return testData

-- | Given a @builder@ function, make and run a test suite on a single transport
testMain :: (NT.Transport -> IO [Test]) -> IO ()
testMain builder = do
  Right (transport, _) <- createTransportExposeInternals
                                     "127.0.0.1" "0" defaultTCPParameters
  testData <- builder transport
  defaultMain testData

main :: IO ()
main = testMain $ timerTests
