import { blockchainTests, expect } from '@0x/contracts-test-utils';
import { AuthorizableRevertErrors, BigNumber, hexUtils } from '@0x/utils';

import { artifacts } from './artifacts';
import {
    FeeCollectorContract,
    FeeCollectorControllerContract,
    TestFixinProtocolFeesContract,
    TestStakingContract,
    TestWethContract,
} from './wrappers';

blockchainTests.resets('ProtocolFees', env => {
    const FEE_MULTIPLIER = 70e3;
    let taker: string;
    let unauthorized: string;
    let protocolFees: TestFixinProtocolFeesContract;
    let staking: TestStakingContract;
    let weth: TestWethContract;
    let feeCollectorController: FeeCollectorControllerContract;
    let singleFeeAmount: BigNumber;

    before(async () => {
        [taker, unauthorized] = await env.getAccountAddressesAsync();
        weth = await TestWethContract.deployFrom0xArtifactAsync(
            artifacts.TestWeth,
            env.provider,
            env.txDefaults,
            artifacts,
        );
        staking = await TestStakingContract.deployFrom0xArtifactAsync(
            artifacts.TestStaking,
            env.provider,
            env.txDefaults,
            artifacts,
            weth.address,
        );
        feeCollectorController = await FeeCollectorControllerContract.deployFrom0xArtifactAsync(
            artifacts.FeeCollectorController,
            env.provider,
            env.txDefaults,
            artifacts,
            weth.address,
            staking.address,
        );
        protocolFees = await TestFixinProtocolFeesContract.deployFrom0xArtifactAsync(
            artifacts.TestFixinProtocolFees,
            env.provider,
            { ...env.txDefaults, from: taker },
            artifacts,
            weth.address,
            staking.address,
            feeCollectorController.address,
            FEE_MULTIPLIER,
        );
        singleFeeAmount = await protocolFees.getSingleProtocolFee().callAsync();
        await weth.mint(taker, singleFeeAmount).awaitTransactionSuccessAsync();
        await weth.approve(protocolFees.address, singleFeeAmount).awaitTransactionSuccessAsync({ from: taker });
    });

    describe('FeeCollector', () => {
        it('should disallow unauthorized initialization', async () => {
            const pool = hexUtils.random();

            await protocolFees.collectProtocolFee(pool).awaitTransactionSuccessAsync({ value: singleFeeAmount });
            await protocolFees.transferFeesForPool(pool).awaitTransactionSuccessAsync();

            const feeCollector = new FeeCollectorContract(
                await protocolFees.getFeeCollector(pool).callAsync(),
                env.provider,
                env.txDefaults,
            );

            const tx = feeCollector
                .initialize(weth.address, staking.address, pool)
                .sendTransactionAsync({ from: unauthorized });
            return expect(tx).to.revertWith(new AuthorizableRevertErrors.SenderNotAuthorizedError(unauthorized));
        });
    });

    describe('_collectProtocolFee()', () => {
        const pool1 = hexUtils.random();
        const pool2 = hexUtils.random();
        let feeCollector1Address: string;
        let feeCollector2Address: string;

        before(async () => {
            feeCollector1Address = await protocolFees.getFeeCollector(pool1).callAsync();
            feeCollector2Address = await protocolFees.getFeeCollector(pool2).callAsync();
        });

        it('should revert if insufficient ETH transferred', async () => {
            const tooLittle = singleFeeAmount.minus(1);
            const tx = protocolFees.collectProtocolFee(pool1).awaitTransactionSuccessAsync({ value: tooLittle });
            return expect(tx).to.revertWith('FixinProtocolFees/ETHER_TRANSFER_FALIED');
        });

        it('should accept ETH fee', async () => {
            const beforeETH = await env.web3Wrapper.getBalanceInWeiAsync(taker);
            await protocolFees.collectProtocolFee(pool1).awaitTransactionSuccessAsync({ value: singleFeeAmount });
            const afterETH = await env.web3Wrapper.getBalanceInWeiAsync(taker);

            // We check for greater than fee spent to allow for spending on gas.
            await expect(beforeETH.minus(afterETH)).to.bignumber.gt(singleFeeAmount);

            await expect(await env.web3Wrapper.getBalanceInWeiAsync(feeCollector1Address)).to.bignumber.eq(
                singleFeeAmount,
            );
        });

        it('should accept ETH after first transfer', async () => {
            await protocolFees.collectProtocolFee(pool1).awaitTransactionSuccessAsync({ value: singleFeeAmount });
            await protocolFees.transferFeesForPool(pool1).awaitTransactionSuccessAsync();
            await protocolFees.collectProtocolFee(pool1).awaitTransactionSuccessAsync({ value: singleFeeAmount });
            await protocolFees.transferFeesForPool(pool1).awaitTransactionSuccessAsync();

            const balanceWETH = await weth.balanceOf(staking.address).callAsync();

            // We leave 1 wei of WETH behind.
            await expect(balanceWETH).to.bignumber.eq(singleFeeAmount.times(2).minus(1));
            await expect(await weth.balanceOf(feeCollector1Address).callAsync()).to.bignumber.equal(1);
            // And no ETH.
            await expect(await env.web3Wrapper.getBalanceInWeiAsync(feeCollector1Address)).to.bignumber.eq(0);
        });

        it('should attribute fees correctly', async () => {
            await protocolFees.collectProtocolFee(pool1).awaitTransactionSuccessAsync({ value: singleFeeAmount });
            await protocolFees.transferFeesForPool(pool1).awaitTransactionSuccessAsync();
            await protocolFees.collectProtocolFee(pool2).awaitTransactionSuccessAsync({ value: singleFeeAmount });
            await protocolFees.transferFeesForPool(pool2).awaitTransactionSuccessAsync();

            const pool1Balance = await staking.balanceForPool(pool1).callAsync();
            const pool2Balance = await staking.balanceForPool(pool2).callAsync();

            const balanceWETH = await weth.balanceOf(staking.address).callAsync();

            await expect(balanceWETH).to.bignumber.equal(singleFeeAmount.times(2).minus(2));

            // We leave 1 wei of WETH behind.
            await expect(pool1Balance).to.bignumber.equal(singleFeeAmount.minus(1));
            await expect(pool2Balance).to.bignumber.equal(singleFeeAmount.minus(1));
            await expect(await weth.balanceOf(feeCollector1Address).callAsync()).to.bignumber.equal(1);
            await expect(await weth.balanceOf(feeCollector2Address).callAsync()).to.bignumber.equal(1);
            await expect(pool2Balance).to.bignumber.equal(singleFeeAmount.minus(1));
            // And no ETH.
            await expect(await env.web3Wrapper.getBalanceInWeiAsync(feeCollector1Address)).to.bignumber.eq(0);
            await expect(await env.web3Wrapper.getBalanceInWeiAsync(feeCollector2Address)).to.bignumber.eq(0);
        });
    });
});
