// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

#import <XCTest/XCTest.h>

#import "OCTRealmTests.h"

#import "OCTSubmanagerCallsImpl.h"
#import "OCTRealmManager.h"
#import "OCTAudioEngine.h"
#import "OCTMessageCall.h"
#import "OCTMessageAbstract.h"
#import "OCTToxAV.h"
#import "OCTTox.h"
#import "OCTToxOptions.h"
#import <OCMock/OCMock.h>

@import AVFoundation;

@interface OCTSubmanagerCallsImpl (Tests)

@property (strong, nonatomic) OCTToxAV *toxAV;
@property (strong, nonatomic) OCTAudioEngine *audioEngine;
@property (strong, nonatomic) OCTVideoEngine *videoEngine;
@property (weak, nonatomic) id<OCTSubmanagerDataSource> dataSource;
@property (strong, nonatomic) OCTCallTimer *timer;

- (void)toxAV:(OCTToxAV *)toxAV receiveCallAudioEnabled:(BOOL)audio videoEnabled:(BOOL)video friendNumber:(OCTToxFriendNumber)friendNumber;
- (void)toxAV:(OCTToxAV *)toxAV callStateChanged:(OCTToxAVCallState)state friendNumber:(OCTToxFriendNumber)friendNumber;
- (void)toxAV:(OCTToxAV *)toxAV audioBitRateChanged:(OCTToxAVAudioBitRate)bitrate stable:(BOOL)stable friendNumber:(OCTToxFriendNumber)friendNumber;
- (void)   toxAV:(OCTToxAV *)toxAV
    receiveAudio:(OCTToxAVPCMData *)pcm
     sampleCount:(OCTToxAVSampleCount)sampleCount
        channels:(OCTToxAVChannels)channels
      sampleRate:(OCTToxAVSampleRate)sampleRate
    friendNumber:(OCTToxFriendNumber)friendNumber;

- (void)                 toxAV:(OCTToxAV *)toxAV
    receiveVideoFrameWithWidth:(OCTToxAVVideoWidth)width height:(OCTToxAVVideoHeight)height
                        yPlane:(OCTToxAVPlaneData *)yPlane uPlane:(OCTToxAVPlaneData *)uPlane
                        vPlane:(OCTToxAVPlaneData *)vPlane
                       yStride:(OCTToxAVStrideData)yStride uStride:(OCTToxAVStrideData)uStride
                       vStride:(OCTToxAVStrideData)vStride
                  friendNumber:(OCTToxFriendNumber)friendNumber;

- (OCTCall *)createCallWithFriend:(OCTFriend *)friend status:(OCTCallStatus)status;
- (OCTCall *)getCurrentCallForFriendNumber:(OCTToxFriendNumber)friendNumber;

@end

@interface OCTSubmanagerCallsImplTests : OCTRealmTests

@property (strong, nonatomic) id dataSource;
@property (strong, nonatomic) OCTSubmanagerCallsImpl *callManager;
@property (strong, nonatomic) OCTTox *tox;
@property (strong, nonatomic) id mockedAudioEngine;
@property (strong, nonatomic) id mockedVideoEngine;
@property (strong, nonatomic) id mockedToxAV;

@end

@implementation OCTSubmanagerCallsImplTests

- (void)setUp
{
    [super setUp];
    self.tox = [[OCTTox alloc] initWithOptions:[OCTToxOptions new] savedData:nil error:nil];
    self.tox = OCMPartialMock(self.tox);
    self.callManager = [[OCTSubmanagerCallsImpl alloc] initWithTox:self.tox];

    self.dataSource = OCMProtocolMock(@protocol(OCTSubmanagerDataSource));
    OCMStub([self.dataSource managerGetRealmManager]).andReturn(self.realmManager);
    OCMStub([self.dataSource managerGetTox]).andReturn(self.tox);

    OCTAudioEngine *audioEngine = [OCTAudioEngine new];
    self.mockedAudioEngine = OCMPartialMock(audioEngine);
    self.callManager.audioEngine = self.mockedAudioEngine;

    OCTVideoEngine *videoEngine = [OCTVideoEngine new];
    self.mockedVideoEngine = OCMPartialMock(videoEngine);
    self.callManager.videoEngine = self.mockedVideoEngine;

    self.mockedToxAV = OCMClassMock([OCTToxAV class]);
    self.callManager.toxAV = self.mockedToxAV;

    self.callManager.dataSource = self.dataSource;
    // Put setup code here. This method is called before the invocation of each test method in the class.
}

- (void)tearDown
{
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    [self.dataSource stopMocking];
    [self.mockedAudioEngine stopMocking];
    [self.mockedVideoEngine stopMocking];
    [self.mockedToxAV stopMocking];
    [(id)self.tox stopMocking];
    self.dataSource = nil;
    self.callManager = nil;
    self.tox = nil;
    self.mockedAudioEngine = nil;
    self.mockedToxAV = nil;
    self.mockedVideoEngine = nil;
    [super tearDown];
}

- (void)testInit
{
    id tox = OCMClassMock([OCTTox class]);
    OCMStub([self.mockedToxAV alloc]).andReturn(self.mockedToxAV);
    OCMStub([self.mockedToxAV initWithTox:tox error:nil]).andReturn(self.mockedToxAV);

    OCTSubmanagerCallsImpl *manager = [[OCTSubmanagerCallsImpl alloc] initWithTox:tox];

    XCTAssertNotNil(manager);
    OCMVerify([(OCTToxAV *)self.mockedToxAV start]);

    [tox stopMocking];
}

- (void)testSetup
{
    OCMStub([self.mockedAudioEngine new]).andReturn(self.mockedAudioEngine);

    OCMStub([self.mockedVideoEngine new]).andReturn(self.mockedVideoEngine);
    OCMStub([self.mockedVideoEngine setupAndReturnError:[OCMArg anyObjectRef]]).andReturn(YES);

    XCTAssertTrue([self.callManager setupAndReturnError:nil]);

    OCMVerify([self.mockedVideoEngine setupAndReturnError:[OCMArg anyObjectRef]]);
}

- (void)testCallToChat
{
    [OCMStub([self.mockedToxAV callFriendNumber:1
                                   audioBitRate:0
                                   videoBitRate:0
                                          error:[OCMArg anyObjectRef]]).andReturn(YES) ignoringNonObjectArgs];

    OCTFriend *friend = [self createFriendWithFriendNumber:1];

    [self.realmManager.realm beginWriteTransaction];
    [self.realmManager.realm addObject:friend];
    [self.realmManager.realm commitWriteTransaction];

    OCTChat *chat = [self.realmManager getOrCreateChatWithFriend:friend];

    OCTCall *call = [self.callManager callToChat:chat enableAudio:YES enableVideo:NO error:nil];

    XCTAssertNotNil(call.chat);

    XCTAssertEqualObjects(call.chat, chat);
    XCTAssertEqual(call.status, OCTCallStatusDialing);
    XCTAssertNil(call.caller);
    XCTAssertTrue([call isOutgoing]);
}

- (void)testEnableVideoForCall
{
    id partialMockedVideoEngine = OCMPartialMock([OCTVideoEngine new]);
    self.callManager.videoEngine = partialMockedVideoEngine;
    [self.mockedVideoEngine setExpectationOrderMatters:YES];
    OCMExpect([partialMockedVideoEngine startSendingVideo]);
    OCMExpect([partialMockedVideoEngine stopSendingVideo]);

    [OCMStub([self.mockedToxAV setVideoBitRate:123 force:YES forFriend:987 error:[OCMArg anyObjectRef]]).andReturn(YES) ignoringNonObjectArgs];
    OCTFriend *friend = [self createFriendWithFriendNumber:987];
    OCTCall *call = [self.callManager createCallWithFriend:friend status:OCTCallStatusActive];

    XCTAssertTrue([self.callManager enableVideoSending:YES forCall:call error:nil]);
    XCTAssertTrue(call.videoIsEnabled);
    XCTAssertEqual(self.callManager.videoEngine.friendNumber, 987);

    XCTAssertTrue([self.callManager enableVideoSending:NO forCall:call error:nil]);
    XCTAssertFalse(call.videoIsEnabled);

    OCMVerifyAll(partialMockedVideoEngine);
}

- (void)testEndCall
{
    OCMStub([self.mockedToxAV callFriendNumber:12 audioBitRate:48 videoBitRate:0 error:[OCMArg anyObjectRef]]).andReturn(YES);
    OCMStub([self.mockedToxAV sendCallControl:OCTToxAVCallControlCancel toFriendNumber:12 error:[OCMArg anyObjectRef]]).andReturn(YES);
    OCMStub([self.mockedAudioEngine stopAudioFlow:[OCMArg anyObjectRef]]).andReturn(YES);

    OCTFriend *friend = [self createFriendWithFriendNumber:12];
    OCTChat *chat = [self.realmManager getOrCreateChatWithFriend:friend];

    OCTCall *call = [self.callManager callToChat:chat enableAudio:YES enableVideo:NO error:nil];

    NSError *error;
    XCTAssertTrue([self.callManager sendCallControl:OCTToxAVCallControlCancel toCall:call error:&error]);

    OCMVerify([self.realmManager deleteObject:call]);

    XCTAssertNotNil(chat.lastMessage.messageCall);
    XCTAssertEqual(chat.lastMessage.messageCall.callEvent, OCTMessageCallEventUnanswered);
    XCTAssertTrue(chat.lastMessage.isOutgoing);
}

- (void)testAnswerCallSuccess
{
    OCMStub([self.mockedAudioEngine startAudioFlow:[OCMArg anyObjectRef]]).andReturn(YES);

    OCTFriend *friend = [self createFriendWithFriendNumber:1234];

    OCTCall *call = [self.callManager createCallWithFriend:friend status:OCTCallStatusRinging];

    OCMStub([self.mockedToxAV answerIncomingCallFromFriend:1234 audioBitRate:0 videoBitRate:0 error:[OCMArg anyObjectRef]]).andReturn(YES);

    XCTAssertTrue([self.callManager answerCall:call enableAudio:NO enableVideo:NO error:nil]);
    XCTAssertEqual(self.callManager.audioEngine.friendNumber, 1234);
}

- (void)testCallStateReceiveFinished
{
    OCMStub([self.mockedAudioEngine isAudioRunning:nil]).andReturn(NO);
    OCMStub([self.mockedAudioEngine friendNumber]).andReturn(89);

    OCTFriend *friend = [self createFriendWithFriendNumber:89];

    OCTCall *call = [self.callManager createCallWithFriend:friend status:OCTCallStatusDialing];
    [self.realmManager updateObject:call withBlock:^(OCTCall *callToUpdate) {
        callToUpdate.status = OCTCallStatusRinging;
    }];

    OCTToxAVCallState state = 0;
    state |= OCTToxAVFriendCallStateFinished;

    [self.callManager toxAV:nil callStateChanged:state friendNumber:89];

    OCTChat *chat = [self.realmManager getOrCreateChatWithFriend:friend];

    XCTAssertNotNil(chat.lastMessage.messageCall);
    XCTAssertEqual(chat.lastMessage.messageCall.callEvent, OCTMessageCallEventUnanswered);

    call = [self.callManager createCallWithFriend:friend status:OCTCallStatusRinging];
    [self.realmManager updateObject:call withBlock:^(OCTCall *callToUpdate) {
        call.status = OCTCallStatusActive;
    }];

    [self.callManager toxAV:nil callStateChanged:state friendNumber:89];

    XCTAssertEqual(chat.lastMessage.messageCall.callEvent, OCTMessageCallEventAnswered);
}

- (void)testFriendAnsweredCall
{
    id timer = OCMClassMock([OCTCallTimer class]);
    self.callManager.timer = timer;

    OCTFriend *friend = [self createFriendWithFriendNumber:92];

    OCTCall *call = [self.callManager createCallWithFriend:friend status:OCTCallStatusRinging];
    [self.realmManager updateObject:call withBlock:^(OCTCall *callToUpdate) {
        callToUpdate.status = OCTCallStatusDialing;
    }];

    OCTToxAVCallState state = 0;
    state |= OCTToxAVFriendCallStateAcceptingVideo;

    OCMStub([self.mockedAudioEngine startAudioFlow:[OCMArg anyObjectRef]]).andReturn(YES);
    [self.callManager toxAV:nil callStateChanged:state friendNumber:92];

    OCMVerify([self.mockedAudioEngine startAudioFlow:[OCMArg anyObjectRef]]);
    OCMVerify([self.mockedAudioEngine setFriendNumber:92]);
    OCMVerify([timer startTimerForCall:[OCMArg isNotNil]]);

    XCTAssertEqual(call.status, OCTCallStatusActive);
    XCTAssertTrue(call.friendAcceptingVideo);
}

- (void)testEnableMicrophone
{
    [self.callManager setEnableMicrophone:NO];

    OCMVerify([self.mockedAudioEngine setEnableMicrophone:NO]);

    XCTAssertFalse(self.callManager.enableMicrophone);
}

- (void)testTogglePauseForCall
{
    OCMStub([self.mockedToxAV sendCallControl:OCTToxAVCallControlPause toFriendNumber:12345 error:nil]).andReturn(YES);
    id partialMockedAudioEngine = OCMPartialMock([OCTAudioEngine new]);
    self.callManager.audioEngine = partialMockedAudioEngine;
    self.callManager.audioEngine.friendNumber = 12345;

    OCTFriend *friend = [self createFriendWithFriendNumber:12345];

    OCTCall *call = [self.callManager createCallWithFriend:friend status:OCTCallStatusActive];
    [self.realmManager updateObject:call withBlock:^(OCTCall *callToUpdate) {
        callToUpdate.videoIsEnabled = YES;
    }];

    OCMStub([self.mockedVideoEngine isSendingVideo]).andReturn(YES);
    OCMStub([partialMockedAudioEngine isAudioRunning:nil]).andReturn(YES);
    OCMStub([self.mockedVideoEngine stopSendingVideo]);
    XCTAssertTrue([self.callManager sendCallControl:OCTToxAVCallControlPause toCall:call error:nil]);

    OCMStub([self.mockedVideoEngine startSendingVideo]);
    OCMVerify([self.mockedVideoEngine stopSendingVideo]);
    OCMVerify([partialMockedAudioEngine stopAudioFlow:nil]);

    OCMStub([partialMockedAudioEngine isAudioRunning:nil]).andReturn(NO);
    OCMStub([self.mockedVideoEngine isSendingVideo]).andReturn(NO);

    OCMStub([self.mockedToxAV sendCallControl:OCTToxAVCallControlResume toFriendNumber:12345 error:nil]).andReturn(YES);
    OCMStub([partialMockedAudioEngine startAudioFlow:[OCMArg anyObjectRef]]).andReturn(YES);
    XCTAssertTrue([self.callManager sendCallControl:OCTToxAVCallControlResume toCall:call error:nil]);

    OCMVerify([self.mockedVideoEngine startSendingVideo]);
    OCMVerify([partialMockedAudioEngine startAudioFlow:[OCMArg anyObjectRef]]);
}

- (void)testSetAudioBitRate
{
    OCTFriend *friend = [self createFriendWithFriendNumber:123456];

    OCTCall *call = [self.callManager createCallWithFriend:friend status:OCTCallStatusActive];

    OCMStub([self.mockedToxAV setAudioBitRate:5555 force:NO forFriend:123456 error:nil]).andReturn(YES);

    XCTAssertTrue([self.callManager setAudioBitrate:5555 forCall:call error:nil]);
    OCMVerify([self.mockedToxAV setAudioBitRate:5555 force:NO forFriend:123456 error:nil]);
}

#pragma mark - Pause Scenarios

- (void)testAnsweringAnotherCallWhileActive
{
    [OCMStub([self.mockedToxAV sendCallControl:123 toFriendNumber:123 error:[OCMArg anyObjectRef]]).andReturn(YES) ignoringNonObjectArgs];
    [OCMStub([self.mockedToxAV answerIncomingCallFromFriend:123 audioBitRate:48 videoBitRate:0 error:[OCMArg anyObjectRef]]).andReturn(YES) ignoringNonObjectArgs];
    [self.mockedToxAV setExpectationOrderMatters:YES];
    OCMExpect([self.mockedToxAV sendCallControl:OCTToxAVCallControlPause toFriendNumber:4 error:[OCMArg anyObjectRef]]);
    OCMExpect([self.mockedToxAV answerIncomingCallFromFriend:5 audioBitRate:48 videoBitRate:0 error:[OCMArg anyObjectRef]]);

    OCMStub([self.callManager.audioEngine isAudioRunning:nil]).andReturn(YES);
    OCMStub([self.callManager.audioEngine stopAudioFlow:[OCMArg anyObjectRef]]).andReturn(YES);
    OCMStub([self.callManager.audioEngine startAudioFlow:[OCMArg anyObjectRef]]).andReturn(YES);

    OCTFriend *firstFriend = [self createFriendWithFriendNumber:4];
    OCTFriend *secondFriend = [self createFriendWithFriendNumber:5];
    OCTCall *firstCall = [self.callManager createCallWithFriend:firstFriend status:OCTCallStatusActive];
    self.callManager.audioEngine.friendNumber = 4;

    // create incoming call
    OCTCall *secondCall = [self.callManager createCallWithFriend:secondFriend status:OCTCallStatusRinging];

    // mock call timer
    id mockedTimer = OCMClassMock([OCTCallTimer class]);
    [mockedTimer setExpectationOrderMatters:YES];
    OCMExpect([mockedTimer stopTimer]);
    OCMExpect([mockedTimer startTimerForCall:[OCMArg isNotNil]]);

    self.callManager.timer = mockedTimer;

    [self.callManager answerCall:secondCall enableAudio:YES enableVideo:NO error:nil];

    XCTAssertEqual(secondCall.status, OCTCallStatusActive);
    XCTAssertEqual(secondCall.pausedStatus, OCTCallPausedStatusNone);
    XCTAssertEqual(self.callManager.audioEngine.friendNumber, 5);

    XCTAssertEqual(firstCall.status, OCTCallStatusActive);
    XCTAssertEqual(firstCall.pausedStatus, OCTCallPausedStatusByUser);

    OCMVerifyAll(mockedTimer);
    OCMVerifyAll(self.mockedAudioEngine);
}

- (void)testPauseControlPermissions
{
    OCTFriend *friend = [self createFriendWithFriendNumber:11];

    OCTCall *call = [self.callManager createCallWithFriend:friend status:OCTCallStatusActive];

    id mockedTimer = OCMClassMock([OCTCallTimer class]);
    [mockedTimer setExpectationOrderMatters:YES];
    self.callManager.timer = mockedTimer;
    OCMExpect([mockedTimer stopTimer]);
    OCMExpect([mockedTimer startTimerForCall:[OCMArg isNotNil]]);

    [self.callManager toxAV:nil callStateChanged:OCTToxAVFriendCallStatePaused friendNumber:11];

    XCTAssertEqual(call.pausedStatus, OCTCallPausedStatusByFriend);
    XCTAssertEqual(call.status, OCTCallStatusActive);

    id strictMockedTimer = OCMStrictClassMock([OCTCallTimer class]);
    self.callManager.timer = strictMockedTimer;

    OCMStub([self.mockedToxAV sendCallControl:OCTToxAVCallControlResume toFriendNumber:11 error:[OCMArg anyObjectRef]]).andReturn(YES);
    [self.callManager sendCallControl:OCTToxAVCallControlResume toCall:call error:nil];

    XCTAssertEqual(call.pausedStatus, OCTCallPausedStatusByFriend);

    OCMStub([self.mockedToxAV sendCallControl:OCTToxAVCallControlPause toFriendNumber:11 error:[OCMArg anyObjectRef]]).andReturn(YES);
    [self.callManager sendCallControl:OCTToxAVCallControlPause toCall:call error:nil];

    XCTAssertEqual(call.pausedStatus, OCTCallPausedStatusByFriend | OCTCallPausedStatusByUser);

    [self.callManager toxAV:nil callStateChanged:OCTToxAVFriendCallStateAcceptingAudio friendNumber:11];
    XCTAssertEqual(call.pausedStatus, OCTCallPausedStatusByUser);

    self.callManager.timer = mockedTimer;

    OCMStub([self.mockedAudioEngine startAudioFlow:[OCMArg anyObjectRef]]).andReturn(YES);
    [self.callManager sendCallControl:OCTToxAVCallControlResume toCall:call error:nil];
    XCTAssertEqual(call.pausedStatus, OCTCallPausedStatusNone);

    OCMVerifyAll(mockedTimer);
}

#pragma mark - Private
- (void)testGetOrCreateCallWithFriend
{
    OCTFriend *friend = [self createFriendWithFriendNumber:222];

    OCTChat *chat = [self.realmManager getOrCreateChatWithFriend:friend];

    OCTCall *call = [self.callManager createCallWithFriend:friend status:OCTCallStatusActive];
    OCTCall *sameCall = [self.callManager getCurrentCallForFriendNumber:222];

    XCTAssertNotNil(call.chat);
    XCTAssertEqualObjects(call.chat, chat);
    XCTAssertEqualObjects(sameCall, call);
}

 #pragma mark - Delegates

- (void)testReceiveCalls
{
    id delegate = OCMProtocolMock(@protocol(OCTSubmanagerCallDelegate));
    OCMStub([delegate respondsToSelector:[OCMArg anySelector]]).andReturn(YES);
    self.callManager.delegate = delegate;

    OCTFriend *friend = [self createFriendWithFriendNumber:221];

    OCTCall *call = [self.callManager createCallWithFriend:friend status:OCTCallStatusActive];

    [self.callManager toxAV:nil receiveCallAudioEnabled:YES videoEnabled:NO friendNumber:221];
    OCMVerify([delegate callSubmanager:self.callManager receiveCall:[OCMArg isNotNil] audioEnabled:YES videoEnabled:NO]);
    XCTAssertEqualObjects(friend, call.caller);
    XCTAssertFalse([call isOutgoing]);
}

- (void)testCallStateChanged
{
    OCTFriend *friend = [self createFriendWithFriendNumber:111];

    OCTCall *call = [self.callManager createCallWithFriend:friend status:OCTCallStatusActive];

    OCTToxAVCallState state = 0;

    state |= OCTToxAVFriendCallStateAcceptingAudio;
    state |= OCTToxAVFriendCallStateAcceptingVideo;

    [self.callManager toxAV:nil callStateChanged:state friendNumber:111];

    call = [self.callManager getCurrentCallForFriendNumber:111];

    XCTAssertTrue(call.friendAcceptingAudio);
    XCTAssertTrue(call.friendAcceptingVideo);
    XCTAssertFalse(call.friendSendingAudio);
    XCTAssertFalse(call.friendSendingVideo);
}

- (void)testReceiveAudio
{
    OCTToxAVPCMData pcm[] = { 1, 2, 3, 4};

    [self.callManager toxAV:nil receiveAudio:pcm sampleCount:4 channels:2 sampleRate:55 friendNumber:123];

    OCMVerify([self.mockedAudioEngine provideAudioFrames:pcm sampleCount:4 channels:2 sampleRate:55 fromFriend:123]);
}

- (void)testReceiveVideo
{
    OCTToxAVVideoHeight height = 1920;
    OCTToxAVVideoHeight width = 1080;
    OCTToxAVPlaneData y[] = {5, 5, 6, 8};
    OCTToxAVPlaneData u[] = {5, 5, 6, 8};
    OCTToxAVPlaneData v[] = {5, 5, 6, 8};
    OCTToxAVStrideData yStride = 44;
    OCTToxAVStrideData uStride = 45;
    OCTToxAVStrideData vStride = 46;

    [self.callManager toxAV:nil
 receiveVideoFrameWithWidth:width
                     height:height
                     yPlane:y
                     uPlane:u
                     vPlane:v
                    yStride:yStride
                    uStride:uStride
                    vStride:vStride
               friendNumber:444];

    OCMVerify([self.mockedVideoEngine receiveVideoFrameWithWidth:width
                                                          height:height
                                                          yPlane:y
                                                          uPlane:u
                                                          vPlane:v
                                                         yStride:yStride
                                                         uStride:uStride
                                                         vStride:vStride
                                                    friendNumber:444]);
}

#pragma mark Test helper methods

- (OCTFriend *)createFriendWithFriendNumber:(OCTToxFriendNumber)friendNumber
{
    OCTFriend *friend = [super createFriendWithFriendNumber:friendNumber];
    friend.friendNumber = friendNumber;

    [self.realmManager.realm beginWriteTransaction];
    [self.realmManager.realm addObject:friend];
    [self.realmManager.realm commitWriteTransaction];

    NSString *publicKey = friend.publicKey;
    OCMStub([self.tox publicKeyFromFriendNumber:friendNumber error:nil]).andReturn(publicKey);

    return friend;
}

@end
