import { CoordinatorRegistryContract } from '@0x/abi-gen-wrappers';
import { constants } from '@0x/contracts-test-utils';
import { defaultOrmConfig, getAppAsync } from '@0x/coordinator-server';
import { BlockchainLifecycle } from '@0x/dev-utils';
import { FillScenarios } from '@0x/fill-scenarios';
import { assetDataUtils } from '@0x/order-utils';
import { SignedOrder } from '@0x/types';
import { BigNumber, fetchAsync, logUtils } from '@0x/utils';
import * as chai from 'chai';
import * as http from 'http';
import 'mocha';
import * as nock from 'nock';

import { ContractWrappers } from '../src';
import { CoordinatorServerErrorMsg } from '../src/utils/coordinator_server_types';

import { chaiSetup } from './utils/chai_setup';
import { migrateOnceAsync } from './utils/migrate';
import { tokenUtils } from './utils/token_utils';
import { provider, web3Wrapper } from './utils/web3_wrapper';

chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
const coordinatorPort = '3000';
const anotherCoordinatorPort = '4000';
const coordinatorEndpoint = 'http://localhost:';

// tslint:disable:custom-no-magic-numbers
describe('CoordinatorWrapper', () => {
    const fillableAmount = new BigNumber(5);
    const takerTokenFillAmount = new BigNumber(5);
    let coordinatorServerApp: http.Server;
    let anotherCoordinatorServerApp: http.Server;
    let contractWrappers: ContractWrappers;
    let fillScenarios: FillScenarios;
    let exchangeContractAddress: string;
    let zrxTokenAddress: string;
    let userAddresses: string[];
    let makerAddress: string;
    let takerAddress: string;
    let feeRecipientAddressOne: string;
    let feeRecipientAddressTwo: string;
    let feeRecipientAddressThree: string;
    let feeRecipientAddressFour: string;

    let makerTokenAddress: string;
    let takerTokenAddress: string;
    let makerAssetData: string;
    let takerAssetData: string;
    let txHash: string;
    let signedOrder: SignedOrder;
    let anotherSignedOrder: SignedOrder;
    let signedOrderWithDifferentFeeRecipient: SignedOrder;
    let signedOrderWithDifferentCoordinatorOperator: SignedOrder;
    let coordinatorRegistryInstance: CoordinatorRegistryContract;

    // for testing server error responses
    let serverValidationError: any;
    let serverCancellationSuccess: any;
    let serverApprovalSuccess: any;

    before(async () => {
        const contractAddresses = await migrateOnceAsync();
        await blockchainLifecycle.startAsync();
        const config = {
            networkId: constants.TESTRPC_NETWORK_ID,
            contractAddresses,
            blockPollingIntervalMs: 10,
        };
        contractWrappers = new ContractWrappers(provider, config);
        exchangeContractAddress = contractWrappers.exchange.address;
        userAddresses = await web3Wrapper.getAvailableAddressesAsync();
        zrxTokenAddress = contractWrappers.exchange.zrxTokenAddress;
        fillScenarios = new FillScenarios(
            provider,
            userAddresses,
            zrxTokenAddress,
            exchangeContractAddress,
            contractWrappers.erc20Proxy.address,
            contractWrappers.erc721Proxy.address,
        );
        [
            ,
            makerAddress,
            takerAddress,
            feeRecipientAddressOne,
            feeRecipientAddressTwo,
            feeRecipientAddressThree,
            feeRecipientAddressFour,
        ] = userAddresses.slice(0, 7);
        [makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
        [makerAssetData, takerAssetData] = [
            assetDataUtils.encodeERC20AssetData(makerTokenAddress),
            assetDataUtils.encodeERC20AssetData(takerTokenAddress),
        ];

        // set up mock coordinator server
        const coordinatorServerConfigs = {
            HTTP_PORT: 3000, // Only used in default instantiation in 0x-coordinator-server/server.js; not used here
            NETWORK_ID_TO_SETTINGS: {
                50: {
                    FEE_RECIPIENTS: [
                        {
                            ADDRESS: feeRecipientAddressOne,
                            PRIVATE_KEY: constants.TESTRPC_PRIVATE_KEYS[
                                userAddresses.indexOf(feeRecipientAddressOne)
                            ].toString('hex'),
                        },
                        {
                            ADDRESS: feeRecipientAddressTwo,
                            PRIVATE_KEY: constants.TESTRPC_PRIVATE_KEYS[
                                userAddresses.indexOf(feeRecipientAddressTwo)
                            ].toString('hex'),
                        },
                        {
                            ADDRESS: feeRecipientAddressThree,
                            PRIVATE_KEY: constants.TESTRPC_PRIVATE_KEYS[
                                userAddresses.indexOf(feeRecipientAddressThree)
                            ].toString('hex'),
                        },
                    ],
                    // Ethereum RPC url, only used in the default instantiation in 0x-coordinator-server/server.js
                    // Not used here when instantiating with the imported app
                    RPC_URL: 'http://ignore',
                },
            },
            // Optional selective delay on fill requests
            SELECTIVE_DELAY_MS: 0,
            EXPIRATION_DURATION_SECONDS: 60, // 1 minute
        };
        coordinatorServerApp = await getAppAsync(
            {
                [config.networkId]: provider,
            },
            coordinatorServerConfigs,
            defaultOrmConfig,
        );

        coordinatorServerApp.listen(coordinatorPort, () => {
            logUtils.log(`Coordinator SERVER API (HTTP) listening on port ${coordinatorPort}!`);
        });

        anotherCoordinatorServerApp = await getAppAsync(
            {
                [config.networkId]: provider,
            },
            coordinatorServerConfigs,
            {
                type: 'sqlite',
                database: 'database.sqlite_2',
                entities: defaultOrmConfig.entities,
                cli: defaultOrmConfig.cli,
                logging: defaultOrmConfig.logging,
                synchronize: defaultOrmConfig.synchronize,
            },
        );

        anotherCoordinatorServerApp.listen(anotherCoordinatorPort, () => {
            logUtils.log(`Coordinator SERVER API (HTTP) listening on port ${anotherCoordinatorPort}!`);
        });

        // setup coordinator registry
        coordinatorRegistryInstance = new CoordinatorRegistryContract(contractAddresses.coordinatorRegistry, provider);

        // register coordinator server
        await web3Wrapper.awaitTransactionSuccessAsync(
            await coordinatorRegistryInstance.setCoordinatorEndpoint.sendTransactionAsync(
                `${coordinatorEndpoint}${coordinatorPort}`,
                {
                    from: feeRecipientAddressOne,
                },
            ),
            constants.AWAIT_TRANSACTION_MINED_MS,
        );
        await web3Wrapper.awaitTransactionSuccessAsync(
            await coordinatorRegistryInstance.setCoordinatorEndpoint.sendTransactionAsync(
                `${coordinatorEndpoint}${coordinatorPort}`,
                {
                    from: feeRecipientAddressTwo,
                },
            ),
            constants.AWAIT_TRANSACTION_MINED_MS,
        );

        // register another coordinator server
        await web3Wrapper.awaitTransactionSuccessAsync(
            await coordinatorRegistryInstance.setCoordinatorEndpoint.sendTransactionAsync(
                `${coordinatorEndpoint}${anotherCoordinatorPort}`,
                {
                    from: feeRecipientAddressThree,
                },
            ),
            constants.AWAIT_TRANSACTION_MINED_MS,
        );
    });
    after(async () => {
        await blockchainLifecycle.revertAsync();
    });
    beforeEach(async () => {
        await blockchainLifecycle.startAsync();

        signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
            makerAssetData,
            takerAssetData,
            new BigNumber(1),
            new BigNumber(1),
            makerAddress,
            takerAddress,
            fillableAmount,
            feeRecipientAddressOne,
            undefined,
            contractWrappers.coordinator.address,
        );
        anotherSignedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
            makerAssetData,
            takerAssetData,
            new BigNumber(1),
            new BigNumber(1),
            makerAddress,
            takerAddress,
            fillableAmount,
            feeRecipientAddressOne,
            undefined,
            contractWrappers.coordinator.address,
        );
        signedOrderWithDifferentFeeRecipient = await fillScenarios.createFillableSignedOrderWithFeesAsync(
            makerAssetData,
            takerAssetData,
            new BigNumber(1),
            new BigNumber(1),
            makerAddress,
            takerAddress,
            fillableAmount,
            feeRecipientAddressTwo,
            undefined,
            contractWrappers.coordinator.address,
        );
        signedOrderWithDifferentCoordinatorOperator = await fillScenarios.createFillableSignedOrderWithFeesAsync(
            makerAssetData,
            takerAssetData,
            new BigNumber(1),
            new BigNumber(1),
            makerAddress,
            takerAddress,
            fillableAmount,
            feeRecipientAddressThree,
            undefined,
            contractWrappers.coordinator.address,
        );
    });
    afterEach(async () => {
        await blockchainLifecycle.revertAsync();
    });
    describe('test setup', () => {
        it('should have coordinator registry which returns an endpoint', async () => {
            const setCoordinatorEndpoint = await coordinatorRegistryInstance.getCoordinatorEndpoint.callAsync(
                feeRecipientAddressOne,
            );
            const anotherSetCoordinatorEndpoint = await coordinatorRegistryInstance.getCoordinatorEndpoint.callAsync(
                feeRecipientAddressThree,
            );
            expect(setCoordinatorEndpoint).to.be.equal(`${coordinatorEndpoint}${coordinatorPort}`);
            expect(anotherSetCoordinatorEndpoint).to.be.equal(`${coordinatorEndpoint}${anotherCoordinatorPort}`);
        });
        it('should have coordinator server endpoints which respond to pings', async () => {
            let result = await fetchAsync(`${coordinatorEndpoint}${coordinatorPort}/v1/ping`);
            expect(result.status).to.be.equal(200);
            expect(await result.text()).to.be.equal('pong');

            result = await fetchAsync(`${coordinatorEndpoint}${anotherCoordinatorPort}/v1/ping`);
            expect(result.status).to.be.equal(200);
            expect(await result.text()).to.be.equal('pong');
        });
    });
    // fill handling is the same for all fill methods so we can test them all through the fillOrder and batchFillOrders interfaces
    describe('fill order(s)', () => {
        describe('#fillOrderAsync', () => {
            it('should fill a valid order', async () => {
                txHash = await contractWrappers.coordinator.fillOrderAsync(
                    signedOrder,
                    takerTokenFillAmount,
                    takerAddress,
                );

                await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS);
            });
        });
        describe('#batchFillOrdersAsync', () => {
            it('should fill a batch of valid orders', async () => {
                const signedOrders = [signedOrder, anotherSignedOrder];
                const takerAssetFillAmounts = Array(2).fill(takerTokenFillAmount);
                txHash = await contractWrappers.coordinator.batchFillOrdersAsync(
                    signedOrders,
                    takerAssetFillAmounts,
                    takerAddress,
                );

                await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS);
            });
            it('should fill a batch of orders with different feeRecipientAddresses with the same coordinator server', async () => {
                const signedOrders = [signedOrder, anotherSignedOrder, signedOrderWithDifferentFeeRecipient];
                const takerAssetFillAmounts = Array(3).fill(takerTokenFillAmount);
                txHash = await contractWrappers.coordinator.batchFillOrdersAsync(
                    signedOrders,
                    takerAssetFillAmounts,
                    takerAddress,
                );

                await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS);
            });
            // coordinator-server, as currently implemented, shares a singleton database connection across
            // all instantiations. Making the request to two different mocked server endpoints still hits the
            // same database and fails because of the uniqueness constraint on transactions in the database.
            it.skip('should fill a batch of orders with different feeRecipientAddresses with different coordinator servers', async () => {
                const signedOrders = [
                    signedOrder,
                    anotherSignedOrder,
                    signedOrderWithDifferentFeeRecipient,
                    signedOrderWithDifferentCoordinatorOperator,
                ];
                const takerAssetFillAmounts = Array(4).fill(takerTokenFillAmount);
                txHash = await contractWrappers.coordinator.batchFillOrdersAsync(
                    signedOrders,
                    takerAssetFillAmounts,
                    takerAddress,
                );

                await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS);
            });

            it('should fill a batch of mixed coordinator and non-coordinator orders', async () => {
                const nonCoordinatorOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
                    makerAssetData,
                    takerAssetData,
                    new BigNumber(1),
                    new BigNumber(1),
                    makerAddress,
                    takerAddress,
                    fillableAmount,
                    feeRecipientAddressOne,
                    undefined,
                );
                const signedOrders = [signedOrder, nonCoordinatorOrder];
                const takerAssetFillAmounts = Array(2).fill(takerTokenFillAmount);
                txHash = await contractWrappers.coordinator.batchFillOrdersAsync(
                    signedOrders,
                    takerAssetFillAmounts,
                    takerAddress,
                );
                await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS);
            });
        });
    });
    describe('soft cancel order(s)', () => {
        describe('#softCancelOrderAsync', () => {
            it('should soft cancel a valid order', async () => {
                const response = await contractWrappers.coordinator.softCancelOrderAsync(signedOrder);
                expect(response.outstandingFillSignatures).to.have.lengthOf(0);
                expect(response.cancellationSignatures).to.have.lengthOf(1);
            });
        });
        describe('#batchSoftCancelOrdersAsync', () => {
            it('should soft cancel a batch of valid orders', async () => {
                const orders = [signedOrder, anotherSignedOrder];
                const response = await contractWrappers.coordinator.batchSoftCancelOrdersAsync(orders);
                expect(response).to.have.lengthOf(1);
                expect(response[0].outstandingFillSignatures).to.have.lengthOf(0);
                expect(response[0].cancellationSignatures).to.have.lengthOf(1);
            });
            it('should soft cancel a batch of orders with different feeRecipientAddresses', async () => {
                const orders = [signedOrder, anotherSignedOrder, signedOrderWithDifferentFeeRecipient];
                const response = await contractWrappers.coordinator.batchSoftCancelOrdersAsync(orders);
                expect(response).to.have.lengthOf(1);
                expect(response[0].outstandingFillSignatures).to.have.lengthOf(0);
                expect(response[0].cancellationSignatures).to.have.lengthOf(2);
            });
            it('should soft cancel a batch of orders with different coordinatorOperator and concatenate responses', async () => {
                const orders = [
                    signedOrder,
                    anotherSignedOrder,
                    signedOrderWithDifferentFeeRecipient,
                    signedOrderWithDifferentCoordinatorOperator,
                ];
                const response = await contractWrappers.coordinator.batchSoftCancelOrdersAsync(orders);
                expect(response).to.have.lengthOf(2);
                expect(response[0].outstandingFillSignatures).to.have.lengthOf(0);
                expect(response[0].cancellationSignatures).to.have.lengthOf(3);
                expect(response[1].cancellationSignatures).to.have.lengthOf(3); // both coordinator servers support the same feeRecipients
            });
        });
    });
    describe('hard cancel order(s)', () => {
        describe('#hardCancelOrderAsync', () => {
            it('should hard cancel a valid order', async () => {
                txHash = await contractWrappers.coordinator.hardCancelOrderAsync(signedOrder);
                await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS);
            });
        });
        describe('#batchHardCancelOrdersAsync', () => {
            it('should hard cancel a batch of valid orders', async () => {
                const orders = [signedOrder, anotherSignedOrder];
                txHash = await contractWrappers.coordinator.batchHardCancelOrdersAsync(orders);
                await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS);
            });
        });
        describe('#cancelOrdersUpTo/getOrderEpochAsync', () => {
            it('should hard cancel orders up to target order epoch', async () => {
                const targetOrderEpoch = new BigNumber(42);
                txHash = await contractWrappers.coordinator.hardCancelOrdersUpToAsync(targetOrderEpoch, makerAddress);

                await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS);
                const orderEpoch = await contractWrappers.exchange.getOrderEpochAsync(
                    makerAddress,
                    contractWrappers.coordinator.address,
                );
                expect(orderEpoch).to.be.bignumber.equal(targetOrderEpoch.plus(1));
            });
        });
    });
    describe('coordinator edge cases', () => {
        it('should throw error when feeRecipientAddress is not in registry', async () => {
            const badOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
                makerAssetData,
                takerAssetData,
                new BigNumber(1),
                new BigNumber(1),
                makerAddress,
                takerAddress,
                fillableAmount,
                feeRecipientAddressFour,
            );

            expect(
                contractWrappers.coordinator.fillOrderAsync(badOrder, takerTokenFillAmount, takerAddress),
            ).to.be.rejected();
        });
        it('should throw error when coordinator endpoint is malformed', async () => {
            await web3Wrapper.awaitTransactionSuccessAsync(
                await coordinatorRegistryInstance.setCoordinatorEndpoint.sendTransactionAsync('localhost', {
                    from: feeRecipientAddressFour,
                }),
                constants.AWAIT_TRANSACTION_MINED_MS,
            );
            expect(
                contractWrappers.coordinator.fillOrderAsync(signedOrder, takerTokenFillAmount, takerAddress),
            ).to.be.rejected();
        });
    });
    describe('coordinator server errors', () => {
        beforeEach('setup', () => {
            serverValidationError = {
                code: 100,
                reason: 'Validation Failed',
                validationErrors: [
                    {
                        field: 'signedTransaction',
                        code: 1011,
                        reason:
                            'A transaction can only be approved once. To request approval to perform the same actions, generate and sign an identical transaction with a different salt value.',
                    },
                ],
            };
            serverCancellationSuccess = {
                outstandingFillSignatures: [
                    {
                        orderHash: '0xd1dc61f3e7e5f41d72beae7863487beea108971de678ca00d903756f842ef3ce',
                        approvalSignatures: [
                            '0x1c7383ca8ebd6de8b5b20b1c2d49bea166df7dfe4af1932c9c52ec07334e859cf2176901da35f4480ceb3ab63d8d0339d851c31929c40d88752689b9a8a535671303',
                        ],
                        expirationTimeSeconds: 1552390380,
                        takerAssetFillAmount: 100000000000000000000,
                    },
                ],
                cancellationSignatures: [
                    '0x2ea3117a8ebd6de8b5b20b1c2d49bea166df7dfe4af1932c9c52ec07334e859cf2176901da35f4480ceb3ab63d8d0339d851c31929c40d88752689b9a855b5a7b401',
                ],
            };
            serverApprovalSuccess = {
                signatures: [
                    '0x1cc07d7ae39679690a91418d46491520f058e4fb14debdf2e98f2376b3970de8512ace44af0be6d1c65617f7aae8c2364ff63f241515ee1559c3eeecb0f671d9e903',
                ],
                expirationTimeSeconds: 1552390014,
            };

            nock(`${coordinatorEndpoint}${coordinatorPort}`)
                .post('/v1/request_transaction', () => true)
                .query({
                    networkId: 50,
                })
                .reply(400, serverValidationError);
        });
        it('should throw error when softCancel fails', done => {
            contractWrappers.coordinator
                .softCancelOrderAsync(signedOrder)
                .then(res => {
                    expect(res).to.be.undefined();
                })
                .catch(err => {
                    expect(err.message).equal(CoordinatorServerErrorMsg.CancellationFailed);
                    expect(err.approvedOrders).to.be.empty('array');
                    expect(err.cancellations).to.be.empty('array');

                    const errorBody = err.errors[0];
                    expect(errorBody.isError).to.be.true();
                    expect(errorBody.status).to.equal(400);
                    expect(errorBody.error).to.deep.equal(serverValidationError);
                    expect(errorBody.orders).to.deep.equal([signedOrder]);
                    expect(errorBody.coordinatorOperator).to.equal(`${coordinatorEndpoint}${coordinatorPort}`);
                    done();
                });
        });
        it('should throw error when batch soft cancel fails with single coordinator operator', done => {
            const orders = [signedOrder, signedOrderWithDifferentFeeRecipient];
            contractWrappers.coordinator
                .batchSoftCancelOrdersAsync(orders)
                .then(res => {
                    expect(res).to.be.undefined();
                })
                .catch(err => {
                    expect(err.message).equal(CoordinatorServerErrorMsg.CancellationFailed);
                    expect(err.approvedOrders).to.be.empty('array');
                    expect(err.cancellations).to.be.empty('array');

                    const errorBody = err.errors[0];
                    expect(errorBody.isError).to.be.true();
                    expect(errorBody.status).to.equal(400);
                    expect(errorBody.error).to.deep.equal(serverValidationError);
                    expect(errorBody.orders).to.deep.equal(orders);
                    expect(errorBody.coordinatorOperator).to.equal(`${coordinatorEndpoint}${coordinatorPort}`);
                    done();
                });
        });
        it('should throw consolidated error when batch soft cancel partially fails with different coordinator operators', done => {
            nock(`${coordinatorEndpoint}${anotherCoordinatorPort}`)
                .post('/v1/request_transaction', () => true)
                .query({
                    networkId: 50,
                })
                .reply(200, serverCancellationSuccess);

            const signedOrders = [signedOrder, signedOrderWithDifferentCoordinatorOperator];
            contractWrappers.coordinator
                .batchSoftCancelOrdersAsync(signedOrders)
                .then(res => {
                    expect(res).to.be.undefined();
                })
                .catch(err => {
                    expect(err.message).equal(CoordinatorServerErrorMsg.CancellationFailed);
                    expect(err.approvedOrders).to.be.empty('array');
                    expect(err.cancellations).to.deep.equal([serverCancellationSuccess]);

                    const errorBody = err.errors[0];
                    expect(errorBody.isError).to.be.true();
                    expect(errorBody.status).to.equal(400);
                    expect(errorBody.error).to.deep.equal(serverValidationError);
                    expect(errorBody.orders).to.deep.equal([signedOrder]);
                    expect(errorBody.coordinatorOperator).to.equal(`${coordinatorEndpoint}${coordinatorPort}`);
                    done();
                });
        });
        it('should throw consolidated error when batch soft cancel totally fails with different coordinator operators', done => {
            nock(`${coordinatorEndpoint}${anotherCoordinatorPort}`)
                .post('/v1/request_transaction', () => true)
                .query({
                    networkId: 50,
                })
                .reply(400, serverValidationError);

            const signedOrders = [signedOrder, signedOrderWithDifferentCoordinatorOperator];
            contractWrappers.coordinator
                .batchSoftCancelOrdersAsync(signedOrders)
                .then(res => {
                    expect(res).to.be.undefined();
                })
                .catch(err => {
                    expect(err.message).equal(CoordinatorServerErrorMsg.CancellationFailed);
                    expect(err.approvedOrders).to.be.empty('array');
                    expect(err.cancellations).to.be.empty('array');

                    const errorBody = err.errors[0];
                    expect(errorBody.isError).to.be.true();
                    expect(errorBody.status).to.equal(400);
                    expect(errorBody.error).to.deep.equal(serverValidationError);
                    expect(errorBody.orders).to.deep.equal([signedOrder]);
                    expect(errorBody.coordinatorOperator).to.equal(`${coordinatorEndpoint}${coordinatorPort}`);

                    const anotherErrorBody = err.errors[1];
                    expect(anotherErrorBody.isError).to.be.true();
                    expect(anotherErrorBody.status).to.equal(400);
                    expect(anotherErrorBody.error).to.deep.equal(serverValidationError);
                    expect(anotherErrorBody.orders).to.deep.equal([signedOrderWithDifferentCoordinatorOperator]);
                    expect(anotherErrorBody.coordinatorOperator).to.equal(
                        `${coordinatorEndpoint}${anotherCoordinatorPort}`,
                    );
                    done();
                });
        });
        it('should throw error when a fill fails', done => {
            contractWrappers.coordinator
                .fillOrderAsync(signedOrder, takerTokenFillAmount, takerAddress)
                .then(res => {
                    expect(res).to.be.undefined();
                })
                .catch(err => {
                    expect(err.message).equal(CoordinatorServerErrorMsg.FillFailed);
                    expect(err.approvedOrders).to.be.empty('array');
                    expect(err.cancellations).to.be.empty('array');

                    const errorBody = err.errors[0];
                    expect(errorBody.isError).to.be.true();
                    expect(errorBody.status).to.equal(400);
                    expect(errorBody.error).to.deep.equal(serverValidationError);
                    expect(errorBody.orders).to.deep.equal([signedOrder]);
                    expect(errorBody.coordinatorOperator).to.equal(`${coordinatorEndpoint}${coordinatorPort}`);
                    done();
                });
        });
        it('should throw error when batch fill fails with single coordinator operator', done => {
            const signedOrders = [signedOrder, signedOrderWithDifferentFeeRecipient];
            const takerAssetFillAmounts = [takerTokenFillAmount, takerTokenFillAmount, takerTokenFillAmount];
            contractWrappers.coordinator
                .batchFillOrdersAsync(signedOrders, takerAssetFillAmounts, takerAddress)
                .then(res => {
                    expect(res).to.be.undefined();
                })
                .catch(err => {
                    expect(err.message).equal(CoordinatorServerErrorMsg.FillFailed);
                    expect(err.approvedOrders).to.be.empty('array');
                    expect(err.cancellations).to.be.empty('array');

                    const errorBody = err.errors[0];
                    expect(errorBody.isError).to.be.true();
                    expect(errorBody.status).to.equal(400);
                    expect(errorBody.error).to.deep.equal(serverValidationError);
                    expect(errorBody.orders).to.deep.equal(signedOrders);
                    expect(errorBody.coordinatorOperator).to.equal(`${coordinatorEndpoint}${coordinatorPort}`);
                    done();
                });
        });
        it('should throw consolidated error when batch fill partially fails with different coordinator operators', done => {
            nock(`${coordinatorEndpoint}${anotherCoordinatorPort}`)
                .post('/v1/request_transaction', () => true)
                .query({
                    networkId: 50,
                })
                .reply(200, serverApprovalSuccess);

            const signedOrders = [signedOrder, signedOrderWithDifferentCoordinatorOperator];
            const takerAssetFillAmounts = [takerTokenFillAmount, takerTokenFillAmount, takerTokenFillAmount];
            contractWrappers.coordinator
                .batchFillOrdersAsync(signedOrders, takerAssetFillAmounts, takerAddress)
                .then(res => {
                    expect(res).to.be.undefined();
                })
                .catch(err => {
                    expect(err.message).equal(CoordinatorServerErrorMsg.FillFailed);
                    expect(err.approvedOrders).to.deep.equal([signedOrderWithDifferentCoordinatorOperator]);
                    expect(err.cancellations).to.be.empty('array');

                    const errorBody = err.errors[0];
                    expect(errorBody.isError).to.be.true();
                    expect(errorBody.status).to.equal(400);
                    expect(errorBody.error).to.deep.equal(serverValidationError);
                    expect(errorBody.orders).to.deep.equal([signedOrder]);
                    expect(errorBody.coordinatorOperator).to.equal(`${coordinatorEndpoint}${coordinatorPort}`);
                    done();
                });
        });
        it('should throw consolidated error when batch fill totally fails with different coordinator operators', done => {
            nock(`${coordinatorEndpoint}${anotherCoordinatorPort}`)
                .post('/v1/request_transaction', () => true)
                .query({
                    networkId: 50,
                })
                .reply(400, serverValidationError);

            const signedOrders = [signedOrder, signedOrderWithDifferentCoordinatorOperator];
            const takerAssetFillAmounts = [takerTokenFillAmount, takerTokenFillAmount, takerTokenFillAmount];
            contractWrappers.coordinator
                .batchFillOrdersAsync(signedOrders, takerAssetFillAmounts, takerAddress)
                .then(res => {
                    expect(res).to.be.undefined();
                })
                .catch(err => {
                    expect(err.message).equal(CoordinatorServerErrorMsg.FillFailed);
                    expect(err.approvedOrders).to.be.empty('array');
                    expect(err.cancellations).to.be.empty('array');

                    const errorBody = err.errors[0];
                    expect(errorBody.isError).to.be.true();
                    expect(errorBody.status).to.equal(400);
                    expect(errorBody.error).to.deep.equal(serverValidationError);
                    expect(errorBody.orders).to.deep.equal([signedOrder]);
                    expect(errorBody.coordinatorOperator).to.equal(`${coordinatorEndpoint}${coordinatorPort}`);

                    const anotherErrorBody = err.errors[1];
                    expect(anotherErrorBody.isError).to.be.true();
                    expect(anotherErrorBody.status).to.equal(400);
                    expect(anotherErrorBody.error).to.deep.equal(serverValidationError);
                    expect(anotherErrorBody.orders).to.deep.equal([signedOrderWithDifferentCoordinatorOperator]);
                    expect(anotherErrorBody.coordinatorOperator).to.equal(
                        `${coordinatorEndpoint}${anotherCoordinatorPort}`,
                    );
                    done();
                });
        });
    });
});
// tslint:disable:max-file-line-count
