module TestUtils
    (
    withNewEndpoint,
    withNewEndpoint2,
    withNewEndpoint3,
    withNewEndpoint4,

    newTCPAddress,
    newUDPAddress,
    newTCPAddress6,
    newUDPAddress6,
    isIPv6Available,

    timeBound,
    troubleshoot,
    shareTransport

    )
  where

-- local imports

import Network.Endpoints
import Network.Transport.Sockets.TCP

-- external imports
import Control.Concurrent
import Control.Concurrent.Async
import Control.Concurrent.STM
import Control.Exception

import qualified Network.Socket as NS

import System.Log.Logger

import Test.HUnit

--------------------------------------------------------------------------------
--------------------------------------------------------------------------------

withNewEndpoint :: Transport -> (Endpoint -> IO ()) -> IO ()
withNewEndpoint transport fn = do
  endpoint <- newEndpoint
  withEndpoint transport endpoint $ fn endpoint

withNewEndpoint2 :: Transport -> (Endpoint -> Endpoint -> IO ()) -> IO ()
withNewEndpoint2 transport fn = do
  withNewEndpoint transport $ \endpoint1 ->
    withNewEndpoint transport $ \endpoint2 ->
      fn endpoint1 endpoint2

withNewEndpoint3 :: Transport -> (Endpoint -> Endpoint -> Endpoint -> IO ()) -> IO ()
withNewEndpoint3 transport fn = withNewEndpoint transport $ \endpoint1 ->
  withNewEndpoint2 transport $ \endpoint2 endpoint3 ->
    fn endpoint1 endpoint2 endpoint3

withNewEndpoint4 :: Transport -> (Endpoint -> Endpoint -> Endpoint -> Endpoint -> IO ()) -> IO ()
withNewEndpoint4 transport fn = withNewEndpoint2 transport $ \endpoint1 endpoint2 ->
  withNewEndpoint2 transport $ \endpoint3 endpoint4 ->
    fn endpoint1 endpoint2 endpoint3 endpoint4

newTCPAddress :: IO Name
newTCPAddress = do
  NS.SockAddrInet p _ <- availablePort NS.AF_INET NS.Stream
  return $ Name $ "localhost:" ++ show p

newUDPAddress :: IO Name
newUDPAddress = do
  NS.SockAddrInet p _ <- availablePort NS.AF_INET NS.Datagram
  return $ Name $ "localhost:" ++ show p

newTCPAddress6 :: IO Name
newTCPAddress6 = do
  NS.SockAddrInet6 p _ _ _ <- availablePort NS.AF_INET6 NS.Stream
  return $ Name $ "localhost:" ++ show p

newUDPAddress6 :: IO Name
newUDPAddress6 = do
  NS.SockAddrInet6 p _ _ _ <- availablePort NS.AF_INET6 NS.Datagram
  return $ Name $ "localhost:" ++ show p

availablePort :: NS.Family -> NS.SocketType -> IO NS.SockAddr
availablePort f t = do
  let hints = NS.defaultHints { NS.addrFamily = f
                              , NS.addrSocketType = t
                              , NS.addrFlags = [ NS.AI_PASSIVE ]
                              , NS.addrProtocol = NS.defaultProtocol }
  addrs <- NS.getAddrInfo (Just hints) Nothing (Just "0")
  let a = head addrs
  bracket
    (NS.socket (NS.addrFamily a) (NS.addrSocketType a) (NS.addrProtocol a))
    NS.close
    (\s -> do
       NS.bind s (NS.addrAddress a)
       addr <- NS.getSocketName s
       if isPrivileged addr
         then availablePort f t
         else return addr
    )

isIPv6Available :: IO Bool
isIPv6Available = do
    addresses <- tcpSocketResolver6 (Name "localhost:1")
    case addresses of
        [] -> return False
        _ -> return True

isPrivileged :: NS.SockAddr -> Bool
isPrivileged (NS.SockAddrInet p _) = p < 1025
isPrivileged (NS.SockAddrInet6 p _ _ _) = p < 1025
isPrivileged _ = False

timeBound :: Int -> IO () -> IO ()
timeBound delay action = do
    outcome <- race (threadDelay delay) action
    assertBool "Test should not block" $ outcome == Right ()

troubleshoot :: IO () -> IO ()
troubleshoot fn = do
    finally (do
        updateGlobalLogger rootLoggerName (setLevel INFO)
        fn) (updateGlobalLogger rootLoggerName (setLevel WARNING))

shareTransport :: IO Transport -> IO (IO Transport)
shareTransport transportFactory  = do
  transport <- transportFactory
  vTransport <- atomically $ newTVar transport
  return $ atomically $ readTVar vTransport
