/*
 * Copyright 2019 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/XCTest.h>

#import <OCMock/OCMock.h>

@import FirebaseCoreInternal;

#import "FBLPromise+Testing.h"
#import "FirebaseInstallations/Source/Tests/Utils/FIRInstallationsItem+Tests.h"

#import "FirebaseInstallations/Source/Library/Errors/FIRInstallationsErrorUtil.h"
#import "FirebaseInstallations/Source/Library/Errors/FIRInstallationsHTTPError.h"
#import "FirebaseInstallations/Source/Library/InstallationsAPI/FIRInstallationsAPIService.h"
#import "FirebaseInstallations/Source/Library/InstallationsStore/FIRInstallationsStoredAuthToken.h"

#import "FirebaseCore/Extension/FirebaseCoreInternal.h"

typedef FBLPromise * (^FIRInstallationsAPIServiceTask)(void);

#pragma mark - Fakes

/// A fake heartbeat logger used for dependency injection during testing.
@interface FIRHeartbeatLoggerFake : NSObject <FIRHeartbeatLoggerProtocol>
@property(nonatomic, copy, nullable) FIRHeartbeatsPayload * (^onFlushHeartbeatsIntoPayloadHandler)
    (void);
@property(nonatomic, copy, nullable) FIRDailyHeartbeatCode (^onHeartbeatCodeForTodayHandler)(void);
@end

@implementation FIRHeartbeatLoggerFake

- (nonnull FIRHeartbeatsPayload *)flushHeartbeatsIntoPayload {
  if (self.onFlushHeartbeatsIntoPayloadHandler) {
    return self.onFlushHeartbeatsIntoPayloadHandler();
  } else {
    return nil;
  }
}

- (FIRDailyHeartbeatCode)heartbeatCodeForToday {
  // This API should not be used by the below tests because the Installations
  // SDK uses only the V2 heartbeat API (`flushHeartbeatsIntoPayload`) for
  // getting heartbeats.
  [self doesNotRecognizeSelector:_cmd];
  return FIRDailyHeartbeatCodeNone;
}

- (void)log {
  // This API should not be used by the below tests because the Installations
  // SDK does not log heartbeats in its networking context.
  [self doesNotRecognizeSelector:_cmd];
}

- (NSString *_Nullable)headerValue {
  return FIRHeaderValueFromHeartbeatsPayload([self flushHeartbeatsIntoPayload]);
}

- (void)asyncHeaderValueWithCompletionHandler:
    (nonnull void (^)(NSString *_Nullable))completionHandler {
  [self doesNotRecognizeSelector:_cmd];
}

@end

#pragma mark - FIRInstallationsAPIService + Internal

@interface FIRInstallationsAPIService (Internal)
- (instancetype)initWithURLSession:(NSURLSession *)URLSession
                            APIKey:(NSString *)APIKey
                         projectID:(NSString *)projectID
                   heartbeatLogger:(id<FIRHeartbeatLoggerProtocol>)heartbeatLogger;
@end

#pragma mark - FIRInstallationsAPIServiceTests

@interface FIRInstallationsAPIServiceTests : XCTestCase
@property(nonatomic) FIRInstallationsAPIService *service;
@property(nonatomic) FIRHeartbeatLoggerFake *heartbeatLoggerFake;
@property(nonatomic) NSString *APIKey;
@property(nonatomic) NSString *projectID;
@property(nonatomic) id mockURLSession;
@end

@implementation FIRInstallationsAPIServiceTests

- (void)setUp {
  self.APIKey = @"api-key";
  self.projectID = @"project-id";
  self.mockURLSession = OCMClassMock([NSURLSession class]);
  self.heartbeatLoggerFake = [[FIRHeartbeatLoggerFake alloc] init];
  self.service = [[FIRInstallationsAPIService alloc] initWithURLSession:self.mockURLSession
                                                                 APIKey:self.APIKey
                                                              projectID:self.projectID
                                                        heartbeatLogger:self.heartbeatLoggerFake];
}

- (void)tearDown {
  self.service = nil;
  self.heartbeatLoggerFake = nil;
  self.mockURLSession = nil;
  self.projectID = nil;
  self.APIKey = nil;

  // Wait for any pending promises to complete.
  XCTAssert(FBLWaitForPromisesWithTimeout(2));
}

- (void)testRegisterInstallationSuccessWhenHeartbeatsNeedSending {
  // Given
  NSString *fixtureName = @"APIRegisterInstallationResponseSuccess.json";
  FIRHeartbeatsPayload *nonEmptyHeartbeatsPayload =
      [FIRHeartbeatLoggingTestUtils nonEmptyHeartbeatsPayload];
  // When
  self.heartbeatLoggerFake.onFlushHeartbeatsIntoPayloadHandler = ^FIRHeartbeatsPayload * {
    return nonEmptyHeartbeatsPayload;
  };
  // Then
  [self assertRegisterInstallationSuccessWithResponseFixtureName:fixtureName
                                                    responseCode:201
                                             expectedFIDOverride:@"aaaaaaaaaaaaaaaaaaaaaa"
                                               heartbeatsPayload:nonEmptyHeartbeatsPayload];
}

- (void)testRegisterInstallationSuccessWhenNoHeartbeatsNeedSending {
  // Given
  NSString *fixtureName = @"APIRegisterInstallationResponseSuccess.json";
  FIRHeartbeatsPayload *emptyHeartbeatsPayload =
      [FIRHeartbeatLoggingTestUtils emptyHeartbeatsPayload];
  // When
  self.heartbeatLoggerFake.onFlushHeartbeatsIntoPayloadHandler = ^FIRHeartbeatsPayload * {
    return emptyHeartbeatsPayload;
  };
  // Then
  [self assertRegisterInstallationSuccessWithResponseFixtureName:fixtureName
                                                    responseCode:201
                                             expectedFIDOverride:@"aaaaaaaaaaaaaaaaaaaaaa"
                                               heartbeatsPayload:emptyHeartbeatsPayload];
}

- (void)testRegisterInstallationSuccess_NoFIDInResponse {
  NSString *fixtureName = @"APIRegisterInstallationResponseSuccessNoFID.json";
  [self assertRegisterInstallationSuccessWithResponseFixtureName:fixtureName
                                                    responseCode:201
                                             expectedFIDOverride:nil
                                               heartbeatsPayload:nil];
}

- (void)testRegisterInstallationSuccess_InvalidInstallation {
  FIRInstallationsItem *installation = [FIRInstallationsItem createUnregisteredInstallationItem];
  installation.firebaseInstallationID = nil;

  __auto_type promise = [self.service registerInstallation:installation];

  XCTAssert(FBLWaitForPromisesWithTimeout(0.5));

  XCTAssertTrue(promise.isRejected);
  XCTAssertNil(promise.value);
  XCTAssertNotNil(promise.error);
}

- (void)testRefreshAuthTokenSuccessWhenHeartbeatsNeedSending {
  // Given
  FIRHeartbeatsPayload *nonEmptyHeartbeatsPayload =
      [FIRHeartbeatLoggingTestUtils nonEmptyHeartbeatsPayload];
  // When
  self.heartbeatLoggerFake.onFlushHeartbeatsIntoPayloadHandler = ^FIRHeartbeatsPayload * {
    return nonEmptyHeartbeatsPayload;
  };
  // Then
  [self assertRefreshAuthTokenSuccessWhenSendingHeartbeatsPayload:nonEmptyHeartbeatsPayload];
}

- (void)testRefreshAuthTokenSuccessWhenNoHeartbeatsNeedSending {
  // Given
  FIRHeartbeatsPayload *emptyHeartbeatsPayload =
      [FIRHeartbeatLoggingTestUtils emptyHeartbeatsPayload];
  // When
  self.heartbeatLoggerFake.onFlushHeartbeatsIntoPayloadHandler = ^FIRHeartbeatsPayload * {
    return emptyHeartbeatsPayload;
  };
  // Then
  [self assertRefreshAuthTokenSuccessWhenSendingHeartbeatsPayload:emptyHeartbeatsPayload];
}

- (void)testRefreshAuthTokenAPIError {
  FIRInstallationsItem *installation = [FIRInstallationsItem createRegisteredInstallationItem];
  installation.firebaseInstallationID = @"qwertyuiopasdfghjklzxcvbnm";

  // 1. Stub URL session:

  // 1.1. URL request validation.
  id URLRequestValidation = [self refreshTokenRequestValidationArgWithInstallation:installation
                                                                 heartbeatsPayload:nil];

  // 1.2. Capture completion to call it later.
  __block void (^taskCompletion)(NSData *, NSURLResponse *, NSError *);
  id completionArg = [OCMArg checkWithBlock:^BOOL(id obj) {
    taskCompletion = obj;
    return YES;
  }];

  // 1.3. Create a data task mock.
  id mockDataTask = OCMClassMock([NSURLSessionDataTask class]);
  OCMExpect([(NSURLSessionDataTask *)mockDataTask resume]);

  // 1.4. Expect `dataTaskWithRequest` to be called.
  OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
                                   completionHandler:completionArg])
      .andReturn(mockDataTask);

  // 1.5. Prepare server response data.
  NSData *errorResponseData =
      [self loadFixtureNamed:@"APIGenerateTokenResponseInvalidRefreshToken.json"];

  // 2. Call
  FBLPromise<FIRInstallationsItem *> *promise =
      [self.service refreshAuthTokenForInstallation:installation];

  // 3. Wait for `[NSURLSession dataTaskWithRequest...]` to be called
  OCMVerifyAllWithDelay(self.mockURLSession, 0.5);

  // 4. Wait for the data task `resume` to be called.
  OCMVerifyAllWithDelay(mockDataTask, 0.5);

  // 5. Call the data task completion.
  taskCompletion(errorResponseData, [self responseWithStatusCode:401], nil);

  // 6. Check result.
  XCTAssert(FBLWaitForPromisesWithTimeout(0.5));

  XCTAssertTrue([FIRInstallationsErrorUtil isAPIError:promise.error withHTTPCode:401]);
  XCTAssertNil(promise.value);
}

- (void)testRefreshAuthToken_WhenAPIError500_ThenRetriesOnce {
  FIRInstallationsItem *installation = [FIRInstallationsItem createRegisteredInstallationItem];
  installation.firebaseInstallationID = @"qwertyuiopasdfghjklzxcvbnm";

  // 1. Stub URL session:

  // 1.1. URL request validation.
  id URLRequestValidation = [self refreshTokenRequestValidationArgWithInstallation:installation
                                                                 heartbeatsPayload:nil];

  // 1.2. Capture completion to call it later.
  __block void (^taskCompletion)(NSData *, NSURLResponse *, NSError *);
  id completionArg = [OCMArg checkWithBlock:^BOOL(id obj) {
    taskCompletion = obj;
    return YES;
  }];

  // 1.3. Create a data task mock.
  id mockDataTask1 = OCMClassMock([NSURLSessionDataTask class]);
  OCMExpect([(NSURLSessionDataTask *)mockDataTask1 resume]);

  // 1.4. Expect `dataTaskWithRequest` to be called.
  OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
                                   completionHandler:completionArg])
      .andReturn(mockDataTask1);

  // 1.5. Prepare server response data.
  NSData *errorResponseData =
      [self loadFixtureNamed:@"APIGenerateTokenResponseInvalidRefreshToken.json"];

  // 2. Call
  FBLPromise<FIRInstallationsItem *> *promise =
      [self.service refreshAuthTokenForInstallation:installation];

  // 3. Wait for `[NSURLSession dataTaskWithRequest...]` to be called
  OCMVerifyAllWithDelay(self.mockURLSession, 0.5);

  // 4. Wait for the data task `resume` to be called.
  OCMVerifyAllWithDelay(mockDataTask1, 0.5);

  // 5. Call the data task completion.
  taskCompletion(errorResponseData,
                 [self responseWithStatusCode:FIRInstallationsHTTPCodesServerInternalError], nil);

  // 6. Retry:

  // 6.1. Expect another API request to be sent.
  id mockDataTask2 = OCMClassMock([NSURLSessionDataTask class]);
  OCMExpect([(NSURLSessionDataTask *)mockDataTask2 resume]);
  OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
                                   completionHandler:completionArg])
      .andReturn(mockDataTask2);
  OCMVerifyAllWithDelay(self.mockURLSession, 1.5);
  OCMVerifyAllWithDelay(mockDataTask2, 1.5);

  // 6.2. Send the API response again.
  taskCompletion(errorResponseData,
                 [self responseWithStatusCode:FIRInstallationsHTTPCodesServerInternalError], nil);

  // 6. Check result.
  XCTAssert(FBLWaitForPromisesWithTimeout(0.5));

  XCTAssertTrue([FIRInstallationsErrorUtil
        isAPIError:promise.error
      withHTTPCode:FIRInstallationsHTTPCodesServerInternalError]);
  XCTAssertNil(promise.value);
}

- (void)testRefreshAuthTokenDataNil {
  FIRInstallationsItem *installation = [FIRInstallationsItem createRegisteredInstallationItem];
  installation.firebaseInstallationID = @"qwertyuiopasdfghjklzxcvbnm";

  // 1. Stub URL session:

  // 1.1. URL request validation.
  id URLRequestValidation = [self refreshTokenRequestValidationArgWithInstallation:installation
                                                                 heartbeatsPayload:nil];

  // 1.2. Capture completion to call it later.
  __block void (^taskCompletion)(NSData *, NSURLResponse *, NSError *);
  id completionArg = [OCMArg checkWithBlock:^BOOL(id obj) {
    taskCompletion = obj;
    return YES;
  }];

  // 1.3. Create a data task mock.
  id mockDataTask = OCMClassMock([NSURLSessionDataTask class]);
  OCMExpect([(NSURLSessionDataTask *)mockDataTask resume]);

  // 1.4. Expect `dataTaskWithRequest` to be called.
  OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
                                   completionHandler:completionArg])
      .andReturn(mockDataTask);

  // 2. Call
  FBLPromise<FIRInstallationsItem *> *promise =
      [self.service refreshAuthTokenForInstallation:installation];

  // 3. Wait for `[NSURLSession dataTaskWithRequest...]` to be called
  OCMVerifyAllWithDelay(self.mockURLSession, 0.5);

  // 4. Wait for the data task `resume` to be called.
  OCMVerifyAllWithDelay(mockDataTask, 0.5);

  // 5. Call the data task completion.
  // HTTP 200 but no data (a potential server failure).
  taskCompletion(nil, [self responseWithStatusCode:200], nil);

  // 6. Check result.
  XCTAssert(FBLWaitForPromisesWithTimeout(0.5));

  XCTAssertEqualObjects(promise.error.userInfo[NSLocalizedFailureReasonErrorKey],
                        @"Failed to serialize JSON data.");
  XCTAssertNil(promise.value);
}

- (void)testDeleteInstallationSuccessWhenHeartbeatsNeedSending {
  // Given
  FIRHeartbeatsPayload *nonEmptyHeartbeatsPayload =
      [FIRHeartbeatLoggingTestUtils nonEmptyHeartbeatsPayload];
  // When
  self.heartbeatLoggerFake.onFlushHeartbeatsIntoPayloadHandler = ^FIRHeartbeatsPayload * {
    return nonEmptyHeartbeatsPayload;
  };
  // Then
  [self assertDeleteInstallationSuccessWhenSendingHeartbeatsPayload:nonEmptyHeartbeatsPayload];
}

- (void)testDeleteInstallationSuccessWhenNoHeartbeatsNeedSending {
  // Given
  FIRHeartbeatsPayload *emptyHeartbeatsPayload =
      [FIRHeartbeatLoggingTestUtils emptyHeartbeatsPayload];
  // When
  self.heartbeatLoggerFake.onFlushHeartbeatsIntoPayloadHandler = ^FIRHeartbeatsPayload * {
    return emptyHeartbeatsPayload;
  };
  // Then
  [self assertDeleteInstallationSuccessWhenSendingHeartbeatsPayload:emptyHeartbeatsPayload];
}

- (void)testDeleteInstallationErrorNotFound {
  FIRInstallationsItem *installation = [FIRInstallationsItem createRegisteredInstallationItem];

  // 1. Stub URL session:

  // 1.1. URL request validation.
  id URLRequestValidation = [self deleteInstallationRequestValidationWithInstallation:installation
                                                                    heartbeatsPayload:nil];

  // 1.2. Capture completion to call it later.
  __block void (^taskCompletion)(NSData *, NSURLResponse *, NSError *);
  id completionArg = [OCMArg checkWithBlock:^BOOL(id obj) {
    taskCompletion = obj;
    return YES;
  }];

  // 1.3. Create a data task mock.
  id mockDataTask = OCMClassMock([NSURLSessionDataTask class]);
  OCMExpect([(NSURLSessionDataTask *)mockDataTask resume]);

  // 1.4. Expect `dataTaskWithRequest` to be called.
  OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
                                   completionHandler:completionArg])
      .andReturn(mockDataTask);

  // 2. Call
  FBLPromise<FIRInstallationsItem *> *promise = [self.service deleteInstallation:installation];

  // 3. Wait for `[NSURLSession dataTaskWithRequest...]` to be called
  OCMVerifyAllWithDelay(self.mockURLSession, 0.5);

  // 4. Wait for the data task `resume` to be called.
  OCMVerifyAllWithDelay(mockDataTask, 0.5);

  // 5. Call the data task completion.
  // HTTP 200 but no data (a potential server failure).
  taskCompletion(nil, [self responseWithStatusCode:404], nil);

  // 6. Check result.
  XCTAssert(FBLWaitForPromisesWithTimeout(0.5));

  XCTAssertTrue([FIRInstallationsErrorUtil isAPIError:promise.error withHTTPCode:404]);
  XCTAssertNil(promise.value);
}

- (void)testDeleteInstallation_WhenAPIError500_ThenRetriesOnce {
  FIRInstallationsItem *installation = [FIRInstallationsItem createRegisteredInstallationItem];

  // 1. Stub URL session:

  // 1.1. URL request validation.
  id URLRequestValidation = [self deleteInstallationRequestValidationWithInstallation:installation
                                                                    heartbeatsPayload:nil];

  // 1.2. Capture completion to call it later.
  __block void (^taskCompletion)(NSData *, NSURLResponse *, NSError *);
  id completionArg = [OCMArg checkWithBlock:^BOOL(id obj) {
    taskCompletion = obj;
    return YES;
  }];

  // 1.3. Create a data task mock.
  id mockDataTask1 = OCMClassMock([NSURLSessionDataTask class]);
  OCMExpect([(NSURLSessionDataTask *)mockDataTask1 resume]);

  // 1.4. Expect `dataTaskWithRequest` to be called.
  OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
                                   completionHandler:completionArg])
      .andReturn(mockDataTask1);

  // 2. Call
  FBLPromise<FIRInstallationsItem *> *promise = [self.service deleteInstallation:installation];

  // 3. Wait for `[NSURLSession dataTaskWithRequest...]` to be called
  OCMVerifyAllWithDelay(self.mockURLSession, 0.5);

  // 4. Wait for the data task `resume` to be called.
  OCMVerifyAllWithDelay(mockDataTask1, 0.5);

  // 5. Call the data task completion.
  // HTTP 200 but no data (a potential server failure).
  taskCompletion(nil, [self responseWithStatusCode:FIRInstallationsHTTPCodesServerInternalError],
                 nil);

  // 6. Retry:
  // 6.1. Wait for the API request to be sent again.
  id mockDataTask2 = OCMClassMock([NSURLSessionDataTask class]);
  OCMExpect([(NSURLSessionDataTask *)mockDataTask2 resume]);
  OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
                                   completionHandler:completionArg])
      .andReturn(mockDataTask2);
  OCMVerifyAllWithDelay(self.mockURLSession, 1.5);
  OCMVerifyAllWithDelay(mockDataTask1, 1.5);

  // 6.1. Send another response.
  taskCompletion(nil, [self responseWithStatusCode:FIRInstallationsHTTPCodesServerInternalError],
                 nil);

  // 7. Check result.
  XCTAssert(FBLWaitForPromisesWithTimeout(0.5));

  XCTAssertTrue([FIRInstallationsErrorUtil
        isAPIError:promise.error
      withHTTPCode:FIRInstallationsHTTPCodesServerInternalError]);
  XCTAssertNil(promise.value);
}

#pragma mark - Helpers

- (void)assertRefreshAuthTokenSuccessWhenSendingHeartbeatsPayload:
    (FIRHeartbeatsPayload *)heartbeatsPayload {
  FIRInstallationsItem *installation = [FIRInstallationsItem createRegisteredInstallationItem];
  installation.firebaseInstallationID = @"qwertyuiopasdfghjklzxcvbnm";

  // 1. Stub URL session:

  // 1.1. URL request validation.
  id URLRequestValidation =
      [self refreshTokenRequestValidationArgWithInstallation:installation
                                           heartbeatsPayload:heartbeatsPayload];

  // 1.2. Capture completion to call it later.
  __block void (^taskCompletion)(NSData *, NSURLResponse *, NSError *);
  id completionArg = [OCMArg checkWithBlock:^BOOL(id obj) {
    taskCompletion = obj;
    return YES;
  }];

  // 1.3. Create a data task mock.
  id mockDataTask = OCMClassMock([NSURLSessionDataTask class]);
  OCMExpect([(NSURLSessionDataTask *)mockDataTask resume]);

  // 1.4. Expect `dataTaskWithRequest` to be called.
  OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
                                   completionHandler:completionArg])
      .andReturn(mockDataTask);

  // 1.5. Prepare server response data.
  NSData *successResponseData = [self loadFixtureNamed:@"APIGenerateTokenResponseSuccess.json"];

  // 2. Call
  FBLPromise<FIRInstallationsItem *> *promise =
      [self.service refreshAuthTokenForInstallation:installation];

  // 3. Wait for `[NSURLSession dataTaskWithRequest...]` to be called
  OCMVerifyAllWithDelay(self.mockURLSession, 0.5);

  // 4. Wait for the data task `resume` to be called.
  OCMVerifyAllWithDelay(mockDataTask, 0.5);

  // 5. Call the data task completion.
  taskCompletion(successResponseData, [self responseWithStatusCode:200], nil);

  // 6. Check result.
  XCTAssert(FBLWaitForPromisesWithTimeout(0.5));

  XCTAssertNil(promise.error);
  XCTAssertNotNil(promise.value);

  XCTAssertNotEqual(promise.value, installation);
  XCTAssertEqualObjects(promise.value.appID, installation.appID);
  XCTAssertEqualObjects(promise.value.firebaseAppName, installation.firebaseAppName);
  XCTAssertEqualObjects(promise.value.firebaseInstallationID, installation.firebaseInstallationID);
  XCTAssertEqualObjects(promise.value.refreshToken, installation.refreshToken);
  XCTAssertEqualObjects(promise.value.authToken.token,
                        @"aaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbb.cccccccccccccccccccccccc");
  [self assertDate:promise.value.authToken.expirationDate
      isApproximatelyEqualCurrentPlusTimeInterval:3987465];
}

- (void)assertDeleteInstallationSuccessWhenSendingHeartbeatsPayload:
    (FIRHeartbeatsPayload *)heartbeatsPayload {
  FIRInstallationsItem *installation = [FIRInstallationsItem createRegisteredInstallationItem];

  // 1. Stub URL session:

  // 1.1. URL request validation.
  id URLRequestValidation =
      [self deleteInstallationRequestValidationWithInstallation:installation
                                              heartbeatsPayload:heartbeatsPayload];

  // 1.2. Capture completion to call it later.
  __block void (^taskCompletion)(NSData *, NSURLResponse *, NSError *);
  id completionArg = [OCMArg checkWithBlock:^BOOL(id obj) {
    taskCompletion = obj;
    return YES;
  }];

  // 1.3. Create a data task mock.
  id mockDataTask = OCMClassMock([NSURLSessionDataTask class]);
  OCMExpect([(NSURLSessionDataTask *)mockDataTask resume]);

  // 1.4. Expect `dataTaskWithRequest` to be called.
  OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
                                   completionHandler:completionArg])
      .andReturn(mockDataTask);

  // 2. Call
  FBLPromise<FIRInstallationsItem *> *promise = [self.service deleteInstallation:installation];

  // 3. Wait for `[NSURLSession dataTaskWithRequest...]` to be called
  OCMVerifyAllWithDelay(self.mockURLSession, 0.5);

  // 4. Wait for the data task `resume` to be called.
  OCMVerifyAllWithDelay(mockDataTask, 0.5);

  // 5. Call the data task completion.
  // HTTP 200 but no data (a potential server failure).
  NSData *successResponseData = [@"{}" dataUsingEncoding:NSUTF8StringEncoding];
  taskCompletion(successResponseData, [self responseWithStatusCode:200], nil);

  // 6. Check result.
  XCTAssert(FBLWaitForPromisesWithTimeout(0.5));

  XCTAssertNil(promise.error);
  XCTAssertEqual(promise.value, installation);
}

- (NSData *)loadFixtureNamed:(NSString *)fileName {
  NSURL *fileURL = [[NSBundle bundleForClass:[self class]] URLForResource:fileName
                                                            withExtension:nil];
  XCTAssertNotNil(fileURL);

  NSError *error;
  NSData *data = [NSData dataWithContentsOfURL:fileURL options:0 error:&error];
  XCTAssertNotNil(data, @"File name: %@ Error: %@", fileName, error);

  return data;
}

- (NSURLResponse *)responseWithStatusCode:(NSUInteger)statusCode {
  NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:[NSURL fileURLWithPath:@"/"]
                                                            statusCode:statusCode
                                                           HTTPVersion:nil
                                                          headerFields:nil];
  return response;
}

- (void)assertDate:(NSDate *)date
    isApproximatelyEqualCurrentPlusTimeInterval:(NSTimeInterval)timeInterval {
  NSDate *expectedDate = [NSDate dateWithTimeIntervalSinceNow:timeInterval];

  NSTimeInterval precision = 10;
  XCTAssert(ABS([date timeIntervalSinceDate:expectedDate]) <= precision,
            @"date: %@ is not equal to expected %@ with precision %f - %@", date, expectedDate,
            precision, self.name);
}

- (id)refreshTokenRequestValidationArgWithInstallation:(FIRInstallationsItem *)installation
                                     heartbeatsPayload:(FIRHeartbeatsPayload *)heartbeatsPayload {
  return [OCMArg checkWithBlock:^BOOL(NSURLRequest *request) {
    XCTAssertEqualObjects(request.HTTPMethod, @"POST");
    XCTAssertEqualObjects(request.URL.absoluteString,
                          @"https://firebaseinstallations.googleapis.com/v1/projects/project-id/"
                          @"installations/qwertyuiopasdfghjklzxcvbnm/authTokens:generate");

    NSMutableDictionary<NSString *, NSString *> *expectedHTTPHeaderFields = @{
      @"Content-Type" : @"application/json",
      @"X-Goog-Api-Key" : self.APIKey,
      @"X-Ios-Bundle-Identifier" : [[NSBundle mainBundle] bundleIdentifier],
      @"Authorization" : [NSString stringWithFormat:@"FIS_v2 %@", installation.refreshToken]
    }
                                                                                .mutableCopy;

    NSString *_Nullable heartbeatHeaderValue =
        FIRHeaderValueFromHeartbeatsPayload(heartbeatsPayload);
    if (heartbeatHeaderValue) {
      expectedHTTPHeaderFields[@"X-firebase-client"] = heartbeatHeaderValue;
    }

    XCTAssertEqualObjects([request allHTTPHeaderFields], expectedHTTPHeaderFields);

    NSError *error;
    NSDictionary *body = [NSJSONSerialization JSONObjectWithData:request.HTTPBody
                                                         options:0
                                                           error:&error];
    XCTAssertNotNil(body, @"Error: %@, test: %@", error, self.name);

    XCTAssertEqualObjects(
        body,
        @{@"installation" : @{@"sdkVersion" : [self SDKVersion]}}, @"%@", self.name);

    return YES;
  }];
}

- (id)deleteInstallationRequestValidationWithInstallation:(FIRInstallationsItem *)installation
                                        heartbeatsPayload:
                                            (FIRHeartbeatsPayload *)heartbeatsPayload {
  return [OCMArg checkWithBlock:^BOOL(NSURLRequest *request) {
    XCTAssert([request isKindOfClass:[NSURLRequest class]], @"Unexpected class: %@",
              [request class]);
    XCTAssertEqualObjects(request.HTTPMethod, @"DELETE");
    NSString *expectedURL = [NSString
        stringWithFormat:
            @"https://firebaseinstallations.googleapis.com/v1/projects/%@/installations/%@/",
            self.projectID, installation.firebaseInstallationID];
    XCTAssertEqualObjects(request.URL.absoluteString, expectedURL);

    NSMutableDictionary<NSString *, NSString *> *expectedHTTPHeaderFields = @{
      @"Content-Type" : @"application/json",
      @"X-Goog-Api-Key" : self.APIKey,
      @"X-Ios-Bundle-Identifier" : [[NSBundle mainBundle] bundleIdentifier],
      @"Authorization" : [NSString stringWithFormat:@"FIS_v2 %@", installation.refreshToken],
    }
                                                                                .mutableCopy;

    NSString *_Nullable heartbeatHeaderValue =
        FIRHeaderValueFromHeartbeatsPayload(heartbeatsPayload);
    if (heartbeatHeaderValue) {
      expectedHTTPHeaderFields[@"X-firebase-client"] = heartbeatHeaderValue;
    }

    XCTAssertEqualObjects([request allHTTPHeaderFields], expectedHTTPHeaderFields);

    NSString *expectedAuthHeader =
        [NSString stringWithFormat:@"FIS_v2 %@", installation.refreshToken];
    XCTAssertEqualObjects(request.allHTTPHeaderFields[@"Authorization"], expectedAuthHeader, @"%@",
                          self.name);

    NSError *error;
    NSDictionary *JSONBody = [NSJSONSerialization JSONObjectWithData:request.HTTPBody
                                                             options:0
                                                               error:&error];
    XCTAssertNotNil(JSONBody, @"Error: %@", error);
    XCTAssertEqualObjects(JSONBody, @{});

    return YES;
  }];
}

- (void)assertRegisterInstallationSuccessWithResponseFixtureName:(NSString *)fixtureName
                                                    responseCode:(NSInteger)responseCode
                                             expectedFIDOverride:(nullable NSString *)overrideFID
                                               heartbeatsPayload:
                                                   (FIRHeartbeatsPayload *)heartbeatsPayload {
  FIRInstallationsItem *installation = [FIRInstallationsItem createUnregisteredInstallationItem];
  installation.IIDDefaultToken = @"iid-auth-token";

  NSString *expectedFID = overrideFID ?: installation.firebaseInstallationID;

  // 1. Stub URL session:

  // 1.1. URL request validation.
  id URLRequestValidation = [OCMArg checkWithBlock:^BOOL(NSURLRequest *request) {
    XCTAssertEqualObjects(request.HTTPMethod, @"POST");
    XCTAssertEqualObjects(
        request.URL.absoluteString,
        @"https://firebaseinstallations.googleapis.com/v1/projects/project-id/installations/");

    NSMutableDictionary<NSString *, NSString *> *expectedHTTPHeaderFields = @{
      @"Content-Type" : @"application/json",
      @"X-Goog-Api-Key" : self.APIKey,
      @"X-Ios-Bundle-Identifier" : [[NSBundle mainBundle] bundleIdentifier],
    }
                                                                                .mutableCopy;

    NSString *_Nullable heartbeatHeaderValue =
        FIRHeaderValueFromHeartbeatsPayload(heartbeatsPayload);
    if (heartbeatHeaderValue) {
      expectedHTTPHeaderFields[@"X-firebase-client"] = heartbeatHeaderValue;
    }

    [expectedHTTPHeaderFields addEntriesFromDictionary:@{
      @"x-goog-fis-ios-iid-migration-auth" : installation.IIDDefaultToken
    }];

    XCTAssertEqualObjects([request allHTTPHeaderFields], expectedHTTPHeaderFields);

    NSError *error;
    NSDictionary *body = [NSJSONSerialization JSONObjectWithData:request.HTTPBody
                                                         options:0
                                                           error:&error];
    XCTAssertNotNil(body, @"Error: %@", error);

    XCTAssertEqualObjects(body[@"fid"], installation.firebaseInstallationID);
    XCTAssertEqualObjects(body[@"authVersion"], @"FIS_v2");
    XCTAssertEqualObjects(body[@"appId"], installation.appID);

    XCTAssertEqualObjects(body[@"sdkVersion"], [self SDKVersion]);

    return YES;
  }];

  // 1.2. Capture completion to call it later.
  __block void (^taskCompletion)(NSData *, NSURLResponse *, NSError *);
  id completionArg = [OCMArg checkWithBlock:^BOOL(id obj) {
    taskCompletion = obj;
    return YES;
  }];

  // 1.3. Create a data task mock.
  id mockDataTask = OCMClassMock([NSURLSessionDataTask class]);
  OCMExpect([(NSURLSessionDataTask *)mockDataTask resume]);

  // 1.4. Expect `dataTaskWithRequest` to be called.
  OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
                                   completionHandler:completionArg])
      .andReturn(mockDataTask);

  // 2. Call
  FBLPromise<FIRInstallationsItem *> *promise = [self.service registerInstallation:installation];

  // 3. Wait for `[NSURLSession dataTaskWithRequest...]` to be called
  OCMVerifyAllWithDelay(self.mockURLSession, 0.5);

  // 4. Wait for the data task `resume` to be called.
  OCMVerifyAllWithDelay(mockDataTask, 0.5);

  // 5. Call the data task completion.
  NSData *responseData = [self loadFixtureNamed:fixtureName];
  taskCompletion(responseData, [self responseWithStatusCode:responseCode], nil);

  // 6. Check result.
  XCTAssert(FBLWaitForPromisesWithTimeout(0.5));

  XCTAssertNil(promise.error);
  XCTAssertNotNil(promise.value);

  XCTAssertNotEqual(promise.value, installation);
  XCTAssertEqualObjects(promise.value.appID, installation.appID);
  XCTAssertEqualObjects(promise.value.firebaseAppName, installation.firebaseAppName);

  // Server may respond with a different FID if the sent FID cannot be accepted.
  XCTAssertEqualObjects(promise.value.firebaseInstallationID, expectedFID);
  XCTAssertEqualObjects(promise.value.refreshToken, @"aaaaaaabbbbbbbbcccccccccdddddddd00000000");
  XCTAssertEqualObjects(promise.value.authToken.token,
                        @"aaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbb.cccccccccccccccccccccccc");
  [self assertDate:promise.value.authToken.expirationDate
      isApproximatelyEqualCurrentPlusTimeInterval:604800];
}

- (NSString *)SDKVersion {
  return [NSString stringWithFormat:@"i:%@", FIRFirebaseVersion()];
}

@end
