/*
 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
 *
 * @APPLE_LICENSE_HEADER_START@
 *
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apple Public Source License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. Please obtain a copy of the License at
 * http://www.opensource.apple.com/apsl/ and read it before using this
 * file.
 *
 * The Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 *
 * @APPLE_LICENSE_HEADER_END@
 */

#if OCTAGON

#import <Foundation/Foundation.h>

#import <XCTest/XCTest.h>
#import <OCMock/OCMock.h>

#import "OTTestsBase.h"

static NSString* OTCKRecordBottledPeerType = @"OTBottledPeer";
static NSString* OTCKRecordEscrowRecordID = @"escrowRecordID";

@interface OTCloudStoreUnitTests : OTTestsBase
@property (nonatomic, strong) OTBottledPeerRecord* fakeBottledPeerRecord;
@end

@implementation OTCloudStoreUnitTests


- (void)setUp {    
    [super setUp];
    self.continueAfterFailure = NO;
    self.fakeBottledPeerRecord = [[OTBottledPeerRecord alloc] init];
    self.fakeBottledPeerRecord.bottle = [@"bottled peer data" dataUsingEncoding:NSUTF8StringEncoding];
    self.fakeBottledPeerRecord.signatureUsingEscrowKey = [@"bottled peer escrow sig" dataUsingEncoding:NSUTF8StringEncoding];
    self.fakeBottledPeerRecord.signatureUsingPeerKey = [@"bottled peer peer sig" dataUsingEncoding:NSUTF8StringEncoding];
    self.fakeBottledPeerRecord.peerID = @"peer id";
    self.fakeBottledPeerRecord.spID = @"sos peer id";
    self.fakeBottledPeerRecord.escrowRecordID = @"escrowRecordID";
    self.fakeBottledPeerRecord.escrowedSigningSPKI = [@"escrowedSigningSPKI" dataUsingEncoding:kCFStringEncodingUTF8];
    self.fakeBottledPeerRecord.peerSigningSPKI = [@"peerSigningSPKI" dataUsingEncoding:kCFStringEncodingUTF8];
}

- (void)tearDown {
    self.zones = nil;
    self.operationQueue = nil;

    [super tearDown];
}

- (void)testWriteSameBottledPeerTwiceToFakeRecord {
    NSError* error = nil;
    
    NSMutableDictionary* recordDictionary = [NSMutableDictionary dictionaryWithObjectsAndKeys:[[NSNumber alloc] initWithInt:1], OTCKRecordBottledPeerType, nil];

    [self expectAddedCKModifyRecords:recordDictionary holdFetch:YES];
    [self startCKKSSubsystem];
    XCTAssertTrue([self.cloudStore uploadBottledPeerRecord:self.fakeBottledPeerRecord escrowRecordID:self.fakeBottledPeerRecord.escrowRecordID error:&error], @"should create bottled peer record");
    XCTAssertNil(error, "error should be nil");

    [self waitForCKModifications];
    OCMVerifyAllWithDelay(self.mockDatabase, 8);
    [self releaseCloudKitFetchHold];
    
    [self expectAddedCKModifyRecords:recordDictionary holdFetch:YES];

    XCTAssertTrue([self.cloudStore uploadBottledPeerRecord:self.fakeBottledPeerRecord escrowRecordID:self.fakeBottledPeerRecord.escrowRecordID error:&error], @"should create bottled peer record");
    XCTAssertNil(error, "error should be nil");
    
    [self waitForCKModifications];
    OCMVerifyAllWithDelay(self.mockDatabase, 8);
    [self releaseCloudKitFetchHold];
}

- (void)testWriteBottledPeerToFakeRecord {
    NSError* error = nil;
    
    NSMutableDictionary* recordDictionary = [NSMutableDictionary dictionary];
    recordDictionary[OTCKRecordBottledPeerType] = [[NSNumber alloc] initWithInt:1];
    
    [self expectAddedCKModifyRecords:recordDictionary holdFetch:YES];
    [self startCKKSSubsystem];

    XCTAssertTrue([self.cloudStore uploadBottledPeerRecord:self.fakeBottledPeerRecord escrowRecordID:self.fakeBottledPeerRecord.escrowRecordID error:&error], @"should create bottled peer record");
    XCTAssertNil(error, "error should be nil");
    
    [self waitForCKModifications];
    OCMVerifyAllWithDelay(self.mockDatabase, 8);
    [self releaseCloudKitFetchHold];
}

- (void)testWriteMultipleBottledPeersToSAMEFakeRecord {
    NSError* error = nil;
    
    NSMutableDictionary* recordDictionary = [NSMutableDictionary dictionary];
    
    recordDictionary[OTCKRecordBottledPeerType] = [[NSNumber alloc] initWithInt:1];

    [self startCKKSSubsystem];

    for(int i = 0; i < 10; i++){
        [self expectAddedCKModifyRecords:recordDictionary holdFetch:NO];

        XCTAssertTrue([self.cloudStore uploadBottledPeerRecord:self.fakeBottledPeerRecord escrowRecordID:self.fakeBottledPeerRecord.escrowRecordID error:&error], @"should create bottled peer record");

        [self waitForCKModifications];

        XCTAssertNil(error, "error should be nil");
        OCMVerifyAllWithDelay(self.mockDatabase, 8);
        [self releaseCloudKitFetchHold];
    }
}

- (void)testWriteBottledPeersToDifferentFakeRecord {
    NSError* error = nil;
    
    NSMutableDictionary* recordDictionary = [NSMutableDictionary dictionary];
    
    recordDictionary[OTCKRecordBottledPeerType] = [[NSNumber alloc] initWithInt:1];
    
    [self startCKKSSubsystem];
    
    for(int i = 0; i < 10; i++){
        [self expectAddedCKModifyRecords:recordDictionary holdFetch:YES];
        NSString *escrowID = [NSString stringWithFormat:@"bp-sospeer%d-hash", i];
        self.fakeBottledPeerRecord.escrowRecordID = escrowID;
        XCTAssertTrue([self.cloudStore uploadBottledPeerRecord:self.fakeBottledPeerRecord escrowRecordID:escrowID error:&error], @"should create bottled peer record");
        [self waitForCKModifications];
        
        XCTAssertNil(error, "error should be nil");
        OCMVerifyAllWithDelay(self.mockDatabase, 8);
        [self releaseCloudKitFetchHold];
    }
    XCTAssertTrue( [[self.cloudStore retrieveListOfEligibleEscrowRecordIDs:&error] count] == 10, @"should have 1 record");
}


- (void)testReadBottledPeerRecordFromCloudKit {
    NSError *error = nil;
    [self startCKKSSubsystem];
    
    CKRecord* newRecord = [[CKRecord alloc]initWithRecordType:OTCKRecordBottledPeerType];
    newRecord[OTCKRecordEscrowRecordID] = @"escrowRecordID";
    [self.otFakeZone addToZone:newRecord];

    [self.cloudStore notifyZoneChange:nil];

    [self waitForCKModifications];

    OCMVerifyAllWithDelay(self.mockDatabase, 8);
    XCTAssertTrue( [[self.cloudStore retrieveListOfEligibleEscrowRecordIDs:&error] count] > 0, @"should have 1 record");
}

-(void) testOTCloudStoreDownloadBP{
    NSError* error = nil;
    [self startCKKSSubsystem];

    CKRecord* newRecord = [[CKRecord alloc]initWithRecordType:OTCKRecordBottledPeerType];
    newRecord[OTCKRecordEscrowRecordID] = @"escrowRecordID";
    [self.otFakeZone addToZone:newRecord];
    
    XCTAssertTrue([self.cloudStore downloadBottledPeerRecord:&error] == YES, @"downloading records should succeed:%@", error);
    XCTAssertNil(error, @"error should be nil");
    
    [self waitForCKModifications];

    OCMVerifyAllWithDelay(self.mockDatabase, 8);

    XCTAssertNil(error, "error should be nil");
    XCTAssertEqual([[self.cloudStore retrieveListOfEligibleEscrowRecordIDs:&error] count], (unsigned long)1, @"should have 1 record");
    XCTAssertNil(error, "error should be nil");
}

-(void) testOTCloudStoreDownloadMultipleBP{
    NSError* error = nil;
    [self startCKKSSubsystem];
    
    for(int i = 0; i < 10; i++){
        CKRecord* newRecord = [[CKRecord alloc]initWithRecordType:OTCKRecordBottledPeerType zoneID:self.otZoneID];
        newRecord[OTCKRecordEscrowRecordID] = [NSString stringWithFormat:@"escrowRecordID%d", i];
        [self.otFakeZone addToZone:newRecord];
    }
    [self waitForCKModifications];

    XCTAssertTrue([self.cloudStore downloadBottledPeerRecord:&error] == YES, @"downloading records should succeed:%@", error);
    XCTAssertNil(error, @"error should be nil");
    [self waitForCKModifications];
    
    OCMVerifyAllWithDelay(self.mockDatabase, 8);
    
    XCTAssertNil(error, "error should be nil");
    XCTAssertEqual( [[self.cloudStore retrieveListOfEligibleEscrowRecordIDs:&error] count], (unsigned long)10, @"should have 1 record");
}

-(void) testOTCloudStoreUploadMultipleToSameRecord{
    NSError* error = nil;
    [self startCKKSSubsystem];
    CKRecord* newRecord = [[CKRecord alloc]initWithRecordType:OTCKRecordBottledPeerType zoneID:self.otZoneID];
    newRecord[OTCKRecordEscrowRecordID] = @"escrowRecordID";
    for(int i = 0; i < 10; i++){
        [self.otFakeZone addToZone:newRecord];
    }
    [self waitForCKModifications];
    
    XCTAssertTrue([self.cloudStore downloadBottledPeerRecord:&error] == YES, @"downloading records should succeed:%@", error);
    XCTAssertNil(error, @"error should be nil");
    [self waitForCKModifications];
    
    OCMVerifyAllWithDelay(self.mockDatabase, 8);
    
    XCTAssertNil(error, "error should be nil");
    XCTAssertEqual([[self.cloudStore retrieveListOfEligibleEscrowRecordIDs:&error] count], (unsigned long)1, @"should have 1 record");
}

-(void) testRemoveRecordIDs{

    [self startCKKSSubsystem];
    NSError *error = nil;
    CKRecord* newRecord = [[CKRecord alloc]initWithRecordType:OTCKRecordBottledPeerType zoneID:self.otZoneID];
    newRecord[OTCKRecordEscrowRecordID] = @"escrowRecordID";
    [self expectCKFetch];

    [self.otFakeZone addToZone:newRecord];
    [self waitForCKModifications];

    [self.cloudStore notifyZoneChange:nil];
    [self waitForCKModifications];

    XCTAssertTrue( [[self.cloudStore retrieveListOfEligibleEscrowRecordIDs:&error] count] == 1, @"should have 1 record");

    [self expectCKFetch];
    XCTAssertTrue([self.cloudStore downloadBottledPeerRecord:&error] == YES, @"downloading records should succeed:%@", error);
    XCTAssertNil(error, @"error should be nil");
    [self waitForCKModifications];
}

-(void) testFetchTimeout
{
    [self startCKKSSubsystem];
    
    NSError* error = nil;
    CKRecord* newRecord = [[CKRecord alloc]initWithRecordType:OTCKRecordBottledPeerType zoneID:self.otZoneID];
    newRecord[OTCKRecordEscrowRecordID] = @"escrowRecordID";

    [self holdCloudKitFetches];

    [self.cloudStore downloadBottledPeerRecord:&error];

    XCTAssertNotNil(error, "error should not be nil");
    XCTAssertTrue([(NSString*)error.userInfo[@"NSLocalizedDescription"] isEqualToString:@"Operation(CKKSResultOperation(cloudkit-fetch-and-process-changes)) timed out waiting to start for [<CKKSResultOperation(fetch-and-process-updates-watcher): ready>]"], "expecting timed out error");
}

-(void) testModifyRecordsTimeout
{
    NSError* error = nil;

    NSMutableDictionary* recordDictionary = [NSMutableDictionary dictionaryWithObjectsAndKeys:[[NSNumber alloc] initWithInt:1], OTCKRecordBottledPeerType, nil];

    [self expectAddedCKModifyRecords:recordDictionary holdFetch:NO];

    [self startCKKSSubsystem];

    [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
    [self startCKKSSubsystem];

    XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:4*NSEC_PER_SEC], @"Key state should have arrived at ready");

    [self holdCloudKitModifications];

    [self.cloudStore uploadBottledPeerRecord:self.fakeBottledPeerRecord
                              escrowRecordID:self.fakeBottledPeerRecord.escrowRecordID error:&error];

    XCTAssertNotNil(error, "error should not be nil");
    XCTAssertTrue([(NSString*)error.userInfo[@"NSLocalizedDescription"] isEqualToString:@"Operation(CKKSResultOperation(cloudkit-modify-changes)) timed out waiting to start for [<CKKSResultOperation(modify-records-watcher): ready>]"], "expecting timed out error");

    [self expectAddedCKModifyRecords:recordDictionary holdFetch:NO];

    [self releaseCloudKitModificationHold];
    [self waitForCKModifications];
}

@end

#endif /* OCTAGON */

