{-# 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       #-}

-- Module      :  Network.Ethereum.Web3.Test.SimpleStorageSpec
-- Copyright   :  Alexander Krupenkin 2016
-- License     :  BSD3
--
-- Maintainer  :  mail@akru.me
-- Stability   :  experimental
-- Portability :  unportable
--
-- SimpleStorage is a Solidity contract which stores a uint256.
-- The point of this test is to test function calls to update and
-- read the value, as well as an event monitor.
--

module Network.Ethereum.Web3.Test.SimpleStorageSpec where

import           Control.Concurrent.Async         (wait)
import           Control.Concurrent.MVar
import           Control.Monad.IO.Class           (liftIO)
import           Control.Monad.Trans.Class        (lift)
import           Control.Monad.Trans.Reader       (ask)
import           Data.Default                     (def)
import           Data.List                        (sort)
import           Data.Monoid                      ((<>))
import           Lens.Micro                       ((.~))
import           Test.Hspec

import qualified Network.Ethereum.Api.Eth         as Eth
import           Network.Ethereum.Api.Provider    (forkWeb3)
import           Network.Ethereum.Api.Types
import           Network.Ethereum.Contract        (new)
import           Network.Ethereum.Contract.Event  (event')
import           Network.Ethereum.Contract.TH
import           Network.Ethereum.Web3


import           Network.Ethereum.Web3.Test.Utils

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

unEvT_CountSet :: EvT_CountSet -> UIntN 256
unEvT_CountSet (EvT_CountSet n) = n

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

spec :: Spec
spec = deploy `before` do
  interactions
  events

interactions :: SpecWith Address
interactions = describe "can interact with a SimpleStorage contract" $ do
    -- todo: this should ideally be arbitrary!
    let theValue = 12345

    it "can set the value of a SimpleStorage contract and read the value back" $ \storage -> do
        _ <- contract storage $ setCount theValue

        now <- web3 Eth.blockNumber
        let later = now + 3
        awaitBlock later

        v <- contract storage count
        v `shouldBe` theValue

    it "can set transaction gas limit" $ \storage -> do
        TxReceipt{..} <- contract storage $ withParam (gasLimit .~ 500000) $ setCount theValue
        Just Transaction{..} <- web3 $ Eth.getTransactionByHash receiptTransactionHash
        txGas `shouldBe` 500000

    it "can estimate transaction gas limit" $ \storage -> do
        TxReceipt{..} <- contract storage $ setCount theValue
        Just Transaction{..} <- web3 $ Eth.getTransactionByHash receiptTransactionHash
        txGas `shouldBe` 42822

events :: SpecWith Address
events = do
    describe "can interact with a SimpleStorage contract across block intervals" $ do
        it "can stream events starting and ending in the future, unbounded" $ \storage -> do
            var <- newMVar []
            let theSets = [1, 2, 3]
            putStrLn "Setting up the filter..."
            fiber <- web3 $ do
                let fltr = (def :: Filter EvT_CountSet) { filterAddress = Just [storage] }
                forkWeb3 $ processUntil' var fltr ((3 ==) . length)
            putStrLn "Setting the values..."
            setValues storage theSets
            wait fiber
            putStrLn "Filter caught 3 values..."
            vals <- takeMVar var
            sort (unEvT_CountSet <$> vals) `shouldBe` sort theSets

        it "can stream events starting and ending in the future, bounded" $ \storage -> do
            var <- newMVar []
            let theSets = [13, 14, 15]
            start <- web3 Eth.blockNumber
            let later = BlockWithNumber (start + 3)
                latest = BlockWithNumber (start + 8)
                fltr = (def :: Filter EvT_CountSet) { filterAddress = Just [storage]
                                                    , filterFromBlock = later
                                                    , filterToBlock = latest
                                                    }
            putStrLn "Setting up the filter..."
            fiber <- web3 $
                forkWeb3 $ processUntil' var fltr ((3 ==) . length)
            awaitBlock (start + 3)
            putStrLn "Setting the values..."
            setValues storage theSets
            wait fiber
            putStrLn "Filter caught 3 values..."
            vals <- takeMVar var
            sort (unEvT_CountSet <$> vals) `shouldBe` init (sort theSets)

        it "can stream events starting in the past and ending in the future" $ \storage -> do
            var <- newMVar []
            blockNumberVar <- newEmptyMVar
            let theSets1 = [7, 8, 9]
                theSets2 = [10, 11, 12]
            start <- web3 Eth.blockNumber
            let fltr = (def :: Filter EvT_CountSet) { filterAddress = Just [storage] }
            fiber <- web3 $ do
                forkWeb3 $ processUntil var fltr ((3 ==) . length) (liftIO . putMVar blockNumberVar . changeBlockNumber)
            putStrLn "Running first transactions as past transactions..."
            setValues storage theSets1
            wait fiber
            putStrLn "All past transactions succeeded... "
            Just end <- takeMVar blockNumberVar
            awaitBlock $ end + 1  -- make past transactions definitively in past
            var' <- newMVar []
            fiber' <- web3 $ do
                let fltr' = (def :: Filter EvT_CountSet) { filterAddress = Just [storage]
                                                        , filterFromBlock = BlockWithNumber start}
                forkWeb3 $ processUntil' var' fltr' ((6 ==) . length)
            putStrLn "Setting more values"
            setValues storage theSets2
            wait fiber'
            putStrLn "All new values have ben set"
            vals <- takeMVar var'
            sort (unEvT_CountSet <$> vals) `shouldBe` sort (theSets1 <> theSets2)

    it "can stream events starting and ending in the past, bounded" $ \storage -> do
        var <- newMVar []
        let theSets = [4, 5, 6]
        start <- web3 Eth.blockNumber
        blockNumberVar <- newEmptyMVar
        let fltr = (def :: Filter EvT_CountSet) { filterAddress = Just [storage] }
        putStrLn "Setting up filter for past transactions..."
        fiber <- web3 $ do
          forkWeb3 $ processUntil var fltr ((3 ==) . length) (liftIO . putMVar blockNumberVar . changeBlockNumber)
        putStrLn "Setting values"
        setValues storage theSets
        wait fiber
        putStrLn "All values have been set"
        Just end <- takeMVar blockNumberVar
        var' <- newMVar []
        let fltr' = fltr { filterFromBlock = BlockWithNumber start
                         , filterToBlock = BlockWithNumber end
                         }
        awaitBlock $ end + 1  -- make it definitively in the past
        web3 $ processUntil' var' fltr' ((3 ==) . length)
        vals <- takeMVar var'
        sort (unEvT_CountSet <$> vals) `shouldBe` sort theSets

processUntil :: MVar [EvT_CountSet]
             -> Filter EvT_CountSet
             -> ([EvT_CountSet] -> Bool)  -- TODO: make it work for any event
             -> (Change -> Web3 ())
             -> Web3 ()
processUntil var fltr predicate action = do
  event' fltr $ \(ev :: EvT_CountSet) -> do
    newV <- liftIO $ modifyMVar var $ \v -> return (ev:v, ev:v)
    if predicate newV
        then do
          change <- ask
          lift $ action change
          return TerminateEvent
        else return ContinueEvent

processUntil' :: MVar [EvT_CountSet]
              -> Filter EvT_CountSet
              -> ([EvT_CountSet] -> Bool)
              -> Web3 ()
processUntil' var fltr predicate = processUntil var fltr predicate (const $ return ())

setValues :: Address -> [UIntN 256] -> IO ()
setValues storage = mapM_ (contract storage . setCount)
