import { DummyERC20TokenContract } from '@0x/contracts-erc20';
import { ExchangeRevertErrors } from '@0x/contracts-exchange';
import { ReferenceFunctions as LibReferenceFunctions } from '@0x/contracts-exchange-libs';
import { blockchainTests, constants, expect, orderHashUtils, toBaseUnitAmount } from '@0x/contracts-test-utils';
import { OrderStatus } from '@0x/types';
import { BigNumber } from '@0x/utils';

import { Actor } from '../framework/actors/base';
import { Maker } from '../framework/actors/maker';
import { actorAddressesByName } from '../framework/actors/utils';
import { BlockchainBalanceStore } from '../framework/balances/blockchain_balance_store';
import { TokenIds } from '../framework/balances/types';
import { DeploymentManager } from '../framework/deployment_manager';

import { MatchOrderTester, TestMatchOrdersArgs, testMatchOrdersAsync } from './match_order_tester';

const { isRoundingErrorCeil, isRoundingErrorFloor } = LibReferenceFunctions;

blockchainTests.resets('matchOrdersWithMaximalFill integration tests', env => {
    // The fee recipient addresses.
    let feeRecipientLeft: Actor;
    let feeRecipientRight: Actor;

    // The address that should be responsible for matching orders.
    let matcher: Actor;

    // Market makers who have opposite maker and taker assets.
    let makerLeft: Maker;
    let makerRight: Maker;

    // The addresses of important assets for testing.
    let makerAssetLeft: DummyERC20TokenContract;
    let makerAssetRight: DummyERC20TokenContract;
    let feeAsset: DummyERC20TokenContract;

    let makerAssetDataLeft: string;
    let makerAssetDataRight: string;
    let feeAssetData: string;

    let deployment: DeploymentManager;
    let matchOrderTester: MatchOrderTester;
    let leftId: BigNumber;
    let rightId: BigNumber;

    let defaultMatchOrdersArgs: TestMatchOrdersArgs;

    before(async () => {
        deployment = await DeploymentManager.deployAsync(env, {
            numErc20TokensToDeploy: 3,
            numErc721TokensToDeploy: 1,
            numErc1155TokensToDeploy: 1,
        });

        makerAssetLeft = deployment.tokens.erc20[0];
        makerAssetRight = deployment.tokens.erc20[1];
        feeAsset = deployment.tokens.erc20[2];

        // Create the fee recipient actors.
        feeRecipientLeft = new Actor({
            name: 'left fee recipient',
            deployment,
        });
        feeRecipientRight = new Actor({
            name: 'right fee recipient',
            deployment,
        });

        // Encode the asset data.
        makerAssetDataLeft = deployment.assetDataEncoder
            .ERC20Token(makerAssetLeft.address)
            .getABIEncodedTransactionData();
        makerAssetDataRight = deployment.assetDataEncoder
            .ERC20Token(makerAssetRight.address)
            .getABIEncodedTransactionData();
        feeAssetData = deployment.assetDataEncoder.ERC20Token(feeAsset.address).getABIEncodedTransactionData();

        // Create two market makers with compatible orders for matching.
        makerLeft = new Maker({
            name: 'left maker',
            deployment,
            orderConfig: {
                makerAssetData: makerAssetDataLeft,
                takerAssetData: makerAssetDataRight,
                makerFeeAssetData: feeAssetData,
                takerFeeAssetData: feeAssetData,
                feeRecipientAddress: feeRecipientLeft.address,
            },
        });
        makerRight = new Maker({
            name: 'right maker',
            deployment,
            orderConfig: {
                makerAssetData: makerAssetDataRight,
                takerAssetData: makerAssetDataLeft,
                makerFeeAssetData: feeAssetData,
                takerFeeAssetData: feeAssetData,
                feeRecipientAddress: feeRecipientRight.address,
            },
        });

        // Create a matcher.
        matcher = new Actor({
            name: 'matcher',
            deployment,
        });

        // Configure the appropriate actors with initial balances.
        await Promise.all([
            ...deployment.tokens.erc20.map(async token => makerLeft.configureERC20TokenAsync(token)),
            ...deployment.tokens.erc20.map(async token => makerRight.configureERC20TokenAsync(token)),
            makerLeft.configureERC20TokenAsync(deployment.tokens.weth, deployment.staking.stakingProxy.address),
            makerRight.configureERC20TokenAsync(deployment.tokens.weth, deployment.staking.stakingProxy.address),
            matcher.configureERC20TokenAsync(feeAsset),
            matcher.configureERC20TokenAsync(deployment.tokens.weth, deployment.staking.stakingProxy.address),
            feeRecipientLeft.configureERC20TokenAsync(feeAsset),
            feeRecipientLeft.configureERC20TokenAsync(deployment.tokens.weth, deployment.staking.stakingProxy.address),
            feeRecipientRight.configureERC20TokenAsync(feeAsset),
            feeRecipientRight.configureERC20TokenAsync(deployment.tokens.weth, deployment.staking.stakingProxy.address),
        ]);

        leftId = await makerLeft.configureERC1155TokenAsync(deployment.tokens.erc1155[0]);
        [rightId] = await makerRight.configureERC721TokenAsync(deployment.tokens.erc721[0]);

        const tokenIds: TokenIds = { erc721: {}, erc1155: {} };
        tokenIds.erc1155[deployment.tokens.erc1155[0].address] = { fungible: [leftId], nonFungible: [] };
        tokenIds.erc721[deployment.tokens.erc721[0].address] = [rightId];

        const blockchainBalanceStore = new BlockchainBalanceStore(
            {
                ...actorAddressesByName([feeRecipientLeft, feeRecipientRight, makerLeft, makerRight, matcher]),
                stakingProxy: deployment.staking.stakingProxy.address,
            },
            {
                erc20: {
                    makerTokenLeft: deployment.tokens.erc20[0],
                    makerTokenRight: deployment.tokens.erc20[1],
                    feeToken: deployment.tokens.erc20[2],
                    weth: deployment.tokens.weth,
                },
                erc721: {
                    nft: deployment.tokens.erc721[0],
                },
                erc1155: {
                    fungible: deployment.tokens.erc1155[0],
                },
            },
            tokenIds,
        );

        matchOrderTester = new MatchOrderTester(deployment, blockchainBalanceStore);

        defaultMatchOrdersArgs = {
            env,
            matchOrderTester,
            makerLeft,
            makerRight,
            leftOrder: constants.STATIC_ORDER_PARAMS,
            rightOrder: constants.STATIC_ORDER_PARAMS,
            matcherAddress: matcher.address,
            expectedTransferAmounts: {},
            withMaximalFill: true,
        };
    });

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

    describe('matchOrdersWithMaximalFill', () => {
        // TODO: These tests should be converted to unit tests.
        it('should transfer correct amounts when right order is fully filled and values pass isRoundingErrorCeil but fail isRoundingErrorFloor', async () => {
            // Create orders to match
            const signedOrderLeft = await makerLeft.signOrderAsync({
                makerAssetAmount: new BigNumber(17),
                takerAssetAmount: new BigNumber(98),
            });
            const signedOrderRight = await makerRight.signOrderAsync({
                makerAssetAmount: new BigNumber(75),
                takerAssetAmount: new BigNumber(13),
            });
            // Assert is rounding error ceil & not rounding error floor
            // These assertions are taken from MixinMatchOrders::calculateMatchedFillResults
            // The rounding error is derived computating how much the left maker will sell.
            const numerator = signedOrderLeft.makerAssetAmount;
            const denominator = signedOrderLeft.takerAssetAmount;
            const target = signedOrderRight.makerAssetAmount;
            const _isRoundingErrorCeil = isRoundingErrorCeil(numerator, denominator, target);
            expect(_isRoundingErrorCeil).to.be.true();
            const _isRoundingErrorFloor = isRoundingErrorFloor(numerator, denominator, target);
            expect(_isRoundingErrorFloor).to.be.false();
            // Match signedOrderLeft with signedOrderRight
            // Note that the left maker received a slightly better sell price.
            // This is intentional; see note in MixinMatchOrders.calculateMatchedFillResults.
            // Because the left maker received a slightly more favorable sell price, the fee
            // paid by the left taker is slightly higher than that paid by the left maker.
            // Fees can be thought of as a tax paid by the seller, derived from the sale price.
            const expectedTransferAmounts = {
                // Left Maker
                leftMakerAssetSoldByLeftMakerAmount: new BigNumber(13),
                leftMakerFeeAssetPaidByLeftMakerAmount: toBaseUnitAmount('76.4705882352941176', 16), // 76.47%
                // Right Maker
                rightMakerAssetSoldByRightMakerAmount: new BigNumber(75),
                rightMakerFeeAssetPaidByRightMakerAmount: toBaseUnitAmount(100, 16), // 100%
                // Taker
                leftTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount('76.5306122448979591', 16), // 76.53%
                rightTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                leftProtocolFeePaidByTakerInEthAmount: DeploymentManager.protocolFee,
                rightProtocolFeePaidByTakerInEthAmount: DeploymentManager.protocolFee,
            };
            await matchOrderTester.matchOrdersAndAssertEffectsAsync(
                {
                    leftOrder: signedOrderLeft,
                    rightOrder: signedOrderRight,
                },
                expectedTransferAmounts,
                matcher.address,
                DeploymentManager.protocolFee.times(2),
                true,
            );
        });

        // TODO: These tests should be converted to unit tests.
        it('should transfer correct amounts when left order is fully filled and values pass isRoundingErrorCeil and isRoundingErrorFloor', async () => {
            // Create orders to match
            const signedOrderLeft = await makerLeft.signOrderAsync({
                makerAssetAmount: new BigNumber(15),
                takerAssetAmount: new BigNumber(90),
            });
            const signedOrderRight = await makerRight.signOrderAsync({
                makerAssetAmount: new BigNumber(196),
                takerAssetAmount: new BigNumber(28),
            });

            // Assert is rounding error floor
            // These assertions are taken from MixinMatchOrders::calculateMatchedFillResults
            // The rounding error is derived computating how much the right maker will buy.
            const numerator = signedOrderRight.makerAssetAmount;
            const denominator = signedOrderRight.takerAssetAmount;
            const target = signedOrderLeft.makerAssetAmount;
            const _isRoundingErrorCeil = isRoundingErrorCeil(numerator, denominator, target);
            expect(_isRoundingErrorCeil).to.be.false();
            const _isRoundingErrorFloor = isRoundingErrorFloor(numerator, denominator, target);
            expect(_isRoundingErrorFloor).to.be.false();

            // Match signedOrderLeft with signedOrderRight
            // Note that the right maker received a slightly better purchase price.
            // This is intentional; see note in MixinMatchOrders.calculateMatchedFillResults.
            // Because the right maker received a slightly more favorable buy price, the fee
            // paid by the right taker is slightly higher than that paid by the right maker.
            // Fees can be thought of as a tax paid by the seller, derived from the sale price.
            const expectedTransferAmounts = {
                // Left Maker
                leftMakerAssetSoldByLeftMakerAmount: new BigNumber(15),
                leftMakerFeeAssetPaidByLeftMakerAmount: toBaseUnitAmount(100, 16), // 100%
                rightMakerAssetBoughtByLeftMakerAmount: new BigNumber(90),
                // Right Maker
                leftMakerAssetBoughtByRightMakerAmount: new BigNumber(15),
                rightMakerAssetSoldByRightMakerAmount: new BigNumber(105),
                rightMakerFeeAssetPaidByRightMakerAmount: toBaseUnitAmount('53.5714285714285714', 16), // 53.57%
                // Taker
                rightMakerAssetReceivedByTakerAmount: new BigNumber(15),
                leftTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                rightTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount('53.5714285714285714', 16), // 53.57%
                leftProtocolFeePaidByTakerInEthAmount: DeploymentManager.protocolFee,
                rightProtocolFeePaidByTakerInEthAmount: DeploymentManager.protocolFee,
            };
            await matchOrderTester.matchOrdersAndAssertEffectsAsync(
                {
                    leftOrder: signedOrderLeft,
                    rightOrder: signedOrderRight,
                },
                expectedTransferAmounts,
                matcher.address,
                DeploymentManager.protocolFee.times(2),
                true,
            );
        });

        it('should transfer correct amounts when left order is fully filled', async () => {
            await testMatchOrdersAsync({
                ...defaultMatchOrdersArgs,
                leftOrder: {
                    makerAssetAmount: new BigNumber(16),
                    takerAssetAmount: new BigNumber(22),
                },
                rightOrder: {
                    makerAssetAmount: new BigNumber(87),
                    takerAssetAmount: new BigNumber(48),
                },
                expectedTransferAmounts: {
                    // Left Maker
                    leftMakerAssetSoldByLeftMakerAmount: new BigNumber(16),
                    leftMakerFeeAssetPaidByLeftMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    rightMakerAssetBoughtByLeftMakerAmount: new BigNumber(22),
                    // Right Maker
                    rightMakerAssetSoldByRightMakerAmount: new BigNumber(29),
                    leftMakerAssetBoughtByRightMakerAmount: new BigNumber(16),
                    rightMakerFeeAssetPaidByRightMakerAmount: toBaseUnitAmount('33.3333333333333333', 16), // 33.33%
                    // Taker
                    rightMakerAssetReceivedByTakerAmount: new BigNumber(7),
                    leftTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                    rightTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount('33.3333333333333333', 16), // 33.33%
                },
            });
        });

        it('should fully fill both orders and pay out profit in both maker assets', async () => {
            await testMatchOrdersAsync({
                ...defaultMatchOrdersArgs,
                leftOrder: {
                    makerAssetAmount: new BigNumber(7),
                    takerAssetAmount: new BigNumber(4),
                },
                rightOrder: {
                    makerAssetAmount: new BigNumber(8),
                    takerAssetAmount: new BigNumber(6),
                },
                expectedTransferAmounts: {
                    // Left Maker
                    leftMakerAssetSoldByLeftMakerAmount: new BigNumber(7),
                    leftMakerFeeAssetPaidByLeftMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    rightMakerAssetBoughtByLeftMakerAmount: new BigNumber(4), // 100%
                    // Right Maker
                    rightMakerAssetSoldByRightMakerAmount: new BigNumber(8),
                    rightMakerFeeAssetPaidByRightMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    leftMakerAssetBoughtByRightMakerAmount: new BigNumber(6), // 100%
                    // Taker
                    leftMakerAssetReceivedByTakerAmount: new BigNumber(1),
                    rightMakerAssetReceivedByTakerAmount: new BigNumber(4),
                    leftTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                    rightTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                },
            });
        });

        it('should give left maker a better sell price when rounding', async () => {
            await testMatchOrdersAsync({
                ...defaultMatchOrdersArgs,
                leftOrder: {
                    makerAssetAmount: new BigNumber(12),
                    takerAssetAmount: new BigNumber(97),
                },
                rightOrder: {
                    makerAssetAmount: new BigNumber(89),
                    takerAssetAmount: new BigNumber(1),
                },
                expectedTransferAmounts: {
                    // Left Maker
                    leftMakerAssetSoldByLeftMakerAmount: new BigNumber(11),
                    leftMakerFeeAssetPaidByLeftMakerAmount: toBaseUnitAmount('91.6666666666666666', 16), // 91.6%
                    // Right Maker
                    rightMakerAssetSoldByRightMakerAmount: new BigNumber(89),
                    leftMakerAssetBoughtByRightMakerAmount: new BigNumber(1),
                    rightMakerFeeAssetPaidByRightMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Taker
                    leftMakerAssetReceivedByTakerAmount: new BigNumber(10),
                    leftTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount('91.7525773195876288', 16), // 91.75%
                    rightTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                },
            });
        });

        it('should give right maker and right taker a favorable fee price when rounding', async () => {
            await testMatchOrdersAsync({
                ...defaultMatchOrdersArgs,
                leftOrder: {
                    makerAssetAmount: new BigNumber(16),
                    takerAssetAmount: new BigNumber(22),
                },
                rightOrder: {
                    makerAssetAmount: new BigNumber(87),
                    takerAssetAmount: new BigNumber(48),
                    makerFee: new BigNumber(10000),
                    takerFee: new BigNumber(10000),
                },
                expectedTransferAmounts: {
                    // Left Maker
                    leftMakerAssetSoldByLeftMakerAmount: new BigNumber(16),
                    leftMakerFeeAssetPaidByLeftMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    rightMakerAssetBoughtByLeftMakerAmount: new BigNumber(22),
                    // Right Maker
                    rightMakerAssetSoldByRightMakerAmount: new BigNumber(29),
                    leftMakerAssetBoughtByRightMakerAmount: new BigNumber(16),
                    rightMakerFeeAssetPaidByRightMakerAmount: new BigNumber(3333), // 3333.3 repeating rounded down to 3333
                    // Taker
                    rightMakerAssetReceivedByTakerAmount: new BigNumber(7),
                    leftTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                    rightTakerFeeAssetPaidByTakerAmount: new BigNumber(3333), // 3333.3 repeating rounded down to 3333
                },
            });
        });

        it('should give left maker and left taker a favorable fee price when rounding', async () => {
            await testMatchOrdersAsync({
                ...defaultMatchOrdersArgs,
                leftOrder: {
                    makerAssetAmount: new BigNumber(12),
                    takerAssetAmount: new BigNumber(97),
                    makerFee: new BigNumber(10000),
                    takerFee: new BigNumber(10000),
                },
                rightOrder: {
                    makerAssetAmount: new BigNumber(89),
                    takerAssetAmount: new BigNumber(1),
                },
                expectedTransferAmounts: {
                    // Left Maker
                    leftMakerAssetSoldByLeftMakerAmount: new BigNumber(11),
                    leftMakerFeeAssetPaidByLeftMakerAmount: new BigNumber(9166), // 9166.6 rounded down to 9166
                    // Right Maker
                    rightMakerAssetSoldByRightMakerAmount: new BigNumber(89),
                    leftMakerAssetBoughtByRightMakerAmount: new BigNumber(1),
                    rightMakerFeeAssetPaidByRightMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Taker
                    leftMakerAssetReceivedByTakerAmount: new BigNumber(10),
                    leftTakerFeeAssetPaidByTakerAmount: new BigNumber(9175), // 9175.2 rounded down to 9175
                    rightTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                },
            });
        });

        it('should give left maker a better sell price when rounding', async () => {
            await testMatchOrdersAsync({
                ...defaultMatchOrdersArgs,
                leftOrder: {
                    makerAssetAmount: new BigNumber(12),
                    takerAssetAmount: new BigNumber(97),
                },
                rightOrder: {
                    makerAssetAmount: new BigNumber(89),
                    takerAssetAmount: new BigNumber(1),
                },
                expectedTransferAmounts: {
                    // Left Maker
                    leftMakerAssetSoldByLeftMakerAmount: new BigNumber(11),
                    leftMakerFeeAssetPaidByLeftMakerAmount: toBaseUnitAmount('91.6666666666666666', 16), // 91.6%
                    // Right Maker
                    rightMakerAssetSoldByRightMakerAmount: new BigNumber(89),
                    leftMakerAssetBoughtByRightMakerAmount: new BigNumber(1),
                    rightMakerFeeAssetPaidByRightMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Taker
                    leftMakerAssetReceivedByTakerAmount: new BigNumber(10),
                    leftTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount('91.7525773195876288', 16), // 91.75%
                    rightTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                },
            });
        });

        it('should transfer the correct amounts when consecutive calls are used to completely fill the left order', async () => {
            // Create orders to match
            const signedOrderLeft = await makerLeft.signOrderAsync({
                makerAssetAmount: toBaseUnitAmount(50, 18),
                takerAssetAmount: toBaseUnitAmount(100, 18),
            });
            const signedOrderRight = await makerRight.signOrderAsync({
                makerAssetAmount: toBaseUnitAmount(10, 18),
                takerAssetAmount: toBaseUnitAmount(2, 18),
            });

            // Match orders
            const expectedTransferAmounts = {
                // Left Maker
                leftMakerAssetSoldByLeftMakerAmount: toBaseUnitAmount(5, 18),
                leftMakerFeeAssetPaidByLeftMakerAmount: toBaseUnitAmount(10, 16), // 10%
                // Right Maker
                rightMakerAssetSoldByRightMakerAmount: toBaseUnitAmount(10, 18),
                leftMakerAssetBoughtByRightMakerAmount: toBaseUnitAmount(2, 18),
                rightMakerFeeAssetPaidByRightMakerAmount: toBaseUnitAmount(100, 16), // 100%
                // Taker
                leftMakerAssetReceivedByTakerAmount: toBaseUnitAmount(3, 18),
                leftTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(10, 16), // 10%
                rightTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                leftProtocolFeePaidByTakerInEthAmount: DeploymentManager.protocolFee,
                rightProtocolFeePaidByTakerInEthAmount: DeploymentManager.protocolFee,
            };
            // prettier-ignore
            const matchResults = await matchOrderTester.matchOrdersAndAssertEffectsAsync(
                {
                    leftOrder: signedOrderLeft,
                    rightOrder: signedOrderRight,
                },
                expectedTransferAmounts,
                matcher.address,
                DeploymentManager.protocolFee.times(2),
                true,
            );

            // Construct second right order
            // Note: This order needs makerAssetAmount=90/takerAssetAmount=[anything <= 45] to fully fill the right order.
            //       However, we use 100/50 to ensure a partial fill as we want to go down the "left fill"
            //       branch in the contract twice for this test.
            const signedOrderRight2 = await makerRight.signOrderAsync({
                makerAssetAmount: toBaseUnitAmount(100, 18),
                takerAssetAmount: toBaseUnitAmount(50, 18),
            });

            // Match the orders.
            const expectedTransferAmounts2 = {
                // Left Maker
                leftMakerAssetSoldByLeftMakerAmount: toBaseUnitAmount(45, 18),
                leftMakerFeeAssetPaidByLeftMakerAmount: toBaseUnitAmount(90, 16), // 90% (10% paid earlier)
                // Right Maker
                rightMakerAssetSoldByRightMakerAmount: toBaseUnitAmount(90, 18),
                rightMakerFeeAssetPaidByRightMakerAmount: toBaseUnitAmount(90, 16), // 90%
                // Taker
                leftTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(90, 16), // 90% (10% paid earlier)
                rightTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(90, 16), // 90%
                leftProtocolFeePaidByTakerInEthAmount: DeploymentManager.protocolFee,
                rightProtocolFeePaidByTakerInEthAmount: DeploymentManager.protocolFee,
            };
            await matchOrderTester.matchOrdersAndAssertEffectsAsync(
                {
                    leftOrder: signedOrderLeft,
                    rightOrder: signedOrderRight2,
                    leftOrderTakerAssetFilledAmount: matchResults.orders.leftOrderTakerAssetFilledAmount,
                },
                expectedTransferAmounts2,
                matcher.address,
                DeploymentManager.protocolFee.times(2),
                true,
            );
        });

        it('should transfer correct amounts when right order fill amount deviates from amount derived by `Exchange.fillOrder`', async () => {
            await testMatchOrdersAsync({
                ...defaultMatchOrdersArgs,
                leftOrder: {
                    makerAssetAmount: new BigNumber(1000),
                    takerAssetAmount: new BigNumber(1005),
                },
                rightOrder: {
                    makerAssetAmount: new BigNumber(2126),
                    takerAssetAmount: new BigNumber(1063),
                },
                expectedTransferAmounts: {
                    // Left Maker
                    leftMakerAssetSoldByLeftMakerAmount: new BigNumber(1000),
                    rightMakerAssetBoughtByLeftMakerAmount: new BigNumber(1005),
                    leftMakerFeeAssetPaidByLeftMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Right Maker
                    // Notes:
                    //  i.
                    //    The left order is fully filled by the right order, so the right maker must sell 1005 units of their asset to the left maker.
                    //    By selling 1005 units, the right maker should theoretically receive 502.5 units of the left maker's asset.
                    //    Since the transfer amount must be an integer, this value must be rounded down to 502 or up to 503.
                    //  ii.
                    //    If the right order were filled via `Exchange.fillOrder` the respective fill amounts would be [1004, 502] or [1006, 503].
                    //    It follows that we cannot trigger a sale of 1005 units of the right maker's asset through `Exchange.fillOrder`.
                    //  iii.
                    //    For an optimal match, the algorithm must choose either [1005, 502] or [1005, 503] as fill amounts for the right order.
                    //    The algorithm favors the right maker when the exchange rate must be rounded, so the final fill for the right order is [1005, 503].
                    //  iv.
                    //    The right maker fee differs from the right taker fee because their exchange rate differs.
                    //    The right maker always receives the better exchange and fee price.
                    rightMakerAssetSoldByRightMakerAmount: new BigNumber(2000),
                    rightMakerFeeAssetPaidByRightMakerAmount: toBaseUnitAmount('94.0733772342427093', 16), // 94.07%
                    // Taker
                    rightMakerAssetReceivedByTakerAmount: new BigNumber(995),
                    leftTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                    rightTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount('94.0733772342427093', 16), // 94.07%
                },
            });
        });

        it("should transfer the correct amounts when orders completely fill each other and taker doesn't take a profit", async () => {
            await testMatchOrdersAsync({
                ...defaultMatchOrdersArgs,
                leftOrder: {
                    makerAssetAmount: toBaseUnitAmount(5, 18),
                    takerAssetAmount: toBaseUnitAmount(10, 18),
                },
                rightOrder: {
                    makerAssetAmount: toBaseUnitAmount(10, 18),
                    takerAssetAmount: toBaseUnitAmount(5, 18),
                },
                expectedTransferAmounts: {
                    // Left Maker
                    leftMakerAssetSoldByLeftMakerAmount: toBaseUnitAmount(5, 18),
                    leftMakerFeeAssetPaidByLeftMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Right Maker
                    rightMakerAssetSoldByRightMakerAmount: toBaseUnitAmount(10, 18),
                    rightMakerFeeAssetPaidByRightMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Taker
                    leftTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                    rightTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                },
            });
        });

        it('should transfer the correct amounts when consecutive calls are used to completely fill the right order', async () => {
            // Create orders to match
            const signedOrderLeft = await makerLeft.signOrderAsync({
                makerAssetAmount: toBaseUnitAmount(10, 18),
                takerAssetAmount: toBaseUnitAmount(2, 18),
            });

            const signedOrderRight = await makerRight.signOrderAsync({
                makerAssetAmount: toBaseUnitAmount(50, 18),
                takerAssetAmount: toBaseUnitAmount(100, 18),
            });

            // Match orders
            const expectedTransferAmounts = {
                // Left Maker
                leftMakerAssetSoldByLeftMakerAmount: toBaseUnitAmount(10, 18),
                leftMakerFeeAssetPaidByLeftMakerAmount: toBaseUnitAmount(100, 16), // 100%
                rightMakerAssetBoughtByLeftMakerAmount: toBaseUnitAmount(2, 18),
                // Right Maker
                rightMakerAssetSoldByRightMakerAmount: toBaseUnitAmount(5, 18),
                leftMakerAssetBoughtByRightMakerAmount: toBaseUnitAmount(10, 18),
                rightMakerFeeAssetPaidByRightMakerAmount: toBaseUnitAmount(10, 16), // 10%
                // Taker
                rightMakerAssetReceivedByTakerAmount: toBaseUnitAmount(3, 18),
                leftTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                rightTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(10, 16), // 10%
                leftProtocolFeePaidByTakerInEthAmount: DeploymentManager.protocolFee,
                rightProtocolFeePaidByTakerInEthAmount: DeploymentManager.protocolFee,
            };
            const matchResults = await matchOrderTester.matchOrdersAndAssertEffectsAsync(
                {
                    leftOrder: signedOrderLeft,
                    rightOrder: signedOrderRight,
                },
                expectedTransferAmounts,
                matcher.address,
                DeploymentManager.protocolFee.times(2),
                true,
            );

            // Create second left order
            // Note: This order needs makerAssetAmount=96/takerAssetAmount=48 to fully fill the right order.
            //       However, we use 100/50 to ensure a partial fill as we want to go down the "right fill"
            //       branch in the contract twice for this test.
            const signedOrderLeft2 = await makerLeft.signOrderAsync({
                makerAssetAmount: toBaseUnitAmount(100, 18),
                takerAssetAmount: toBaseUnitAmount(50, 18),
            });

            // Match signedOrderLeft2 with signedOrderRight
            const expectedTransferAmounts2 = {
                // Left Maker
                leftMakerAssetSoldByLeftMakerAmount: toBaseUnitAmount(90, 18),
                leftMakerFeeAssetPaidByLeftMakerAmount: toBaseUnitAmount(90, 16), // 90%
                // Right Maker
                rightMakerAssetSoldByRightMakerAmount: toBaseUnitAmount(45, 18),
                rightMakerFeeAssetPaidByRightMakerAmount: toBaseUnitAmount(90, 16), // 90%
                // Taker
                leftTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(90, 16), // 96%
                rightTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(90, 16), // 90%
                leftProtocolFeePaidByTakerInEthAmount: DeploymentManager.protocolFee,
                rightProtocolFeePaidByTakerInEthAmount: DeploymentManager.protocolFee,
            };

            await matchOrderTester.matchOrdersAndAssertEffectsAsync(
                {
                    leftOrder: signedOrderLeft2,
                    rightOrder: signedOrderRight,
                    rightOrderTakerAssetFilledAmount: matchResults.orders.rightOrderTakerAssetFilledAmount,
                },
                expectedTransferAmounts2,
                matcher.address,
                DeploymentManager.protocolFee.times(2),
                true,
            );
        });

        it('should transfer the correct amounts if fee recipient is the same across both matched orders', async () => {
            await testMatchOrdersAsync({
                ...defaultMatchOrdersArgs,
                leftOrder: {
                    makerAssetAmount: toBaseUnitAmount(5, 18),
                    takerAssetAmount: toBaseUnitAmount(10, 18),
                    feeRecipientAddress: feeRecipientLeft.address,
                },
                rightOrder: {
                    makerAssetAmount: toBaseUnitAmount(10, 18),
                    takerAssetAmount: toBaseUnitAmount(2, 18),
                    feeRecipientAddress: feeRecipientLeft.address,
                },
                expectedTransferAmounts: {
                    // Left Maker
                    leftMakerAssetSoldByLeftMakerAmount: toBaseUnitAmount(5, 18),
                    leftMakerFeeAssetPaidByLeftMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Right Maker
                    rightMakerAssetSoldByRightMakerAmount: toBaseUnitAmount(10, 18),
                    leftMakerAssetBoughtByRightMakerAmount: toBaseUnitAmount(2, 18),
                    rightMakerFeeAssetPaidByRightMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Taker
                    leftMakerAssetReceivedByTakerAmount: toBaseUnitAmount(3, 18),
                    leftTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                    rightTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                },
            });
        });

        it('should transfer the correct amounts if taker == leftMaker', async () => {
            await testMatchOrdersAsync({
                ...defaultMatchOrdersArgs,
                leftOrder: {
                    makerAssetAmount: toBaseUnitAmount(5, 18),
                    takerAssetAmount: toBaseUnitAmount(10, 18),
                },
                rightOrder: {
                    makerAssetAmount: toBaseUnitAmount(10, 18),
                    takerAssetAmount: toBaseUnitAmount(2, 18),
                },
                expectedTransferAmounts: {
                    // Left Maker
                    leftMakerAssetSoldByLeftMakerAmount: toBaseUnitAmount(5, 18),
                    leftMakerFeeAssetPaidByLeftMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Right Maker
                    rightMakerAssetSoldByRightMakerAmount: toBaseUnitAmount(10, 18),
                    leftMakerAssetBoughtByRightMakerAmount: toBaseUnitAmount(2, 18),
                    rightMakerFeeAssetPaidByRightMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Taker
                    leftMakerAssetReceivedByTakerAmount: toBaseUnitAmount(3, 18),
                    leftTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                    rightTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                },
                matcherAddress: makerLeft.address,
            });
        });

        it('should transfer the correct amounts if taker == rightMaker', async () => {
            await testMatchOrdersAsync({
                ...defaultMatchOrdersArgs,
                leftOrder: {
                    makerAssetAmount: toBaseUnitAmount(5, 18),
                    takerAssetAmount: toBaseUnitAmount(10, 18),
                },
                rightOrder: {
                    makerAssetAmount: toBaseUnitAmount(10, 18),
                    takerAssetAmount: toBaseUnitAmount(2, 18),
                },
                expectedTransferAmounts: {
                    // Left Maker
                    leftMakerAssetSoldByLeftMakerAmount: toBaseUnitAmount(5, 18),
                    leftMakerFeeAssetPaidByLeftMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Right Maker
                    rightMakerAssetSoldByRightMakerAmount: toBaseUnitAmount(10, 18),
                    leftMakerAssetBoughtByRightMakerAmount: toBaseUnitAmount(2, 18),
                    rightMakerFeeAssetPaidByRightMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Taker
                    leftMakerAssetReceivedByTakerAmount: toBaseUnitAmount(3, 18),
                    leftTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                    rightTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                },
                matcherAddress: makerRight.address,
            });
        });

        it('should transfer the correct amounts if taker == leftFeeRecipient', async () => {
            await testMatchOrdersAsync({
                ...defaultMatchOrdersArgs,
                leftOrder: {
                    makerAssetAmount: toBaseUnitAmount(5, 18),
                    takerAssetAmount: toBaseUnitAmount(10, 18),
                },
                rightOrder: {
                    makerAssetAmount: toBaseUnitAmount(10, 18),
                    takerAssetAmount: toBaseUnitAmount(2, 18),
                },
                expectedTransferAmounts: {
                    // Left Maker
                    leftMakerAssetSoldByLeftMakerAmount: toBaseUnitAmount(5, 18),
                    leftMakerFeeAssetPaidByLeftMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Right Maker
                    rightMakerAssetSoldByRightMakerAmount: toBaseUnitAmount(10, 18),
                    leftMakerAssetBoughtByRightMakerAmount: toBaseUnitAmount(2, 18),
                    rightMakerFeeAssetPaidByRightMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Taker
                    leftMakerAssetReceivedByTakerAmount: toBaseUnitAmount(3, 18),
                    leftTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                    rightTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                },
                matcherAddress: feeRecipientLeft.address,
            });
        });

        it('should transfer the correct amounts if taker == rightFeeRecipient', async () => {
            await testMatchOrdersAsync({
                ...defaultMatchOrdersArgs,
                leftOrder: {
                    makerAssetAmount: toBaseUnitAmount(5, 18),
                    takerAssetAmount: toBaseUnitAmount(10, 18),
                },
                rightOrder: {
                    makerAssetAmount: toBaseUnitAmount(10, 18),
                    takerAssetAmount: toBaseUnitAmount(2, 18),
                },
                expectedTransferAmounts: {
                    // Left Maker
                    leftMakerAssetSoldByLeftMakerAmount: toBaseUnitAmount(5, 18),
                    leftMakerFeeAssetPaidByLeftMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Right Maker
                    rightMakerAssetSoldByRightMakerAmount: toBaseUnitAmount(10, 18),
                    leftMakerAssetBoughtByRightMakerAmount: toBaseUnitAmount(2, 18),
                    rightMakerFeeAssetPaidByRightMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Taker
                    leftMakerAssetReceivedByTakerAmount: toBaseUnitAmount(3, 18),
                    leftTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                    rightTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                },
                matcherAddress: feeRecipientRight.address,
            });
        });

        it('should transfer the correct amounts if leftMaker == leftFeeRecipient && rightMaker == rightFeeRecipient', async () => {
            await testMatchOrdersAsync({
                ...defaultMatchOrdersArgs,
                leftOrder: {
                    makerAssetAmount: toBaseUnitAmount(5, 18),
                    takerAssetAmount: toBaseUnitAmount(10, 18),
                    feeRecipientAddress: makerLeft.address,
                },
                rightOrder: {
                    makerAssetAmount: toBaseUnitAmount(10, 18),
                    takerAssetAmount: toBaseUnitAmount(2, 18),
                    feeRecipientAddress: makerRight.address,
                },
                expectedTransferAmounts: {
                    // Left Maker
                    leftMakerAssetSoldByLeftMakerAmount: toBaseUnitAmount(5, 18),
                    leftMakerFeeAssetPaidByLeftMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Right Maker
                    rightMakerAssetSoldByRightMakerAmount: toBaseUnitAmount(10, 18),
                    leftMakerAssetBoughtByRightMakerAmount: toBaseUnitAmount(2, 18),
                    rightMakerFeeAssetPaidByRightMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Taker
                    leftMakerAssetReceivedByTakerAmount: toBaseUnitAmount(3, 18),
                    leftTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                    rightTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                },
            });
        });

        it('should transfer the correct amounts if leftMaker == leftFeeRecipient && leftMakerFeeAsset == leftTakerAsset', async () => {
            await testMatchOrdersAsync({
                ...defaultMatchOrdersArgs,
                leftOrder: {
                    makerAssetAmount: toBaseUnitAmount(5, 18),
                    takerAssetAmount: toBaseUnitAmount(10, 18),
                    makerFeeAssetData: makerAssetDataRight,
                    feeRecipientAddress: makerLeft.address,
                },
                rightOrder: {
                    makerAssetAmount: toBaseUnitAmount(10, 18),
                    takerAssetAmount: toBaseUnitAmount(2, 18),
                },
                expectedTransferAmounts: {
                    // Left Maker
                    leftMakerAssetSoldByLeftMakerAmount: toBaseUnitAmount(5, 18),
                    leftMakerFeeAssetPaidByLeftMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Right Maker
                    rightMakerAssetSoldByRightMakerAmount: toBaseUnitAmount(10, 18),
                    leftMakerAssetBoughtByRightMakerAmount: toBaseUnitAmount(2, 18),
                    rightMakerFeeAssetPaidByRightMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Taker
                    leftMakerAssetReceivedByTakerAmount: toBaseUnitAmount(3, 18),
                    leftTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                    rightTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                },
            });
        });

        it('should transfer the correct amounts if rightMaker == rightFeeRecipient && rightMakerFeeAsset == rightTakerAsset', async () => {
            await testMatchOrdersAsync({
                ...defaultMatchOrdersArgs,
                leftOrder: {
                    makerAssetAmount: toBaseUnitAmount(5, 18),
                    takerAssetAmount: toBaseUnitAmount(10, 18),
                },
                rightOrder: {
                    makerAssetAmount: toBaseUnitAmount(10, 18),
                    takerAssetAmount: toBaseUnitAmount(2, 18),
                    makerFeeAssetData: makerAssetDataLeft,
                    feeRecipientAddress: makerRight.address,
                },
                expectedTransferAmounts: {
                    // Left Maker
                    leftMakerAssetSoldByLeftMakerAmount: toBaseUnitAmount(5, 18),
                    leftMakerFeeAssetPaidByLeftMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Right Maker
                    rightMakerAssetSoldByRightMakerAmount: toBaseUnitAmount(10, 18),
                    leftMakerAssetBoughtByRightMakerAmount: toBaseUnitAmount(2, 18),
                    rightMakerFeeAssetPaidByRightMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Taker
                    leftMakerAssetReceivedByTakerAmount: toBaseUnitAmount(3, 18),
                    leftTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                    rightTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                },
            });
        });

        it(`should transfer the correct amounts if rightMaker == rightFeeRecipient && rightTakerAsset == rightMakerFeeAsset
            && leftMaker == leftFeeRecipient && leftTakerAsset == leftMakerFeeAsset`, async () => {
            await testMatchOrdersAsync({
                ...defaultMatchOrdersArgs,
                leftOrder: {
                    makerAssetAmount: toBaseUnitAmount(5, 18),
                    takerAssetAmount: toBaseUnitAmount(10, 18),
                    makerFeeAssetData: makerAssetDataRight,
                    feeRecipientAddress: makerLeft.address,
                },
                rightOrder: {
                    makerAssetAmount: toBaseUnitAmount(10, 18),
                    takerAssetAmount: toBaseUnitAmount(2, 18),
                    makerFeeAssetData: makerAssetDataRight,
                    feeRecipientAddress: makerRight.address,
                },
                expectedTransferAmounts: {
                    // Left Maker
                    leftMakerAssetSoldByLeftMakerAmount: toBaseUnitAmount(5, 18),
                    leftMakerFeeAssetPaidByLeftMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Right Maker
                    rightMakerAssetSoldByRightMakerAmount: toBaseUnitAmount(10, 18),
                    leftMakerAssetBoughtByRightMakerAmount: toBaseUnitAmount(2, 18),
                    rightMakerFeeAssetPaidByRightMakerAmount: toBaseUnitAmount(100, 16), // 100%
                    // Taker
                    leftMakerAssetReceivedByTakerAmount: toBaseUnitAmount(3, 18),
                    leftTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                    rightTakerFeeAssetPaidByTakerAmount: toBaseUnitAmount(100, 16), // 100%
                },
            });
        });

        it('should revert if left order is not fillable', async () => {
            // Create orders to match
            const signedOrderLeft = await makerLeft.signOrderAsync({
                makerAssetAmount: toBaseUnitAmount(5, 18),
                takerAssetAmount: toBaseUnitAmount(10, 18),
            });
            const signedOrderRight = await makerRight.signOrderAsync({
                makerAssetAmount: toBaseUnitAmount(10, 18),
                takerAssetAmount: toBaseUnitAmount(2, 18),
            });
            const orderHashHexLeft = orderHashUtils.getOrderHashHex(signedOrderLeft);
            // Cancel left order
            await makerLeft.cancelOrderAsync(signedOrderLeft);

            // Match orders
            const expectedError = new ExchangeRevertErrors.OrderStatusError(orderHashHexLeft, OrderStatus.Cancelled);
            const tx = deployment.exchange
                .matchOrdersWithMaximalFill(
                    signedOrderLeft,
                    signedOrderRight,
                    signedOrderLeft.signature,
                    signedOrderRight.signature,
                )
                .awaitTransactionSuccessAsync({ from: matcher.address });
            return expect(tx).to.revertWith(expectedError);
        });

        it('should revert if right order is not fillable', async () => {
            // Create orders to match
            const signedOrderLeft = await makerLeft.signOrderAsync({
                makerAssetAmount: toBaseUnitAmount(5, 18),
                takerAssetAmount: toBaseUnitAmount(10, 18),
            });
            const signedOrderRight = await makerRight.signOrderAsync({
                makerAssetAmount: toBaseUnitAmount(10, 18),
                takerAssetAmount: toBaseUnitAmount(2, 18),
            });
            const orderHashHexRight = orderHashUtils.getOrderHashHex(signedOrderRight);
            // Cancel right order
            await makerRight.cancelOrderAsync(signedOrderRight);

            // Match orders
            const expectedError = new ExchangeRevertErrors.OrderStatusError(orderHashHexRight, OrderStatus.Cancelled);
            const tx = deployment.exchange
                .matchOrdersWithMaximalFill(
                    signedOrderLeft,
                    signedOrderRight,
                    signedOrderLeft.signature,
                    signedOrderRight.signature,
                )
                .awaitTransactionSuccessAsync({ from: matcher.address });
            return expect(tx).to.revertWith(expectedError);
        });

        it('should revert if there is not a positive spread', async () => {
            // Create orders to match
            const signedOrderLeft = await makerLeft.signOrderAsync({
                makerAssetAmount: toBaseUnitAmount(5, 18),
                takerAssetAmount: toBaseUnitAmount(100, 18),
            });
            const signedOrderRight = await makerRight.signOrderAsync({
                makerAssetAmount: toBaseUnitAmount(1, 18),
                takerAssetAmount: toBaseUnitAmount(200, 18),
            });
            const orderHashHexLeft = orderHashUtils.getOrderHashHex(signedOrderLeft);
            const orderHashHexRight = orderHashUtils.getOrderHashHex(signedOrderRight);
            // Match orders
            const expectedError = new ExchangeRevertErrors.NegativeSpreadError(orderHashHexLeft, orderHashHexRight);
            const tx = deployment.exchange
                .matchOrdersWithMaximalFill(
                    signedOrderLeft,
                    signedOrderRight,
                    signedOrderLeft.signature,
                    signedOrderRight.signature,
                )
                .awaitTransactionSuccessAsync({ from: matcher.address });
            return expect(tx).to.revertWith(expectedError);
        });

        it('should revert if the left maker asset is not equal to the right taker asset ', async () => {
            // Create orders to match
            const signedOrderLeft = await makerLeft.signOrderAsync({
                makerAssetAmount: toBaseUnitAmount(5, 18),
                takerAssetAmount: toBaseUnitAmount(10, 18),
            });
            const signedOrderRight = await makerRight.signOrderAsync({
                takerAssetData: deployment.assetDataEncoder
                    .ERC20Token(makerAssetRight.address)
                    .getABIEncodedTransactionData(),
                makerAssetAmount: toBaseUnitAmount(10, 18),
                takerAssetAmount: toBaseUnitAmount(2, 18),
            });
            // We are assuming assetData fields of the right order are the
            // reverse of the left order, rather than checking equality. This
            // saves a bunch of gas, but as a result if the assetData fields are
            // off then the failure ends up happening at signature validation
            const reconstructedOrderRight = {
                ...signedOrderRight,
                takerAssetData: signedOrderLeft.makerAssetData,
            };
            const orderHashHex = orderHashUtils.getOrderHashHex(reconstructedOrderRight);
            const expectedError = new ExchangeRevertErrors.SignatureError(
                ExchangeRevertErrors.SignatureErrorCode.BadOrderSignature,
                orderHashHex,
                signedOrderRight.makerAddress,
                signedOrderRight.signature,
            );
            // Match orders
            const tx = deployment.exchange
                .matchOrdersWithMaximalFill(
                    signedOrderLeft,
                    signedOrderRight,
                    signedOrderLeft.signature,
                    signedOrderRight.signature,
                )
                .awaitTransactionSuccessAsync({ from: matcher.address });
            return expect(tx).to.revertWith(expectedError);
        });

        it('should revert if the right maker asset is not equal to the left taker asset', async () => {
            // Create orders to match
            const signedOrderLeft = await makerLeft.signOrderAsync({
                takerAssetData: deployment.assetDataEncoder
                    .ERC20Token(makerAssetLeft.address)
                    .getABIEncodedTransactionData(),
                makerAssetAmount: toBaseUnitAmount(5, 18),
                takerAssetAmount: toBaseUnitAmount(10, 18),
            });
            const signedOrderRight = await makerRight.signOrderAsync({
                makerAssetAmount: toBaseUnitAmount(10, 18),
                takerAssetAmount: toBaseUnitAmount(2, 18),
            });
            const reconstructedOrderRight = {
                ...signedOrderRight,
                makerAssetData: signedOrderLeft.takerAssetData,
            };
            const orderHashHex = orderHashUtils.getOrderHashHex(reconstructedOrderRight);
            const expectedError = new ExchangeRevertErrors.SignatureError(
                ExchangeRevertErrors.SignatureErrorCode.BadOrderSignature,
                orderHashHex,
                signedOrderRight.makerAddress,
                signedOrderRight.signature,
            );
            // Match orders
            const tx = deployment.exchange
                .matchOrdersWithMaximalFill(
                    signedOrderLeft,
                    signedOrderRight,
                    signedOrderLeft.signature,
                    signedOrderRight.signature,
                )
                .awaitTransactionSuccessAsync({ from: matcher.address });
            return expect(tx).to.revertWith(expectedError);
        });
    });
});
// tslint:disable-line:max-file-line-count
