/*
 *
 * Copyright 2015 gRPC authors.
 *
 * 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/XCTest.h>
#import <grpc/grpc.h>
#import <grpc/support/port_platform.h>

#import <GRPCClient/GRPCCall+ChannelArg.h>
#import <GRPCClient/GRPCCall+OAuth2.h>
#import <GRPCClient/GRPCCall+Tests.h>
#import <GRPCClient/GRPCCall.h>
#import <GRPCClient/internal_testing/GRPCCall+InternalTests.h>
#import <ProtoRPC/ProtoMethod.h>
#import <RxLibrary/GRXBufferedPipe.h>
#import <RxLibrary/GRXWriteable.h>
#import <RxLibrary/GRXWriter+Immediate.h>
#import "src/objective-c/tests/RemoteTestClient/Messages.pbobjc.h"

#include <netinet/in.h>

#import "../Common/TestUtils.h"
#import "../version.h"

#define TEST_TIMEOUT 16

// The server address is derived from preprocessor macro, which is
// in turn derived from environment variable of the same name.
#define NSStringize_helper(x) #x
#define NSStringize(x) @NSStringize_helper(x)
static NSString *const kPackage = @"grpc.testing";
static NSString *const kService = @"TestService";

static GRPCProtoMethod *kInexistentMethod;
static GRPCProtoMethod *kEmptyCallMethod;
static GRPCProtoMethod *kUnaryCallMethod;
static GRPCProtoMethod *kFullDuplexCallMethod;

/** Observer class for testing that responseMetadata is KVO-compliant */
@interface PassthroughObserver : NSObject
- (instancetype)initWithCallback:(void (^)(NSString *, id, NSDictionary *))callback
    NS_DESIGNATED_INITIALIZER;

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context;
@end

@implementation PassthroughObserver {
  void (^_callback)(NSString *, id, NSDictionary *);
}

- (instancetype)init {
  return [self initWithCallback:nil];
}

- (instancetype)initWithCallback:(void (^)(NSString *, id, NSDictionary *))callback {
  if (!callback) {
    return nil;
  }
  if ((self = [super init])) {
    _callback = callback;
  }
  return self;
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
  _callback(keyPath, object, change);
  [object removeObserver:self forKeyPath:keyPath];
}

@end

#pragma mark Tests

/**
 * A few tests similar to InteropTests, but which use the generic gRPC client (GRPCCall) rather than
 * a generated proto library on top of it. Its RPCs are sent to a local cleartext server.
 *
 * TODO(jcanizales): Run them also against a local SSL server and against a remote server.
 */
@interface GRPCClientTests : XCTestCase
@end

@implementation GRPCClientTests

+ (void)setUp {
  NSLog(@"GRPCClientTests Started");
}

- (void)setUp {
  // Add a custom user agent prefix and suffix that will be used in test
  [GRPCCall setUserAgentPrefix:@"Foo" forHost:GRPCGetLocalInteropTestServerAddressPlainText()];
  [GRPCCall setUserAgentSuffix:@"Suffix" forHost:GRPCGetLocalInteropTestServerAddressPlainText()];
  // Register test server as non-SSL.
  [GRPCCall useInsecureConnectionsForHost:GRPCGetLocalInteropTestServerAddressPlainText()];

  // This method isn't implemented by the remote server.
  kInexistentMethod = [[GRPCProtoMethod alloc] initWithPackage:kPackage
                                                       service:kService
                                                        method:@"Inexistent"];
  kEmptyCallMethod = [[GRPCProtoMethod alloc] initWithPackage:kPackage
                                                      service:kService
                                                       method:@"EmptyCall"];
  kUnaryCallMethod = [[GRPCProtoMethod alloc] initWithPackage:kPackage
                                                      service:kService
                                                       method:@"UnaryCall"];
  kFullDuplexCallMethod = [[GRPCProtoMethod alloc] initWithPackage:kPackage
                                                           service:kService
                                                            method:@"FullDuplexCall"];
}

- (void)testConnectionToRemoteServer {
  __weak XCTestExpectation *expectation = [self expectationWithDescription:@"Server reachable."];

  GRPCCall *call = [[GRPCCall alloc] initWithHost:GRPCGetLocalInteropTestServerAddressPlainText()
                                             path:kInexistentMethod.HTTPPath
                                   requestsWriter:[GRXWriter writerWithValue:[NSData data]]];

  id<GRXWriteable> responsesWriteable = [[GRXWriteable alloc]
      initWithValueHandler:^(NSData *value) {
        XCTFail(@"Received unexpected response: %@", value);
      }
      completionHandler:^(NSError *errorOrNil) {
        XCTAssertNotNil(errorOrNil, @"Finished without error!");
        XCTAssertEqual(errorOrNil.code, 12, @"Finished with unexpected error: %@", errorOrNil);
        [expectation fulfill];
      }];

  [call startWithWriteable:responsesWriteable];

  [self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil];
}

- (void)testEmptyRPC {
  __weak XCTestExpectation *response =
      [self expectationWithDescription:@"Empty response received."];
  __weak XCTestExpectation *completion = [self expectationWithDescription:@"Empty RPC completed."];

  GRPCCall *call = [[GRPCCall alloc] initWithHost:GRPCGetLocalInteropTestServerAddressPlainText()
                                             path:kEmptyCallMethod.HTTPPath
                                   requestsWriter:[GRXWriter writerWithValue:[NSData data]]];

  id<GRXWriteable> responsesWriteable = [[GRXWriteable alloc]
      initWithValueHandler:^(NSData *value) {
        XCTAssertNotNil(value, @"nil value received as response.");
        XCTAssertEqual([value length], 0, @"Non-empty response received: %@", value);
        [response fulfill];
      }
      completionHandler:^(NSError *errorOrNil) {
        XCTAssertNil(errorOrNil, @"Finished with unexpected error: %@", errorOrNil);
        [completion fulfill];
      }];

  [call startWithWriteable:responsesWriteable];

  [self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil];
}

- (void)testSimpleProtoRPC {
  __weak XCTestExpectation *response = [self expectationWithDescription:@"Expected response."];
  __weak XCTestExpectation *completion = [self expectationWithDescription:@"RPC completed."];

  RMTSimpleRequest *request = [RMTSimpleRequest message];
  request.responseSize = 100;
  request.fillUsername = YES;
  request.fillOauthScope = YES;
  GRXWriter *requestsWriter = [GRXWriter writerWithValue:[request data]];

  GRPCCall *call = [[GRPCCall alloc] initWithHost:GRPCGetLocalInteropTestServerAddressPlainText()
                                             path:kUnaryCallMethod.HTTPPath
                                   requestsWriter:requestsWriter];

  id<GRXWriteable> responsesWriteable = [[GRXWriteable alloc]
      initWithValueHandler:^(NSData *value) {
        XCTAssertNotNil(value, @"nil value received as response.");
        XCTAssertGreaterThan(value.length, 0, @"Empty response received.");
        RMTSimpleResponse *responseProto = [RMTSimpleResponse parseFromData:value error:NULL];
        // We expect empty strings, not nil:
        XCTAssertNotNil(responseProto.username, @"Response's username is nil.");
        XCTAssertNotNil(responseProto.oauthScope, @"Response's OAuth scope is nil.");
        [response fulfill];
      }
      completionHandler:^(NSError *errorOrNil) {
        XCTAssertNil(errorOrNil, @"Finished with unexpected error: %@", errorOrNil);
        [completion fulfill];
      }];

  [call startWithWriteable:responsesWriteable];

  [self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil];
}

- (void)testResponseMetadataKVO {
  __weak XCTestExpectation *response =
      [self expectationWithDescription:@"Empty response received."];
  __weak XCTestExpectation *completion = [self expectationWithDescription:@"Empty RPC completed."];
  __weak XCTestExpectation *metadata = [self expectationWithDescription:@"Metadata changed."];

  GRPCCall *call = [[GRPCCall alloc] initWithHost:GRPCGetLocalInteropTestServerAddressPlainText()
                                             path:kEmptyCallMethod.HTTPPath
                                   requestsWriter:[GRXWriter writerWithValue:[NSData data]]];

  PassthroughObserver *observer = [[PassthroughObserver alloc]
      initWithCallback:^(NSString *keypath, id object, NSDictionary *change) {
        if ([keypath isEqual:@"responseHeaders"]) {
          [metadata fulfill];
        }
      }];

  [call addObserver:observer forKeyPath:@"responseHeaders" options:0 context:NULL];

  id<GRXWriteable> responsesWriteable = [[GRXWriteable alloc]
      initWithValueHandler:^(NSData *value) {
        XCTAssertNotNil(value, @"nil value received as response.");
        XCTAssertEqual([value length], 0, @"Non-empty response received: %@", value);
        [response fulfill];
      }
      completionHandler:^(NSError *errorOrNil) {
        XCTAssertNil(errorOrNil, @"Finished with unexpected error: %@", errorOrNil);
        [completion fulfill];
      }];

  [call startWithWriteable:responsesWriteable];

  [self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil];
}

- (void)testUserAgentPrefixAndSuffix {
  __weak XCTestExpectation *response =
      [self expectationWithDescription:@"Empty response received."];
  __weak XCTestExpectation *completion = [self expectationWithDescription:@"Empty RPC completed."];

  GRPCCall *call = [[GRPCCall alloc] initWithHost:GRPCGetLocalInteropTestServerAddressPlainText()
                                             path:kEmptyCallMethod.HTTPPath
                                   requestsWriter:[GRXWriter writerWithValue:[NSData data]]];
  // Setting this special key in the header will cause the interop server to echo back the
  // user-agent value, which we confirm.
  call.requestHeaders[@"x-grpc-test-echo-useragent"] = @"";

  id<GRXWriteable> responsesWriteable = [[GRXWriteable alloc]
      initWithValueHandler:^(NSData *value) {
        XCTAssertNotNil(value, @"nil value received as response.");
        XCTAssertEqual([value length], 0, @"Non-empty response received: %@", value);

        NSString *userAgent = call.responseHeaders[@"x-grpc-test-echo-useragent"];
        NSError *error = nil;

        // Test the regex is correct
        NSString *expectedUserAgent = @"Foo grpc-objc-cfstream/";
        expectedUserAgent = [expectedUserAgent stringByAppendingString:GRPC_OBJC_VERSION_STRING];
        expectedUserAgent = [expectedUserAgent stringByAppendingString:@" Suffix"];
        expectedUserAgent = [expectedUserAgent stringByAppendingString:@" grpc-c/"];
        expectedUserAgent = [expectedUserAgent stringByAppendingString:GRPC_C_VERSION_STRING];
        expectedUserAgent = [expectedUserAgent stringByAppendingString:@" ("];
        expectedUserAgent = [expectedUserAgent stringByAppendingString:@GPR_PLATFORM_STRING];
        expectedUserAgent = [expectedUserAgent stringByAppendingString:@"; chttp2)"];
        XCTAssertEqualObjects(userAgent, expectedUserAgent);

        // Change in format of user-agent field in a direction that does not match the regex will
        // likely cause problem for certain gRPC users. For details, refer to internal doc
        // https://goo.gl/c2diBc
        NSRegularExpression *regex = [NSRegularExpression
            regularExpressionWithPattern:@" grpc-[a-zA-Z0-9]+(-[a-zA-Z0-9]+)?/[^ ,]+( \\([^)]*\\))?"
                                 options:0
                                   error:&error];
        NSString *customUserAgent =
            [regex stringByReplacingMatchesInString:userAgent
                                            options:0
                                              range:NSMakeRange(0, [userAgent length])
                                       withTemplate:@""];

        NSArray *userAgentArray = [customUserAgent componentsSeparatedByString:@" "];
        XCTAssertEqual([userAgentArray count], 2);
        XCTAssertEqualObjects([userAgentArray objectAtIndex:0], @"Foo");
        XCTAssertEqualObjects([userAgentArray objectAtIndex:1], @"Suffix");
        [response fulfill];
      }
      completionHandler:^(NSError *errorOrNil) {
        XCTAssertNil(errorOrNil, @"Finished with unexpected error: %@", errorOrNil);
        [completion fulfill];
      }];

  [call startWithWriteable:responsesWriteable];

  [self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil];
}

- (void)testTrailers {
  __weak XCTestExpectation *response =
      [self expectationWithDescription:@"Empty response received."];
  __weak XCTestExpectation *completion = [self expectationWithDescription:@"Empty RPC completed."];

  GRPCCall *call = [[GRPCCall alloc] initWithHost:GRPCGetLocalInteropTestServerAddressPlainText()
                                             path:kEmptyCallMethod.HTTPPath
                                   requestsWriter:[GRXWriter writerWithValue:[NSData data]]];
  // Setting this special key in the header will cause the interop server to echo back the
  // trailer data.
  const unsigned char raw_bytes[] = {1, 2, 3, 4};
  NSData *trailer_data = [NSData dataWithBytes:raw_bytes length:sizeof(raw_bytes)];
  call.requestHeaders[@"x-grpc-test-echo-trailing-bin"] = trailer_data;

  id<GRXWriteable> responsesWriteable = [[GRXWriteable alloc]
      initWithValueHandler:^(NSData *value) {
        XCTAssertNotNil(value, @"nil value received as response.");
        XCTAssertEqual([value length], 0, @"Non-empty response received: %@", value);
        [response fulfill];
      }
      completionHandler:^(NSError *errorOrNil) {
        XCTAssertNil(errorOrNil, @"Finished with unexpected error: %@", errorOrNil);
        XCTAssertEqualObjects((NSData *)call.responseTrailers[@"x-grpc-test-echo-trailing-bin"],
                              trailer_data, @"Did not receive expected trailer");
        [completion fulfill];
      }];

  [call startWithWriteable:responsesWriteable];
  [self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil];
}

// TODO(makarandd): Move to a different file that contains only unit tests
- (void)testExceptions {
  GRXWriter *writer = [GRXWriter writerWithValue:[NSData data]];
  // Try to set parameters to nil for GRPCCall. This should cause an exception
  @try {
    (void)[[GRPCCall alloc] initWithHost:nil path:nil requestsWriter:writer];
    XCTFail(@"Did not receive an exception when parameters are nil");
  } @catch (NSException *theException) {
    NSLog(@"Received exception as expected: %@", theException.name);
  }

  // Set state to Finished by force
  GRXWriter *requestsWriter = [GRXWriter emptyWriter];
  [requestsWriter finishWithError:nil];
  @try {
    (void)[[GRPCCall alloc] initWithHost:GRPCGetLocalInteropTestServerAddressPlainText()
                                    path:kUnaryCallMethod.HTTPPath
                          requestsWriter:requestsWriter];
    XCTFail(@"Did not receive an exception when GRXWriter has incorrect state.");
  } @catch (NSException *theException) {
    NSLog(@"Received exception as expected: %@", theException.name);
  }
}

- (void)testAlternateDispatchQueue {
  const int32_t kPayloadSize = 100;
  RMTSimpleRequest *request = [RMTSimpleRequest message];
  request.responseSize = kPayloadSize;

  __weak XCTestExpectation *expectation1 =
      [self expectationWithDescription:@"AlternateDispatchQueue1"];

  // Use default (main) dispatch queue
  NSString *main_queue_label =
      [NSString stringWithUTF8String:dispatch_queue_get_label(dispatch_get_main_queue())];

  GRXWriter *requestsWriter1 = [GRXWriter writerWithValue:[request data]];

  GRPCCall *call1 = [[GRPCCall alloc] initWithHost:GRPCGetLocalInteropTestServerAddressPlainText()
                                              path:kUnaryCallMethod.HTTPPath
                                    requestsWriter:requestsWriter1];

  id<GRXWriteable> responsesWriteable1 = [[GRXWriteable alloc]
      initWithValueHandler:^(NSData *value) {
        NSString *label =
            [NSString stringWithUTF8String:dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)];
        XCTAssert([label isEqualToString:main_queue_label]);

        [expectation1 fulfill];
      }
         completionHandler:^(NSError *errorOrNil){
         }];

  [call1 startWithWriteable:responsesWriteable1];

  [self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil];

  // Use a custom  queue
  __weak XCTestExpectation *expectation2 =
      [self expectationWithDescription:@"AlternateDispatchQueue2"];

  NSString *queue_label = @"test.queue1";
  dispatch_queue_t queue = dispatch_queue_create([queue_label UTF8String], DISPATCH_QUEUE_SERIAL);

  GRXWriter *requestsWriter2 = [GRXWriter writerWithValue:[request data]];

  GRPCCall *call2 = [[GRPCCall alloc] initWithHost:GRPCGetLocalInteropTestServerAddressPlainText()
                                              path:kUnaryCallMethod.HTTPPath
                                    requestsWriter:requestsWriter2];

  [call2 setResponseDispatchQueue:queue];

  id<GRXWriteable> responsesWriteable2 = [[GRXWriteable alloc]
      initWithValueHandler:^(NSData *value) {
        NSString *label =
            [NSString stringWithUTF8String:dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)];
        XCTAssert([label isEqualToString:queue_label]);

        [expectation2 fulfill];
      }
         completionHandler:^(NSError *errorOrNil){
         }];

  [call2 startWithWriteable:responsesWriteable2];

  [self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil];
}

- (void)testTimeout {
  __weak XCTestExpectation *completion = [self expectationWithDescription:@"RPC completed."];

  GRXBufferedPipe *pipe = [GRXBufferedPipe pipe];
  GRPCCall *call = [[GRPCCall alloc] initWithHost:GRPCGetLocalInteropTestServerAddressPlainText()
                                             path:kFullDuplexCallMethod.HTTPPath
                                   requestsWriter:pipe];

  id<GRXWriteable> responsesWriteable = [[GRXWriteable alloc]
      initWithValueHandler:^(NSData *value) {
        XCTAssert(0, @"Failure: response received; Expect: no response received.");
      }
      completionHandler:^(NSError *errorOrNil) {
        XCTAssertNotNil(errorOrNil,
                        @"Failure: no error received; Expect: receive deadline exceeded.");
        XCTAssertEqual(errorOrNil.code, GRPCErrorCodeDeadlineExceeded);
        [completion fulfill];
      }];

  call.timeout = 0.001;
  [call startWithWriteable:responsesWriteable];

  [self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil];
}

- (int)findFreePort {
  struct sockaddr_in addr;
  unsigned int addr_len = sizeof(addr);
  memset(&addr, 0, sizeof(addr));
  addr.sin_family = AF_INET;
  int fd = socket(AF_INET, SOCK_STREAM, 0);
  XCTAssertEqual(bind(fd, (struct sockaddr *)&addr, sizeof(addr)), 0);
  XCTAssertEqual(getsockname(fd, (struct sockaddr *)&addr, &addr_len), 0);
  XCTAssertEqual(addr_len, sizeof(addr));
  close(fd);
  return addr.sin_port;
}

- (void)testErrorCode {
  int port = [self findFreePort];
  NSString *const kPhonyAddress = [NSString stringWithFormat:@"localhost:%d", port];
  __weak XCTestExpectation *completion =
      [self expectationWithDescription:@"Received correct error code."];

  GRPCCall *call = [[GRPCCall alloc] initWithHost:kPhonyAddress
                                             path:kEmptyCallMethod.HTTPPath
                                   requestsWriter:[GRXWriter writerWithValue:[NSData data]]];

  id<GRXWriteable> responsesWriteable = [[GRXWriteable alloc]
      initWithValueHandler:^(NSData *value) {
        // Should not reach here
        XCTAssert(NO);
      }
      completionHandler:^(NSError *errorOrNil) {
        XCTAssertNotNil(errorOrNil, @"Finished with no error");
        XCTAssertEqual(errorOrNil.code, GRPC_STATUS_UNAVAILABLE);
        [completion fulfill];
      }];

  [call startWithWriteable:responsesWriteable];

  [self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil];
}

- (void)testTimeoutBackoffWithTimeout:(double)timeout Backoff:(double)backoff {
  const double maxConnectTime = timeout > backoff ? timeout : backoff;
  const double kMargin = 0.2;

  __weak XCTestExpectation *completion = [self expectationWithDescription:@"Timeout in a second."];
  NSString *const kPhonyAddress = [NSString stringWithFormat:@"8.8.8.8:1"];
  [GRPCCall useInsecureConnectionsForHost:kPhonyAddress];
  [GRPCCall setMinConnectTimeout:timeout * 1000
                  initialBackoff:backoff * 1000
                      maxBackoff:0
                         forHost:kPhonyAddress];
  GRPCCall *call = [[GRPCCall alloc] initWithHost:kPhonyAddress
                                             path:@"/phonyPath"
                                   requestsWriter:[GRXWriter writerWithValue:[NSData data]]];
  NSDate *startTime = [NSDate date];
  id<GRXWriteable> responsesWriteable = [[GRXWriteable alloc]
      initWithValueHandler:^(id value) {
        XCTAssert(NO, @"Received message. Should not reach here");
      }
      completionHandler:^(NSError *errorOrNil) {
        XCTAssertNotNil(errorOrNil, @"Finished with no error");
        // The call must fail before maxConnectTime. However there is no lower bound on the time
        // taken for connection. A shorter time happens when connection is actively refused
        // by 8.8.8.8:1 before maxConnectTime elapsed.
        XCTAssertLessThan([[NSDate date] timeIntervalSinceDate:startTime],
                          maxConnectTime + kMargin);
        [completion fulfill];
      }];

  [call startWithWriteable:responsesWriteable];

  [self waitForExpectationsWithTimeout:TEST_TIMEOUT handler:nil];
}

// The numbers of the following three tests are selected to be smaller than the default values of
// initial backoff (1s) and min_connect_timeout (20s), so that if they fail we know the default
// values fail to be overridden by the channel args.
- (void)testTimeoutBackoff1 {
  [self testTimeoutBackoffWithTimeout:0.7 Backoff:0.3];
}

- (void)testTimeoutBackoff2 {
  [self testTimeoutBackoffWithTimeout:0.3 Backoff:0.7];
}

@end
