{-# LANGUAGE ScopedTypeVariables #-}

module Main where

import           Data.Maybe
import           Data.List
import           Data.IORef
import           Test.HUnit

import           System.Environment
import           Razao.Util
import           Market.Util

import           Mercado
import           Mercado.Types
import           Mercado.Internals
import           Mercado.RateLimiter

import           Data.Time.Clock.POSIX
import           Data.Word
import           Control.Exception
import           Control.Concurrent.Async

import           JsonParser
import           Data.HashMap.Strict

---------------------------------------
main = do
    -- setup exchange configuration
    config <- getMBConfig :: IO Mercado
    walletBitcoinAddress  <- getEnv "MERCADO_BTC_TRANSFER_DESTINATION_WALLET"
    walletLitecoinAddress <- getEnv "MERCADO_LTC_TRANSFER_DESTINATION_WALLET"
    let btcWithdrawalWallet = Wallet walletBitcoinAddress
        ltcWithdrawalWallet = Wallet walletLitecoinAddress
    runTestTT
      (tests
        config
        (btcWithdrawalWallet :: Wallet BTC)
        (ltcWithdrawalWallet :: Wallet LTC)
        (undefined :: Price BRL)
        (undefined :: Vol LTC))

tests config btcWallet ltcWallet priceTy volTy =
  TestList [
      TestLabel "request orderbook"                 (requestOrderbook      config priceTy volTy)
    , TestLabel "parse sample orderbook"            (parseSampleOrderbook         priceTy volTy)
    , TestLabel "parse sample operation response"   (parseSampleOperation         priceTy volTy)
    , TestLabel "parse sample limit order response" (parseSampleLimitOrder        priceTy volTy)

    , TestLabel "sell at Mercado"    (sellCointest     config priceTy volTy)
    , TestLabel "cancel limit order" (cancelLimitOrder config priceTy volTy)
    , TestLabel "get order info"     (getOrderInfo     config priceTy volTy)
    , TestLabel "get my orders"      (getMyOrders      config priceTy volTy)

    , TestLabel "get my funds"                   (getMyFunds config)
    , TestLabel "get BTC Trades"                 testGetMercadoBTCTrades
    , TestLabel "parse sample funds available"   parseSampleFunds
    , TestLabel "parse sample transfer response" parseSampleBTCTransfer

    -- ** DANGEROUS ** ** DANGEROUS ** ** DANGEROUS ** This can destroy real coins!
    -- , TestLabel "withdraw bitcoins from Mercado"  (sendCoins config btcWallet)
    -- , TestLabel "withdraw litecoins from Mercado" (sendCoins config ltcWallet)

    -- ** Can only run by themselves
    -- , TestLabel "Rate limit X requests in (X-1)*(1/rate) secs" (rateLimitReqs config priceTy volTy 120)
    -- , TestLabel "Cancel All Pending Orders" (testCancelAllPendingOrders config priceTy volTy)
    ]

---------------------------- TESTS --------------------------------
requestOrderbook :: forall p v. (Coin p, Coin v) => Mercado -> Price p -> Vol v -> Test
requestOrderbook mercado _ _ = TestCase $ do
  book <- getMercadoBook mercado :: IO (MercadoBook p v)
  now  <- getPOSIXTime
  putStrLn $ "\n timestamp: " ++ show now
  putStrLn $ "\n" ++ showTopN 4 book

---------------------------------------
parseSampleOrderbook :: forall p v. (Coin p, Coin v) => Price p -> Vol v -> Test
parseSampleOrderbook _ _ = TestCase $ do
  jVal <- parseFailWrapper parse sampleOrderBookResponse
  eSampleBook' <- throwIfLeft id $ badFormatWrapper convertMercadoBook 0 jVal
  assertEqual "Is parsed orderbook (aka Quotebook) what we expected?"
      (sampleOrderBook :: MercadoBook (Price p) (Vol v))
      (eSampleBook'    :: MercadoBook (Price p) (Vol v))

sampleOrderBookResponse =
        "{\"orderbook\":" ++
            "{\"bids\": " ++
                "[{\"order_id\": 6517812, \"quantity\": \"1.43206110\", \"limit_price\": \"1731.00011\", \"is_owner\": false}," ++
                "{\"order_id\": 6517811, \"quantity\": \"0.02606995\", \"limit_price\": \"1731.00010\", \"is_owner\": false}," ++
                "{\"order_id\": 6517628, \"quantity\": \"0.03992486\", \"limit_price\": \"1730.00000\", \"is_owner\": true}," ++
                "{\"order_id\": 6517705, \"quantity\": \"0.45130000\", \"limit_price\": \"1730.00000\", \"is_owner\": true}," ++
                "{\"order_id\": 6482716, \"quantity\": \"0.03200000\", \"limit_price\": \"1717.00000\", \"is_owner\": false}" ++
                "]," ++
            "\"asks\":" ++
                "[{\"order_id\": 6517733, \"quantity\": \"1.93789286\", \"limit_price\": \"1749.99999\", \"is_owner\": false}," ++
                "{\"order_id\": 6508273, \"quantity\": \"0.84083982\", \"limit_price\": \"1763.00000\", \"is_owner\": true}," ++
                "{\"order_id\": 6512735, \"quantity\": \"0.11448000\", \"limit_price\": \"1763.00000\", \"is_owner\": false}," ++
                "{\"order_id\": 6517679, \"quantity\": \"0.13082008\", \"limit_price\": \"1768.64807\", \"is_owner\": false}," ++
                "{\"order_id\": 6517675, \"quantity\": \"0.37760000\", \"limit_price\": \"1768.64808\", \"is_owner\": true}" ++
                "]," ++
            "\"latest_order_id\": 6517832" ++
            "}" ++
        "}"

sampleOrderBook :: (Coin p, Coin v) => MercadoBook (Price p) (Vol v)
sampleOrderBook = QuoteBook
        { bids =
            [ Quote { side = Bid, price = 1731.00011, volume = 1.43206110, qtail = (OID 0 6517812, False)}
            , Quote { side = Bid, price = 1731.00010, volume = 0.02606995, qtail = (OID 0 6517811, False)}
            , Quote { side = Bid, price = 1730.00000, volume = 0.03992486, qtail = (OID 0 6517628, True)}
            , Quote { side = Bid, price = 1730.00000, volume = 0.45130000, qtail = (OID 0 6517705, True)}
            , Quote { side = Bid, price = 1717.00000, volume = 0.03200000, qtail = (OID 0 6482716, False)}
            ]
        , asks =
            [ Quote { side = Ask, price = 1749.99999, volume = 1.93789286, qtail = (OID 0 6517733, False)}
            , Quote { side = Ask, price = 1763.00000, volume = 0.84083982, qtail = (OID 0 6508273, True)}
            , Quote { side = Ask, price = 1763.00000, volume = 0.11448000, qtail = (OID 0 6512735, False)}
            , Quote { side = Ask, price = 1768.64807, volume = 0.13082008, qtail = (OID 0 6517679, False)}
            , Quote { side = Ask, price = 1768.64808, volume = 0.37760000, qtail = (OID 0 6517675, True)}
            ]
        , counter = OID 0 6517832 }

---------------------------------------
parseSampleOperation :: forall p v. (Coin p, Coin v) => Price p -> Vol v -> Test
parseSampleOperation _ _ = TestCase $ do
  jVal <- parseFailWrapper parse sampleOperationResponse

  -- lambda ignores first (Timestamp) parameter
  sampleFill' <- throwIfLeft id $ badFormatWrapper (\ _ -> convertMercadoOperation (OID 0 42) ) undefined jVal
  assertEqual "Is parsed fill what we expected?"
      (sampleFill  :: Fill p v)
      (sampleFill' :: Fill p v)

sampleOperationResponse :: String
sampleOperationResponse =
                    "{" ++
                        "\"operation_id\": 223474," ++
                        "\"quantity\": \"0.04028000\"," ++
                        "\"price\": \"1713.00000\"," ++
                        "\"fee_rate\": \"0.30\"," ++
                        "\"executed_timestamp\": \"1459437628\"" ++
                    "}"

sampleFill :: (Coin p, Coin v) => Fill p v
sampleFill = Fill
    { fillID     = 223474
    , mFillTime  = Just 1459437628
    , fillVolume = 0.04028000
    , fillPrice  = 1713
    , fillFee    = 0.2069989
    , orderId    = OID 0 42
    }

---------------------------------------
parseSampleLimitOrder :: forall p v. (Coin p, Coin v) => Price p -> Vol v -> Test
parseSampleLimitOrder _ _ = TestCase $ do
    jVal <- parseFailWrapper parse sampleLimitOrderResponse
    sampleLimit' <- throwIfLeft id $ convertMercadoResponse (badFormatWrapper convertLimitOrderResp) jVal
    assertEqual "Is parsed order what we expected?"
        (sampleLimitOrder, sampleFills :: [Fill p v])
        sampleLimit'

sampleLimitOrder :: (Coin p, Coin v) => Order p v (Confirmation p v)
sampleLimitOrder = LimitOrder
        { oSide       = Ask
        , limitPrice  = 1711
        , limitVolume = 4.0
        , aConfirmation =
                Conf { orderID      = OID 0 7808547
                     , mTimestamp   = Just 1459437628
                     , mExecuted    = Just (1711.52114 , 2.85769964)
                     , mOrderStatus = Just Inactive
                     }
        }

sampleFills :: (Coin p, Coin v) => [Fill p v]
sampleFills =
    [ Fill
        { fillID     = 223474
        , fillPrice  = 1713
        , fillVolume = 0.04028000
        , fillFee    = 0.01 * 0.7 * 1713 * 0.04028000
        , mFillTime  = Just 1459437628
        , orderId    = OID 0 7808547
        }
    , Fill
        { fillID     = 223475
        , fillPrice  = 1711.50000
        , fillVolume = 2.81741964
        , fillFee    = 0.01 * 0.7 * 1711.5 * 2.81741964
        , mFillTime  = Just 1459437628
        , orderId    = OID 0 7808547
        }
    ]

sampleLimitOrderResponse =
    "{" ++
        "\"response_data\": {" ++
            "\"order\": {" ++
                "\"order_id\": 7808547," ++
                "\"coin_pair\": \"BRLBTC\"," ++
                "\"order_type\": 2," ++
                "\"status\": 3," ++
                "\"has_fills\": true," ++
                "\"quantity\": \"4.00000000\"," ++
                "\"limit_price\": \"1711.00000\"," ++
                "\"executed_quantity\": \"2.85769964\"," ++
                "\"executed_price_avg\": \"1711.52114\"," ++
                "\"fee\": \"34.23709348\"," ++
                "\"created_timestamp\": \"1459437628\"," ++
                "\"updated_timestamp\": \"1459437631\"," ++
                "\"operations\": [" ++
                    "{" ++
                        "\"operation_id\": 223474," ++
                        "\"quantity\": \"0.04028000\"," ++
                        "\"price\": \"1713.00000\"," ++
                        "\"fee_rate\": \"0.70\"," ++
                        "\"executed_timestamp\": \"1459437628\"" ++
                    "}," ++
                    "{" ++
                        "\"operation_id\": 223475," ++
                        "\"quantity\": \"2.81741964\"," ++
                        "\"price\": \"1711.50000\"," ++
                        "\"fee_rate\": \"0.70\"," ++
                        "\"executed_timestamp\": \"1459437628\"" ++
                    "}" ++
                "]" ++
            "}" ++
        "}," ++
        "\"status_code\": 100," ++
        "\"server_unix_timestamp\": \"1459437631\"" ++
    "}"

---------------------------------------
sellCointest :: forall p v. (Coin p, Coin v) => Mercado -> Price p -> Vol v -> Test
sellCointest config _ _ = TestCase $ do
  conf <- sellAt config (99777 :: Price p) (0.01 :: Vol v)
  putStrLn $ "\nConfirmation: " ++ show conf

---------------------------------------
-- FIX ME! This test should be improved by checking that the
-- order disappears from the order book.
cancelLimitOrder :: forall p v. (Coin p, Coin v) => Mercado -> Price p -> Vol v -> Test
cancelLimitOrder config _ _ = TestCase $ do
  conf <- sellAt config (9966.55 :: Price p) (0.01 :: Vol v)
  putStrLn $ "\nConfirmation: " ++ show conf
  status <- cancelMercadoOrder config (orderID $ conf) :: IO (Order p v (Confirmation p v), [Fill p v])
  putStrLn $ "Cancellation status: " ++ show status

---------------------------------------
-- This will fail for all except live BRLBTC market
getOrderInfo :: forall p v. (Coin p, Coin v) => Mercado -> Price p -> Vol v -> Test
getOrderInfo config _ _ = TestCase $ do
    orderInfo' <- getMercadoOrderInfo config (OID 0 7808547)
    assertEqual "Is order info what we expected?"
        (sampleLimitOrder , sampleFills )
        (orderInfo' :: (Order p v (Confirmation p v), [Fill p v]))

---------------------------------------
-- This will fail for all except live BRLBTC market
getMyOrders :: forall p v. (Coin p, Coin v) => Mercado -> Price p -> Vol v -> Test
getMyOrders config _ _ = TestCase $ do
  os <- getMercadoOrders config False Nothing (Just 1458667750) (Just 1459468800)
  if length (os :: [(Order p v (Confirmation p v), [Fill p v])]) /= 27
    then assertFailure $ "Wrong number of orders returned: " ++ show (length os) ++ " instead of 27"
    else return ()

---------------------------------------
getMyFunds :: Mercado -> Test
getMyFunds config = TestCase $ do
  funds <- getMercadoFunds config
  putStrLn $ "\n" ++ (show funds)

---------------------------------------
-- To do: This is BRLBTC Only
testGetMercadoBTCTrades :: Test
testGetMercadoBTCTrades = TestCase $ do
  trades <- getMercadoBTCTrades
  putStrLn $ "\n" ++ (showList' trades)

showList' :: Show a => [a] -> String
showList' = intercalate "\n" . fmap show . take 10

---------------------------------------
parseSampleFunds = TestCase $ do
    jVal <- parseFailWrapper parse sampleFundsResponse
    sampleFunds' <- throwIfLeft id $ convertMercadoResponse (badFormatWrapper convertMercadoGetInfoResp) jVal
    assertEqual "Is parsed funds response what we expected?"
        sampleFunds
        sampleFunds'


sampleFundsResponse =
    "{" ++
        "\"response_data\": {" ++
            "\"balance\": {" ++
                "\"brl\": {" ++
                    "\"available\": \"4247.97487\"," ++
                    "\"total\": \"5000.99999\"" ++
                "}," ++
                "\"btc\": {" ++
                    "\"available\": \"2.03584796\"," ++
                    "\"total\": \"2.22924796\"," ++
                    "\"amount_open_orders\": 18" ++
                "}," ++
                "\"ltc\": {" ++
                    "\"available\": \"0.00000000\"," ++
                    "\"total\": \"0.00000000\"," ++
                    "\"amount_open_orders\": 0" ++
                "}" ++
            "}," ++
            "\"withdrawal_limits\": {" ++
                "\"brl\": {" ++
                    "\"available\": \"80000.00\"," ++
                    "\"total\": \"80000.00\"" ++
                "}, " ++
                "\"btc\": {" ++
                    "\"available\": \"25.00000000\"," ++
                    "\"total\": \"25.00000000\"" ++
                "}," ++
                "\"ltc\": {" ++
                    "\"available\": \"500.00000000\"," ++
                    "\"total\": \"500.00000000\"" ++
                "}" ++
            "}" ++
        "}," ++
        "\"status_code\": 100," ++
        "\"server_unix_timestamp\": \"1458661056\"" ++
    "}"

sampleFunds = (4247.97487, 2.03584796, 0.0, 1458661056)


---------------------------------------
parseSampleBTCTransfer = TestCase $ do
    jVal <- parseFailWrapper parse sampleBTCTransferResponse
    sampleTransfer' <- throwIfLeft id $ convertMercadoResponse (badFormatWrapper convertMercadoWithdrawalResponse) jVal
    assertEqual "Is parsed funds response what we expected?"
        sampleBTCTransfer
        sampleTransfer'


sampleBTCTransferResponse =
    "{" ++
        "\"response_data\": {" ++
            "\"withdrawal\": {" ++
                "\"id\": 350011," ++
                "\"coin\": \"BTC\"," ++
                "\"quantity\": \"291.01234009\"," ++
                "\"fee\": \"0.00090000\"," ++
                "\"address\": \"mjav1RxWrkfRHFHDq22LQir4obUJkJufw7\"," ++
                "\"description\": \"\"," ++
                "\"status\": 1," ++
                "\"tx\": \"\"," ++
                "\"created_timestamp\": \"1495312314\"," ++
                "\"updated_timestamp\": \"1495312317\"" ++
            "}" ++
        "}," ++
        "\"status_code\": 100," ++
        "\"server_unix_timestamp\": \"1495312315\"" ++
        "}"

sampleBTCTransfer =
    MercadoTransfer
        { transfTime  = 1495312314
        , transfVol   = 291.01234009
        , transfFee   = 0.00090000
        , toWallet    = Wallet "mjav1RxWrkfRHFHDq22LQir4obUJkJufw7"
        , mTransferID = Nothing
        }
---------------------------------------
-- ** DANGEROUS ** ** DANGEROUS ** ** DANGEROUS ** May use real Coins!
sendCoins :: forall v. Coin v => Mercado -> Wallet v -> Test
sendCoins config wallet = TestCase $ do
  let volume = (0.01234 :: Vol v)
  putStrLn $ "\nattempting to send " ++ show volume ++ " to: " ++ show wallet
  tran <- mercadoWithdraw config wallet volume
  putStrLn $ "transaction as obtained: " ++ show tran
  assertEqual "wallet incorrect" wallet (toWallet  tran)
  assertEqual "volume incorrect" volume (transfVol tran)

---------------------------------------
-- Makes 'reps' asynchronous calls to getMercadoBook
-- because of built-in rate limiting, these should wait and only
-- start a new execution once every rateLimitInterval

rateLimitReqs :: forall p v. (Coin p, Coin v) => Mercado -> Price p -> Vol v -> Int -> Test
rateLimitReqs config _ _ reps = TestCase $ do
    let list = take reps $ repeat config
    start <- getPOSIXTime
    books <- mapConcurrently getMercadoBook list :: IO ([MercadoBook p v])
    end   <- getPOSIXTime
    putStrLn ("\nIt was all done in: " ++ show (end - start))

---------------------------------------
-- Places 2 orders and then attempts to cancel all orders.
-- Checks that there are no pending orders after the cancellation.
testCancelAllPendingOrders :: forall p v. (Coin p, Coin v) => Mercado -> Price p -> Vol v -> Test
testCancelAllPendingOrders config _ _ = TestCase $ do
  sellAt config (99555.44 :: Price p) (0.01 :: Vol v)
  buyAt  config (   11.22 :: Price p) (0.01 :: Vol v)
  os  <- getMercadoOrders config True Nothing Nothing Nothing
  if length (os :: [(Order p v (Confirmation p v), [Fill p v])]) < 2
    then assertFailure ("Placed two orders but they were not returned by getMercadoOrders: " ++ show os)
    else do
      cancelAllMercadoPendingOrders config :: IO([(Order p v (Confirmation p v), [Fill p v])])
      os' <- getMercadoOrders config True Nothing Nothing Nothing
      if length (os' :: [(Order p v (Confirmation p v), [Fill p v])]) /= 0
        then assertFailure ("Not all orders canceled: " ++ show os')
        else return ()
