import { DevUtilsContract } from '@0x/contracts-dev-utils';
import { blockchainTests, constants, describe, provider } from '@0x/contracts-test-utils';
import * as _ from 'lodash';

import {
    FillOrderCombinatorialUtils,
    fillOrderCombinatorialUtilsFactoryAsync,
} from './utils/fill_order_combinatorial_utils';
import {
    AllowanceAmountScenario,
    AssetDataScenario,
    BalanceAmountScenario,
    ExpirationTimeSecondsScenario,
    FeeAssetDataScenario,
    FeeRecipientAddressScenario,
    OrderAssetAmountScenario,
    TakerAssetFillAmountScenario,
    TakerScenario,
} from './utils/fill_order_scenarios';

const defaultFillScenario = {
    orderScenario: {
        takerScenario: TakerScenario.Unspecified,
        feeRecipientScenario: FeeRecipientAddressScenario.EthUserAddress,
        makerAssetAmountScenario: OrderAssetAmountScenario.Large,
        takerAssetAmountScenario: OrderAssetAmountScenario.Large,
        makerFeeScenario: OrderAssetAmountScenario.Large,
        takerFeeScenario: OrderAssetAmountScenario.Large,
        expirationTimeSecondsScenario: ExpirationTimeSecondsScenario.InFuture,
        makerAssetDataScenario: AssetDataScenario.ERC20EighteenDecimals,
        takerAssetDataScenario: AssetDataScenario.ERC20EighteenDecimals,
        makerFeeAssetDataScenario: FeeAssetDataScenario.ERC20EighteenDecimals,
        takerFeeAssetDataScenario: FeeAssetDataScenario.ERC20EighteenDecimals,
    },
    takerAssetFillAmountScenario: TakerAssetFillAmountScenario.LessThanTakerAssetAmount,
    makerStateScenario: {
        traderAssetBalance: BalanceAmountScenario.Higher,
        traderAssetAllowance: AllowanceAmountScenario.Unlimited,
        feeBalance: BalanceAmountScenario.Higher,
        feeAllowance: AllowanceAmountScenario.Unlimited,
    },
    takerStateScenario: {
        traderAssetBalance: BalanceAmountScenario.Higher,
        traderAssetAllowance: AllowanceAmountScenario.Unlimited,
        feeBalance: BalanceAmountScenario.Higher,
        feeAllowance: AllowanceAmountScenario.Unlimited,
    },
};

blockchainTests.resets('FillOrder Tests', ({ web3Wrapper, txDefaults }) => {
    let fillOrderCombinatorialUtils: FillOrderCombinatorialUtils;
    let devUtils: DevUtilsContract;

    before(async () => {
        devUtils = new DevUtilsContract(constants.NULL_ADDRESS, provider);
        fillOrderCombinatorialUtils = await fillOrderCombinatorialUtilsFactoryAsync(web3Wrapper, txDefaults);
    });

    describe('Fill tests', () => {
        it('should transfer the correct amounts when makerAssetAmount > takerAssetAmount', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    takerAssetAmountScenario: OrderAssetAmountScenario.Small,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioSuccessAsync(fillScenario, devUtils);
        });

        it('should transfer the correct amounts when makerAssetAmount < takerAssetAmount', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    makerAssetAmountScenario: OrderAssetAmountScenario.Small,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioSuccessAsync(fillScenario, devUtils);
        });

        it('should transfer the correct amounts when makerAssetAmount < takerAssetAmount with zero decimals', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    makerAssetAmountScenario: OrderAssetAmountScenario.Small,
                    makerAssetDataScenario: AssetDataScenario.ERC20ZeroDecimals,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioSuccessAsync(fillScenario, devUtils);
        });

        it('should transfer the correct amounts when taker is specified and order is claimed by taker', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    takerScenario: TakerScenario.CorrectlySpecified,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioSuccessAsync(fillScenario, devUtils);
        });

        it('should transfer the correct amounts maker == feeRecipient', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    feeRecipientScenario: FeeRecipientAddressScenario.MakerAddress,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioSuccessAsync(fillScenario, devUtils);
        });

        it('should transfer the correct amounts maker == feeRecipient and makerFeeAsset == takerAsset', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    feeRecipientScenario: FeeRecipientAddressScenario.MakerAddress,
                    makerFeeAssetDataScenario: FeeAssetDataScenario.TakerToken,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioSuccessAsync(fillScenario, devUtils);
        });

        it('should transfer the correct amounts taker == feeRecipient', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    feeRecipientScenario: FeeRecipientAddressScenario.TakerAddress,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioSuccessAsync(fillScenario, devUtils);
        });

        it('should transfer the correct amounts taker == feeRecipient and takerFeeAsset == makerAsset', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    feeRecipientScenario: FeeRecipientAddressScenario.TakerAddress,
                    takerFeeAssetDataScenario: FeeAssetDataScenario.MakerToken,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioSuccessAsync(fillScenario, devUtils);
        });

        it('should fill remaining value if takerAssetFillAmount > remaining takerAssetAmount', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                takerAssetFillAmountScenario: TakerAssetFillAmountScenario.GreaterThanTakerAssetAmount,
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioSuccessAsync(fillScenario, devUtils);
        });

        it('should revert when taker is specified and order is claimed by other', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    takerScenario: TakerScenario.IncorrectlySpecified,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioFailureAsync(fillScenario, devUtils);
        });

        it('should revert if makerAssetAmount is 0', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    makerAssetAmountScenario: OrderAssetAmountScenario.Zero,
                },
                takerAssetFillAmountScenario: TakerAssetFillAmountScenario.GreaterThanTakerAssetAmount,
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioFailureAsync(fillScenario, devUtils);
        });

        it('should revert if takerAssetAmount is 0', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    takerAssetAmountScenario: OrderAssetAmountScenario.Zero,
                },
                takerAssetFillAmountScenario: TakerAssetFillAmountScenario.GreaterThanTakerAssetAmount,
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioFailureAsync(fillScenario, devUtils);
        });

        it('should revert if an order is expired', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    expirationTimeSecondsScenario: ExpirationTimeSecondsScenario.InPast,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioFailureAsync(fillScenario, devUtils);
        });
    });

    describe('ERC20', () => {
        const assetCombinations = getAllPossiblePairs([
            AssetDataScenario.ERC20ZeroDecimals,
            AssetDataScenario.ERC20FiveDecimals,
            AssetDataScenario.ERC20EighteenDecimals,
        ]);
        for (const [makerAsset, takerAsset] of assetCombinations) {
            it(`should transfer correct amounts between ${makerAsset} and ${takerAsset}`, async () => {
                const fillScenario = {
                    ...defaultFillScenario,
                    orderScenario: {
                        ...defaultFillScenario.orderScenario,
                        makerAssetDataScenario: makerAsset,
                        takerAssetDataScenario: takerAsset,
                    },
                };
                await fillOrderCombinatorialUtils.testFillOrderScenarioSuccessAsync(fillScenario, devUtils);
            });
        }
        it('should be able to pay maker fee with taker asset', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    makerFeeAssetDataScenario: FeeAssetDataScenario.TakerToken,
                },
                takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyTakerAssetAmount,
                makerStateScenario: {
                    ...defaultFillScenario.makerStateScenario,
                    feeBalance: BalanceAmountScenario.Zero,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioSuccessAsync(fillScenario, devUtils);
        });

        it('should be able to pay taker fee with maker asset', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    takerFeeAssetDataScenario: FeeAssetDataScenario.MakerToken,
                },
                takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyTakerAssetAmount,
                takerStateScenario: {
                    ...defaultFillScenario.takerStateScenario,
                    feeBalance: BalanceAmountScenario.Zero,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioSuccessAsync(fillScenario, devUtils);
        });

        it('should not be able to pay maker fee with maker asset if none is left over (double-spend)', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    makerFeeAssetDataScenario: FeeAssetDataScenario.MakerToken,
                },
                takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyTakerAssetAmount,
                makerStateScenario: {
                    ...defaultFillScenario.makerStateScenario,
                    traderAssetBalance: BalanceAmountScenario.Exact,
                    feeBalance: BalanceAmountScenario.Zero,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioFailureAsync(fillScenario, devUtils);
        });

        it('should not be able to pay taker fee with taker asset if none is left over (double-spend)', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    takerFeeAssetDataScenario: FeeAssetDataScenario.TakerToken,
                },
                takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyTakerAssetAmount,
                takerStateScenario: {
                    ...defaultFillScenario.takerStateScenario,
                    traderAssetBalance: BalanceAmountScenario.Exact,
                    feeBalance: BalanceAmountScenario.Zero,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioFailureAsync(fillScenario, devUtils);
        });

        it('should be able to pay taker fee with maker asset', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    takerFeeAssetDataScenario: FeeAssetDataScenario.MakerToken,
                },
                takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyTakerAssetAmount,
                takerStateScenario: {
                    ...defaultFillScenario.takerStateScenario,
                    feeBalance: BalanceAmountScenario.Zero,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioSuccessAsync(fillScenario, devUtils);
        });

        it('should revert if maker balance is too low to fill order', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                makerStateScenario: {
                    ...defaultFillScenario.makerStateScenario,
                    traderAssetBalance: BalanceAmountScenario.TooLow,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioFailureAsync(fillScenario, devUtils);
        });

        it('should revert if taker balance is too low to fill order', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                takerStateScenario: {
                    ...defaultFillScenario.makerStateScenario,
                    traderAssetBalance: BalanceAmountScenario.TooLow,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioFailureAsync(fillScenario, devUtils);
        });

        it('should revert if maker allowances are too low to fill order', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                makerStateScenario: {
                    ...defaultFillScenario.makerStateScenario,
                    traderAssetAllowance: AllowanceAmountScenario.TooLow,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioFailureAsync(fillScenario, devUtils);
        });

        it('should revert if taker allowances are too low to fill order', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                takerStateScenario: {
                    ...defaultFillScenario.makerStateScenario,
                    traderAssetAllowance: AllowanceAmountScenario.TooLow,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioFailureAsync(fillScenario, devUtils);
        });

        it('should revert if maker fee balance is too low to fill order', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                makerStateScenario: {
                    ...defaultFillScenario.makerStateScenario,
                    feeBalance: BalanceAmountScenario.TooLow,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioFailureAsync(fillScenario, devUtils);
        });

        it('should revert if taker fee balance is too low to fill order', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                takerStateScenario: {
                    ...defaultFillScenario.makerStateScenario,
                    feeBalance: BalanceAmountScenario.TooLow,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioFailureAsync(fillScenario, devUtils);
        });

        it('should revert if maker fee allowances are too low to fill order', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                makerStateScenario: {
                    ...defaultFillScenario.makerStateScenario,
                    feeAllowance: AllowanceAmountScenario.TooLow,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioFailureAsync(fillScenario, devUtils);
        });

        it('should revert if taker fee allowances are too low to fill order', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                takerStateScenario: {
                    ...defaultFillScenario.makerStateScenario,
                    feeAllowance: AllowanceAmountScenario.TooLow,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioFailureAsync(fillScenario, devUtils);
        });
    });

    describe('ERC721', () => {
        it('should be able to pay maker fee with taker ERC721', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    takerAssetDataScenario: AssetDataScenario.ERC721,
                    makerFeeAssetDataScenario: FeeAssetDataScenario.TakerToken,
                },
                takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyTakerAssetAmount,
                makerStateScenario: {
                    ...defaultFillScenario.makerStateScenario,
                    feeBalance: BalanceAmountScenario.Zero,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioSuccessAsync(fillScenario, devUtils);
        });

        it('should be able to pay taker fee with maker ERC721', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    makerAssetDataScenario: AssetDataScenario.ERC721,
                    takerFeeAssetDataScenario: FeeAssetDataScenario.MakerToken,
                },
                takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyTakerAssetAmount,
                takerStateScenario: {
                    ...defaultFillScenario.takerStateScenario,
                    feeBalance: BalanceAmountScenario.Zero,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioSuccessAsync(fillScenario, devUtils);
        });

        it('should not be able to pay maker fee with maker ERC721 (double-spend)', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    makerAssetDataScenario: AssetDataScenario.ERC721,
                    makerFeeAssetDataScenario: FeeAssetDataScenario.MakerToken,
                },
                takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyTakerAssetAmount,
                makerStateScenario: {
                    ...defaultFillScenario.makerStateScenario,
                    feeBalance: BalanceAmountScenario.Zero,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioFailureAsync(fillScenario, devUtils);
        });

        it('should be able to pay taker fee with taker ERC721 (double-spend)', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    takerAssetDataScenario: AssetDataScenario.ERC721,
                    takerFeeAssetDataScenario: FeeAssetDataScenario.TakerToken,
                },
                takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyTakerAssetAmount,
                takerStateScenario: {
                    ...defaultFillScenario.takerStateScenario,
                    feeBalance: BalanceAmountScenario.Zero,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioFailureAsync(fillScenario, devUtils);
        });
    });

    describe('ERC1155', () => {
        const assetTypes = [AssetDataScenario.ERC1155Fungible, AssetDataScenario.ERC1155NonFungible];
        for (const assetType of assetTypes) {
            describe(_.startCase(_.toLower((/ERC1155(.+)/.exec(assetType) as string[])[1])), () => {
                it('should be able to pay maker fee with taker asset', async () => {
                    const fillScenario = {
                        ...defaultFillScenario,
                        orderScenario: {
                            ...defaultFillScenario.orderScenario,
                            takerAssetDataScenario: assetType,
                            makerFeeAssetDataScenario: FeeAssetDataScenario.TakerToken,
                        },
                        takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyTakerAssetAmount,
                        makerStateScenario: {
                            ...defaultFillScenario.makerStateScenario,
                            feeBalance: BalanceAmountScenario.Zero,
                        },
                    };
                    await fillOrderCombinatorialUtils.testFillOrderScenarioSuccessAsync(fillScenario, devUtils);
                });

                it('should be able to pay taker fee with maker asset', async () => {
                    const fillScenario = {
                        ...defaultFillScenario,
                        orderScenario: {
                            ...defaultFillScenario.orderScenario,
                            makerAssetDataScenario: assetType,
                            takerFeeAssetDataScenario: FeeAssetDataScenario.MakerToken,
                        },
                        takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyTakerAssetAmount,
                        takerStateScenario: {
                            ...defaultFillScenario.takerStateScenario,
                            feeBalance: BalanceAmountScenario.Zero,
                        },
                    };
                    await fillOrderCombinatorialUtils.testFillOrderScenarioSuccessAsync(fillScenario, devUtils);
                });

                it('should not be able to pay maker fee with maker asset if not enough left (double-spend)', async () => {
                    const fillScenario = {
                        ...defaultFillScenario,
                        orderScenario: {
                            ...defaultFillScenario.orderScenario,
                            makerAssetDataScenario: assetType,
                            makerFeeAssetDataScenario: FeeAssetDataScenario.MakerToken,
                        },
                        takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyTakerAssetAmount,
                        makerStateScenario: {
                            ...defaultFillScenario.makerStateScenario,
                            traderAssetBalance: BalanceAmountScenario.Exact,
                            feeBalance: BalanceAmountScenario.Zero,
                        },
                    };
                    await fillOrderCombinatorialUtils.testFillOrderScenarioFailureAsync(fillScenario, devUtils);
                });

                it('should be able to pay taker fee with taker asset if not enough left (double-spend)', async () => {
                    const fillScenario = {
                        ...defaultFillScenario,
                        orderScenario: {
                            ...defaultFillScenario.orderScenario,
                            takerAssetDataScenario: assetType,
                            takerFeeAssetDataScenario: FeeAssetDataScenario.TakerToken,
                        },
                        takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyTakerAssetAmount,
                        takerStateScenario: {
                            ...defaultFillScenario.takerStateScenario,
                            traderAssetBalance: BalanceAmountScenario.Exact,
                            feeBalance: BalanceAmountScenario.Zero,
                        },
                    };
                    await fillOrderCombinatorialUtils.testFillOrderScenarioFailureAsync(fillScenario, devUtils);
                });
            });
        }
    });

    describe('MultiAssetProxy', () => {
        it('should be able to pay maker fee with taker MAP', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    takerAssetDataScenario: AssetDataScenario.MultiAssetERC20,
                    makerFeeAssetDataScenario: FeeAssetDataScenario.TakerToken,
                },
                takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyTakerAssetAmount,
                makerStateScenario: {
                    ...defaultFillScenario.makerStateScenario,
                    feeBalance: BalanceAmountScenario.Zero,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioSuccessAsync(fillScenario, devUtils);
        });

        it('should be able to pay taker fee with maker MAP', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    makerAssetDataScenario: AssetDataScenario.MultiAssetERC20,
                    takerFeeAssetDataScenario: FeeAssetDataScenario.MakerToken,
                },
                takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyTakerAssetAmount,
                takerStateScenario: {
                    ...defaultFillScenario.takerStateScenario,
                    feeBalance: BalanceAmountScenario.Zero,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioSuccessAsync(fillScenario, devUtils);
        });

        it('should not be able to pay maker fee with maker MAP (double-spend)', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    makerAssetDataScenario: AssetDataScenario.MultiAssetERC20,
                    makerFeeAssetDataScenario: FeeAssetDataScenario.MakerToken,
                },
                takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyTakerAssetAmount,
                makerStateScenario: {
                    ...defaultFillScenario.makerStateScenario,
                    feeBalance: BalanceAmountScenario.Zero,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioFailureAsync(fillScenario, devUtils);
        });

        it('should be able to pay taker fee with taker MAP (double-spend)', async () => {
            const fillScenario = {
                ...defaultFillScenario,
                orderScenario: {
                    ...defaultFillScenario.orderScenario,
                    takerAssetDataScenario: AssetDataScenario.MultiAssetERC20,
                    takerFeeAssetDataScenario: FeeAssetDataScenario.TakerToken,
                },
                takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyTakerAssetAmount,
                takerStateScenario: {
                    ...defaultFillScenario.takerStateScenario,
                    feeBalance: BalanceAmountScenario.Zero,
                },
            };
            await fillOrderCombinatorialUtils.testFillOrderScenarioFailureAsync(fillScenario, devUtils);
        });
    });

    describe('Maker/taker asset combinations', () => {
        const assetDataScenarios = [
            AssetDataScenario.ERC20EighteenDecimals,
            AssetDataScenario.ERC721,
            AssetDataScenario.ERC1155Fungible,
            AssetDataScenario.ERC1155NonFungible,
            AssetDataScenario.MultiAssetERC20,
        ];
        for (const [makerAsset, takerAsset] of getAllPossiblePairs(assetDataScenarios)) {
            it(`should successfully exchange ${makerAsset} for ${takerAsset}`, async () => {
                const fillScenario = {
                    ...defaultFillScenario,
                    orderScenario: {
                        ...defaultFillScenario.orderScenario,
                        makerAssetDataScenario: makerAsset,
                        takerAssetDataScenario: takerAsset,
                    },
                    takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyTakerAssetAmount,
                };
                await fillOrderCombinatorialUtils.testFillOrderScenarioSuccessAsync(fillScenario, devUtils);
            });
        }
    });

    describe('Maker/taker fee asset combinations', () => {
        const feeAssetPairs = getAllPossiblePairs([
            FeeAssetDataScenario.ERC20EighteenDecimals,
            FeeAssetDataScenario.ERC721,
            FeeAssetDataScenario.ERC1155Fungible,
            FeeAssetDataScenario.ERC1155NonFungible,
            FeeAssetDataScenario.MultiAssetERC20,
        ]);
        for (const [makerFeeAsset, takerFeeAsset] of feeAssetPairs) {
            it(`should successfully pay maker fee ${makerFeeAsset} and taker fee ${takerFeeAsset}`, async () => {
                const fillScenario = {
                    ...defaultFillScenario,
                    orderScenario: {
                        ...defaultFillScenario.orderScenario,
                        makerFeeAssetDataScenario: makerFeeAsset,
                        takerFeeAssetDataScenario: takerFeeAsset,
                    },
                    takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyTakerAssetAmount,
                };
                await fillOrderCombinatorialUtils.testFillOrderScenarioSuccessAsync(fillScenario, devUtils);
            });
        }
    });

    describe.optional('Combinatorially generated fills orders', () => {
        const allFillScenarios = FillOrderCombinatorialUtils.generateFillOrderCombinations();
        for (const fillScenario of allFillScenarios) {
            const description = `Combinatorial OrderFill: ${JSON.stringify(fillScenario)}`;
            it(description, async () => {
                await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(fillScenario, devUtils);
            });
        }
    });
});

function getAllPossiblePairs<T>(choices: T[]): Array<[T, T]> {
    const pairs: Array<[T, T]> = [];
    for (const i of _.times(choices.length)) {
        for (const j of _.times(choices.length)) {
            pairs.push([choices[i], choices[j]]);
        }
    }
    return pairs;
}
// tslint:disable: max-file-line-count
