/*
 * libjingle
 * Copyright 2013, Google Inc.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *  3. The name of the author may not be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#import <Foundation/Foundation.h>

#import "RTCICEServer.h"
#import "RTCMediaConstraints.h"
#import "RTCMediaStream.h"
#import "RTCPair.h"
#import "RTCPeerConnection.h"
#import "RTCPeerConnectionFactory.h"
#import "RTCPeerConnectionSyncObserver.h"
#import "RTCSessionDescription.h"
#import "RTCSessionDescriptionSyncObserver.h"
#import "RTCVideoRenderer.h"
#import "RTCVideoTrack.h"

#include "webrtc/base/gunit.h"
#include "webrtc/base/ssladapter.h"

#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif

@interface RTCPeerConnectionTest : NSObject

// Returns whether the two sessions are of the same type.
+ (BOOL)isSession:(RTCSessionDescription*)session1
    ofSameTypeAsSession:(RTCSessionDescription*)session2;

// Create and add tracks to pc, with the given source, label, and IDs
- (RTCMediaStream*)addTracksToPeerConnection:(RTCPeerConnection*)pc
                                 withFactory:(RTCPeerConnectionFactory*)factory
                                 videoSource:(RTCVideoSource*)videoSource
                                 streamLabel:(NSString*)streamLabel
                                videoTrackID:(NSString*)videoTrackID
                                audioTrackID:(NSString*)audioTrackID;

- (void)testCompleteSessionWithFactory:(RTCPeerConnectionFactory*)factory;

@end

@implementation RTCPeerConnectionTest

+ (BOOL)isSession:(RTCSessionDescription*)session1
    ofSameTypeAsSession:(RTCSessionDescription*)session2 {
  return [session1.type isEqual:session2.type];
}

- (RTCMediaStream*)addTracksToPeerConnection:(RTCPeerConnection*)pc
                                 withFactory:(RTCPeerConnectionFactory*)factory
                                 videoSource:(RTCVideoSource*)videoSource
                                 streamLabel:(NSString*)streamLabel
                                videoTrackID:(NSString*)videoTrackID
                                audioTrackID:(NSString*)audioTrackID {
  RTCMediaStream* localMediaStream = [factory mediaStreamWithLabel:streamLabel];
  RTCVideoTrack* videoTrack =
      [factory videoTrackWithID:videoTrackID source:videoSource];
  RTCVideoRenderer* videoRenderer =
      [[RTCVideoRenderer alloc] initWithDelegate:nil];
  [videoTrack addRenderer:videoRenderer];
  [localMediaStream addVideoTrack:videoTrack];
  // Test that removal/re-add works.
  [localMediaStream removeVideoTrack:videoTrack];
  [localMediaStream addVideoTrack:videoTrack];
  RTCAudioTrack* audioTrack = [factory audioTrackWithID:audioTrackID];
  [localMediaStream addAudioTrack:audioTrack];
  RTCMediaConstraints* constraints = [[RTCMediaConstraints alloc] init];
  [pc addStream:localMediaStream constraints:constraints];
  return localMediaStream;
}

- (void)testCompleteSessionWithFactory:(RTCPeerConnectionFactory*)factory {
  NSArray* mandatory = @[
    [[RTCPair alloc] initWithKey:@"DtlsSrtpKeyAgreement" value:@"true"],
    [[RTCPair alloc] initWithKey:@"internalSctpDataChannels" value:@"true"],
  ];
  RTCMediaConstraints* constraints = [[RTCMediaConstraints alloc] init];
  RTCMediaConstraints* pcConstraints =
      [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatory
                                            optionalConstraints:nil];

  RTCPeerConnectionSyncObserver* offeringExpectations =
      [[RTCPeerConnectionSyncObserver alloc] init];
  RTCPeerConnection* pcOffer =
      [factory peerConnectionWithICEServers:nil
                                constraints:pcConstraints
                                   delegate:offeringExpectations];

  RTCPeerConnectionSyncObserver* answeringExpectations =
      [[RTCPeerConnectionSyncObserver alloc] init];

  RTCPeerConnection* pcAnswer =
      [factory peerConnectionWithICEServers:nil
                                constraints:pcConstraints
                                   delegate:answeringExpectations];
  // TODO(hughv): Create video capturer
  RTCVideoCapturer* capturer = nil;
  RTCVideoSource* videoSource =
      [factory videoSourceWithCapturer:capturer constraints:constraints];

  // Here and below, "oLMS" refers to offerer's local media stream, and "aLMS"
  // refers to the answerer's local media stream, with suffixes of "a0" and "v0"
  // for audio and video tracks, resp.  These mirror chrome historical naming.
  RTCMediaStream* oLMSUnused = [self addTracksToPeerConnection:pcOffer
                                                   withFactory:factory
                                                   videoSource:videoSource
                                                   streamLabel:@"oLMS"
                                                  videoTrackID:@"oLMSv0"
                                                  audioTrackID:@"oLMSa0"];

  RTCDataChannel* offerDC =
      [pcOffer createDataChannelWithLabel:@"offerDC"
                                   config:[[RTCDataChannelInit alloc] init]];
  EXPECT_TRUE([offerDC.label isEqual:@"offerDC"]);
  offerDC.delegate = offeringExpectations;
  offeringExpectations.dataChannel = offerDC;

  RTCSessionDescriptionSyncObserver* sdpObserver =
      [[RTCSessionDescriptionSyncObserver alloc] init];
  [pcOffer createOfferWithDelegate:sdpObserver constraints:constraints];
  [sdpObserver wait];
  EXPECT_TRUE(sdpObserver.success);
  RTCSessionDescription* offerSDP = sdpObserver.sessionDescription;
  EXPECT_EQ([@"offer" compare:offerSDP.type options:NSCaseInsensitiveSearch],
            NSOrderedSame);
  EXPECT_GT([offerSDP.description length], 0);

  sdpObserver = [[RTCSessionDescriptionSyncObserver alloc] init];
  [answeringExpectations expectSignalingChange:RTCSignalingHaveRemoteOffer];
  [answeringExpectations expectAddStream:@"oLMS"];
  [pcAnswer setRemoteDescriptionWithDelegate:sdpObserver
                          sessionDescription:offerSDP];
  [sdpObserver wait];

  RTCMediaStream* aLMSUnused = [self addTracksToPeerConnection:pcAnswer
                                                   withFactory:factory
                                                   videoSource:videoSource
                                                   streamLabel:@"aLMS"
                                                  videoTrackID:@"aLMSv0"
                                                  audioTrackID:@"aLMSa0"];

  sdpObserver = [[RTCSessionDescriptionSyncObserver alloc] init];
  [pcAnswer createAnswerWithDelegate:sdpObserver constraints:constraints];
  [sdpObserver wait];
  EXPECT_TRUE(sdpObserver.success);
  RTCSessionDescription* answerSDP = sdpObserver.sessionDescription;
  EXPECT_EQ([@"answer" compare:answerSDP.type options:NSCaseInsensitiveSearch],
            NSOrderedSame);
  EXPECT_GT([answerSDP.description length], 0);

  [offeringExpectations expectICECandidates:2];
  [answeringExpectations expectICECandidates:2];

  sdpObserver = [[RTCSessionDescriptionSyncObserver alloc] init];
  [answeringExpectations expectSignalingChange:RTCSignalingStable];
  [pcAnswer setLocalDescriptionWithDelegate:sdpObserver
                         sessionDescription:answerSDP];
  [sdpObserver wait];
  EXPECT_TRUE(sdpObserver.sessionDescription == NULL);

  sdpObserver = [[RTCSessionDescriptionSyncObserver alloc] init];
  [offeringExpectations expectSignalingChange:RTCSignalingHaveLocalOffer];
  [pcOffer setLocalDescriptionWithDelegate:sdpObserver
                        sessionDescription:offerSDP];
  [sdpObserver wait];
  EXPECT_TRUE(sdpObserver.sessionDescription == NULL);

  [offeringExpectations expectICEConnectionChange:RTCICEConnectionChecking];
  [offeringExpectations expectICEConnectionChange:RTCICEConnectionConnected];
  // TODO(fischman): figure out why this is flaky and re-introduce (and remove
  // special-casing from the observer!).
  // [offeringExpectations expectICEConnectionChange:RTCICEConnectionCompleted];
  [answeringExpectations expectICEConnectionChange:RTCICEConnectionChecking];
  [answeringExpectations expectICEConnectionChange:RTCICEConnectionConnected];

  [offeringExpectations expectStateChange:kRTCDataChannelStateOpen];
  [answeringExpectations expectDataChannel:@"offerDC"];
  [answeringExpectations expectStateChange:kRTCDataChannelStateOpen];

  [offeringExpectations expectICEGatheringChange:RTCICEGatheringComplete];
  [answeringExpectations expectICEGatheringChange:RTCICEGatheringComplete];

  sdpObserver = [[RTCSessionDescriptionSyncObserver alloc] init];
  [offeringExpectations expectSignalingChange:RTCSignalingStable];
  [offeringExpectations expectAddStream:@"aLMS"];
  [pcOffer setRemoteDescriptionWithDelegate:sdpObserver
                         sessionDescription:answerSDP];
  [sdpObserver wait];
  EXPECT_TRUE(sdpObserver.sessionDescription == NULL);

  EXPECT_TRUE([offerSDP.type isEqual:pcOffer.localDescription.type]);
  EXPECT_TRUE([answerSDP.type isEqual:pcOffer.remoteDescription.type]);
  EXPECT_TRUE([offerSDP.type isEqual:pcAnswer.remoteDescription.type]);
  EXPECT_TRUE([answerSDP.type isEqual:pcAnswer.localDescription.type]);

  for (RTCICECandidate* candidate in offeringExpectations
           .releaseReceivedICECandidates) {
    [pcAnswer addICECandidate:candidate];
  }
  for (RTCICECandidate* candidate in answeringExpectations
           .releaseReceivedICECandidates) {
    [pcOffer addICECandidate:candidate];
  }

  [offeringExpectations waitForAllExpectationsToBeSatisfied];
  [answeringExpectations waitForAllExpectationsToBeSatisfied];

  EXPECT_EQ(pcOffer.signalingState, RTCSignalingStable);
  EXPECT_EQ(pcAnswer.signalingState, RTCSignalingStable);

  // Test send and receive UTF-8 text
  NSString* text = @"你好";
  NSData* textData = [text dataUsingEncoding:NSUTF8StringEncoding];
  RTCDataBuffer* buffer =
      [[RTCDataBuffer alloc] initWithData:textData isBinary:NO];
  [answeringExpectations expectMessage:[textData copy] isBinary:NO];
  EXPECT_TRUE([offeringExpectations.dataChannel sendData:buffer]);
  [answeringExpectations waitForAllExpectationsToBeSatisfied];

  // Test send and receive binary data
  const size_t byteLength = 5;
  char bytes[byteLength] = {1, 2, 3, 4, 5};
  NSData* byteData = [NSData dataWithBytes:bytes length:byteLength];
  buffer = [[RTCDataBuffer alloc] initWithData:byteData isBinary:YES];
  [answeringExpectations expectMessage:[byteData copy] isBinary:YES];
  EXPECT_TRUE([offeringExpectations.dataChannel sendData:buffer]);
  [answeringExpectations waitForAllExpectationsToBeSatisfied];

  [offeringExpectations expectStateChange:kRTCDataChannelStateClosing];
  [answeringExpectations expectStateChange:kRTCDataChannelStateClosing];
  [offeringExpectations expectStateChange:kRTCDataChannelStateClosed];
  [answeringExpectations expectStateChange:kRTCDataChannelStateClosed];

  [answeringExpectations.dataChannel close];
  [offeringExpectations.dataChannel close];

  [offeringExpectations waitForAllExpectationsToBeSatisfied];
  [answeringExpectations waitForAllExpectationsToBeSatisfied];
  // Don't need to listen to further state changes.
  // TODO(tkchin): figure out why Closed->Closing without this.
  offeringExpectations.dataChannel.delegate = nil;
  answeringExpectations.dataChannel.delegate = nil;

  // Let the audio feedback run for 2s to allow human testing and to ensure
  // things stabilize.  TODO(fischman): replace seconds with # of video frames,
  // when we have video flowing.
  [[NSRunLoop currentRunLoop]
      runUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]];

  [offeringExpectations expectICEConnectionChange:RTCICEConnectionClosed];
  [answeringExpectations expectICEConnectionChange:RTCICEConnectionClosed];
  [offeringExpectations expectSignalingChange:RTCSignalingClosed];
  [answeringExpectations expectSignalingChange:RTCSignalingClosed];

  [pcOffer close];
  [pcAnswer close];

  [offeringExpectations waitForAllExpectationsToBeSatisfied];
  [answeringExpectations waitForAllExpectationsToBeSatisfied];

  capturer = nil;
  videoSource = nil;
  pcOffer = nil;
  pcAnswer = nil;
  // TODO(fischman): be stricter about shutdown checks; ensure thread
  // counts return to where they were before the test kicked off, and
  // that all objects have in fact shut down.
}

@end

// TODO(fischman): move {Initialize,Cleanup}SSL into alloc/dealloc of
// RTCPeerConnectionTest and avoid the appearance of RTCPeerConnectionTest being
// a TestBase since it's not.
TEST(RTCPeerConnectionTest, SessionTest) {
  @autoreleasepool {
    rtc::InitializeSSL();
    // Since |factory| will own the signaling & worker threads, it's important
    // that it outlive the created PeerConnections since they self-delete on the
    // signaling thread, and if |factory| is freed first then a last refcount on
    // the factory will expire during this teardown, causing the signaling
    // thread to try to Join() with itself.  This is a hack to ensure that the
    // factory outlives RTCPeerConnection:dealloc.
    // See https://code.google.com/p/webrtc/issues/detail?id=3100.
    RTCPeerConnectionFactory* factory = [[RTCPeerConnectionFactory alloc] init];
    @autoreleasepool {
      RTCPeerConnectionTest* pcTest = [[RTCPeerConnectionTest alloc] init];
      [pcTest testCompleteSessionWithFactory:factory];
    }
    rtc::CleanupSSL();
  }
}
