/*
 * Copyright 2017 Google
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

@import XCTest;

#import <OCMock/OCMock.h>

#import "Protos/GtalkCore.pbobjc.h"
#import <GoogleToolboxForMac/GTMDefines.h>

#import "FIRMessagingClient.h"
#import "FIRMessagingConnection.h"
#import "FIRMessagingDataMessageManager.h"
#import "FIRMessagingFakeConnection.h"
#import "FIRMessagingRmqManager.h"
#import "FIRMessagingSecureSocket.h"
#import "FIRMessagingUtilities.h"

static NSString *const kDeviceAuthId = @"123456";
static NSString *const kSecretToken = @"56789";

// used to verify if we are sending in the right proto or not.
// set it to negative value to disable this check
static FIRMessagingProtoTag currentProtoSendTag;

@interface FIRMessagingSecureSocket ()

@property(nonatomic, readwrite, assign) FIRMessagingSecureSocketState state;

@end

@interface FIRMessagingSecureSocket (test_FIRMessagingConnection)

- (void)_successconnectToHost:(NSString *)host
                         port:(NSUInteger)port
                    onRunLoop:(NSRunLoop *)runLoop;
- (void)_fakeSuccessfulSocketConnect;

@end

@implementation FIRMessagingSecureSocket (test_FIRMessagingConnection)

- (void)_successconnectToHost:(NSString *)host
                         port:(NSUInteger)port
                    onRunLoop:(NSRunLoop *)runLoop {
  // created ports, opened streams
  // invoke callback async
  [self _fakeSuccessfulSocketConnect];
}

- (void)_fakeSuccessfulSocketConnect {
  self.state = kFIRMessagingSecureSocketOpen;
  [self.delegate secureSocketDidConnect:self];
}

@end

// make sure these are defined in FIRMessagingConnection
@interface FIRMessagingConnection () <FIRMessagingSecureSocketDelegate>

@property(nonatomic, readwrite, assign) int64_t lastLoginServerTimestamp;
@property(nonatomic, readwrite, assign) int lastStreamIdAcked;
@property(nonatomic, readwrite, assign) int inStreamId;
@property(nonatomic, readwrite, assign) int outStreamId;

@property(nonatomic, readwrite, strong) FIRMessagingSecureSocket *socket;

@property(nonatomic, readwrite, strong) NSMutableArray *unackedS2dIds;
@property(nonatomic, readwrite, strong) NSMutableDictionary *ackedS2dMap;
@property(nonatomic, readwrite, strong) NSMutableArray *d2sInfos;

- (void)setupConnectionSocket;
- (void)connectToSocket:(FIRMessagingSecureSocket *)socket;
- (NSTimeInterval)connectionTimeoutInterval;
- (void)sendHeartbeatPing;

@end


@interface FIRMessagingConnectionTest : XCTestCase

@property(nonatomic, readwrite, assign) BOOL didSuccessfullySendData;

@property(nonatomic, readwrite, strong) NSUserDefaults *userDefaults;
@property(nonatomic, readwrite, strong) FIRMessagingConnection *fakeConnection;
@property(nonatomic, readwrite, strong) id mockClient;
@property(nonatomic, readwrite, strong) id mockConnection;
@property(nonatomic, readwrite, strong) id mockRmq;
@property(nonatomic, readwrite, strong) id mockDataMessageManager;

@end

@implementation FIRMessagingConnectionTest

- (void)setUp {
  [super setUp];
  _userDefaults = [[NSUserDefaults alloc] init];
  _mockRmq = OCMClassMock([FIRMessagingRmqManager class]);
  _mockDataMessageManager = OCMClassMock([FIRMessagingDataMessageManager class]);
  // fake connection is only used to simulate the socket behavior
  _fakeConnection = [[FIRMessagingFakeConnection alloc] initWithAuthID:kDeviceAuthId
                                                        token:kSecretToken
                                                         host:[FIRMessagingFakeConnection fakeHost]
                                                         port:[FIRMessagingFakeConnection fakePort]
                                                      runLoop:[NSRunLoop currentRunLoop]
                                                  rmq2Manager:_mockRmq
                                                   fcmManager:_mockDataMessageManager];

  _mockClient = OCMClassMock([FIRMessagingClient class]);
  _fakeConnection.delegate = _mockClient;
  _mockConnection = OCMPartialMock(_fakeConnection);
  _didSuccessfullySendData = NO;
}

- (void)tearDown {
  [self.fakeConnection teardown];
  [super tearDown];
}

- (void)testInitialConnectionNotConnected {
  XCTAssertEqual(kFIRMessagingConnectionNotConnected, [self.fakeConnection state]);
}

- (void)testSuccessfulSocketConnection {
  [self.fakeConnection signIn];

  // should be connected now
  XCTAssertEqual(kFIRMessagingConnectionConnected, self.fakeConnection.state);
  XCTAssertEqual(0, self.fakeConnection.lastStreamIdAcked);
  XCTAssertEqual(0, self.fakeConnection.inStreamId);
  XCTAssertEqual(0, self.fakeConnection.ackedS2dMap.count);
  XCTAssertEqual(0, self.fakeConnection.unackedS2dIds.count);

  [self stubSocketDisconnect:self.fakeConnection.socket];
}

- (void)testSignInAndThenSignOut {
  [self.fakeConnection signIn];
  [self stubSocketDisconnect:self.fakeConnection.socket];
  [self.fakeConnection signOut];
  XCTAssertEqual(kFIRMessagingSecureSocketClosed, self.fakeConnection.socket.state);
}

- (void)testSuccessfulSignIn {
  [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
  XCTAssertEqual(self.fakeConnection.state, kFIRMessagingConnectionSignedIn);
  XCTAssertEqual(self.fakeConnection.outStreamId, 2);
  XCTAssertTrue(self.didSuccessfullySendData);
}

- (void)testSignOut_whenSignedIn {
  [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];

  // should be signed in now
  id mockSocket = self.fakeConnection.socket;
  [self.fakeConnection signOut];
  XCTAssertEqual(self.fakeConnection.state, kFIRMessagingConnectionNotConnected);
  XCTAssertEqual(self.fakeConnection.outStreamId, 3);
  XCTAssertNil([(FIRMessagingSecureSocket *)mockSocket delegate]);
  XCTAssertTrue(self.didSuccessfullySendData);
  OCMVerify([mockSocket sendData:[OCMArg any]
                         withTag:kFIRMessagingProtoTagClose
                           rmqId:[OCMArg isNil]]);
}

- (void)testReceiveCloseProto {
  [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];

  id mockSocket = self.fakeConnection.socket;
  GtalkClose *close = [[GtalkClose alloc] init];
  [self.fakeConnection secureSocket:mockSocket
                     didReceiveData:[close data]
                            withTag:kFIRMessagingProtoTagClose];
  XCTAssertEqual(self.fakeConnection.state, kFIRMessagingConnectionNotConnected);
  XCTAssertTrue(self.didSuccessfullySendData);
}

- (void)testLoginRequest {
  XCTAssertEqual(kFIRMessagingConnectionNotConnected, [self.fakeConnection state]);
  [self.fakeConnection setupConnectionSocket];

  id socketMock = OCMPartialMock(self.fakeConnection.socket);
  self.fakeConnection.socket = socketMock;

  [[[socketMock stub]
      andDo:^(NSInvocation *invocation) {
        [socketMock _fakeSuccessfulSocketConnect];
      }]
      connectToHost:[FIRMessagingFakeConnection fakeHost]
               port:[FIRMessagingFakeConnection fakePort]
          onRunLoop:[OCMArg any]];

  [[[socketMock stub] andCall:@selector(_sendData:withTag:rmqId:) onObject:self]
      // do nothing
      sendData:[OCMArg any]
       withTag:kFIRMessagingProtoTagLoginRequest
         rmqId:[OCMArg isNil]];

  // swizzle disconnect socket
  OCMVerify([[[socketMock stub] andCall:@selector(_disconnectSocket)
                               onObject:self] disconnect]);

  currentProtoSendTag = kFIRMessagingProtoTagLoginRequest;
  // send login request
  [self.fakeConnection connectToSocket:socketMock];

  // verify login request sent
  XCTAssertEqual(1, self.fakeConnection.outStreamId);
  XCTAssertTrue(self.didSuccessfullySendData);
}

- (void)testLoginRequest_withPendingMessagesInRmq {
  // TODO: add fake messages to rmq and test login request with them
}

- (void)testLoginRequest_withSuccessfulResponse {
  [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];

  OCMVerify([self.mockClient didLoginWithConnection:[OCMArg isEqual:self.fakeConnection]]);

  // should send a heartbeat ping too
  XCTAssertEqual(self.fakeConnection.outStreamId, 2);
  // update for the received login response proto
  XCTAssertEqual(self.fakeConnection.inStreamId, 1);
  // did send data during login
  XCTAssertTrue(self.didSuccessfullySendData);
}

- (void)testConnectionTimeout {
  XCTAssertEqual(kFIRMessagingConnectionNotConnected, [self.fakeConnection state]);

  [self.fakeConnection setupConnectionSocket];

  id socketMock = OCMPartialMock(self.fakeConnection.socket);
  self.fakeConnection.socket = socketMock;

  [[[socketMock stub]
      andDo:^(NSInvocation *invocation) {
        [socketMock _fakeSuccessfulSocketConnect];
      }]
      connectToHost:[FIRMessagingFakeConnection fakeHost]
               port:[FIRMessagingFakeConnection fakePort]
          onRunLoop:[OCMArg any]];

  [self.fakeConnection connectToSocket:socketMock];
  XCTAssertEqual(self.fakeConnection.state, kFIRMessagingConnectionConnected);

  GtalkLoginResponse *response = [[GtalkLoginResponse alloc] init];
  [response setId_p:@""];

  // connection timeout has been scheduled
  // should disconnect since we wait for more time
  XCTestExpectation *disconnectExpectation =
      [self expectationWithDescription:
          @"FCM connection should timeout without receiving "
          @"any data for a timeout interval"];
  [[[socketMock stub]
      andDo:^(NSInvocation *invocation) {
        [self _disconnectSocket];
        [disconnectExpectation fulfill];
      }] disconnect];

  // simulate connection receiving login response
  [self.fakeConnection secureSocket:socketMock
                     didReceiveData:[response data]
                            withTag:kFIRMessagingProtoTagLoginResponse];

  [self waitForExpectationsWithTimeout:2.0
                               handler:^(NSError *error) {
                                 XCTAssertNil(error);
                               }];

  [socketMock verify];
  XCTAssertEqual(self.fakeConnection.state, kFIRMessagingConnectionNotConnected);
}

- (void)testDataMessageReceive {
  [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
  GtalkDataMessageStanza *stanza = [[GtalkDataMessageStanza alloc] init];
  [stanza setCategory:@"special"];
  [stanza setFrom:@"xyz"];
  [self.fakeConnection secureSocket:self.fakeConnection.socket
                     didReceiveData:[stanza data]
                            withTag:kFIRMessagingProtoTagDataMessageStanza];

  OCMVerify([self.mockClient connectionDidRecieveMessage:[OCMArg checkWithBlock:^BOOL(id obj) {
      GtalkDataMessageStanza *message = (GtalkDataMessageStanza *)obj;
      return [[message category] isEqual:@"special"] && [[message from] isEqual:@"xyz"];
  }]]);
  // did send data while login
  XCTAssertTrue(self.didSuccessfullySendData);
}

- (void)testDataMessageReceiveWithInvalidTag {
  [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
  GtalkDataMessageStanza *stanza = [[GtalkDataMessageStanza alloc] init];
  BOOL didCauseException = NO;
  @try {
    [self.fakeConnection secureSocket:self.fakeConnection.socket
                       didReceiveData:[stanza data]
                              withTag:kFIRMessagingProtoTagInvalid];
  } @catch (NSException *exception) {
    didCauseException = YES;
  } @finally {
  }
  XCTAssertFalse(didCauseException);
}

- (void)testDataMessageReceiveWithTagThatDoesntEquateToClass {
  [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
  GtalkDataMessageStanza *stanza = [[GtalkDataMessageStanza alloc] init];
  BOOL didCauseException = NO;
  int8_t tagWhichDoesntEquateToClass = INT8_MAX;
  @try {
    [self.fakeConnection secureSocket:self.fakeConnection.socket
                       didReceiveData:[stanza data]
                              withTag:tagWhichDoesntEquateToClass];
  } @catch (NSException *exception) {
    didCauseException = YES;
  } @finally {
  }
  XCTAssertFalse(didCauseException);
}

- (void)testHeartbeatSend {
  [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection]; // outstreamId should be 2
  XCTAssertEqual(self.fakeConnection.outStreamId, 2);
  [self.fakeConnection sendHeartbeatPing];
  id mockSocket = self.fakeConnection.socket;
  OCMVerify([mockSocket sendData:[OCMArg any]
                         withTag:kFIRMessagingProtoTagHeartbeatPing
                           rmqId:[OCMArg isNil]]);
  XCTAssertEqual(self.fakeConnection.outStreamId, 3);
  // did send data
  XCTAssertTrue(self.didSuccessfullySendData);
}

- (void)testHeartbeatReceived {
  [self setupSuccessfulLoginRequestWithConnection:self.fakeConnection];
  XCTAssertEqual(self.fakeConnection.outStreamId, 2);
  GtalkHeartbeatPing *ping = [[GtalkHeartbeatPing alloc] init];
  [self.fakeConnection secureSocket:self.fakeConnection.socket
                     didReceiveData:[ping data]
                            withTag:kFIRMessagingProtoTagHeartbeatPing];
  XCTAssertEqual(self.fakeConnection.inStreamId, 2);
  id mockSocket = self.fakeConnection.socket;
  OCMVerify([mockSocket sendData:[OCMArg any]
                         withTag:kFIRMessagingProtoTagHeartbeatAck
                           rmqId:[OCMArg isNil]]);
  XCTAssertEqual(self.fakeConnection.outStreamId, 3);
  // did send data
  XCTAssertTrue(self.didSuccessfullySendData);
}

// TODO: Add tests for Selective/Stream ACK's

#pragma mark - Stubs

- (void)_disconnectSocket {
  self.fakeConnection.socket.state = kFIRMessagingSecureSocketClosed;
}

- (void)_sendData:(NSData *)data withTag:(int8_t)tag rmqId:(NSString *)rmqId {
  _GTMDevLog(@"FIRMessaging Socket: Send data with Tag: %d rmq: %@", tag, rmqId);
  if (currentProtoSendTag > 0) {
    XCTAssertEqual(tag, currentProtoSendTag);
  }
  self.didSuccessfullySendData = YES;
}

#pragma mark - Private Helpers

/**
 * Stub socket disconnect to prevent spurious assert. Since we mock the socket object being
 * used by the connection, while we teardown the client we also disconnect the socket to tear
 * it down. Since we are using mock sockets we need to stub the `disconnect` to prevent some
 * assertions from taking place.
 * The `_disconectSocket` has the gist of the actual socket disconnect without any assertions.
 */
- (void)stubSocketDisconnect:(id)mockSocket {
  [[[mockSocket stub] andCall:@selector(_disconnectSocket)
                     onObject:self] disconnect];

  [mockSocket verify];
}

- (void)mockSuccessfulSignIn {
  XCTAssertEqual(kFIRMessagingConnectionNotConnected, [self.fakeConnection state]);
  [self.fakeConnection setupConnectionSocket];

  id socketMock = OCMPartialMock(self.fakeConnection.socket);
  self.fakeConnection.socket = socketMock;

  [[[socketMock stub]
      andDo:^(NSInvocation *invocation) {
        [socketMock _fakeSuccessfulSocketConnect];
      }]
      connectToHost:[FIRMessagingFakeConnection fakeHost]
               port:[FIRMessagingFakeConnection fakePort]
          onRunLoop:[OCMArg any]];

  [[[socketMock stub] andCall:@selector(_sendData:withTag:rmqId:) onObject:self]
      // do nothing
      sendData:[OCMArg any]
       withTag:kFIRMessagingProtoTagLoginRequest
         rmqId:[OCMArg isNil]];

  // send login request
  currentProtoSendTag = kFIRMessagingProtoTagLoginRequest;
  [self.fakeConnection connectToSocket:socketMock];

  GtalkLoginResponse *response = [[GtalkLoginResponse alloc] init];
  [response setId_p:@""];

  // simulate connection receiving login response
  [self.fakeConnection secureSocket:socketMock
                     didReceiveData:[response data]
                            withTag:kFIRMessagingProtoTagLoginResponse];

  OCMVerify([self.mockClient didLoginWithConnection:[OCMArg isEqual:self.fakeConnection]]);

  // should receive data
  XCTAssertTrue(self.didSuccessfullySendData);
  // should send a heartbeat ping too
  XCTAssertEqual(self.fakeConnection.outStreamId, 2);
  // update for the received login response proto
  XCTAssertEqual(self.fakeConnection.inStreamId, 1);
}

- (void)setupSuccessfulLoginRequestWithConnection:(FIRMessagingConnection *)fakeConnection {
  [fakeConnection setupConnectionSocket];

  id socketMock = OCMPartialMock(fakeConnection.socket);
  fakeConnection.socket = socketMock;

  [[[socketMock stub]
      andDo:^(NSInvocation *invocation) {
        [socketMock _fakeSuccessfulSocketConnect];
      }]
      connectToHost:[FIRMessagingFakeConnection fakeHost]
               port:[FIRMessagingFakeConnection fakePort]
          onRunLoop:[OCMArg any]];

  [[[socketMock stub] andCall:@selector(_sendData:withTag:rmqId:) onObject:self]
       // do nothing
       sendData:[OCMArg any]
        withTag:kFIRMessagingProtoTagLoginRequest
          rmqId:[OCMArg isNil]];

  // swizzle disconnect socket
  [[[socketMock stub] andCall:@selector(_disconnectSocket)
                     onObject:self] disconnect];

  // send login request
  currentProtoSendTag = kFIRMessagingProtoTagLoginRequest;
  [fakeConnection connectToSocket:socketMock];

  GtalkLoginResponse *response = [[GtalkLoginResponse alloc] init];
  [response setId_p:@""];

  // simulate connection receiving login response
  [fakeConnection secureSocket:socketMock
                didReceiveData:[response data]
                       withTag:kFIRMessagingProtoTagLoginResponse];
}

@end
