{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE Rank2Types #-}
module Network.JsonRpc.Tests (tests) where

import Control.Concurrent
import Control.Concurrent.Async
import Control.Concurrent.STM
import Control.Exception hiding (assert)
import Control.Monad
import Data.Aeson.Types
import Data.Conduit
import qualified Data.Conduit.List as CL
import Data.List
import Data.Conduit.Network
import Data.Conduit.TMChan
import qualified Data.HashMap.Strict as M
import Data.Maybe
import Data.Text (Text)
import Network.JsonRpc
import Network.JsonRpc.Arbitrary
import Test.QuickCheck
import Test.QuickCheck.Monadic
import Test.Framework
import Test.Framework.Providers.QuickCheck2

tests :: [Test]
tests =
    [ testGroup "JSON-RPC Requests"
        [ testProperty "Check fields"
            (reqFields :: Request Value -> Bool)
        , testProperty "Encode/decode"
            (reqDecode :: Request Value -> Bool)
        ]
    , testGroup "JSON-RPC Notifications"
        [ testProperty "Check fields"
            (notifFields :: Notif Value -> Bool)
        , testProperty "Encode/decode"
            (notifDecode :: Notif Value -> Bool)
        ]
    , testGroup "JSON-RPC Responses"
        [ testProperty "Check fields"
            (resFields :: Response Value -> Bool)
        , testProperty "Encode/decode"
            (resDecode :: ReqRes Value Value -> Bool)
        , testProperty "Bad response id"
            (rpcBadResId :: ReqRes Value Value -> Bool)
        , testProperty "Error response"
            (rpcErrRes :: (ReqRes Value Value, ErrorObj) -> Bool)
        ]
    , testGroup "JSON-RPC Conduits"
        [ testProperty "Outgoing conduit"
            (newMsgConduit :: [Message Value Value Value] -> Property)
        , testProperty "Decode requests"
            (decodeReqConduit :: ([Request Value], Ver) -> Property)
        , testProperty "Decode responses" 
            (decodeResConduit :: ([ReqRes Value Value], Ver) -> Property)
        , testProperty "Bad responses" 
            (decodeErrConduit :: ([ReqRes Value Value], Ver) -> Property)
        , testProperty "Sending messages" sendMsgNet
        , testProperty "Two-way communication" twoWayNet
        , testProperty "Real network communication" realNet
        ]
    ]

--
-- Requests
--

reqFields :: (ToRequest a, ToJSON a) => Request a -> Bool
reqFields rq = case rq of
    Request V1 m p i -> r1ks && vals m p i
    Request V2 m p i -> r2ks && vals m p i
  where
    (Object o) = toJSON rq
    r1ks = sort (M.keys o) == ["id", "method", "params"]
    r2ks = sort (M.keys o) == ["id", "jsonrpc", "method", "params"]
        || sort (M.keys o) == ["id", "jsonrpc", "method"]
    vals m p i = fromMaybe False $ parseMaybe (f m p i) o
    f m p i _ = do
        j <- o .:? "jsonrpc"
        guard $ fromMaybe True $ fmap (== ("2.0" :: Text)) j
        i' <- o .: "id"
        guard $ i == i'
        m' <- o .: "method"
        guard $ m == m'
        p' <- o .:? "params" .!= Null
        guard $ (toJSON p) == p'
        return True

reqDecode :: (Eq a, ToRequest a, ToJSON a, FromRequest a) => Request a -> Bool
reqDecode rq = case parseMaybe parseRequest (toJSON rq) of
    Nothing  -> False
    Just rqE -> either (const False) (rq ==) rqE

--
-- Notifications
--

notifFields :: (ToNotif a, ToJSON a) => Notif a -> Bool
notifFields rn = case rn of
    Notif V1 m p -> n1ks && vals m p
    Notif V2 m p -> n2ks && vals m p
  where
    (Object o) = toJSON rn
    n1ks = sort (M.keys o) == ["id", "method", "params"]
    n2ks = sort (M.keys o) == ["jsonrpc", "method", "params"]
        || sort (M.keys o) == ["jsonrpc", "method"]
    vals m p = fromMaybe False $ parseMaybe (f m p) o
    f m p _ = do
        i <- o .:? "id" .!= Null
        guard $ i == Null
        j <- o .:? "jsonrpc"
        guard $ fromMaybe True $ fmap (== ("2.0" :: Text)) j
        m' <- o .: "method"
        guard $ m == m'
        p' <- o .:? "params" .!= Null
        guard $ (toJSON p) == p'
        return True

notifDecode :: (Eq a, ToNotif a, ToJSON a, FromNotif a)
            => Notif a -> Bool
notifDecode rn = case parseMaybe parseNotif (toJSON rn) of
    Nothing  -> False
    Just rnE -> either (const False) (rn ==) rnE

--
-- Responses
--

resFields :: (Eq a, ToJSON a, FromJSON a) => Response a -> Bool
resFields rs = case rs of
    Response V1 s i -> s1ks && vals s i
    Response V2 s i -> s2ks && vals s i
  where
    (Object o) = toJSON rs
    s1ks = sort (M.keys o) == ["error", "id", "result"]
    s2ks = sort (M.keys o) == ["id", "jsonrpc", "result"]
    vals s i = fromMaybe False $ parseMaybe (f s i) o
    f s i _ = do
        i' <- o .: "id"
        guard $ i == i'
        j <- o .:? "jsonrpc"
        guard $ fromMaybe True $ fmap (== ("2.0" :: Text)) j
        s' <- o .: "result"
        guard $ s == s'
        e <- o .:? "error" .!= Null
        guard $ e == Null
        return True

resDecode :: (Eq r, ToJSON r, FromResponse r)
          => ReqRes q r -> Bool
resDecode (ReqRes rq rs) = case parseMaybe (parseResponse rq) (toJSON rs) of
    Nothing -> False
    Just rsE -> either (const False) (rs ==) rsE

rpcBadResId :: forall q r. (ToJSON r, FromResponse r)
            => ReqRes q r -> Bool
rpcBadResId (ReqRes rq rs) = case parseMaybe f (toJSON rs') of
    Nothing -> True
    _ -> False
  where
    f :: FromResponse r => Value -> Parser (Either ErrorObj (Response r))
    f = parseResponse rq
    rs' = rs { getResId = IdNull }

rpcErrRes :: forall q r. FromResponse r => (ReqRes q r, ErrorObj) -> Bool
rpcErrRes (ReqRes rq _, re) = case parseMaybe f (toJSON re') of
    Nothing -> False
    Just (Left _) -> True
    _ -> False
  where
    f :: FromResponse r => Value -> Parser (Either ErrorObj (Response r))
    f = parseResponse rq
    re' = re { getErrId = getReqId rq }

--
-- Conduit
--

newMsgConduit :: ( ToRequest q, ToJSON q, ToNotif n, ToJSON n
                 , ToJSON r, FromResponse r )
              => [Message q n r] -> Property
newMsgConduit (snds) = monadicIO $ do
    msgs <- run $ do
        qs <- atomically initSession
        CL.sourceList snds' $= msgConduit False qs $$ CL.consume
    assert $ length msgs == length snds'
    assert $ length (filter rqs msgs) == length (filter rqs snds')
    assert $ map idn (filter rqs msgs) == take (length (filter rqs msgs)) [1..]
  where
    rqs (MsgRequest _) = True
    rqs _ = False
    idn (MsgRequest rq) = getIdInt $ getReqId rq
    idn _ = error "Unexpected request"
    snds' = flip map snds $ \m -> case m of
        (MsgRequest rq) -> MsgRequest $ rq { getReqId = IdNull }
        _ -> m

decodeReqConduit :: forall q. (ToRequest q, FromRequest q, Eq q, ToJSON q)
                 => ([Request q], Ver) -> Property
decodeReqConduit (vs, ver) = monadicIO $ do
    inmsgs <- run $ do
        qs  <- atomically initSession
        qs' <- atomically initSession
        CL.sourceList vs
            $= CL.map f
            $= msgConduit False qs
            $= encodeConduit
            $= decodeConduit ver False qs'
            $$ CL.consume
    assert $ null $ filter unexpected inmsgs
    assert $ all (uncurry match) (zip vs inmsgs)
  where
    unexpected :: IncomingMsg () q () () -> Bool
    unexpected (IncomingMsg (MsgRequest _) Nothing) = False
    unexpected _ = True
    match rq (IncomingMsg (MsgRequest rq') _) =
        rq { getReqId = getReqId rq' } == rq'
    match _ _ = False
    f rq = MsgRequest $ rq { getReqId = IdNull } :: Message q () ()

decodeResConduit :: forall q r.
                    ( ToRequest q, FromRequest q, Eq q, ToJSON q, ToJSON r
                    , FromResponse r, Eq r )
                 => ([ReqRes q r], Ver) -> Property
decodeResConduit (rr, ver) = monadicIO $ do
    inmsgs <- run $ do
        qs  <- atomically initSession
        qs' <- atomically initSession
        CL.sourceList vs
            $= CL.map f
            $= msgConduit False qs
            $= encodeConduit
            $= decodeConduit ver False qs'
            $= CL.map respond
            $= encodeConduit
            $= decodeConduit ver False qs
            $$ CL.consume
    assert $ null $ filter unexpected inmsgs
    assert $ all (uncurry match) (zip vs inmsgs)
  where
    unexpected :: IncomingMsg q () () r -> Bool
    unexpected (IncomingMsg (MsgResponse _) (Just _)) = False
    unexpected _ = True

    match rq (IncomingMsg (MsgResponse rs) (Just rq')) =
        rq { getReqId = getReqId rq' } == rq'
            && rs == g rq'
    match _ _ = False

    respond :: IncomingMsg () q () () -> Response r
    respond (IncomingMsg (MsgRequest rq) Nothing) = g rq
    respond _ = undefined

    f rq = MsgRequest $ rq { getReqId = IdNull } :: Message q () ()
    vs = map (\(ReqRes rq _) -> rq) rr

    g rq = let (ReqRes _ rs) = fromJust $ find h rr
               h (ReqRes rq' _) = getReqParams rq == getReqParams rq'
           in  rs { getResId = getReqId rq }

decodeErrConduit :: forall q r.
                    ( ToRequest q, FromRequest q, Eq q, ToJSON q, ToJSON r
                    , FromResponse r, Eq r, Show r, Show q )
                 => ([ReqRes q r], Ver) -> Property
decodeErrConduit (vs, ver) = monadicIO $ do
    inmsgs <- run $ do
        qs  <- atomically initSession
        qs' <- atomically initSession
        CL.sourceList vs
            $= CL.map f
            $= msgConduit False qs
            $= encodeConduit
            $= decodeConduit ver False qs'
            $= CL.map respond
            $= encodeConduit
            $= decodeConduit ver False qs
            $$ CL.consume
    assert $ null $ filter unexpected inmsgs
    assert $ all (uncurry match) (zip vs inmsgs)
  where
    unexpected :: IncomingMsg q () () r -> Bool
    unexpected (IncomingMsg (MsgError _) (Just _)) = False
    -- unexpected _ = True
    unexpected i = error $ show i

    match (ReqRes rq _) (IncomingMsg (MsgError _) (Just rq')) =
        rq' { getReqId = getReqId rq } == rq
    match _ _ = False

    respond :: IncomingMsg () q () () -> ErrorObj
    respond (IncomingMsg (MsgRequest (Request ver' _ _ i)) Nothing) =
        ErrorObj ver' "test" (getIdInt i) Null i
    respond _ = undefined

    f (ReqRes rq _) = MsgRequest $ rq { getReqId = IdNull } :: Message q () ()

type ClientAppConduits = AppConduits Value Value Value () () () IO
type ServerAppConduits = AppConduits () () () Value Value Value IO

sendMsgNet :: ([Message Value Value Value], Ver) -> Property
sendMsgNet (rs, ver) = monadicIO $ do
    rt <- run $ do
        mv <- newEmptyMVar
        to <- atomically $ newTBMChan 128
        ti <- atomically $ newTBMChan 128
        let tiSink   = sinkTBMChan ti True
            toSource = sourceTBMChan to
            toSink   = sinkTBMChan to True
            tiSource = sourceTBMChan ti
        withAsync (srv tiSink toSource mv) $ \_ ->
            runConduits ver False toSink tiSource (cliApp mv)
    assert $ length rt == length rs
    assert $ all (uncurry match) (zip rs rt)
  where
    srv tiSink toSource mv = runConduits ver False tiSink toSource (srvApp mv)

    srvApp :: MVar [IncomingMsg () Value Value Value]
           -> ServerAppConduits -> IO ()
    srvApp mv (src, snk) =
        (CL.sourceNull $$ snk) >> (src $$ CL.consume) >>= putMVar mv

    cliApp :: MVar [IncomingMsg () Value Value Value]
           -> ClientAppConduits -> IO [IncomingMsg () Value Value Value]
    cliApp mv (src, snk) =
        (CL.sourceList rs $$ snk) >> (src $$ CL.sinkNull) >> readMVar mv

    match (MsgRequest rq) (IncomingMsg (MsgRequest rq') Nothing) =
        rq == rq'
    match (MsgNotif rn) (IncomingMsg (MsgNotif rn') Nothing) =
        rn == rn'
    match (MsgResponse _) (IncomingError e) =
        getErrMsg e == "Id not recognized"
    match (MsgError e) (IncomingMsg (MsgError e') Nothing) =
        getErrMsg e == getErrMsg e'
    match (MsgError _) (IncomingError e) =
        getErrMsg e == "Id not recognized"
    match _ _ = False

type TwoWayAppConduits = AppConduits Value Value Value Value Value Value IO

twoWayNet :: ([Message Value Value Value], Ver) -> Property
twoWayNet (rr, ver) = monadicIO $ do
    rt <- run $ do
        to <- atomically $ newTBMChan 128
        ti <- atomically $ newTBMChan 128
        let tiSink   = sinkTBMChan ti True
            toSource = sourceTBMChan to
            toSink   = sinkTBMChan to True
            tiSource = sourceTBMChan ti
        withAsync (srv tiSink toSource) $ \_ ->
            runConduits ver False toSink tiSource cliApp
    assert $ length rt == length rs
    assert $ all (uncurry match) (zip rs rt)
  where
    rs = map f rr where
        f (MsgRequest rq) = MsgRequest $ rq { getReqId = IdNull }
        f m = m

    cliApp :: TwoWayAppConduits -> IO [IncomingMsg Value Value Value Value]
    cliApp (src, snk) = (CL.sourceList rs $$ snk) >> (src $$ CL.consume)

    srv tiSink toSource = runConduits ver False tiSink toSource srvApp

    srvApp :: TwoWayAppConduits -> IO ()
    srvApp (src, snk) = src $= CL.map respond $$ snk

    respond (IncomingError e) =
        MsgError e
    respond (IncomingMsg (MsgRequest (Request ver' _ p i)) _) =
        MsgResponse (Response ver' p i)
    respond (IncomingMsg (MsgNotif rn) _) =
        MsgNotif rn
    respond (IncomingMsg (MsgError e) _) =
        MsgNotif (Notif (getErrVer e) "error" (toJSON e))
    respond _ = undefined

    match (MsgRequest (Request ver' m p _))
        ( IncomingMsg (MsgResponse (Response ver'' p' _))
                      (Just (Request ver''' m' p'' _)) ) =
        p == p' && p == p'' && m == m' && ver' == ver'' && ver'' == ver'''
    match (MsgNotif (Notif ver' _ p))
        (IncomingMsg (MsgNotif (Notif ver'' _ p')) Nothing) =
        p == p' && ver' == ver''
    match (MsgResponse (Response ver' _ _))
        (IncomingMsg (MsgError e) Nothing) =
        ver' == getErrVer e && getErrMsg e == "Id not recognized"
    match (MsgError e@(ErrorObj _ _ _ _ IdNull))
        (IncomingMsg (MsgNotif (Notif _ "error" e')) Nothing) =
        toJSON e == e'
    match (MsgError _)
        (IncomingMsg (MsgError e) Nothing) =
        getErrMsg e == "Id not recognized"
    match _ _ = False

realNet :: ([Request Value], Ver) -> Property
realNet (rr, ver) = monadicIO $ do
    rs <- run $ do
        withAsync (tcpServer ver ss srvApp) $ \_ -> cli
    assert $ length rs == length rr
    assert $
        map (getReqParams . fromJust . matchingReq) rs == map getReqParams rr
  where
    ss = serverSettings 58493 "127.0.0.1"
    cs = clientSettings 58493 "127.0.0.1"

    cli = do
        cE <- try $ tcpClient ver True cs cliApp
        either (const cli) return
            (cE :: Either SomeException [IncomingMsg Value () () Value])

    srvApp :: AppConduits () () Value Value () () IO -> IO ()
    srvApp (src, snk) = src $= CL.map respond $$ snk

    cliApp :: AppConduits Value () () () () Value IO
           -> IO [IncomingMsg Value () () Value]
    cliApp (src, snk) = do
        CL.sourceList (map f rr) $$ snk
        src $$ CL.consume
      where
        f rq = MsgRequest (rq { getReqId = IdNull })

    respond (IncomingMsg (MsgRequest (Request ver' _ p i)) _) =
        MsgResponse (Response ver' p i)
    respond _ = undefined
