text: /*!
   * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
   */
  
  
  import sinon from 'sinon';
  import StateMachine from 'javascript-state-machine';
  import uuid from 'uuid';
  import {assert} from '@webex/test-helper-chai';
  import {Credentials} from '@webex/webex-core';
  import Support from '@webex/internal-plugin-support';
  import MockWebex from '@webex/test-helper-mock-webex';
  import Meetings, {CONSTANTS} from '@webex/plugin-meetings';
  import Meeting from '@webex/plugin-meetings/src/meeting';
  import Members from '@webex/plugin-meetings/src/members';
  import Roap from '@webex/plugin-meetings/src/roap';
  import MeetingRequest from '@webex/plugin-meetings/src/meeting/request';
  import LocusInfo from '@webex/plugin-meetings/src/locus-info';
  import MediaProperties from '@webex/plugin-meetings/src/media/properties';
  import WebRTCStats from '@webex/plugin-meetings/src/stats';
  import MeetingUtil from '@webex/plugin-meetings/src/meeting/util';
  import Media from '@webex/plugin-meetings/src/media/index';
  import PeerConnectionManager from '@webex/plugin-meetings/src/peer-connection-manager';
  import ReconnectionManager from '@webex/plugin-meetings/src/reconnection-manager';
  import MediaUtil from '@webex/plugin-meetings/src/media/util';
  import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy';
  import LoggerConfig from '@webex/plugin-meetings/src/common/logs/logger-config';
  import TriggerProxy from '@webex/plugin-meetings/src/common/events/trigger-proxy';
  import Metrics from '@webex/plugin-meetings/src/metrics';
  import bowser from 'bowser';
  
  import {UserNotJoinedError, MeetingNotActiveError, UserInLobbyError, NoMediaEstablishedYetError} from '../../../../src/common/errors/webex-errors';
  import ParameterError from '../../../../src/common/errors/parameter';
  import DefaultSDKConfig from '../../../../src/config';
  
  // Non-stubbed function
  const {getDisplayMedia} = Media;
  
  describe('plugin-meetings', () => {
    const logger = {
      info: () => {},
      log: () => {},
      error: () => {},
      warn: () => {},
      trace: () => {},
      debug: () => {}
    };
  
    before(() => {
      LoggerConfig.set({verboseEvents: true, enable: false});
      LoggerProxy.set(logger);
    });
  
    let webex;
    let meeting;
    let uuid1;
    let uuid2;
    let uuid3;
    let uuid4;
    let url1;
    let url2;
    let test1;
    let test2;
    let test3;
    let test4;
  
    beforeEach(() => {
      webex = new MockWebex({
        children: {
          meetings: Meetings,
          credentials: Credentials,
          support: Support
        },
        config: {
          credentials: {
            client_id: 'mock-client-id'
          },
          meetings: {
            reconnection: {
              enabled: false
            },
            mediaSettings: {},
            metrics: {},
            stats: {}
          }
        }
      });
  
      webex.internal.support.submitLogs = sinon.stub().returns(Promise.resolve());
      webex.credentials.getOrgId = sinon.stub().returns('fake-org-id');
      webex.internal.metrics.submitClientMetrics = sinon.stub().returns(Promise.resolve());
      webex.meetings.uploadLogs = sinon.stub().returns(Promise.resolve());
  
  
      TriggerProxy.trigger = sinon.stub().returns(true);
      Metrics.initPayload = sinon.stub();
      Metrics.postEvent = sinon.stub();
      Metrics.initialSetup(null, webex);
      MediaUtil.createPeerConnection = sinon.stub().returns({});
  
      uuid1 = uuid.v4();
      uuid2 = uuid.v4();
      uuid3 = uuid.v4();
      uuid4 = uuid.v4();
      url1 = `https://example.com/${uuid.v4()}`;
      url2 = `https://example2.com/${uuid.v4()}`;
      test1 = `test-${uuid.v4()}`;
      test2 = `test2-${uuid.v4()}`;
      test3 = `test3-${uuid.v4()}`;
      test4 = `test4-${uuid.v4()}`;
  
      meeting = new Meeting(
        {
          userId: uuid1,
          resource: uuid2,
          deviceUrl: uuid3,
          locus: {url: url1}
        },
        {
          parent: webex
        }
      );
  
      meeting.members.selfId = uuid1;
      meeting.startMediaQualityMetrics = sinon.stub();
    });
  
    describe('meeting index', () => {
      describe('Public Api Contract', () => {
        describe('#constructor', () => {
          it('should have created a meeting object with public properties', () => {
            assert.exists(meeting);
            assert.exists(meeting.webex);
            assert.exists(meeting.options);
            assert.exists(meeting.attrs);
            assert.exists(meeting.id);
            assert.equal(meeting.userId, uuid1);
            assert.equal(meeting.resource, uuid2);
            assert.equal(meeting.deviceUrl, uuid3);
            assert.equal(meeting.roapSeq, -1);
            assert.deepEqual(meeting.meetingInfo, {});
            assert.instanceOf(meeting.members, Members);
            assert.instanceOf(meeting.roap, Roap);
            assert.instanceOf(meeting.reconnectionManager, ReconnectionManager);
            assert.isNull(meeting.audio);
            assert.isNull(meeting.video);
            assert.instanceOf(meeting.meetingFiniteStateMachine, StateMachine);
            assert.isNull(meeting.stats);
            assert.isNull(meeting.conversationUrl);
            assert.equal(meeting.locusUrl, url1);
            assert.isNull(meeting.sipUri);
            assert.isNull(meeting.partner);
            assert.isNull(meeting.type);
            assert.isNull(meeting.owner);
            assert.isNull(meeting.hostId);
            assert.isNull(meeting.policy);
            assert.instanceOf(meeting.meetingRequest, MeetingRequest);
            assert.instanceOf(meeting.locusInfo, LocusInfo);
            assert.instanceOf(meeting.mediaProperties, MediaProperties);
          });
        });
        describe('#invite', () => {
          it('should have #invite', () => {
            assert.exists(meeting.invite);
          });
          beforeEach(() => {
            meeting.members.addMember = sinon.stub().returns(Promise.resolve(test1));
          });
          it('should proxy members #addMember and return a promise', async () => {
            const invite = meeting.invite(uuid1, false);
  
            assert.exists(invite.then);
            await invite;
            assert.calledOnce(meeting.members.addMember);
            assert.calledWith(meeting.members.addMember, uuid1, false);
          });
        });
        describe('#admit', () => {
          it('should have #admit', () => {
            assert.exists(meeting.admit);
          });
          beforeEach(() => {
            meeting.members.admitMembers = sinon.stub().returns(Promise.resolve(test1));
          });
          it('should proxy members #admitMembers and return a promise', async () => {
            const admit = meeting.members.admitMembers([uuid1]);
  
            assert.exists(admit.then);
            await admit;
            assert.calledOnce(meeting.members.admitMembers);
            assert.calledWith(meeting.members.admitMembers, [uuid1]);
          });
        });
        describe('#getMembers', () => {
          it('should have #getMembers', () => {
            assert.exists(meeting.getMembers);
          });
          it('should get the members object as an instance and return Members', async () => {
            const members = meeting.getMembers();
  
            assert.instanceOf(members, Members);
          });
        });
        describe('#getStats', () => {
          it('should have #getStats', () => {
            assert.exists(meeting.getStats);
          });
          it('should create stats if not exists and return WebRTCStats', () => {
            assert.notOk(meeting.stats);
            meeting.createStats = sinon.stub().returns(new WebRTCStats({}, {parent: webex}));
            const stats = meeting.getStats();
  
            assert.calledOnce(meeting.createStats);
            assert.instanceOf(stats, WebRTCStats);
          });
        });
        describe('#isAudioMuted', () => {
          it('should have #isAudioMuted', () => {
            assert.exists(meeting.invite);
          });
          it('should get the audio muted status and return as a boolean', () => {
            const muted = meeting.isAudioMuted();
  
            assert.isNotOk(muted);
          });
        });
        describe('#isAudioSelf', () => {
          it('should have #isAudioSelf', () => {
            assert.exists(meeting.invite);
          });
          it('should get the audio self status and return as a boolean', () => {
            const self = meeting.isAudioSelf();
  
            assert.isNotOk(self);
          });
        });
        describe('#isVideoMuted', () => {
          it('should have #isVideoMuted', () => {
            assert.exists(meeting.isVideoMuted);
          });
          it('should get the video muted status and return as a boolean', () => {
            const muted = meeting.isVideoMuted();
  
            assert.isNotOk(muted);
          });
        });
        describe('#isVideoSelf', () => {
          it('should have #isVideoSelf', () => {
            assert.exists(meeting.invite);
          });
          it('should get the video self status and return as a boolean', () => {
            const self = meeting.isVideoSelf();
  
            assert.isNotOk(self);
          });
        });
        describe('#muteAudio', () => {
          it('should have #muteAudio', () => {
            assert.exists(meeting.muteAudio);
          });
          describe('before audio is defined', () => {
            it('should reject and return a promise', async () => {
              await meeting.muteAudio().catch((err) => {
                assert.instanceOf(err, UserNotJoinedError);
              });
            });
  
            it('should reject and return a promise', async () => {
              meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
              await meeting.muteAudio().catch((err) => {
                assert.instanceOf(err, NoMediaEstablishedYetError);
              });
            });
  
            it('should reject and return a promise', async () => {
              meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
              meeting.mediaId = 'mediaId';
              await meeting.muteAudio().catch((err) => {
                assert.instanceOf(err, ParameterError);
              });
            });
          });
          describe('after audio is defined', () => {
            let toggle;
  
            beforeEach(() => {
              toggle = sinon.stub().returns(Promise.resolve(true));
              meeting.audio = {toggle};
            });
  
            it('should return a promise resolution', async () => {
              meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
              meeting.mediaId = 'mediaId';
  
              const audio = meeting.muteAudio();
  
              assert.exists(audio.then);
              await audio;
              assert.calledOnce(toggle);
              assert.calledWith(toggle, {mute: true, self: true});
            });
          });
        });
        describe('#unmuteAudio', () => {
          it('should have #unmuteAudio', () => {
            assert.exists(meeting.unmuteAudio);
          });
          describe('before audio is defined', () => {
            it('should reject when user not joined', async () => {
              await meeting.unmuteAudio().catch((err) => {
                assert.instanceOf(err, UserNotJoinedError);
              });
            });
  
            it('should reject when no media is established yet ', async () => {
              meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
              await meeting.unmuteAudio().catch((err) => {
                assert.instanceOf(err, NoMediaEstablishedYetError);
              });
            });
  
            it('should reject when audio is not there or established', async () => {
              meeting.mediaId = 'mediaId';
              meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
              await meeting.unmuteAudio().catch((err) => {
                assert.instanceOf(err, ParameterError);
              });
            });
          });
          describe('after audio is defined', () => {
            let toggle, participantMute;
  
            beforeEach(() => {
              toggle = sinon.stub().returns(Promise.resolve(true));
              participantMute = sinon.stub().returns(Promise.resolve(true));
              meeting.mediaId = 'mediaId';
              meeting.audio = {toggle};
              meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
              meeting.mute = participantMute;
            });
  
            it('should return a promise resolution', async () => {
              meeting.audio = {toggle};
  
  
              const audio = meeting.unmuteAudio();
  
              assert.exists(audio.then);
              await audio;
              assert.calledOnce(toggle);
              assert.calledWith(toggle, {mute: false, self: true});
            });
  
            it('should call the participant unmute', () => {
              meeting.mediaId = 'mediaId';
              meeting.unmuteAudio()
                .then(() => {
                  assert.calledOnce(participantMute);
                  assert.calledWith(participantMute, uuid1, false);
                });
            });
          });
        });
        describe('#muteVideo', () => {
          it('should have #muteVideo', () => {
            assert.exists(meeting.muteVideo);
          });
          describe('before video is defined', () => {
            it('should reject when user not joined', async () => {
              await meeting.muteVideo().catch((err) => {
                assert.instanceOf(err, UserNotJoinedError);
              });
            });
  
            it('should reject when no media is established', async () => {
              meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
              await meeting.muteVideo().catch((err) => {
                assert.instanceOf(err, NoMediaEstablishedYetError);
              });
            });
  
            it('should reject when no video added or established', async () => {
              meeting.mediaId = 'mediaId';
              meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
              await meeting.muteVideo().catch((err) => {
                assert.instanceOf(err, ParameterError);
              });
            });
          });
          describe('after video is defined', () => {
            it('should return a promise resolution', async () => {
              const toggle = sinon.stub().returns(Promise.resolve(true));
  
              meeting.mediaId = 'mediaId';
              meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
              meeting.video = {toggle};
              const video = meeting.muteVideo();
  
              assert.exists(video.then);
              await video;
              assert.calledOnce(toggle);
              assert.calledWith(toggle, {mute: true, self: true});
            });
          });
        });
        describe('#unmuteVideo', () => {
          it('should have #unmuteVideo', () => {
            assert.exists(meeting.unmuteVideo);
          });
          describe('before video is defined', () => {
            it('should reject no user joined', async () => {
              await meeting.unmuteVideo().catch((err) => {
                assert.instanceOf(err, Error);
              });
            });
  
            it('should reject no media established', async () => {
              meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
              await meeting.unmuteVideo().catch((err) => {
                assert.instanceOf(err, Error);
              });
            });
  
            it('should reject when no video added or established', async () => {
              meeting.mediaId = 'mediaId';
              meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
              await meeting.unmuteVideo().catch((err) => {
                assert.instanceOf(err, Error);
              });
            });
          });
          describe('after video is defined', () => {
            it('should return a promise resolution', async () => {
              const toggle = sinon.stub().returns(Promise.resolve(true));
  
              meeting.mediaId = 'mediaId';
              meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
              meeting.video = {toggle};
              const video = meeting.unmuteVideo();
  
              assert.exists(video.then);
              await video;
              assert.calledOnce(toggle);
              assert.calledWith(toggle, {mute: false, self: true});
            });
          });
        });
        describe('#joinWithMedia', () => {
          it('should have #joinWithMedia', () => {
            assert.exists(meeting.joinWithMedia);
          });
          describe('resolution', () => {
            it('should success and return a promise', async () => {
              meeting.join = sinon.stub().returns(Promise.resolve(test1));
              meeting.getMediaStreams = sinon.stub().returns(Promise.resolve([test2, test3]));
              meeting.addMedia = sinon.stub().returns(Promise.resolve(test4));
              await meeting.joinWithMedia({});
              assert.calledOnce(meeting.join);
              assert.calledOnce(meeting.getMediaStreams);
            });
          });
          describe('rejection', () => {
            it('should error out and return a promise', async () => {
              meeting.join = sinon.stub().returns(Promise.reject());
              meeting.getMediaStreams = sinon.stub().returns(true);
              assert.isRejected(meeting.joinWithMedia({}));
            });
          });
        });
        describe('#getMediaStreams', () => {
          let sandbox;
  
          beforeEach(() => {
            sandbox = sinon.createSandbox();
            sandbox.stub(Media, 'getSupportedDevice').returns(Promise.resolve({sendAudio: true, sendVideo: true}));
            sandbox.stub(Media, 'getUserMedia').returns(Promise.resolve(['stream1', 'stream2']));
          });
          afterEach(() => {
            sandbox.restore();
            sandbox = null;
          });
          it('should have #getMediaStreams', () => {
            assert.exists(meeting.getMediaStreams);
          });
          it('should proxy Media getUserMedia, and return a promise', async () => {
            await meeting.getMediaStreams({sendAudio: true, sendVideo: true});
  
            assert.calledOnce(Media.getUserMedia);
          });
          it('uses the preferred video device if set', async () => {
            const videoDevice = 'video1';
            const mediaDirection = {sendAudio: true, sendVideo: true, sendShare: false};
            const audioVideoSettings = {};
  
            sandbox.stub(meeting.mediaProperties, 'videoDeviceId').value(videoDevice);
  
            await meeting.getMediaStreams(mediaDirection, audioVideoSettings);
  
            assert.calledWith(Media.getUserMedia, {
              ...mediaDirection,
              isSharing: false
            },
            {
              video: {
                deviceId: videoDevice
              }
            });
          });
          it('will set a new preferred video input device if passed in', async () => {
            // if audioVideo settings parameter specifies a new video device it
            // will store that device as the preferred video device.
            // Which is the case with meeting.updateVideo()
            const oldVideoDevice = 'video1';
            const newVideoDevice = 'video2';
            const mediaDirection = {sendAudio: true, sendVideo: true, sendShare: false};
            const audioVideoSettings = {video: {deviceId: newVideoDevice}};
  
            sandbox.stub(meeting.mediaProperties, 'videoDeviceId').value(oldVideoDevice);
            sandbox.stub(meeting.mediaProperties, 'setVideoDeviceId');
  
            await meeting.getMediaStreams(mediaDirection, audioVideoSettings);
  
            assert.calledWith(meeting.mediaProperties.setVideoDeviceId, newVideoDevice);
          });
        });
        describe('#join', () => {
          it('should have #join', () => {
            assert.exists(meeting.join);
          });
          beforeEach(() => {
            meeting.setCorrelationId = sinon.stub().returns(true);
            meeting.setLocus = sinon.stub().returns(true);
          });
          describe('successful', () => {
            beforeEach(() => {
              MeetingUtil.joinMeeting = sinon.stub().returns(Promise.resolve());
            });
            it('should join the meeting and return promise', async () => {
              const join = meeting.join();
  
              assert.exists(join.then);
              await join;
              assert.calledOnce(MeetingUtil.joinMeeting);
              assert.calledOnce(meeting.setLocus);
            });
          });
          describe('failure', () => {
            beforeEach(() => {
              MeetingUtil.joinMeeting = sinon.stub().returns(Promise.reject());
              meeting.logger.log = sinon.stub().returns(true);
            });
            describe('guest join', () => {
              beforeEach(() => {
                MeetingUtil.isPinOrGuest = sinon.stub().returns(true);
                MeetingUtil.hasOwner = sinon.stub().returns(false);
              });
              it('should try to join the meeting and return intent failure promise', async () => {
                await meeting.join().catch(() => {
                  assert.calledOnce(MeetingUtil.joinMeeting);
                });
              });
            });
            describe('total failure', () => {
              beforeEach(() => {
                MeetingUtil.isPinOrGuest = sinon.stub().returns(false);
              });
            });
            it('should try to join the meeting and return promise reject', async () => {
              await meeting.join().catch(() => {
                assert.calledOnce(MeetingUtil.joinMeeting);
              });
            });
          });
        });
        describe('#addMedia', () => {
          beforeEach(() => {
            meeting.mediaProperties.setMediaDirection = sinon.stub().returns(true);
            meeting.audio = true;
            meeting.video = true;
            Media.attachMedia = sinon.stub().returns(Promise.resolve([test1, test2]));
            meeting.setMercuryListener = sinon.stub().returns(true);
            meeting.setRemoteStream = sinon.stub().returns(true);
            meeting.startMediaQualityMetrics = sinon.stub();
            meeting.setMercuryListener = sinon.stub();
            meeting.roap.sendRoapMediaRequest = sinon.stub().returns(new Promise((resolve) => {
              meeting.mediaProperties.peerConnection.connectionState = CONSTANTS.CONNECTION_STATE.CONNECTED;
              resolve();
            }));
            PeerConnectionManager.setContentSlides = sinon.stub().returns(true);
            MeetingUtil.startInternalStats = sinon.stub();
          });
  
          it('should have #addMedia', () => {
            assert.exists(meeting.addMedia);
          });
  
          it('should reject promise if meeting is not active', async () => {
            await meeting.addMedia().catch((err) => {
              assert.instanceOf(err, MeetingNotActiveError);
            });
          });
  
          it('should reject promise if user already in left state', async () => {
            meeting.meetingState = 'ACTIVE';
            await meeting.addMedia().catch((err) => {
              assert.instanceOf(err, UserNotJoinedError);
            });
          });
  
          it('should reject promise if user is guest and in lobby ', async () => {
            meeting.meetingState = 'ACTIVE';
            meeting.locusInfo.parsedLocus = {self: {state: 'IDLE'}};
            meeting.guest = true;
  
            try {
              await meeting.addMedia();
              assert.fail('addMedia should have thrown an exception.');
            }
            catch (err) {
              assert.instanceOf(err, UserInLobbyError);
            }
          });
  
  
          it('should attach the media and return promise', async () => {
            meeting.meetingState = 'ACTIVE';
            const media = meeting.addMedia({
              mediaSettings: {}
            });
  
            assert.exists(media);
            await media;
            assert.calledOnce(meeting.mediaProperties.setMediaDirection);
            assert.calledOnce(Media.attachMedia);
            assert.calledOnce(meeting.setMercuryListener);
            assert.calledOnce(meeting.setRemoteStream);
            assert.calledOnce(meeting.roap.sendRoapMediaRequest);
            /* statsAnalyzer is initiated inside of addMedia so there isn't
            * a good way to mock it without mocking the constructor
            */
            // assert.calledOnce(meeting.startMediaQualityMetrics);
          });
        });
        describe('#acknowledge', () => {
          it('should have #acknowledge', () => {
            assert.exists(meeting.acknowledge);
          });
          beforeEach(() => {
            meeting.meetingRequest.acknowledgeMeeting = sinon.stub().returns(Promise.resolve());
          });
          it('should acknowledge incoming and return a promise', async () => {
            const ack = meeting.acknowledge('INCOMING', false);
  
            assert.exists(ack.then);
            await ack;
            assert.calledOnce(meeting.meetingRequest.acknowledgeMeeting);
          });
          it('should acknowledge a non incoming and return a promise', async () => {
            const ack = meeting.acknowledge(test1, false);
  
            assert.exists(ack.then);
            await ack;
            assert.notCalled(meeting.meetingRequest.acknowledgeMeeting);
          });
        });
        describe('#decline', () => {
          it('should have #decline', () => {
            assert.exists(meeting.decline);
          });
          beforeEach(() => {
            meeting.meetingRequest.declineMeeting = sinon.stub().returns(Promise.resolve());
            meeting.meetingFiniteStateMachine.ring();
          });
          it('should decline the meeting and trigger meeting destroy for 1:1', async () => {
            await meeting.decline();
            assert.calledOnce(meeting.meetingRequest.declineMeeting);
          });
        });
        describe('#leave', () => {
          let sandbox;
  
          it('should have #leave', () => {
            assert.exists(meeting.leave);
          });
  
          it('should reject if meeting is already inactive', async () => {
            await meeting.leave().catch((err) => {
              assert.instanceOf(err, MeetingNotActiveError);
            });
          });
  
          it('should reject if meeting is already left', async () => {
            meeting.meetingState = 'ACTIVE';
            await meeting.leave().catch((err) => {
              assert.instanceOf(err, UserNotJoinedError);
            });
          });
  
          beforeEach(() => {
            sandbox = sinon.createSandbox();
            meeting.meetingFiniteStateMachine.ring();
            meeting.meetingFiniteStateMachine.join();
            meeting.meetingRequest.leaveMeeting = sinon.stub().returns(Promise.resolve({body: 'test'}));
            meeting.locusInfo.onFullLocus = sinon.stub().returns(true);
            // the 3 need to be promises because we do closeLocalStream.then(closeLocalShare.then) etc in the src code
            meeting.closeLocalStream = sinon.stub().returns(Promise.resolve());
            meeting.closeLocalShare = sinon.stub().returns(Promise.resolve());
            meeting.closeRemoteStream = sinon.stub().returns(Promise.resolve());
            sandbox.stub(meeting, 'closeRemoteTracks').returns(Promise.resolve());
            meeting.closePeerConnections = sinon.stub().returns(Promise.resolve());
            meeting.unsetLocalVideoTrack = sinon.stub().returns(true);
            meeting.unsetLocalShareTrack = sinon.stub().returns(true);
            meeting.unsetRemoteTracks = sinon.stub();
            meeting.statsAnalyzer = {stopAnalyzer: sinon.stub()};
            meeting.unsetRemoteStream = sinon.stub().returns(true);
            meeting.unsetPeerConnections = sinon.stub().returns(true);
            meeting.roap.stop = sinon.stub().returns(Promise.resolve());
            meeting.logger.error = sinon.stub().returns(true);
  
            // A meeting needs to be joined to leave
            meeting.meetingState = 'ACTIVE';
            meeting.state = 'JOINED';
          });
          afterEach(() => {
            sandbox.restore();
            sandbox = null;
          });
          it('should leave the meeting and return promise', async () => {
            const leave = meeting.leave();
  
            assert.exists(leave.then);
            await leave;
            assert.calledOnce(meeting.meetingRequest.leaveMeeting);
            assert.calledOnce(meeting.closeLocalStream);
            assert.calledOnce(meeting.closeLocalShare);
            assert.calledOnce(meeting.closeRemoteTracks);
            assert.calledOnce(meeting.closePeerConnections);
            assert.calledOnce(meeting.unsetLocalVideoTrack);
            assert.calledOnce(meeting.unsetLocalShareTrack);
            assert.calledOnce(meeting.unsetRemoteTracks);
            assert.calledOnce(meeting.unsetPeerConnections);
            assert.calledOnce(meeting.roap.stop);
          });
          describe('after audio/video is defined', () => {
            let toggle;
  
            beforeEach(() => {
              toggle = sinon.stub().returns(Promise.resolve(true));
  
              meeting.audio = {toggle};
              meeting.video = {toggle};
            });
  
            it('should delete audio and video state machines when leaving the meeting', async () => {
              const leave = meeting.leave();
  
              assert.exists(leave.then);
              await leave;
  
              assert.isNull(meeting.audio);
              assert.isNull(meeting.video);
            });
          });
          it('should leave the meeting without leaving resource', async () => {
            const leave = meeting.leave({resourceId: null});
  
            assert.exists(leave.then);
            await leave;
            assert.calledWith(meeting.meetingRequest.leaveMeeting, {
              locusUrl: meeting.locusUrl,
              correlationId: meeting.correlationId,
              selfId: meeting.selfId,
              resourceId: null,
              deviceUrl: meeting.deviceUrl
            });
          });
          it('should leave the meeting on the resource', async () => {
            const leave = meeting.leave();
  
            assert.exists(leave.then);
            await leave;
            assert.calledWith(meeting.meetingRequest.leaveMeeting, {
              locusUrl: meeting.locusUrl,
              correlationId: meeting.correlationId,
              selfId: meeting.selfId,
              resourceId: meeting.resourceId,
              deviceUrl: meeting.deviceUrl
            });
          });
        });
        describe('#share', () => {
          it('should have #share', () => {
            assert.exists(meeting.share);
          });
          beforeEach(() => {
            meeting.locusInfo.mediaShares = [{name: 'content', url: url1}];
            meeting.locusInfo.self = {url: url1};
            meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
          });
          it('should send the share', async () => {
            const share = meeting.share();
  
            assert.exists(share.then);
            await share;
            assert.calledOnce(meeting.meetingRequest.changeMeetingFloor);
          });
        });
  
        describe('#shareScreen', () => {
          let mediaDirection;
  
          it('should have #shareScreen', () => {
            assert.exists(meeting.shareScreen);
          });
  
          beforeEach(() => {
            mediaDirection = {sendAudio: true, sendVideo: true, sendShare: false};
            meeting.mediaProperties.mediaDirection = mediaDirection;
            Media.getDisplayMedia = sinon.stub().returns(Promise.resolve());
            meeting.updateShare = sinon.stub().returns(Promise.resolve());
          });
  
          it('should call get display media', async () => {
            await meeting.shareScreen();
  
            assert.calledOnce(Media.getDisplayMedia);
          });
  
          it('should call updateShare', async () => {
            await meeting.shareScreen();
  
            assert.calledOnce(meeting.updateShare);
          });
  
          it('properly assigns default values', async () => {
            await meeting.shareScreen({sharePreferences: {highFrameRate: true}});
  
            assert.calledWith(Media.getDisplayMedia, {sendShare: true, sendAudio: false, sharePreferences: {highFrameRate: true}});
          });
        });
  
        describe('#shareScreen resolutions', () => {
          let _getDisplayMedia = null;
          const config = DefaultSDKConfig.meetings;
          const {resolution} = config;
          const shareOptions = {
            sendShare: true,
            sendAudio: false
          };
          const fireFoxOptions = {
            audio: false,
            video: {
              audio: shareOptions.sendAudio,
              video: shareOptions.sendShare
            }
          };
  
          const MediaStream = {
            getVideoTracks: () => [{
              applyConstraints: () => {}
            }]
          };
  
          const MediaConstraint = {
            cursor: 'always',
            aspectRatio: config.aspectRatio,
            frameRate: config.screenFrameRate,
            width: null,
            height: null
          };
  
          const browserConditionalValue = (value) => {
            const key = bowser.name.toLowerCase();
            const defaultKey = 'default';
  
            return value[key] || value[defaultKey];
          };
  
          before(() => {
            meeting.updateShare = sinon.stub().returns(Promise.resolve());
  
            if (!global.navigator) {
              global.navigator = {
                mediaDevices: {
                  getDisplayMedia: null
                }
              };
            }
            _getDisplayMedia = global.navigator.mediaDevices.getDisplayMedia;
            Object.defineProperty(
              global.navigator.mediaDevices,
              'getDisplayMedia',
              {
                value: sinon.stub().returns(Promise.resolve(MediaStream)),
                writable: true
              }
            );
          });
  
          after(() => {
            // clean up for browser
            Object.defineProperty(
              global.navigator.mediaDevices,
              'getDisplayMedia',
              {
                value: _getDisplayMedia,
                writable: true
              }
            );
          });
  
          // eslint-disable-next-line max-len
          it('will use shareConstraints if defined in provided options', () => {
            const SHARE_WIDTH = 640;
            const SHARE_HEIGHT = 480;
            const shareConstraints = {
              highFrameRate: 2,
              maxWidth: SHARE_WIDTH,
              maxHeight: SHARE_HEIGHT,
              idealWidth: SHARE_WIDTH,
              idealHeight: SHARE_HEIGHT
            };
  
            // If sharePreferences.shareConstraints is defined it ignores
            // default SDK config settings
            getDisplayMedia({
              ...shareOptions,
              sharePreferences: {shareConstraints}
            }, config);
  
            // eslint-disable-next-line no-undef
            assert.calledWith(navigator.mediaDevices.getDisplayMedia,
              browserConditionalValue({
                default: {
                  video: {...shareConstraints}
                },
                // Firefox is being handled differently
                firefox: fireFoxOptions
              }));
          });
  
          // eslint-disable-next-line max-len
          it('will use default resolution if shareConstraints is undefined and highFrameRate is defined', () => {
            // If highFrameRate is defined it ignores default SDK config settings
            getDisplayMedia({
              ...shareOptions,
              sharePreferences: {
                highFrameRate: true
              }
            }, config);
  
            // eslint-disable-next-line no-undef
            assert.calledWith(navigator.mediaDevices.getDisplayMedia,
              browserConditionalValue({
                default: {
                  video: {
                    ...MediaConstraint,
                    frameRate: config.videoShareFrameRate,
                    width: resolution.idealWidth,
                    height: resolution.idealHeight,
                    maxWidth: resolution.maxWidth,
                    maxHeight: resolution.maxHeight,
                    idealWidth: resolution.idealWidth,
                    idealHeight: resolution.idealHeight
                  }
                },
                firefox: fireFoxOptions
              }));
          });
  
          // eslint-disable-next-line max-len
          it('will use default screenResolution if shareConstraints, highFrameRate, and SDK defaults is undefined', () => {
            getDisplayMedia(shareOptions);
            const {screenResolution} = config;
  
            // eslint-disable-next-line no-undef
            assert.calledWith(navigator.mediaDevices.getDisplayMedia,
              browserConditionalValue({
                default: {
                  video: {
                    ...MediaConstraint,
                    width: screenResolution.idealWidth,
                    height: screenResolution.idealHeight
                  }
                },
                firefox: fireFoxOptions
              }));
          });
  
          // Test screenResolution
          // eslint-disable-next-line max-len
          it('will use SDK config screenResolution if set, with shareConstraints and highFrameRate being undefined', () => {
            const SHARE_WIDTH = 800;
            const SHARE_HEIGHT = 600;
            const customConfig = {
              screenResolution: {
                maxWidth: SHARE_WIDTH,
                maxHeight: SHARE_HEIGHT,
                idealWidth: SHARE_WIDTH,
                idealHeight: SHARE_HEIGHT
              }
            };
  
            getDisplayMedia(shareOptions, customConfig);
  
            // eslint-disable-next-line no-undef
            assert.calledWith(navigator.mediaDevices.getDisplayMedia,
              browserConditionalValue({
                default: {
                  video: {
                    ...MediaConstraint,
                    width: SHARE_WIDTH,
                    height: SHARE_HEIGHT,
                    maxWidth: SHARE_WIDTH,
                    maxHeight: SHARE_HEIGHT,
                    idealWidth: SHARE_WIDTH,
                    idealHeight: SHARE_HEIGHT
                  }
                },
                firefox: fireFoxOptions
              }));
          });
  
          // Test screenFrameRate
          it('will use SDK config screenFrameRate if set, with shareConstraints and highFrameRate being undefined', () => {
            const SHARE_WIDTH = 800;
            const SHARE_HEIGHT = 600;
            const customConfig = {
              screenFrameRate: 999,
              screenResolution: {
                maxWidth: SHARE_WIDTH,
                maxHeight: SHARE_HEIGHT,
                idealWidth: SHARE_WIDTH,
                idealHeight: SHARE_HEIGHT
              }
            };
  
            getDisplayMedia(shareOptions, customConfig);
  
            // eslint-disable-next-line no-undef
            assert.calledWith(navigator.mediaDevices.getDisplayMedia,
              browserConditionalValue({
                default: {
                  video: {
                    ...MediaConstraint,
                    frameRate: customConfig.screenFrameRate,
                    width: SHARE_WIDTH,
                    height: SHARE_HEIGHT,
                    maxWidth: SHARE_WIDTH,
                    maxHeight: SHARE_HEIGHT,
                    idealWidth: SHARE_WIDTH,
                    idealHeight: SHARE_HEIGHT
                  }
                },
                firefox: fireFoxOptions
              }));
          });
        });
  
        describe('#stopShare', () => {
          it('should have #stopShare', () => {
            assert.exists(meeting.stopShare);
          });
          beforeEach(() => {
            meeting.mediaProperties.mediaDirection = {receiveShare: true};
            meeting.updateShare = sinon.stub().returns(Promise.resolve());
          });
          it('should call updateShare', async () => {
            const share = meeting.stopShare();
  
            assert.exists(share.then);
            await share;
            assert.calledOnce(meeting.updateShare);
          });
        });
        describe('#updateAudio', () => {
          describe('when canUpdateMedia is true', () => {
            beforeEach(() => {
              meeting.canUpdateMedia = sinon.stub().returns(true);
            });
            describe('when options are valid', () => {
              beforeEach(() => {
                MeetingUtil.validateOptions = sinon.stub().returns(Promise.resolve());
              });
              describe('when mediaDirection is undefined', () => {
                beforeEach(() => {
                  meeting.mediaProperties.mediaDirection = null;
                  MeetingUtil.updateTransceiver = sinon.stub();
                });
  
                it('sets previousMediaDirection to an empty object', () => meeting.updateAudio({
                  sendAudio: true,
                  receiveAudio: true
                }).then(() => {
                  assert.calledOnce(MeetingUtil.updateTransceiver);
                }));
              });
            });
          });
        });
  
        describe('#sendDTMF', () => {
          it('should have #sendDTMF', () => {
            assert.exists(meeting.sendDTMF);
          });
  
          it('should call the meetingRequest sendDTMF method', async () => {
            const tones = '123';
  
            meeting.meetingRequest.sendDTMF = sinon.stub().returns(Promise.resolve());
  
            meeting.locusInfo.self = {
              enableDTMF: true,
              url: url2
            };
  
            await meeting.sendDTMF(tones);
  
            assert.calledWith(meeting.meetingRequest.sendDTMF, {
              locusUrl: meeting.locusInfo.self.url,
              deviceUrl: meeting.deviceUrl,
              tones
            });
          });
  
          describe('when meeting does not have a locus', () => {
            it('should throw an error', () => {
              assert.isRejected(meeting.sendDTMF('123'));
            });
          });
  
          describe('when meeting does not have DTMF enabled', () => {
            it('should throw an error', () => {
              meeting.locusInfo.self = {
                enableDTMF: false,
                url: url2
              };
  
              assert.isRejected(meeting.sendDTMF('123'));
            });
          });
        });
  
        describe('#changeVideoLayout', () => {
          describe('when media direction has recieve video and there is remoteStream', () => {
            let mediaDirection;
  
            beforeEach(() => {
              mediaDirection = {
                sendAudio: true,
                sendVideo: true,
                sendShare: false,
                receiveVideo: true
              };
              meeting.getMediaStreams = sinon.stub().returns(Promise.resolve([]));
              meeting.updateVideo = sinon.stub().returns(Promise.resolve());
              meeting.mediaProperties.mediaDirection = mediaDirection;
              meeting.mediaProperties.remoteVideoTrack = sinon.stub().returns({mockTrack: 'mockTrack'});
            });
  
            it('should have receiveVideo true and remote video track should exist', () => {
              assert.equal(meeting.mediaProperties.mediaDirection.receiveVideo, true);
              assert.exists(meeting.mediaProperties.remoteVideoTrack);
            });
  
            it('has layoutType which exists in the list of allowed layoutTypes and should call meetingRequest changeVideoLayout method', async () => {
              const layoutType = 'Equal';
  
              meeting.meetingRequest.changeVideoLayout = sinon.stub().returns(Promise.resolve());
  
              meeting.locusInfo.self = {
                url: url2
              };
  
              await meeting.changeVideoLayout(layoutType);
  
              assert(CONSTANTS.LAYOUT_TYPES.includes(layoutType));
              assert.calledWith(meeting.meetingRequest.changeVideoLayout, {
                locusUrl: meeting.locusInfo.self.url,
                deviceUrl: meeting.deviceUrl,
                layoutType
              });
            });
  
            it('doesn\'t have layoutType which exists in the list of allowed layoutTypes should throw an error', async () => {
              const layoutType = 'Invalid Layout';
  
              meeting.meetingRequest.changeVideoLayout = sinon.stub().returns(Promise.resolve());
  
              meeting.locusInfo.self = {
                url: url2
              };
  
              assert.isRejected(meeting.changeVideoLayout(layoutType));
            });
          });
  
          it('should throw error when there is no remote stream', () => {
            const layoutType = 'Equal';
  
            const mediaDirection = {
              sendAudio: true,
              sendVideo: true,
              sendShare: false,
              receiveVideo: true
            };
  
            meeting.mediaProperties.mediaDirection = mediaDirection;
            assert.isRejected(meeting.changeVideoLayout(layoutType));
          });
  
          it('should throw error when mediaDirection.receiveVideo is false', () => {
            const layoutType = 'Equal';
  
            const mediaDirection = {
              sendAudio: true,
              sendVideo: true,
              sendShare: false,
              receiveVideo: false
            };
  
            meeting.mediaProperties.mediaDirection = mediaDirection;
            meeting.mediaProperties.remoteVideoTrack = sinon.stub().returns({mockTrack: 'mockTrack'});
            assert.isRejected(meeting.changeVideoLayout(layoutType));
          });
        });
  
        describe('#setLocalVideoQuality', () => {
          let mediaDirection;
  
          beforeEach(() => {
            mediaDirection = {sendAudio: true, sendVideo: true, sendShare: false};
            meeting.getMediaStreams = sinon.stub().returns(Promise.resolve([]));
            meeting.updateVideo = sinon.stub().returns(Promise.resolve());
            meeting.mediaProperties.mediaDirection = mediaDirection;
          });
  
          it('should have #setLocalVideoQuality', () => {
            assert.exists(meeting.setLocalVideoQuality);
          });
  
          it('should call getMediaStreams with the proper level', () => meeting.setLocalVideoQuality(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
            assert.calledWith(meeting.getMediaStreams,
              mediaDirection,
              CONSTANTS.VIDEO_RESOLUTIONS[CONSTANTS.QUALITY_LEVELS.LOW]);
          }));
  
          it('should set mediaProperty with the proper level', () => meeting.setLocalVideoQuality(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
            assert.equal(meeting.mediaProperties.localQualityLevel, CONSTANTS.QUALITY_LEVELS.LOW);
          }));
  
          it('should error if set to a invalid level', () => {
            assert.isRejected(meeting.setLocalVideoQuality('invalid'));
          });
  
          it('should error if sendVideo is set to false', () => {
            meeting.mediaProperties.mediaDirection = {sendVideo: false};
            assert.isRejected(meeting.setLocalVideoQuality('LOW'));
          });
        });
  
        describe('#setRemoteQualityLevel', () => {
          let mediaDirection;
  
          beforeEach(() => {
            mediaDirection = {receiveAudio: true, receiveVideo: true, receiveShare: false};
            meeting.updateMedia = sinon.stub().returns(Promise.resolve());
            meeting.mediaProperties.mediaDirection = mediaDirection;
          });
  
          it('should have #setRemoteQualityLevel', () => {
            assert.exists(meeting.setRemoteQualityLevel);
          });
  
          it('should set mediaProperty with the proper level', () => meeting.setRemoteQualityLevel(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
            assert.equal(meeting.mediaProperties.remoteQualityLevel, CONSTANTS.QUALITY_LEVELS.LOW);
          }));
  
          it('should call updateMedia', () => meeting.setRemoteQualityLevel(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
            assert.calledOnce(meeting.updateMedia);
          }));
  
          it('should error if set to a invalid level', () => {
            assert.isRejected(meeting.setRemoteQualityLevel('invalid'));
          });
  
          it('should error if receiveVideo is set to false', () => {
            meeting.mediaProperties.mediaDirection = {receiveVideo: false};
            assert.isRejected(meeting.setRemoteQualityLevel('LOW'));
          });
        });
  
        describe('#setMeetingQuality', () => {
          let mediaDirection;
  
          beforeEach(() => {
            mediaDirection = {
              receiveAudio: true, receiveVideo: true, receiveShare: false, sendVideo: true
            };
            meeting.setRemoteQualityLevel = sinon.stub().returns(Promise.resolve());
            meeting.setLocalVideoQuality = sinon.stub().returns(Promise.resolve());
            meeting.mediaProperties.mediaDirection = mediaDirection;
          });
  
          it('should have #setMeetingQuality', () => {
            assert.exists(meeting.setMeetingQuality);
          });
  
          it('should call setRemoteQualityLevel', () => meeting.setMeetingQuality(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
            assert.calledOnce(meeting.setRemoteQualityLevel);
          }));
  
          it('should not call setRemoteQualityLevel when receiveVideo and receiveAudio are false', () => {
            mediaDirection.receiveAudio = false;
            mediaDirection.receiveVideo = false;
            meeting.mediaProperties.mediaDirection = mediaDirection;
  
            return meeting.setMeetingQuality(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
              assert.notCalled(meeting.setRemoteQualityLevel);
            });
          });
  
          it('should call setLocalVideoQuality', () => meeting.setMeetingQuality(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
            assert.calledOnce(meeting.setLocalVideoQuality);
          }));
  
          it('should not call setLocalVideoQuality when sendVideo is false', () => {
            mediaDirection.sendVideo = false;
            meeting.mediaProperties.mediaDirection = mediaDirection;
  
            return meeting.setMeetingQuality(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
              assert.notCalled(meeting.setLocalVideoQuality);
            });
          });
  
          it('should error if set to a invalid level', () => {
            assert.isRejected(meeting.setMeetingQuality('invalid'));
          });
        });
      });
  
      describe('Public Event Triggers', () => {
        let sandbox;
        const {ENDED} = CONSTANTS;
  
        beforeEach(() => {
          const fakeMediaTrack = () => ({stop: () => {}, readyState: ENDED});
  
          sandbox = sinon.createSandbox();
          sandbox.stub(Media, 'stopTracks').returns(Promise.resolve());
          sandbox.stub(meeting.mediaProperties, 'audioTrack').value(fakeMediaTrack());
          sandbox.stub(meeting.mediaProperties, 'videoTrack').value(fakeMediaTrack());
          sandbox.stub(meeting.mediaProperties, 'shareTrack').value(fakeMediaTrack());
          sandbox.stub(meeting.mediaProperties, 'remoteAudioTrack').value(fakeMediaTrack());
          sandbox.stub(meeting.mediaProperties, 'remoteVideoTrack').value(fakeMediaTrack());
          sandbox.stub(meeting.mediaProperties, 'remoteShare').value(fakeMediaTrack());
        });
        afterEach(() => {
          sandbox.restore();
          sandbox = null;
        });
  
        describe('#reconnect', () => {
          it('should have #reconnect', () => {
            assert.exists(meeting.reconnect);
          });
          describe('successful reconnect', () => {
            beforeEach(() => {
              meeting.config.reconnection.enabled = true;
              meeting.reconnectionManager = new ReconnectionManager(meeting);
              meeting.reconnectionManager.reconnect = sinon.stub().returns(Promise.resolve());
              meeting.reconnectionManager.reset = sinon.stub().returns(true);
            });
  
            it('should trigger reconnection success', async () => {
              await meeting.reconnect();
              assert.calledWith(
                TriggerProxy.trigger,
                sinon.match.instanceOf(Meeting),
                {file: 'meeting/index', function: 'reconnect'},
                'meeting:reconnectionSuccess'
              );
            });
  
            it('should reset after reconnection success', async () => {
              await meeting.reconnect();
              assert.calledOnce(meeting.reconnectionManager.reset);
            });
          });
  
          describe('unsuccessful reconnect', () => {
            beforeEach(() => {
              meeting.config.reconnection.enabled = true;
              meeting.reconnectionManager = new ReconnectionManager(meeting);
              meeting.reconnectionManager.reconnect = sinon.stub().returns(Promise.reject());
              meeting.reconnectionManager.reset = sinon.stub().returns(true);
            });
  
            it('should trigger an unsuccessful reconnection', async () => {
              await assert.isRejected(meeting.reconnect());
              assert.calledWith(
                TriggerProxy.trigger,
                sinon.match.instanceOf(Meeting),
                {file: 'meeting/index', function: 'reconnect'},
                'meeting:reconnectionFailure',
                {error: sinon.match.any}
              );
            });
  
            it('should reset after an unsuccessful reconnection', async () => {
              await assert.isRejected(meeting.reconnect());
              assert.calledOnce(meeting.reconnectionManager.reset);
            });
          });
        });
        describe('#closeRemoteStream', () => {
          it('should stop remote tracks, and trigger a media:stopped event when the remote tracks are stopped', async () => {
            await meeting.closeRemoteTracks();
  
            assert.equal(TriggerProxy.trigger.callCount, 4);
            assert.calledWith(
              TriggerProxy.trigger,
              sinon.match.instanceOf(Meeting),
              {file: 'meeting/index', function: 'closeRemoteTracks'},
              'media:stopped',
              {type: 'remoteAudio'}
            );
            assert.calledWith(
              TriggerProxy.trigger,
              sinon.match.instanceOf(Meeting),
              {file: 'meeting/index', function: 'closeRemoteTracks'},
              'media:stopped',
              {type: 'remoteVideo'}
            );
            assert.calledWith(
              TriggerProxy.trigger,
              sinon.match.instanceOf(Meeting),
              {file: 'meeting/index', function: 'closeRemoteTracks'},
              'media:stopped',
              {type: 'remoteShare'}
            );
          });
        });
        describe('#closeLocalShare', () => {
          it('should stop the stream, and trigger a media:stopped event when the local share stream stops', async () => {
            await meeting.closeLocalShare();
            assert.calledTwice(TriggerProxy.trigger);
  
            assert.equal(TriggerProxy.trigger.getCall(1).args[2], 'media:stopped');
            assert.deepEqual(TriggerProxy.trigger.getCall(1).args[3], {type: 'localShare'});
          });
        });
        describe('#closeLocalStream', () => {
          it('should stop the stream, and trigger a media:stopped event when the local stream stops', async () => {
            await meeting.closeLocalStream();
            assert.calledTwice(TriggerProxy.trigger);
            assert.calledWith(
              TriggerProxy.trigger,
              sinon.match.instanceOf(Meeting),
              {file: 'meeting/index', function: 'closeLocalStream'},
              'media:stopped',
              {type: 'local'}
            );
          });
        });
        describe('#setLocalTracks', () => {
          it('stores the current video device as the preferred video device', () => {
            const videoDevice = 'video1';
            const fakeTrack = {getSettings: () => ({deviceId: videoDevice})};
            const fakeStream = 'stream1';
  
            sandbox.stub(MeetingUtil, 'getTrack').returns({audioTrack: null, videoTrack: fakeTrack});
            sandbox.stub(meeting.mediaProperties, 'setMediaSettings');
            sandbox.stub(meeting.mediaProperties, 'setVideoDeviceId');
            sandbox.stub(MediaUtil, 'createMediaStream').returns(true);
  
            meeting.setLocalTracks(fakeStream);
  
            assert.calledWith(meeting.mediaProperties.setVideoDeviceId, videoDevice);
          });
        });
        describe('#setLocalShareTrack', () => {
          it('should trigger a media:ready event with local share stream', () => {
            const track = {
              getSettings: sinon.stub().returns({
                aspectRatio: '1.7',
                frameRate: 30,
                height: 1980,
                width: 1080,
                displaySurface: true,
                cursor: true
              })
            };
            const getVideoTracks = sinon.stub().returns([track]);
  
            meeting.mediaProperties.setLocalShareTrack = sinon.stub().returns(true);
            meeting.mediaProperties.shareTrack = {getVideoTracks, getSettings: track.getSettings};
            meeting.stopShare = sinon.stub().returns(true);
            meeting.mediaProperties.mediaDirection = {};
            meeting.setLocalShareTrack(test1);
            assert.calledTwice(TriggerProxy.trigger);
            assert.calledWith(
              TriggerProxy.trigger,
              sinon.match.instanceOf(Meeting),
              {file: 'meeting/index', function: 'setLocalShareTrack'},
              'media:ready'
            );
            assert.calledOnce(meeting.mediaProperties.setLocalShareTrack);
            assert.equal(meeting.mediaProperties.localStream, undefined);
            meeting.mediaProperties.shareTrack.onended();
            assert.calledOnce(meeting.stopShare);
          });
        });
        describe('#setRemoteStream', () => {
          beforeEach(() => {
            MediaUtil.createMediaStream = sinon.stub().returns(true);
            meeting.statsAnalyzer = {startAnalyzer: sinon.stub()};
          });
          it('should trigger a media:ready event when remote stream track ontrack is fired', () => {
            const pc = {};
  
            meeting.setRemoteStream(pc);
            pc.ontrack({track: 'track', transceiver: {mid: '0'}});
            assert.equal(TriggerProxy.trigger.getCall(1).args[2], 'media:ready');
            assert.deepEqual(TriggerProxy.trigger.getCall(1).args[3], {type: 'remoteAudio', stream: true});
  
            pc.ontrack({track: 'track', transceiver: {mid: '1'}});
            assert.equal(TriggerProxy.trigger.getCall(2).args[2], 'media:ready');
            assert.deepEqual(TriggerProxy.trigger.getCall(2).args[3], {type: 'remoteVideo', stream: true});
  
            pc.ontrack({transceiver: {mid: '2'}, track: 'track'});
            assert.equal(TriggerProxy.trigger.getCall(3).args[2], 'media:ready');
            assert.deepEqual(TriggerProxy.trigger.getCall(3).args[3], {type: 'remoteShare', stream: true});
  
  
            // special case for safari
            pc.ontrack({target: {audioTransceiver: {receiver: {track: {id: 'trackId'}}}}, transceiver: {}, track: {id: 'trackId'}});
            assert.equal(TriggerProxy.trigger.getCall(1).args[2], 'media:ready');
            assert.deepEqual(TriggerProxy.trigger.getCall(1).args[3], {type: 'remoteAudio', stream: true});
          });
        });
        describe('#setUpLocusInfoSelfListener', () => {
          it('listens to the self unadmitted guest event', (done) => {
            meeting.locusInfo.emit({function: 'test', file: 'test'}, 'SELF_UNADMITTED_GUEST', test1);
            assert.calledTwice(TriggerProxy.trigger);
            assert.calledWith(
              TriggerProxy.trigger,
              sinon.match.instanceOf(Meeting),
              {file: 'meeting/index', function: 'setUpLocusInfoSelfListener'},
              'meeting:self:lobbyWaiting',
              {payload: test1}
            );
            done();
          });
          it('listens to the self admitted guest event', (done) => {
            meeting.locusInfo.emit({function: 'test', file: 'test'}, 'SELF_ADMITTED_GUEST', test1);
            assert.calledTwice(TriggerProxy.trigger);
            assert.calledWith(
              TriggerProxy.trigger,
              sinon.match.instanceOf(Meeting),
              {file: 'meeting/index', function: 'setUpLocusInfoSelfListener'},
              'meeting:self:guestAdmitted',
              {payload: test1}
            );
            done();
          });
        });
      });
      describe('Private Detailed API and Helpers', () => {
        let sandbox;
  
        beforeEach(() => {
          sandbox = sinon.createSandbox();
          sandbox.stub(meeting.mediaProperties, 'unsetRemoteTracks').returns(Promise.resolve());
        });
  
        afterEach(() => {
          sandbox.restore();
          sandbox = null;
        });
  
        describe('#stopFloorRequest', () => {
          it('should have #stopFloorRequest', () => {
            assert.exists(meeting.stopFloorRequest);
          });
          beforeEach(() => {
            meeting.locusInfo.mediaShares = [{name: 'content', url: url1}];
            meeting.locusInfo.self = {url: url2};
            meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
          });
          it('should call change meeting floor', async () => {
            const share = meeting.share();
  
            assert.exists(share.then);
            await share;
            assert.calledOnce(meeting.meetingRequest.changeMeetingFloor);
          });
        });
  
        describe('#setSipUri', () => {
          it('should set the sip Uri and return null', () => {
            assert.notOk(meeting.sipUri);
            meeting.setSipUri(test1);
            assert.equal(meeting.sipUri, test1);
          });
        });
        describe('#unsetRemoteTracks', () => {
          it('should unset the remote tracks and return null', () => {
            meeting.mediaProperties.unsetRemoteTracks = sinon.stub().returns(true);
            meeting.unsetRemoteTracks();
            assert.calledOnce(meeting.mediaProperties.unsetRemoteTracks);
          });
        });
        describe('#unsetLocalVideoTrack', () => {
          it('should unset the local stream and return null', () => {
            meeting.mediaProperties.unsetLocalVideoTrack = sinon.stub().returns(true);
            meeting.unsetLocalVideoTrack();
            assert.calledOnce(meeting.mediaProperties.unsetLocalVideoTrack);
          });
        });
        describe('#unsetLocalShareTrack', () => {
          it('should unset the local share stream and return null', () => {
            meeting.mediaProperties.unsetLocalShareTrack = sinon.stub().returns(true);
            meeting.unsetLocalShareTrack();
            assert.calledOnce(meeting.mediaProperties.unsetLocalShareTrack);
          });
        });
        // TODO: remove
        describe('#setMercuryListener', () => {
          it('should listen to mercury events', () => {
            meeting.reconnect = sinon.stub().returns(true);
            meeting.webex.internal.mercury.on = sinon.stub().returns(true);
            meeting.setMercuryListener(test1, test2);
            assert.instanceOf(meeting.reconnectionManager, ReconnectionManager);
            assert.calledTwice(meeting.webex.internal.mercury.on);
          });
        });
        describe('#closePeerConnections', () => {
          it('should close the peer connections, and return a promise', async () => {
            PeerConnectionManager.close = sinon.stub().returns(Promise.resolve());
            const pcs = meeting.closePeerConnections();
  
            assert.exists(pcs.then);
            await pcs;
            assert.calledOnce(PeerConnectionManager.close);
          });
        });
        describe('#unsetPeerConnections', () => {
          it('should unset the peer connections', () => {
            meeting.mediaProperties.unsetPeerConnection = sinon.stub().returns(true);
            meeting.webex.internal.mercury.off = sinon.stub().returns(true);
            meeting.unsetPeerConnections();
            assert.calledOnce(meeting.mediaProperties.unsetPeerConnection);
          });
        });
        describe('#createStats', () => {
          it('should create stats for the meeting object', () => {
            const stats = meeting.createStats();
  
            assert.instanceOf(stats, WebRTCStats);
            assert.instanceOf(meeting.getStats(), WebRTCStats);
          });
        });
        describe('#parseMeetingInfo', () => {
          it('should parse meeting info, set values, and return null', () => {
            meeting.parseMeetingInfo({
              body: {
                conversationUrl: uuid1,
                locusUrl: url1,
                sipMeetingUri: test1,
                owner: test2
              }
            });
            assert.equal(meeting.conversationUrl, uuid1);
            assert.equal(meeting.locusUrl, url1);
            assert.equal(meeting.sipUri, test1);
            assert.equal(meeting.owner, test2);
          });
        });
        describe('#parseLocus', () => {
          describe('when CALL and participants', () => {
            beforeEach(() => {
              meeting.setLocus = sinon.stub().returns(true);
              MeetingUtil.getLocusPartner = sinon.stub().returns({person: {sipUrl: uuid3}});
            });
            it('should parse the locus object and set meeting properties and return null', () => {
              meeting.type = 'CALL';
              meeting.parseLocus({url: url1, participants: [{id: uuid1}], self: {id: uuid2}});
              assert.calledOnce(meeting.setLocus);
              assert.calledWith(meeting.setLocus, {url: url1, participants: [{id: uuid1}], self: {id: uuid2}});
              assert.calledOnce(MeetingUtil.getLocusPartner);
              assert.calledWith(MeetingUtil.getLocusPartner, [{id: uuid1}], {id: uuid2});
              assert.deepEqual(meeting.partner, {person: {sipUrl: uuid3}});
              assert.equal(meeting.sipUri, uuid3);
            });
          });
        });
        describe('#setRoapSeq', () => {
          it('should set the roap seq and return null', () => {
            assert.equal(-1, meeting.roapSeq);
            meeting.setRoapSeq(1);
            assert.equal(meeting.roapSeq, 1);
          });
        });
        describe('#setCorrelationId', () => {
          it('should set the correlationId and return undefined', () => {
            assert.ok(meeting.correlationId);
            meeting.setCorrelationId(uuid1);
            assert.equal(meeting.correlationId, uuid1);
          });
        });
        describe('#setLocus', () => {
          beforeEach(() => {
            meeting.locusInfo.initialSetup = sinon.stub().returns(true);
          });
          it('should read the locus object, set on the meeting and return null', () => {
            meeting.parseLocus({
              mediaConnections: [test1],
              locusUrl: url1,
              locusId: uuid1,
              selfId: uuid2,
              mediaId: uuid3,
              host: {id: uuid4}
            });
            assert.calledOnce(meeting.locusInfo.initialSetup);
            assert.calledWith(meeting.locusInfo.initialSetup, {
              mediaConnections: [test1],
              locusUrl: url1,
              locusId: uuid1,
              selfId: uuid2,
              mediaId: uuid3,
              host: {id: uuid4}
            });
            assert.equal(meeting.mediaConnections, test1);
            assert.equal(meeting.locusUrl, url1);
            assert.equal(meeting.locusId, uuid1);
            assert.equal(meeting.selfId, uuid2);
            assert.equal(meeting.mediaId, uuid3);
            assert.equal(meeting.hostId, uuid4);
          });
        });
        describe('preferred video device', () => {
          describe('#getVideoDeviceId', () => {
            it('returns the preferred video device', () => {
              const videoDevice = 'video1';
  
              sandbox.stub(meeting.mediaProperties, 'videoDeviceId').value(videoDevice);
  
              assert.equal(meeting.mediaProperties.getVideoDeviceId(), videoDevice);
            });
            it('returns null if the preferred video device is not set', () => {
              sandbox.stub(meeting.mediaProperties, 'videoDeviceId').value(undefined);
  
              assert.equal(meeting.mediaProperties.getVideoDeviceId(), null);
            });
          });
          describe('#setVideoDeviceId', () => {
            it('sets the preferred video device', () => {
              const videoDevice = 'video1';
  
              sandbox.stub(meeting.mediaProperties, 'videoDeviceId').value(undefined);
              meeting.mediaProperties.setVideoDeviceId(videoDevice);
  
              assert.equal(meeting.mediaProperties.videoDeviceId, videoDevice);
            });
          });
        });
      });
    });
  });
  