'use strict';

import should from 'should';
import sinon from 'sinon';
import MetaApiWebsocketClient from './metaApiWebsocket.client';
import * as helpers from '../../helpers/helpers';
import Server from 'socket.io';
import log4js from 'log4js';
import {ForbiddenError} from '../errorHandler';
import MetaApi from '../../metaApi/metaApi';
import {AssertionError} from 'assert';

/**
 * @test {MetaApiWebsocketClient}
 */
// eslint-disable-next-line max-statements
describe('MetaApiWebsocketClient', () => {

  const logger = log4js.getLogger('test');

  let io;
  let server;
  let server1;
  let serverNewYork;
  let clock;
  let client: MetaApiWebsocketClient;
  let sandbox = sinon.createSandbox();
  let activeSynchronizationIdsStub;

  const stubs = {
    latencyService: {
      onConnected: sinon.stub(),
      onDisconnected: sinon.stub(),
      onUnsubscribe: sinon.stub(),
      onDealsSynchronized: sinon.stub(),
      getActiveInstances: sinon.stub(),
      waitConnectedInstance: sinon.stub()
    }
  };
  const metaApi = {
    _connectionRegistry: {
      rpcConnections: {},
      streamingConnections: {}
    }
  };
  const synchronizationThrottler = {
    activeSynchronizationIds: ['synchronizationId'],
    onDisconnect: () => {},
    updateSynchronizationId: () => {},
    removeSynchronizationId: () => {},
    scheduleSynchronize: () => {}
  };
  const domainClient = {
    getSettings: () => {}
  };
  const commonOptions: Partial<MetaApiWebsocketClient.Options> = {
    useNativeSocketIoServer: true
  };
  const options = {
    application: 'application',
    domain: 'project-stock.agiliumlabs.cloud',
    requestTimeout: 1.5,
    useSharedClientApi: true,
    disableInternalJobs: true,
    retryOpts: {retries: 3, minDelayInSeconds: 0.1, maxDelayInSeconds: 0.5},
    region: undefined,
    ...commonOptions
  };
  let accountInformation = {
    broker: 'True ECN Trading Ltd',
    currency: 'USD',
    server: 'ICMarketsSC-Demo',
    balance: 7319.9,
    equity: 7306.649913200001,
    margin: 184.1,
    freeMargin: 7120.22,
    leverage: 100,
    marginLevel: 3967.58283542
  };

  before(() => {
    MetaApi.enableLog4jsLogging();
    log4js.configure(helpers.assembleLog4jsConfig({
      levels: {
        'StickySocketConnection': 'DEBUG'
      }
    }));
  });

  beforeEach(async () => {
    clock = sinon.useFakeTimers({shouldAdvanceTime: true});
    client = new MetaApiWebsocketClient(metaApi, domainClient, 'token', {...options});
    client.url = 'http://localhost:6784';
    (client as any)._socketInstances = {'vint-hill': {0: [], 1: []}, 'new-york': {0: []}};
    
    io = Server(6784, {path: '/ws', pingTimeout: 1000000});

    io.on('connect', socket => {
      server = socket;
      if (socket.request._query['auth-token'] !== 'token') {
        socket.emit({error: 'UnauthorizedError', message: 'Authorization token invalid'});
        socket.close();
      }
    });
    (client as any)._regionsByAccounts.accountId = {region: 'vint-hill', connections: 1};
    (client as any)._regionsByAccounts.accountIdReplica = {region: 'new-york', connections: 1};
    (client as any)._socketInstancesByAccounts = {0: {accountId: 0, accountIdReplica: 0}, 1: {accountId: 0}};
    (client as any)._accountsByReplicaId.accountId = 'accountId';
    (client as any)._accountsByReplicaId.accountIdReplica = 'accountId';
    (client as any)._accountReplicas.accountId = {
      'vint-hill': 'accountId',
      'new-york': 'accountIdReplica'
    };
    (client as any)._connectedHosts = {
      'accountId:vint-hill:0:ps-mpa-1': 'ps-mpa-1',
      'accountId:new-york:0:ps-mpa-2': 'ps-mpa-2'
    };
    await client.connect(0, 'new-york');
    serverNewYork = server;
    await client.connect(1, 'vint-hill');
    server1 = server;
    server1.on('request', async data => {
      if (data.type === 'unsubscribe' && data.accountId === 'accountId') {
        server1.emit('response', {requestId: data.requestId, type: 'response', accountId: 'accountId'});
      }
    });
    await client.connect(0, 'vint-hill');
    activeSynchronizationIdsStub = sandbox.stub(
      (client as any)._socketInstances['vint-hill'][0][0].synchronizationThrottler,
      'activeSynchronizationIds'
    ).get(() => []);
    sandbox.stub(
      (client as any)._socketInstances['vint-hill'][1][0].synchronizationThrottler, 
      'activeSynchronizationIds'
    ).get(() => []);
    stubs.latencyService.onConnected = sandbox.stub((client as any)._latencyService, 'onConnected');
    stubs.latencyService.onDisconnected = sandbox.stub((client as any)._latencyService, 'onDisconnected');
    stubs.latencyService.onUnsubscribe = sandbox.stub((client as any)._latencyService, 'onUnsubscribe');
    stubs.latencyService.onDealsSynchronized = sandbox.stub((client as any)._latencyService, 'onDealsSynchronized');
    stubs.latencyService.waitConnectedInstance = sandbox.stub((client as any)._latencyService, 'waitConnectedInstance')
      .resolves('accountId:vint-hill:0:ps-mpa-1');
    stubs.latencyService.getActiveInstances = sandbox.stub((client as any)._latencyService, 'getActiveAccountInstances')
      .returns([]);
  });

  afterEach(async () => {
    clock.restore();
    sandbox.restore();
    let resolve;
    let promise = new Promise(res => resolve = res);
    client.close();
    io.close(() => resolve());
    await promise;
  });

  /**
   * @test {MetaApiWebsocketClient#connect}
   */
  describe('connect', () => {

    beforeEach(() => {
      client.close();
    });

    /**
     * @test {MetaApiWebsocketClient#connect}
     */
    it('should throw validation error if connecting to region when configured another one', async () => {
      sandbox.stub(domainClient, 'getSettings').resolves({domain: 'v3.agiliumlabs.cloud'});
      sandbox.stub(options, 'region').value('region1');
      client = new MetaApiWebsocketClient(metaApi, domainClient, 'token', options);
      client.url = 'http://localhost:6784';
      try {
        await client.connect(0, 'region2');
        throw new AssertionError({message: 'Should not be thrown'});
      } catch (err) {
        err.name.should.equal('ValidationError');
        logger.info(err);
      }
    });

    /**
     * @test {MetaApiWebsocketClient#connect}
     */
    it('should not throw error if connecting to same region as configured', async () => {
      sandbox.stub(domainClient, 'getSettings').resolves({domain: 'v3.agiliumlabs.cloud'});
      sandbox.stub(options, 'region').value('region1');
      client = new MetaApiWebsocketClient(metaApi, domainClient, 'token', options);
      client.url = 'http://localhost:6784';
      await client.connect(0, 'region1');
    });

    /**
     * @test {MetaApiWebsocketClient#connect}
     */
    it('should connect to any region if it is not restrained in config', async () => {
      sandbox.stub(domainClient, 'getSettings').resolves({domain: 'v3.agiliumlabs.cloud'});
      sandbox.stub(options, 'region').value(undefined);
      client = new MetaApiWebsocketClient(metaApi, domainClient, 'token', options);
      client.url = 'http://localhost:6784';
      await client.connect(0, 'region1');
      await client.connect(0, 'region2');
    });

  });

  /**
   * @test {MetaApiWebsocketClient#_tryReconnect}
   */
  it('should change client id on reconnect', async () => {
    client.close();
    let clientId;
    let connectAmount = 0;
    io.on('connect', socket => {
      connectAmount++;
      socket.request.headers['client-id'].should.equal(socket.request._query.clientId);
      socket.request.headers['client-id'].should.not.equal(clientId);
      socket.request._query.clientId.should.not.equal(clientId);
      clientId = socket.request._query.clientId;
      socket.disconnect();
    });
    await client.connect(0, 'vint-hill');
    await new Promise(res => setTimeout(res, 50));
    await clock.tickAsync(1500);
    await new Promise(res => setTimeout(res, 50));
    connectAmount.should.be.aboveOrEqual(2);
  });

  /**
   * @test {MetaApiWebsocketClient#connect}
   */
  it('should retry connection if first attempt timed out', async () => {
    let positions = [{
      id: '46214692',
      type: 'POSITION_TYPE_BUY',
      symbol: 'GBPUSD',
      magic: 1000,
      time: new Date('2020-04-15T02:45:06.521Z'),
      updateTime: new Date('2020-04-15T02:45:06.521Z'),
      openPrice: 1.26101,
      currentPrice: 1.24883,
      currentTickValue: 1,
      volume: 0.07,
      swap: 0,
      profit: -85.25999999999966,
      commission: -0.25,
      clientId: 'TE_GBPUSD_7hyINWqAlE',
      stopLoss: 1.17721,
      unrealizedProfit: -85.25999999999901,
      realizedProfit: -6.536993168992922e-13
    }];
    await new Promise(resolve => {
      client.close();
      io.close(resolve);
    });
    client = new MetaApiWebsocketClient(metaApi, domainClient, 'token', {
      application: 'application', 
      domain: 'project-stock.agiliumlabs.cloud',
      requestTimeout: 1.5,
      useSharedClientApi: false,
      connectTimeout: 0.1,
      retryOpts: {retries: 3, minDelayInSeconds: 0.1, maxDelayInSeconds: 0.5},
      minReconnectTimeoutInMs: 25,
      ...commonOptions
    });
    client.url = 'http://localhost:6785';
    (async () => {
      await new Promise(res => setTimeout(res, 200));
      io = Server(6785, {path: '/ws', pingTimeout: 30000});
      io.on('connect', socket => {
        server = socket;
        server.on('request', data => {
          if (data.type === 'getPositions' && data.accountId === 'accountId' && data.application === 'RPC') {
            server.emit('response', {type: 'response', accountId: data.accountId, 
              requestId: data.requestId, positions});
          }
        });
      });
    })();
    (client as any)._regionsByAccounts.accountId = {region: 'vint-hill', connections: 1};
    (client as any)._accountsByReplicaId.accountId = 'accountId';
    (client as any)._accountReplicas.accountId = {
      'vint-hill': 'accountId'
    };
    (client as any)._connectedHosts = {
      'accountId:vint-hill:0:ps-mpa-1': 'ps-mpa-1'
    };
    sandbox.stub((client as any)._latencyService, 'waitConnectedInstance').resolves('accountId:vint-hill:0:ps-mpa-1');
    let actual = await client.getPositions('accountId');
    actual.should.match(positions);
    io.close();
  });

  /**
   * @test {MetaApiWebsocketClient#connect}
   */
  it('should wait for connected instance before sending requests', async () => {
    let positions = [{
      id: '46214692',
      type: 'POSITION_TYPE_BUY',
      symbol: 'GBPUSD',
      magic: 1000,
      time: new Date('2020-04-15T02:45:06.521Z'),
      updateTime: new Date('2020-04-15T02:45:06.521Z'),
      openPrice: 1.26101,
      currentPrice: 1.24883,
      currentTickValue: 1,
      volume: 0.07,
      swap: 0,
      profit: -85.25999999999966,
      commission: -0.25,
      clientId: 'TE_GBPUSD_7hyINWqAlE',
      stopLoss: 1.17721,
      unrealizedProfit: -85.25999999999901,
      realizedProfit: -6.536993168992922e-13
    }];
    let resolve;
    let promise = new Promise(res => resolve = res);
    client.close();
    io.close(() => resolve());
    await promise;
    io = Server(6785, {path: '/ws', pingTimeout: 1000000});
    client = new MetaApiWebsocketClient(metaApi, domainClient, 'token', {
      application: 'application', 
      domain: 'project-stock.agiliumlabs.cloud', requestTimeout: 1.5, useSharedClientApi: false,
      retryOpts: {retries: 3, minDelayInSeconds: 0.1, maxDelayInSeconds: 0.5},
      ...commonOptions
    });
    client.url = 'http://localhost:6785';
    client.addAccountCache('accountId', {'vint-hill': 'accountId'});
    sandbox.stub((client as any)._subscriptionManager, 'isSubscriptionActive').returns(true);
    io.on('connect', socket => {
      server = socket;
      if (socket.request._query['auth-token'] !== 'token') {
        socket.emit({error: 'UnauthorizedError', message: 'Authorization token invalid'});
        socket.close();
      }
      server.on('request', data => {
        if (data.type === 'getPositions' && data.accountId === 'accountId' && data.application === 'RPC') {
          server.emit('response', {type: 'response', accountId: data.accountId, 
            requestId: data.requestId, positions});
        } else if (data.type === 'subscribe') {
          server.emit('response', {type: 'response', accountId: data.accountId, 
            requestId: data.requestId});
        }
      });
    });
    client.url = 'http://localhost:6785';
    (client as any)._regionsByAccounts.accountId = {region: 'vint-hill', connections: 1};
    (client as any)._regionsByAccounts.accountIdReplica = {region: 'new-york', connections: 1};
    (client as any)._accountsByReplicaId.accountId = 'accountId';
    (client as any)._accountReplicas.accountId = {
      'vint-hill': 'accountId',
    };
    (client as any)._connectedHosts = {
      'accountId:vint-hill:0:ps-mpa-1': 'ps-mpa-1'
    };
    server.on('request', data => {
      if (data.type === 'getPositions' && data.accountId === 'accountId' && data.application === 'RPC') {
        server.emit('response', {type: 'response', accountId: data.accountId, 
          requestId: data.requestId, positions});
      }
    });
    sandbox.stub((client as any)._latencyService, 'onConnected');
    sandbox.stub((client as any)._latencyService, 'onDisconnected');
    sandbox.stub((client as any)._latencyService, 'onUnsubscribe');
    sandbox.stub((client as any)._latencyService, 'waitConnectedInstance').callsFake(async () => {
      await new Promise(res => setTimeout(res, 50));
      return 'accountId:vint-hill:0:ps-mpa-1';
    });
    let actual = await client.getPositions('accountId');
    actual.should.match(positions);
    io.close();
  });

  /**
   * @test {MetaApiWebsocketClient#_getServerUrl}
   */
  it('should connect to shared server', async () => {
    client.close();
    sandbox.stub(domainClient, 'getSettings').resolves({
      domain: 'v3.agiliumlabs.cloud'
    });
    client = new MetaApiWebsocketClient(metaApi, domainClient, 'token', {
      application: 'application',
      domain: 'project-stock.agiliumlabs.cloud', requestTimeout: 1.5, useSharedClientApi: true,
      ...commonOptions
    });
    (client as any)._socketInstances = {'vint-hill': {0: [{
      connected: true,
      requestResolves: [],
      socket: {disconnect: () => {}}
    }]}};
    const url = await (client as any)._getServerUrl(0, 0, 'vint-hill');
    should(url).eql('https://mt-client-api-v1.vint-hill-a.v3.agiliumlabs.cloud');
  });

  /**
   * @test {MetaApiWebsocketClient#_getServerUrl}
   */
  it('should connect to dedicated server', async () => {
    client.close();
    sandbox.stub(domainClient, 'getSettings').resolves({
      hostname: 'mt-client-api-dedicated',
      domain: 'project-stock.agiliumlabs.cloud'
    });
    client = new MetaApiWebsocketClient(metaApi, domainClient, 'token', {
      application: 'application', 
      domain: 'project-stock.agiliumlabs.cloud', requestTimeout: 1.5, useSharedClientApi: true,
      ...commonOptions
    });
    (client as any)._socketInstances = {'vint-hill': {0: [{
      connected: true,
      requestResolves: [],
      socket: {disconnect: () => {}}
    }]}};
    const url = await (client as any)._getServerUrl(0, 0, 'vint-hill');
    should(url).eql('https://mt-client-api-v1.vint-hill-a.project-stock.agiliumlabs.cloud');
  });

  describe('addAccountCache', () => {

    /**
     * @test {MetaApiWebsocketClient#addAccountCache}
     */
    it('should add account cache', async () => {
      client.addAccountCache('accountId2', {'vint-hill': 'accountId2'});
      sinon.assert.match(client.getAccountRegion('accountId2'), 'vint-hill');
      sinon.assert.match(client.accountReplicas.accountId2, {'vint-hill': 'accountId2'});
      sinon.assert.match(client.accountsByReplicaId.accountId2, 'accountId2');
      client.addAccountCache('accountId2', {'vint-hill': 'accountId2'});
      sinon.assert.match(client.getAccountRegion('accountId2'), 'vint-hill');
      client.removeAccountCache('accountId2');
      sinon.assert.match(client.getAccountRegion('accountId2'), 'vint-hill');
      client.removeAccountCache('accountId2');
      sinon.assert.match(client.getAccountRegion('accountId2'), 'vint-hill');
      for (let i = 0; i < 5; i++) {
        clock.tick(30 * 60 * 1000 + 500);
        client.clearAccountCacheJob();
      }
      sinon.assert.match(client.getAccountRegion('accountId2'), undefined);
      sinon.assert.match(client.accountReplicas.accountId2, undefined);
      sinon.assert.match(client.accountsByReplicaId.accountId2, undefined);
    });

    /**
     * @test {MetaApiWebsocketClient#addAccountCache}
     */
    it('should delay region deletion if a request is made', async () => {
      server.on('request', data => {
        if (data.type === 'getAccountInformation' && data.accountId === 'accountId2' &&
          data.application === 'RPC') {
          server.emit('response', {
            type: 'response', accountId: data.accountId, requestId: data.requestId,
            accountInformation
          });
        }
      });

      client.addAccountCache('accountId2', {'vint-hill': 'accountId2'});
      sinon.assert.match(client.getAccountRegion('accountId2'), 'vint-hill');
      await client.getAccountInformation('accountId2');
      sinon.assert.match(client.getAccountRegion('accountId2'), 'vint-hill');
      await clock.tickAsync(30 * 60 * 1000 + 500);
      client.clearAccountCacheJob();
      
      sinon.assert.match(client.getAccountRegion('accountId2'), 'vint-hill');
      await client.getAccountInformation('accountId2');
      client.removeAccountCache('accountId2');
      await clock.tickAsync(30 * 60 * 1000 + 500);
      client.clearAccountCacheJob();
      
      await client.getAccountInformation('accountId2');
      await clock.tickAsync(30 * 60 * 1000 + 500);
      client.clearAccountCacheJob();
      
      sinon.assert.match(client.getAccountRegion('accountId2'), 'vint-hill');
      for (let i = 0; i < 5; i++) {
        await clock.tickAsync(30 * 60 * 1000 + 500);
        client.clearAccountCacheJob();
      }
      sinon.assert.match(client.getAccountRegion('accountId2'), undefined);
    }).timeout(3000);

    /**
     * @test {MetaApiWebsocketClient#addAccountCache}
     */
    it('should correctly clear account cache including accounts with several replicas', () => {
      client.addAccountCache('accountId1', {
        'vint-hill': 'accountId1',
        'new-york': 'accountId2'
      });
      client.removeAccountCache('accountId1');

      clock.tick(1000 * 60 * 60 * 3);
      client.clearAccountCacheJob();
      should(client.getAccountRegion('accountId1')).be.undefined();
      should(client.getAccountRegion('accountId2')).be.undefined();
    });

    /**
     * @test {MetaApiWebsocketClient#addAccountCache}
     */
    it('should correctly clear account cache added on synchroniation packet', async function() {
      this.retries(2);

      server.emit('synchronization', {type: 'keepalive', accountId: 'accountId1'});
      await new Promise(res => setTimeout(res, 25));

      clock.tick(1000 * 60 * 60 * 3);
      client.clearAccountCacheJob();
    });

    /**
     * @test {MetaApiWebsocketClient#addAccountCache}
     * @test {MetaApiWebsocketClient#updateAccountCache}
     */
    it('should update account cache', async () => {
      client.addAccountCache('accountId1', {
        'vint-hill': 'accountId1',
        'new-york': 'accountId2',
        'tokyo': 'accountId3',
      });
      client.updateAccountCache('accountId1', {
        'vint-hill': 'accountId1',
        'tokyo': 'accountId4',
        'singapore': 'accountId5'
      });
      sinon.assert.match(client.accountReplicas.accountId1, {
        'vint-hill': 'accountId1',
        'tokyo': 'accountId4',
        'singapore': 'accountId5'});
    });

  });

  /**
   * @test {MetaApiWebsocketClient#onAccountDeleted}
   */
  describe('onAccountDeleted', () => {

    let cancelAccountStub;

    beforeEach(() => {
      cancelAccountStub = sandbox.stub((client as any)._subscriptionManager, 'cancelAccount');
    });

    /**
     * @test {MetaApiWebsocketClient#onAccountDeleted}
     */
    it('should delete master account', async () => {
      client.addAccountCache('accountId1', {
        'vint-hill': 'accountId1',
        'new-york': 'accountId2',
        'tokyo': 'accountId3',
      });

      client.onAccountDeleted('accountId1');
      sinon.assert.calledWith(cancelAccountStub, 'accountId1');
      sinon.assert.calledWith(stubs.latencyService.onUnsubscribe, 'accountId1');
      should(client.getAccountRegion('accountId1')).be.undefined();
      should(client.getAccountRegion('accountId2')).be.undefined();
      should(client.getAccountRegion('accountId3')).be.undefined();
      should(client.accountReplicas.accountId1).be.undefined();
    });

    /**
     * @test {MetaApiWebsocketClient#onAccountDeleted}
     */
    it('should delete replica', async () => {
      client.addAccountCache('accountId1', {
        'vint-hill': 'accountId1',
        'new-york': 'accountId2',
        'tokyo': 'accountId3',
      });
      client.onAccountDeleted('accountId2');
      sinon.assert.calledWith(cancelAccountStub, 'accountId2');
      sinon.assert.calledWith(stubs.latencyService.onUnsubscribe, 'accountId2');
      should(client.getAccountRegion('accountId1')).eql('vint-hill');
      should(client.getAccountRegion('accountId2')).be.undefined();
      should(client.getAccountRegion('accountId3')).eql('tokyo');
      should(client.accountReplicas.accountId1).eql({
        'vint-hill': 'accountId1',
        'tokyo': 'accountId3',
      });
    });

  });

  /**
   * @test {MetaApiWebsocketClient#getAccountInformation}
   */
  describe('getAccountInformation', () => {

    /**
     * @test {MetaApiWebsocketClient#getAccountInformation}
     */
    it('should retrieve MetaTrader account information from API', async () => {
      server.on('request', data => {
        should(data.refreshTerminalState).be.undefined();
        if (data.type === 'getAccountInformation' && data.accountId === 'accountId' && data.application === 'RPC') {
          server.emit('response', {
            type: 'response', accountId: data.accountId, requestId: data.requestId,
            accountInformation
          });
        }
      });
      let actual = await client.getAccountInformation('accountId');
      actual.should.match(accountInformation);
    });

    /**
     * @test {MetaApiWebsocketClient#getAccountInformation}
     */
    it('should set refresh terminal state flag if it is specified', async () => {
      server.on('request', data => {
        data.should.match({type: 'getAccountInformation', refreshTerminalState: true});
        server.emit('response', {
          type: 'response', accountId: data.accountId, requestId: data.requestId,
          accountInformation
        });
      });
      await client.getAccountInformation('accountId', {refreshTerminalState: true});
    });

  });

  /**
   * @test {MetaApiWebsocketClient#getPositions}
   */
  describe('getPositions', () => {

    /**
     * @test {MetaApiWebsocketClient#getPositions}
     */
    it('should retrieve MetaTrader positions from API', async () => {
      let positions = [{
        id: '46214692',
        type: 'POSITION_TYPE_BUY',
        symbol: 'GBPUSD',
        magic: 1000,
        time: new Date('2020-04-15T02:45:06.521Z'),
        updateTime: new Date('2020-04-15T02:45:06.521Z'),
        openPrice: 1.26101,
        currentPrice: 1.24883,
        currentTickValue: 1,
        volume: 0.07,
        swap: 0,
        profit: -85.25999999999966,
        commission: -0.25,
        clientId: 'TE_GBPUSD_7hyINWqAlE',
        stopLoss: 1.17721,
        unrealizedProfit: -85.25999999999901,
        realizedProfit: -6.536993168992922e-13
      }];
      server.on('request', data => {
        should(data.refreshTerminalState).be.undefined();
        if (data.type === 'getPositions' && data.accountId === 'accountId' && data.application === 'RPC') {
          server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, positions});
        }
      });
      let actual = await client.getPositions('accountId');
      actual.should.match(positions);
    });

    /**
     * @test {MetaApiWebsocketClient#getPositions}
     */
    it('should set refresh terminal state flag if it is specified', async () => {
      server.on('request', data => {
        data.should.match({type: 'getPositions', refreshTerminalState: true});
        server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId});
      });
      await client.getPositions('accountId', {refreshTerminalState: true});
    });

  });

  /**
   * @test {MetaApiWebsocketClient#getPosition}
   */
  describe('getPosition', () => {

    /**
     * @test {MetaApiWebsocketClient#getPosition}
     */
    it('should retrieve MetaTrader position from API by id', async () => {
      let position = {
        id: '46214692',
        type: 'POSITION_TYPE_BUY',
        symbol: 'GBPUSD',
        magic: 1000,
        time: new Date('2020-04-15T02:45:06.521Z'),
        updateTime: new Date('2020-04-15T02:45:06.521Z'),
        openPrice: 1.26101,
        currentPrice: 1.24883,
        currentTickValue: 1,
        volume: 0.07,
        swap: 0,
        profit: -85.25999999999966,
        commission: -0.25,
        clientId: 'TE_GBPUSD_7hyINWqAlE',
        stopLoss: 1.17721,
        unrealizedProfit: -85.25999999999901,
        realizedProfit: -6.536993168992922e-13
      };
      server.on('request', data => {
        should(data.refreshTerminalState).be.undefined();
        if (data.type === 'getPosition' && data.accountId === 'accountId' && data.positionId === '46214692' &&
          data.application === 'RPC') {
          server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, position});
        }
      });
      let actual = await client.getPosition('accountId', '46214692');
      actual.should.match(position);
    });

    /**
     * @test {MetaApiWebsocketClient#getPosition}
     */
    it('should set refresh terminal state flag if it is specified', async () => {
      server.on('request', data => {
        data.should.match({type: 'getPosition', refreshTerminalState: true});
        server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId});
      });
      await client.getPosition('accountId', '123', {refreshTerminalState: true});
    });

  });

  /**
   * @test {MetaApiWebsocketClient#getOrders}
   */
  describe('getOrders', () => {

    /**
     * @test {MetaApiWebsocketClient#getOrders}
     */
    it('should retrieve MetaTrader orders from API', async () => {
      let orders = [{
        id: '46871284',
        type: 'ORDER_TYPE_BUY_LIMIT',
        state: 'ORDER_STATE_PLACED',
        symbol: 'AUDNZD',
        magic: 123456,
        platform: 'mt5',
        time: new Date('2020-04-20T08:38:58.270Z'),
        openPrice: 1.03,
        currentPrice: 1.05206,
        volume: 0.01,
        currentVolume: 0.01,
        comment: 'COMMENT2'
      }];
      server.on('request', data => {
        should(data.refreshTerminalState).be.undefined();
        if (data.type === 'getOrders' && data.accountId === 'accountId' && data.application === 'RPC') {
          server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, orders});
        }
      });
      let actual = await client.getOrders('accountId');
      actual.should.match(orders);
    });

    /**
     * @test {MetaApiWebsocketClient#getOrders}
     */
    it('should set refresh terminal state flag if it is specified', async () => {
      server.on('request', data => {
        data.should.match({type: 'getOrders', refreshTerminalState: true});
        server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId});
      });
      await client.getOrders('accountId', {refreshTerminalState: true});
    });

  });

  /**
   * @test {MetaApiWebsocketClient#getOrder}
   */
  describe('getOrder', () => {

    /**
     * @test {MetaApiWebsocketClient#getOrder}
     */
    it('should retrieve MetaTrader order from API by id', async () => {
      let order = {
        id: '46871284',
        type: 'ORDER_TYPE_BUY_LIMIT',
        state: 'ORDER_STATE_PLACED',
        symbol: 'AUDNZD',
        magic: 123456,
        platform: 'mt5',
        time: new Date('2020-04-20T08:38:58.270Z'),
        openPrice: 1.03,
        currentPrice: 1.05206,
        volume: 0.01,
        currentVolume: 0.01,
        comment: 'COMMENT2'
      };
      server.on('request', data => {
        should(data.refreshTerminalState).be.undefined();
        if (data.type === 'getOrder' && data.accountId === 'accountId' && data.orderId === '46871284' &&
          data.application === 'RPC') {
          server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, order});
        }
      });
      let actual = await client.getOrder('accountId', '46871284');
      actual.should.match(order);
    });

    /**
     * @test {MetaApiWebsocketClient#getOrder}
     */
    it('should set refresh terminal state flag if it is specified', async () => {
      server.on('request', data => {
        data.should.match({type: 'getOrder', refreshTerminalState: true});
        server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId});
      });
      await client.getOrder('accountId', '123', {refreshTerminalState: true});
    });

  });

  /**
   * @test {MetaApiWebsocketClient#getHistoryOrdersByTicket}
   */
  it('should retrieve MetaTrader history orders from API by ticket', async () => {
    let historyOrders = [{
      clientId: 'TE_GBPUSD_7hyINWqAlE',
      currentPrice: 1.261,
      currentVolume: 0,
      doneTime: new Date('2020-04-15T02:45:06.521Z'),
      id: '46214692',
      magic: 1000,
      platform: 'mt5',
      positionId: '46214692',
      state: 'ORDER_STATE_FILLED',
      symbol: 'GBPUSD',
      time: new Date('2020-04-15T02:45:06.260Z'),
      type: 'ORDER_TYPE_BUY',
      volume: 0.07
    }];
    server.on('request', data => {
      if (data.type === 'getHistoryOrdersByTicket' && data.accountId === 'accountId' && data.ticket === '46214692' &&
        data.application === 'RPC') {
        server.emit('response', {
          type: 'response', accountId: data.accountId, requestId: data.requestId, historyOrders,
          synchronizing: false
        });
      }
    });
    let actual = await client.getHistoryOrdersByTicket('accountId', '46214692');
    actual.should.match({historyOrders, synchronizing: false});
  });

  /**
   * @test {MetaApiWebsocketClient#getHistoryOrdersByPosition}
   */
  it('should retrieve MetaTrader history orders from API by position', async () => {
    let historyOrders = [{
      clientId: 'TE_GBPUSD_7hyINWqAlE',
      currentPrice: 1.261,
      currentVolume: 0,
      doneTime: new Date('2020-04-15T02:45:06.521Z'),
      id: '46214692',
      magic: 1000,
      platform: 'mt5',
      positionId: '46214692',
      state: 'ORDER_STATE_FILLED',
      symbol: 'GBPUSD',
      time: new Date('2020-04-15T02:45:06.260Z'),
      type: 'ORDER_TYPE_BUY',
      volume: 0.07
    }];
    server.on('request', data => {
      if (data.type === 'getHistoryOrdersByPosition' && data.accountId === 'accountId' &&
        data.positionId === '46214692' && data.application === 'RPC') {
        server.emit('response', {
          type: 'response', accountId: data.accountId, requestId: data.requestId, historyOrders,
          synchronizing: false
        });
      }
    });
    let actual = await client.getHistoryOrdersByPosition('accountId', '46214692');
    actual.should.match({historyOrders, synchronizing: false});
  });

  /**
   * @test {MetaApiWebsocketClient#getHistoryOrdersByTimeRange}
   */
  it('should retrieve MetaTrader history orders from API by time range', async () => {
    let historyOrders = [{
      clientId: 'TE_GBPUSD_7hyINWqAlE',
      currentPrice: 1.261,
      currentVolume: 0,
      doneTime: new Date('2020-04-15T02:45:06.521Z'),
      id: '46214692',
      magic: 1000,
      platform: 'mt5',
      positionId: '46214692',
      state: 'ORDER_STATE_FILLED',
      symbol: 'GBPUSD',
      time: new Date('2020-04-15T02:45:06.260Z'),
      type: 'ORDER_TYPE_BUY',
      volume: 0.07
    }];
    server.on('request', data => {
      if (data.type === 'getHistoryOrdersByTimeRange' && data.accountId === 'accountId' &&
        data.startTime === '2020-04-15T02:45:00.000Z' && data.endTime === '2020-04-15T02:46:00.000Z' &&
        data.offset === 1 && data.limit === 100 && data.application === 'RPC') {
        server.emit('response', {
          type: 'response', accountId: data.accountId, requestId: data.requestId, historyOrders,
          synchronizing: false
        });
      }
    });
    let actual = await client.getHistoryOrdersByTimeRange('accountId', new Date('2020-04-15T02:45:00.000Z'),
      new Date('2020-04-15T02:46:00.000Z'), 1, 100);
    actual.should.match({historyOrders, synchronizing: false});
  });

  /**
   * @test {MetaApiWebsocketClient#getDealsByTicket}
   */
  it('should retrieve MetaTrader deals from API by ticket', async () => {
    let deals = [{
      clientId: 'TE_GBPUSD_7hyINWqAlE',
      commission: -0.25,
      entryType: 'DEAL_ENTRY_IN',
      id: '33230099',
      magic: 1000,
      platform: 'mt5',
      orderId: '46214692',
      positionId: '46214692',
      price: 1.26101,
      profit: 0,
      swap: 0,
      symbol: 'GBPUSD',
      time: new Date('2020-04-15T02:45:06.521Z'),
      type: 'DEAL_TYPE_BUY',
      volume: 0.07
    }];
    server.on('request', data => {
      if (data.type === 'getDealsByTicket' && data.accountId === 'accountId' && data.ticket === '46214692' &&
        data.application === 'RPC') {
        server.emit('response', {
          type: 'response', accountId: data.accountId, requestId: data.requestId, deals,
          synchronizing: false
        });
      }
    });
    let actual = await client.getDealsByTicket('accountId', '46214692');
    actual.should.match({deals, synchronizing: false});
  });

  /**
   * @test {MetaApiWebsocketClient#getDealsByPosition}
   */
  it('should retrieve MetaTrader deals from API by position', async () => {
    let deals = [{
      clientId: 'TE_GBPUSD_7hyINWqAlE',
      commission: -0.25,
      entryType: 'DEAL_ENTRY_IN',
      id: '33230099',
      magic: 1000,
      platform: 'mt5',
      orderId: '46214692',
      positionId: '46214692',
      price: 1.26101,
      profit: 0,
      swap: 0,
      symbol: 'GBPUSD',
      time: new Date('2020-04-15T02:45:06.521Z'),
      type: 'DEAL_TYPE_BUY',
      volume: 0.07
    }];
    server.on('request', data => {
      if (data.type === 'getDealsByPosition' && data.accountId === 'accountId' && data.positionId === '46214692' &&
        data.application === 'RPC') {
        server.emit('response', {
          type: 'response', accountId: data.accountId, requestId: data.requestId, deals,
          synchronizing: false
        });
      }
    });
    let actual = await client.getDealsByPosition('accountId', '46214692');
    actual.should.match({deals, synchronizing: false});
  });

  /**
   * @test {MetaApiWebsocketClient#getDealsByTimeRange}
   */
  it('should retrieve MetaTrader deals from API by time range', async () => {
    let deals = [{
      clientId: 'TE_GBPUSD_7hyINWqAlE',
      commission: -0.25,
      entryType: 'DEAL_ENTRY_IN',
      id: '33230099',
      magic: 1000,
      platform: 'mt5',
      orderId: '46214692',
      positionId: '46214692',
      price: 1.26101,
      profit: 0,
      swap: 0,
      symbol: 'GBPUSD',
      time: new Date('2020-04-15T02:45:06.521Z'),
      type: 'DEAL_TYPE_BUY',
      volume: 0.07
    }];
    server.on('request', data => {
      if (data.type === 'getDealsByTimeRange' && data.accountId === 'accountId' &&
        data.startTime === '2020-04-15T02:45:00.000Z' && data.endTime === '2020-04-15T02:46:00.000Z' &&
        data.offset === 1 && data.limit === 100 && data.application === 'RPC') {
        server.emit('response', {
          type: 'response', accountId: data.accountId, requestId: data.requestId, deals,
          synchronizing: false
        });
      }
    });
    let actual = await client.getDealsByTimeRange('accountId', new Date('2020-04-15T02:45:00.000Z'),
      new Date('2020-04-15T02:46:00.000Z'), 1, 100);
    actual.should.match({deals, synchronizing: false});
  });

  /**
   * @test {MetaApiWebsocketClient#removeApplication}
   */
  it('should remove application from API', async () => {
    let requestReceived = false;
    server.on('request', data => {
      if (data.type === 'removeApplication' && data.accountId === 'accountId' && data.application === 'application') {
        requestReceived = true;
        server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId});
      }
    });
    await client.removeApplication('accountId');
    requestReceived.should.be.true();
  });

  /**
   * @test {MetaApiWebsocketClient#trade}
   */
  it('should execute a trade via new API version', async () => {
    let trade = {
      actionType: 'ORDER_TYPE_SELL',
      symbol: 'AUDNZD',
      volume: 0.07
    };
    let response = {
      numericCode: 10009,
      stringCode: 'TRADE_RETCODE_DONE',
      message: 'Request completed',
      orderId: '46870472'
    };
    sandbox.stub((client as any)._subscriptionManager, 'isSubscriptionActive').returns(true);
    server.emit('synchronization', {type: 'authenticated', accountId: 'accountId', host: 'ps-mpa-1',
      instanceIndex: 0, replicas: 1});
    await new Promise(res => setTimeout(res, 100));
    let instanceIndex;
    server.on('request', data => {
      instanceIndex = data.instanceIndex;
      data.trade.should.match(trade);
      if (data.type === 'trade' && data.accountId === 'accountId' && data.application === 'application') {
        server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, response});
      }
    });
    let actual = await client.trade('accountId', trade);
    actual.should.match(response);
    should.equal(instanceIndex, 0);
  });

  /**
   * @test {MetaApiWebsocketClient#trade}
   */
  it('should execute a trade via a replica account', async () => {
    stubs.latencyService.getActiveInstances.returns(['accountId:new-york:0:ps-mpa-2']);
    let trade = {
      actionType: 'ORDER_TYPE_SELL',
      symbol: 'AUDNZD',
      volume: 0.07
    };
    let response = {
      numericCode: 10009,
      stringCode: 'TRADE_RETCODE_DONE',
      message: 'Request completed',
      orderId: '46870472'
    };
    sandbox.stub((client as any)._subscriptionManager, 'isSubscriptionActive').returns(true);
    serverNewYork.emit('synchronization', {type: 'authenticated', accountId: 'accountIdReplica', host: 'ps-mpa-2',
      instanceIndex: 0, replicas: 1});
    await new Promise(res => setTimeout(res, 100));
    let instanceIndex;
    serverNewYork.on('request', data => {
      instanceIndex = data.instanceIndex;
      data.trade.should.match(trade);
      if (data.type === 'trade' && data.accountId === 'accountIdReplica' && data.application === 'application') {
        serverNewYork.emit('response', {type: 'response', accountId: data.accountId, 
          requestId: data.requestId, response});
      }
    });
    let actual = await client.trade('accountId', trade);
    actual.should.match(response);
    should.equal(instanceIndex, 0);
  });

  /**
   * @test {MetaApiWebsocketClient#trade}
   */
  it('should execute an RPC trade', async () => {
    let trade = {
      actionType: 'ORDER_TYPE_SELL',
      symbol: 'AUDNZD',
      volume: 0.07
    };
    let response = {
      numericCode: 10009,
      stringCode: 'TRADE_RETCODE_DONE',
      message: 'Request completed',
      orderId: '46870472'
    };
    sandbox.stub((client as any)._subscriptionManager, 'isSubscriptionActive').returns(true);
    server.emit('synchronization', {type: 'authenticated', accountId: 'accountId', host: 'ps-mpa-1',
      instanceIndex: 0, replicas: 1});
    await new Promise(res => setTimeout(res, 50));
    let instanceIndex;
    server.on('request', data => {
      instanceIndex = data.instanceIndex;
      data.trade.should.match(trade);
      if (data.type === 'trade' && data.accountId === 'accountId' && data.application === 'RPC') {
        server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, response});
      }
    });
    let actual = await client.trade('accountId', trade, 'RPC');
    actual.should.match(response);
    should.not.exist(instanceIndex);
  });

  /**
   * @test {MetaApiWebsocketClient#trade}
   */
  it('should execute a trade via API and receive trade error from old API version', async () => {
    let trade = {
      actionType: 'ORDER_TYPE_SELL',
      symbol: 'AUDNZD',
      volume: 0.07
    };
    let response = {
      error: 10006,
      description: 'TRADE_RETCODE_REJECT',
      message: 'Request rejected',
      orderId: '46870472'
    };
    server.on('request', data => {
      data.trade.should.match(trade);
      if (data.type === 'trade' && data.accountId === 'accountId' && data.application === 'application') {
        server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, response});
      }
    });
    try {
      await client.trade('accountId', trade);
      throw new AssertionError({message: 'Trade error expected'});
    } catch (err) {
      err.message.should.equal('Request rejected');
      err.name.should.equal('TradeError');
      err.stringCode.should.equal('TRADE_RETCODE_REJECT');
      err.numericCode.should.equal(10006);
    }
  });

  /**
   * @test {MetaApiWebsocketClient#subscribe}
   */
  it('should connect to MetaTrader terminal', async () => {
    let requestReceived = false;
    server.on('request', data => {
      if (data.type === 'subscribe' && data.accountId === 'accountId' && data.application === 'application' &&
        data.instanceIndex === 0) {
        server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId});
        requestReceived = true;
      }
    });
    await client.subscribe('accountId', 0);
    await new Promise(res => setTimeout(res, 50));
    requestReceived.should.be.true();
  });

  /**
   * @test {MetaApiWebsocketClient#subscribe}
   */
  it('should connect to MetaTrader terminal via a replica even if synced main', async () => {
    stubs.latencyService.getActiveInstances.returns(['accountId:vint-hill:0:ps-mpa-1']);
    let requestReceived = false;
    serverNewYork.on('request', data => {
      if (data.type === 'subscribe' && data.accountId === 'accountIdReplica' && data.application === 'application' &&
        data.instanceIndex === 0) {
        serverNewYork.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId});
        requestReceived = true;
      }
    });
    await client.subscribe('accountIdReplica', 0);
    await new Promise(res => setTimeout(res, 50));
    requestReceived.should.be.true();
  });

  /**
   * @test {MetaApiWebsocketClient#subscribe}
   */
  it('should create new instance when account limit is reached', async () => {
    sinon.assert.match(client.socketInstances['vint-hill'][0].length, 1);
    for (let i = 0; i < 100; i++) {
      (client as any)._socketInstancesByAccounts[0]['accountId' + i] = 0;
      (client as any)._regionsByAccounts['accountId' + i] = {region: 'vint-hill', connections: 1};
    }

    io.removeAllListeners('connect');
    io.on('connect', socket => {
      socket.on('request', data => {
        if (data.type === 'subscribe' && data.accountId === 'accountId101' && data.application === 'application' &&
          data.instanceIndex === 0) {
          socket.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId});
        }
      });
    });
    (client as any)._regionsByAccounts.accountId101 = {region: 'vint-hill', connections: 1};
    await client.subscribe('accountId101', 0);
    await new Promise(res => setTimeout(res, 50));
    sinon.assert.match(client.socketInstances['vint-hill'][0].length, 2);
  });

  /**
   * @test {MetaApiWebsocketClient#subscribe}
   */
  it('should return error if connect to MetaTrader terminal failed', async () => {
    let requestReceived = false;
    server.on('request', data => {
      if (data.type === 'subscribe' && data.accountId === 'accountId' && data.application === 'application') {
        requestReceived = true;
      }
      server.emit('processingError', {
        id: 1, error: 'NotAuthenticatedError', message: 'Error message',
        requestId: data.requestId
      });
    });
    let success = true;
    try {
      await client.subscribe('accountId', 0);
      success = false;
    } catch (err) {
      err.name.should.equal('NotConnectedError');
    }
    success.should.be.true();
    requestReceived.should.be.true();
  });

  /**
   * @test {MetaApiWebsocketClient#getSymbols}
   */
  it('should retrieve symbols from API', async () => {
    let symbols = ['EURUSD'];
    server.on('request', data => {
      if (data.type === 'getSymbols' && data.accountId === 'accountId' && data.application === 'RPC') {
        server.emit('response', {
          type: 'response', accountId: data.accountId, requestId: data.requestId, symbols
        });
      }
    });
    let actual = await client.getSymbols('accountId');
    actual.should.match(symbols);
  });

  /**
   * @test {MetaApiWebsocketClient#getSymbolSpecification}
   */
  it('should retrieve symbol specification from API', async () => {
    let specification = {
      symbol: 'AUDNZD',
      tickSize: 0.00001,
      minVolume: 0.01,
      maxVolume: 100,
      volumeStep: 0.01
    };
    server.on('request', data => {
      if (data.type === 'getSymbolSpecification' && data.accountId === 'accountId' && data.symbol === 'AUDNZD' &&
        data.application === 'RPC') {
        server.emit('response', {
          type: 'response', accountId: data.accountId, requestId: data.requestId,
          specification
        });
      }
    });
    let actual = await client.getSymbolSpecification('accountId', 'AUDNZD');
    actual.should.match(specification);
  });

  /**
   * @test {MetaApiWebsocketClient#getSymbolPrice}
   */
  it('should retrieve symbol price from API', async () => {
    let price = {
      symbol: 'AUDNZD',
      bid: 1.05297,
      ask: 1.05309,
      profitTickValue: 0.59731,
      lossTickValue: 0.59736
    };
    server.on('request', data => {
      if (data.type === 'getSymbolPrice' && data.accountId === 'accountId' && data.symbol === 'AUDNZD' &&
        data.application === 'RPC' && data.keepSubscription === true) {
        server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, price});
      }
    });
    let actual = await client.getSymbolPrice('accountId', 'AUDNZD', true);
    actual.should.match(price);
  });

  /**
   * @test {MetaApiWebsocketClient#getCandle}
   */
  it('should retrieve candle from API', async () => {
    let candle = {
      symbol: 'AUDNZD',
      timeframe: '15m',
      time: new Date('2020-04-07T03:45:00.000Z'),
      brokerTime: '2020-04-07 06:45:00.000',
      open: 1.03297,
      high: 1.06309,
      low: 1.02705,
      close: 1.043,
      tickVolume: 1435,
      spread: 17,
      volume: 345
    };
    server.on('request', data => {
      if (data.type === 'getCandle' && data.accountId === 'accountId' && data.symbol === 'AUDNZD' &&
        data.application === 'RPC' && data.timeframe === '15m' && data.keepSubscription === true) {
        server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, candle});
      }
    });
    let actual = await client.getCandle('accountId', 'AUDNZD', '15m', true);
    actual.should.match(candle);
  });

  /**
   * @test {MetaApiWebsocketClient#getTick}
   */
  it('should retrieve latest tick from API', async () => {
    let tick = {
      symbol: 'AUDNZD',
      time: new Date('2020-04-07T03:45:00.000Z'),
      brokerTime: '2020-04-07 06:45:00.000',
      bid: 1.05297,
      ask: 1.05309,
      last: 0.5298,
      volume: 0.13,
      side: 'buy'
    };
    server.on('request', data => {
      if (data.type === 'getTick' && data.accountId === 'accountId' && data.symbol === 'AUDNZD' &&
        data.application === 'RPC' && data.keepSubscription === true) {
        server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, tick});
      }
    });
    let actual = await client.getTick('accountId', 'AUDNZD', true);
    actual.should.match(tick);
  });

  /**
   * @test {MetaApiWebsocketClient#getBook}
   */
  it('should retrieve latest order book from API', async () => {
    let book = {
      symbol: 'AUDNZD',
      time: new Date('2020-04-07T03:45:00.000Z'),
      brokerTime: '2020-04-07 06:45:00.000',
      book: [
        {
          type: 'BOOK_TYPE_SELL',
          price: 1.05309,
          volume: 5.67
        },
        {
          type: 'BOOK_TYPE_BUY',
          price: 1.05297,
          volume: 3.45
        }
      ]
    };
    server.on('request', data => {
      if (data.type === 'getBook' && data.accountId === 'accountId' && data.symbol === 'AUDNZD' &&
        data.application === 'RPC' && data.keepSubscription === true) {
        server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, book});
      }
    });
    let actual = await client.getBook('accountId', 'AUDNZD', true);
    actual.should.match(book);
  });

  /**
   * @test {MetaApiWebsocketClient#sendUptime}
   */
  it('should sent uptime stats to the server', async () => {
    server.on('request', data => {
      if (data.type === 'saveUptime' && data.accountId === 'accountId' &&
        JSON.stringify(data.uptime) === JSON.stringify({'1h': 100}) &&
        data.application === 'application') {
        server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId});
      }
    });
    await client.saveUptime('accountId', {'1h': 100});
  });

  /**
   * @test {MetaApiWebsocketClient#refreshTerminalState}
   */
  describe('refreshTerminalState', () => {

    /**
     * @test {MetaApiWebsocketClient#refreshTerminalState}
     */
    it('should initiate refreshing terminal state and return symbols, initiated to refresh', async () => {
      server.on('request', data => {
        data.should.match({type: 'refreshTerminalState', application: 'RPC', accountId: 'accountId'});
        server.emit('response', {
          type: 'response', accountId: data.accountId, requestId: data.requestId,
          symbols: ['EURUSD', 'BTCUSD']
        });
      });
      should(await client.refreshTerminalState('accountId')).deepEqual(['EURUSD', 'BTCUSD']);
    });

  });

  /**
   * @test {MetaApiWebsocketClient#refreshTerminalState}
   */
  describe('refreshSymbolQuotes', () => {

    /**
     * @test {MetaApiWebsocketClient#refreshTerminalState}
     */
    it('should retrieve throttled quotes', async () => {
      server.on('request', data => {
        data.should.match({
          type: 'refreshSymbolQuotes', application: 'RPC', accountId: 'accountId',
          symbols: ['EURUSD', 'BTCUSD']
        });
        server.emit('response', {
          type: 'response', accountId: data.accountId, requestId: data.requestId,
          refreshedQuotes: {
            quotes: [{symbol: 'EURUSD'}, {symbol: 'BTCUSD'}],
            balance: 1100
          }
        });
      });
      should(await client.refreshSymbolQuotes('accountId', ['EURUSD', 'BTCUSD'])).deepEqual({
        quotes: [{symbol: 'EURUSD'}, {symbol: 'BTCUSD'}],
        balance: 1100
      });
    });

  });

  /**
   * @test {MetaApiWebsocketClient#unsubscribe}
   */
  describe('unsubscription', () => {

    /**
     * @test {MetaApiWebsocketClient#unsubscribe}
     */
    it('should unsubscribe from account data', async () => {
      let requestReceived = false;

      let response = {type: 'response', accountId: 'accountId'};
      server.on('request', data => {
        if (data.type === 'unsubscribe' && data.accountId === 'accountId') {
          requestReceived = true;
          server.emit('response', Object.assign({requestId: data.requestId}, response));
        }
      });
      await client.unsubscribe('accountId');
      sinon.assert.match(requestReceived, true);
      client.socketInstancesByAccounts.should.not.have.property('accountId');
      sinon.assert.calledWith((client as any)._latencyService.onUnsubscribe, 'accountId');
    });

    /**
     * @test {MetaApiWebsocketClient#unsubscribe}
     */
    it('should not throw errors on unsubscribe', async () => {
      server.on('request', data => {
        server.emit('processingError', {
          id: 1, error: 'ValidationError', message: 'Validation failed',
          details: [{parameter: 'volume', message: 'Required value.'}], requestId: data.requestId
        });
      });
      await client.unsubscribe('accountId');
      server.removeAllListeners('request');
      server.on('request', data => {
        server.emit('processingError', {
          id: 1, error: 'NotFoundError', message: 'Account not found', requestId: data.requestId
        });
      });
      server1.on('request', data => {
        server1.emit('processingError', {
          id: 1, error: 'NotFoundError', message: 'Account not found', requestId: data.requestId
        });
      });
      await client.unsubscribe('accountId');
    });

    /**
     * @test {MetaApiWebsocketClient#unsubscribe}
     */
    it('should ignore timeout error on unsubscribe', async () => {
      let promise = client.unsubscribe('accountId').catch(() => {});
      await clock.tickAsync(15000);
      await promise;
    }).timeout(20000);

    /**
     * @test {MetaApiWebsocketClient#unsubscribe}
     */
    it('should repeat unsubscription on synchronization packets if account must be unsubscribed', async () => {
      let prices = [{
        symbol: 'AUDNZD',
        bid: 1.05916,
        ask: 1.05927,
        profitTickValue: 0.602,
        lossTickValue: 0.60203
      }];
      let subscribeServerHandler = sandbox.stub();
      let unsubscribeServerHandler = sandbox.stub();
      server.on('request', data => {
        let serverHandler;
        if (data.type === 'subscribe' && data.accountId === 'accountId') {
          serverHandler = subscribeServerHandler;
        } else if (data.type === 'unsubscribe' && data.accountId === 'accountId') {
          serverHandler = unsubscribeServerHandler;
        }
        if (serverHandler) {
          serverHandler();
          let response = {type: 'response', accountId: 'accountId'};
          server.emit('response', Object.assign({requestId: data.requestId}, response));
        }
      });
      // Subscribing
      await client.subscribe('accountId', 0);
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledOnce(subscribeServerHandler);
      // Unsubscribing
      await client.unsubscribe('accountId');
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledOnce(unsubscribeServerHandler);
      // Sending a packet, should throttle first repeat unsubscribe request
      server.emit('synchronization', {type: 'prices', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0, prices });
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledOnce(unsubscribeServerHandler);
      // Repeat a packet after a while, should unsubscribe again
      await clock.tickAsync(11000);
      server.emit('synchronization', {type: 'prices', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0, prices });
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledTwice(unsubscribeServerHandler);
      // Repeat a packet, should throttle unsubscribe request
      server.emit('synchronization', {type: 'prices', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0, prices });
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledTwice(unsubscribeServerHandler);
      // Repeat a packet after a while, should not throttle unsubscribe request
      await clock.tickAsync(11000);
      server.emit('synchronization', {type: 'prices', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0, prices });
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledThrice(unsubscribeServerHandler);
    });

  });

  describe('error handling', () => {

    /**
     * @test {MetaApiWebsocketClient#trade}
     */
    it('should handle ValidationError', async () => {
      let trade = {
        actionType: 'ORDER_TYPE_SELL',
        symbol: 'AUDNZD'
      };
      server.on('request', data => {
        server.emit('processingError', {
          id: 1, error: 'ValidationError', message: 'Validation failed',
          details: [{parameter: 'volume', message: 'Required value.'}], requestId: data.requestId
        });
      });
      try {
        await client.trade('accountId', trade);
        throw new Error('ValidationError extected');
      } catch (err) {
        err.name.should.equal('ValidationError');
        err.details.should.match([{
          parameter: 'volume',
          message: 'Required value.'
        }]);
      }
    });

    /**
     * @test {MetaApiWebsocketClient#getPosition}
     */
    it('should handle NotFoundError', async () => {
      server.on('request', data => {
        server.emit('processingError', {
          id: 1, error: 'NotFoundError', message: 'Position id 1234 not found',
          requestId: data.requestId
        });
      });
      try {
        await client.getPosition('accountId', '1234');
        throw new Error('NotFoundError extected');
      } catch (err) {
        err.name.should.equal('NotFoundError');
      }
    });

    /**
     * @test {MetaApiWebsocketClient#getPosition}
     */
    it('should handle NotSynchronizedError', async () => {
      server.on('request', data => {
        server.emit('processingError', {
          id: 1, error: 'NotSynchronizedError', message: 'Error message',
          requestId: data.requestId
        });
      });
      try {
        await client.getPosition('accountId', '1234');
        throw new Error('NotSynchronizedError extected');
      } catch (err) {
        err.name.should.equal('NotSynchronizedError');
      }
    }).timeout(8000);

    /**
     * @test {MetaApiWebsocketClient#getPosition}
     */
    it('should handle NotConnectedError', async () => {
      server.on('request', data => {
        server.emit('processingError', {
          id: 1, error: 'NotAuthenticatedError', message: 'Error message',
          requestId: data.requestId
        });
      });
      try {
        await client.getPosition('accountId', '1234');
        throw new Error('NotConnectedError extected');
      } catch (err) {
        err.name.should.equal('NotConnectedError');
      }
    });

    /**
     * @test {MetaApiWebsocketClient#getPosition}
     */
    it('should handle other errors', async () => {
      server.on('request', data => {
        server.emit('processingError', {
          id: 1, error: 'Error', message: 'Error message',
          requestId: data.requestId
        });
      });
      try {
        await client.getPosition('accountId', '1234');
        throw new Error('InternalError extected');
      } catch (err) {
        err.name.should.equal('InternalError');
      }
    }).timeout(8000);

  });

  describe('connection status synchronization', () => {

    let sessionId;

    beforeEach(() => {
      sandbox.stub((client as any)._subscriptionManager, 'isSubscriptionActive').returns(true);
      sessionId = client.socketInstances['vint-hill'][0][0].sessionId;
      (client as any)._socketInstancesByAccounts = {0: {accountId: 0}, 1: {accountId: 0}};
    });

    afterEach(() => {
      client.removeAllListeners();
    });

    it('should process authenticated synchronization event', async () => {
      let listener: any = {
        onConnected: () => {
        }
      };
      sandbox.stub(listener, 'onConnected').resolves();
      client.addSynchronizationListener('accountId', listener);
      server.emit('synchronization', {type: 'authenticated', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0, replicas: 2});
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledWith(listener.onConnected, 'vint-hill:0:ps-mpa-1', 2);
      sinon.assert.calledWith((client as any)._latencyService.onConnected, 'accountId:vint-hill:0:ps-mpa-1');
    });

    it('should send trade requests to both instances', async () => {
      let listener: any = {
        onConnected: () => {
        }
      };
      let instanceCalled0 = false;
      let instanceCalled1 = false;
      let trade = {
        actionType: 'ORDER_TYPE_SELL',
        symbol: 'AUDNZD',
        volume: 0.07
      };
      let response = {
        numericCode: 10009,
        stringCode: 'TRADE_RETCODE_DONE',
        message: 'Request completed',
        orderId: '46870472'
      };
      server.on('request', data => {
        if (data.type === 'trade' && data.accountId === 'accountId' && data.application === 'application') {
          instanceCalled0 = true;
          server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, response});
        }
      });
      server1.on('request', data => {
        if (data.type === 'trade' && data.accountId === 'accountId' && data.application === 'application') {
          instanceCalled1 = true;
          server1.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, response});
        }
      });
      sandbox.stub(listener, 'onConnected').resolves();
      client.addSynchronizationListener('accountId', listener);
      await client.trade('accountId', trade, undefined, 'high');
      await new Promise(res => setTimeout(res, 100));
      sinon.assert.match(instanceCalled0, true);
      sinon.assert.match(instanceCalled1, true);
    });

    it('should not send requests to mismatching instances', async () => {
      let requestReceivedAssigned0 = false;
      let requestReceivedAssigned1 = false;
      server.on('request', data => {
        if (data.type === 'subscribe' && data.accountId === 'accountId' && data.application === 'application'
        && data.instanceIndex === 0) {
          requestReceivedAssigned0 = true;
          server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId});
        }
      });
      server1.on('request', data => {
        if (data.type === 'subscribe' && data.accountId === 'accountId' && data.application === 'application' 
        && data.instanceIndex === 1) {
          requestReceivedAssigned1 = true;
          server1.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId});
        }
      });
      await client.subscribe('accountId', 0);
      sinon.assert.match(requestReceivedAssigned0, true);
      await client.subscribe('accountId', 1);
      sinon.assert.match(requestReceivedAssigned1, true);

      let requestReceivedAuthenticated0 = false;
      let requestReceivedAuthenticated1 = false;
      server.on('request', data => {
        if (data.type === 'removeApplication' && data.accountId === 'accountId' && data.application === 'application'
        && data.instanceIndex === 0) {
          requestReceivedAuthenticated0 = true;
          server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId});
        }
      });
      server1.on('request', data => {
        if (data.type === 'removeApplication' && data.accountId === 'accountId' && data.application === 'application'
        && data.instanceIndex === 1) {
          requestReceivedAuthenticated1 = true;
          server1.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId});
        }
      });
      await client.removeApplication('accountId');
      sinon.assert.match(requestReceivedAuthenticated0, true);
      sinon.assert.match(requestReceivedAuthenticated1, false);

      requestReceivedAuthenticated0 = false;
      stubs.latencyService.getActiveInstances.returns(['accountId:vint-hill:1:ps-mpa-1']);
      await client.removeApplication('accountId');
      sinon.assert.match(requestReceivedAuthenticated0, false);
      sinon.assert.match(requestReceivedAuthenticated1, true);

      let listener: any = {
        onConnected: () => {
        }
      };
      let instanceCalledTrade0 = false;
      let instanceCalledTrade1 = false;
      let trade = {
        actionType: 'ORDER_TYPE_SELL',
        symbol: 'AUDNZD',
        volume: 0.07
      };
      let response = {
        numericCode: 10009,
        stringCode: 'TRADE_RETCODE_DONE',
        message: 'Request completed',
        orderId: '46870472'
      };
      server.on('request', data => {
        if (data.type === 'trade' && data.accountId === 'accountId' && data.application === 'application') {
          instanceCalledTrade0 = true;
          server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, response});
        }
      });
      server1.on('request', data => {
        if (data.type === 'trade' && data.accountId === 'accountId' && data.application === 'application') {
          instanceCalledTrade1 = true;
          server1.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, response});
        }
      });
      stubs.latencyService.getActiveInstances.returns(['accountId:vint-hill:0:ps-mpa-1']);
      sandbox.stub(listener, 'onConnected').resolves();
      client.addSynchronizationListener('accountId', listener);
      await client.trade('accountId', trade, undefined, 'regular');
      sinon.assert.match(instanceCalledTrade0, true);
      sinon.assert.match(instanceCalledTrade1, false);

      instanceCalledTrade0 = false;
      stubs.latencyService.getActiveInstances.returns(['accountId:vint-hill:1:ps-mpa-1']);
      await client.trade('accountId', trade, undefined, 'regular');
      sinon.assert.match(instanceCalledTrade0, false);
      sinon.assert.match(instanceCalledTrade1, true);
    });

    it('should process authenticated synchronization event with session id', async () => {
      let listener: any = {
        onConnected: () => {
        }
      };
      sandbox.stub(listener, 'onConnected').resolves();
      client.addSynchronizationListener('accountId', listener);
      server.emit('synchronization', {type: 'authenticated', accountId: 'accountId', host: 'ps-mpa-2',
        instanceIndex: 0, replicas: 1, sessionId: 'wrong'});
      server.emit('synchronization', {type: 'authenticated', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0, replicas: 2, sessionId });
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.callCount(listener.onConnected, 1);
      sinon.assert.calledWith(listener.onConnected, 'vint-hill:0:ps-mpa-1', 2);
    });

    it('should cancel subscribe on authenticated event', async () => {
      let listener: any = {
        onConnected: () => {}
      };
      sandbox.stub(listener, 'onConnected').resolves();
      const cancelSubscribeStub = sandbox.stub((client as any)._subscriptionManager, 'cancelSubscribe');
      const cancelAccountStub = sandbox.stub((client as any)._subscriptionManager, 'cancelAccount');
      client.addSynchronizationListener('accountId', listener);
      (client as any)._socketInstancesByAccounts[0].accountId2 = 0;
      (client as any)._socketInstancesByAccounts[1].accountId2 = 0;
      (client as any)._regionsByAccounts.accountId2 = {region: 'vint-hill', connections: 1};
      server.emit('synchronization', {type: 'authenticated', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0, replicas: 2, sessionId });
      server.emit('synchronization', {type: 'authenticated', accountId: 'accountId2', host: 'ps-mpa-2',
        instanceIndex: 0, replicas: 1, sessionId });
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledWith(cancelSubscribeStub, 'accountId:0');
      sinon.assert.calledWith(cancelAccountStub, 'accountId2');
    });

    it('should process broker connection status event', async () => {
      let listener: any = {
        onConnected: () => {},
        onBrokerConnectionStatusChanged: () => {}
      };
      sandbox.stub(listener, 'onBrokerConnectionStatusChanged').resolves();
      client.addSynchronizationListener('accountId', listener);
      server.emit('synchronization', {type: 'authenticated', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0});
      server.emit('synchronization', {type: 'status', accountId: 'accountId', host: 'ps-mpa-1', connected: true,
        instanceIndex: 0});
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledWith(listener.onBrokerConnectionStatusChanged, 'vint-hill:0:ps-mpa-1', true);
    });

    it('should call an onDisconnect if there was no signal for a long time', async () => {
      let listener: any = {
        onConnected: () => {},
        onDisconnected: () => {},
        onBrokerConnectionStatusChanged: () => {},
        onStreamClosed: () => {},
      };
      sandbox.stub(listener, 'onDisconnected').resolves();
      client.addSynchronizationListener('accountId', listener);
      server.emit('synchronization', {type: 'authenticated', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0, replicas: 2});
      server.emit('synchronization', {type: 'status', accountId: 'accountId', host: 'ps-mpa-1', connected: true,
        instanceIndex: 0});
      await new Promise(res => setTimeout(res, 50));
      await clock.tickAsync(10000);
      await server.emit('synchronization', {type: 'status', accountId: 'accountId', host: 'ps-mpa-1', connected: true,
        instanceIndex: 0});
      await new Promise(res => setTimeout(res, 50));
      await clock.tickAsync(55000);
      sinon.assert.notCalled(listener.onDisconnected);
      server.emit('synchronization', {type: 'authenticated', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0, replicas: 2});
      await new Promise(res => setTimeout(res, 50));
      await clock.tickAsync(10000);
      sinon.assert.notCalled(listener.onDisconnected);
      await clock.tickAsync(55000);
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledWith(listener.onDisconnected, 'vint-hill:0:ps-mpa-1');
      sinon.assert.calledWith((client as any)._latencyService.onDisconnected, 'accountId:vint-hill:0:ps-mpa-1');
      await clock.tickAsync(10000);
      clock.restore();
    });

    it('should close stream on timeout if another stream exists', async () => {
      let listener: any = {
        onConnected: () => {},
        onDisconnected: () => {},
        onStreamClosed: () => {},
        onBrokerConnectionStatusChanged: () => {}
      };
      const onTimeoutStub = sandbox.stub((client as any)._subscriptionManager, 'onTimeout').resolves();
      const onStreamClosedStub = sandbox.stub(listener, 'onStreamClosed').resolves();
      const onDisconnectedStub = sandbox.stub(listener, 'onDisconnected').resolves();
      sandbox.stub((client as any)._subscriptionManager, 'onDisconnected').resolves();
      client.addSynchronizationListener('accountId', listener);
      server.emit('synchronization', {type: 'authenticated', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0, replicas: 2});
      await new Promise(res => setTimeout(res, 50));
      await clock.tickAsync(15000);
      server.emit('synchronization', {type: 'authenticated', accountId: 'accountId', host: 'ps-mpa-2',
        instanceIndex: 0, replicas: 2});
      server.emit('synchronization', {type: 'status', accountId: 'accountId', host: 'ps-mpa-1', connected: true,
        instanceIndex: 0});
      server.emit('synchronization', {type: 'status', accountId: 'accountId', host: 'ps-mpa-2', connected: true,
        instanceIndex: 0});
      await new Promise(res => setTimeout(res, 50));
      await clock.tickAsync(15000);
      server.emit('synchronization', {type: 'status', accountId: 'accountId', host: 'ps-mpa-1', connected: true,
        instanceIndex: 0});
      server.emit('synchronization', {type: 'status', accountId: 'accountId', host: 'ps-mpa-2', connected: true,
        instanceIndex: 0});
      await new Promise(res => setTimeout(res, 50));
      await clock.tickAsync(55000);
      sinon.assert.notCalled(onDisconnectedStub);
      server.emit('synchronization', {type: 'status', accountId: 'accountId', host: 'ps-mpa-1', connected: true,
        instanceIndex: 0});
      server.emit('synchronization', {type: 'status', accountId: 'accountId', host: 'ps-mpa-2', connected: true,
        instanceIndex: 0});
      await new Promise(res => setTimeout(res, 50));
      await clock.tickAsync(15000);
      server.emit('synchronization', {type: 'status', accountId: 'accountId', host: 'ps-mpa-2', connected: true,
        instanceIndex: 0});
      sinon.assert.notCalled(onDisconnectedStub);
      await new Promise(res => setTimeout(res, 50));
      await clock.tickAsync(55000);
      sinon.assert.calledWith(onStreamClosedStub, 'vint-hill:0:ps-mpa-1');
      sinon.assert.notCalled(onDisconnectedStub);
      sinon.assert.notCalled(onTimeoutStub);
      await new Promise(res => setTimeout(res, 50));
      await clock.tickAsync(15000);
      sinon.assert.calledWith(onDisconnectedStub, 'vint-hill:0:ps-mpa-2');
      sinon.assert.notCalled((client as any)._subscriptionManager.onDisconnected);
      sinon.assert.calledWith(onTimeoutStub, 'accountId', 0);
    });

    it('should process server-side health status event', async () => {
      let listener: any = {
        onConnected: () => {},
        onBrokerConnectionStatusChanged: () => {},
        onHealthStatus: () => {}
      };
      sandbox.stub(listener, 'onHealthStatus').resolves();
      client.addSynchronizationListener('accountId', listener);
      server.emit('synchronization', {type: 'authenticated', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0});
      server.emit('synchronization', {type: 'status', accountId: 'accountId', host: 'ps-mpa-1', connected: true,
        healthStatus: {restApiHealthy: true}, instanceIndex: 0});
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledWith(listener.onHealthStatus, 'vint-hill:0:ps-mpa-1', {restApiHealthy: true});
    });

    it('should process disconnected synchronization event', async () => {
      let listener: any = {
        onConnected: () => {},
        onDisconnected: () => {},
        onStreamClosed: () => {},
      };
      sandbox.stub(listener, 'onDisconnected').resolves();
      sandbox.stub(listener, 'onStreamClosed').resolves();
      sandbox.stub((client as any)._subscriptionManager, 'onDisconnected').resolves();
      client.addSynchronizationListener('accountId', listener);
      server.emit('synchronization', {type: 'authenticated', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0});
      server.emit('synchronization', {type: 'disconnected', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0});
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledWith(listener.onDisconnected, 'vint-hill:0:ps-mpa-1');
      sinon.assert.calledWith((client as any)._subscriptionManager.onDisconnected, 'accountId', 0);
      sinon.assert.calledWith(listener.onStreamClosed, 'vint-hill:0:ps-mpa-1');
    });

    it('should close the stream if host name disconnected and another stream exists', async () => {
      let listener: any = {
        onConnected: () => {},
        onDisconnected: () => {},
        onStreamClosed: () => {},
      };
      sandbox.stub(listener, 'onDisconnected').resolves();
      sandbox.stub(listener, 'onStreamClosed').resolves();
      const onDisconnectedStub = sandbox.stub((client as any)._subscriptionManager, 'onDisconnected').resolves();
      client.addSynchronizationListener('accountId', listener);
      server.emit('synchronization', {type: 'authenticated', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0, replicas: 2});
      server.emit('synchronization', {type: 'authenticated', accountId: 'accountId', host: 'ps-mpa-2',
        instanceIndex: 0, replicas: 2});
      server.emit('synchronization', {type: 'disconnected', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0});
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledWith(listener.onStreamClosed, 'vint-hill:0:ps-mpa-1');
      sinon.assert.notCalled(listener.onDisconnected);
      sinon.assert.notCalled(onDisconnectedStub);
      server.emit('synchronization', {type: 'disconnected', accountId: 'accountId', host: 'ps-mpa-2',
        instanceIndex: 0});
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledOnce(listener.onDisconnected);
      sinon.assert.calledWith(onDisconnectedStub, 'accountId', 0);
    });
  });

  describe('terminal state synchronization', () => {

    beforeEach(() => {
      sandbox.stub((client as any)._subscriptionManager, 'isSubscriptionActive').returns(true);
    });

    afterEach(() => {
      client.removeAllListeners();
    });

    it('should only accept packets with own synchronization ids', async () => {
      let listener: any = {
        onAccountInformationUpdated: () => {},
        onSynchronizationStarted: () => {}
      };
      sandbox.stub(listener, 'onAccountInformationUpdated').resolves();
      client.addSynchronizationListener('accountId', listener);
      sandbox.stub((client as any)._socketInstances['vint-hill'][0][0].synchronizationThrottler, 
        'activeSynchronizationIds').get(() => ['synchronizationId']);
      server.emit('synchronization', {type: 'accountInformation', accountId: 'accountId', 
        accountInformation: {}, instanceIndex: 0});
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.callCount(listener.onAccountInformationUpdated, 1);
      server.emit('synchronization', {type: 'synchronizationStarted', accountId: 'accountId',
        instanceIndex: 0, synchronizationId: 'synchronizationId'});
      await new Promise(res => setTimeout(res, 50));
      server.emit('synchronization', {type: 'accountInformation', accountId: 'accountId',
        accountInformation: {}, instanceIndex: 0, synchronizationId: 'wrong'});
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.callCount(listener.onAccountInformationUpdated, 1);
      server.emit('synchronization', {type: 'accountInformation', accountId: 'accountId', 
        accountInformation: {}, instanceIndex: 0, synchronizationId: 'synchronizationId'});
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.callCount(listener.onAccountInformationUpdated, 2);
    });

    /**
     * @test {MetaApiWebsocketClient#synchronize}
     */
    it('should synchronize with MetaTrader terminal', async () => {
      let requestReceived = false;
      // eslint-disable-next-line complexity
      server.on('request', data => {
        if (data.type === 'synchronize' && data.accountId === 'accountId' &&
          data.host === 'ps-mpa-1' &&
          data.startingHistoryOrderTime === '2020-01-01T00:00:00.000Z' &&
          data.startingDealTime === '2020-01-02T00:00:00.000Z' && data.requestId === 'synchronizationId' &&
          data.application === 'application' && data.instanceIndex === 0) {
          requestReceived = true;
          server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId});
        }
      });
      await client.synchronize('accountId', 0, 'ps-mpa-1', 'synchronizationId', new Date('2020-01-01T00:00:00.000Z'),
        new Date('2020-01-02T00:00:00.000Z'), () => ({
          specificationsHashes: '1111',
          positionsHashes: '2222',
          ordersHashes: '3333'
        }));
      requestReceived.should.be.true();
    });

    it('should process synchronization started event', async () => {
      (client as any)._socketInstances['vint-hill'][0][0].synchronizationThrottler = synchronizationThrottler;
      let listener: any = {
        onSynchronizationStarted: () => {},
        onPositionsSynchronized: () => {},
        onPendingOrdersSynchronized: () => {},
        onAccountInformationUpdated: () => {},
      };
      sandbox.stub(listener, 'onSynchronizationStarted').resolves();
      sandbox.stub(listener, 'onPositionsSynchronized').resolves();
      sandbox.stub(listener, 'onPendingOrdersSynchronized').resolves();
      client.addSynchronizationListener('accountId', listener);
      server.emit('synchronization', {type: 'synchronizationStarted', accountId: 'accountId', instanceIndex: 0,
        synchronizationId: 'synchronizationId', host: 'ps-mpa-1'});
      server.emit('synchronization', {type: 'accountInformation', accountId: 'accountId', 
        accountInformation, instanceIndex: 0, host: 'ps-mpa-1', synchronizationId: 'synchronizationId'});
      await new Promise(res => setTimeout(res, 100));
      sinon.assert.calledWith(listener.onSynchronizationStarted, 'vint-hill:0:ps-mpa-1',
        undefined, undefined, undefined, 'synchronizationId');
      sinon.assert.notCalled(listener.onPositionsSynchronized);
      sinon.assert.notCalled(listener.onPendingOrdersSynchronized);
    });

    it('should process synchronization started event with no updates', async () => {
      (client as any)._socketInstances['vint-hill'][0][0].synchronizationThrottler = synchronizationThrottler;
      const hashes = {specificationsHashes: ['shash0', 'shash1', 'shash2'],
        positionsHashes: ['phash0', 'phash1', 'phash2'],
        ordersHashes: ['ohash0', 'ohash1', 'ohash2']};
      sandbox.stub(synchronizationThrottler, 'scheduleSynchronize').resolves();
      let listener: any = {
        onSynchronizationStarted: () => {},
        onPositionsSynchronized: () => {},
        onPendingOrdersSynchronized: () => {},
        onAccountInformationUpdated: () => {},
      };
      sandbox.stub(listener, 'onSynchronizationStarted').resolves();
      sandbox.stub(listener, 'onPositionsSynchronized').resolves();
      sandbox.stub(listener, 'onPendingOrdersSynchronized').resolves();
      client.addSynchronizationListener('accountId', listener);
      client.synchronize('accountId', 0, 'ps-mpa-1', 'synchronizationId', 
        new Date('2020-04-15T02:45:06.521Z'), new Date('2020-04-15T02:45:06.521Z'), hashes);
      server.emit('synchronization', {type: 'synchronizationStarted', accountId: 'accountId', instanceIndex: 0,
        specificationsHashIndex: 0, positionsHashIndex: 1, ordersHashIndex: 2,
        synchronizationId: 'synchronizationId', host: 'ps-mpa-1'});
      server.emit('synchronization', {type: 'accountInformation', accountId: 'accountId', 
        accountInformation, instanceIndex: 0, host: 'ps-mpa-1', synchronizationId: 'synchronizationId'});
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledWith(synchronizationThrottler.scheduleSynchronize as any, 'accountId', {
        host: 'ps-mpa-1', instanceIndex: 0, requestId: 'synchronizationId',
        startingDealTime: new Date('2020-04-15T02:45:06.521Z'),
        startingHistoryOrderTime: new Date('2020-04-15T02:45:06.521Z'),
        type: 'synchronize', version: 2
      }, hashes);
      sinon.assert.calledWith(listener.onSynchronizationStarted, 'vint-hill:0:ps-mpa-1', 'shash0', 'phash1', 'ohash2');
      sinon.assert.calledWith(listener.onPositionsSynchronized, 'vint-hill:0:ps-mpa-1', 'synchronizationId');
      sinon.assert.calledWith(listener.onPendingOrdersSynchronized, 'vint-hill:0:ps-mpa-1', 'synchronizationId');
    });

    it('should process synchronization started event without updating positions', async () => {
      let orders = [{
        id: '46871284',
        type: 'ORDER_TYPE_BUY_LIMIT',
        state: 'ORDER_STATE_PLACED',
        symbol: 'AUDNZD',
        magic: 123456,
        platform: 'mt5',
        time: new Date('2020-04-20T08:38:58.270Z'),
        openPrice: 1.03,
        currentPrice: 1.05206,
        volume: 0.01,
        currentVolume: 0.01,
        comment: 'COMMENT2'
      }];
      (client as any)._socketInstances['vint-hill'][0][0].synchronizationThrottler = synchronizationThrottler;
      const hashes = {specificationsHashes: ['shash0', 'shash1', 'shash2'],
        positionsHashes: ['phash0', 'phash1', 'phash2'],
        ordersHashes: ['ohash0', 'ohash1', 'ohash2']};
      let listener: any = {
        onSynchronizationStarted: () => {},
        onPositionsSynchronized: () => {},
        onPendingOrdersSynchronized: () => {},
        onPendingOrdersReplaced: () => {},
        onAccountInformationUpdated: () => {},
      };
      sandbox.stub(listener, 'onSynchronizationStarted').resolves();
      sandbox.stub(listener, 'onPositionsSynchronized').resolves();
      sandbox.stub(listener, 'onPendingOrdersSynchronized').resolves();
      client.addSynchronizationListener('accountId', listener);
      client.synchronize('accountId', 0, 'ps-mpa-1', 'synchronizationId', 
        new Date('2020-04-15T02:45:06.521Z'), new Date('2020-04-15T02:45:06.521Z'), hashes);
      server.emit('synchronization', {type: 'synchronizationStarted', accountId: 'accountId', instanceIndex: 0,
        synchronizationId: 'synchronizationId', host: 'ps-mpa-1', positionsHashIndex: 1});
      server.emit('synchronization', {type: 'accountInformation', accountId: 'accountId', 
        accountInformation, instanceIndex: 0, host: 'ps-mpa-1', synchronizationId: 'synchronizationId'});
      await new Promise(res => setTimeout(res, 100));
      sinon.assert.calledWith(listener.onSynchronizationStarted, 'vint-hill:0:ps-mpa-1', undefined,
        'phash1', undefined, 'synchronizationId');
      sinon.assert.calledWith(listener.onPositionsSynchronized, 'vint-hill:0:ps-mpa-1', 'synchronizationId');
      sinon.assert.notCalled(listener.onPendingOrdersSynchronized);
      server.emit('synchronization', {type: 'orders', accountId: 'accountId', orders, instanceIndex: 0,
        synchronizationId: 'synchronizationId', host: 'ps-mpa-1'});
      await new Promise(res => setTimeout(res, 100));
      sinon.assert.calledWith(listener.onPendingOrdersSynchronized, 'vint-hill:0:ps-mpa-1', 'synchronizationId');
    });

    it('should process synchronization started event without updating orders', async () => {
      let positions = [{
        id: '46214692',
        type: 'POSITION_TYPE_BUY',
        symbol: 'GBPUSD',
        magic: 1000,
        time: '2020-04-15T02:45:06.521Z',
        updateTime: '2020-04-15T02:45:06.521Z',
        openPrice: 1.26101,
        currentPrice: 1.24883,
        currentTickValue: 1,
        volume: 0.07,
        swap: 0,
        profit: -85.25999999999966,
        commission: -0.25,
        clientId: 'TE_GBPUSD_7hyINWqAlE',
        stopLoss: 1.17721,
        unrealizedProfit: -85.25999999999901,
        realizedProfit: -6.536993168992922e-13
      }];

      (client as any)._socketInstances['vint-hill'][0][0].synchronizationThrottler = synchronizationThrottler;
      const hashes = {specificationsHashes: ['shash0', 'shash1', 'shash2'],
        positionsHashes: ['phash0', 'phash1', 'phash2'],
        ordersHashes: ['ohash0', 'ohash1', 'ohash2']};
      let listener: any = {
        onSynchronizationStarted: () => {},
        onPositionsSynchronized: () => {},
        onPendingOrdersSynchronized: () => {},
        onPositionsReplaced: () => {},
        onAccountInformationUpdated: () => {},
      };
      sandbox.stub(listener, 'onSynchronizationStarted').resolves();
      sandbox.stub(listener, 'onPositionsSynchronized').resolves();
      sandbox.stub(listener, 'onPendingOrdersSynchronized').resolves();
      client.addSynchronizationListener('accountId', listener);
      client.synchronize('accountId', 0, 'ps-mpa-1', 'synchronizationId', 
        new Date('2020-04-15T02:45:06.521Z'), new Date('2020-04-15T02:45:06.521Z'), hashes);
      server.emit('synchronization', {type: 'synchronizationStarted', accountId: 'accountId', instanceIndex: 0,
        synchronizationId: 'synchronizationId', host: 'ps-mpa-1', ordersHashIndex: 2});
      server.emit('synchronization', {type: 'accountInformation', accountId: 'accountId', 
        accountInformation, instanceIndex: 0, host: 'ps-mpa-1', synchronizationId: 'synchronizationId'});
      await new Promise(res => setTimeout(res, 100));
      sinon.assert.calledWith(listener.onSynchronizationStarted, 'vint-hill:0:ps-mpa-1', undefined,
        undefined, 'ohash2', 'synchronizationId');
      sinon.assert.notCalled(listener.onPositionsSynchronized);
      sinon.assert.notCalled(listener.onPendingOrdersSynchronized);
      server.emit('synchronization', {type: 'positions', accountId: 'accountId', positions, instanceIndex: 0,
        synchronizationId: 'synchronizationId', host: 'ps-mpa-1'});
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledWith(listener.onPositionsSynchronized, 'vint-hill:0:ps-mpa-1', 'synchronizationId');
      sinon.assert.calledWith(listener.onPendingOrdersSynchronized, 'vint-hill:0:ps-mpa-1', 'synchronizationId');
    });

    it('should synchronize account information', async () => {
      let listener: any = {
        onAccountInformationUpdated: () => {
        }
      };
      sandbox.stub(listener, 'onAccountInformationUpdated').resolves();
      client.addSynchronizationListener('accountId', listener);
      server.emit('synchronization', {type: 'accountInformation', accountId: 'accountId',
        host: 'ps-mpa-1', accountInformation, instanceIndex: 0});
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledWith(listener.onAccountInformationUpdated, 'vint-hill:0:ps-mpa-1', accountInformation);
    });

    it('should synchronize positions', async () => {
      let positions = [{
        id: '46214692',
        type: 'POSITION_TYPE_BUY',
        symbol: 'GBPUSD',
        magic: 1000,
        time: new Date('2020-04-15T02:45:06.521Z'),
        updateTime: new Date('2020-04-15T02:45:06.521Z'),
        openPrice: 1.26101,
        currentPrice: 1.24883,
        currentTickValue: 1,
        volume: 0.07,
        swap: 0,
        profit: -85.25999999999966,
        commission: -0.25,
        clientId: 'TE_GBPUSD_7hyINWqAlE',
        stopLoss: 1.17721,
        unrealizedProfit: -85.25999999999901,
        realizedProfit: -6.536993168992922e-13
      }];
      (client as any)._socketInstances['vint-hill'][0][0].synchronizationThrottler = synchronizationThrottler;
      let listener: any = {
        onPositionsReplaced: () => {},
        onPositionsSynchronized: () => {},
        onSynchronizationStarted: () => {}
      };
      sandbox.stub(listener, 'onPositionsReplaced').resolves();
      sandbox.stub(listener, 'onPositionsSynchronized').resolves();
      client.addSynchronizationListener('accountId', listener);
      server.emit('synchronization', {type: 'synchronizationStarted', accountId: 'accountId',
        instanceIndex: 0, synchronizationId: 'synchronizationId', host: 'ps-mpa-1'});
      await new Promise(res => setTimeout(res, 50));
      server.emit('synchronization', {type: 'positions', accountId: 'accountId', positions, instanceIndex: 0,
        synchronizationId: 'synchronizationId', host: 'ps-mpa-1'});
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledWith(listener.onPositionsReplaced, 'vint-hill:0:ps-mpa-1', positions);
      sinon.assert.calledWith(listener.onPositionsSynchronized, 'vint-hill:0:ps-mpa-1', 'synchronizationId');
    });

    it('should synchronize orders', async () => {
      let orders = [{
        id: '46871284',
        type: 'ORDER_TYPE_BUY_LIMIT',
        state: 'ORDER_STATE_PLACED',
        symbol: 'AUDNZD',
        magic: 123456,
        platform: 'mt5',
        time: new Date('2020-04-20T08:38:58.270Z'),
        openPrice: 1.03,
        currentPrice: 1.05206,
        volume: 0.01,
        currentVolume: 0.01,
        comment: 'COMMENT2'
      }];
      let listener: any = {
        onPendingOrdersReplaced: () => {},
        onPendingOrdersSynchronized: () => {},
        onSynchronizationStarted: () => {}
      };
      (client as any)._socketInstances['vint-hill'][0][0].synchronizationThrottler = synchronizationThrottler;
      sandbox.stub(listener, 'onPendingOrdersReplaced').resolves();
      sandbox.stub(listener, 'onPendingOrdersSynchronized').resolves();
      client.addSynchronizationListener('accountId', listener);
      server.emit('synchronization', {type: 'synchronizationStarted', accountId: 'accountId',
        instanceIndex: 0, synchronizationId: 'synchronizationId', host: 'ps-mpa-1'});
      await new Promise(res => setTimeout(res, 50));
      server.emit('synchronization', {type: 'orders', accountId: 'accountId', orders, instanceIndex: 0,
        synchronizationId: 'synchronizationId', host: 'ps-mpa-1'});
      await new Promise(res => setTimeout(res, 100));
      sinon.assert.calledWith(listener.onPendingOrdersReplaced, 'vint-hill:0:ps-mpa-1', orders);
      sinon.assert.calledWith(listener.onPendingOrdersSynchronized, 'vint-hill:0:ps-mpa-1', 'synchronizationId');
    });

    it('should synchronize history orders', async () => {
      let historyOrders = [{
        clientId: 'TE_GBPUSD_7hyINWqAlE',
        currentPrice: 1.261,
        currentVolume: 0,
        doneTime: new Date('2020-04-15T02:45:06.521Z'),
        id: '46214692',
        magic: 1000,
        platform: 'mt5',
        positionId: '46214692',
        state: 'ORDER_STATE_FILLED',
        symbol: 'GBPUSD',
        time: new Date('2020-04-15T02:45:06.260Z'),
        type: 'ORDER_TYPE_BUY',
        volume: 0.07
      }];
      let listener: any = {
        onHistoryOrderAdded: () => {
        }
      };
      sandbox.stub(listener, 'onHistoryOrderAdded').resolves();
      client.addSynchronizationListener('accountId', listener);
      server.emit('synchronization', {type: 'historyOrders', accountId: 'accountId', historyOrders,
        instanceIndex: 0, host: 'ps-mpa-1'});
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledWith(listener.onHistoryOrderAdded, 'vint-hill:0:ps-mpa-1', historyOrders[0]);
    });

    it('should synchronize deals', async () => {
      let deals = [{
        clientId: 'TE_GBPUSD_7hyINWqAlE',
        commission: -0.25,
        entryType: 'DEAL_ENTRY_IN',
        id: '33230099',
        magic: 1000,
        platform: 'mt5',
        orderId: '46214692',
        positionId: '46214692',
        price: 1.26101,
        profit: 0,
        swap: 0,
        symbol: 'GBPUSD',
        time: new Date('2020-04-15T02:45:06.521Z'),
        type: 'DEAL_TYPE_BUY',
        volume: 0.07
      }];
      let listener: any = {
        onDealAdded: () => {
        }
      };
      sandbox.stub(listener, 'onDealAdded').resolves();
      client.addSynchronizationListener('accountId', listener);
      server.emit('synchronization', {type: 'deals', accountId: 'accountId', deals, instanceIndex: 0,
        host: 'ps-mpa-1'});
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledWith(listener.onDealAdded, 'vint-hill:0:ps-mpa-1', deals[0]);
    });

    it('should process synchronization updates', async () => {
      let update = {
        accountInformation: {
          broker: 'True ECN Trading Ltd',
          currency: 'USD',
          server: 'ICMarketsSC-Demo',
          balance: 7319.9,
          equity: 7306.649913200001,
          margin: 184.1,
          freeMargin: 7120.22,
          leverage: 100,
          marginLevel: 3967.58283542
        },
        updatedPositions: [{
          id: '46214692',
          type: 'POSITION_TYPE_BUY',
          symbol: 'GBPUSD',
          magic: 1000,
          time: new Date('2020-04-15T02:45:06.521Z'),
          updateTime: new Date('2020-04-15T02:45:06.521Z'),
          openPrice: 1.26101,
          currentPrice: 1.24883,
          currentTickValue: 1,
          volume: 0.07,
          swap: 0,
          profit: -85.25999999999966,
          commission: -0.25,
          clientId: 'TE_GBPUSD_7hyINWqAlE',
          stopLoss: 1.17721,
          unrealizedProfit: -85.25999999999901,
          realizedProfit: -6.536993168992922e-13
        }],
        removedPositionIds: ['1234'],
        updatedOrders: [{
          id: '46871284',
          type: 'ORDER_TYPE_BUY_LIMIT',
          state: 'ORDER_STATE_PLACED',
          symbol: 'AUDNZD',
          magic: 123456,
          platform: 'mt5',
          time: new Date('2020-04-20T08:38:58.270Z'),
          openPrice: 1.03,
          currentPrice: 1.05206,
          volume: 0.01,
          currentVolume: 0.01,
          comment: 'COMMENT2'
        }],
        completedOrderIds: ['2345'],
        historyOrders: [{
          clientId: 'TE_GBPUSD_7hyINWqAlE',
          currentPrice: 1.261,
          currentVolume: 0,
          doneTime: new Date('2020-04-15T02:45:06.521Z'),
          id: '46214692',
          magic: 1000,
          platform: 'mt5',
          positionId: '46214692',
          state: 'ORDER_STATE_FILLED',
          symbol: 'GBPUSD',
          time: new Date('2020-04-15T02:45:06.260Z'),
          type: 'ORDER_TYPE_BUY',
          volume: 0.07
        }],
        deals: [{
          clientId: 'TE_GBPUSD_7hyINWqAlE',
          commission: -0.25,
          entryType: 'DEAL_ENTRY_IN',
          id: '33230099',
          magic: 1000,
          platform: 'mt5',
          orderId: '46214692',
          positionId: '46214692',
          price: 1.26101,
          profit: 0,
          swap: 0,
          symbol: 'GBPUSD',
          time: new Date('2020-04-15T02:45:06.521Z'),
          type: 'DEAL_TYPE_BUY',
          volume: 0.07
        }]
      };
      let listener: any = {
        onAccountInformationUpdated: () => {},
        onPositionsUpdated: () => {},
        onPositionUpdated: () => {},
        onPositionRemoved: () => {},
        onPendingOrdersUpdated: () => {},
        onPendingOrderUpdated: () => {},
        onPendingOrderCompleted: () => {},
        onHistoryOrderAdded: () => {},
        onDealAdded: () => {}
      };
      sandbox.stub(listener, 'onAccountInformationUpdated').resolves();
      sandbox.stub(listener, 'onPositionsUpdated').resolves();
      sandbox.stub(listener, 'onPositionUpdated').resolves();
      sandbox.stub(listener, 'onPositionRemoved').resolves();
      sandbox.stub(listener, 'onPendingOrdersUpdated').resolves();
      sandbox.stub(listener, 'onPendingOrderUpdated').resolves();
      sandbox.stub(listener, 'onPendingOrderCompleted').resolves();
      sandbox.stub(listener, 'onHistoryOrderAdded').resolves();
      sandbox.stub(listener, 'onDealAdded').resolves();
      client.addSynchronizationListener('accountId', listener);
      server.emit('synchronization', Object.assign({type: 'update', accountId: 'accountId', instanceIndex: 0,
        host: 'ps-mpa-1'}, update));
      await new Promise(res => setTimeout(res, 100));
      sinon.assert.calledWith(listener.onAccountInformationUpdated, 'vint-hill:0:ps-mpa-1', update.accountInformation);
      sinon.assert.calledWith(listener.onPositionsUpdated, 'vint-hill:0:ps-mpa-1', update.updatedPositions);
      sinon.assert.calledWith(listener.onPositionUpdated, 'vint-hill:0:ps-mpa-1', update.updatedPositions[0]);
      sinon.assert.calledWith(listener.onPositionRemoved, 'vint-hill:0:ps-mpa-1', update.removedPositionIds[0]);
      sinon.assert.calledWith(listener.onPendingOrdersUpdated, 'vint-hill:0:ps-mpa-1', update.updatedOrders);
      sinon.assert.calledWith(listener.onPendingOrderUpdated, 'vint-hill:0:ps-mpa-1', update.updatedOrders[0]);
      sinon.assert.calledWith(listener.onPendingOrderCompleted, 'vint-hill:0:ps-mpa-1', update.completedOrderIds[0]);
      sinon.assert.calledWith(listener.onHistoryOrderAdded, 'vint-hill:0:ps-mpa-1', update.historyOrders[0]);
      sinon.assert.calledWith(listener.onDealAdded, 'vint-hill:0:ps-mpa-1', update.deals[0]);
    });

    // eslint-disable-next-line max-statements
    it('should call updates again if they arrive during sync', async () => {
      (client as any)._socketInstances['vint-hill'][0][0].synchronizationThrottler = synchronizationThrottler;
      sandbox.stub(synchronizationThrottler, 'activeSynchronizationIds').get(() => ['ABC']);
      let update = {
        accountInformation: {
          broker: 'True ECN Trading Ltd',
          currency: 'USD',
          server: 'ICMarketsSC-Demo',
          balance: 7319.9,
          equity: 7306.649913200001,
          margin: 184.1,
          freeMargin: 7120.22,
          leverage: 100,
          marginLevel: 3967.58283542
        },
        updatedPositions: [{
          id: '46214692',
          type: 'POSITION_TYPE_BUY',
          symbol: 'GBPUSD',
          magic: 1000,
          time: new Date('2020-04-15T02:45:06.521Z'),
          updateTime: new Date('2020-04-15T02:45:06.521Z'),
          openPrice: 1.26101,
          currentPrice: 1.24883,
          currentTickValue: 1,
          volume: 0.07,
          swap: 0,
          profit: -85.25999999999966,
          commission: -0.25,
          clientId: 'TE_GBPUSD_7hyINWqAlE',
          stopLoss: 1.17721,
          unrealizedProfit: -85.25999999999901,
          realizedProfit: -6.536993168992922e-13
        }],
        removedPositionIds: ['1234'],
        updatedOrders: [{
          id: '46871284',
          type: 'ORDER_TYPE_BUY_LIMIT',
          state: 'ORDER_STATE_PLACED',
          symbol: 'AUDNZD',
          magic: 123456,
          platform: 'mt5',
          time: new Date('2020-04-20T08:38:58.270Z'),
          openPrice: 1.03,
          currentPrice: 1.05206,
          volume: 0.01,
          currentVolume: 0.01,
          comment: 'COMMENT2'
        }],
        completedOrderIds: ['2345'],
        historyOrders: [{
          clientId: 'TE_GBPUSD_7hyINWqAlE',
          currentPrice: 1.261,
          currentVolume: 0,
          doneTime: new Date('2020-04-15T02:45:06.521Z'),
          id: '46214692',
          magic: 1000,
          platform: 'mt5',
          positionId: '46214692',
          state: 'ORDER_STATE_FILLED',
          symbol: 'GBPUSD',
          time: new Date('2020-04-15T02:45:06.260Z'),
          type: 'ORDER_TYPE_BUY',
          volume: 0.07
        }],
        deals: [{
          clientId: 'TE_GBPUSD_7hyINWqAlE',
          commission: -0.25,
          entryType: 'DEAL_ENTRY_IN',
          id: '33230099',
          magic: 1000,
          platform: 'mt5',
          orderId: '46214692',
          positionId: '46214692',
          price: 1.26101,
          profit: 0,
          swap: 0,
          symbol: 'GBPUSD',
          time: new Date('2020-04-15T02:45:06.521Z'),
          type: 'DEAL_TYPE_BUY',
          volume: 0.07
        }]
      };
      let listener: any = {
        onSynchronizationStarted: () => {},
        onAccountInformationUpdated: () => {},
        onPositionsUpdated: () => {},
        onPositionUpdated: () => {},
        onPositionRemoved: () => {},
        onPendingOrdersUpdated: () => {},
        onPendingOrderUpdated: () => {},
        onPendingOrderCompleted: () => {},
        onHistoryOrderAdded: () => {},
        onDealAdded: () => {},
        onDealsSynchronized: () => {}
      };
      sandbox.stub(listener, 'onSynchronizationStarted').resolves();
      sandbox.stub(listener, 'onAccountInformationUpdated').resolves();
      sandbox.stub(listener, 'onPositionsUpdated').resolves();
      sandbox.stub(listener, 'onPositionUpdated').resolves();
      sandbox.stub(listener, 'onPositionRemoved').resolves();
      sandbox.stub(listener, 'onPendingOrdersUpdated').resolves();
      sandbox.stub(listener, 'onPendingOrderUpdated').resolves();
      sandbox.stub(listener, 'onPendingOrderCompleted').resolves();
      sandbox.stub(listener, 'onHistoryOrderAdded').resolves();
      sandbox.stub(listener, 'onDealAdded').resolves();
      sandbox.stub(listener, 'onDealsSynchronized').resolves();
      client.addSynchronizationListener('accountId', listener);
      server.emit('synchronization', {type: 'synchronizationStarted', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0, synchronizationId: 'ABC'});
      await new Promise(res => setTimeout(res, 50));
      server.emit('synchronization', Object.assign({type: 'update', accountId: 'accountId', instanceIndex: 0,
        host: 'ps-mpa-1'}, update));
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledOnce(listener.onAccountInformationUpdated);
      sinon.assert.calledOnce(listener.onPositionsUpdated);
      sinon.assert.calledOnce(listener.onPositionUpdated);
      sinon.assert.calledOnce(listener.onPositionRemoved);
      sinon.assert.calledOnce(listener.onPendingOrdersUpdated);
      sinon.assert.calledOnce(listener.onPendingOrderCompleted);
      sinon.assert.calledOnce(listener.onHistoryOrderAdded);
      sinon.assert.calledOnce(listener.onDealAdded);
      server.emit('synchronization', Object.assign({type: 'update', accountId: 'accountId', instanceIndex: 0,
        host: 'ps-mpa-1'}, update));
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledTwice(listener.onAccountInformationUpdated);
      sinon.assert.calledTwice(listener.onPositionsUpdated);
      sinon.assert.calledTwice(listener.onPositionUpdated);
      sinon.assert.calledTwice(listener.onPositionRemoved);
      sinon.assert.calledTwice(listener.onPendingOrdersUpdated);
      sinon.assert.calledTwice(listener.onPendingOrderCompleted);
      sinon.assert.calledTwice(listener.onHistoryOrderAdded);
      sinon.assert.calledTwice(listener.onDealAdded);
      server.emit('synchronization', {type: 'dealSynchronizationFinished', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0, synchronizationId: 'ABC'});
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledWith(listener.onAccountInformationUpdated, 'vint-hill:0:ps-mpa-1', update.accountInformation);
      sinon.assert.calledWith(listener.onPositionsUpdated, 'vint-hill:0:ps-mpa-1', update.updatedPositions);
      sinon.assert.calledWith(listener.onPositionUpdated, 'vint-hill:0:ps-mpa-1', update.updatedPositions[0]);
      sinon.assert.calledWith(listener.onPositionRemoved, 'vint-hill:0:ps-mpa-1', update.removedPositionIds[0]);
      sinon.assert.calledWith(listener.onPendingOrdersUpdated, 'vint-hill:0:ps-mpa-1', update.updatedOrders);
      sinon.assert.calledWith(listener.onPendingOrderUpdated, 'vint-hill:0:ps-mpa-1', update.updatedOrders[0]);
      sinon.assert.calledWith(listener.onPendingOrderCompleted, 'vint-hill:0:ps-mpa-1', update.completedOrderIds[0]);
      sinon.assert.calledWith(listener.onHistoryOrderAdded, 'vint-hill:0:ps-mpa-1', update.historyOrders[0]);
      sinon.assert.calledWith(listener.onDealAdded, 'vint-hill:0:ps-mpa-1', update.deals[0]);
      sinon.assert.callCount(listener.onAccountInformationUpdated, 4);
      sinon.assert.callCount(listener.onPositionsUpdated, 4);
      sinon.assert.callCount(listener.onPositionUpdated, 4);
      sinon.assert.callCount(listener.onPositionRemoved, 4);
      sinon.assert.callCount(listener.onPendingOrdersUpdated, 4);
      sinon.assert.callCount(listener.onPendingOrderCompleted, 4);
      sinon.assert.callCount(listener.onHistoryOrderAdded, 4);
      sinon.assert.callCount(listener.onDealAdded, 4);
      server.emit('synchronization', Object.assign({type: 'update', accountId: 'accountId', instanceIndex: 0,
        host: 'ps-mpa-1'}, update));
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.callCount(listener.onAccountInformationUpdated, 5);
      sinon.assert.callCount(listener.onPositionsUpdated, 5);
      sinon.assert.callCount(listener.onPositionUpdated, 5);
      sinon.assert.callCount(listener.onPositionRemoved, 5);
      sinon.assert.callCount(listener.onPendingOrdersUpdated, 5);
      sinon.assert.callCount(listener.onPendingOrderCompleted, 5);
      sinon.assert.callCount(listener.onHistoryOrderAdded, 5);
      sinon.assert.callCount(listener.onDealAdded, 5);
    });

    /**
     * @test {MetaApiWebsocketClient#getServerTime}
     */
    it('should retrieve server time from API', async () => {
      let serverTime = {
        time: new Date('2022-01-01T00:00:00.000Z'),
        brokerTime: '2022-01-01 02:00:00.000Z'
      };
      server.on('request', data => {
        if (data.type === 'getServerTime' && data.accountId === 'accountId' &&
          data.application === 'RPC') {
          server.emit('response', {
            type: 'response', accountId: data.accountId, requestId: data.requestId,
            serverTime
          });
        }
      });
      let actual = await client.getServerTime('accountId');
      actual.should.match(serverTime);
    });

    /**
     * @test {MetaApiWebsocketClient#calculateMargin}
     */
    it('should calculate margin', async () => {
      let margin = {
        margin: 110
      };
      let order = {
        symbol: 'EURUSD',
        type: 'ORDER_TYPE_BUY',
        volume: 0.1,
        openPrice: 1.1
      };
      server.on('request', data => {
        if (data.type === 'calculateMargin' && data.accountId === 'accountId' &&
          data.application === 'MetaApi' && JSON.stringify(data.order) === JSON.stringify(order)) {
          server.emit('response', {
            type: 'response', accountId: data.accountId, requestId: data.requestId,
            margin
          });
        }
      });
      let actual = await client.calculateMargin('accountId', 'MetaApi', 'high', order);
      actual.should.match(margin);
    });

  });

  describe('market data synchronization', () => {

    beforeEach(() => {
      sandbox.stub((client as any)._subscriptionManager, 'isSubscriptionActive').returns(true);
    });

    afterEach(() => {
      client.removeAllListeners();
    });

    /**
     * @test {MetaApiWebsocketClient#rpcRequest}
     */
    it('should retry request on failure', async () => {
      let requestCounter = 0;
      let order = {
        id: '46871284',
        type: 'ORDER_TYPE_BUY_LIMIT',
        state: 'ORDER_STATE_PLACED',
        symbol: 'AUDNZD',
        magic: 123456,
        platform: 'mt5',
        time: new Date('2020-04-20T08:38:58.270Z'),
        openPrice: 1.03,
        currentPrice: 1.05206,
        volume: 0.01,
        currentVolume: 0.01,
        comment: 'COMMENT2'
      };
      server.on('request', data => {
        if (requestCounter > 1 && data.type === 'getOrder' && data.accountId === 'accountId' &&
          data.orderId === '46871284' && data.application === 'RPC') {
          server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, order});
        } 
        requestCounter++;
      });
      let actual = await client.getOrder('accountId', '46871284');
      actual.should.match(order);
    }).timeout(20000);

    /**
     * @test {MetaApiWebsocketClient#rpcRequest}
     */
    it('should wait specified amount of time on too many requests error', async () => {
      let requestCounter = 0;
      let order = {
        id: '46871284',
        type: 'ORDER_TYPE_BUY_LIMIT',
        state: 'ORDER_STATE_PLACED',
        symbol: 'AUDNZD',
        magic: 123456,
        platform: 'mt5',
        time: new Date('2020-04-20T08:38:58.270Z'),
        openPrice: 1.03,
        currentPrice: 1.05206,
        volume: 0.01,
        currentVolume: 0.01,
        comment: 'COMMENT2'
      };
      server.on('request', data => {
        if (requestCounter > 0 && data.type === 'getOrder' && data.accountId === 'accountId' &&
          data.orderId === '46871284' && data.application === 'RPC') {
          server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, order});
        } else {
          server.emit('processingError', {
            id: 1, error: 'TooManyRequestsError', requestId: data.requestId,
            message: 'The API allows 10000 requests per 60 minutes to avoid overloading our servers.',
            status_code: 429, metadata: {
              periodInMinutes: 60, maxRequestsForPeriod: 10000, 
              recommendedRetryTime: new Date(Date.now() + 1000)
            }
          });
        }
        requestCounter++;
      });
      const startTime = Date.now();
      let actual = await client.getOrder('accountId', '46871284');
      actual.should.match(order);
      (Date.now() - startTime).should.be.approximately(1000, 100);
    }).timeout(10000);

    /**
     * @test {MetaApiWebsocketClient#rpcRequest}
     */
    it('should return too many requests exception if recommended time is beyond max request time', async () => {
      let requestCounter = 0;
      let order = {
        id: '46871284',
        type: 'ORDER_TYPE_BUY_LIMIT',
        state: 'ORDER_STATE_PLACED',
        symbol: 'AUDNZD',
        magic: 123456,
        platform: 'mt5',
        time: new Date('2020-04-20T08:38:58.270Z'),
        openPrice: 1.03,
        currentPrice: 1.05206,
        volume: 0.01,
        currentVolume: 0.01,
        comment: 'COMMENT2'
      };
      server.on('request', data => {
        if (requestCounter > 0 && data.type === 'getOrder' && data.accountId === 'accountId' &&
              data.orderId === '46871284' && data.application === 'RPC') {
          server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, order});
        } else {
          server.emit('processingError', {
            id: 1, error: 'TooManyRequestsError', requestId: data.requestId,
            message: 'The API allows 10000 requests per 60 minutes to avoid overloading our servers.',
            status_code: 429, metadata: {
              periodInMinutes: 60, maxRequestsForPeriod: 10000, 
              recommendedRetryTime: new Date(Date.now() + 60000)
            }
          });
        }
        requestCounter++;
      });

      try {
        await client.getOrder('accountId', '46871284');
        throw new Error('TooManyRequestsError expected');
      } catch (err) {
        err.name.should.equal('TooManyRequestsError');
      }
    }).timeout(10000);    

    /**
     * @test {MetaApiWebsocketClient#rpcRequest}
     */
    it('should not retry request on validation error', async () => {
      let requestCounter = 0;
      server.on('request', data => {
        if (requestCounter > 0 && data.type === 'subscribeToMarketData' && data.accountId === 'accountId' &&
          data.symbol === 'EURUSD' && data.application === 'application' && data.instanceIndex === 0) {
          server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId});
        } else {
          server.emit('processingError', {
            id: 1, error: 'ValidationError', message: 'Error message', requestId: data.requestId
          });
        }
        requestCounter ++;
      });
      try {
        await client.subscribeToMarketData('accountId', 'EURUSD', 'regular');
        throw new Error('ValidationError expected');
      } catch (err) {
        err.name.should.equal('ValidationError');
      }
      sinon.assert.match(requestCounter, 1);
    }).timeout(6000);
    
    /**
     * @test {MetaApiWebsocketClient#rpcRequest}
     */
    it('should not retry trade requests on fail', async () => {
      let requestCounter = 0;
      let trade = {
        actionType: 'ORDER_TYPE_SELL',
        symbol: 'AUDNZD',
        volume: 0.07
      };
      server.on('request', data => {
        if (data.type === 'trade' && data.accountId === 'accountId' && data.application === 'application') {
          if(requestCounter > 0) {
            sinon.assert.fail();
          }
          requestCounter++;
        }
      });
      try {
        await client.trade('accountId', trade);
        throw new Error('TimeoutError expected');
      } catch (err) {
        err.name.should.equal('TimeoutError');
      }
    }).timeout(6000);

    /**
     * @test {MetaApiWebsocketClient#rpcRequest}
     */
    it('should not retry request if connection closed between retries', async () => {
      let requestCounter = 0;
      let response = {type: 'response', accountId: 'accountId'};
      server.on('request', async data => {
        if (data.type === 'unsubscribe' && data.accountId === 'accountId') {
          server.emit('response', Object.assign({requestId: data.requestId}, response));
        }
  
        if (data.type === 'getOrders' && data.accountId === 'accountId' && data.application === 'RPC') {
          requestCounter++;
          server.emit('processingError', {
            id: 1, error: 'NotSynchronizedError', message: 'Error message',
            requestId: data.requestId
          });
        }
      });
      client.unsubscribe('accountId');
      try {
        await client.getOrders('accountId');
        throw new Error('NotSynchronizedError expected');
      } catch (err) {
        err.name.should.equal('NotSynchronizedError');
      }
      requestCounter.should.equal(1);
      client.socketInstancesByAccounts.should.not.have.property('accountId');
    });
  
    /**
     * @test {MetaApiWebsocketClient#rpcRequest}
     */
    it('should return timeout error if no server response received', async () => {
      let trade = {
        actionType: 'ORDER_TYPE_SELL',
        symbol: 'AUDNZD',
        volume: 0.07
      };
      server.on('request', data => {
      });
      try {
        await client.trade('accountId', trade);
        throw new Error('TimeoutError extected');
      } catch (err) {
        err.name.should.equal('TimeoutError');
      }
    }).timeout(20000);

    /**
     * @test {MetaApiWebsocketClient#subscribeToMarketData}
     */
    it('should subscribe to market data with MetaTrader terminal', async () => {
      let requestReceived = false;
      server.emit('synchronization', {type: 'authenticated', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0, replicas: 1});
      await new Promise(res => setTimeout(res, 50));
      server.on('request', data => {
        if (data.type === 'subscribeToMarketData' && data.accountId === 'accountId' && data.symbol === 'EURUSD' &&
          data.application === 'application' && data.instanceIndex === 0 &&
          JSON.stringify(data.subscriptions) === JSON.stringify([{type: 'quotes'}])) {
          requestReceived = true;
          server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId});
        }
      });
      await client.subscribeToMarketData('accountId', 'EURUSD', [{type: 'quotes'}], 'regular');
      requestReceived.should.be.true();
    });

    /**
     * @test {MetaApiWebsocketClient#subscribeToMarketData}
     */
    it('should subscribe to market data with MetaTrader terminal for high reliability account', async () => {
      let requestReceived = false;
      let requestReceived1 = false;
      server.on('request', data => {
        if (data.type === 'subscribeToMarketData' && data.accountId === 'accountId' && data.symbol === 'EURUSD' &&
              data.application === 'application' && data.instanceIndex === 0 &&
              JSON.stringify(data.subscriptions) === JSON.stringify([{type: 'quotes'}])) {
          requestReceived = true;
          server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId});
        }
      });
      server1.on('request', data => {
        if (data.type === 'subscribeToMarketData' && data.accountId === 'accountId' && data.symbol === 'EURUSD' &&
              data.application === 'application' && data.instanceIndex === 1 &&
              JSON.stringify(data.subscriptions) === JSON.stringify([{type: 'quotes'}])) {
          requestReceived1 = true;
          server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId});
        }
      });
      await client.subscribeToMarketData('accountId', 'EURUSD', [{type: 'quotes'}], 'high');
      await new Promise(res => setTimeout(res, 100));
      requestReceived.should.be.true();
      requestReceived1.should.be.true();
    });

    /**
     * @test {MetaApiWebsocketClient#refreshMarketDataSubscriptions}
     */
    it('should refresh market data subscriptions', async () => {
      let requestReceived = false;
      server.on('request', data => {
        if (data.type === 'refreshMarketDataSubscriptions' && data.accountId === 'accountId' && 
          data.application === 'application' && data.instanceIndex === 0 &&
          JSON.stringify(data.subscriptions) === JSON.stringify([{symbol: 'EURUSD'}])) {
          requestReceived = true;
          server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId});
        }
      });
      await client.refreshMarketDataSubscriptions('accountId', 0, [{symbol: 'EURUSD'}]);
      requestReceived.should.be.true();
    });

    /**
     * @test {MetaApiWebsocketClient#unsubscribeFromMarketData}
     */
    it('should unsubscribe from market data with MetaTrader terminal', async () => {
      let requestReceived = false;
      server.emit('synchronization', {type: 'authenticated', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0, replicas: 1});
      await new Promise(res => setTimeout(res, 50));
      server.on('request', data => {
        if (data.type === 'unsubscribeFromMarketData' && data.accountId === 'accountId' && data.symbol === 'EURUSD' &&
          data.application === 'application' && data.instanceIndex === 0 &&
          JSON.stringify(data.subscriptions) === JSON.stringify([{type: 'quotes'}])) {
          requestReceived = true;
          server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId});
        }
      });
      await client.unsubscribeFromMarketData('accountId', 'EURUSD', [{type: 'quotes'}], 'regular');
      requestReceived.should.be.true();
    });

    it('should finish synchronizing deals', async () => {
      (client as any)._socketInstances['vint-hill'][0][0].synchronizationThrottler = synchronizationThrottler;
      sandbox.stub(synchronizationThrottler, 'activeSynchronizationIds').get(() => ['ABC']);
      let listener: any = {
        onSynchronizationStarted: () => {},
        onDealsSynchronized: () => {}
      };
      const syncStub = sandbox.stub(listener, 'onDealsSynchronized').resolves();
      sandbox.stub(synchronizationThrottler, 'removeSynchronizationId');
      client.addSynchronizationListener('accountId', listener);
      server.emit('synchronization', {type: 'synchronizationStarted', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0, synchronizationId: 'ABC'});
      server.emit('synchronization', {type: 'dealSynchronizationFinished', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0, synchronizationId: 'ABC'});
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledWith(syncStub, 'vint-hill:0:ps-mpa-1', 'ABC');
      sinon.assert.calledWith((client as any)._latencyService.onDealsSynchronized, 'accountId:vint-hill:0:ps-mpa-1');
      sinon.assert.calledWith(synchronizationThrottler.removeSynchronizationId as any, 'ABC');
    });

    it('should finish synchronizing orders', async () => {
      (client as any)._socketInstances['vint-hill'][0][0].synchronizationThrottler = synchronizationThrottler;
      sandbox.stub(synchronizationThrottler, 'activeSynchronizationIds').get(() => ['ABC']);
      let listener: any = {
        onSynchronizationStarted: () => {},
        onHistoryOrdersSynchronized: () => {}
      };
      const syncStub = sandbox.stub(listener, 'onHistoryOrdersSynchronized').resolves();
      client.addSynchronizationListener('accountId', listener);
      server.emit('synchronization', {type: 'synchronizationStarted', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0, synchronizationId: 'ABC'});
      server.emit('synchronization', {type: 'orderSynchronizationFinished', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0, synchronizationId: 'ABC'});
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledWith(syncStub, 'vint-hill:0:ps-mpa-1', 'ABC');
    });

    it('should downgrade subscription', async () => {
      let listener: any = {
        onSynchronizationStarted: () => {},
        onSubscriptionDowngraded: () => {}
      };
      const syncStub = sandbox.stub(listener, 'onSubscriptionDowngraded').resolves();
      client.addSynchronizationListener('accountId', listener);
      server.emit('synchronization', {type: 'downgradeSubscription', accountId: 'accountId', host: 'ps-mpa-1',
        instanceIndex: 0, symbol: 'EURUSD', unsubscriptions: [{type: 'ticks'}, {type: 'books'}]});
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledWith(syncStub, 'vint-hill:0:ps-mpa-1', 'EURUSD', undefined,
        [{ type: 'ticks' }, { type: 'books' }]);
    });

    it('should synchronize symbol specifications', async () => {
      let specifications = [{
        symbol: 'EURUSD',
        tickSize: 0.00001,
        minVolume: 0.01,
        maxVolume: 200,
        volumeStep: 0.01
      }];
      let listener: any = {
        onSymbolSpecificationsUpdated: () => {
        },
        onSymbolSpecificationUpdated: () => {
        },
        onSymbolSpecificationRemoved: () => {
        }
      };
      sandbox.stub(listener, 'onSymbolSpecificationsUpdated').resolves();
      sandbox.stub(listener, 'onSymbolSpecificationUpdated').resolves();
      sandbox.stub(listener, 'onSymbolSpecificationRemoved').resolves();
      client.addSynchronizationListener('accountId', listener);
      server.emit('synchronization',
        {type: 'specifications', accountId: 'accountId', specifications, instanceIndex: 0, host: 'ps-mpa-1',
          removedSymbols: ['AUDNZD']});
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledWith(listener.onSymbolSpecificationsUpdated, 'vint-hill:0:ps-mpa-1',
        specifications, ['AUDNZD']);
      sinon.assert.calledWith(listener.onSymbolSpecificationUpdated, 'vint-hill:0:ps-mpa-1', specifications[0]);
      sinon.assert.calledWith(listener.onSymbolSpecificationRemoved, 'vint-hill:0:ps-mpa-1', 'AUDNZD');
    });

    it('should synchronize symbol prices', async () => {
      let prices = [{
        symbol: 'AUDNZD',
        bid: 1.05916,
        ask: 1.05927,
        profitTickValue: 0.602,
        lossTickValue: 0.60203
      }];
      let ticks = [{
        symbol: 'AUDNZD',
        time: new Date('2020-04-07T03:45:00.000Z'),
        brokerTime: '2020-04-07 06:45:00.000',
        bid: 1.05297,
        ask: 1.05309,
        last: 0.5298,
        volume: 0.13,
        side: 'buy'
      }];
      let candles = [{
        symbol: 'AUDNZD',
        timeframe: '15m',
        time: new Date('2020-04-07T03:45:00.000Z'),
        brokerTime: '2020-04-07 06:45:00.000',
        open: 1.03297,
        high: 1.06309,
        low: 1.02705,
        close: 1.043,
        tickVolume: 1435,
        spread: 17,
        volume: 345
      }];
      let books = [{
        symbol: 'AUDNZD',
        time: new Date('2020-04-07T03:45:00.000Z'),
        brokerTime: '2020-04-07 06:45:00.000',
        book: [
          {
            type: 'BOOK_TYPE_SELL',
            price: 1.05309,
            volume: 5.67
          },
          {
            type: 'BOOK_TYPE_BUY',
            price: 1.05297,
            volume: 3.45
          }
        ]
      }];
      let listener: any = {
        onSymbolPriceUpdated: () => {},
        onSymbolPricesUpdated: () => {},
        onCandlesUpdated: () => {},
        onTicksUpdated: () => {},
        onBooksUpdated: () => {}
      };
      sandbox.stub(listener, 'onSymbolPriceUpdated').resolves();
      sandbox.stub(listener, 'onSymbolPricesUpdated').resolves();
      sandbox.stub(listener, 'onCandlesUpdated').resolves();
      sandbox.stub(listener, 'onTicksUpdated').resolves();
      sandbox.stub(listener, 'onBooksUpdated').resolves();
      client.addSynchronizationListener('accountId', listener);
      server.emit('synchronization', {type: 'prices', accountId: 'accountId', host: 'ps-mpa-1', prices,
        ticks, candles, books, equity: 100, margin: 200, freeMargin: 400, marginLevel: 40000, instanceIndex: 0});
      await new Promise(res => setTimeout(res, 50));
      sinon.assert.calledWith(listener.onSymbolPricesUpdated, 'vint-hill:0:ps-mpa-1', prices, 100, 200, 400, 40000);
      sinon.assert.calledWith(listener.onCandlesUpdated, 'vint-hill:0:ps-mpa-1', candles, 100, 200, 400, 40000);
      sinon.assert.calledWith(listener.onTicksUpdated, 'vint-hill:0:ps-mpa-1', ticks, 100, 200, 400, 40000);
      sinon.assert.calledWith(listener.onBooksUpdated, 'vint-hill:0:ps-mpa-1', books, 100, 200, 400, 40000);
      sinon.assert.calledWith(listener.onSymbolPriceUpdated, 'vint-hill:0:ps-mpa-1', prices[0]);
    });

  });

  describe('wait for server-side terminal state synchronization', () => {

    /**
     * @test {MetaApiWebsocketClient#waitSynchronized}
     */
    it('should wait for server-side terminal state synchronization', async () => {
      let requestReceived = false;
      server.on('request', data => {
        if (data.type === 'waitSynchronized' && data.accountId === 'accountId' &&
          data.applicationPattern === 'app.*' && data.timeoutInSeconds === 10 &&
          data.application === 'application' && data.instanceIndex === 0) {
          requestReceived = true;
          server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId});
        }
      });
      await client.waitSynchronized('accountId', 0, 'app.*', 10);
      requestReceived.should.be.true();
      client.removeAllListeners();
    });

  });

  describe('latency monitoring', () => {

    beforeEach(() => {
      sandbox.stub((client as any)._subscriptionManager, 'isSubscriptionActive').returns(true);
    });

    /**
     * @test {LatencyListener#onResponse}
     */
    it('should invoke latency listener on response', async () => {
      let accountId;
      let requestType;
      let actualTimestamps;
      let listener: any = {
        onResponse: (aid, type, ts) => {
          accountId = aid;
          requestType = type;
          actualTimestamps = ts;
        }
      };
      client.addLatencyListener(listener);
      let price = {};
      let timestamps;
      server.on('request', data => {
        if (data.type === 'getSymbolPrice' && data.accountId === 'accountId' && data.symbol === 'AUDNZD' &&
          data.application === 'RPC' && data.timestamps.clientProcessingStarted) {
          timestamps = Object.assign(data.timestamps, {serverProcessingStarted: new Date(),
            serverProcessingFinished: new Date()});
          timestamps.clientProcessingStarted = new Date(timestamps.clientProcessingStarted);
          server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, price,
            timestamps});
        }
      });
      await client.getSymbolPrice('accountId', 'AUDNZD');
      await new Promise(res => setTimeout(res, 100));
      accountId.should.equal('accountId');
      requestType.should.equal('getSymbolPrice');
      actualTimestamps.should.match(timestamps);
      should.exist(actualTimestamps.clientProcessingStarted);
      should.exist(actualTimestamps.clientProcessingFinished);
      should.exist(actualTimestamps.serverProcessingStarted);
      should.exist(actualTimestamps.serverProcessingFinished);
    });

    /**
     * @test {LatencyListener#onSymbolPrice}
     */
    it('should measure price streaming latencies', async () => {
      let prices = [{
        symbol: 'AUDNZD',
        timestamps: {
          eventGenerated: new Date(),
          serverProcessingStarted: new Date(),
          serverProcessingFinished: new Date()
        }
      }];
      let accountId;
      let symbol;
      let actualTimestamps;
      let listener: any = {
        onSymbolPrice: (aid, sym, ts) => {
          accountId = aid;
          symbol = sym;
          actualTimestamps = ts;
        }
      };
      client.addLatencyListener(listener);
      server.emit('synchronization', {type: 'prices', accountId: 'accountId', prices, equity: 100, margin: 200,
        freeMargin: 400, marginLevel: 40000});
      await new Promise(res => setTimeout(res, 50));
      accountId.should.equal('accountId');
      symbol.should.equal('AUDNZD');
      actualTimestamps.should.match(prices[0].timestamps);
      should.exist(actualTimestamps.clientProcessingFinished);
    });

    /**
     * @test {LatencyListener#onUpdate}
     */
    it('should measure update latencies', async () => {
      let update = {
        timestamps: {
          eventGenerated: new Date(),
          serverProcessingStarted: new Date(),
          serverProcessingFinished: new Date()
        }
      };
      let accountId;
      let actualTimestamps;
      let listener: any = {
        onUpdate: (aid, ts) => {
          accountId = aid;
          actualTimestamps = ts;
        }
      };
      client.addLatencyListener(listener);
      server.emit('synchronization', Object.assign({type: 'update', accountId: 'accountId'}, update));
      await new Promise(res => setTimeout(res, 50));
      accountId.should.equal('accountId');
      actualTimestamps.should.match(update.timestamps);
      should.exist(actualTimestamps.clientProcessingFinished);
    });

    /**
     * @test {LatencyListener#onTrade}
     */
    it('should process trade latency', async () => {
      let trade = {};
      let response = {
        numericCode: 10009,
        stringCode: 'TRADE_RETCODE_DONE',
        message: 'Request completed',
        orderId: '46870472'
      };
      let timestamps = {
        clientExecutionStarted: new Date(),
        serverExecutionStarted: new Date(),
        serverExecutionFinished: new Date(),
        tradeExecuted: new Date()
      };
      let accountId;
      let actualTimestamps;
      let listener: any = {
        onTrade: (aid, ts) => {
          accountId = aid;
          actualTimestamps = ts;
        }
      };
      client.addLatencyListener(listener);
      server.on('request', data => {
        data.trade.should.match(trade);
        if (data.type === 'trade' && data.accountId === 'accountId' && data.application === 'application') {
          server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, response,
            timestamps});
        }
      });
      await client.trade('accountId', trade);
      accountId.should.equal('accountId');
      actualTimestamps.should.match(timestamps);
      should.exist(actualTimestamps.clientProcessingFinished);
    });

  });

  it('should reconnect to server on disconnect', async () => {
    const trade = {
      actionType: 'ORDER_TYPE_SELL',
      symbol: 'AUDNZD',
      volume: 0.07
    };
    const response = {
      numericCode: 10009,
      stringCode: 'TRADE_RETCODE_DONE',
      message: 'Request completed',
      orderId: '46870472'
    };
    let listener: any = {
      onReconnected: () => {},
    };
    sandbox.stub(listener, 'onReconnected').resolves();
    sandbox.stub((client as any)._packetOrderer, 'onReconnected').resolves();
    sandbox.stub((client as any)._subscriptionManager, 'onReconnected').resolves();
    client.addReconnectListener(listener, 'accountId');
    let requestCounter = 0;
    server.on('request', async data => {
      if (data.type === 'trade' && data.accountId === 'accountId' && data.application === 'application') {
        requestCounter++;
        await server.emit('response', {type: 'response', accountId: data.accountId, 
          requestId: data.requestId, response});
      }
      await server.disconnect();
    });
  
    client.trade('accountId', trade);
    await new Promise(res => setTimeout(res, 50));
    await clock.tickAsync(1500);
    await new Promise(res => setTimeout(res, 50));
    sinon.assert.calledOnce(listener.onReconnected);
    sinon.assert.calledWith((client as any)._subscriptionManager.onReconnected, 0, 0, ['accountId']);
    sinon.assert.calledWith((client as any)._packetOrderer.onReconnected, ['accountId']);

    server.on('request', async data => {
      if (data.type === 'trade' && data.accountId === 'accountId' && data.application === 'application') {
        requestCounter++;
        await server.emit('response', {type: 'response', accountId: data.accountId, 
          requestId: data.requestId, response});
      }
      await server.disconnect();
    });
  
    client.trade('accountId', trade);
    await new Promise(res => setTimeout(res, 50));
    await clock.tickAsync(1500);
    await new Promise(res => setTimeout(res, 50));
    sinon.assert.match(requestCounter, 2);
  });

  /**
   * @test {MetaApiWebsocketClient#rpcRequest}
   */
  it('should cancel synchronization on disconnect', async () => {
    sandbox.stub((client as any)._subscriptionManager, 'isSubscriptionActive').returns(true);
    activeSynchronizationIdsStub.get(() => [
      'synchronizationId', 'ABC2', 'ABC3', 'ABC4'
    ]);
    (client as any)._socketInstancesByAccounts[0].accountId2 = 1;
    (client as any)._socketInstancesByAccounts[0].accountId3 = 0;
    (client as any)._socketInstancesByAccounts[1].accountId4 = 0;
    client.addAccountCache('accountId2', {'vint-hill': 'accountId2'});
    client.addAccountCache('accountId3', {'new-york': 'accountId3'});
    client.addAccountCache('accountId4', {'vint-hill': 'accountId4'});
    server.emit('synchronization', {type: 'synchronizationStarted', accountId: 'accountId',
      sequenceTimestamp: 1603124267178, synchronizationId: 'synchronizationId'});
    server.emit('synchronization', {type: 'synchronizationStarted', accountId: 'accountId2',
      sequenceTimestamp: 1603124267178, synchronizationId: 'ABC2'});
    server.emit('synchronization', {type: 'synchronizationStarted', accountId: 'accountId3',
      sequenceTimestamp: 1603124267178, synchronizationId: 'ABC3'});
    server.emit('synchronization', {type: 'synchronizationStarted', accountId: 'accountId4',
      sequenceTimestamp: 1603124267178, synchronizationId: 'ABC4'});
    await new Promise(res => setTimeout(res, 50));
    should.exist((client as any)._synchronizationFlags.synchronizationId);
    should.exist((client as any)._synchronizationFlags.ABC2);
    should.exist((client as any)._synchronizationFlags.ABC3);
    should.exist((client as any)._synchronizationFlags.ABC4);
    await server.disconnect();
    await new Promise(res => setTimeout(res, 1200));
    should.not.exist((client as any)._synchronizationFlags.synchronizationId);
    should.exist((client as any)._synchronizationFlags.ABC2);
    should.exist((client as any)._synchronizationFlags.ABC3);
    should.exist((client as any)._synchronizationFlags.ABC4);
  }).timeout(5000);

  /**
   * @test {MetaApiWebsocketClient#rpcRequest}
   */
  it('should remove reconnect listener', async () => {
    let trade = {
      actionType: 'ORDER_TYPE_SELL',
      symbol: 'AUDNZD',
      volume: 0.07
    };
    let response = {
      numericCode: 10009,
      stringCode: 'TRADE_RETCODE_DONE',
      message: 'Request completed',
      orderId: '46870472'
    };
    const listener: any = {onReconnected: async () => {}};
    sandbox.stub(listener, 'onReconnected').resolves();
    client.addReconnectListener(listener, 'accountId');
    sandbox.stub((client as any)._subscriptionManager, 'onReconnected');
    let requestCounter = 0;
    server.on('request', data => {
      data.trade.should.match(trade);
      requestCounter++;
      if (data.type === 'trade' && data.accountId === 'accountId' && data.application === 'application') {
        server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, response});
      }
      server.disconnect();
    });

    await client.trade('accountId', trade);
    await new Promise(res => setTimeout(res, 50));
    await clock.tickAsync(1100);
    await new Promise(res => setTimeout(res, 50));
    sinon.assert.calledOnce(listener.onReconnected);
    client.removeReconnectListener(listener);

    server.on('request', data => {
      data.trade.should.match(trade);
      requestCounter++;
      if (data.type === 'trade' && data.accountId === 'accountId' && data.application === 'application') {
        server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, response});
      }
      server.disconnect();
    });

    await client.trade('accountId', trade);
    await new Promise(res => setTimeout(res, 50));
    await clock.tickAsync(1100);
    await new Promise(res => setTimeout(res, 50));
    sinon.assert.calledOnce(listener.onReconnected);
    sinon.assert.match(requestCounter, 2);
  });

  /**
   * @test {MetaApiWebsocketClient#queuePacket}
   */
  it('should process packets in order', async () => {
    let ordersCallTime = 0;
    let positionsCallTime = 0;
    let disconnectedCallTime = 0;
    let pricesCallTime = 0;
    let listener: any = {
      onConnected: () => {},
      onDisconnected: async () => {
        await new Promise(res => setTimeout(res, 5000));
        disconnectedCallTime = Date.now();
      },
      onStreamClosed: () => {},
      onPendingOrdersReplaced: async () => {
        await new Promise(res => setTimeout(res, 10000));
        ordersCallTime = Date.now();
      },
      onPendingOrdersSynchronized: () => {},
      onPositionsReplaced: async () => {
        await new Promise(res => setTimeout(res, 1000));
        positionsCallTime = Date.now();
      },
      onPositionsSynchronized: () => {},
      onSymbolPriceUpdated: () => {},
      onSymbolPricesUpdated: async () => {
        await new Promise(res => setTimeout(res, 1000));
        pricesCallTime = Date.now();
      },
    };
    let resolve;
    let promise = new Promise(res => resolve = res);
    client.close();
    io.close(() => resolve());
    await promise;
    io = Server(6785, {path: '/ws', pingTimeout: 1000000});
    client = new MetaApiWebsocketClient(metaApi, domainClient, 'token', {
      application: 'application', 
      domain: 'project-stock.agiliumlabs.cloud', requestTimeout: 1.5, useSharedClientApi: false,
      retryOpts: {retries: 3, minDelayInSeconds: 0.1, maxDelayInSeconds: 0.5},
      ...commonOptions
    });
    sandbox.stub((client as any)._latencyService, 'onConnected');
    sandbox.stub((client as any)._latencyService, 'onDisconnected');
    sandbox.stub((client as any)._latencyService, 'onUnsubscribe');
    client.url = 'http://localhost:6785';
    client.addAccountCache('accountId', {'vint-hill': 'accountId'});
    sandbox.stub((client as any)._subscriptionManager, 'isSubscriptionActive').returns(true);
    io.on('connect', socket => {
      server = socket;
      if (socket.request._query['auth-token'] !== 'token') {
        socket.emit({error: 'UnauthorizedError', message: 'Authorization token invalid'});
        socket.close();
      }
      server.on('request', data => {
        if (data.type === 'getPositions' && data.accountId === 'accountId' && data.application === 'RPC') {
          server.emit('response', {type: 'response', accountId: data.accountId, 
            requestId: data.requestId, positions: []});
        } else if (data.type === 'subscribe') {
          server.emit('response', {type: 'response', accountId: data.accountId, 
            requestId: data.requestId});
        }
      });
    });
    sandbox.stub((client as any)._latencyService, 'waitConnectedInstance').resolves('accountId:vint-hill:0:ps-mpa-1');
    await client.subscribe('accountId', 1);
    await client.getPositions('accountId');
    client.addSynchronizationListener('accountId', listener);
    sandbox.stub((client as any)._packetOrderer, 'restoreOrder').callsFake((arg) => {
      return [arg];
    });
    server.emit('synchronization', {type: 'authenticated', accountId: 'accountId', host: 'ps-mpa-1',
      instanceIndex: 0, replicas: 2, sequenceNumber: 1});
    await new Promise(res => setTimeout(res, 50));
    await clock.tickAsync(59000);
    server.emit('synchronization', {type: 'orders', accountId: 'accountId', orders: [], instanceIndex: 0,
      host: 'ps-mpa-1', sequenceNumber: 2});
    server.emit('synchronization', {type: 'prices', accountId: 'accountId', prices: [{symbol: 'EURUSD'}], 
      instanceIndex: 0, host: 'ps-mpa-1', equity: 100, margin: 200, freeMargin: 400, marginLevel: 40000});
    await new Promise(res => setTimeout(res, 50));
    await clock.tickAsync(3000);
    server.emit('synchronization', {type: 'positions', accountId: 'accountId', positions: [], instanceIndex: 0,
      host: 'ps-mpa-1', sequenceNumber: 3});
    await new Promise(res => setTimeout(res, 50));
    await clock.tickAsync(20000);
    await new Promise(res => setTimeout(res, 50));
    pricesCallTime.should.not.eql(0);
    (ordersCallTime).should.be.above(pricesCallTime);
    (disconnectedCallTime).should.be.above(ordersCallTime);
    (positionsCallTime).should.be.above(disconnectedCallTime);
  });

  /**
   * @test {MetaApiWebsocketClient#queuePacket}
   */
  it('should not process old synchronization packet without gaps in sequence numbers', async () => {
    let listener: any = {
      onSynchronizationStarted: sinon.fake(),
      onPendingOrdersReplaced: sinon.fake(),
      onPendingOrdersSynchronized: () => {}
    };
    client.addSynchronizationListener('accountId', listener);
    sandbox.stub((client as any)._subscriptionManager, 'isSubscriptionActive').returns(true);
    sandbox.stub((client as any)._packetOrderer, 'restoreOrder').callsFake(arg => [arg]);

    sandbox.stub((client as any)._socketInstances['vint-hill'][0][0].synchronizationThrottler,
      'activeSynchronizationIds').get(() => ['ABC']);
    server.emit('synchronization', {type: 'synchronizationStarted', accountId: 'accountId',
      sequenceNumber: 1, sequenceTimestamp: 1603124267178, synchronizationId: 'ABC'});
    server.emit('synchronization', {type: 'orders', accountId: 'accountId', orders: [],
      sequenceNumber: 2, sequenceTimestamp: 1603124267181, synchronizationId: 'ABC'});
    await new Promise(res => setTimeout(res, 50));
    sinon.assert.calledOnce(listener.onSynchronizationStarted);
    sinon.assert.calledOnce(listener.onPendingOrdersReplaced);

    sandbox.stub((client as any)._socketInstances['vint-hill'][0][0].synchronizationThrottler,
      'activeSynchronizationIds').get(() => ['DEF']);
    server.emit('synchronization', {type: 'synchronizationStarted', accountId: 'accountId',
      sequenceNumber: 3, sequenceTimestamp: 1603124267190, synchronizationId: 'DEF'});
    server.emit('synchronization', {type: 'orders', accountId: 'accountId', orders: [],
      sequenceNumber: 4, sequenceTimestamp: 1603124267192, synchronizationId: 'ABC'});
    server.emit('synchronization', {type: 'orders', accountId: 'accountId', orders: [],
      sequenceNumber: 5, sequenceTimestamp: 1603124267195, synchronizationId: 'DEF'});
    await new Promise(res => setTimeout(res, 50));
    sinon.assert.calledTwice(listener.onSynchronizationStarted);
    sinon.assert.calledTwice(listener.onPendingOrdersReplaced);
  });

  /**
   * @test {MetaApiWebsocketClient#queueEvent}
   */
  it('should process queued events sequentially', async () => {
    let event1 = sandbox.stub().callsFake(() => new Promise(res => setTimeout(res, 100)));
    let event2 = sandbox.stub().callsFake(() => new Promise(res => setTimeout(res, 25)));
    client.queueEvent('accountId', 'test', event1);
    client.queueEvent('accountId', 'test', event2);
    
    await clock.tickAsync(75);
    sinon.assert.calledOnce(event1);
    sinon.assert.notCalled(event2);
    
    await clock.tickAsync(30);
    sinon.assert.calledOnce(event2);
  });

  /**
   * @test {MetaApiWebsocketClient#queueEvent}
   * @test {MetaApiWebsocketClient#queuePacket}
   */
  it('should process queued events among synchronization packets', async () => {
    let listener: any = {
      onSynchronizationStarted: sandbox.stub().callsFake(() => new Promise(res => setTimeout(res, 100)))
    };
    let event = sandbox.stub().callsFake(() => new Promise(res => setTimeout(res, 25)));
    client.addSynchronizationListener('accountId', listener);

    client.queuePacket({} as any, {
      type: 'synchronizationStarted', accountId: 'accountId', instanceIndex: 0, sequenceNumber: 1, sequenceTimestamp: 1,
      synchronizationId: 'synchronizationId', host: 'ps-mpa-1'
    });
    client.queueEvent('accountId', 'test', event);
    
    await clock.tickAsync(75);
    sinon.assert.calledOnce(listener.onSynchronizationStarted);
    sinon.assert.notCalled(event);

    await clock.tickAsync(30);
    sinon.assert.calledOnce(event);
  });

  /**
   * @test {MetaApiWebsocketClient#queueEvent}
   */
  it('should not throw errors from queued events', async () => {
    let event = sandbox.stub().rejects();
    client.queueEvent('accountId', 'test', event);
    await new Promise(res => setTimeout(res, 10));
    sinon.assert.calledOnce(event);
  });

  /**
   * @test {MetaApiWebsocketClient#rpcRequest}
   */
  describe('rpcRequest', () => {

    /**
     * @test {MetaApiWebsocketClient#rpcRequest}
     */
    it('should convert ForbiddenError to corresponding error class', async () => {
      server.on('request', data => {
        if (data.type === 'getAccountInformation' && data.accountId === 'accountId' && data.application === 'RPC') {
          server.emit('processingError', {error: 'ForbiddenError', message: 'test', requestId: data.requestId});
        }
      });
      try {
        await client.getAccountInformation('accountId');
        throw new AssertionError({message: 'Should not be thrown'});
      } catch (err) {
        err.should.be.instanceof(ForbiddenError);
        err.should.match({
          name: 'ForbiddenError',
          message: 'test'
        });
      }
    });

  });

});
