{-# LANGUAGE LambdaCase          #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}
module CalculatorExample (tests) where

import Data.Word
import Test.Hspec

import Control.Concurrent       (threadDelay)
import Control.Concurrent.Async (race_)
import Control.Exception.Safe   (throwIO, try)
import Data.Default             (def)
import Data.Foldable            (for_)
import Data.String              (fromString)
import Network.Simple.TCP       (connect, serve)
import System.Environment       (getEnv)
import System.Exit              (ExitCode(ExitSuccess))
import System.IO.Error          (isDoesNotExistError)
import System.Process           (callProcess, readProcessWithExitCode)

import Capnp.Rpc.Transport  (socketTransport)
import Capnp.Rpc.Untyped    (ConnConfig(..), handleConn)
import Capnp.TraversalLimit (defaultLimit)

import qualified Examples.Rpc.CalculatorClient
import qualified Examples.Rpc.CalculatorServer

getExe :: String -> IO (Maybe FilePath)
getExe varName =
    try (getEnv varName) >>= \case
        Left e
            | isDoesNotExistError e -> do
                putStrLn $ varName ++ " not set; skipping."
                pure Nothing
            | otherwise ->
                throwIO e
        Right path ->
            pure (Just path)

tests :: Spec
tests = describe "Check our example against the C++ implementation" $ do
    clientPath <- runIO $ getExe "CXX_CALCULATOR_CLIENT"
    serverPath <- runIO $ getExe "CXX_CALCULATOR_SERVER"
    for_ clientPath $ \clientPath ->
        it "Should pass when run against our server" $
            Examples.Rpc.CalculatorServer.main
                `race_` (waitForServer >> cxxClient clientPath 4000)
    for_ serverPath $ \serverPath ->
        it "Should pass when run against our client" $
            cxxServer serverPath 4000
                `race_` (waitForServer >> Examples.Rpc.CalculatorClient.main)
    for_ ((,) <$> clientPath <*> serverPath) $ \(clientPath, serverPath) ->
        it "Should pass when run aginst the C++ server, proxied through us." $
            cxxServer serverPath 4000
                `race_` (waitForServer >> runProxy 4000 6000)
                -- we wait twice, so that the proxy also has time to start:
                `race_` (waitForServer >> waitForServer >> cxxClient clientPath 6000)
  where
    -- | Give the server a bit of time to start up.
    waitForServer :: IO ()
    waitForServer = threadDelay 100000

    cxxServer :: FilePath -> Word16 -> IO ()
    cxxServer path port =
        callProcess path ["localhost:" ++ show port]
    cxxClient :: FilePath -> Word16 -> IO ()
    cxxClient path port = do
        (eStatus, out, err) <- readProcessWithExitCode path ["localhost:" ++ show port] ""
        (eStatus, out, err)
            `shouldBe`
            ( ExitSuccess
            , unlines
                [ "Evaluating a literal... PASS"
                , "Using add and subtract... PASS"
                , "Pipelining eval() calls... PASS"
                , "Defining functions... PASS"
                , "Using a callback... PASS"
                ]
            , ""
            )

-- | @'runProxy' serverPort clientPort@ connects to the server listening at
-- localhost:serverPort, requests its bootstrap interface, and then listens
-- on clientPort, offering a proxy of the server's bootstrap interface as our
-- own.
runProxy :: Word16 -> Word16 -> IO ()
runProxy serverPort clientPort =
    connect "localhost" (fromString $ show serverPort) $ \(serverSock, _addr) ->
        handleConn (socketTransport serverSock defaultLimit) def
            { debugMode = True
            , withBootstrap = Just $ \_sup client ->
                serve "localhost" (fromString $ show clientPort) $ \(clientSock, _addr) ->
                    handleConn (socketTransport clientSock defaultLimit) def
                        { getBootstrap = \_sup -> pure $ Just client
                        , debugMode = True
                        }
            }
