/*
 * 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 <OCMock/OCMock.h>
#import <XCTest/XCTest.h>

#import <sqlite3.h>

#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
#import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigContent.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigDefines.h"
#import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h"

@interface RCNConfigDBManager (Test)
- (void)removeDatabaseOnDatabaseQueueAtPath:(NSString *)path;
- (void)insertExperimentTableWithKey:(NSString *)key
                               value:(NSData *)serializedValue
                   completionHandler:(RCNDBCompletion)handler;
- (void)deleteExperimentTableForKey:(NSString *)key;
- (void)createOrOpenDatabase;
@end

@interface RCNConfigDBManagerTest : XCTestCase {
  NSString *_DBPath;
}
@property(nonatomic, strong) RCNConfigDBManager *DBManager;
@property(nonatomic, assign) NSTimeInterval expectionTimeout;
@end

@implementation RCNConfigDBManagerTest

- (void)setUp {
  [super setUp];
  // always remove the database at the start of testing
  _DBPath = [RCNTestUtilities remoteConfigPathForTestDatabase];

  _expectionTimeout = 10.0;
  id classMock = OCMClassMock([RCNConfigDBManager class]);
  OCMStub([classMock remoteConfigPathForDatabase]).andReturn(_DBPath);
  _DBManager = [[RCNConfigDBManager alloc] init];
}

- (void)tearDown {
  // Causes crash if main thread exits before the RCNConfigDB queue cleans up
  //  [_DBManager removeDatabaseOnDatabaseQueueAtPath:_DBPath];
}

- (void)testV1NamespaceMigrationToV2Namespace {
  // Write v1 namespace.
  XCTestExpectation *loadConfigContentExpectation =
      [self expectationWithDescription:@"test v1 namespace migration to v2 namespace"];
  NSString *namespace_p = @"testNamespace";
  NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier;
  __block int count = 0;
  for (int i = 0; i <= 100; ++i) {
    // Check namespace is updated after database write is completed.
    RCNDBCompletion insertCompletion = ^void(BOOL success,
                                             NSDictionary<NSString *, NSString *> *result) {
      count++;
      XCTAssertTrue(success);
      if (count == 100) {
        // Migrate to the new namespace.
        [self->_DBManager createOrOpenDatabase];
        [self->_DBManager
            loadMainWithBundleIdentifier:bundleIdentifier
                       completionHandler:^(
                           BOOL loadSuccess,
                           NSDictionary<NSString *, NSDictionary<NSString *, id> *> *fetchedConfig,
                           NSDictionary<NSString *, NSDictionary<NSString *, id> *> *activeConfig,
                           NSDictionary<NSString *, NSDictionary<NSString *, id> *> *defaultConfig,
                           NSDictionary *unusedRolloutMetadata) {
                         XCTAssertTrue(loadSuccess);
                         NSString *fullyQualifiedNamespace =
                             [NSString stringWithFormat:@"%@:%@", namespace_p, kFIRDefaultAppName];
                         XCTAssertNotNil(fetchedConfig[fullyQualifiedNamespace]);
                         XCTAssertEqual([fetchedConfig[fullyQualifiedNamespace] count], 101U);
                         XCTAssertEqual([fetchedConfig[namespace_p] count], 0);
                         if (loadSuccess) {
                           [loadConfigContentExpectation fulfill];
                         }
                       }];
      }
    };
    NSString *value = [NSString stringWithFormat:@"value%d", i];
    NSString *key = [NSString stringWithFormat:@"key%d", i];
    NSArray<id> *values =
        @[ bundleIdentifier, namespace_p, key, [value dataUsingEncoding:NSUTF8StringEncoding] ];
    [_DBManager insertMainTableWithValues:values
                               fromSource:RCNDBSourceFetched
                        completionHandler:insertCompletion];
  }

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

- (void)testWriteAndLoadMainTableResult {
  XCTestExpectation *loadConfigContentExpectation =
      [self expectationWithDescription:@"Write and read metadata in database serializedly"];
  NSString *namespace_p = @"namespace_1";
  NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier;
  __block int count = 0;
  for (int i = 0; i <= 100; ++i) {
    // check DB write correctly
    RCNDBCompletion insertCompletion = ^void(BOOL success, NSDictionary *result) {
      count++;
      XCTAssertTrue(success);
      if (count == 100) {
        // check DB read correctly
        [self->_DBManager
            loadMainWithBundleIdentifier:bundleIdentifier
                       completionHandler:^(BOOL success, NSDictionary *fetchedConfig,
                                           NSDictionary *activeConfig, NSDictionary *defaultConfig,
                                           NSDictionary *unusedRolloutMetadata) {
                         NSMutableDictionary *res = [fetchedConfig mutableCopy];
                         XCTAssertTrue(success);
                         FIRRemoteConfigValue *value = res[namespace_p][@"key100"];
                         XCTAssertEqualObjects(value.stringValue, @"value100");
                         if (success) {
                           [loadConfigContentExpectation fulfill];
                         }
                       }];
      }
    };
    NSString *value = [NSString stringWithFormat:@"value%d", i];
    NSString *key = [NSString stringWithFormat:@"key%d", i];
    NSArray *values =
        @[ bundleIdentifier, namespace_p, key, [value dataUsingEncoding:NSUTF8StringEncoding] ];
    [_DBManager insertMainTableWithValues:values
                               fromSource:RCNDBSourceFetched
                        completionHandler:insertCompletion];
  }

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

- (void)testWriteAndLoadMetadataResult {
  XCTestExpectation *writeAndLoadMetadataExpectation =
      [self expectationWithDescription:@"Write and load metadata in database successfully"];
  NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier;
  NSString *namespace = @"test_namespace";
  NSTimeInterval lastFetchTimestamp = [NSDate date].timeIntervalSince1970;

  NSDictionary *deviceContext =
      @{@"app_version" : @"1.0.1", @"app_build" : @"1.0.1.11", @"os_version" : @"iOS9.1"};
  NSDictionary *syncedDBCustomVariables = @{@"user_level" : @15, @"user_experiences" : @"2468"};
  NSArray *successFetchTimes = @[];
  NSTimeInterval now = [NSDate date].timeIntervalSince1970;
  NSArray *failureFetchTimes =
      @[ [NSNumber numberWithDouble:now - 200], [NSNumber numberWithDouble:now] ];

  // serialize objects
  NSError *error;
  NSData *serializedAppContext = [NSJSONSerialization dataWithJSONObject:syncedDBCustomVariables
                                                                 options:NSJSONWritingPrettyPrinted
                                                                   error:&error];
  NSData *serializedDeviceContext =
      [NSJSONSerialization dataWithJSONObject:deviceContext
                                      options:NSJSONWritingPrettyPrinted
                                        error:&error];
  NSData *serializedDigestPerNamespace =
      [NSJSONSerialization dataWithJSONObject:@{} options:NSJSONWritingPrettyPrinted error:&error];
  NSData *serializedSuccessTime = [NSJSONSerialization dataWithJSONObject:successFetchTimes
                                                                  options:NSJSONWritingPrettyPrinted
                                                                    error:&error];
  NSData *serializedFailureTime = [NSJSONSerialization dataWithJSONObject:failureFetchTimes
                                                                  options:NSJSONWritingPrettyPrinted
                                                                    error:&error];
  NSDictionary *columnNameToValue = @{
    RCNKeyBundleIdentifier : bundleIdentifier,
    RCNKeyNamespace : namespace,
    RCNKeyFetchTime : @(lastFetchTimestamp),
    RCNKeyDigestPerNamespace : serializedDigestPerNamespace,
    RCNKeyDeviceContext : serializedDeviceContext,
    RCNKeyAppContext : serializedAppContext,
    RCNKeySuccessFetchTime : serializedSuccessTime,
    RCNKeyFailureFetchTime : serializedFailureTime,
    RCNKeyLastFetchStatus : @(FIRRemoteConfigFetchStatusSuccess),
    RCNKeyLastFetchError : @(FIRRemoteConfigErrorUnknown),
    RCNKeyLastApplyTime : @(now - 100),
    RCNKeyLastSetDefaultsTime : @(now - 200)
  };

  RCNDBCompletion completion = ^(BOOL success, NSDictionary *result1) {
    NSDictionary *result = [self->_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier
                                                                    namespace:namespace];
    XCTAssertNotNil(result);
    XCTAssertEqualObjects(result[RCNKeyBundleIdentifier], bundleIdentifier);
    XCTAssertEqual([result[RCNKeyFetchTime] doubleValue], lastFetchTimestamp);
    XCTAssertEqualObjects([result[RCNKeyDigestPerNamespace] copy], @{});
    XCTAssertEqualObjects([result[RCNKeyDeviceContext] copy], deviceContext);
    XCTAssertEqualObjects([result[RCNKeyAppContext] copy], syncedDBCustomVariables);
    XCTAssertEqualObjects([result[RCNKeySuccessFetchTime] copy], successFetchTimes);
    // TODO(chliang): Fix the flakiness caused by the commented out test
    // XCTAssertTrue([[result[RCNKeyFailureFetchTime] copy] isEqualToArray:failureFetchTimes]);
    XCTAssertEqual([result[RCNKeyLastFetchStatus] intValue],
                   (int)FIRRemoteConfigFetchStatusSuccess);
    XCTAssertEqual([result[RCNKeyLastFetchError] intValue], (int)FIRRemoteConfigErrorUnknown);
    XCTAssertEqual([result[RCNKeyLastApplyTime] doubleValue], now - 100);
    XCTAssertEqual([result[RCNKeyLastSetDefaultsTime] doubleValue], now - 200);
    [writeAndLoadMetadataExpectation fulfill];
  };

  [_DBManager insertMetadataTableWithValues:columnNameToValue completionHandler:completion];
  [self waitForExpectationsWithTimeout:_expectionTimeout
                               handler:^(NSError *error) {
                                 XCTAssertNil(error);
                               }];
}

- (void)testWriteAndLoadMetadataForMultipleNamespaces {
  XCTestExpectation *writeAndLoadMetadataForMultipleNamespacesExpectation =
      [self expectationWithDescription:@"Metadata is stored and read based on namespace"];
  NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier;
  NSDictionary *deviceContext = @{};
  NSDictionary *syncedDBCustomVariables = @{};
  NSError *error;
  NSData *serializedAppContext = [NSJSONSerialization dataWithJSONObject:syncedDBCustomVariables
                                                                 options:NSJSONWritingPrettyPrinted
                                                                   error:&error];
  NSData *serializedDeviceContext =
      [NSJSONSerialization dataWithJSONObject:deviceContext
                                      options:NSJSONWritingPrettyPrinted
                                        error:&error];
  NSData *serializedDigestPerNamespace =
      [NSJSONSerialization dataWithJSONObject:@{} options:NSJSONWritingPrettyPrinted error:&error];
  NSData *serializedSuccessTime = [NSJSONSerialization dataWithJSONObject:@[]
                                                                  options:NSJSONWritingPrettyPrinted
                                                                    error:&error];
  NSData *serializedFailureTime = [NSJSONSerialization dataWithJSONObject:@[]
                                                                  options:NSJSONWritingPrettyPrinted
                                                                    error:&error];

  // Metadata for first namespace
  NSString *namespace = @"test_namespace";
  double lastApplyTime = 100;
  double lastSetDefaultsTime = 200;
  NSDictionary *valuesForNamespace = @{
    RCNKeyBundleIdentifier : bundleIdentifier,
    RCNKeyNamespace : namespace,
    RCNKeyFetchTime : @(0),
    RCNKeyDigestPerNamespace : serializedDigestPerNamespace,
    RCNKeyDeviceContext : serializedDeviceContext,
    RCNKeyAppContext : serializedAppContext,
    RCNKeySuccessFetchTime : serializedSuccessTime,
    RCNKeyFailureFetchTime : serializedFailureTime,
    RCNKeyLastFetchStatus : @(FIRRemoteConfigFetchStatusSuccess),
    RCNKeyLastFetchError : @(FIRRemoteConfigErrorUnknown),
    RCNKeyLastApplyTime : @(lastApplyTime),
    RCNKeyLastSetDefaultsTime : @(lastSetDefaultsTime)
  };

  // Metadata for second namespace
  NSString *namespace2 = @"test_namespace_2";
  double lastApplyTime2 = 300;
  double lastSetDefaultsTime2 = 400;
  NSDictionary *valuesForNamespace2 = @{
    RCNKeyBundleIdentifier : bundleIdentifier,
    RCNKeyNamespace : namespace2,
    RCNKeyFetchTime : @(0),
    RCNKeyDigestPerNamespace : serializedDigestPerNamespace,
    RCNKeyDeviceContext : serializedDeviceContext,
    RCNKeyAppContext : serializedAppContext,
    RCNKeySuccessFetchTime : serializedSuccessTime,
    RCNKeyFailureFetchTime : serializedFailureTime,
    RCNKeyLastFetchStatus : @(FIRRemoteConfigFetchStatusSuccess),
    RCNKeyLastFetchError : @(FIRRemoteConfigErrorUnknown),
    RCNKeyLastApplyTime : @(lastApplyTime2),
    RCNKeyLastSetDefaultsTime : @(lastSetDefaultsTime2)
  };

  RCNDBCompletion insertMetadataCompletion = ^void(BOOL success, NSDictionary *result) {
    XCTAssertTrue(success);

    // Load metadata for both namespaces and verify they retain their separate values
    NSDictionary *resultForNamespace =
        [self->_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier namespace:namespace];
    NSDictionary *resultForNamespace2 =
        [self->_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier namespace:namespace2];

    XCTAssertNotNil(resultForNamespace);
    XCTAssertEqual([resultForNamespace[RCNKeyLastApplyTime] doubleValue], lastApplyTime);
    XCTAssertEqual([resultForNamespace[RCNKeyLastSetDefaultsTime] doubleValue],
                   lastSetDefaultsTime);

    XCTAssertNotNil(resultForNamespace2);
    XCTAssertEqual([resultForNamespace2[RCNKeyLastApplyTime] doubleValue], lastApplyTime2);
    XCTAssertEqual([resultForNamespace2[RCNKeyLastSetDefaultsTime] doubleValue],
                   lastSetDefaultsTime2);
    [writeAndLoadMetadataForMultipleNamespacesExpectation fulfill];
  };

  // Write metadata for first namespace
  [_DBManager insertMetadataTableWithValues:valuesForNamespace
                          completionHandler:^(BOOL success, NSDictionary *result1) {
                            XCTAssertTrue(success);
                            // Write metadata for second namespace
                            [self->_DBManager
                                insertMetadataTableWithValues:valuesForNamespace2
                                            completionHandler:insertMetadataCompletion];
                          }];
  [self waitForExpectationsWithTimeout:_expectionTimeout
                               handler:^(NSError *error) {
                                 XCTAssertNil(error);
                               }];
}

// Create a key each for two namespaces, delete it from one namespace, read both namespaces.
- (void)testDeleteParamAndLoadMainTable {
  XCTestExpectation *namespaceDeleteExpectation =
      [self expectationWithDescription:@"Contents of 'namespace_delete' should be deleted."];
  XCTestExpectation *namespaceKeepExpectation =
      [self expectationWithDescription:@"Write a key to namespace_keep and read back again."];
  NSString *namespaceToDelete = @"namespace_delete";
  NSString *namespaceToKeep = @"namespace_keep";
  NSString *bundleIdentifier = @"testBundleID";

  // Write something to the database for both namespaces.
  // Completion handler for the write to namespace_delete namespace.
  RCNDBCompletion insertNamespace1Completion = ^void(BOOL success, NSDictionary *result) {
    XCTAssertTrue(success);

    // Delete the key for given namespace.
    [self->_DBManager deleteRecordFromMainTableWithNamespace:namespaceToDelete
                                            bundleIdentifier:bundleIdentifier
                                                  fromSource:RCNDBSourceActive];

    // Read from the database and verify expected values.
    [self->_DBManager
        loadMainWithBundleIdentifier:bundleIdentifier
                   completionHandler:^(BOOL success, NSDictionary *fetchedConfig,
                                       NSDictionary *activeConfig, NSDictionary *defaultConfig,
                                       NSDictionary *unusedRolloutMetadata) {
                     NSMutableDictionary *res = [activeConfig mutableCopy];
                     XCTAssertTrue(success);
                     FIRRemoteConfigValue *value = res[namespaceToDelete][@"keyToDelete"];
                     XCTAssertNil(value);

                     FIRRemoteConfigValue *value2 = res[namespaceToKeep][@"keyToRetain"];
                     XCTAssertTrue([value2.stringValue isEqualToString:@"valueToRetain"]);

                     [namespaceDeleteExpectation fulfill];
                   }];
  };

  // Insert a key into the second namespace.
  RCNDBCompletion insertNamespace2Completion = ^void(BOOL success, NSDictionary *result) {
    XCTAssertTrue(success);

    // Ensure DB read succeeds.
    [self->_DBManager
        loadMainWithBundleIdentifier:bundleIdentifier
                   completionHandler:^(BOOL success, NSDictionary *fetchedConfig,
                                       NSDictionary *activeConfig, NSDictionary *defaultConfig,
                                       NSDictionary *unusedRolloutMetadata) {
                     NSMutableDictionary *res = [activeConfig mutableCopy];
                     XCTAssertTrue(success);
                     FIRRemoteConfigValue *value2 = res[namespaceToKeep][@"keyToRetain"];
                     XCTAssertTrue([value2.stringValue isEqualToString:@"valueToRetain"]);

                     [namespaceKeepExpectation fulfill];
                   }];
  };
  // We will delete this key after storing in the database.
  NSString *valueToDelete = @"valueToDelete";
  NSString *keyToDelete = @"keyToDelete";
  NSArray *items = @[
    bundleIdentifier, namespaceToDelete, keyToDelete,
    [valueToDelete dataUsingEncoding:NSUTF8StringEncoding]
  ];
  [_DBManager insertMainTableWithValues:items
                             fromSource:RCNDBSourceActive
                      completionHandler:insertNamespace1Completion];

  // This key value will be retained.
  NSString *valueToRetain = @"valueToRetain";
  NSString *keyToRetain = @"keyToRetain";
  NSArray *items2 = @[
    bundleIdentifier, namespaceToKeep, keyToRetain,
    [valueToRetain dataUsingEncoding:NSUTF8StringEncoding]
  ];
  [_DBManager insertMainTableWithValues:items2
                             fromSource:RCNDBSourceActive
                      completionHandler:insertNamespace2Completion];

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

- (void)testWriteAndLoadExperiments {
  XCTestExpectation *updateAndLoadExperimentExpectation =
      [self expectationWithDescription:@"Update and load experiment in database successfully"];

  NSError *error;
  NSArray *payload2 = @[ @"ab", @"cd" ];
  NSData *payloadData2 = [NSJSONSerialization dataWithJSONObject:payload2
                                                         options:NSJSONWritingPrettyPrinted
                                                           error:&error];
  NSDictionary *payload3 =
      @{@"experiment_ID" : @"35667", @"experiment_activate_name" : @"activate_game"};
  NSData *payloadData3 = [NSJSONSerialization dataWithJSONObject:payload3
                                                         options:NSJSONWritingPrettyPrinted
                                                           error:&error];
  NSArray *payloads = @[ [[NSData alloc] init], payloadData2, payloadData3 ];

  RCNDBCompletion writePayloadCompletion = ^(BOOL success, NSDictionary *result) {
    NSDictionary *metadata =
        @{@"last_known_start_time" : @(-11), @"experiment_new_metadata" : @"wonderful"};
    XCTAssertTrue(success);
    RCNDBCompletion writeMetadataCompletion = ^(BOOL success, NSDictionary *result) {
      XCTAssertTrue(success);
      RCNDBCompletion readCompletion = ^(BOOL success, NSDictionary *experimentResults) {
        XCTAssertTrue(success);
        XCTAssertNotNil(experimentResults[@RCNExperimentTableKeyPayload]);
        XCTAssertEqualObjects(payloads, experimentResults[@RCNExperimentTableKeyPayload]);

        XCTAssertNotNil(experimentResults[@RCNExperimentTableKeyMetadata]);
        XCTAssertEqualWithAccuracy(
            -11,
            [experimentResults[@RCNExperimentTableKeyMetadata][@"last_known_start_time"]
                doubleValue],
            1.0);
        XCTAssertEqualObjects(
            @"wonderful",
            experimentResults[@RCNExperimentTableKeyMetadata][@"experiment_new_metadata"]);
        [updateAndLoadExperimentExpectation fulfill];
      };
      [self->_DBManager loadExperimentWithCompletionHandler:readCompletion];
    };

    NSError *error;
    XCTAssertTrue([NSJSONSerialization isValidJSONObject:metadata]);
    NSData *serializedMetadata = [NSJSONSerialization dataWithJSONObject:metadata
                                                                 options:NSJSONWritingPrettyPrinted
                                                                   error:&error];

    [self->_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyMetadata
                                             value:serializedMetadata
                                 completionHandler:writeMetadataCompletion];
  };
  [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyPayload
                                     value:[[NSData alloc] init]
                         completionHandler:nil];
  [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyPayload
                                     value:payloadData2
                         completionHandler:nil];
  [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyPayload
                                     value:payloadData3
                         completionHandler:writePayloadCompletion];

  [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil];
}

- (void)testWriteAndLoadActivatedExperiments {
  XCTestExpectation *updateAndLoadExperimentExpectation =
      [self expectationWithDescription:@"Update and load experiment in database successfully"];

  NSError *error;
  NSArray *payload2 = @[ @"ab", @"cd" ];
  NSData *payloadData2 = [NSJSONSerialization dataWithJSONObject:payload2
                                                         options:NSJSONWritingPrettyPrinted
                                                           error:&error];
  NSDictionary *payload3 =
      @{@"experiment_ID" : @"35667", @"experiment_activate_name" : @"activate_game"};
  NSData *payloadData3 = [NSJSONSerialization dataWithJSONObject:payload3
                                                         options:NSJSONWritingPrettyPrinted
                                                           error:&error];
  NSArray *payloads = @[ [[NSData alloc] init], payloadData2, payloadData3 ];

  RCNDBCompletion writePayloadCompletion = ^(BOOL success, NSDictionary *result) {
    XCTAssertTrue(success);
    RCNDBCompletion readCompletion = ^(BOOL success, NSDictionary *experimentResults) {
      XCTAssertTrue(success);
      XCTAssertNotNil(experimentResults[@RCNExperimentTableKeyActivePayload]);
      XCTAssertEqualObjects(payloads, experimentResults[@RCNExperimentTableKeyActivePayload]);
      [updateAndLoadExperimentExpectation fulfill];
    };
    [self->_DBManager loadExperimentWithCompletionHandler:readCompletion];
  };
  [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActivePayload
                                     value:[[NSData alloc] init]
                         completionHandler:nil];
  [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActivePayload
                                     value:payloadData2
                         completionHandler:nil];
  [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActivePayload
                                     value:payloadData3
                         completionHandler:writePayloadCompletion];

  [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil];
}

- (void)testWriteAndLoadMetadataMultipleTimes {
  XCTestExpectation *updateAndLoadMetadataExpectation = [self
      expectationWithDescription:@"Update and load experiment metadata in database successfully"];

  RCNDBCompletion readCompletion = ^(BOOL success, NSDictionary *experimentResults) {
    XCTAssertTrue(success);
    XCTAssertNotNil(experimentResults[@RCNExperimentTableKeyPayload]);
    XCTAssertNotNil(experimentResults[@RCNExperimentTableKeyMetadata]);
    XCTAssertEqualWithAccuracy(
        12345678,
        [experimentResults[@RCNExperimentTableKeyMetadata][@"last_known_start_time"] doubleValue],
        1.0);
    XCTAssertEqualObjects(
        @"wonderful",
        experimentResults[@RCNExperimentTableKeyMetadata][@"experiment_new_metadata"]);

    [updateAndLoadMetadataExpectation fulfill];
  };
  NSDictionary *metadata =
      @{@"last_known_start_time" : @(-11), @"experiment_new_metadata" : @"wonderful"};
  NSError *error;
  XCTAssertTrue([NSJSONSerialization isValidJSONObject:metadata]);
  NSData *serializedMetadata = [NSJSONSerialization dataWithJSONObject:metadata
                                                               options:NSJSONWritingPrettyPrinted
                                                                 error:&error];

  [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyMetadata
                                     value:serializedMetadata
                         completionHandler:nil];

  metadata = @{@"last_known_start_time" : @(12345678), @"experiment_new_metadata" : @"wonderful"};
  XCTAssertTrue([NSJSONSerialization isValidJSONObject:metadata]);
  serializedMetadata = [NSJSONSerialization dataWithJSONObject:metadata
                                                       options:NSJSONWritingPrettyPrinted
                                                         error:&error];

  [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyMetadata
                                     value:serializedMetadata
                         completionHandler:nil];
  [_DBManager loadExperimentWithCompletionHandler:readCompletion];

  [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil];
}

- (void)testWriteAndLoadFetchedAndActiveRollout {
  XCTestExpectation *writeAndLoadFetchedRolloutExpectation =
      [self expectationWithDescription:@"Write and load rollout in database successfully"];

  NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier;

  NSArray *fetchedRollout = @[
    @{
      @"rollout_id" : @"1",
      @"variant_id" : @"B",
      @"affected_parameter_keys" : @[ @"key_1", @"key_2" ]
    },
    @{
      @"rollout_id" : @"2",
      @"variant_id" : @"1",
      @"affected_parameter_keys" : @[ @"key_1", @"key_3" ]
    }
  ];

  NSArray *activeRollout = @[
    @{
      @"rollout_id" : @"1",
      @"variant_id" : @"B",
      @"affected_parameter_keys" : @[ @"key_1", @"key_2" ]
    },
    @{
      @"rollout_id" : @"3",
      @"variant_id" : @"a",
      @"affected_parameter_keys" : @[ @"key_1", @"key_3" ]
    }
  ];

  RCNDBCompletion writeRolloutCompletion = ^(BOOL success, NSDictionary *result) {
    XCTAssertTrue(success);
    RCNDBLoadCompletion loadCompletion = ^(
        BOOL success, NSDictionary *unusedFetchedConfig, NSDictionary *unusedActiveConfig,
        NSDictionary *unusedDefaultConfig, NSDictionary *rolloutMetadata) {
      XCTAssertTrue(success);
      XCTAssertNotNil(rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]);
      XCTAssertEqualObjects(fetchedRollout, rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]);
      XCTAssertNotNil(rolloutMetadata[@RCNRolloutTableKeyActiveMetadata]);
      XCTAssertEqualObjects(activeRollout, rolloutMetadata[@RCNRolloutTableKeyActiveMetadata]);

      [writeAndLoadFetchedRolloutExpectation fulfill];
    };
    [self->_DBManager loadMainWithBundleIdentifier:bundleIdentifier
                                 completionHandler:loadCompletion];
  };
  [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyFetchedMetadata
                                          value:fetchedRollout
                              completionHandler:nil];
  [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyActiveMetadata
                                          value:activeRollout
                              completionHandler:writeRolloutCompletion];

  [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil];
}

- (void)testUpdateAndLoadRollout {
  XCTestExpectation *updateAndLoadFetchedRolloutExpectation =
      [self expectationWithDescription:@"Update and load rollout in database successfully"];

  NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier;

  NSArray *fetchedRollout = @[ @{
    @"rollout_id" : @"1",
    @"variant_id" : @"B",
    @"affected_parameter_keys" : @[ @"key_1", @"key_2" ]
  } ];

  NSArray *updatedFetchedRollout = @[
    @{
      @"rollout_id" : @"1",
      @"variant_id" : @"B",
      @"affected_parameter_keys" : @[ @"key_1", @"key_2" ]
    },
    @{
      @"rollout_id" : @"2",
      @"variant_id" : @"1",
      @"affected_parameter_keys" : @[ @"key_1", @"key_3" ]
    }
  ];

  RCNDBCompletion writeRolloutCompletion = ^(BOOL success, NSDictionary *result) {
    XCTAssertTrue(success);
    RCNDBLoadCompletion loadCompletion =
        ^(BOOL success, NSDictionary *unusedFetchedConfig, NSDictionary *unusedActiveConfig,
          NSDictionary *unusedDefaultConfig, NSDictionary *rolloutMetadata) {
          XCTAssertTrue(success);
          XCTAssertNotNil(rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]);
          XCTAssertEqualObjects(updatedFetchedRollout,
                                rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]);

          [updateAndLoadFetchedRolloutExpectation fulfill];
        };
    [self->_DBManager loadMainWithBundleIdentifier:bundleIdentifier
                                 completionHandler:loadCompletion];
  };
  [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyFetchedMetadata
                                          value:fetchedRollout
                              completionHandler:nil];
  [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyFetchedMetadata
                                          value:updatedFetchedRollout
                              completionHandler:writeRolloutCompletion];

  [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil];
}
- (void)testLoadEmptyRollout {
  XCTestExpectation *updateAndLoadFetchedRolloutExpectation =
      [self expectationWithDescription:@"Load empty rollout in database successfully"];

  NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier;

  NSArray *emptyResult = [[NSArray alloc] init];

  RCNDBLoadCompletion loadCompletion =
      ^(BOOL success, NSDictionary *unusedFetchedConfig, NSDictionary *unusedActiveConfig,
        NSDictionary *unusedDefaultConfig, NSDictionary *rolloutMetadata) {
        XCTAssertTrue(success);
        XCTAssertNotNil(rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]);
        XCTAssertEqualObjects(emptyResult, rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]);
        XCTAssertNotNil(rolloutMetadata[@RCNRolloutTableKeyActiveMetadata]);
        XCTAssertEqualObjects(emptyResult, rolloutMetadata[@RCNRolloutTableKeyActiveMetadata]);

        [updateAndLoadFetchedRolloutExpectation fulfill];
      };
  [self->_DBManager loadMainWithBundleIdentifier:bundleIdentifier completionHandler:loadCompletion];
  [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil];
}

- (void)testUpdateAndloadLastFetchStatus {
  XCTestExpectation *updateAndLoadMetadataExpectation = [self
      expectationWithDescription:@"Update and load last fetch status in database successfully."];
  NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier;
  NSString *namespace = @"test_namespace";

  // Metadata row must exist before update
  RCNDBCompletion createMetadataCompletion = ^(BOOL success, NSDictionary *createResult) {
    NSDictionary *result = [self->_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier
                                                                    namespace:namespace];
    XCTAssertTrue(success);
    XCTAssertNotNil(result);
    XCTAssertEqual([result[RCNKeyLastFetchStatus] intValue],
                   (int)FIRRemoteConfigFetchStatusSuccess);
    XCTAssertEqual([result[RCNKeyLastFetchError] intValue], (int)FIRRemoteConfigErrorUnknown);

    RCNDBCompletion updateMetadataCompletion = ^(BOOL success, NSDictionary *updateResult) {
      NSDictionary *result = [self->_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier
                                                                      namespace:namespace];

      XCTAssertTrue(success);
      XCTAssertNotNil(result);
      XCTAssertEqual([result[RCNKeyLastFetchStatus] intValue],
                     (int)FIRRemoteConfigFetchStatusThrottled);
      XCTAssertEqual([result[RCNKeyLastFetchError] intValue], (int)FIRRemoteConfigErrorThrottled);
      [updateAndLoadMetadataExpectation fulfill];
    };
    // Update with throttle status.
    [self->_DBManager
        updateMetadataWithOption:RCNUpdateOptionFetchStatus
                       namespace:namespace
                          values:@[
                            @(FIRRemoteConfigFetchStatusThrottled), @(FIRRemoteConfigErrorThrottled)
                          ]
               completionHandler:updateMetadataCompletion];
  };

  [_DBManager insertMetadataTableWithValues:[self createSampleMetadata]
                          completionHandler:createMetadataCompletion];
  [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil];
}

/// Tests that we can insert values in the database and can update them.
- (void)testInsertAndUpdateApplyTime {
  XCTestExpectation *updateAndLoadMetadataExpectation =
      [self expectationWithDescription:@"Update and load apply time in database successfully."];
  NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier;
  NSString *namespace = @"test_namespace";
  NSTimeInterval lastApplyTimestamp = [NSDate date].timeIntervalSince1970;

  // Metadata row must exist before update
  RCNDBCompletion createMetadataCompletion = ^(BOOL success, NSDictionary *createResult) {
    XCTAssertTrue(success);
    // Read newly created metadata.
    NSDictionary *result = [self->_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier
                                                                    namespace:namespace];
    XCTAssertNotNil(result);
    XCTAssertEqual([result[RCNKeyLastApplyTime] doubleValue], (double)100);
    RCNDBCompletion updateMetadataCompletion = ^(BOOL success, NSDictionary *updateResult) {
      NSDictionary *result = [self->_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier
                                                                      namespace:namespace];

      XCTAssertTrue(success);
      XCTAssertNotNil(result);
      XCTAssertEqual([result[RCNKeyLastApplyTime] doubleValue], lastApplyTimestamp);
      [updateAndLoadMetadataExpectation fulfill];
    };
    // Update apply config timestamp.
    [self->_DBManager updateMetadataWithOption:RCNUpdateOptionApplyTime
                                     namespace:namespace
                                        values:@[ @(lastApplyTimestamp) ]
                             completionHandler:updateMetadataCompletion];
  };

  [_DBManager insertMetadataTableWithValues:[self createSampleMetadata]
                          completionHandler:createMetadataCompletion];
  [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil];
}

- (void)testUpdateAndLoadSetDefaultsTime {
  XCTestExpectation *updateAndLoadMetadataExpectation = [self
      expectationWithDescription:@"Update and load set defaults time in database successfully."];
  NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier;
  NSString *namespace = @"test_namespace";
  NSTimeInterval lastSetDefaultsTimestamp = [NSDate date].timeIntervalSince1970;

  // Metadata row must exist before update
  RCNDBCompletion createMetadataCompletion = ^(BOOL success, NSDictionary *createResult) {
    NSDictionary *result = [self->_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier
                                                                    namespace:namespace];
    XCTAssertTrue(success);
    XCTAssertNotNil(result);
    XCTAssertEqual([result[RCNKeyLastSetDefaultsTime] doubleValue], (double)200);
    RCNDBCompletion updateMetadataCompletion = ^(BOOL success, NSDictionary *updateResult) {
      NSDictionary *result = [self->_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier
                                                                      namespace:namespace];

      XCTAssertTrue(success);
      XCTAssertNotNil(result);
      XCTAssertEqual([result[RCNKeyLastSetDefaultsTime] doubleValue], lastSetDefaultsTimestamp);
      [updateAndLoadMetadataExpectation fulfill];
    };
    // Update setting default config timestamp.
    [self->_DBManager updateMetadataWithOption:RCNUpdateOptionDefaultTime
                                     namespace:namespace
                                        values:@[ @(lastSetDefaultsTimestamp) ]
                             completionHandler:updateMetadataCompletion];
  };

  [_DBManager insertMetadataTableWithValues:[self createSampleMetadata]
                          completionHandler:createMetadataCompletion];
  [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil];
}

- (NSDictionary *)createSampleMetadata {
  NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier;
  NSString *namespace = @"test_namespace";

  NSDictionary *deviceContext = @{};
  NSDictionary *syncedDBCustomVariables = @{};
  NSArray *successFetchTimes = @[];
  NSArray *failureFetchTimes = @[];

  // serialize objects
  NSError *error;
  NSData *serializedAppContext = [NSJSONSerialization dataWithJSONObject:syncedDBCustomVariables
                                                                 options:NSJSONWritingPrettyPrinted
                                                                   error:&error];
  NSData *serializedDeviceContext =
      [NSJSONSerialization dataWithJSONObject:deviceContext
                                      options:NSJSONWritingPrettyPrinted
                                        error:&error];
  NSData *serializedDigestPerNamespace =
      [NSJSONSerialization dataWithJSONObject:@{} options:NSJSONWritingPrettyPrinted error:&error];
  NSData *serializedSuccessTime = [NSJSONSerialization dataWithJSONObject:successFetchTimes
                                                                  options:NSJSONWritingPrettyPrinted
                                                                    error:&error];
  NSData *serializedFailureTime = [NSJSONSerialization dataWithJSONObject:failureFetchTimes
                                                                  options:NSJSONWritingPrettyPrinted
                                                                    error:&error];
  return @{
    RCNKeyBundleIdentifier : bundleIdentifier,
    RCNKeyNamespace : namespace,
    RCNKeyFetchTime : @(0),
    RCNKeyDigestPerNamespace : serializedDigestPerNamespace,
    RCNKeyDeviceContext : serializedDeviceContext,
    RCNKeyAppContext : serializedAppContext,
    RCNKeySuccessFetchTime : serializedSuccessTime,
    RCNKeyFailureFetchTime : serializedFailureTime,
    RCNKeyLastFetchStatus : @(FIRRemoteConfigFetchStatusSuccess),
    RCNKeyLastFetchError : @(FIRRemoteConfigErrorUnknown),
    RCNKeyLastApplyTime : @(100),
    RCNKeyLastSetDefaultsTime : @(200)
  };
}

@end
