import {
    assetDataUtils,
    BalanceAndProxyAllowanceLazyStore,
    ExchangeTransferSimulator,
    orderHashUtils,
    OrderStateUtils,
    OrderValidationUtils,
} from '@0xproject/order-utils';
import { AssetProxyId, RevertReason, SignatureType, SignedOrder } from '@0xproject/types';
import { BigNumber, errorUtils, logUtils } from '@0xproject/utils';
import { Web3Wrapper } from '@0xproject/web3-wrapper';
import * as chai from 'chai';
import { LogWithDecodedArgs, Provider, TxData } from 'ethereum-types';
import * as _ from 'lodash';
import 'make-promises-safe';

import { ExchangeContract, ExchangeFillEventArgs } from '../../generated_contract_wrappers/exchange';
import { TestLibsContract } from '../../generated_contract_wrappers/test_libs';

import { artifacts } from './artifacts';
import { expectTransactionFailedAsync } from './assertions';
import { AssetWrapper } from './asset_wrapper';
import { chaiSetup } from './chai_setup';
import { constants } from './constants';
import { ERC20Wrapper } from './erc20_wrapper';
import { ERC721Wrapper } from './erc721_wrapper';
import { ExchangeWrapper } from './exchange_wrapper';
import { OrderFactoryFromScenario } from './order_factory_from_scenario';
import { orderUtils } from './order_utils';
import { signingUtils } from './signing_utils';
import { SimpleAssetBalanceAndProxyAllowanceFetcher } from './simple_asset_balance_and_proxy_allowance_fetcher';
import { SimpleOrderFilledCancelledFetcher } from './simple_order_filled_cancelled_fetcher';
import {
    AllowanceAmountScenario,
    AssetDataScenario,
    BalanceAmountScenario,
    ExpirationTimeSecondsScenario,
    FeeRecipientAddressScenario,
    FillScenario,
    OrderAssetAmountScenario,
    TakerAssetFillAmountScenario,
    TakerScenario,
    TraderStateScenario,
} from './types';

chaiSetup.configure();
const expect = chai.expect;

/**
 * Instantiates a new instance of FillOrderCombinatorialUtils. Since this method has some
 * required async setup, a factory method is required.
 * @param web3Wrapper Web3Wrapper instance
 * @param txDefaults Default Ethereum tx options
 * @return FillOrderCombinatorialUtils instance
 */
export async function fillOrderCombinatorialUtilsFactoryAsync(
    web3Wrapper: Web3Wrapper,
    txDefaults: Partial<TxData>,
): Promise<FillOrderCombinatorialUtils> {
    const accounts = await web3Wrapper.getAvailableAddressesAsync();
    const userAddresses = _.slice(accounts, 0, 5);
    const [ownerAddress, makerAddress, takerAddress] = userAddresses;
    const makerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];

    const provider = web3Wrapper.getProvider();
    const erc20Wrapper = new ERC20Wrapper(provider, userAddresses, ownerAddress);
    const erc721Wrapper = new ERC721Wrapper(provider, userAddresses, ownerAddress);

    const erc20EighteenDecimalTokenCount = 3;
    const eighteenDecimals = new BigNumber(18);
    const [
        erc20EighteenDecimalTokenA,
        erc20EighteenDecimalTokenB,
        zrxToken,
    ] = await erc20Wrapper.deployDummyTokensAsync(erc20EighteenDecimalTokenCount, eighteenDecimals);
    const zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address);

    const erc20FiveDecimalTokenCount = 2;
    const fiveDecimals = new BigNumber(5);
    const [erc20FiveDecimalTokenA, erc20FiveDecimalTokenB] = await erc20Wrapper.deployDummyTokensAsync(
        erc20FiveDecimalTokenCount,
        fiveDecimals,
    );
    const erc20Proxy = await erc20Wrapper.deployProxyAsync();
    await erc20Wrapper.setBalancesAndAllowancesAsync();

    const [erc721Token] = await erc721Wrapper.deployDummyTokensAsync();
    const erc721Proxy = await erc721Wrapper.deployProxyAsync();
    await erc721Wrapper.setBalancesAndAllowancesAsync();
    const erc721Balances = await erc721Wrapper.getBalancesAsync();

    const assetWrapper = new AssetWrapper([erc20Wrapper, erc721Wrapper]);

    const exchangeContract = await ExchangeContract.deployFrom0xArtifactAsync(
        artifacts.Exchange,
        provider,
        txDefaults,
        zrxAssetData,
    );
    const exchangeWrapper = new ExchangeWrapper(exchangeContract, provider);
    await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, ownerAddress);
    await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, ownerAddress);

    await web3Wrapper.awaitTransactionSuccessAsync(
        await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeContract.address, {
            from: ownerAddress,
        }),
        constants.AWAIT_TRANSACTION_MINED_MS,
    );
    await web3Wrapper.awaitTransactionSuccessAsync(
        await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeContract.address, {
            from: ownerAddress,
        }),
        constants.AWAIT_TRANSACTION_MINED_MS,
    );

    const orderFactory = new OrderFactoryFromScenario(
        userAddresses,
        zrxToken.address,
        [erc20EighteenDecimalTokenA.address, erc20EighteenDecimalTokenB.address],
        [erc20FiveDecimalTokenA.address, erc20FiveDecimalTokenB.address],
        erc721Token,
        erc721Balances,
        exchangeContract.address,
    );

    const testLibsContract = await TestLibsContract.deployFrom0xArtifactAsync(artifacts.TestLibs, provider, txDefaults);

    const fillOrderCombinatorialUtils = new FillOrderCombinatorialUtils(
        orderFactory,
        ownerAddress,
        makerAddress,
        makerPrivateKey,
        takerAddress,
        zrxAssetData,
        exchangeWrapper,
        assetWrapper,
        testLibsContract,
    );
    return fillOrderCombinatorialUtils;
}

export class FillOrderCombinatorialUtils {
    public orderFactory: OrderFactoryFromScenario;
    public ownerAddress: string;
    public makerAddress: string;
    public makerPrivateKey: Buffer;
    public takerAddress: string;
    public zrxAssetData: string;
    public exchangeWrapper: ExchangeWrapper;
    public assetWrapper: AssetWrapper;
    public testLibsContract: TestLibsContract;
    public static generateFillOrderCombinations(): FillScenario[] {
        const takerScenarios = [
            TakerScenario.Unspecified,
            // TakerScenario.CorrectlySpecified,
            // TakerScenario.IncorrectlySpecified,
        ];
        const feeRecipientScenarios = [
            FeeRecipientAddressScenario.EthUserAddress,
            // FeeRecipientAddressScenario.BurnAddress,
        ];
        const makerAssetAmountScenario = [
            OrderAssetAmountScenario.Large,
            // OrderAssetAmountScenario.Zero,
            // OrderAssetAmountScenario.Small,
        ];
        const takerAssetAmountScenario = [
            OrderAssetAmountScenario.Large,
            // OrderAssetAmountScenario.Zero,
            // OrderAssetAmountScenario.Small,
        ];
        const makerFeeScenario = [
            OrderAssetAmountScenario.Large,
            // OrderAssetAmountScenario.Small,
            // OrderAssetAmountScenario.Zero,
        ];
        const takerFeeScenario = [
            OrderAssetAmountScenario.Large,
            // OrderAssetAmountScenario.Small,
            // OrderAssetAmountScenario.Zero,
        ];
        const expirationTimeSecondsScenario = [
            ExpirationTimeSecondsScenario.InFuture,
            ExpirationTimeSecondsScenario.InPast,
        ];
        const makerAssetDataScenario = [
            AssetDataScenario.ERC20FiveDecimals,
            AssetDataScenario.ERC20NonZRXEighteenDecimals,
            AssetDataScenario.ERC721,
            AssetDataScenario.ZRXFeeToken,
        ];
        const takerAssetDataScenario = [
            AssetDataScenario.ERC20FiveDecimals,
            AssetDataScenario.ERC20NonZRXEighteenDecimals,
            AssetDataScenario.ERC721,
            AssetDataScenario.ZRXFeeToken,
        ];
        const takerAssetFillAmountScenario = [
            TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount,
            // TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount,
            // TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount,
        ];
        const makerAssetBalanceScenario = [
            BalanceAmountScenario.Higher,
            // BalanceAmountScenario.Exact,
            // BalanceAmountScenario.TooLow,
        ];
        const makerAssetAllowanceScenario = [
            AllowanceAmountScenario.Higher,
            // AllowanceAmountScenario.Exact,
            // AllowanceAmountScenario.TooLow,
            // AllowanceAmountScenario.Unlimited,
        ];
        const makerZRXBalanceScenario = [
            BalanceAmountScenario.Higher,
            // BalanceAmountScenario.Exact,
            // BalanceAmountScenario.TooLow,
        ];
        const makerZRXAllowanceScenario = [
            AllowanceAmountScenario.Higher,
            // AllowanceAmountScenario.Exact,
            // AllowanceAmountScenario.TooLow,
            // AllowanceAmountScenario.Unlimited,
        ];
        const takerAssetBalanceScenario = [
            BalanceAmountScenario.Higher,
            // BalanceAmountScenario.Exact,
            // BalanceAmountScenario.TooLow,
        ];
        const takerAssetAllowanceScenario = [
            AllowanceAmountScenario.Higher,
            // AllowanceAmountScenario.Exact,
            // AllowanceAmountScenario.TooLow,
            // AllowanceAmountScenario.Unlimited,
        ];
        const takerZRXBalanceScenario = [
            BalanceAmountScenario.Higher,
            // BalanceAmountScenario.Exact,
            // BalanceAmountScenario.TooLow,
        ];
        const takerZRXAllowanceScenario = [
            AllowanceAmountScenario.Higher,
            // AllowanceAmountScenario.Exact,
            // AllowanceAmountScenario.TooLow,
            // AllowanceAmountScenario.Unlimited,
        ];
        const fillScenarioArrays = FillOrderCombinatorialUtils._getAllCombinations([
            takerScenarios,
            feeRecipientScenarios,
            makerAssetAmountScenario,
            takerAssetAmountScenario,
            makerFeeScenario,
            takerFeeScenario,
            expirationTimeSecondsScenario,
            makerAssetDataScenario,
            takerAssetDataScenario,
            takerAssetFillAmountScenario,
            makerAssetBalanceScenario,
            makerAssetAllowanceScenario,
            makerZRXBalanceScenario,
            makerZRXAllowanceScenario,
            takerAssetBalanceScenario,
            takerAssetAllowanceScenario,
            takerZRXBalanceScenario,
            takerZRXAllowanceScenario,
        ]);

        const fillScenarios = _.map(fillScenarioArrays, fillScenarioArray => {
            // tslint:disable:custom-no-magic-numbers
            const fillScenario: FillScenario = {
                orderScenario: {
                    takerScenario: fillScenarioArray[0] as TakerScenario,
                    feeRecipientScenario: fillScenarioArray[1] as FeeRecipientAddressScenario,
                    makerAssetAmountScenario: fillScenarioArray[2] as OrderAssetAmountScenario,
                    takerAssetAmountScenario: fillScenarioArray[3] as OrderAssetAmountScenario,
                    makerFeeScenario: fillScenarioArray[4] as OrderAssetAmountScenario,
                    takerFeeScenario: fillScenarioArray[5] as OrderAssetAmountScenario,
                    expirationTimeSecondsScenario: fillScenarioArray[6] as ExpirationTimeSecondsScenario,
                    makerAssetDataScenario: fillScenarioArray[7] as AssetDataScenario,
                    takerAssetDataScenario: fillScenarioArray[8] as AssetDataScenario,
                },
                takerAssetFillAmountScenario: fillScenarioArray[9] as TakerAssetFillAmountScenario,
                makerStateScenario: {
                    traderAssetBalance: fillScenarioArray[10] as BalanceAmountScenario,
                    traderAssetAllowance: fillScenarioArray[11] as AllowanceAmountScenario,
                    zrxFeeBalance: fillScenarioArray[12] as BalanceAmountScenario,
                    zrxFeeAllowance: fillScenarioArray[13] as AllowanceAmountScenario,
                },
                takerStateScenario: {
                    traderAssetBalance: fillScenarioArray[14] as BalanceAmountScenario,
                    traderAssetAllowance: fillScenarioArray[15] as AllowanceAmountScenario,
                    zrxFeeBalance: fillScenarioArray[16] as BalanceAmountScenario,
                    zrxFeeAllowance: fillScenarioArray[17] as AllowanceAmountScenario,
                },
            };
            // tslint:enable:custom-no-magic-numbers
            return fillScenario;
        });

        return fillScenarios;
    }
    /**
     * Recursive implementation of generating all combinations of the supplied
     * string-containing arrays.
     */
    private static _getAllCombinations(arrays: string[][]): string[][] {
        // Base case
        if (arrays.length === 1) {
            const remainingValues = _.map(arrays[0], val => {
                return [val];
            });
            return remainingValues;
        } else {
            const result = [];
            const restOfArrays = arrays.slice(1);
            const allCombinationsOfRemaining = FillOrderCombinatorialUtils._getAllCombinations(restOfArrays); // recur with the rest of array
            // tslint:disable:prefer-for-of
            for (let i = 0; i < allCombinationsOfRemaining.length; i++) {
                for (let j = 0; j < arrays[0].length; j++) {
                    result.push([arrays[0][j], ...allCombinationsOfRemaining[i]]);
                }
            }
            // tslint:enable:prefer-for-of
            return result;
        }
    }
    constructor(
        orderFactory: OrderFactoryFromScenario,
        ownerAddress: string,
        makerAddress: string,
        makerPrivateKey: Buffer,
        takerAddress: string,
        zrxAssetData: string,
        exchangeWrapper: ExchangeWrapper,
        assetWrapper: AssetWrapper,
        testLibsContract: TestLibsContract,
    ) {
        this.orderFactory = orderFactory;
        this.ownerAddress = ownerAddress;
        this.makerAddress = makerAddress;
        this.makerPrivateKey = makerPrivateKey;
        this.takerAddress = takerAddress;
        this.zrxAssetData = zrxAssetData;
        this.exchangeWrapper = exchangeWrapper;
        this.assetWrapper = assetWrapper;
        this.testLibsContract = testLibsContract;
    }
    public async testFillOrderScenarioAsync(
        provider: Provider,
        fillScenario: FillScenario,
        isVerbose: boolean = false,
    ): Promise<void> {
        // 1. Generate order
        const order = this.orderFactory.generateOrder(fillScenario.orderScenario);

        // 2. Sign order
        const orderHashBuff = orderHashUtils.getOrderHashBuffer(order);
        const signature = signingUtils.signMessage(orderHashBuff, this.makerPrivateKey, SignatureType.EthSign);
        const signedOrder = {
            ...order,
            signature: `0x${signature.toString('hex')}`,
        };

        const balanceAndProxyAllowanceFetcher = new SimpleAssetBalanceAndProxyAllowanceFetcher(this.assetWrapper);
        const orderFilledCancelledFetcher = new SimpleOrderFilledCancelledFetcher(
            this.exchangeWrapper,
            this.zrxAssetData,
        );

        // 3. Figure out fill amount
        const takerAssetFillAmount = await this._getTakerAssetFillAmountAsync(
            signedOrder,
            fillScenario.takerAssetFillAmountScenario,
            balanceAndProxyAllowanceFetcher,
            orderFilledCancelledFetcher,
        );

        // 4. Permutate the maker and taker balance/allowance scenarios
        await this._modifyTraderStateAsync(
            fillScenario.makerStateScenario,
            fillScenario.takerStateScenario,
            signedOrder,
            takerAssetFillAmount,
        );

        // 5. If I fill it by X, what are the resulting balances/allowances/filled amounts expected?
        const orderValidationUtils = new OrderValidationUtils(orderFilledCancelledFetcher);
        const lazyStore = new BalanceAndProxyAllowanceLazyStore(balanceAndProxyAllowanceFetcher);
        const exchangeTransferSimulator = new ExchangeTransferSimulator(lazyStore);

        let fillRevertReasonIfExists;
        try {
            await orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
                exchangeTransferSimulator,
                provider,
                signedOrder,
                takerAssetFillAmount,
                this.takerAddress,
                this.zrxAssetData,
            );
            if (isVerbose) {
                logUtils.log(`Expecting fillOrder to succeed.`);
            }
        } catch (err) {
            fillRevertReasonIfExists = err.message;
            if (isVerbose) {
                logUtils.log(`Expecting fillOrder to fail with:`);
                logUtils.log(err);
            }
        }

        // 6. Fill the order
        await this._fillOrderAndAssertOutcomeAsync(
            signedOrder,
            takerAssetFillAmount,
            lazyStore,
            fillRevertReasonIfExists,
        );

        await this._abiEncodeFillOrderAndAssertOutcomeAsync(signedOrder, takerAssetFillAmount);
    }
    private async _fillOrderAndAssertOutcomeAsync(
        signedOrder: SignedOrder,
        takerAssetFillAmount: BigNumber,
        lazyStore: BalanceAndProxyAllowanceLazyStore,
        fillRevertReasonIfExists: RevertReason | undefined,
    ): Promise<void> {
        if (!_.isUndefined(fillRevertReasonIfExists)) {
            return expectTransactionFailedAsync(
                this.exchangeWrapper.fillOrderAsync(signedOrder, this.takerAddress, { takerAssetFillAmount }),
                fillRevertReasonIfExists,
            );
        }

        const makerAddress = signedOrder.makerAddress;
        const makerAssetData = signedOrder.makerAssetData;
        const takerAssetData = signedOrder.takerAssetData;
        const feeRecipient = signedOrder.feeRecipientAddress;

        const expMakerAssetBalanceOfMaker = await lazyStore.getBalanceAsync(makerAssetData, makerAddress);
        const expMakerAssetAllowanceOfMaker = await lazyStore.getProxyAllowanceAsync(makerAssetData, makerAddress);
        const expTakerAssetBalanceOfMaker = await lazyStore.getBalanceAsync(takerAssetData, makerAddress);
        const expZRXAssetBalanceOfMaker = await lazyStore.getBalanceAsync(this.zrxAssetData, makerAddress);
        const expZRXAssetAllowanceOfMaker = await lazyStore.getProxyAllowanceAsync(this.zrxAssetData, makerAddress);
        const expTakerAssetBalanceOfTaker = await lazyStore.getBalanceAsync(takerAssetData, this.takerAddress);
        const expTakerAssetAllowanceOfTaker = await lazyStore.getProxyAllowanceAsync(takerAssetData, this.takerAddress);
        const expMakerAssetBalanceOfTaker = await lazyStore.getBalanceAsync(makerAssetData, this.takerAddress);
        const expZRXAssetBalanceOfTaker = await lazyStore.getBalanceAsync(this.zrxAssetData, this.takerAddress);
        const expZRXAssetAllowanceOfTaker = await lazyStore.getProxyAllowanceAsync(
            this.zrxAssetData,
            this.takerAddress,
        );
        const expZRXAssetBalanceOfFeeRecipient = await lazyStore.getBalanceAsync(this.zrxAssetData, feeRecipient);

        const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
        const alreadyFilledTakerAmount = await this.exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash);
        const remainingTakerAmountToFill = signedOrder.takerAssetAmount.minus(alreadyFilledTakerAmount);
        const expFilledTakerAmount = takerAssetFillAmount.gt(remainingTakerAmountToFill)
            ? remainingTakerAmountToFill
            : alreadyFilledTakerAmount.add(takerAssetFillAmount);

        const expFilledMakerAmount = orderUtils.getPartialAmount(
            expFilledTakerAmount,
            signedOrder.takerAssetAmount,
            signedOrder.makerAssetAmount,
        );
        const expMakerFeePaid = orderUtils.getPartialAmount(
            expFilledTakerAmount,
            signedOrder.takerAssetAmount,
            signedOrder.makerFee,
        );
        const expTakerFeePaid = orderUtils.getPartialAmount(
            expFilledTakerAmount,
            signedOrder.takerAssetAmount,
            signedOrder.takerFee,
        );
        const fillResults = await this.exchangeWrapper.getFillOrderResultsAsync(signedOrder, this.takerAddress, {
            takerAssetFillAmount,
        });
        expect(fillResults.takerAssetFilledAmount).to.be.bignumber.equal(
            expFilledTakerAmount,
            'takerAssetFilledAmount',
        );
        expect(fillResults.makerAssetFilledAmount).to.be.bignumber.equal(
            expFilledMakerAmount,
            'makerAssetFilledAmount',
        );
        expect(fillResults.takerFeePaid).to.be.bignumber.equal(expTakerFeePaid, 'takerFeePaid');
        expect(fillResults.makerFeePaid).to.be.bignumber.equal(expMakerFeePaid, 'makerFeePaid');

        // - Let's fill the order!
        const txReceipt = await this.exchangeWrapper.fillOrderAsync(signedOrder, this.takerAddress, {
            takerAssetFillAmount,
        });

        const actFilledTakerAmount = await this.exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash);
        expect(actFilledTakerAmount).to.be.bignumber.equal(expFilledTakerAmount, 'filledTakerAmount');

        expect(txReceipt.logs.length).to.be.equal(1, 'logs length');
        // tslint:disable-next-line:no-unnecessary-type-assertion
        const log = txReceipt.logs[0] as LogWithDecodedArgs<ExchangeFillEventArgs>;
        expect(log.args.makerAddress).to.be.equal(makerAddress, 'log.args.makerAddress');
        expect(log.args.takerAddress).to.be.equal(this.takerAddress, 'log.args.this.takerAddress');
        expect(log.args.feeRecipientAddress).to.be.equal(feeRecipient, 'log.args.feeRecipientAddress');
        expect(log.args.makerAssetFilledAmount).to.be.bignumber.equal(
            expFilledMakerAmount,
            'log.args.makerAssetFilledAmount',
        );
        expect(log.args.takerAssetFilledAmount).to.be.bignumber.equal(
            expFilledTakerAmount,
            'log.args.takerAssetFilledAmount',
        );
        expect(log.args.makerFeePaid).to.be.bignumber.equal(expMakerFeePaid, 'log.args.makerFeePaid');
        expect(log.args.takerFeePaid).to.be.bignumber.equal(expTakerFeePaid, 'logs.args.takerFeePaid');
        expect(log.args.orderHash).to.be.equal(orderHash, 'log.args.orderHash');
        expect(log.args.makerAssetData).to.be.equal(makerAssetData, 'log.args.makerAssetData');
        expect(log.args.takerAssetData).to.be.equal(takerAssetData, 'log.args.takerAssetData');

        const actMakerAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, makerAssetData);
        expect(actMakerAssetBalanceOfMaker).to.be.bignumber.equal(
            expMakerAssetBalanceOfMaker,
            'makerAssetBalanceOfMaker',
        );

        const actMakerAssetAllowanceOfMaker = await this.assetWrapper.getProxyAllowanceAsync(
            makerAddress,
            makerAssetData,
        );
        expect(actMakerAssetAllowanceOfMaker).to.be.bignumber.equal(
            expMakerAssetAllowanceOfMaker,
            'makerAssetAllowanceOfMaker',
        );

        const actTakerAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, takerAssetData);
        expect(actTakerAssetBalanceOfMaker).to.be.bignumber.equal(
            expTakerAssetBalanceOfMaker,
            'takerAssetBalanceOfMaker',
        );

        const actZRXAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, this.zrxAssetData);
        expect(actZRXAssetBalanceOfMaker).to.be.bignumber.equal(expZRXAssetBalanceOfMaker, 'ZRXAssetBalanceOfMaker');

        const actZRXAssetAllowanceOfMaker = await this.assetWrapper.getProxyAllowanceAsync(
            makerAddress,
            this.zrxAssetData,
        );
        expect(actZRXAssetAllowanceOfMaker).to.be.bignumber.equal(
            expZRXAssetAllowanceOfMaker,
            'ZRXAssetAllowanceOfMaker',
        );

        const actTakerAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, takerAssetData);
        expect(actTakerAssetBalanceOfTaker).to.be.bignumber.equal(
            expTakerAssetBalanceOfTaker,
            'TakerAssetBalanceOfTaker',
        );

        const actTakerAssetAllowanceOfTaker = await this.assetWrapper.getProxyAllowanceAsync(
            this.takerAddress,
            takerAssetData,
        );

        expect(actTakerAssetAllowanceOfTaker).to.be.bignumber.equal(
            expTakerAssetAllowanceOfTaker,
            'TakerAssetAllowanceOfTaker',
        );

        const actMakerAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, makerAssetData);
        expect(actMakerAssetBalanceOfTaker).to.be.bignumber.equal(
            expMakerAssetBalanceOfTaker,
            'MakerAssetBalanceOfTaker',
        );

        const actZRXAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, this.zrxAssetData);
        expect(actZRXAssetBalanceOfTaker).to.be.bignumber.equal(expZRXAssetBalanceOfTaker, 'ZRXAssetBalanceOfTaker');

        const actZRXAssetAllowanceOfTaker = await this.assetWrapper.getProxyAllowanceAsync(
            this.takerAddress,
            this.zrxAssetData,
        );
        expect(actZRXAssetAllowanceOfTaker).to.be.bignumber.equal(
            expZRXAssetAllowanceOfTaker,
            'ZRXAssetAllowanceOfTaker',
        );

        const actZRXAssetBalanceOfFeeRecipient = await this.assetWrapper.getBalanceAsync(
            feeRecipient,
            this.zrxAssetData,
        );
        expect(actZRXAssetBalanceOfFeeRecipient).to.be.bignumber.equal(
            expZRXAssetBalanceOfFeeRecipient,
            'ZRXAssetBalanceOfFeeRecipient',
        );
    }
    private async _abiEncodeFillOrderAndAssertOutcomeAsync(
        signedOrder: SignedOrder,
        takerAssetFillAmount: BigNumber,
    ): Promise<void> {
        const params = orderUtils.createFill(signedOrder, takerAssetFillAmount);
        const expectedAbiEncodedData = this.exchangeWrapper.abiEncodeFillOrder(signedOrder, { takerAssetFillAmount });
        const libsAbiEncodedData = await this.testLibsContract.publicAbiEncodeFillOrder.callAsync(
            params.order,
            params.takerAssetFillAmount,
            params.signature,
        );
        expect(libsAbiEncodedData).to.be.equal(expectedAbiEncodedData, 'ABIEncodedFillOrderData');
    }
    private async _getTakerAssetFillAmountAsync(
        signedOrder: SignedOrder,
        takerAssetFillAmountScenario: TakerAssetFillAmountScenario,
        balanceAndProxyAllowanceFetcher: SimpleAssetBalanceAndProxyAllowanceFetcher,
        orderFilledCancelledFetcher: SimpleOrderFilledCancelledFetcher,
    ): Promise<BigNumber> {
        const orderStateUtils = new OrderStateUtils(balanceAndProxyAllowanceFetcher, orderFilledCancelledFetcher);
        const fillableTakerAssetAmount = await orderStateUtils.getMaxFillableTakerAssetAmountAsync(
            signedOrder,
            this.takerAddress,
        );

        let takerAssetFillAmount;
        switch (takerAssetFillAmountScenario) {
            case TakerAssetFillAmountScenario.Zero:
                takerAssetFillAmount = new BigNumber(0);
                break;

            case TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount:
                takerAssetFillAmount = fillableTakerAssetAmount;
                break;

            case TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount:
                takerAssetFillAmount = fillableTakerAssetAmount.add(1);
                break;

            case TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount:
                const takerAssetProxyId = assetDataUtils.decodeAssetProxyId(signedOrder.takerAssetData);
                const makerAssetProxyId = assetDataUtils.decodeAssetProxyId(signedOrder.makerAssetData);
                const isEitherAssetERC721 =
                    takerAssetProxyId === AssetProxyId.ERC721 || makerAssetProxyId === AssetProxyId.ERC721;
                if (isEitherAssetERC721) {
                    throw new Error(
                        'Cannot test `TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount` together with ERC721 assets since orders involving ERC721 must always be filled exactly.',
                    );
                }
                takerAssetFillAmount = fillableTakerAssetAmount.div(2).floor();
                break;

            default:
                throw errorUtils.spawnSwitchErr('TakerAssetFillAmountScenario', takerAssetFillAmountScenario);
        }

        return takerAssetFillAmount;
    }
    private async _modifyTraderStateAsync(
        makerStateScenario: TraderStateScenario,
        takerStateScenario: TraderStateScenario,
        signedOrder: SignedOrder,
        takerAssetFillAmount: BigNumber,
    ): Promise<void> {
        const makerAssetFillAmount = orderUtils.getPartialAmount(
            takerAssetFillAmount,
            signedOrder.takerAssetAmount,
            signedOrder.makerAssetAmount,
        );
        switch (makerStateScenario.traderAssetBalance) {
            case BalanceAmountScenario.Higher:
                break; // Noop since this is already the default

            case BalanceAmountScenario.TooLow:
                if (makerAssetFillAmount.eq(0)) {
                    throw new Error(`Cannot set makerAssetBalanceOfMaker TooLow if makerAssetFillAmount is 0`);
                }
                const tooLowBalance = makerAssetFillAmount.minus(1);
                await this.assetWrapper.setBalanceAsync(
                    signedOrder.makerAddress,
                    signedOrder.makerAssetData,
                    tooLowBalance,
                );
                break;

            case BalanceAmountScenario.Exact:
                const exactBalance = makerAssetFillAmount;
                await this.assetWrapper.setBalanceAsync(
                    signedOrder.makerAddress,
                    signedOrder.makerAssetData,
                    exactBalance,
                );
                break;

            default:
                throw errorUtils.spawnSwitchErr(
                    'makerStateScenario.traderAssetBalance',
                    makerStateScenario.traderAssetBalance,
                );
        }

        const makerFee = orderUtils.getPartialAmount(
            takerAssetFillAmount,
            signedOrder.takerAssetAmount,
            signedOrder.makerFee,
        );
        switch (makerStateScenario.zrxFeeBalance) {
            case BalanceAmountScenario.Higher:
                break; // Noop since this is already the default

            case BalanceAmountScenario.TooLow:
                if (makerFee.eq(0)) {
                    throw new Error(`Cannot set zrxAsserBalanceOfMaker TooLow if makerFee is 0`);
                }
                const tooLowBalance = makerFee.minus(1);
                await this.assetWrapper.setBalanceAsync(signedOrder.makerAddress, this.zrxAssetData, tooLowBalance);
                break;

            case BalanceAmountScenario.Exact:
                const exactBalance = makerFee;
                await this.assetWrapper.setBalanceAsync(signedOrder.makerAddress, this.zrxAssetData, exactBalance);
                break;

            default:
                throw errorUtils.spawnSwitchErr('makerStateScenario.zrxFeeBalance', makerStateScenario.zrxFeeBalance);
        }

        switch (makerStateScenario.traderAssetAllowance) {
            case AllowanceAmountScenario.Higher:
                break; // Noop since this is already the default

            case AllowanceAmountScenario.TooLow:
                const tooLowAllowance = makerAssetFillAmount.minus(1);
                await this.assetWrapper.setProxyAllowanceAsync(
                    signedOrder.makerAddress,
                    signedOrder.makerAssetData,
                    tooLowAllowance,
                );
                break;

            case AllowanceAmountScenario.Exact:
                const exactAllowance = makerAssetFillAmount;
                await this.assetWrapper.setProxyAllowanceAsync(
                    signedOrder.makerAddress,
                    signedOrder.makerAssetData,
                    exactAllowance,
                );
                break;

            case AllowanceAmountScenario.Unlimited:
                await this.assetWrapper.setProxyAllowanceAsync(
                    signedOrder.makerAddress,
                    signedOrder.makerAssetData,
                    constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
                );
                break;

            default:
                throw errorUtils.spawnSwitchErr(
                    'makerStateScenario.traderAssetAllowance',
                    makerStateScenario.traderAssetAllowance,
                );
        }

        switch (makerStateScenario.zrxFeeAllowance) {
            case AllowanceAmountScenario.Higher:
                break; // Noop since this is already the default

            case AllowanceAmountScenario.TooLow:
                const tooLowAllowance = makerFee.minus(1);
                await this.assetWrapper.setProxyAllowanceAsync(
                    signedOrder.makerAddress,
                    this.zrxAssetData,
                    tooLowAllowance,
                );
                break;

            case AllowanceAmountScenario.Exact:
                const exactAllowance = makerFee;
                await this.assetWrapper.setProxyAllowanceAsync(
                    signedOrder.makerAddress,
                    this.zrxAssetData,
                    exactAllowance,
                );
                break;

            case AllowanceAmountScenario.Unlimited:
                await this.assetWrapper.setProxyAllowanceAsync(
                    signedOrder.makerAddress,
                    this.zrxAssetData,
                    constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
                );
                break;

            default:
                throw errorUtils.spawnSwitchErr(
                    'makerStateScenario.zrxFeeAllowance',
                    makerStateScenario.zrxFeeAllowance,
                );
        }

        switch (takerStateScenario.traderAssetBalance) {
            case BalanceAmountScenario.Higher:
                break; // Noop since this is already the default

            case BalanceAmountScenario.TooLow:
                if (takerAssetFillAmount.eq(0)) {
                    throw new Error(`Cannot set takerAssetBalanceOfTaker TooLow if takerAssetFillAmount is 0`);
                }
                const tooLowBalance = takerAssetFillAmount.minus(1);
                await this.assetWrapper.setBalanceAsync(this.takerAddress, signedOrder.takerAssetData, tooLowBalance);
                break;

            case BalanceAmountScenario.Exact:
                const exactBalance = takerAssetFillAmount;
                await this.assetWrapper.setBalanceAsync(this.takerAddress, signedOrder.takerAssetData, exactBalance);
                break;

            default:
                throw errorUtils.spawnSwitchErr(
                    'takerStateScenario.traderAssetBalance',
                    takerStateScenario.traderAssetBalance,
                );
        }

        const takerFee = orderUtils.getPartialAmount(
            takerAssetFillAmount,
            signedOrder.takerAssetAmount,
            signedOrder.takerFee,
        );
        switch (takerStateScenario.zrxFeeBalance) {
            case BalanceAmountScenario.Higher:
                break; // Noop since this is already the default

            case BalanceAmountScenario.TooLow:
                if (takerFee.eq(0)) {
                    throw new Error(`Cannot set zrxAssetBalanceOfTaker TooLow if takerFee is 0`);
                }
                const tooLowBalance = takerFee.minus(1);
                await this.assetWrapper.setBalanceAsync(this.takerAddress, this.zrxAssetData, tooLowBalance);
                break;

            case BalanceAmountScenario.Exact:
                const exactBalance = takerFee;
                await this.assetWrapper.setBalanceAsync(this.takerAddress, this.zrxAssetData, exactBalance);
                break;

            default:
                throw errorUtils.spawnSwitchErr('takerStateScenario.zrxFeeBalance', takerStateScenario.zrxFeeBalance);
        }

        switch (takerStateScenario.traderAssetAllowance) {
            case AllowanceAmountScenario.Higher:
                break; // Noop since this is already the default

            case AllowanceAmountScenario.TooLow:
                const tooLowAllowance = takerAssetFillAmount.minus(1);
                await this.assetWrapper.setProxyAllowanceAsync(
                    this.takerAddress,
                    signedOrder.takerAssetData,
                    tooLowAllowance,
                );
                break;

            case AllowanceAmountScenario.Exact:
                const exactAllowance = takerAssetFillAmount;
                await this.assetWrapper.setProxyAllowanceAsync(
                    this.takerAddress,
                    signedOrder.takerAssetData,
                    exactAllowance,
                );
                break;

            case AllowanceAmountScenario.Unlimited:
                await this.assetWrapper.setProxyAllowanceAsync(
                    this.takerAddress,
                    signedOrder.takerAssetData,
                    constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
                );
                break;

            default:
                throw errorUtils.spawnSwitchErr(
                    'takerStateScenario.traderAssetAllowance',
                    takerStateScenario.traderAssetAllowance,
                );
        }

        switch (takerStateScenario.zrxFeeAllowance) {
            case AllowanceAmountScenario.Higher:
                break; // Noop since this is already the default

            case AllowanceAmountScenario.TooLow:
                const tooLowAllowance = takerFee.minus(1);
                await this.assetWrapper.setProxyAllowanceAsync(this.takerAddress, this.zrxAssetData, tooLowAllowance);
                break;

            case AllowanceAmountScenario.Exact:
                const exactAllowance = takerFee;
                await this.assetWrapper.setProxyAllowanceAsync(this.takerAddress, this.zrxAssetData, exactAllowance);
                break;

            case AllowanceAmountScenario.Unlimited:
                await this.assetWrapper.setProxyAllowanceAsync(
                    this.takerAddress,
                    this.zrxAssetData,
                    constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
                );
                break;

            default:
                throw errorUtils.spawnSwitchErr(
                    'takerStateScenario.zrxFeeAllowance',
                    takerStateScenario.zrxFeeAllowance,
                );
        }
    }
} // tslint:disable:max-file-line-count
