import { BigNumber, WSClient } from '@0x/mesh-rpc-client';
import * as sinon from 'sinon';

import { MeshOrderProvider } from '../../src';
import { BaseOrderProvider } from '../../src/order_provider/base_order_provider';
import { OrderStore } from '../../src/order_store';
import { utils } from '../../src/utils';
import { createOrder } from '../utils';

import { SERVER_PORT, setupServerAsync, stopServer } from './mock_ws_server';

describe('MeshOrderProvider', () => {
    let orderStore: OrderStore;
    let provider: BaseOrderProvider;
    const stubs: sinon.SinonStub[] = [];

    const websocketEndpoint = `ws://localhost:${SERVER_PORT}`;
    const makerAssetData = '0xf47261b000000000000000000000000089d24a6b4ccb1b6faa2625fe562bdd9a23260359';
    const takerAssetData = '0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
    const subscriptionId = 'subscriptionId';
    const addedResponse = {
        jsonrpc: '2.0',
        method: 'mesh_subscription',
        params: {
            subscription: subscriptionId,
            result: [
                {
                    orderHash: '0xa452cc6e2c7756376f0f2379e7dd81aa9285b26515774d0ad8801a4c243a30a3',
                    signedOrder: {
                        chainId: 1337,
                        exchangeAddress: '0x4eacd0af335451709e1e7b570b8ea68edec8bc97',
                        makerAddress: '0x8c5c2671b71bad73d8b6fb7e8ef6fe5ec95ff661',
                        makerAssetData,
                        makerFeeAssetData: '0x',
                        makerAssetAmount: '19501674723',
                        makerFee: '0',
                        takerAddress: '0x0000000000000000000000000000000000000000',
                        takerAssetData,
                        takerFeeAssetData: '0x',
                        takerAssetAmount: '132880707765170593819',
                        takerFee: '0',
                        senderAddress: '0x0000000000000000000000000000000000000000',
                        feeRecipientAddress: '0x0000000000000000000000000000000000000000',
                        expirationTimeSeconds: '1574687060',
                        salt: '1574686820004',
                        signature:
                            '0x1b64e67271f10832485356d9ef203b7e2c855067c1253b4e66ee06e85cd46427b157fc4c60f86bd637291f971d1443f65f631b76b887b7f82ebb36499f2f9cf10d03',
                    },
                    endState: 'ADDED',
                    fillableTakerAssetAmount: '132880707765170593819',
                    contractEvents: [],
                },
            ],
        },
    };
    const removedResponse = {
        ...addedResponse,
        ...{
            params: {
                ...addedResponse.params,
                result: [{ ...addedResponse.params.result[0], endState: 'CANCELLED', fillableTakerAssetAmount: '0' }],
            },
        },
    };

    let wsServer: any;
    let connection: any;
    afterEach(() => {
        void provider.destroyAsync();
        stubs.forEach(s => s.restore());
        stopServer();
    });
    beforeEach(async () => {
        orderStore = new OrderStore();
        stubs.push(
            sinon
                .stub(WSClient.prototype as any, '_startInternalLivenessCheckAsync')
                .callsFake(async () => Promise.resolve()),
        );
    });
    describe('#createSubscriptionForAssetPairAsync', () => {
        beforeEach(async () => {
            wsServer = await setupServerAsync();
            wsServer.on('connect', (conn: any) => {
                connection = conn;
                conn.on('message', (message: any) => {
                    const jsonRpcRequest = JSON.parse(message.utf8Data);
                    if (jsonRpcRequest.method === 'mesh_subscribe') {
                        connection.sendUTF(
                            JSON.stringify({
                                id: jsonRpcRequest.id,
                                jsonrpc: '2.0',
                                result: subscriptionId,
                            }),
                        );
                    }
                });
            });
        });
        test('fetches order on  first subscription', async () => {
            const getOrdersStub = sinon
                .stub(WSClient.prototype, 'getOrdersAsync')
                .callsFake(async () => Promise.resolve([]));
            stubs.push(getOrdersStub);
            const subscriptionStub = sinon
                .stub(WSClient.prototype, 'subscribeToOrdersAsync')
                .callsFake(async () => Promise.resolve('suscriptionId'));
            stubs.push(subscriptionStub);
            provider = new MeshOrderProvider({ websocketEndpoint }, orderStore);
            await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
            expect(getOrdersStub.callCount).toBe(1);
            expect(subscriptionStub.callCount).toBe(1);
        });
        test('fetches once when the same subscription is called', async () => {
            const stub = sinon.stub(WSClient.prototype, 'getOrdersAsync').callsFake(async () => Promise.resolve([]));
            stubs.push(stub);
            stubs.push(
                sinon
                    .stub(WSClient.prototype, 'subscribeToOrdersAsync')
                    .callsFake(async () => Promise.resolve(subscriptionId)),
            );
            provider = new MeshOrderProvider({ websocketEndpoint }, orderStore);
            await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
            await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
            await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
            expect(stub.callCount).toBe(1);
        });
        test('stores the orders', async () => {
            const order = createOrder(makerAssetData, takerAssetData);
            const orderInfo = {
                orderHash: '0x00',
                signedOrder: order.order,
                fillableTakerAssetAmount: new BigNumber(1),
            };
            stubs.push(
                sinon.stub(WSClient.prototype, 'getOrdersAsync').callsFake(async () => Promise.resolve([orderInfo])),
            );
            stubs.push(
                sinon
                    .stub(WSClient.prototype, 'subscribeToOrdersAsync')
                    .callsFake(async () => Promise.resolve(subscriptionId)),
            );
            provider = new MeshOrderProvider({ websocketEndpoint }, orderStore);
            await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
            const orders = orderStore.getOrderSetForAssets(makerAssetData, takerAssetData);
            expect(orders.size()).toBe(1);
        });
        test('stores the orders from a subscription update', async () => {
            const eventResponse = JSON.stringify(addedResponse);
            stubs.push(sinon.stub(WSClient.prototype, 'getOrdersAsync').callsFake(async () => Promise.resolve([])));
            provider = new MeshOrderProvider({ websocketEndpoint }, orderStore);
            await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
            connection.sendUTF(eventResponse);
            await utils.delayAsync(100);
            const orders = orderStore.getOrderSetForAssets(makerAssetData, takerAssetData);
            expect(orders.size()).toBe(1);
        });
        test('removes orders on a subscription update', async () => {
            const added = JSON.stringify(addedResponse);
            const removed = JSON.stringify(removedResponse);
            stubs.push(sinon.stub(WSClient.prototype, 'getOrdersAsync').callsFake(async () => Promise.resolve([])));
            provider = new MeshOrderProvider({ websocketEndpoint }, orderStore);
            await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
            connection.sendUTF(added);
            await utils.delayAsync(100);
            const orders = orderStore.getOrderSetForAssets(makerAssetData, takerAssetData);
            expect(orders.size()).toBe(1);
            connection.sendUTF(removed);
            await utils.delayAsync(100);
            expect(orders.size()).toBe(0);
        });
    });
    describe('reconnnect', () => {
        test.skip('revalidates all stored orders', async () => {
            wsServer = await setupServerAsync();
            const orderInfoResponse = {
                jsonrpc: '2.0',
                result: {
                    accepted: [],
                    rejected: [{ ...addedResponse.params.result[0], kind: 'CANCELLED', fillableTakerAssetAmount: 0 }],
                },
            };
            wsServer.on('connect', (conn: any) => {
                connection = conn;
                conn.on('message', (message: any) => {
                    const jsonRpcRequest = JSON.parse(message.utf8Data);
                    if (jsonRpcRequest.method === 'mesh_subscribe') {
                        connection.sendUTF(
                            JSON.stringify({
                                id: jsonRpcRequest.id,
                                jsonrpc: '2.0',
                                result: subscriptionId,
                            }),
                        );
                    } else if (jsonRpcRequest.method === 'mesh_addOrders') {
                        connection.sendUTF(
                            JSON.stringify({
                                id: jsonRpcRequest.id,
                                ...orderInfoResponse,
                            }),
                        );
                    }
                });
            });

            const getOrdersStub = sinon
                .stub(WSClient.prototype, 'getOrdersAsync')
                .callsFake(async () => Promise.resolve([]));
            const addOrdersStub = sinon
                .stub(WSClient.prototype, 'addOrdersAsync')
                .callsFake(async () => Promise.resolve({ accepted: [], rejected: [] }));
            stubs.push(getOrdersStub);
            stubs.push(addOrdersStub);
            provider = new MeshOrderProvider(
                {
                    websocketEndpoint,
                    wsOpts: {
                        reconnectAfter: 1,
                        clientConfig: {
                            fragmentOutgoingMessages: false,
                        },
                    },
                },
                orderStore,
            );
            await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
            const orders = orderStore.getOrderSetForAssets(makerAssetData, takerAssetData);
            expect(getOrdersStub.callCount).toBe(1);
            // Orders are not added on a subscription, only during reconnnect
            expect(addOrdersStub.callCount).toBe(0);
            const added = JSON.stringify(addedResponse);
            connection.sendUTF(added);
            await utils.delayAsync(100);
            expect(orders.size()).toBe(1);
            // Drop the connection and check orders are re-validated
            connection.drop();
            await utils.delayAsync(100);
            expect(addOrdersStub.callCount).toBe(1);
        });
    });
    describe('#getAvailableAssetDatasAsync', () => {
        test('stores the orders', async () => {
            const order = createOrder(makerAssetData, takerAssetData);
            const orderInfo = {
                orderHash: '0x00',
                signedOrder: order.order,
                fillableTakerAssetAmount: new BigNumber(1),
            };
            stubs.push(
                sinon.stub(WSClient.prototype, 'getOrdersAsync').callsFake(async () => Promise.resolve([orderInfo])),
            );
            stubs.push(
                sinon
                    .stub(WSClient.prototype, 'subscribeToOrdersAsync')
                    .callsFake(async () => Promise.resolve(subscriptionId)),
            );
            provider = new MeshOrderProvider({ websocketEndpoint }, orderStore);
            await provider.createSubscriptionForAssetPairAsync(makerAssetData, takerAssetData);
            const assetPairs = await provider.getAvailableAssetDatasAsync();
            expect(assetPairs.length).toBe(2);
            const assetDataA = {
                assetData: makerAssetData,
                maxAmount: new BigNumber(
                    '115792089237316195423570985008687907853269984665640564039457584007913129639935',
                ),
                minAmount: new BigNumber('0'),
                precision: 18,
            };
            const assetDataB = {
                ...assetDataA,
                assetData: takerAssetData,
            };
            expect(assetPairs).toMatchObject([
                { assetDataA, assetDataB },
                { assetDataA: assetDataB, assetDataB: assetDataA },
            ]);
        });
    });
});
