/*
 * 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 "Example/InstanceID/Tests/FIRInstanceIDFakeKeychain.h"
#import "Firebase/InstanceID/FIRInstanceIDAuthKeyChain.h"
#import "Firebase/InstanceID/FIRInstanceIDBackupExcludedPlist.h"
#import "Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h"
#import "Firebase/InstanceID/FIRInstanceIDCheckinService.h"
#import "Firebase/InstanceID/FIRInstanceIDCheckinStore.h"
#import "Firebase/InstanceID/FIRInstanceIDStore.h"
#import "Firebase/InstanceID/FIRInstanceIDUtilities.h"
#import "Firebase/InstanceID/Private/FIRInstanceIDCheckinPreferences.h"

static const NSTimeInterval kExpectationTimeout = 12;

@interface FIRInstanceIDCheckinStore ()
- (NSString *)bundleIdentifierForKeychainAccount;
@end

// Testing constants
static NSString *const kFakeCheckinPlistName = @"com.google.test.IIDStoreTestCheckin";
static NSString *const kSubDirectoryName = @"FirebaseInstanceIDCheckinTest";

static NSString *const kAuthID = @"test-auth-id";
static NSString *const kDigest = @"test-digest";
static NSString *const kSecret = @"test-secret";
static NSString *const kFakeErrorDomain = @"fakeDomain";
static const NSUInteger kFakeErrorCode = -1;

static int64_t const kLastCheckinTimestamp = 123456;

@interface FIRInstanceIDCheckinStoreTest : XCTestCase

@end

@implementation FIRInstanceIDCheckinStoreTest

- (void)setUp {
  [super setUp];
  [FIRInstanceIDStore createSubDirectory:kSubDirectoryName];
}

- (void)tearDown {
  NSString *path = [self pathForCheckinPlist];
  if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
    NSError *error;
    [[NSFileManager defaultManager] removeItemAtPath:path error:&error];
  }
  [FIRInstanceIDStore removeSubDirectory:kSubDirectoryName error:nil];
  [super tearDown];
}

/**
 *  Keychain read failure should lead to checkin preferences with invalid credentials.
 */
- (void)testInvalidCheckinPreferencesOnKeychainFail {
  XCTestExpectation *checkinInvalidExpectation = [self
      expectationWithDescription:@"Checkin preference should be invalid after keychain failure"];
  FIRInstanceIDBackupExcludedPlist *checkinPlist =
      [[FIRInstanceIDBackupExcludedPlist alloc] initWithFileName:kFakeCheckinPlistName
                                                    subDirectory:kSubDirectoryName];

  FIRInstanceIDFakeKeychain *fakeKeychain = [[FIRInstanceIDFakeKeychain alloc] init];

  FIRInstanceIDCheckinStore *checkinStore =
      [[FIRInstanceIDCheckinStore alloc] initWithCheckinPlist:checkinPlist keychain:fakeKeychain];
  __block FIRInstanceIDCheckinPreferences *preferences =
      [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kAuthID secretToken:kSecret];
  [preferences updateWithCheckinPlistContents:[[self class] newCheckinPlistPreferences]];
  [checkinStore saveCheckinPreferences:preferences
                               handler:^(NSError *error) {
                                 XCTAssertNil(error);
                                 fakeKeychain.cannotReadFromKeychain = YES;
                                 preferences = [checkinStore cachedCheckinPreferences];

                                 XCTAssertNil(preferences.deviceID);
                                 XCTAssertNil(preferences.secretToken);
                                 XCTAssertFalse([preferences hasValidCheckinInfo]);

                                 [checkinInvalidExpectation fulfill];
                               }];
  [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
}

/**
 *  CheckinStore should not be able to save the checkin preferences if the write to the
 *  Keychain fails.
 */
- (void)testCheckinSaveFailsOnKeychainWriteFailure {
  XCTestExpectation *checkinSaveFailsExpectation =
      [self expectationWithDescription:@"Checkin save should fail after keychain write failure"];
  FIRInstanceIDBackupExcludedPlist *checkinPlist =
      [[FIRInstanceIDBackupExcludedPlist alloc] initWithFileName:kFakeCheckinPlistName
                                                    subDirectory:kSubDirectoryName];
  FIRInstanceIDFakeKeychain *fakeKeychain = [[FIRInstanceIDFakeKeychain alloc] init];
  fakeKeychain.cannotWriteToKeychain = YES;

  FIRInstanceIDCheckinStore *checkinStore =
      [[FIRInstanceIDCheckinStore alloc] initWithCheckinPlist:checkinPlist keychain:fakeKeychain];

  __block FIRInstanceIDCheckinPreferences *preferences =
      [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kAuthID secretToken:kSecret];
  [preferences updateWithCheckinPlistContents:[[self class] newCheckinPlistPreferences]];
  [checkinStore saveCheckinPreferences:preferences
                               handler:^(NSError *error) {
                                 XCTAssertNotNil(error);

                                 preferences = [checkinStore cachedCheckinPreferences];
                                 XCTAssertNil(preferences.deviceID);
                                 XCTAssertNil(preferences.secretToken);
                                 XCTAssertFalse([preferences hasValidCheckinInfo]);
                                 [checkinSaveFailsExpectation fulfill];
                               }];
  [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
}

- (void)testCheckinSaveFailsOnPlistWriteFailure {
  XCTestExpectation *checkinSaveFailsExpectation =
      [self expectationWithDescription:@"Checkin save should fail after plist write failure"];
  FIRInstanceIDBackupExcludedPlist *checkinPlist =
      [[FIRInstanceIDBackupExcludedPlist alloc] initWithFileName:kFakeCheckinPlistName
                                                    subDirectory:kSubDirectoryName];
  id plistMock = OCMPartialMock(checkinPlist);
  NSError *error = [NSError errorWithDomain:kFakeErrorDomain code:kFakeErrorCode userInfo:nil];
  OCMStub([plistMock writeDictionary:[OCMArg any] error:[OCMArg setTo:error]]).andReturn(NO);

  FIRInstanceIDFakeKeychain *fakeKeychain = [[FIRInstanceIDFakeKeychain alloc] init];

  FIRInstanceIDCheckinStore *checkinStore =
      [[FIRInstanceIDCheckinStore alloc] initWithCheckinPlist:plistMock keychain:fakeKeychain];

  __block FIRInstanceIDCheckinPreferences *preferences =
      [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kAuthID secretToken:kSecret];
  [preferences updateWithCheckinPlistContents:[[self class] newCheckinPlistPreferences]];
  [checkinStore saveCheckinPreferences:preferences
                               handler:^(NSError *error) {
                                 XCTAssertNotNil(error);
                                 XCTAssertEqual(error.code, kFakeErrorCode);

                                 preferences = [checkinStore cachedCheckinPreferences];
                                 XCTAssertNil(preferences.deviceID);
                                 XCTAssertNil(preferences.secretToken);
                                 XCTAssertFalse([preferences hasValidCheckinInfo]);
                                 [checkinSaveFailsExpectation fulfill];
                               }];
  [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
}

- (void)testCheckinSaveSuccess {
  XCTestExpectation *checkinSaveSuccessExpectation =
      [self expectationWithDescription:@"Checkin save should succeed"];
  FIRInstanceIDBackupExcludedPlist *checkinPlist =
      [[FIRInstanceIDBackupExcludedPlist alloc] initWithFileName:kFakeCheckinPlistName
                                                    subDirectory:kSubDirectoryName];
  id plistMock = OCMPartialMock(checkinPlist);

  FIRInstanceIDFakeKeychain *fakeKeychain = [[FIRInstanceIDFakeKeychain alloc] init];
  FIRInstanceIDCheckinStore *checkinStore =
      [[FIRInstanceIDCheckinStore alloc] initWithCheckinPlist:plistMock keychain:fakeKeychain];

  __block FIRInstanceIDCheckinPreferences *preferences =
      [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kAuthID secretToken:kSecret];
  [preferences updateWithCheckinPlistContents:[[self class] newCheckinPlistPreferences]];
  [checkinStore saveCheckinPreferences:preferences
                               handler:^(NSError *error) {
                                 XCTAssertNil(error);

                                 preferences = [checkinStore cachedCheckinPreferences];
                                 XCTAssertEqualObjects(preferences.deviceID, kAuthID);
                                 XCTAssertEqualObjects(preferences.secretToken, kSecret);
                                 [checkinSaveSuccessExpectation fulfill];
                               }];
  [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
}

#pragma mark - Private Helpers

- (BOOL)savePreferencesToPlist:(NSDictionary *)preferences {
  NSString *path = [self pathForCheckinPlist];
  return [preferences writeToFile:path atomically:YES];
}

- (NSString *)pathForCheckinPlist {
  NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
  NSString *plistNameWithExtension = [NSString stringWithFormat:@"%@.plist", kFakeCheckinPlistName];
  return [paths[0] stringByAppendingPathComponent:plistNameWithExtension];
}

+ (NSDictionary *)checkinPreferences {
  return @{
    kFIRInstanceIDDeviceAuthIdKey : kAuthID,
    kFIRInstanceIDSecretTokenKey : kSecret,
    kFIRInstanceIDDigestStringKey : kDigest,
    kFIRInstanceIDGServicesDictionaryKey : @{},
    kFIRInstanceIDLastCheckinTimeKey : @(kLastCheckinTimestamp),
  };
}

+ (NSDictionary *)newCheckinPlistPreferences {
  NSMutableDictionary *oldPreferences = [[self checkinPreferences] mutableCopy];
  [oldPreferences removeObjectForKey:kFIRInstanceIDDeviceAuthIdKey];
  [oldPreferences removeObjectForKey:kFIRInstanceIDSecretTokenKey];
  oldPreferences[kFIRInstanceIDLastCheckinTimeKey] =
      @(FIRInstanceIDCurrentTimestampInMilliseconds() - 1000);
  return [oldPreferences copy];
}

@end
