import { ContractAddresses, ContractWrappers, ERC20TokenContract } from '@0x/contract-wrappers';
import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils';
import { assetDataUtils } from '@0x/order-utils';
import { MarketOperation, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';

import { SwapQuote, SwapQuoteConsumer } from '../src';
import { ExtensionContractType } from '../src/types';

import { chaiSetup } from './utils/chai_setup';
import { migrateOnceAsync } from './utils/migrate';
import { getFullyFillableSwapQuoteWithNoFees, getSignedOrdersWithNoFeesAsync } from './utils/swap_quote';
import { provider, web3Wrapper } from './utils/web3_wrapper';

chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);

const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000);
const TESTRPC_NETWORK_ID = 50;
const FILLABLE_AMOUNTS = [new BigNumber(3), new BigNumber(2), new BigNumber(5)].map(value =>
    value.multipliedBy(ONE_ETH_IN_WEI),
);

const UNLIMITED_ALLOWANCE_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1); // tslint:disable-line:custom-no-magic-numbers

describe('SwapQuoteConsumer', () => {
    let contractWrappers: ContractWrappers;
    let erc20Token: ERC20TokenContract;
    let userAddresses: string[];
    let coinbaseAddress: string;
    let makerAddress: string;
    let takerAddress: string;
    let feeRecipient: string;
    let makerTokenAddress: string;
    let takerTokenAddress: string;
    let makerAssetData: string;
    let takerAssetData: string;
    let wethAssetData: string;
    let contractAddresses: ContractAddresses;

    const networkId = TESTRPC_NETWORK_ID;

    let orders: SignedOrder[];
    let marketSellSwapQuote: SwapQuote;
    let swapQuoteConsumer: SwapQuoteConsumer;
    let erc20ProxyAddress: string;

    before(async () => {
        contractAddresses = await migrateOnceAsync();
        await blockchainLifecycle.startAsync();
        userAddresses = await web3Wrapper.getAvailableAddressesAsync();
        const config = {
            networkId,
            contractAddresses,
        };
        contractWrappers = new ContractWrappers(provider, config);
        [coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses;
        [makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
        erc20Token = new ERC20TokenContract(makerTokenAddress, provider);
        [makerAssetData, takerAssetData, wethAssetData] = [
            assetDataUtils.encodeERC20AssetData(makerTokenAddress),
            assetDataUtils.encodeERC20AssetData(takerTokenAddress),
            assetDataUtils.encodeERC20AssetData(contractAddresses.etherToken),
        ];
    });
    after(async () => {
        await blockchainLifecycle.revertAsync();
    });
    beforeEach(async () => {
        await blockchainLifecycle.startAsync();
        const UNLIMITED_ALLOWANCE = UNLIMITED_ALLOWANCE_IN_BASE_UNITS;
        erc20ProxyAddress = contractWrappers.erc20Proxy.address;

        const totalFillableAmount = FILLABLE_AMOUNTS.reduce(
            (a: BigNumber, c: BigNumber) => a.plus(c),
            new BigNumber(0),
        );

        await erc20Token.transfer.sendTransactionAsync(makerAddress, totalFillableAmount, {
            from: coinbaseAddress,
        });

        await erc20Token.approve.sendTransactionAsync(erc20ProxyAddress, UNLIMITED_ALLOWANCE, {
            from: makerAddress,
        });
        orders = await getSignedOrdersWithNoFeesAsync(
            provider,
            makerAssetData,
            wethAssetData,
            makerAddress,
            takerAddress,
            FILLABLE_AMOUNTS,
            contractAddresses.exchange,
        );

        marketSellSwapQuote = getFullyFillableSwapQuoteWithNoFees(
            makerAssetData,
            wethAssetData,
            orders,
            MarketOperation.Sell,
        );

        swapQuoteConsumer = new SwapQuoteConsumer(provider, {
            networkId,
        });
    });
    afterEach(async () => {
        await blockchainLifecycle.revertAsync();
    });
    // TODO(david): write tests to ensure options work for executeSwapQuote
    // describe('executeSwapQuoteOrThrowAsync', () => {
    //     /*
    //      * Testing that SwapQuoteConsumer logic correctly performs a execution (doesn't throw or revert)
    //      * Does not test the validity of the state change performed by the forwarder smart contract
    //      */
    //     it('should perform an asset swap with Forwarder contract when provided corresponding useExtensionContract option', async () => {
    //         let makerBalance = await erc20TokenContract.balanceOf.callAsync(makerAddress);
    //         let takerBalance = await erc20TokenContract.balanceOf.callAsync(takerAddress);
    //         expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
    //         expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
    //         await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, { takerAddress, useExtensionContract: ConsumerType.Forwarder });
    //         makerBalance = await erc20TokenContract.balanceOf.callAsync(makerAddress);
    //         takerBalance = await erc20TokenContract.balanceOf.callAsync(takerAddress);
    //         expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
    //         expect(makerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
    //     });
    //     it('should perform an asset swap with Exchange contract when provided corresponding useExtensionContract option', async () => {
    //         let makerBalance = await erc20TokenContract.balanceOf.callAsync(makerAddress);
    //         let takerBalance = await erc20TokenContract.balanceOf.callAsync(takerAddress);
    //         expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
    //         expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
    //         await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, { takerAddress });
    //         makerBalance = await erc20TokenContract.balanceOf.callAsync(makerAddress);
    //         takerBalance = await erc20TokenContract.balanceOf.callAsync(takerAddress);
    //         expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI));
    //         expect(makerBalance).to.bignumber.equal(constants.ZERO_AMOUNT);
    //     });
    // });

    describe('getSmartContractParamsOrThrow', () => {
        describe('valid swap quote', async () => {
            // TODO(david) Check for valid MethodAbi
            it('should provide correct and optimized smart contract params for Forwarder contract when provided corresponding useExtensionContract option', async () => {
                const { toAddress } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(marketSellSwapQuote, {
                    useExtensionContract: ExtensionContractType.Forwarder,
                });
                expect(toAddress).to.deep.equal(contractWrappers.forwarder.address);
            });
            it('should provide correct and optimized smart contract params for Exchange contract when provided corresponding useExtensionContract option', async () => {
                const { toAddress } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync(marketSellSwapQuote, {
                    useExtensionContract: ExtensionContractType.None,
                });
                expect(toAddress).to.deep.equal(contractWrappers.exchange.address);
            });
        });
    });

    describe('getCalldataOrThrow', () => {
        describe('valid swap quote', async () => {
            it('should provide correct and optimized calldata options for Forwarder contract when provided corresponding useExtensionContract option', async () => {
                const { toAddress } = await swapQuoteConsumer.getCalldataOrThrowAsync(marketSellSwapQuote, {
                    useExtensionContract: ExtensionContractType.Forwarder,
                });
                expect(toAddress).to.deep.equal(contractWrappers.forwarder.address);
            });
            it('should provide correct and optimized smart contract params for Exchange contract when provided corresponding useExtensionContract option', async () => {
                const { toAddress } = await swapQuoteConsumer.getCalldataOrThrowAsync(marketSellSwapQuote, {
                    useExtensionContract: ExtensionContractType.None,
                });
                expect(toAddress).to.deep.equal(contractWrappers.exchange.address);
            });
        });
    });
});
