{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE DeriveGeneric         #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE KindSignatures        #-}
{-# LANGUAGE LambdaCase            #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns        #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE QuasiQuotes           #-}
{-# LANGUAGE RecordWildCards       #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TemplateHaskell       #-}
{-# LANGUAGE TypeApplications      #-}

-- Module      :  Network.Ethereum.Web3.Test.LinearizationSpec
-- Copyright   :  Alexander Krupenkin 2016
-- License     :  BSD3
--
-- Maintainer  :  mail@akru.me
-- Stability   :  experimental
-- Portability :  unportable
--

module Network.Ethereum.Web3.Test.LinearizationSpec where

import           Control.Concurrent               (forkIO)
import           Control.Concurrent.Async         (forConcurrently_)
import           Control.Concurrent.MVar
import           Control.Concurrent.STM           (atomically)
import           Control.Concurrent.STM.TQueue    (TQueue, flushTQueue,
                                                   newTQueueIO, writeTQueue)
import           Control.Monad                    (void)
import           Control.Monad.IO.Class           (MonadIO (..))
import           Control.Monad.Trans.Reader       (ReaderT, ask)
import           Data.Default
import           Data.Either
import           Data.List                        (sort)
import           Data.Maybe                       (fromJust)
import           System.Random                    (randomRIO)
import           Test.Hspec

import qualified Network.Ethereum.Api.Eth         as Eth
import           Network.Ethereum.Api.Types       (Change (..), Filter (..),
                                                   TxReceipt, unQuantity)
import           Network.Ethereum.Contract        (new)
import           Network.Ethereum.Contract.Event
import           Network.Ethereum.Contract.TH     (abiFrom)
import           Network.Ethereum.Web3
import           Network.Ethereum.Web3.Test.Utils

[abiFrom|test/contracts/Linearization.json|]

deploy :: IO Address
deploy = do
    Just address <- web3 $ withAccount () $ withParam id $ new LinearizationContract
    putStrLn $ "Linearization: " ++ show address
    return address

spec :: Spec
spec = do
    deploy `before` linearizationSpec
    deploy `before` floodSpec

floodCount :: Int
floodCount = 200

linearizationSpec :: SpecWith Address
linearizationSpec =
    describe "can bundle and linearize events" $ do
        it "can call e12" $ \linearization -> do
            var <- monitorE1OrE2 linearization
            _ <- contract linearization e12
            res <- takeMVar var
            res `shouldSatisfy` isLeft

        it "can call e21" $ \linearization -> do
            -- wait on the next block
            web3 Eth.blockNumber >>= \bn -> awaitBlock (bn + 1)
            var <- monitorE1OrE2 linearization
            _ <- contract linearization e21
            res <- takeMVar var
            res `shouldSatisfy` isRight

singleFlood :: forall m. (MonadIO m) => Address -> m TxReceipt
singleFlood linearization = liftIO $ do
    rando :: Int <- randomRIO (1, 4)
    contract linearization $
        case rando of
            1 -> e1
            2 -> e2
            3 -> e3
            4 -> e4
            _ -> error "got a number outside of (1,4) after randomR (1,4)"

floodSpec :: SpecWith Address
floodSpec = describe "can correctly demonstrate the difference between `multiEvent` and multiple `event'`s" $ do
    it "properly linearizes with `multiEvent` when flooded and doesn't with multiple `event`s" $ \linearization -> do
        multiQ <- monitorAllFourMulti linearization
        parQ <- monitorAllFourPar linearization
        -- to let the filter settle so we dont block indefinitely on missing events?
        sleepBlocks 10

        -- flood em and wait for all to finish
        liftIO . forConcurrently_ [1..floodCount] . const $ singleFlood linearization

        -- to let the event listeners catch up
        sleepBlocks 10

        -- wait for all multiEvents to be received and flush em out
        multiReceivedEvents <- liftIO . atomically $ flushTQueue multiQ
        parReceivedEvents <- liftIO . atomically $ flushTQueue parQ

        -- did we get at least 1/4 of the events? (this is a gotcha when flooding, sometimes nonces get repeated)
        length multiReceivedEvents `shouldSatisfy` (>= (floodCount `div` 4))
        length parReceivedEvents `shouldSatisfy` (>= (floodCount `div` 4))

        -- did both listeners see the same amount of events?
        length multiReceivedEvents `shouldBe` length parReceivedEvents

        -- the events pushed into the multi TQueue should already be sorted if they happened in the right order
        sort multiReceivedEvents `shouldBe` multiReceivedEvents
        -- the events pushed into the TQueue should not be sorted if they didnt come in in the right order
        sort parReceivedEvents `shouldNotBe` parReceivedEvents

monitorE1OrE2 :: Address -> IO (MVar (Either E1 E2))
monitorE1OrE2 addr = do
    var <- newEmptyMVar
    let fltr1 = (def :: Filter E1) { filterAddress = Just [addr] }
        fltr2 = (def :: Filter E2) { filterAddress = Just [addr] }
        filters = fltr1 :? fltr2 :? NilFilters
        handler1 ev1 = do
            liftIO $ putMVar var (Left ev1)
            return TerminateEvent
        handler2 ev2 = do
            liftIO $ putMVar var (Right ev2)
            return TerminateEvent
        handlers = H handler1 :& H handler2 :& RNil
    _ <- web3 $ multiEvent filters handlers
    return var

data EventTag = ETE1 | ETE2 | ETE3 | ETE4
    deriving (Eq, Read, Show)

instance {-# OVERLAPPING #-} Ord (EventTag, Integer, Integer) where
    (_, b1, t1) `compare` (_, b2, t2) =
        let bCmp = b1 `compare` b2
        in if bCmp == EQ then t1 `compare` t2
                         else bCmp

monitorAllFourMulti :: Address
                    -> IO (TQueue (EventTag, Integer, Integer))
monitorAllFourMulti addr = do
    q <- newTQueueIO
    let f :: forall a. Default (Filter a) => Filter a
        f = defFilter addr
        h = enqueueingHandler q
        filters = f @E1 :? f @E2 :? f @E3 :? f @E4 :? NilFilters
        handlers = h ETE1 :& h ETE2 :& h ETE3 :& h ETE4 :& RNil
    void . web3 $ multiEvent filters handlers
    return q

monitorAllFourPar :: Address
                  -> IO (TQueue (EventTag, Integer, Integer))
monitorAllFourPar addr = do
    q <- newTQueueIO
    let f :: forall a. Default (Filter a) => Filter a
        f = defFilter addr
        h = enqueueingHandler q
        unH (H h) = h

    void . forkIO . web3 $ event' (f @E1) (unH $ h ETE1)
    void . forkIO . web3 $ event' (f @E2) (unH $ h ETE2)
    void . forkIO . web3 $ event' (f @E3) (unH $ h ETE3)
    void . forkIO . web3 $ event' (f @E4) (unH $ h ETE4)
    return q

defFilter :: forall a. Default (Filter a) => Address -> Filter a
defFilter addr = (def :: Filter a) { filterAddress = Just [addr] }

enqueueingHandler :: forall a. TQueue (EventTag, Integer, Integer)
                  -> EventTag
                  -> Handler (ReaderT Change Web3 EventAction) a
enqueueingHandler q tag = H . const $ do
    Change{..} <- ask
    let bn = unQuantity $ fromJust changeBlockNumber
        li = unQuantity $ fromJust changeLogIndex
    liftIO . atomically $ writeTQueue q (tag, bn, li)
    return ContinueEvent
