import { tokenUtils } from '@0x/dev-utils';
import { ETH_TOKEN_ADDRESS, FillQuoteTransformerOrderType, SignatureType } from '@0x/protocol-utils';
import { TakerRequestQueryParamsUnnested, V4RFQIndicativeQuote } from '@0x/quote-server';
import { StatusCodes } from '@0x/types';
import { BigNumber, logUtils } from '@0x/utils';
import Axios from 'axios';
import * as chai from 'chai';
import { Agent as HttpAgent } from 'http';
import { Agent as HttpsAgent } from 'https';
import _ = require('lodash');
import 'mocha';

import { constants, KEEP_ALIVE_TTL } from '../src/constants';
import {
    AltMockedRfqQuoteResponse,
    AltQuoteModel,
    AltQuoteRequestData,
    AltQuoteSide,
    AltRfqMakerAssetOfferings,
    MarketOperation,
    MockedRfqQuoteResponse,
} from '../src/types';
import { NULL_ADDRESS } from '../src/utils/market_operation_utils/constants';
import { QuoteRequestor } from '../src/utils/quote_requestor';

import { chaiSetup } from './utils/chai_setup';
import { RfqQuoteEndpoint, testHelpers } from './utils/test_helpers';

const quoteRequestorHttpClient = Axios.create({
    httpAgent: new HttpAgent({ keepAlive: true, timeout: KEEP_ALIVE_TTL }),
    httpsAgent: new HttpsAgent({ keepAlive: true, timeout: KEEP_ALIVE_TTL }),
});

chaiSetup.configure();
const expect = chai.expect;
const ALT_MM_API_KEY = 'averysecurekey';
const ALT_PROFILE = 'acoolprofile';
const ALT_RFQ_CREDS = {
    altRfqApiKey: ALT_MM_API_KEY,
    altRfqProfile: ALT_PROFILE,
};

const CREATED_STATUS_CODE = 201;

function makeThreeMinuteExpiry(): BigNumber {
    const expiry = new Date(Date.now());
    expiry.setMinutes(expiry.getMinutes() + 3);
    return new BigNumber(Math.round(expiry.valueOf() / constants.ONE_SECOND_MS));
}

describe('QuoteRequestor', async () => {
    const [makerToken, takerToken, otherToken1] = tokenUtils.getDummyERC20TokenAddresses();
    const validSignature = { v: 28, r: '0x', s: '0x', signatureType: SignatureType.EthSign };

    const altRfqAssetOfferings: AltRfqMakerAssetOfferings = {
        'https://132.0.0.1': [
            {
                id: 'XYZ-123',
                baseAsset: makerToken,
                quoteAsset: takerToken,
                baseAssetDecimals: 2,
                quoteAssetDecimals: 3,
            },
        ],
    };

    describe('requestRfqmFirmQuotesAsync for firm quotes', async () => {
        it('should return successful RFQM requests', async () => {
            const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a';
            const txOrigin = takerAddress;
            const apiKey = 'my-ko0l-api-key';

            // Set up RFQM responses
            // tslint:disable-next-line:array-type
            const mockedRequests: MockedRfqQuoteResponse[] = [];
            const altMockedRequests: AltMockedRfqQuoteResponse[] = [];

            const expectedParams: TakerRequestQueryParamsUnnested = {
                sellTokenAddress: takerToken,
                buyTokenAddress: makerToken,
                sellAmountBaseUnits: '10000',
                comparisonPrice: undefined,
                takerAddress,
                txOrigin,
                isLastLook: 'true', // the major difference between RFQ-T and RFQ-M
                protocolVersion: '4',
                feeAmount: '1000000000',
                feeToken: ETH_TOKEN_ADDRESS,
                feeType: 'fixed',
            };
            const mockedDefaults = {
                requestApiKey: apiKey,
                requestParams: expectedParams,
                responseCode: StatusCodes.Success,
            };
            const validSignedOrder = {
                makerToken,
                takerToken,
                makerAmount: new BigNumber('1000'),
                takerAmount: new BigNumber('1000'),
                maker: takerAddress,
                taker: takerAddress,
                pool: '0x',
                salt: '0',
                chainId: 1,
                verifyingContract: takerAddress,
                txOrigin,
                expiry: makeThreeMinuteExpiry(),
                signature: validSignature,
            };
            // request is to sell 10000 units of the base token
            // 10 units at 3 decimals
            const altFirmRequestData = {
                market: 'XYZ-123',
                model: AltQuoteModel.Firm,
                profile: ALT_PROFILE,
                side: AltQuoteSide.Sell,
                meta: {
                    txOrigin,
                    taker: takerAddress,
                    client: apiKey,
                },
                value: '10',
            };
            const altFirmResponse = {
                ...altFirmRequestData,
                id: 'random_id',
                // tslint:disable-next-line:custom-no-magic-numbers
                price: new BigNumber(10 / 100).toString(),
                status: 'active',
                data: {
                    '0xv4order': validSignedOrder,
                },
            };

            // [GOOD] Successful response
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://1337.0.0.1',
                responseData: {
                    signedOrder: validSignedOrder,
                },
            });
            // [GOOD] Another Successful response
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://37.0.0.1',
                responseData: { signedOrder: validSignedOrder },
            });
            // [BAD] Test out a bad response code, ensure it doesnt cause throw
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://420.0.0.1',
                responseData: { error: 'bad request' },
                responseCode: StatusCodes.InternalError,
            });
            // [BAD] Test out a successful response code but a partial order
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://421.0.0.1',
                responseData: { signedOrder: { makerToken: '123' } },
            });
            // [BAD] A successful response code and invalid response data (encoding)
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://421.1.0.1',
                responseData: 'this is not JSON!',
            });
            // [BAD] A successful response code and valid order, but for wrong maker asset data
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://422.0.0.1',
                responseData: { signedOrder: { ...validSignedOrder, makerToken: '0x1234' } },
            });
            // [BAD] A successful response code and valid order, but for wrong taker asset data
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://423.0.0.1',
                responseData: { signedOrder: { ...validSignedOrder, takerToken: '0x1234' } },
            });
            // [BAD] A successful response code and good order but its unsigned
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://424.0.0.1',
                responseData: { signedOrder: _.omit(validSignedOrder, ['signature']) },
            });
            // [BAD] A successful response code and good order but for the wrong txOrigin
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://425.0.0.1',
                responseData: { signedOrder: { ...validSignedOrder, txOrigin: NULL_ADDRESS } },
            });
            // [GOOD] A successful response code and order from an alt RFQ implementation
            altMockedRequests.push({
                endpoint: 'https://132.0.0.1',
                mmApiKey: ALT_MM_API_KEY,
                responseCode: CREATED_STATUS_CODE,
                requestData: altFirmRequestData,
                responseData: altFirmResponse,
            });

            const normalizedSuccessfulOrder = {
                order: {
                    ..._.omit(validSignedOrder, ['signature']),
                    makerAmount: new BigNumber(validSignedOrder.makerAmount),
                    takerAmount: new BigNumber(validSignedOrder.takerAmount),
                    expiry: new BigNumber(validSignedOrder.expiry),
                    salt: new BigNumber(validSignedOrder.salt),
                },
                signature: validSignedOrder.signature,
                type: FillQuoteTransformerOrderType.Rfq,
            };

            return testHelpers.withMockedRfqQuotes(
                mockedRequests,
                altMockedRequests,
                RfqQuoteEndpoint.Firm,
                async () => {
                    const qr = new QuoteRequestor(
                        {}, // No RFQ-T asset offerings
                        {
                            'https://1337.0.0.1': [[makerToken, takerToken]],
                            'https://420.0.0.1': [[makerToken, takerToken]],
                            'https://421.0.0.1': [[makerToken, takerToken]],
                            'https://421.1.0.1': [[makerToken, takerToken]],
                            'https://422.0.0.1': [[makerToken, takerToken]],
                            'https://423.0.0.1': [[makerToken, takerToken]],
                            'https://424.0.0.1': [[makerToken, takerToken]],
                            'https://425.0.0.1': [[makerToken, takerToken]],
                            'https://426.0.0.1': [] /* Shouldn't ping an RFQ provider when they don't support the requested asset pair. */,
                            'https://37.0.0.1': [[makerToken, takerToken]],
                        },
                        quoteRequestorHttpClient,
                        ALT_RFQ_CREDS,
                    );
                    const resp = await qr.requestRfqmFirmQuotesAsync(
                        makerToken,
                        takerToken,
                        new BigNumber(10000),
                        MarketOperation.Sell,
                        undefined,
                        {
                            apiKey,
                            takerAddress,
                            txOrigin: takerAddress,
                            intentOnFilling: true,
                            altRfqAssetOfferings,
                            isLastLook: true,
                            fee: {
                                amount: new BigNumber('1000000000'),
                                token: ETH_TOKEN_ADDRESS,
                                type: 'fixed',
                            },
                        },
                    );
                    expect(resp).to.deep.eq([
                        normalizedSuccessfulOrder,
                        normalizedSuccessfulOrder,
                        normalizedSuccessfulOrder,
                    ]);
                },
                quoteRequestorHttpClient,
            );
        });
    });
    describe('requestRfqtFirmQuotesAsync for firm quotes', async () => {
        it('should return successful RFQT requests', async () => {
            const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a';
            const txOrigin = takerAddress;
            const apiKey = 'my-ko0l-api-key';

            // Set up RFQT responses
            // tslint:disable-next-line:array-type
            const mockedRequests: MockedRfqQuoteResponse[] = [];
            const altMockedRequests: AltMockedRfqQuoteResponse[] = [];

            const expectedParams: TakerRequestQueryParamsUnnested = {
                sellTokenAddress: takerToken,
                buyTokenAddress: makerToken,
                sellAmountBaseUnits: '10000',
                comparisonPrice: undefined,
                takerAddress,
                txOrigin,
                protocolVersion: '4',
            };
            const mockedDefaults = {
                requestApiKey: apiKey,
                requestParams: expectedParams,
                responseCode: StatusCodes.Success,
            };
            const validSignedOrder = {
                makerToken,
                takerToken,
                makerAmount: new BigNumber('1000'),
                takerAmount: new BigNumber('1000'),
                maker: takerAddress,
                taker: takerAddress,
                pool: '0x',
                salt: '0',
                chainId: 1,
                verifyingContract: takerAddress,
                txOrigin,
                expiry: makeThreeMinuteExpiry(),
                signature: validSignature,
            };
            // request is to sell 10000 units of the base token
            // 10 units at 3 decimals
            const altFirmRequestData = {
                market: 'XYZ-123',
                model: AltQuoteModel.Firm,
                profile: ALT_PROFILE,
                side: AltQuoteSide.Sell,
                meta: {
                    txOrigin,
                    taker: takerAddress,
                    client: apiKey,
                },
                value: '10',
            };
            const altFirmResponse = {
                ...altFirmRequestData,
                id: 'random_id',
                // tslint:disable-next-line:custom-no-magic-numbers
                price: new BigNumber(10 / 100).toString(),
                status: 'active',
                data: {
                    '0xv4order': validSignedOrder,
                },
            };

            // Successful response
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://1337.0.0.1',
                responseData: {
                    signedOrder: validSignedOrder,
                },
            });
            // Another Successful response
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://37.0.0.1',
                responseData: { signedOrder: validSignedOrder },
            });
            // Test out a bad response code, ensure it doesnt cause throw
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://420.0.0.1',
                responseData: { error: 'bad request' },
                responseCode: StatusCodes.InternalError,
            });
            // Test out a successful response code but a partial order
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://421.0.0.1',
                responseData: { signedOrder: { makerToken: '123' } },
            });
            // A successful response code and invalid response data (encoding)
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://421.1.0.1',
                responseData: 'this is not JSON!',
            });
            // A successful response code and valid order, but for wrong maker asset data
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://422.0.0.1',
                responseData: { signedOrder: { ...validSignedOrder, makerToken: '0x1234' } },
            });
            // A successful response code and valid order, but for wrong taker asset data
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://423.0.0.1',
                responseData: { signedOrder: { ...validSignedOrder, takerToken: '0x1234' } },
            });
            // A successful response code and good order but its unsigned
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://424.0.0.1',
                responseData: { signedOrder: _.omit(validSignedOrder, ['signature']) },
            });
            // A successful response code and good order but for the wrong txOrigin
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://425.0.0.1',
                responseData: { signedOrder: { ...validSignedOrder, txOrigin: NULL_ADDRESS } },
            });
            // A successful response code and order from an alt RFQ implementation
            altMockedRequests.push({
                endpoint: 'https://132.0.0.1',
                mmApiKey: ALT_MM_API_KEY,
                responseCode: CREATED_STATUS_CODE,
                requestData: altFirmRequestData,
                responseData: altFirmResponse,
            });

            const normalizedSuccessfulOrder = {
                order: {
                    ..._.omit(validSignedOrder, ['signature']),
                    makerAmount: new BigNumber(validSignedOrder.makerAmount),
                    takerAmount: new BigNumber(validSignedOrder.takerAmount),
                    expiry: new BigNumber(validSignedOrder.expiry),
                    salt: new BigNumber(validSignedOrder.salt),
                },
                signature: validSignedOrder.signature,
                type: FillQuoteTransformerOrderType.Rfq,
            };

            return testHelpers.withMockedRfqQuotes(
                mockedRequests,
                altMockedRequests,
                RfqQuoteEndpoint.Firm,
                async () => {
                    const qr = new QuoteRequestor(
                        {
                            'https://1337.0.0.1': [[makerToken, takerToken]],
                            'https://420.0.0.1': [[makerToken, takerToken]],
                            'https://421.0.0.1': [[makerToken, takerToken]],
                            'https://421.1.0.1': [[makerToken, takerToken]],
                            'https://422.0.0.1': [[makerToken, takerToken]],
                            'https://423.0.0.1': [[makerToken, takerToken]],
                            'https://424.0.0.1': [[makerToken, takerToken]],
                            'https://425.0.0.1': [[makerToken, takerToken]],
                            'https://426.0.0.1': [] /* Shouldn't ping an RFQ-T provider when they don't support the requested asset pair. */,
                            'https://37.0.0.1': [[makerToken, takerToken]],
                        },
                        {},
                        quoteRequestorHttpClient,
                        ALT_RFQ_CREDS,
                    );
                    const resp = await qr.requestRfqtFirmQuotesAsync(
                        makerToken,
                        takerToken,
                        new BigNumber(10000),
                        MarketOperation.Sell,
                        undefined,
                        {
                            apiKey,
                            takerAddress,
                            txOrigin: takerAddress,
                            intentOnFilling: true,
                            altRfqAssetOfferings,
                        },
                    );
                    expect(resp).to.deep.eq([
                        normalizedSuccessfulOrder,
                        normalizedSuccessfulOrder,
                        normalizedSuccessfulOrder,
                    ]);
                },
                quoteRequestorHttpClient,
            );
        });
    });
    describe('requestRfqmIndicativeQuotesAsync for Indicative quotes', async () => {
        it('should return successful RFQM requests', async () => {
            const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a';
            const apiKey = 'my-ko0l-api-key';

            // Set up RFQ responses
            // tslint:disable-next-line:array-type
            const mockedRequests: MockedRfqQuoteResponse[] = [];
            const expectedParams: TakerRequestQueryParamsUnnested = {
                sellTokenAddress: takerToken,
                buyTokenAddress: makerToken,
                sellAmountBaseUnits: '10000',
                comparisonPrice: undefined,
                takerAddress,
                txOrigin: takerAddress,
                isLastLook: 'true', // the major difference between RFQ-T and RFQ-M
                protocolVersion: '4',
                feeAmount: '1000000000',
                feeToken: ETH_TOKEN_ADDRESS,
                feeType: 'fixed',
            };
            const mockedDefaults = {
                requestApiKey: apiKey,
                requestParams: expectedParams,
                responseCode: StatusCodes.Success,
            };

            // [GOOD] Successful response
            const successfulQuote1 = {
                makerToken,
                takerToken,
                makerAmount: new BigNumber(expectedParams.sellAmountBaseUnits),
                takerAmount: new BigNumber(expectedParams.sellAmountBaseUnits),
                expiry: makeThreeMinuteExpiry(),
            };

            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://1337.0.0.1',
                responseData: successfulQuote1,
            });
            // [GOOD] Another Successful response
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://37.0.0.1',
                responseData: successfulQuote1,
            });

            // [BAD] Test out a bad response code, ensure it doesnt cause throw
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://420.0.0.1',
                responseData: { error: 'bad request' },
                responseCode: StatusCodes.InternalError,
            });
            // [BAD] Test out a successful response code but an invalid order
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://421.0.0.1',
                responseData: { makerToken: '123' },
            });
            // [BAD] A successful response code and valid response data, but for wrong maker asset data
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://422.0.0.1',
                responseData: { ...successfulQuote1, makerToken: otherToken1 },
            });
            // [BAD] A successful response code and valid response data, but for wrong taker asset data
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://423.0.0.1',
                responseData: { ...successfulQuote1, takerToken: otherToken1 },
            });

            return testHelpers.withMockedRfqQuotes(
                mockedRequests,
                [],
                RfqQuoteEndpoint.Indicative,
                async () => {
                    const qr = new QuoteRequestor(
                        {}, // No RFQ-T asset offerings
                        {
                            'https://1337.0.0.1': [[makerToken, takerToken]],
                            'https://37.0.0.1': [[makerToken, takerToken]],
                            'https://420.0.0.1': [[makerToken, takerToken]],
                            'https://421.0.0.1': [[makerToken, takerToken]],
                            'https://422.0.0.1': [[makerToken, takerToken]],
                            'https://423.0.0.1': [[makerToken, takerToken]],
                            'https://424.0.0.1': [[makerToken, takerToken]],
                        },
                        quoteRequestorHttpClient,
                    );
                    const resp = await qr.requestRfqmIndicativeQuotesAsync(
                        makerToken,
                        takerToken,
                        new BigNumber(10000),
                        MarketOperation.Sell,
                        undefined,
                        {
                            apiKey,
                            takerAddress,
                            txOrigin: takerAddress,
                            intentOnFilling: true,
                            isLastLook: true,
                            fee: {
                                type: 'fixed',
                                token: ETH_TOKEN_ADDRESS,
                                amount: new BigNumber('1000000000'),
                            },
                        },
                    );
                    expect(resp.sort()).to.eql([successfulQuote1, successfulQuote1].sort());
                },
                quoteRequestorHttpClient,
            );
        });
    });
    describe('requestRfqtIndicativeQuotesAsync for Indicative quotes', async () => {
        it('should optionally accept a "comparisonPrice" parameter', async () => {
            const response = QuoteRequestor.makeQueryParameters(
                otherToken1, // tx origin
                otherToken1, // taker
                MarketOperation.Sell,
                makerToken,
                takerToken,
                new BigNumber(1000),
                new BigNumber(300.2),
            );
            expect(response.comparisonPrice).to.eql('300.2');
        });
        it('should return successful RFQT requests', async () => {
            const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a';
            const apiKey = 'my-ko0l-api-key';

            // Set up RFQT responses
            // tslint:disable-next-line:array-type
            const mockedRequests: MockedRfqQuoteResponse[] = [];
            const expectedParams: TakerRequestQueryParamsUnnested = {
                sellTokenAddress: takerToken,
                buyTokenAddress: makerToken,
                sellAmountBaseUnits: '10000',
                comparisonPrice: undefined,
                takerAddress,
                txOrigin: takerAddress,
                protocolVersion: '4',
            };
            const mockedDefaults = {
                requestApiKey: apiKey,
                requestParams: expectedParams,
                responseCode: StatusCodes.Success,
            };

            // Successful response
            const successfulQuote1 = {
                makerToken,
                takerToken,
                makerAmount: new BigNumber(expectedParams.sellAmountBaseUnits),
                takerAmount: new BigNumber(expectedParams.sellAmountBaseUnits),
                expiry: makeThreeMinuteExpiry(),
            };

            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://1337.0.0.1',
                responseData: successfulQuote1,
            });
            // Test out a bad response code, ensure it doesnt cause throw
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://420.0.0.1',
                responseData: { error: 'bad request' },
                responseCode: StatusCodes.InternalError,
            });
            // Test out a successful response code but an invalid order
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://421.0.0.1',
                responseData: { makerToken: '123' },
            });
            // A successful response code and valid response data, but for wrong maker asset data
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://422.0.0.1',
                responseData: { ...successfulQuote1, makerToken: otherToken1 },
            });
            // A successful response code and valid response data, but for wrong taker asset data
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://423.0.0.1',
                responseData: { ...successfulQuote1, takerToken: otherToken1 },
            });
            // Another Successful response
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://37.0.0.1',
                responseData: successfulQuote1,
            });

            return testHelpers.withMockedRfqQuotes(
                mockedRequests,
                [],
                RfqQuoteEndpoint.Indicative,
                async () => {
                    const qr = new QuoteRequestor(
                        {
                            'https://1337.0.0.1': [[makerToken, takerToken]],
                            'https://420.0.0.1': [[makerToken, takerToken]],
                            'https://421.0.0.1': [[makerToken, takerToken]],
                            'https://422.0.0.1': [[makerToken, takerToken]],
                            'https://423.0.0.1': [[makerToken, takerToken]],
                            'https://424.0.0.1': [[makerToken, takerToken]],
                            'https://37.0.0.1': [[makerToken, takerToken]],
                        },
                        {},
                        quoteRequestorHttpClient,
                    );
                    const resp = await qr.requestRfqtIndicativeQuotesAsync(
                        makerToken,
                        takerToken,
                        new BigNumber(10000),
                        MarketOperation.Sell,
                        undefined,
                        {
                            apiKey,
                            takerAddress,
                            txOrigin: takerAddress,
                            intentOnFilling: true,
                        },
                    );
                    expect(resp.sort()).to.eql([successfulQuote1, successfulQuote1].sort());
                },
                quoteRequestorHttpClient,
            );
        });
        it('should only return RFQT requests that meet the timeout', async () => {
            const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a';
            const apiKey = 'my-ko0l-api-key';
            const maxTimeoutMs = 10;
            // tslint:disable-next-line:custom-no-magic-numbers
            const exceedTimeoutMs = maxTimeoutMs + 50;

            // Set up RFQT responses
            // tslint:disable-next-line:array-type
            const mockedRequests: MockedRfqQuoteResponse[] = [];
            const expectedParams: TakerRequestQueryParamsUnnested = {
                sellTokenAddress: takerToken,
                buyTokenAddress: makerToken,
                sellAmountBaseUnits: '10000',
                comparisonPrice: undefined,
                takerAddress,
                txOrigin: takerAddress,
                protocolVersion: '4',
            };
            const mockedDefaults = {
                requestApiKey: apiKey,
                requestParams: expectedParams,
                responseCode: StatusCodes.Success,
            };

            // Successful response
            const successfulQuote1 = {
                makerToken,
                takerToken,
                makerAmount: new BigNumber(expectedParams.sellAmountBaseUnits),
                takerAmount: new BigNumber(expectedParams.sellAmountBaseUnits),
                expiry: makeThreeMinuteExpiry(),
            };

            // One good request
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://1337.0.0.1',
                responseData: successfulQuote1,
            });

            // One request that will timeout
            mockedRequests.push({
                ...mockedDefaults,
                endpoint: 'https://420.0.0.1',
                responseData: successfulQuote1,
                callback: async () => {
                    // tslint:disable-next-line:no-inferred-empty-object-type
                    return new Promise((resolve, reject) => {
                        setTimeout(() => {
                            resolve([StatusCodes.Success, successfulQuote1]);
                        }, exceedTimeoutMs);
                    });
                },
            });

            return testHelpers.withMockedRfqQuotes(
                mockedRequests,
                [],
                RfqQuoteEndpoint.Indicative,
                async () => {
                    const qr = new QuoteRequestor(
                        {
                            'https://1337.0.0.1': [[makerToken, takerToken]],
                            'https://420.0.0.1': [[makerToken, takerToken]],
                        },
                        {},
                        quoteRequestorHttpClient,
                    );
                    const resp = await qr.requestRfqtIndicativeQuotesAsync(
                        makerToken,
                        takerToken,
                        new BigNumber(10000),
                        MarketOperation.Sell,
                        undefined,
                        {
                            apiKey,
                            takerAddress,
                            txOrigin: takerAddress,
                            intentOnFilling: true,
                            makerEndpointMaxResponseTimeMs: maxTimeoutMs,
                        },
                    );
                    expect(resp.sort()).to.eql([successfulQuote1].sort()); // notice only one result, despite two requests made
                },
                quoteRequestorHttpClient,
            );
        });
        it('should return successful RFQT indicative quote requests (Buy)', async () => {
            const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a';
            const apiKey = 'my-ko0l-api-key';

            // Set up RFQT responses
            // tslint:disable-next-line:array-type
            const mockedRequests: MockedRfqQuoteResponse[] = [];
            const expectedParams: TakerRequestQueryParamsUnnested = {
                sellTokenAddress: takerToken,
                buyTokenAddress: makerToken,
                buyAmountBaseUnits: '10000',
                comparisonPrice: undefined,
                takerAddress,
                txOrigin: takerAddress,
                protocolVersion: '4',
            };
            // Successful response
            const successfulQuote1 = {
                makerToken,
                takerToken,
                makerAmount: new BigNumber(expectedParams.buyAmountBaseUnits),
                takerAmount: new BigNumber(expectedParams.buyAmountBaseUnits),
                expiry: makeThreeMinuteExpiry(),
            };
            mockedRequests.push({
                endpoint: 'https://1337.0.0.1',
                requestApiKey: apiKey,
                requestParams: expectedParams,
                responseData: successfulQuote1,
                responseCode: StatusCodes.Success,
            });

            return testHelpers.withMockedRfqQuotes(
                mockedRequests,
                [],
                RfqQuoteEndpoint.Indicative,
                async () => {
                    const qr = new QuoteRequestor(
                        { 'https://1337.0.0.1': [[makerToken, takerToken]] },
                        {},
                        quoteRequestorHttpClient,
                    );
                    const resp = await qr.requestRfqtIndicativeQuotesAsync(
                        makerToken,
                        takerToken,
                        new BigNumber(10000),
                        MarketOperation.Buy,
                        undefined,
                        {
                            apiKey,
                            takerAddress,
                            txOrigin: takerAddress,
                            intentOnFilling: true,
                        },
                    );
                    expect(resp.sort()).to.eql([successfulQuote1].sort());
                },
                quoteRequestorHttpClient,
            );
        });
        it('should return successful alt indicative quotes', async () => {
            const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a';
            const txOrigin = '0xf209925defc99488e3afff1174e48b4fa628302a';
            const apiKey = 'my-ko0l-api-key';

            // base token has 2 decimals
            // quote token has 3 decimals
            const baseToken = makerToken;
            const quoteToken = takerToken;

            // Set up RFQT responses
            const altMockedRequests: AltMockedRfqQuoteResponse[] = [];
            const altScenarios: Array<{
                successfulQuote: V4RFQIndicativeQuote;
                requestedMakerToken: string;
                requestedTakerToken: string;
                requestedAmount: BigNumber;
                requestedOperation: MarketOperation;
            }> = [];

            // SCENARIO 1
            // buy, base asset specified
            // requesting to buy 100 units (10000 base units) of the base token
            // returning a price of 0.01, which should mean 10000 maker, 1000 taker amount
            const buyAmountAltRequest: AltQuoteRequestData = {
                market: 'XYZ-123',
                model: AltQuoteModel.Indicative,
                profile: ALT_PROFILE,
                side: AltQuoteSide.Sell,
                meta: {
                    txOrigin,
                    taker: takerAddress,
                    client: apiKey,
                },
                amount: '100',
            };
            // Successful response
            const buyAmountAltResponse = {
                ...buyAmountAltRequest,
                id: 'random_id',
                // tslint:disable-next-line:custom-no-magic-numbers
                price: new BigNumber(0.01).toString(),
                status: 'live',
            };
            const successfulBuyAmountQuote: V4RFQIndicativeQuote = {
                makerToken: baseToken,
                takerToken: quoteToken,
                makerAmount: new BigNumber(10000),
                takerAmount: new BigNumber(1000),
                expiry: new BigNumber(0),
            };
            altMockedRequests.push({
                endpoint: 'https://132.0.0.1',
                mmApiKey: ALT_MM_API_KEY,
                responseCode: CREATED_STATUS_CODE,
                requestData: buyAmountAltRequest,
                responseData: buyAmountAltResponse,
            });
            altScenarios.push({
                successfulQuote: successfulBuyAmountQuote,
                requestedMakerToken: baseToken,
                requestedTakerToken: quoteToken,
                requestedAmount: new BigNumber(10000),
                requestedOperation: MarketOperation.Buy,
            });

            // SCENARIO 2
            // alt buy, quote asset specified
            // user is requesting to sell 1 unit of the quote token, or 1000 base units
            // returning a price of 0.01, which should mean 10000 maker amount, 1000 taker amount
            const buyValueAltRequest: AltQuoteRequestData = {
                market: 'XYZ-123',
                model: AltQuoteModel.Indicative,
                profile: ALT_PROFILE,
                side: AltQuoteSide.Sell,
                meta: {
                    txOrigin,
                    taker: takerAddress,
                    client: apiKey,
                },
                value: '1',
            };
            // Successful response
            const buyValueAltResponse = {
                ...buyValueAltRequest,
                id: 'random_id',
                // tslint:disable-next-line:custom-no-magic-numbers
                price: new BigNumber(0.01).toString(),
                status: 'live',
            };
            const successfulBuyValueQuote: V4RFQIndicativeQuote = {
                makerToken: baseToken,
                takerToken: quoteToken,
                makerAmount: new BigNumber(10000),
                takerAmount: new BigNumber(1000),
                expiry: new BigNumber(0),
            };
            altMockedRequests.push({
                endpoint: 'https://132.0.0.1',
                mmApiKey: ALT_MM_API_KEY,
                responseCode: CREATED_STATUS_CODE,
                requestData: buyValueAltRequest,
                responseData: buyValueAltResponse,
            });
            altScenarios.push({
                successfulQuote: successfulBuyValueQuote,
                requestedMakerToken: baseToken,
                requestedTakerToken: quoteToken,
                requestedAmount: new BigNumber(1000),
                requestedOperation: MarketOperation.Sell,
            });

            // SCENARIO 3
            // alt sell, base asset specified
            // user is requesting to sell 100 units (10000 base units) of the base token
            // returning a price of 0.01, which should mean 10000 taker amount, 1000 maker amount
            const sellAmountAltRequest: AltQuoteRequestData = {
                market: 'XYZ-123',
                model: AltQuoteModel.Indicative,
                profile: ALT_PROFILE,
                side: AltQuoteSide.Buy,
                meta: {
                    txOrigin,
                    taker: takerAddress,
                    client: apiKey,
                },
                amount: '100',
            };
            // Successful response
            const sellAmountAltResponse = {
                ...sellAmountAltRequest,
                id: 'random_id',
                // tslint:disable-next-line:custom-no-magic-numbers
                price: new BigNumber(0.01).toString(),
                status: 'live',
            };
            const successfulSellAmountQuote: V4RFQIndicativeQuote = {
                makerToken: quoteToken,
                takerToken: baseToken,
                makerAmount: new BigNumber(1000),
                takerAmount: new BigNumber(10000),
                expiry: new BigNumber(0),
            };
            altMockedRequests.push({
                endpoint: 'https://132.0.0.1',
                mmApiKey: ALT_MM_API_KEY,
                responseCode: CREATED_STATUS_CODE,
                requestData: sellAmountAltRequest,
                responseData: sellAmountAltResponse,
            });
            altScenarios.push({
                successfulQuote: successfulSellAmountQuote,
                requestedMakerToken: quoteToken,
                requestedTakerToken: baseToken,
                requestedAmount: new BigNumber(10000),
                requestedOperation: MarketOperation.Sell,
            });

            // SCENARIO 4
            // alt sell, quote asset specified
            // user is requesting to buy 1 unit (1000 base units) of the quote token
            // returning a price of 0.01, which should mean 10000 taker amount, 1000 maker amount
            const sellValueAltRequest: AltQuoteRequestData = {
                market: 'XYZ-123',
                model: AltQuoteModel.Indicative,
                profile: ALT_PROFILE,
                side: AltQuoteSide.Buy,
                meta: {
                    txOrigin,
                    taker: takerAddress,
                    client: apiKey,
                },
                value: '1',
            };
            // Successful response
            const sellValueAltResponse = {
                ...sellValueAltRequest,
                id: 'random_id',
                // tslint:disable-next-line:custom-no-magic-numbers
                price: new BigNumber(0.01).toString(),
                status: 'live',
            };
            const successfulSellValueQuote: V4RFQIndicativeQuote = {
                makerToken: quoteToken,
                takerToken: baseToken,
                makerAmount: new BigNumber(1000),
                takerAmount: new BigNumber(10000),
                expiry: new BigNumber(0),
            };
            altMockedRequests.push({
                endpoint: 'https://132.0.0.1',
                mmApiKey: ALT_MM_API_KEY,
                responseCode: CREATED_STATUS_CODE,
                requestData: sellValueAltRequest,
                responseData: sellValueAltResponse,
            });
            altScenarios.push({
                successfulQuote: successfulSellValueQuote,
                requestedMakerToken: quoteToken,
                requestedTakerToken: baseToken,
                requestedAmount: new BigNumber(1000),
                requestedOperation: MarketOperation.Buy,
            });

            let scenarioCounter = 1;
            for (const altScenario of altScenarios) {
                logUtils.log(`Alt MM indicative scenario ${scenarioCounter}`);
                scenarioCounter += 1;
                await testHelpers.withMockedRfqQuotes(
                    [],
                    altMockedRequests,
                    RfqQuoteEndpoint.Indicative,
                    async () => {
                        const qr = new QuoteRequestor({}, {}, quoteRequestorHttpClient, ALT_RFQ_CREDS);
                        const resp = await qr.requestRfqtIndicativeQuotesAsync(
                            altScenario.requestedMakerToken,
                            altScenario.requestedTakerToken,
                            altScenario.requestedAmount,
                            altScenario.requestedOperation,
                            undefined,
                            {
                                apiKey,
                                takerAddress,
                                txOrigin,
                                intentOnFilling: true,
                                altRfqAssetOfferings,
                            },
                        );
                        // hack to get the expiry right, since it's dependent on the current timestamp
                        const expected = { ...altScenario.successfulQuote, expiry: resp[0].expiry };
                        expect(resp.sort()).to.eql([expected].sort());
                    },
                    quoteRequestorHttpClient,
                );
            }
        });
    });
});
