/*
 *  Copyright (c) 2014-present, Facebook, Inc.
 *  All rights reserved.
 *
 *  This source code is licensed under the BSD-style license found in the
 *  LICENSE file in the root directory of this source tree. An additional grant
 *  of patent rights can be found in the PATENTS file in the same directory.
 *
 */

#import <XCTest/XCTest.h>

#import <ComponentKitTestHelpers/CKAnalyticsListenerSpy.h>
#import <ComponentKitTestHelpers/CKLifecycleTestComponent.h>
#import <ComponentKitTestHelpers/CKTestRunLoopRunning.h>
#import <ComponentKitTestHelpers/CKRenderComponentTestHelpers.h>

#import <ComponentKit/CKComponent.h>
#import <ComponentKit/CKComponentInternal.h>
#import <ComponentKit/CKCompositeComponent.h>
#import <ComponentKit/CKComponentProvider.h>
#import <ComponentKit/CKComponentSubclass.h>
#import <ComponentKit/CKDataSourceAppliedChanges.h>
#import <ComponentKit/CKDataSourceChange.h>
#import <ComponentKit/CKDataSourceChangeset.h>
#import <ComponentKit/CKDataSourceConfiguration.h>
#import <ComponentKit/CKDataSourceConfigurationInternal.h>
#import <ComponentKit/CKDataSourceInternal.h>
#import <ComponentKit/CKDataSourceItem.h>
#import <ComponentKit/CKDataSourceListener.h>
#import <ComponentKit/CKDataSourceState.h>
#import <ComponentKit/CKDataSourceChangesetModification.h>

#import "CKDataSourceStateTestHelpers.h"

static NSString *const kTestInvalidateControllerContext = @"kTestInvalidateControllerContext";
static NSString *const kTestInitialiseControllerContext = @"kTestInitialiseControllerContext";
static NSNumber *const kTestinitialiseControllerModel = @2;

@interface CKDataSourceTests : XCTestCase <CKDataSourceAsyncListener>
@end

@implementation CKDataSourceTests
{
  NSMutableArray<CKDataSourceAppliedChanges *> *_announcedChanges;
  NSInteger _willGenerateChangeCounter;
  NSInteger _didGenerateChangeCounter;
  NSInteger _syncModificationStartCounter;
  CKDataSourceState *_state;
  void(^_didModifyPreviousStateBlock)(void);
  UITraitCollection *_currentTraitCollection;
}

static CKComponent *ComponentProvider(id<NSObject> model, id<NSObject> context)
{
  if ([context isEqual:kTestInvalidateControllerContext]) {
    return CK::ComponentBuilder()
               .build();
  } else if ([context isEqual:kTestInitialiseControllerContext] || model == kTestinitialiseControllerModel) {
    return [CKCompositeComponentWithScope newWithComponentProvider:^{
      return [CKLifecycleTestComponent new];
    }];
  } else {
    return [CKLifecycleTestComponent new];
  }
}

- (void)setUp
{
  [super setUp];
  _announcedChanges = [NSMutableArray new];
}

- (void)tearDown
{
  [_announcedChanges removeAllObjects];
  _willGenerateChangeCounter = 0;
  _didGenerateChangeCounter = 0;
  _syncModificationStartCounter = 0;
  [super tearDown];
}

- (void)testDataSourceSynchronouslyInsertingItemsAnnouncesInsertion
{
  CKDataSource *ds = [[CKDataSource alloc]
                      initWithConfiguration:
                      [[CKDataSourceConfiguration alloc]
                       initWithComponentProviderFunc:ComponentProvider
                       context:nil
                       sizeRange:{}]];
  [ds addListener:self];

  CKDataSourceChangeset *insertion =
  [[[[CKDataSourceChangesetBuilder dataSourceChangeset]
     withInsertedSections:[NSIndexSet indexSetWithIndex:0]]
    withInsertedItems:@{[NSIndexPath indexPathForItem:0 inSection:0]: @1}]
   build];
  [ds applyChangeset:insertion mode:CKUpdateModeSynchronous userInfo:nil];

  CKDataSourceAppliedChanges *expectedAppliedChanges =
  [[CKDataSourceAppliedChanges alloc] initWithUpdatedIndexPaths:nil
                                              removedIndexPaths:nil
                                                removedSections:nil
                                                movedIndexPaths:nil
                                               insertedSections:[NSIndexSet indexSetWithIndex:0]
                                             insertedIndexPaths:[NSSet setWithObject:[NSIndexPath indexPathForItem:0 inSection:0]]
                                                       userInfo:nil];

  XCTAssertEqualObjects(_announcedChanges.firstObject, expectedAppliedChanges);
  XCTAssertEqual(_syncModificationStartCounter, 1);
  XCTAssertEqual(_willGenerateChangeCounter, 0);
  XCTAssertEqual(_didGenerateChangeCounter, 0);
}

- (void)testDataSourceAsynchronouslyInsertingItemsAnnouncesInsertionAsynchronously
{
  CKDataSource *ds = [[CKDataSource alloc]
                      initWithConfiguration:
                      [[CKDataSourceConfiguration alloc]
                       initWithComponentProviderFunc:ComponentProvider
                       context:nil
                       sizeRange:{}]];
  [ds addListener:self];

  CKDataSourceChangeset *insertion =
  [[[[CKDataSourceChangesetBuilder dataSourceChangeset]
     withInsertedSections:[NSIndexSet indexSetWithIndex:0]]
    withInsertedItems:@{[NSIndexPath indexPathForItem:0 inSection:0]: @1}]
   build];
  [ds applyChangeset:insertion mode:CKUpdateModeAsynchronous userInfo:nil];

  CKDataSourceAppliedChanges *expectedAppliedChanges =
  [[CKDataSourceAppliedChanges alloc] initWithUpdatedIndexPaths:nil
                                              removedIndexPaths:nil
                                                removedSections:nil
                                                movedIndexPaths:nil
                                               insertedSections:[NSIndexSet indexSetWithIndex:0]
                                             insertedIndexPaths:[NSSet setWithObject:[NSIndexPath indexPathForItem:0 inSection:0]]
                                                       userInfo:nil];

  XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL(void){
    return [_announcedChanges.firstObject isEqual:expectedAppliedChanges];
  }));
  XCTAssertEqual(_syncModificationStartCounter, 0);
  XCTAssertEqual(_willGenerateChangeCounter, 1);
  XCTAssertEqual(_didGenerateChangeCounter, 1);
}

- (void)testDataSourceUpdatingConfigurationAnnouncesUpdate
{
  CKDataSource *ds = CKComponentTestDataSource(ComponentProvider, self);

  CKDataSourceConfiguration *config = [[CKDataSourceConfiguration alloc] initWithComponentProviderFunc:ComponentProvider
                                                                                               context:@"new context"
                                                                                             sizeRange:{}];
  [ds updateConfiguration:config mode:CKUpdateModeSynchronous userInfo:nil];

  CKDataSourceAppliedChanges *expectedAppliedChanges =
  [[CKDataSourceAppliedChanges alloc] initWithUpdatedIndexPaths:[NSSet setWithObject:[NSIndexPath indexPathForItem:0 inSection:0]]
                                              removedIndexPaths:nil
                                                removedSections:nil
                                                movedIndexPaths:nil
                                               insertedSections:nil
                                             insertedIndexPaths:nil
                                                       userInfo:nil];

  XCTAssertEqual(_announcedChanges.count, 2);
  XCTAssertEqualObjects(_announcedChanges[1], expectedAppliedChanges);
  XCTAssertEqual([_state configuration], config);
  XCTAssertEqual(_syncModificationStartCounter, 2);
  XCTAssertEqual(_willGenerateChangeCounter, 0);
  XCTAssertEqual(_didGenerateChangeCounter, 0);
}

- (void)testDataSourceReloadingAnnouncesUpdate
{
  CKDataSource *ds = CKComponentTestDataSource(ComponentProvider, self);
  [ds reloadWithMode:CKUpdateModeSynchronous userInfo:nil];

  CKDataSourceAppliedChanges *expectedAppliedChanges =
  [[CKDataSourceAppliedChanges alloc] initWithUpdatedIndexPaths:[NSSet setWithObject:[NSIndexPath indexPathForItem:0 inSection:0]]
                                              removedIndexPaths:nil
                                                removedSections:nil
                                                movedIndexPaths:nil
                                               insertedSections:nil
                                             insertedIndexPaths:nil
                                                       userInfo:nil];

  XCTAssertEqual(_announcedChanges.count, 2);
  XCTAssertEqualObjects(_announcedChanges[1], expectedAppliedChanges);
  XCTAssertEqual(_syncModificationStartCounter, 2);
  XCTAssertEqual(_willGenerateChangeCounter, 0);
  XCTAssertEqual(_didGenerateChangeCounter, 0);
}

- (void)testDataSourceSynchronousReloadCancelsPreviousAsynchronousReload
{
  CKDataSource *ds = CKComponentTestDataSource(ComponentProvider, self);

  // The initial asynchronous reload should be canceled by the immediately subsequent synchronous reload.
  // We then request *another* async reload so that we can wait for it to complete and assert that the initial
  // async reload doesn't actually take effect after the synchronous reload.
  [ds reloadWithMode:CKUpdateModeAsynchronous userInfo:@{@"id": @1}];
  [ds reloadWithMode:CKUpdateModeSynchronous userInfo:@{@"id": @2}];
  [ds reloadWithMode:CKUpdateModeAsynchronous userInfo:@{@"id": @3}];

  CKDataSourceAppliedChanges *expectedAppliedChangesForSyncReload =
  [[CKDataSourceAppliedChanges alloc] initWithUpdatedIndexPaths:[NSSet setWithObject:[NSIndexPath indexPathForItem:0 inSection:0]]
                                              removedIndexPaths:nil
                                                removedSections:nil
                                                movedIndexPaths:nil
                                               insertedSections:nil
                                             insertedIndexPaths:nil
                                                       userInfo:@{@"id": @2}];
  CKDataSourceAppliedChanges *expectedAppliedChangesForSecondAsyncReload =
  [[CKDataSourceAppliedChanges alloc] initWithUpdatedIndexPaths:[NSSet setWithObject:[NSIndexPath indexPathForItem:0 inSection:0]]
                                              removedIndexPaths:nil
                                                removedSections:nil
                                                movedIndexPaths:nil
                                               insertedSections:nil
                                             insertedIndexPaths:nil
                                                       userInfo:@{@"id": @3}];
  XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL{
    return _announcedChanges.count == 3
    && [_announcedChanges[1] isEqual:expectedAppliedChangesForSyncReload]
    && [_announcedChanges[2] isEqual:expectedAppliedChangesForSecondAsyncReload];
  }));
  XCTAssertEqual(_syncModificationStartCounter, 2);
}

- (void)testDataSourceDeallocatingDataSourceTriggersInvalidateOnMainThread
{
  CKLifecycleTestComponentController *controller = nil;
  @autoreleasepool {
    // We dispatch empty operation on Data Source to background so that
    // DataSource deallocation is also triggered on background.
    // CKLifecycleTestComponent will assert if it receives an invalidation not on the main thread,
    CKDataSource *dataSource = CKComponentTestDataSource(ComponentProvider, self);
    controller = ((CKLifecycleTestComponent *)[[_state objectAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]] rootLayout].component()).controller;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      [dataSource hash];
    });
  }
  XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL(void){
    return controller.calledInvalidateController;
  }));
}

- (void)testDataSourceAddingComponentByUpdatingConfigurationTriggersDidInitOnMainThread
{
  const auto firstIndexPath = [NSIndexPath indexPathForRow:0 inSection:0];
  CKDataSource *dataSource = CKComponentTestDataSource(ComponentProvider, self);
  const auto firstController = (CKLifecycleTestComponentController *)((CKLifecycleTestComponent *)[[_state objectAtIndexPath:firstIndexPath] rootLayout].component()).controller;
  XCTAssertTrue(firstController.calledDidInit);
  [dataSource updateConfiguration:[_state.configuration copyWithContext:kTestInitialiseControllerContext sizeRange:{}]
                             mode:CKUpdateModeSynchronous
                         userInfo:@{}];
  const auto secondController = (CKLifecycleTestComponentController *)((CKComponent *)((CKCompositeComponent *)[[_state objectAtIndexPath:firstIndexPath] rootLayout].component()).child).controller;
  XCTAssertNotEqual(firstController, secondController);
  XCTAssertTrue(firstController.calledInvalidateController);
  XCTAssertTrue(secondController.calledDidInit);
}

- (void)testDataSourceAddingComponentByApplyingChangesetTriggersDidInitOnMainThread
{
  const auto firstIndexPath = [NSIndexPath indexPathForRow:0 inSection:0];
  CKDataSource *dataSource = CKComponentTestDataSource(ComponentProvider, self);
  const auto firstController = (CKLifecycleTestComponentController *)((CKLifecycleTestComponent *)[[_state objectAtIndexPath:firstIndexPath] rootLayout].component()).controller;
  XCTAssertTrue(firstController.calledDidInit);
  [dataSource applyChangeset:[[[CKDataSourceChangesetBuilder dataSourceChangeset]
                              withUpdatedItems:@{firstIndexPath: kTestinitialiseControllerModel}]
                              build]
                        mode:CKUpdateModeSynchronous
                    userInfo:@{}];
  const auto secondController = (CKLifecycleTestComponentController *)((CKComponent *)((CKCompositeComponent *)[[_state objectAtIndexPath:firstIndexPath] rootLayout].component()).child).controller;
  XCTAssertNotEqual(firstController, secondController);
  XCTAssertTrue(firstController.calledInvalidateController);
  XCTAssertTrue(secondController.calledDidInit);
}

- (void)testDataSourceRemovingComponentTriggersInvalidateOnMainThread
{
  CKDataSource *dataSource = CKComponentTestDataSource(ComponentProvider, self);
  const auto controller = ((CKLifecycleTestComponent *)[[_state objectAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]] rootLayout].component()).controller;
  [dataSource updateConfiguration:[_state.configuration copyWithContext:kTestInvalidateControllerContext sizeRange:{}]
                             mode:CKUpdateModeSynchronous
                         userInfo:@{}];
  XCTAssertTrue(controller.calledInvalidateController);
}

- (void)testDataSourceRemovingItemTriggersInvalidateOnMainThread
{
  CKDataSource *dataSource = CKComponentTestDataSource(ComponentProvider, self);
  const auto controller = ((CKLifecycleTestComponent *)[[_state objectAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]] rootLayout].component()).controller;
  [dataSource applyChangeset:[[[CKDataSourceChangesetBuilder dataSourceChangeset]
                               withRemovedItems:[NSSet setWithObject:[NSIndexPath indexPathForRow:0 inSection:0]]]
                              build]
                        mode:CKUpdateModeSynchronous
                    userInfo:@{}];
  XCTAssertTrue(controller.calledInvalidateController);
}

- (void)testDataSourceRemovingSectionTriggersInvalidateOnMainThread
{
  CKDataSource *dataSource = CKComponentTestDataSource(ComponentProvider, self);
  const auto controller = ((CKLifecycleTestComponent *)[[_state objectAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]] rootLayout].component()).controller;
  [dataSource applyChangeset:[[[CKDataSourceChangesetBuilder dataSourceChangeset]
                               withRemovedSections:[NSIndexSet indexSetWithIndex:0]]
                              build]
                        mode:CKUpdateModeSynchronous
                    userInfo:@{}];
  XCTAssertTrue(controller.calledInvalidateController);
}

- (void)testDataSourceApplyingPrecomputedChange
{
  const auto dataSource = CKComponentTestDataSource(ComponentProvider, self);
  const auto insertion =
  [[[CKDataSourceChangesetBuilder dataSourceChangeset]
    withInsertedItems:@{[NSIndexPath indexPathForItem:0 inSection:0]: @1}]
   build];
  const auto modification =
  [[CKDataSourceChangesetModification alloc]
   initWithChangeset:insertion
   stateListener:nil userInfo:@{} qos:CKDataSourceQOSDefault];
  const auto change = [modification changeFromState:_state];
  const auto isApplied = [dataSource applyChange:change];
  XCTAssertTrue(isApplied, @"Change should be applied to datasource successfully.");
  XCTAssertEqual(_state, change.state);
}

- (void)testDataSourceApplyingPrecomputedChangeAfterStateIsChanged
{
  const auto dataSource = CKComponentTestDataSource(ComponentProvider, self);
  const auto insertion =
  [[[CKDataSourceChangesetBuilder dataSourceChangeset]
    withInsertedItems:@{[NSIndexPath indexPathForItem:0 inSection:0]: @1}]
   build];
  const auto modification =
  [[CKDataSourceChangesetModification alloc]
   initWithChangeset:insertion
   stateListener:nil userInfo:@{} qos:CKDataSourceQOSDefault];
  const auto change = [modification changeFromState:_state];
  [dataSource reloadWithMode:CKUpdateModeSynchronous userInfo:@{}];
  const auto newState = _state;
  const auto isApplied = [dataSource applyChange:change];
  XCTAssertFalse(isApplied, @"Applying change to datasource should fail.");
  XCTAssertEqualObjects(_state, newState, @"State should remain the same.");
}

- (void)testDataSourceVerifyingPrecomputedChange
{
  const auto dataSource = CKComponentTestDataSource(ComponentProvider, self);
  const auto insertion =
  [[[CKDataSourceChangesetBuilder dataSourceChangeset]
    withInsertedItems:@{[NSIndexPath indexPathForItem:0 inSection:0]: @1}]
   build];
  const auto modification =
  [[CKDataSourceChangesetModification alloc]
   initWithChangeset:insertion
   stateListener:nil userInfo:@{} qos:CKDataSourceQOSDefault];
  const auto change = [modification changeFromState:_state];
  const auto isValid = [dataSource verifyChange:change];
  XCTAssertTrue(isValid, @"Change should be valid.");
}

- (void)testDataSourceVerifyingPrecomputedChangeAfterStateIsChanged
{
  const auto dataSource = CKComponentTestDataSource(ComponentProvider, self);
  const auto insertion =
  [[[CKDataSourceChangesetBuilder dataSourceChangeset]
    withInsertedItems:@{[NSIndexPath indexPathForItem:0 inSection:0]: @1}]
   build];
  const auto modification =
  [[CKDataSourceChangesetModification alloc]
   initWithChangeset:insertion
   stateListener:nil userInfo:@{} qos:CKDataSourceQOSDefault];
  const auto change = [modification changeFromState:_state];
  [dataSource reloadWithMode:CKUpdateModeSynchronous userInfo:@{}];
  const auto isValid = [dataSource verifyChange:change];
  XCTAssertFalse(isValid, @"Change should not be valid since state has changed.");
}

- (void)testDataSourceComponentInControllerIsNotUpdatedAfterComponentBuild
{
  CKComponentController *componentController = nil;
  // Autorelease pool is needed here to make sure `oldState` is deallocated so that weak reference of component
  // in `CKComponentController` is nil.
  @autoreleasepool {
    const auto dataSource = CKComponentTestDataSource(ComponentProvider,
                                                      self,
                                                      {});
    CKComponent *component = (CKComponent *)[_state objectAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]].rootLayout.component();
    componentController = component.controller;
    const auto update =
    [[[CKDataSourceChangesetBuilder dataSourceChangeset]
      withUpdatedItems:@{[NSIndexPath indexPathForItem:0 inSection:0]: @1}]
     build];
    [dataSource applyChangeset:update mode:CKUpdateModeSynchronous userInfo:@{}];
  }
  // `latestComponent` is updated so `componentController.component` returns the latest generation of component even
  // after `oldState` is deallocated.
  XCTAssertNotEqual(componentController.component, nil);
}

/**
 This test covers the case when a "re-entrant" changeset application happens in `didModifyPreviousState`
 event callback. We should make sure no redundant work is done in background queue becasue of this.
 */
- (void)testDataSourceApplyingChangesetInDidModifyPreviousStateCallback
{
  const auto dataSource = CKComponentTestDataSource(ComponentProvider, self);
  _didModifyPreviousStateBlock = ^{
    [dataSource
     applyChangeset:[[CKDataSourceChangesetBuilder dataSourceChangeset] build]
     mode:CKUpdateModeAsynchronous
     userInfo:@{}];
    _didModifyPreviousStateBlock = nil;
  };
  // Applying this changeset asynchronously triggers another changeset application in `_didModifyPreviousStateBlock`.
  [dataSource
   applyChangeset:[[CKDataSourceChangesetBuilder dataSourceChangeset] build]
   mode:CKUpdateModeAsynchronous
   userInfo:@{}];
  CKRunRunLoopUntilBlockIsTrue(^BOOL{
    return _announcedChanges.count == 3;
  });
  // Applying another changeset to make sure all asynchronous work is finished in background queue.
  [dataSource
   applyChangeset:[[CKDataSourceChangesetBuilder dataSourceChangeset] build]
   mode:CKUpdateModeAsynchronous
   userInfo:@{}];
  CKRunRunLoopUntilBlockIsTrue(^BOOL{
    return _announcedChanges.count == 4;
  });
  // `_willGenerateChangeCounter` matches the number of asynchronous changeset applications.
  XCTAssertEqual(_willGenerateChangeCounter, 3);
}

- (void)test_WhenReceivesStateUpdate_ReportsToAnalyticsListener
{
  const auto analyticsListenerSpy = [CKAnalyticsListenerSpy new];
  const auto dataSource = CKComponentTestDataSource(ComponentProvider, self, analyticsListenerSpy);
  const auto item = [_state objectAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
  const auto handle =
  [[CKComponentScopeHandle alloc] initWithListener:dataSource
                                    rootIdentifier:[item.scopeRoot globalIdentifier]
                                 componentTypeName:"CKTestComponent"
                                      initialState:nil];

  [dataSource componentScopeHandle:handle
                    rootIdentifier:[item.scopeRoot globalIdentifier]
             didReceiveStateUpdate:^(id x){ return x; }
                          metadata:{}
                              mode:CKUpdateModeSynchronous];

  const auto event = analyticsListenerSpy.events.front();
  event.match([&](CK::AnalyticsListenerSpy::DidReceiveStateUpdate drsu){
    XCTAssertEqual(drsu.handle, handle);
    XCTAssertEqual(drsu.rootID, [item.scopeRoot globalIdentifier]);
  });
}

- (void)test_WhenTraitCollectionIsSet_CurrentTraitCollectionIsCorrectInWorkQueue
{
  if (@available(iOS 13.0, tvOS 13.0, *)) {
    const auto dataSource = CKComponentTestDataSource(ComponentProvider, self);
    [dataSource setTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceIdiom:UIUserInterfaceIdiomCarPlay]];
    [dataSource
     applyChangeset:[[CKDataSourceChangesetBuilder dataSourceChangeset] build]
     mode:CKUpdateModeAsynchronous
     userInfo:@{}];
    CKRunRunLoopUntilBlockIsTrue(^BOOL{
      return _didGenerateChangeCounter == 1;
    });
    XCTAssertEqual(_currentTraitCollection.userInterfaceIdiom, UIUserInterfaceIdiomCarPlay);
  }
}

#pragma mark - Listener

- (void)dataSource:(CKDataSource *)dataSource
     didModifyPreviousState:(CKDataSourceState *)previousState
                  withState:(CKDataSourceState *)state
          byApplyingChanges:(CKDataSourceAppliedChanges *)changes
{
  _state = state;
  [_announcedChanges addObject:changes];
  if (_didModifyPreviousStateBlock) {
    _didModifyPreviousStateBlock();
  }
}

- (void)dataSource:(CKDataSource *)dataSource willSyncApplyModificationWithUserInfo:(NSDictionary *)userInfo
{
  _syncModificationStartCounter++;
}

- (void)dataSource:(CKDataSource *)dataSource willGenerateNewStateWithUserInfo:(NSDictionary *)userInfo
{
  _willGenerateChangeCounter++;
  if (@available(iOS 13.0, tvOS 13.0, *)) {
    _currentTraitCollection = [UITraitCollection currentTraitCollection];
  }
}

- (void)dataSource:(CKDataSource *)dataSource didGenerateNewState:(CKDataSourceState *)newState changes:(CKDataSourceAppliedChanges *)changes
{
  _didGenerateChangeCounter++;
}

- (void)dataSource:(CKDataSource *)dataSource
 willApplyDeferredChangeset:(CKDataSourceChangeset *)deferredChangeset {}

@end
