import { ERC20TokenEvents, ERC20TokenTransferEventArgs } from '@0x/contracts-erc20';
import { ExchangeEvents, ExchangeFillEventArgs } from '@0x/contracts-exchange';
import { ReferenceFunctions } from '@0x/contracts-exchange-libs';
import {
    constants as stakingConstants,
    IStakingEventsEpochEndedEventArgs,
    IStakingEventsEpochFinalizedEventArgs,
    IStakingEventsEvents,
    IStakingEventsRewardsPaidEventArgs,
    IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs,
} from '@0x/contracts-staking';
import {
    blockchainTests,
    constants,
    expect,
    orderHashUtils,
    toBaseUnitAmount,
    verifyEvents,
} from '@0x/contracts-test-utils';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';

import { Actor } from '../framework/actors/base';
import { FeeRecipient } from '../framework/actors/fee_recipient';
import { OperatorStakerMaker, StakerKeeper } from '../framework/actors/hybrids';
import { Maker } from '../framework/actors/maker';
import { Taker } from '../framework/actors/taker';
import { actorAddressesByName } from '../framework/actors/utils';
import { BlockchainBalanceStore } from '../framework/balances/blockchain_balance_store';
import { LocalBalanceStore } from '../framework/balances/local_balance_store';
import { DeploymentManager } from '../framework/deployment_manager';

blockchainTests.resets('fillOrder integration tests', env => {
    let deployment: DeploymentManager;
    let balanceStore: BlockchainBalanceStore;

    let feeRecipient: FeeRecipient;
    let operator: OperatorStakerMaker;
    let maker: Maker;
    let taker: Taker;
    let delegator: StakerKeeper;

    let poolId: string;
    let operatorShare: number;

    before(async () => {
        deployment = await DeploymentManager.deployAsync(env, {
            numErc20TokensToDeploy: 2,
            numErc721TokensToDeploy: 0,
            numErc1155TokensToDeploy: 0,
        });
        const [makerToken, takerToken] = deployment.tokens.erc20;

        feeRecipient = new FeeRecipient({
            name: 'Fee recipient',
            deployment,
        });
        const orderConfig = {
            feeRecipientAddress: feeRecipient.address,
            makerAssetData: deployment.assetDataEncoder.ERC20Token(makerToken.address).getABIEncodedTransactionData(),
            takerAssetData: deployment.assetDataEncoder.ERC20Token(takerToken.address).getABIEncodedTransactionData(),
            makerFeeAssetData: deployment.assetDataEncoder
                .ERC20Token(makerToken.address)
                .getABIEncodedTransactionData(),
            takerFeeAssetData: deployment.assetDataEncoder
                .ERC20Token(takerToken.address)
                .getABIEncodedTransactionData(),
            makerFee: constants.ZERO_AMOUNT,
            takerFee: constants.ZERO_AMOUNT,
        };
        operator = new OperatorStakerMaker({
            name: 'Pool operator',
            deployment,
            orderConfig,
        });
        maker = new Maker({
            name: 'Maker',
            deployment,
            orderConfig,
        });
        taker = new Taker({ name: 'Taker', deployment });
        delegator = new StakerKeeper({ name: 'Delegator', deployment });

        await operator.configureERC20TokenAsync(makerToken);
        await maker.configureERC20TokenAsync(makerToken);
        await taker.configureERC20TokenAsync(takerToken);
        await taker.configureERC20TokenAsync(deployment.tokens.weth, deployment.staking.stakingProxy.address);

        await operator.configureERC20TokenAsync(deployment.tokens.zrx);
        await delegator.configureERC20TokenAsync(deployment.tokens.zrx);

        // Create a staking pool with the operator as a maker.
        operatorShare = stakingConstants.PPM * 0.95;
        poolId = await operator.createStakingPoolAsync(operatorShare, true);
        // A vanilla maker joins the pool as well.
        await maker.joinStakingPoolAsync(poolId);

        const tokenOwners = {
            ...actorAddressesByName([feeRecipient, operator, maker, taker, delegator]),
            StakingProxy: deployment.staking.stakingProxy.address,
            ZrxVault: deployment.staking.zrxVault.address,
        };
        const tokenContracts = {
            erc20: { makerToken, takerToken, ZRX: deployment.tokens.zrx, WETH: deployment.tokens.weth },
        };
        balanceStore = new BlockchainBalanceStore(tokenOwners, tokenContracts);
        await balanceStore.updateBalancesAsync();
    });

    after(async () => {
        Actor.count = 0;
    });

    async function simulateFillAsync(
        order: SignedOrder,
        txReceipt: TransactionReceiptWithDecodedLogs,
        msgValue?: BigNumber,
    ): Promise<LocalBalanceStore> {
        let remainingValue = msgValue !== undefined ? msgValue : DeploymentManager.protocolFee;
        const localBalanceStore = LocalBalanceStore.create(balanceStore);
        // Transaction gas cost
        localBalanceStore.burnGas(txReceipt.from, DeploymentManager.gasPrice.times(txReceipt.gasUsed));

        // Taker -> Maker
        await localBalanceStore.transferAssetAsync(
            taker.address,
            maker.address,
            order.takerAssetAmount,
            order.takerAssetData,
        );
        // Maker -> Taker
        await localBalanceStore.transferAssetAsync(
            maker.address,
            taker.address,
            order.makerAssetAmount,
            order.makerAssetData,
        );

        // Protocol fee
        if (remainingValue.isGreaterThanOrEqualTo(DeploymentManager.protocolFee)) {
            localBalanceStore.sendEth(
                txReceipt.from,
                deployment.staking.stakingProxy.address,
                DeploymentManager.protocolFee,
            );
            remainingValue = remainingValue.minus(DeploymentManager.protocolFee);
        } else {
            await localBalanceStore.transferAssetAsync(
                taker.address,
                deployment.staking.stakingProxy.address,
                DeploymentManager.protocolFee,
                deployment.assetDataEncoder.ERC20Token(deployment.tokens.weth.address).getABIEncodedTransactionData(),
            );
        }

        return localBalanceStore;
    }

    function verifyFillEvents(order: SignedOrder, receipt: TransactionReceiptWithDecodedLogs): void {
        // Ensure that the fill event was correct.
        verifyEvents<ExchangeFillEventArgs>(
            receipt,
            [
                {
                    makerAddress: maker.address,
                    feeRecipientAddress: feeRecipient.address,
                    makerAssetData: order.makerAssetData,
                    takerAssetData: order.takerAssetData,
                    makerFeeAssetData: order.makerFeeAssetData,
                    takerFeeAssetData: order.takerFeeAssetData,
                    orderHash: orderHashUtils.getOrderHashHex(order),
                    takerAddress: taker.address,
                    senderAddress: taker.address,
                    makerAssetFilledAmount: order.makerAssetAmount,
                    takerAssetFilledAmount: order.takerAssetAmount,
                    makerFeePaid: constants.ZERO_AMOUNT,
                    takerFeePaid: constants.ZERO_AMOUNT,
                    protocolFeePaid: DeploymentManager.protocolFee,
                },
            ],
            ExchangeEvents.Fill,
        );

        // Ensure that the transfer events were correctly emitted.
        verifyEvents<ERC20TokenTransferEventArgs>(
            receipt,
            [
                {
                    _from: taker.address,
                    _to: maker.address,
                    _value: order.takerAssetAmount,
                },
                {
                    _from: maker.address,
                    _to: taker.address,
                    _value: order.makerAssetAmount,
                },
            ],
            ERC20TokenEvents.Transfer,
        );
    }

    it('should fill an order', async () => {
        // Create and fill the order
        const order = await maker.signOrderAsync();
        const receipt = await taker.fillOrderAsync(order, order.takerAssetAmount);

        // Check balances
        const expectedBalances = await simulateFillAsync(order, receipt);
        await balanceStore.updateBalancesAsync();
        balanceStore.assertEquals(expectedBalances);

        // There should have been a fill event and two transfer events. A
        // 'StakingPoolEarnedRewardsInEpoch' event should not be expected because the only staking
        // pool that was created does not have enough stake.
        verifyFillEvents(order, receipt);
    });
    it('should activate a staking pool if it has sufficient stake', async () => {
        // Stake just enough to qualify the pool for rewards.
        await delegator.stakeAsync(toBaseUnitAmount(100), poolId);

        // The delegator, functioning as a keeper, ends the epoch so that delegated stake (theirs
        // and the operator's) becomes active. This puts the staking pool above the minimumPoolStake
        // threshold, so it should be able to earn rewards in the new epoch.
        // Finalizing the pool shouldn't settle rewards because it didn't earn rewards last epoch.
        await delegator.endEpochAsync();
        await delegator.finalizePoolsAsync([poolId]);
        await balanceStore.updateBalancesAsync();

        // Create and fill the order
        const order = await maker.signOrderAsync();
        const receipt = await taker.fillOrderAsync(order, order.takerAssetAmount);

        // Check balances
        const expectedBalances = await simulateFillAsync(order, receipt);
        await balanceStore.updateBalancesAsync();
        balanceStore.assertEquals(expectedBalances);

        // In addition to the fill event and two transfer events emitted in the previous test, we
        // now expect a `StakingPoolEarnedRewardsInEpoch` event to be emitted because the staking
        // pool now has enough stake in the current epoch to earn rewards.
        verifyFillEvents(order, receipt);
        const currentEpoch = await deployment.staking.stakingWrapper.currentEpoch().callAsync();
        verifyEvents<IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs>(
            receipt,
            [
                {
                    epoch: currentEpoch,
                    poolId,
                },
            ],
            IStakingEventsEvents.StakingPoolEarnedRewardsInEpoch,
        );
    });
    it('should pay out rewards to operator and delegator', async () => {
        // Operator and delegator each stake some ZRX; wait an epoch so that the stake is active.
        await operator.stakeAsync(toBaseUnitAmount(100), poolId);
        await delegator.stakeAsync(toBaseUnitAmount(50), poolId);
        await delegator.endEpochAsync();

        // Create and fill the order. One order's worth of protocol fees are now available as rewards.
        const order = await maker.signOrderAsync();
        await taker.fillOrderAsync(order, order.takerAssetAmount);
        const rewardsAvailable = DeploymentManager.protocolFee;

        // Fetch the current balances
        await balanceStore.updateBalancesAsync();
        const expectedBalances = LocalBalanceStore.create(balanceStore);

        // End the epoch. This should wrap the staking proxy's ETH balance.
        const endEpochReceipt = await delegator.endEpochAsync();
        const newEpoch = await deployment.staking.stakingWrapper.currentEpoch().callAsync();

        // Check balances
        expectedBalances.wrapEth(
            deployment.staking.stakingProxy.address,
            deployment.tokens.weth.address,
            DeploymentManager.protocolFee,
        );
        expectedBalances.burnGas(delegator.address, DeploymentManager.gasPrice.times(endEpochReceipt.gasUsed));
        await balanceStore.updateBalancesAsync();
        balanceStore.assertEquals(expectedBalances);

        // Check the EpochEnded event
        const weightedDelegatorStake = toBaseUnitAmount(50).times(0.9);
        verifyEvents<IStakingEventsEpochEndedEventArgs>(
            endEpochReceipt,
            [
                {
                    epoch: newEpoch.minus(1),
                    numPoolsToFinalize: new BigNumber(1),
                    rewardsAvailable,
                    totalFeesCollected: DeploymentManager.protocolFee,
                    totalWeightedStake: toBaseUnitAmount(100).plus(weightedDelegatorStake),
                },
            ],
            IStakingEventsEvents.EpochEnded,
        );

        // The rewards are split between the operator and delegator based on the pool's operatorShare
        const operatorReward = ReferenceFunctions.getPartialAmountFloor(
            new BigNumber(operatorShare),
            new BigNumber(constants.PPM_DENOMINATOR),
            rewardsAvailable,
        );
        const delegatorReward = rewardsAvailable.minus(operatorReward);

        // Finalize the pool. This should automatically pay the operator in WETH.
        const [finalizePoolReceipt] = await delegator.finalizePoolsAsync([poolId]);

        // Check balances
        await expectedBalances.transferAssetAsync(
            deployment.staking.stakingProxy.address,
            operator.address,
            operatorReward,
            deployment.assetDataEncoder.ERC20Token(deployment.tokens.weth.address).getABIEncodedTransactionData(),
        );
        expectedBalances.burnGas(delegator.address, DeploymentManager.gasPrice.times(finalizePoolReceipt.gasUsed));
        await balanceStore.updateBalancesAsync();
        balanceStore.assertEquals(expectedBalances);

        // Check finalization events
        verifyEvents<IStakingEventsRewardsPaidEventArgs>(
            finalizePoolReceipt,
            [
                {
                    epoch: newEpoch,
                    poolId,
                    operatorReward,
                    membersReward: delegatorReward,
                },
            ],
            IStakingEventsEvents.RewardsPaid,
        );
        verifyEvents<IStakingEventsEpochFinalizedEventArgs>(
            finalizePoolReceipt,
            [
                {
                    epoch: newEpoch.minus(1),
                    rewardsPaid: rewardsAvailable,
                    rewardsRemaining: constants.ZERO_AMOUNT,
                },
            ],
            IStakingEventsEvents.EpochFinalized,
        );
    });
    it('should credit rewards from orders made by the operator to their pool', async () => {
        // Stake just enough to qualify the pool for rewards.
        await delegator.stakeAsync(toBaseUnitAmount(100), poolId);
        await delegator.endEpochAsync();

        // Create and fill the order
        const order = await operator.signOrderAsync();
        await taker.fillOrderAsync(order, order.takerAssetAmount);

        // Check that the pool has collected fees from the above fill.
        const poolStats = await deployment.staking.stakingWrapper.getStakingPoolStatsThisEpoch(poolId).callAsync();
        expect(poolStats.feesCollected).to.bignumber.equal(DeploymentManager.protocolFee);
    });
    it('should collect WETH fees and pay out rewards', async () => {
        // Operator and delegator each stake some ZRX; wait an epoch so that the stake is active.
        await operator.stakeAsync(toBaseUnitAmount(100), poolId);
        await delegator.stakeAsync(toBaseUnitAmount(50), poolId);
        await delegator.endEpochAsync();

        // Fetch the current balances
        await balanceStore.updateBalancesAsync();

        // Create and fill the order. One order's worth of protocol fees are now available as rewards.
        const order = await maker.signOrderAsync();
        const receipt = await taker.fillOrderAsync(order, order.takerAssetAmount, { value: constants.ZERO_AMOUNT });
        const rewardsAvailable = DeploymentManager.protocolFee;
        const expectedBalances = await simulateFillAsync(order, receipt, constants.ZERO_AMOUNT);

        // End the epoch. This should wrap the staking proxy's ETH balance.
        const endEpochReceipt = await delegator.endEpochAsync();

        // Check balances
        expectedBalances.burnGas(delegator.address, DeploymentManager.gasPrice.times(endEpochReceipt.gasUsed));
        await balanceStore.updateBalancesAsync();
        balanceStore.assertEquals(expectedBalances);

        // The rewards are split between the operator and delegator based on the pool's operatorShare
        const operatorReward = ReferenceFunctions.getPartialAmountFloor(
            new BigNumber(operatorShare),
            new BigNumber(constants.PPM_DENOMINATOR),
            rewardsAvailable,
        );

        // Finalize the pool. This should automatically pay the operator in WETH.
        const [finalizePoolReceipt] = await delegator.finalizePoolsAsync([poolId]);

        // Check balances
        await expectedBalances.transferAssetAsync(
            deployment.staking.stakingProxy.address,
            operator.address,
            operatorReward,
            deployment.assetDataEncoder.ERC20Token(deployment.tokens.weth.address).getABIEncodedTransactionData(),
        );
        expectedBalances.burnGas(delegator.address, DeploymentManager.gasPrice.times(finalizePoolReceipt.gasUsed));
        await balanceStore.updateBalancesAsync();
        balanceStore.assertEquals(expectedBalances);
    });
});
