import { IAssetDataContract } from '@0x/contracts-asset-proxy';
import { DummyERC721TokenContract } from '@0x/contracts-erc721';
import { ForwarderContract } from '@0x/contracts-exchange-forwarder';
import {
    blockchainTests,
    constants,
    getLatestBlockTimestampAsync,
    hexConcat,
    toBaseUnitAmount,
} from '@0x/contracts-test-utils';
import { generatePseudoRandomSalt } from '@0x/order-utils';
import { SignatureType, SignedOrder } from '@0x/types';
import { AbiEncoder, BigNumber, ExchangeForwarderRevertErrors } from '@0x/utils';

import { deployEth2DaiBridgeAsync } from '../bridges/deploy_eth2dai_bridge';
import { deployUniswapBridgeAsync } from '../bridges/deploy_uniswap_bridge';
import { Actor } from '../framework/actors/base';
import { FeeRecipient } from '../framework/actors/fee_recipient';
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 { DeploymentManager } from '../framework/deployment_manager';
import { TestEth2DaiContract, TestUniswapExchangeContract } from '../wrappers';

import { deployForwarderAsync } from './deploy_forwarder';
import { ForwarderTestFactory } from './forwarder_test_factory';

blockchainTests.resets('Forwarder <> ERC20Bridge integration tests', env => {
    let deployment: DeploymentManager;
    let balanceStore: BlockchainBalanceStore;
    let testFactory: ForwarderTestFactory;

    let forwarder: ForwarderContract;
    let assetDataEncoder: IAssetDataContract;
    let eth2Dai: TestEth2DaiContract;
    let uniswapExchange: TestUniswapExchangeContract;

    let erc721Token: DummyERC721TokenContract;
    let nftId: BigNumber;
    let makerTokenAssetData: string;
    let makerFeeTokenAssetData: string;
    let eth2DaiBridgeAssetData: string;
    let uniswapBridgeAssetData: string;

    let maker: Maker;
    let taker: Taker;
    let orderFeeRecipient: FeeRecipient;
    let forwarderFeeRecipient: FeeRecipient;

    let eth2DaiBridgeOrder: SignedOrder;
    let uniswapBridgeOrder: SignedOrder;

    before(async () => {
        assetDataEncoder = new IAssetDataContract(constants.NULL_ADDRESS, env.provider);
        deployment = await DeploymentManager.deployAsync(env, {
            numErc20TokensToDeploy: 2,
            numErc721TokensToDeploy: 1,
            numErc1155TokensToDeploy: 0,
        });
        const [makerToken, makerFeeToken] = deployment.tokens.erc20;
        [erc721Token] = deployment.tokens.erc721;

        forwarder = await deployForwarderAsync(deployment, env);
        const eth2DaiContracts = await deployEth2DaiBridgeAsync(deployment, env);
        const [eth2DaiBridge] = eth2DaiContracts;
        [, eth2Dai] = eth2DaiContracts;
        const uniswapContracts = await deployUniswapBridgeAsync(deployment, env, [makerToken.address]);
        const [uniswapBridge] = uniswapContracts;
        [, [uniswapExchange]] = uniswapContracts;

        makerTokenAssetData = assetDataEncoder.ERC20Token(makerToken.address).getABIEncodedTransactionData();
        makerFeeTokenAssetData = assetDataEncoder.ERC20Token(makerFeeToken.address).getABIEncodedTransactionData();
        const wethAssetData = assetDataEncoder
            .ERC20Token(deployment.tokens.weth.address)
            .getABIEncodedTransactionData();

        const bridgeDataEncoder = AbiEncoder.create([{ name: 'fromTokenAddress', type: 'address' }]);
        const bridgeData = bridgeDataEncoder.encode([deployment.tokens.weth.address]);
        eth2DaiBridgeAssetData = assetDataEncoder
            .ERC20Bridge(makerToken.address, eth2DaiBridge.address, bridgeData)
            .getABIEncodedTransactionData();
        uniswapBridgeAssetData = assetDataEncoder
            .ERC20Bridge(makerToken.address, uniswapBridge.address, bridgeData)
            .getABIEncodedTransactionData();

        taker = new Taker({ name: 'Taker', deployment });
        orderFeeRecipient = new FeeRecipient({
            name: 'Order fee recipient',
            deployment,
        });
        forwarderFeeRecipient = new FeeRecipient({
            name: 'Forwarder fee recipient',
            deployment,
        });

        const fifteenMinutesInSeconds = 15 * 60;
        const currentBlockTimestamp = await getLatestBlockTimestampAsync();
        const orderDefaults = {
            chainId: deployment.chainId,
            exchangeAddress: deployment.exchange.address,
            takerAddress: constants.NULL_ADDRESS,
            feeRecipientAddress: orderFeeRecipient.address,
            senderAddress: constants.NULL_ADDRESS,
            makerAssetAmount: toBaseUnitAmount(2),
            takerAssetAmount: toBaseUnitAmount(1),
            takerAssetData: wethAssetData,
            makerFee: constants.ZERO_AMOUNT,
            takerFee: constants.ZERO_AMOUNT,
            makerFeeAssetData: makerFeeTokenAssetData,
            takerFeeAssetData: wethAssetData,
            expirationTimeSeconds: new BigNumber(currentBlockTimestamp).plus(fifteenMinutesInSeconds),
            salt: generatePseudoRandomSalt(),
            signature: hexConcat(SignatureType.Wallet),
        };
        eth2DaiBridgeOrder = {
            ...orderDefaults,
            makerAddress: eth2DaiBridge.address,
            makerAssetData: eth2DaiBridgeAssetData,
        };
        uniswapBridgeOrder = {
            ...orderDefaults,
            makerAddress: uniswapBridge.address,
            makerAssetData: uniswapBridgeAssetData,
        };

        maker = new Maker({
            name: 'Maker',
            deployment,
            orderConfig: { ...orderDefaults, makerFee: toBaseUnitAmount(0.01) },
        });
        await maker.configureERC20TokenAsync(makerToken);
        await maker.configureERC20TokenAsync(makerFeeToken);
        await forwarder.approveMakerAssetProxy(makerTokenAssetData).awaitTransactionSuccessAsync();
        [nftId] = await maker.configureERC721TokenAsync(erc721Token);

        // We need to top up the TestUniswapExchange with some ETH so that it can perform tokenToEthSwapInput
        await uniswapExchange.topUpEth().awaitTransactionSuccessAsync({
            from: forwarderFeeRecipient.address,
            value: constants.ONE_ETHER.times(10),
        });

        const tokenOwners = {
            ...actorAddressesByName([maker, taker, orderFeeRecipient, forwarderFeeRecipient]),
            Forwarder: forwarder.address,
            StakingProxy: deployment.staking.stakingProxy.address,
        };
        const tokenContracts = {
            erc20: { makerToken, makerFeeToken, wETH: deployment.tokens.weth },
            erc721: { erc721Token },
        };
        const tokenIds = { erc721: { [erc721Token.address]: [nftId] } };
        balanceStore = new BlockchainBalanceStore(tokenOwners, tokenContracts, tokenIds);

        testFactory = new ForwarderTestFactory(forwarder, deployment, balanceStore, taker, forwarderFeeRecipient);
    });

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

    describe('marketSellOrdersWithEth', () => {
        it('should fully fill a single Eth2DaiBridge order without a taker fee', async () => {
            await testFactory.marketSellTestAsync([eth2DaiBridgeOrder], 1);
        });
        it('should partially fill a single Eth2DaiBridge order without a taker fee', async () => {
            await testFactory.marketSellTestAsync([eth2DaiBridgeOrder], 0.34);
        });
        it('should correctly handle excess maker asset acquired from Eth2Dai', async () => {
            const bridgeExcessBuyAmount = new BigNumber(1);
            await eth2Dai.setExcessBuyAmount(bridgeExcessBuyAmount).awaitTransactionSuccessAsync();
            await testFactory.marketSellTestAsync([eth2DaiBridgeOrder], 0.34, { bridgeExcessBuyAmount });
        });
        it('should fill a single Eth2DaiBridge order with a WETH taker fee', async () => {
            const order = {
                ...eth2DaiBridgeOrder,
                takerFee: toBaseUnitAmount(0.01),
            };
            await testFactory.marketSellTestAsync([order], 0.78);
        });
        it('should fill a single Eth2DaiBridge order with a percentage taker fee', async () => {
            const order = {
                ...eth2DaiBridgeOrder,
                takerFee: toBaseUnitAmount(0.01),
                takerFeeAssetData: makerTokenAssetData,
            };
            await testFactory.marketSellTestAsync([order], 0.78);
        });
        it('should fill an Eth2DaiBridge order along with non-bridge orders, with an affiliate fee', async () => {
            const orders = [
                // ERC721 order
                await maker.signOrderAsync({
                    makerAssetAmount: new BigNumber(1),
                    makerAssetData: assetDataEncoder
                        .ERC721Token(erc721Token.address, nftId)
                        .getABIEncodedTransactionData(),
                    takerFee: toBaseUnitAmount(0.01),
                }),
                eth2DaiBridgeOrder,
                await maker.signOrderAsync({ makerAssetData: makerTokenAssetData }), // Non-bridge order of the same ERC20
            ];
            await testFactory.marketSellTestAsync(orders, 2.56, { forwarderFeePercentage: 1 });
        });
        it('should fully fill a single UniswapBridge order without a taker fee', async () => {
            await testFactory.marketSellTestAsync([uniswapBridgeOrder], 1);
        });
        it('should partially fill a single UniswapBridge order without a taker fee', async () => {
            await testFactory.marketSellTestAsync([uniswapBridgeOrder], 0.34);
        });
        it('should correctly handle excess maker asset acquired from Uniswap', async () => {
            const bridgeExcessBuyAmount = new BigNumber(1);
            await uniswapExchange.setExcessBuyAmount(bridgeExcessBuyAmount).awaitTransactionSuccessAsync();
            await testFactory.marketSellTestAsync([uniswapBridgeOrder], 0.34, { bridgeExcessBuyAmount });
        });
        it('should fill a single UniswapBridge order with a WETH taker fee', async () => {
            const order = {
                ...uniswapBridgeOrder,
                takerFee: toBaseUnitAmount(0.01),
            };
            await testFactory.marketSellTestAsync([order], 0.78);
        });
        it('should fill a single UniswapBridge order with a percentage taker fee', async () => {
            const order = {
                ...uniswapBridgeOrder,
                takerFee: toBaseUnitAmount(0.01),
                takerFeeAssetData: makerTokenAssetData,
            };
            await testFactory.marketSellTestAsync([order], 0.78);
        });
        it('should fill an UniswapBridge order along with non-bridge orders', async () => {
            const orders = [
                // ERC721 order
                await maker.signOrderAsync({
                    makerAssetAmount: new BigNumber(1),
                    makerAssetData: assetDataEncoder
                        .ERC721Token(erc721Token.address, nftId)
                        .getABIEncodedTransactionData(),
                    takerFee: toBaseUnitAmount(0.01),
                }),
                uniswapBridgeOrder,
                await maker.signOrderAsync({ makerAssetData: makerTokenAssetData }), // Non-bridge order of the same ERC20
            ];
            await testFactory.marketSellTestAsync(orders, 2.56, { forwarderFeePercentage: 1 });
        });
        it('should fill multiple bridge orders', async () => {
            await testFactory.marketSellTestAsync([eth2DaiBridgeOrder, uniswapBridgeOrder], 1.23);
        });
        it('should revert if the takerFee is denominated in a different token', async () => {
            const order = {
                ...eth2DaiBridgeOrder,
                takerFee: toBaseUnitAmount(0.01),
                takerFeeAssetData: makerFeeTokenAssetData,
            };
            const expectedError = new ExchangeForwarderRevertErrors.UnsupportedFeeError(makerFeeTokenAssetData);
            await testFactory.marketSellTestAsync([order], 1.23, { revertError: expectedError });
        });
    });
    describe('marketBuyOrdersWithEth', () => {
        it('should fully fill a single Eth2DaiBridge order without a taker fee', async () => {
            await testFactory.marketBuyTestAsync([eth2DaiBridgeOrder], 1);
        });
        it('should partially fill a single Eth2DaiBridge order without a taker fee', async () => {
            await testFactory.marketBuyTestAsync([eth2DaiBridgeOrder], 0.34);
        });
        it('should return excess ETH', async () => {
            await testFactory.marketBuyTestAsync([eth2DaiBridgeOrder], 1, { ethValueAdjustment: 1 });
        });
        it('should correctly handle excess maker asset acquired from Eth2Dai', async () => {
            const bridgeExcessBuyAmount = new BigNumber(1);
            await eth2Dai.setExcessBuyAmount(bridgeExcessBuyAmount).awaitTransactionSuccessAsync();
            await testFactory.marketBuyTestAsync([eth2DaiBridgeOrder], 0.34, { bridgeExcessBuyAmount });
        });
        it('should fill a single Eth2DaiBridge order with a WETH taker fee', async () => {
            const order = {
                ...eth2DaiBridgeOrder,
                takerFee: toBaseUnitAmount(0.01),
            };
            await testFactory.marketBuyTestAsync([order], 0.78);
        });
        it('should fill a single Eth2DaiBridge order with a percentage taker fee', async () => {
            const order = {
                ...eth2DaiBridgeOrder,
                takerFee: toBaseUnitAmount(0.01),
                takerFeeAssetData: makerTokenAssetData,
            };
            await testFactory.marketBuyTestAsync([order], 0.78);
        });
        it('should fill an Eth2DaiBridge order along with non-bridge orders, with an affiliate fee', async () => {
            const orders = [
                // ERC721 order
                await maker.signOrderAsync({
                    makerAssetAmount: new BigNumber(1),
                    makerAssetData: assetDataEncoder
                        .ERC721Token(erc721Token.address, nftId)
                        .getABIEncodedTransactionData(),
                    takerFee: toBaseUnitAmount(0.01),
                }),
                eth2DaiBridgeOrder,
                await maker.signOrderAsync({ makerAssetData: makerTokenAssetData }), // Non-bridge order of the same ERC20
            ];
            await testFactory.marketBuyTestAsync(orders, 2.56, { forwarderFeePercentage: 1 });
        });
        it('should revert if the amount of ETH sent is too low to fill the makerAssetAmount (Eth2Dai)', async () => {
            const expectedError = new ExchangeForwarderRevertErrors.CompleteBuyFailedError(
                eth2DaiBridgeOrder.makerAssetAmount.times(0.5),
                constants.ZERO_AMOUNT,
            );
            await testFactory.marketBuyTestAsync([eth2DaiBridgeOrder], 0.5, {
                ethValueAdjustment: -2,
                revertError: expectedError,
            });
        });
        it('should fully fill a single UniswapBridge order without a taker fee', async () => {
            await testFactory.marketBuyTestAsync([uniswapBridgeOrder], 1);
        });
        it('should partially fill a single UniswapBridge order without a taker fee', async () => {
            await testFactory.marketBuyTestAsync([uniswapBridgeOrder], 0.34);
        });
        it('should correctly handle excess maker asset acquired from Uniswap', async () => {
            const bridgeExcessBuyAmount = new BigNumber(1);
            await uniswapExchange.setExcessBuyAmount(bridgeExcessBuyAmount).awaitTransactionSuccessAsync();
            await testFactory.marketBuyTestAsync([uniswapBridgeOrder], 0.34, { bridgeExcessBuyAmount });
        });
        it('should fill a single UniswapBridge order with a WETH taker fee', async () => {
            const order = {
                ...uniswapBridgeOrder,
                takerFee: toBaseUnitAmount(0.01),
            };
            await testFactory.marketBuyTestAsync([order], 0.78);
        });
        it('should fill a single UniswapBridge order with a percentage taker fee', async () => {
            const order = {
                ...uniswapBridgeOrder,
                takerFee: toBaseUnitAmount(0.01),
                takerFeeAssetData: makerTokenAssetData,
            };
            await testFactory.marketBuyTestAsync([order], 0.78);
        });
        it('should fill an UniswapBridge order along with non-bridge orders', async () => {
            const orders = [
                // ERC721 order
                await maker.signOrderAsync({
                    makerAssetAmount: new BigNumber(1),
                    makerAssetData: assetDataEncoder
                        .ERC721Token(erc721Token.address, nftId)
                        .getABIEncodedTransactionData(),
                    takerFee: toBaseUnitAmount(0.01),
                }),
                uniswapBridgeOrder,
                await maker.signOrderAsync({ makerAssetData: makerTokenAssetData }), // Non-bridge order of the same ERC20
            ];
            await testFactory.marketBuyTestAsync(orders, 2.56, { forwarderFeePercentage: 1 });
        });
        it('should revert if the amount of ETH sent is too low to fill the makerAssetAmount (Uniswap)', async () => {
            const expectedError = new ExchangeForwarderRevertErrors.CompleteBuyFailedError(
                uniswapBridgeOrder.makerAssetAmount.times(0.5),
                constants.ZERO_AMOUNT,
            );
            await testFactory.marketBuyTestAsync([uniswapBridgeOrder], 0.5, {
                ethValueAdjustment: -2,
                revertError: expectedError,
            });
        });
        it('should fill multiple bridge orders', async () => {
            await testFactory.marketBuyTestAsync([eth2DaiBridgeOrder, uniswapBridgeOrder], 1.23);
        });
        it('should revert if the takerFee is denominated in a different token', async () => {
            const order = {
                ...eth2DaiBridgeOrder,
                takerFee: toBaseUnitAmount(0.01),
                takerFeeAssetData: makerFeeTokenAssetData,
            };
            const expectedError = new ExchangeForwarderRevertErrors.UnsupportedFeeError(makerFeeTokenAssetData);
            await testFactory.marketBuyTestAsync([order], 1.23, { revertError: expectedError });
        });
    });
});
// tslint:disable:max-file-line-count
