import { ERC20Wrapper } from '@0x/contracts-asset-proxy';
import { blockchainTests, constants, describe, expect, shortZip } from '@0x/contracts-test-utils';
import { BigNumber, StakingRevertErrors } from '@0x/utils';
import * as _ from 'lodash';

import { DelegatorsByPoolId, OperatorByPoolId, StakeInfo, StakeStatus } from '../src/types';

import { FinalizerActor } from './actors/finalizer_actor';
import { PoolOperatorActor } from './actors/pool_operator_actor';
import { StakerActor } from './actors/staker_actor';
import { deployAndConfigureContractsAsync, StakingApiWrapper } from './utils/api_wrapper';
import { toBaseUnitAmount } from './utils/number_utils';

// tslint:disable:no-unnecessary-type-assertion
// tslint:disable:max-file-line-count
blockchainTests.resets('Testing Rewards', env => {
    // tokens & addresses
    let accounts: string[];
    let owner: string;
    let actors: string[];
    let exchangeAddress: string;
    let takerAddress: string;
    // wrappers
    let stakingApiWrapper: StakingApiWrapper;
    // let testWrapper: TestRewardBalancesContract;
    let erc20Wrapper: ERC20Wrapper;
    // test parameters
    let stakers: StakerActor[];
    let poolOperatorStaker: StakerActor;
    let poolId: string;
    let poolOperator: PoolOperatorActor;
    let finalizer: FinalizerActor;
    // tests
    before(async () => {
        // create accounts
        accounts = await env.getAccountAddressesAsync();
        owner = accounts[0];
        exchangeAddress = accounts[1];
        takerAddress = accounts[2];
        actors = accounts.slice(3);
        // set up ERC20Wrapper
        erc20Wrapper = new ERC20Wrapper(env.provider, accounts, owner);
        // deploy staking contracts
        stakingApiWrapper = await deployAndConfigureContractsAsync(env, owner, erc20Wrapper);
        // set up staking parameters
        await stakingApiWrapper.utils.setParamsAsync({
            minimumPoolStake: new BigNumber(2),
            cobbDouglasAlphaNumerator: new BigNumber(1),
            cobbDouglasAlphaDenominator: new BigNumber(6),
        });
        // setup stakers
        stakers = actors.slice(0, 2).map(a => new StakerActor(a, stakingApiWrapper));
        // setup pools
        poolOperator = new PoolOperatorActor(actors[2], stakingApiWrapper);
        // Create a pool where all rewards go to members.
        poolId = await poolOperator.createStakingPoolAsync(0, true);
        // Stake something in the pool or else it won't get any rewards.
        poolOperatorStaker = new StakerActor(poolOperator.getOwner(), stakingApiWrapper);
        await poolOperatorStaker.stakeWithPoolAsync(poolId, new BigNumber(2));
        // set exchange address
        await stakingApiWrapper.stakingContract.addExchangeAddress(exchangeAddress).awaitTransactionSuccessAsync();
        // associate operators for tracking in Finalizer
        const operatorByPoolId: OperatorByPoolId = {};
        operatorByPoolId[poolId] = poolOperator.getOwner();
        // associate actors with pools for tracking in Finalizer
        const stakersByPoolId: DelegatorsByPoolId = {};
        stakersByPoolId[poolId] = actors.slice(0, 3);
        // create Finalizer actor
        finalizer = new FinalizerActor(actors[3], stakingApiWrapper, [poolId], operatorByPoolId, stakersByPoolId);
        // Skip to next epoch so operator stake is realized.
        await stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync();
    });
    describe('Reward Simulation', () => {
        interface EndBalances {
            // staker 1
            stakerRewardBalance_1?: BigNumber;
            stakerWethBalance_1?: BigNumber;
            // staker 2
            stakerRewardBalance_2?: BigNumber;
            stakerWethBalance_2?: BigNumber;
            // operator
            operatorWethBalance?: BigNumber;
            // undivided balance in reward pool
            poolRewardBalance?: BigNumber;
            membersRewardBalance?: BigNumber;
        }
        const validateEndBalances = async (_expectedEndBalances: EndBalances): Promise<void> => {
            const expectedEndBalances = {
                // staker 1
                stakerRewardBalance_1:
                    _expectedEndBalances.stakerRewardBalance_1 !== undefined
                        ? _expectedEndBalances.stakerRewardBalance_1
                        : constants.ZERO_AMOUNT,
                stakerWethBalance_1:
                    _expectedEndBalances.stakerWethBalance_1 !== undefined
                        ? _expectedEndBalances.stakerWethBalance_1
                        : constants.ZERO_AMOUNT,
                // staker 2
                stakerRewardBalance_2:
                    _expectedEndBalances.stakerRewardBalance_2 !== undefined
                        ? _expectedEndBalances.stakerRewardBalance_2
                        : constants.ZERO_AMOUNT,
                stakerWethBalance_2:
                    _expectedEndBalances.stakerWethBalance_2 !== undefined
                        ? _expectedEndBalances.stakerWethBalance_2
                        : constants.ZERO_AMOUNT,
                // operator
                operatorWethBalance:
                    _expectedEndBalances.operatorWethBalance !== undefined
                        ? _expectedEndBalances.operatorWethBalance
                        : constants.ZERO_AMOUNT,
                // undivided balance in reward pool
                poolRewardBalance:
                    _expectedEndBalances.poolRewardBalance !== undefined
                        ? _expectedEndBalances.poolRewardBalance
                        : constants.ZERO_AMOUNT,
            };
            const finalEndBalancesAsArray = await Promise.all([
                // staker 1
                stakingApiWrapper.stakingContract
                    .computeRewardBalanceOfDelegator(poolId, stakers[0].getOwner())
                    .callAsync(),
                stakingApiWrapper.wethContract.balanceOf(stakers[0].getOwner()).callAsync(),
                // staker 2
                stakingApiWrapper.stakingContract
                    .computeRewardBalanceOfDelegator(poolId, stakers[1].getOwner())
                    .callAsync(),
                stakingApiWrapper.wethContract.balanceOf(stakers[1].getOwner()).callAsync(),
                // operator
                stakingApiWrapper.wethContract.balanceOf(poolOperator.getOwner()).callAsync(),
                // undivided balance in reward pool
                stakingApiWrapper.stakingContract.rewardsByPoolId(poolId).callAsync(),
            ]);
            expect(finalEndBalancesAsArray[0], 'stakerRewardBalance_1').to.be.bignumber.equal(
                expectedEndBalances.stakerRewardBalance_1,
            );
            expect(finalEndBalancesAsArray[1], 'stakerWethBalance_1').to.be.bignumber.equal(
                expectedEndBalances.stakerWethBalance_1,
            );
            expect(finalEndBalancesAsArray[2], 'stakerRewardBalance_2').to.be.bignumber.equal(
                expectedEndBalances.stakerRewardBalance_2,
            );
            expect(finalEndBalancesAsArray[3], 'stakerWethBalance_2').to.be.bignumber.equal(
                expectedEndBalances.stakerWethBalance_2,
            );
            expect(finalEndBalancesAsArray[4], 'operatorWethBalance').to.be.bignumber.equal(
                expectedEndBalances.operatorWethBalance,
            );
            expect(finalEndBalancesAsArray[5], 'poolRewardBalance').to.be.bignumber.equal(
                expectedEndBalances.poolRewardBalance,
            );
        };
        const payProtocolFeeAndFinalize = async (_fee?: BigNumber) => {
            const fee = _fee !== undefined ? _fee : constants.ZERO_AMOUNT;
            if (!fee.eq(constants.ZERO_AMOUNT)) {
                await stakingApiWrapper.stakingContract
                    .payProtocolFee(poolOperator.getOwner(), takerAddress, fee)
                    .awaitTransactionSuccessAsync({ from: exchangeAddress, value: fee });
            }
            await finalizer.finalizeAsync();
        };
        it('Reward balance should be zero if not delegated', async () => {
            // sanity balances - all zero
            await validateEndBalances({});
        });
        it('Reward balance should be zero if not delegated, when epoch is greater than 0', async () => {
            await payProtocolFeeAndFinalize();
            // sanity balances - all zero
            await validateEndBalances({});
        });
        it('Reward balance should be zero in same epoch as delegation', async () => {
            const amount = toBaseUnitAmount(4);
            await stakers[0].stakeAsync(amount);
            await stakers[0].moveStakeAsync(
                new StakeInfo(StakeStatus.Undelegated),
                new StakeInfo(StakeStatus.Delegated, poolId),
                amount,
            );
            await payProtocolFeeAndFinalize();
            // sanit check final balances - all zero
            await validateEndBalances({});
        });
        it('Operator should receive entire reward if no delegators in their pool', async () => {
            const reward = toBaseUnitAmount(10);
            await payProtocolFeeAndFinalize(reward);
            // sanity check final balances - all zero
            await validateEndBalances({
                operatorWethBalance: reward,
            });
        });
        it(`Operator should receive entire reward if no delegators in their pool
            (staker joins this epoch but is active next epoch)`, async () => {
            // delegate
            const amount = toBaseUnitAmount(4);
            await stakers[0].stakeWithPoolAsync(poolId, amount);
            // finalize
            const reward = toBaseUnitAmount(10);
            await payProtocolFeeAndFinalize(reward);
            // sanity check final balances
            await validateEndBalances({
                operatorWethBalance: reward,
            });
        });
        it('Should give pool reward to delegator', async () => {
            // delegate
            const amount = toBaseUnitAmount(4);
            await stakers[0].stakeWithPoolAsync(poolId, amount);
            // skip epoch, so staker can start earning rewards
            await payProtocolFeeAndFinalize();
            // finalize
            const reward = toBaseUnitAmount(10);
            await payProtocolFeeAndFinalize(reward);
            // sanity check final balances
            await validateEndBalances({
                stakerRewardBalance_1: reward,
                poolRewardBalance: reward,
                membersRewardBalance: reward,
            });
        });
        it('Should split pool reward between delegators', async () => {
            const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)];
            const totalStakeAmount = toBaseUnitAmount(10);
            // first staker delegates
            await stakers[0].stakeWithPoolAsync(poolId, stakeAmounts[0]);
            // second staker delegates
            await stakers[1].stakeWithPoolAsync(poolId, stakeAmounts[1]);
            // skip epoch, so staker can start earning rewards
            await payProtocolFeeAndFinalize();
            // finalize
            const reward = toBaseUnitAmount(10);
            await payProtocolFeeAndFinalize(reward);
            // sanity check final balances
            await validateEndBalances({
                stakerRewardBalance_1: reward.times(stakeAmounts[0]).dividedToIntegerBy(totalStakeAmount),
                stakerRewardBalance_2: reward.times(stakeAmounts[1]).dividedToIntegerBy(totalStakeAmount),
                poolRewardBalance: reward,
                membersRewardBalance: reward,
            });
        });
        it('Should split pool reward between delegators, when they join in different epochs', async () => {
            // first staker delegates (epoch 1)

            const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)];
            const totalStakeAmount = toBaseUnitAmount(10);
            await stakers[0].stakeAsync(stakeAmounts[0]);
            await stakers[0].moveStakeAsync(
                new StakeInfo(StakeStatus.Undelegated),
                new StakeInfo(StakeStatus.Delegated, poolId),
                stakeAmounts[0],
            );

            // skip epoch, so staker can start earning rewards
            await payProtocolFeeAndFinalize();

            // second staker delegates (epoch 2)
            await stakers[1].stakeAsync(stakeAmounts[1]);
            await stakers[1].moveStakeAsync(
                new StakeInfo(StakeStatus.Undelegated),
                new StakeInfo(StakeStatus.Delegated, poolId),
                stakeAmounts[1],
            );

            // skip epoch, so staker can start earning rewards
            await payProtocolFeeAndFinalize();
            // finalize

            const reward = toBaseUnitAmount(10);
            await payProtocolFeeAndFinalize(reward);
            // sanity check final balances
            await validateEndBalances({
                stakerRewardBalance_1: reward.times(stakeAmounts[0]).dividedToIntegerBy(totalStakeAmount),
                stakerRewardBalance_2: reward.times(stakeAmounts[1]).dividedToIntegerBy(totalStakeAmount),
                poolRewardBalance: reward,
                membersRewardBalance: reward,
            });
        });
        it('Should give pool reward to delegators only for the epoch during which they delegated', async () => {
            const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)];
            const totalStakeAmount = toBaseUnitAmount(10);
            // first staker delegates (epoch 1)
            await stakers[0].stakeWithPoolAsync(poolId, stakeAmounts[0]);
            // skip epoch, so first staker can start earning rewards
            await payProtocolFeeAndFinalize();
            // second staker delegates (epoch 2)
            await stakers[1].stakeWithPoolAsync(poolId, stakeAmounts[1]);
            // only the first staker will get this reward
            const rewardForOnlyFirstDelegator = toBaseUnitAmount(10);
            await payProtocolFeeAndFinalize(rewardForOnlyFirstDelegator);
            // finalize
            const rewardForBothDelegators = toBaseUnitAmount(20);
            await payProtocolFeeAndFinalize(rewardForBothDelegators);
            // sanity check final balances
            await validateEndBalances({
                stakerRewardBalance_1: rewardForOnlyFirstDelegator.plus(
                    rewardForBothDelegators.times(stakeAmounts[0]).dividedToIntegerBy(totalStakeAmount),
                ),
                stakerRewardBalance_2: rewardForBothDelegators
                    .times(stakeAmounts[1])
                    .dividedToIntegerBy(totalStakeAmount),
                poolRewardBalance: rewardForOnlyFirstDelegator.plus(rewardForBothDelegators),
                membersRewardBalance: rewardForOnlyFirstDelegator.plus(rewardForBothDelegators),
            });
        });
        it('Should split pool reward between delegators, over several consecutive epochs', async () => {
            const rewardForOnlyFirstDelegator = toBaseUnitAmount(10);
            const sharedRewards = [
                toBaseUnitAmount(20),
                toBaseUnitAmount(16),
                toBaseUnitAmount(24),
                toBaseUnitAmount(5),
                toBaseUnitAmount(0),
                toBaseUnitAmount(17),
            ];
            const totalSharedRewardsAsNumber = _.sumBy(sharedRewards, v => {
                return v.toNumber();
            });
            const totalSharedRewards = new BigNumber(totalSharedRewardsAsNumber);
            const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)];
            const totalStakeAmount = toBaseUnitAmount(10);
            // first staker delegates (epoch 1)
            await stakers[0].stakeWithPoolAsync(poolId, stakeAmounts[0]);
            // skip epoch, so first staker can start earning rewards
            await payProtocolFeeAndFinalize();
            // second staker delegates (epoch 2)
            await stakers[1].stakeWithPoolAsync(poolId, stakeAmounts[1]);
            // only the first staker will get this reward
            await payProtocolFeeAndFinalize(rewardForOnlyFirstDelegator);
            // earn a bunch of rewards
            for (const reward of sharedRewards) {
                await payProtocolFeeAndFinalize(reward);
            }
            // sanity check final balances
            await validateEndBalances({
                stakerRewardBalance_1: rewardForOnlyFirstDelegator.plus(
                    totalSharedRewards.times(stakeAmounts[0]).dividedToIntegerBy(totalStakeAmount),
                ),
                stakerRewardBalance_2: totalSharedRewards.times(stakeAmounts[1]).dividedToIntegerBy(totalStakeAmount),
                poolRewardBalance: rewardForOnlyFirstDelegator.plus(totalSharedRewards),
                membersRewardBalance: rewardForOnlyFirstDelegator.plus(totalSharedRewards),
            });
        });
        it('Should withdraw existing rewards when undelegating stake', async () => {
            const stakeAmount = toBaseUnitAmount(4);
            // first staker delegates (epoch 1)
            await stakers[0].stakeWithPoolAsync(poolId, stakeAmount);
            // skip epoch, so first staker can start earning rewards
            await payProtocolFeeAndFinalize();
            // earn reward
            const reward = toBaseUnitAmount(10);
            await payProtocolFeeAndFinalize(reward);
            // undelegate (withdraws delegator's rewards)
            await stakers[0].moveStakeAsync(
                new StakeInfo(StakeStatus.Delegated, poolId),
                new StakeInfo(StakeStatus.Undelegated),
                stakeAmount,
            );
            // sanity check final balances
            await validateEndBalances({
                stakerRewardBalance_1: constants.ZERO_AMOUNT,
                stakerWethBalance_1: reward,
            });
        });
        it('Should withdraw existing rewards correctly when delegating more stake', async () => {
            const stakeAmount = toBaseUnitAmount(4);
            // first staker delegates (epoch 1)
            await stakers[0].stakeWithPoolAsync(poolId, stakeAmount);
            // skip epoch, so first staker can start earning rewards
            await payProtocolFeeAndFinalize();
            // earn reward
            const reward = toBaseUnitAmount(10);
            await payProtocolFeeAndFinalize(reward);
            // add more stake
            await stakers[0].stakeWithPoolAsync(poolId, stakeAmount);
            // sanity check final balances
            await validateEndBalances({
                stakerRewardBalance_1: constants.ZERO_AMOUNT,
                stakerWethBalance_1: reward,
            });
        });
        it('Should continue earning rewards after adding more stake and progressing several epochs', async () => {
            const rewardBeforeAddingMoreStake = toBaseUnitAmount(10);
            const rewardsAfterAddingMoreStake = [
                toBaseUnitAmount(20),
                toBaseUnitAmount(16),
                toBaseUnitAmount(24),
                toBaseUnitAmount(5),
                toBaseUnitAmount(0),
                toBaseUnitAmount(17),
            ];
            const totalRewardsAfterAddingMoreStake = BigNumber.sum(...rewardsAfterAddingMoreStake);
            const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)];
            const totalStake = BigNumber.sum(...stakeAmounts);
            // first staker delegates (epoch 1)
            await stakers[0].stakeWithPoolAsync(poolId, stakeAmounts[0]);
            // skip epoch, so first staker can start earning rewards
            await payProtocolFeeAndFinalize();
            // second staker delegates (epoch 2)
            await stakers[1].stakeWithPoolAsync(poolId, stakeAmounts[1]);
            // only the first staker will get this reward
            await payProtocolFeeAndFinalize(rewardBeforeAddingMoreStake);
            // earn a bunch of rewards
            for (const reward of rewardsAfterAddingMoreStake) {
                await payProtocolFeeAndFinalize(reward);
            }
            // sanity check final balances
            await validateEndBalances({
                stakerRewardBalance_1: rewardBeforeAddingMoreStake.plus(
                    totalRewardsAfterAddingMoreStake
                        .times(stakeAmounts[0])
                        .dividedBy(totalStake)
                        .integerValue(BigNumber.ROUND_DOWN),
                ),
                stakerRewardBalance_2: totalRewardsAfterAddingMoreStake
                    .times(stakeAmounts[1])
                    .dividedBy(totalStake)
                    .integerValue(BigNumber.ROUND_DOWN),
                poolRewardBalance: rewardBeforeAddingMoreStake.plus(totalRewardsAfterAddingMoreStake),
                membersRewardBalance: rewardBeforeAddingMoreStake.plus(totalRewardsAfterAddingMoreStake),
            });
        });
        it('Should stop collecting rewards after undelegating', async () => {
            // first staker delegates (epoch 1)
            const rewardForDelegator = toBaseUnitAmount(10);
            const rewardNotForDelegator = toBaseUnitAmount(7);
            const stakeAmount = toBaseUnitAmount(4);
            await stakers[0].stakeWithPoolAsync(poolId, stakeAmount);
            // skip epoch, so first staker can start earning rewards
            await payProtocolFeeAndFinalize();
            // earn reward
            await payProtocolFeeAndFinalize(rewardForDelegator);

            // undelegate stake and finalize epoch
            await stakers[0].moveStakeAsync(
                new StakeInfo(StakeStatus.Delegated, poolId),
                new StakeInfo(StakeStatus.Undelegated),
                stakeAmount,
            );

            await payProtocolFeeAndFinalize();

            // this should not go do the delegator
            await payProtocolFeeAndFinalize(rewardNotForDelegator);

            // sanity check final balances
            await validateEndBalances({
                stakerWethBalance_1: rewardForDelegator,
                operatorWethBalance: rewardNotForDelegator,
            });
        });
        it('Should stop collecting rewards after undelegating, after several epochs', async () => {
            // first staker delegates (epoch 1)
            const rewardForDelegator = toBaseUnitAmount(10);
            const rewardsNotForDelegator = [
                toBaseUnitAmount(20),
                toBaseUnitAmount(16),
                toBaseUnitAmount(24),
                toBaseUnitAmount(5),
                toBaseUnitAmount(0),
                toBaseUnitAmount(17),
            ];
            const totalRewardsNotForDelegator = BigNumber.sum(...rewardsNotForDelegator);
            const stakeAmount = toBaseUnitAmount(4);
            await stakers[0].stakeWithPoolAsync(poolId, stakeAmount);
            // skip epoch, so first staker can start earning rewards
            await payProtocolFeeAndFinalize();
            // earn reward
            await payProtocolFeeAndFinalize(rewardForDelegator);
            // undelegate stake and finalize epoch
            await stakers[0].moveStakeAsync(
                new StakeInfo(StakeStatus.Delegated, poolId),
                new StakeInfo(StakeStatus.Undelegated),
                stakeAmount,
            );
            await payProtocolFeeAndFinalize();
            // this should not go do the delegator
            for (const reward of rewardsNotForDelegator) {
                await payProtocolFeeAndFinalize(reward);
            }
            // sanity check final balances
            await validateEndBalances({
                stakerWethBalance_1: rewardForDelegator,
                operatorWethBalance: totalRewardsNotForDelegator,
            });
        });
        it('Should collect fees correctly when leaving and returning to a pool', async () => {
            // first staker delegates (epoch 1)
            const rewardsForDelegator = [toBaseUnitAmount(10), toBaseUnitAmount(15)];
            const rewardNotForDelegator = toBaseUnitAmount(7);
            const stakeAmount = toBaseUnitAmount(4);
            await stakers[0].stakeWithPoolAsync(poolId, stakeAmount);
            // skip epoch, so first staker can start earning rewards
            await payProtocolFeeAndFinalize();
            // earn reward
            await payProtocolFeeAndFinalize(rewardsForDelegator[0]);
            // undelegate stake and finalize epoch
            await stakers[0].moveStakeAsync(
                new StakeInfo(StakeStatus.Delegated, poolId),
                new StakeInfo(StakeStatus.Undelegated),
                stakeAmount,
            );
            await payProtocolFeeAndFinalize();
            // this should not go do the delegator
            await payProtocolFeeAndFinalize(rewardNotForDelegator);
            // delegate stake and go to next epoch
            await stakers[0].moveStakeAsync(
                new StakeInfo(StakeStatus.Undelegated),
                new StakeInfo(StakeStatus.Delegated, poolId),
                stakeAmount,
            );
            await payProtocolFeeAndFinalize();
            // this reward should go to delegator
            await payProtocolFeeAndFinalize(rewardsForDelegator[1]);
            // sanity check final balances
            await validateEndBalances({
                stakerRewardBalance_1: rewardsForDelegator[1],
                stakerWethBalance_1: rewardsForDelegator[0],
                operatorWethBalance: rewardNotForDelegator,
                poolRewardBalance: rewardsForDelegator[1],
            });
        });
        it('Should collect fees correctly when re-delegating after un-delegating', async () => {
            // Note - there are two ranges over which payouts are computed (see _computeRewardBalanceOfDelegator).
            // This triggers the first range (rewards for `delegatedStake.currentEpoch`), but not the second.
            // first staker delegates (epoch 1)
            const rewardForDelegator = toBaseUnitAmount(10);
            const stakeAmount = toBaseUnitAmount(4);
            await stakers[0].stakeAsync(stakeAmount);
            await stakers[0].moveStakeAsync(
                new StakeInfo(StakeStatus.Undelegated),
                new StakeInfo(StakeStatus.Delegated, poolId),
                stakeAmount,
            );
            // skip epoch, so staker can start earning rewards
            await payProtocolFeeAndFinalize();
            // undelegate stake and finalize epoch
            await stakers[0].moveStakeAsync(
                new StakeInfo(StakeStatus.Delegated, poolId),
                new StakeInfo(StakeStatus.Undelegated),
                stakeAmount,
            );
            // this should go to the delegator
            await payProtocolFeeAndFinalize(rewardForDelegator);
            // delegate stake ~ this will result in a payout where rewards are computed on
            // the balance's `currentEpochBalance` field but not the `nextEpochBalance` field.
            await stakers[0].moveStakeAsync(
                new StakeInfo(StakeStatus.Undelegated),
                new StakeInfo(StakeStatus.Delegated, poolId),
                stakeAmount,
            );
            // sanity check final balances
            await validateEndBalances({
                stakerRewardBalance_1: constants.ZERO_AMOUNT,
                stakerWethBalance_1: rewardForDelegator,
                operatorWethBalance: constants.ZERO_AMOUNT,
                poolRewardBalance: constants.ZERO_AMOUNT,
            });
        });
        it('Should withdraw delegator rewards when calling `withdrawDelegatorRewards`', async () => {
            // first staker delegates (epoch 1)
            const rewardForDelegator = toBaseUnitAmount(10);
            const stakeAmount = toBaseUnitAmount(4);
            await stakers[0].stakeAsync(stakeAmount);
            await stakers[0].moveStakeAsync(
                new StakeInfo(StakeStatus.Undelegated),
                new StakeInfo(StakeStatus.Delegated, poolId),
                stakeAmount,
            );
            // skip epoch, so staker can start earning rewards
            await payProtocolFeeAndFinalize();
            // this should go to the delegator
            await payProtocolFeeAndFinalize(rewardForDelegator);
            await stakingApiWrapper.stakingContract.withdrawDelegatorRewards(poolId).awaitTransactionSuccessAsync({
                from: stakers[0].getOwner(),
            });
            // sanity check final balances
            await validateEndBalances({
                stakerRewardBalance_1: constants.ZERO_AMOUNT,
                stakerWethBalance_1: rewardForDelegator,
                operatorWethBalance: constants.ZERO_AMOUNT,
                poolRewardBalance: constants.ZERO_AMOUNT,
            });
        });
        it('should fail to withdraw delegator rewards if the pool has not been finalized for the previous epoch', async () => {
            const rewardForDelegator = toBaseUnitAmount(10);
            const stakeAmount = toBaseUnitAmount(4);
            await stakers[0].stakeAsync(stakeAmount);
            await stakers[0].moveStakeAsync(
                new StakeInfo(StakeStatus.Undelegated),
                new StakeInfo(StakeStatus.Delegated, poolId),
                stakeAmount,
            );
            await stakingApiWrapper.stakingContract
                .payProtocolFee(poolOperator.getOwner(), takerAddress, rewardForDelegator)
                .awaitTransactionSuccessAsync({ from: exchangeAddress, value: rewardForDelegator });
            const currentEpoch = await stakingApiWrapper.stakingContract.currentEpoch().callAsync();
            await stakingApiWrapper.utils.fastForwardToNextEpochAsync();
            await stakingApiWrapper.utils.endEpochAsync();
            const expectedError = new StakingRevertErrors.PoolNotFinalizedError(poolId, currentEpoch);
            expect(
                stakingApiWrapper.stakingContract.withdrawDelegatorRewards(poolId).awaitTransactionSuccessAsync({
                    from: stakers[0].getOwner(),
                }),
            ).to.revertWith(expectedError);
        });
        it(`payout should be based on stake at the time of rewards`, async () => {
            const staker = stakers[0];
            const stakeAmount = toBaseUnitAmount(5);
            // stake and delegate
            await stakers[0].stakeWithPoolAsync(poolId, stakeAmount);
            // skip epoch, so staker can start earning rewards
            await payProtocolFeeAndFinalize();
            // undelegate some stake
            const undelegateAmount = toBaseUnitAmount(2.5);
            await staker.moveStakeAsync(
                new StakeInfo(StakeStatus.Delegated, poolId),
                new StakeInfo(StakeStatus.Undelegated),
                undelegateAmount,
            );
            // finalize
            const reward = toBaseUnitAmount(10);
            await payProtocolFeeAndFinalize(reward);
            // withdraw rewards
            await staker.withdrawDelegatorRewardsAsync(poolId);
            await validateEndBalances({
                stakerRewardBalance_1: toBaseUnitAmount(0),
                stakerWethBalance_1: reward,
            });
        });
        it(`should split payout between two delegators when syncing rewards`, async () => {
            const stakeAmounts = [toBaseUnitAmount(5), toBaseUnitAmount(10)];
            const totalStakeAmount = BigNumber.sum(...stakeAmounts);
            // stake and delegate both
            const stakersAndStake = shortZip(stakers, stakeAmounts);
            for (const [staker, stakeAmount] of stakersAndStake) {
                await staker.stakeWithPoolAsync(poolId, stakeAmount);
            }
            // skip epoch, so stakers can start earning rewards
            await payProtocolFeeAndFinalize();
            // finalize
            const reward = toBaseUnitAmount(10);
            await payProtocolFeeAndFinalize(reward);
            // withdraw rewards
            for (const [staker] of _.reverse(stakersAndStake)) {
                await staker.withdrawDelegatorRewardsAsync(poolId);
            }
            const expectedStakerRewards = stakeAmounts.map(n => reward.times(n).dividedToIntegerBy(totalStakeAmount));
            await validateEndBalances({
                stakerRewardBalance_1: toBaseUnitAmount(0),
                stakerRewardBalance_2: toBaseUnitAmount(0),
                stakerWethBalance_1: expectedStakerRewards[0],
                stakerWethBalance_2: expectedStakerRewards[1],
                poolRewardBalance: new BigNumber(1), // Rounding error
                membersRewardBalance: new BigNumber(1), // Rounding error
            });
        });
        it(`delegator should not be credited payout twice by syncing rewards twice`, async () => {
            const stakeAmounts = [toBaseUnitAmount(5), toBaseUnitAmount(10)];
            const totalStakeAmount = BigNumber.sum(...stakeAmounts);
            // stake and delegate both
            const stakersAndStake = shortZip(stakers, stakeAmounts);
            for (const [staker, stakeAmount] of stakersAndStake) {
                await staker.stakeWithPoolAsync(poolId, stakeAmount);
            }
            // skip epoch, so staker can start earning rewards
            await payProtocolFeeAndFinalize();
            // finalize
            const reward = toBaseUnitAmount(10);
            await payProtocolFeeAndFinalize(reward);
            const expectedStakerRewards = stakeAmounts.map(n => reward.times(n).dividedToIntegerBy(totalStakeAmount));
            await validateEndBalances({
                stakerRewardBalance_1: expectedStakerRewards[0],
                stakerRewardBalance_2: expectedStakerRewards[1],
                stakerWethBalance_1: toBaseUnitAmount(0),
                stakerWethBalance_2: toBaseUnitAmount(0),
                poolRewardBalance: reward,
                membersRewardBalance: reward,
            });
            // First staker will withdraw rewards.
            const sneakyStaker = stakers[0];
            const sneakyStakerExpectedWethBalance = expectedStakerRewards[0];
            await sneakyStaker.withdrawDelegatorRewardsAsync(poolId);
            // Should have been credited the correct amount of rewards.
            let sneakyStakerWethBalance = await stakingApiWrapper.wethContract
                .balanceOf(sneakyStaker.getOwner())
                .callAsync();
            expect(sneakyStakerWethBalance, 'WETH balance after first undelegate').to.bignumber.eq(
                sneakyStakerExpectedWethBalance,
            );
            // Now he'll try to do it again to see if he gets credited twice.
            await sneakyStaker.withdrawDelegatorRewardsAsync(poolId);
            /// The total amount credited should remain the same.
            sneakyStakerWethBalance = await stakingApiWrapper.wethContract
                .balanceOf(sneakyStaker.getOwner())
                .callAsync();
            expect(sneakyStakerWethBalance, 'WETH balance after second undelegate').to.bignumber.eq(
                sneakyStakerExpectedWethBalance,
            );
        });
    });
});
// tslint:enable:no-unnecessary-type-assertion
