/**
 * Copyright (c) 2016-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 <OCMock/OCMock.h>

#import <IGListKit/IGListKit.h>

#import "IGListAdapterInternal.h"
#import "IGListTestOffsettingLayout.h"
#import "IGTestCell.h"
#import "IGTestDelegateController.h"
#import "IGTestDelegateDataSource.h"
#import "IGTestObject.h"

#define genIndexPath(s) [NSIndexPath indexPathForItem:0 inSection:s]
#define genTestObject(k, v) [[IGTestObject alloc] initWithKey:k value:v]

#define genExpectation [self expectationWithDescription:NSStringFromSelector(_cmd)]

@interface IGListAdapterE2ETests : XCTestCase

@property (nonatomic, strong) IGListCollectionView *collectionView;
@property (nonatomic, strong) IGListAdapter *adapter;
@property (nonatomic, strong) IGListAdapterUpdater *updater;
@property (nonatomic, strong) IGTestDelegateDataSource *dataSource;
@property (nonatomic, strong) UIWindow *window;

@end

@implementation IGListAdapterE2ETests

- (void)setUp {
    [super setUp];

    self.window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];

    UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
    self.collectionView = [[IGListCollectionView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) collectionViewLayout:layout];

    [self.window addSubview:self.collectionView];

    self.dataSource = [[IGTestDelegateDataSource alloc] init];

    self.updater = [[IGListAdapterUpdater alloc] init];
    self.adapter = [[IGListAdapter alloc] initWithUpdater:self.updater viewController:nil workingRangeSize:2];
}

- (void)tearDown {
    [super tearDown];

    self.window = nil;
    self.collectionView = nil;
    self.adapter = nil;
    self.dataSource = nil;
    self.updater = nil;
}

- (void)setupWithObjects:(NSArray *)objects {
    self.dataSource.objects = objects;
    self.adapter.collectionView = self.collectionView;
    self.adapter.dataSource = self.dataSource;
    [self.collectionView layoutIfNeeded];
}

- (void)test_whenSettingUpTest_thenCollectionViewIsLoaded {
    [self setupWithObjects:@[
                             genTestObject(@1, @2),
                             genTestObject(@2, @3)
                             ]];
    XCTAssertEqual(self.collectionView.numberOfSections, 2);
    XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2);
    XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 3);
}

- (void)test_whenUsingStringValue_thenCellLabelsAreConfigured {
    [self setupWithObjects:@[
                             genTestObject(@0, @"Foo"),
                             genTestObject(@1, @"Bar")
                             ]];

    IGTestCell *cell = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(0)];
    XCTAssertEqualObjects(cell.label.text, @"Foo");
    XCTAssertEqual(cell.delegate, [self.adapter sectionControllerForObject:self.dataSource.objects[0]]);
}

- (void)test_whenUpdating_withEqualObjects_thatCellConfigurationDoesntChange {
    [self setupWithObjects:@[
                             genTestObject(@0, @"Foo"),
                             genTestObject(@1, @"Bar")
                             ]];

    // Get the section controller before we change the data source or perform updates
    id c0 = [self.adapter sectionControllerForObject:self.dataSource.objects[0]];

    // Set equal but new-instance objects on the data source
    self.dataSource.objects = @[
                                genTestObject(@0, @"Foo"),
                                genTestObject(@1, @"Bar")
                                ];

    // Perform updates on the adapter and check that the cell config uses the same section controller as before the updates
    XCTestExpectation *expectation = genExpectation;
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
        IGTestCell *cell = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(0)];
        XCTAssertEqualObjects(cell.label.text, @"Foo");
        XCTAssertNotNil(cell.delegate);
        XCTAssertEqual(cell.delegate, c0);
        XCTAssertEqual(cell.delegate, [self.adapter sectionControllerForObject:self.dataSource.objects[0]]);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenReloadingItem_cellConfigurationChanges {
    [self setupWithObjects:@[
                             genTestObject(@0, @"Foo"),
                             genTestObject(@1, @"Bar")
                             ]];

    // make sure our cells are propertly configured
    IGTestCell *cell1 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(0)];
    IGTestCell *cell2 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(1)];
    XCTAssertEqualObjects(cell1.label.text, @"Foo");
    XCTAssertEqualObjects(cell2.label.text, @"Bar");

    // Change the string value of both instances in the data source
    IGTestObject *item1 = self.dataSource.objects[0];
    item1.value = @"Baz";
    IGTestObject *item2 = self.dataSource.objects[1];
    item2.value = @"Quz";

    // Only reload the first item, not the second
    [self.adapter reloadObjects:@[item1]];

    // The collection view will likely create new cells
    cell1 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(0)];
    cell2 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(1)];

    // Make sure that the cell in the first section was reloaded
    XCTAssertEqualObjects(cell1.label.text, @"Baz");
    // The cell in the second section should not be reloaded and should equal the string value from setup
    XCTAssertEqualObjects(cell2.label.text, @"Bar");
}

- (void)test_whenObjectEqualityChanges_thatSectionCountChanges {
    [self setupWithObjects:@[
                             genTestObject(@1, @2),
                             genTestObject(@2, @2)
                             ]];

    self.dataSource.objects = @[
                                genTestObject(@1, @2),
                                genTestObject(@2, @3), // updated to 3 items (from 2)
                                genTestObject(@3, @2), // insert new object
                                ];

    XCTestExpectation *expectation = genExpectation;
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) {
        XCTAssertEqual(self.collectionView.numberOfSections, 3);
        XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2);
        XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 3);
        XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 2);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenUpdatesComplete_thatCellsExist {
    [self setupWithObjects:@[
                             genTestObject(@1, @2),
                             genTestObject(@2, @2),
                             ]];
    XCTestExpectation *expectation = genExpectation;
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
        XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]);
        XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]]);
        XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]);
        XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:1]]);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenReloadDataCompletes_thatCellsExist {
    [self setupWithObjects:@[
                            genTestObject(@1, @2),
                            genTestObject(@2, @2)
                            ]];
    XCTestExpectation *expectation = genExpectation;
    [self.adapter reloadDataWithCompletion:^(BOOL finished) {
        XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]);
        XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]]);
        XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]);
        XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:1]]);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenSectionControllerInsertsIndexes_thatCountsAreUpdated {
    [self setupWithObjects:@[
                             genTestObject(@1, @2),
                             genTestObject(@2, @2)
                             ]];

    IGTestObject *object = self.dataSource.objects[0];
    IGListSectionController<IGListSectionType> *sectionController = [self.adapter sectionControllerForObject:object];

    XCTestExpectation *expectation = genExpectation;
    [sectionController.collectionContext performBatchAnimated:YES updates:^(id<IGListBatchContext> batchContext) {
        object.value = @3;
        [batchContext insertInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:2]];
    } completion:^(BOOL finished2) {
        XCTAssertEqual([self.collectionView numberOfSections], 2);
        XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenSectionControllerDeletesIndexes_thatCountsAreUpdated {
    // 2 sections each with 2 objects
    [self setupWithObjects:@[
                             genTestObject(@1, @2),
                             genTestObject(@2, @2)
                             ]];

    IGTestObject *object = self.dataSource.objects[0];
    IGListSectionController<IGListSectionType> *sectionController = [self.adapter sectionControllerForObject:object];

    XCTestExpectation *expectation = genExpectation;
    [sectionController.collectionContext performBatchAnimated:YES updates:^(id<IGListBatchContext> batchContext) {
        object.value = @1;
        [batchContext deleteInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]];
    } completion:^(BOOL finished2) {
        XCTAssertEqual([self.collectionView numberOfSections], 2);
        XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenSectionControllerReloadsIndexes_thatCellConfigurationUpdates {
    [self setupWithObjects:@[
                             genTestObject(@1, @"a"),
                             genTestObject(@2, @"b")
                             ]];
    XCTAssertEqual([self.collectionView numberOfSections], 2);
    IGTestCell *cell = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
    XCTAssertEqualObjects(cell.label.text, @"a");

    IGTestObject *object = self.dataSource.objects[0];
    IGListSectionController<IGListSectionType> *sectionController = [self.adapter sectionControllerForObject:object];

    XCTestExpectation *expectation = genExpectation;
    [sectionController.collectionContext performBatchAnimated:YES updates:^(id<IGListBatchContext> batchContext) {
        object.value = @"c";
        [batchContext reloadInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]];
    } completion:^(BOOL finished2) {
        XCTAssertEqual([self.collectionView numberOfSections], 2);
        IGTestCell *updatedCell = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
        XCTAssertEqualObjects(updatedCell.label.text, @"c");
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenSectionControllerReloads_thatCountsAreUpdated {
    [self setupWithObjects:@[
                             genTestObject(@1, @2),
                             genTestObject(@2, @2)
                             ]];

    IGTestObject *object = self.dataSource.objects[0];
    IGListSectionController<IGListSectionType> *sectionController = [self.adapter sectionControllerForObject:object];

    XCTestExpectation *expectation = genExpectation;
    [sectionController.collectionContext performBatchAnimated:YES updates:^(id<IGListBatchContext> batchContext) {
        object.value = @3;
        [batchContext reloadSectionController:sectionController];
    } completion:^(BOOL finished2) {
        XCTAssertEqual([self.collectionView numberOfSections], 2);
        XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenPerformingUpdates_withSectionControllerMutations_thatCollectionCountsAreUpdated {
    [self setupWithObjects:@[
                             genTestObject(@1, @2),
                             genTestObject(@2, @2)
                             ]];
    IGTestObject *object1 = self.dataSource.objects[0];
    IGTestObject *object2 = self.dataSource.objects[1];

    // insert a new object in front of the one we are doing an item-level insert on
    self.dataSource.objects = @[
                                genTestObject(@3, @1), // new
                                object1,
                                object2,
                                ];

    IGListSectionController<IGListSectionType> *sectionController1 = [self.adapter sectionControllerForObject:object1];
    IGListSectionController<IGListSectionType> *sectionController2 = [self.adapter sectionControllerForObject:object2];

    [self.adapter performUpdatesAnimated:YES completion:nil];

    XCTestExpectation *expectation = genExpectation;
    [sectionController1.collectionContext performBatchAnimated:YES updates:^(id<IGListBatchContext> batchContext) {
        object1.value = @1;
        object2.value = @3;
        [batchContext deleteInSectionController:sectionController1 atIndexes:[NSIndexSet indexSetWithIndex:0]];
        [batchContext insertInSectionController:sectionController2 atIndexes:[NSIndexSet indexSetWithIndex:2]];
        [batchContext reloadInSectionController:sectionController2 atIndexes:[NSIndexSet indexSetWithIndex:0]];
    } completion:^(BOOL finished2) {
        // 3 sections now b/c of the insert
        XCTAssertEqual([self.collectionView numberOfSections], 3);
        XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1);
        XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1);
        XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 3);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenSectionControllerMoves_withSectionControllerMutations_thatCollectionViewWorks {
    [self setupWithObjects:@[
                             genTestObject(@1, @2),
                             genTestObject(@2, @2)
                             ]];

    IGTestObject *object = self.dataSource.objects[0];
    self.dataSource.objects = @[
                                genTestObject(@2, @2),
                                object, // moved from 0 to 1
                                ];

    IGListSectionController<IGListSectionType> *sectionController = [self.adapter sectionControllerForObject:object];

    // queue the update that performs the section move
    [self.adapter performUpdatesAnimated:YES completion:nil];

    XCTestExpectation *expectation = genExpectation;

    // queue an item update that gets batched with the section move
    [sectionController.collectionContext performBatchAnimated:YES updates:^(id<IGListBatchContext> batchContext) {
        object.value = @3;
        [batchContext insertInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:2]];
    } completion:^(BOOL finished2) {
        XCTAssertEqual([self.collectionView numberOfSections], 2);
        // the object we are tracking should now be in section 1 and have 3 items
        XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 3);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenItemIsRemoved_withSectionControllerMutations_thatCollectionViewWorks {
    // 2 sections each with 2 objects
    [self setupWithObjects:@[
                             genTestObject(@2, @2),
                             genTestObject(@1, @2)
                             ]];
    IGTestObject *object = self.dataSource.objects[1];

    // object at index 1 deleted
    self.dataSource.objects = @[
                                genTestObject(@2, @2),
                                ];

    IGListSectionController<IGListSectionType> *sectionController = [self.adapter sectionControllerForObject:object];

    [self.adapter performUpdatesAnimated:YES completion:nil];

    XCTestExpectation *expectation = genExpectation;
    [sectionController.collectionContext performBatchAnimated:YES updates:^(id<IGListBatchContext> batchContext) {
        object.value = @1;
        [batchContext deleteInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]];
    } completion:^(BOOL finished2) {
        XCTAssertEqual([self.collectionView numberOfSections], 1);
        [expectation fulfill];
    }];

    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenPerformingUpdates_withUnequalItem_withItemMoving_thatCollectionViewCountsUpdate {
    [self setupWithObjects:@[
                            genTestObject(@1, @2),
                            genTestObject(@2, @2),
                            ]];

    self.dataSource.objects = @[
                                genTestObject(@3, @2),
                                genTestObject(@1, @3), // moved from index 0 to 1, value changed from 2 to 3
                                genTestObject(@2, @2),
                                ];

    XCTestExpectation *expectation = genExpectation;
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) {
        XCTAssertEqual([self.collectionView numberOfSections], 3);
        XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 3);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenPerformingUpdates_withItemMoving_withSectionControllerReloadIndexes_thatCollectionViewCountsUpdate {
    [self setupWithObjects:@[
                            genTestObject(@1, @2),
                            genTestObject(@2, @3),
                            ]];

    IGListSectionController<IGListSectionType> *sectionController = [self.adapter sectionControllerForObject:self.dataSource.objects[0]];

    self.dataSource.objects = @[
                                genTestObject(@2, @3),
                                genTestObject(@1, @2), // moved from index 0 to 1
                                ];

    [self.adapter performUpdatesAnimated:YES completion:nil];

    XCTestExpectation *expectation = genExpectation;
    [sectionController.collectionContext performBatchAnimated:YES updates:^(id<IGListBatchContext> batchContext) {
        [batchContext reloadInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]];
    } completion:^(BOOL finished2) {
        XCTAssertEqual([self.collectionView numberOfSections], 2);
        XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3);
        XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenPerformingUpdates_withSectionControllerReloadIndexes_withItemDeleted_thatCollectionViewCountsUpdate {
    [self setupWithObjects:@[
                             genTestObject(@1, @2), // item that will be deleted
                             genTestObject(@2, @3),
                             ]];

    IGListSectionController<IGListSectionType> *sectionController = [self.adapter sectionControllerForObject:self.dataSource.objects[0]];

    self.dataSource.objects = @[
                                genTestObject(@2, @3),
                                ];

    [self.adapter performUpdatesAnimated:YES completion:nil];

    XCTestExpectation *expectation = genExpectation;
    [sectionController.collectionContext performBatchAnimated:YES updates:^(id<IGListBatchContext> batchContext) {
        [batchContext reloadInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]];
    } completion:^(BOOL finished2) {
        XCTAssertEqual([self.collectionView numberOfSections], 1);
        XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenPerformingUpdates_withNewItemInstances_thatSectionControllersEqual {
    [self setupWithObjects:@[
                            genTestObject(@1, @2),
                            genTestObject(@2, @2)
                            ]];

    // grab section controllers before updating the objects
    NSArray *beforeupdateObjects = self.dataSource.objects;
    IGListSectionController<IGListSectionType> *sectionController1 = [self.adapter sectionControllerForObject:beforeupdateObjects.firstObject];
    IGListSectionController<IGListSectionType> *sectionController2 = [self.adapter sectionControllerForObject:beforeupdateObjects.lastObject];

    self.dataSource.objects = @[
                                genTestObject(@1, @3), // new instance, value changed from 2 to 3
                                genTestObject(@2, @2), // new instance but unchanged
                                ];

    XCTestExpectation *expectation = genExpectation;
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) {
        XCTAssertEqual([self.collectionView numberOfSections], 2);
        XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3);
        XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2);

        NSArray *afterupdateObjects = [self.adapter objects];
        // pointer equality
        XCTAssertEqual([self.adapter sectionControllerForObject:afterupdateObjects.firstObject], sectionController1);
        XCTAssertEqual([self.adapter sectionControllerForObject:afterupdateObjects.lastObject], sectionController2);

        [expectation fulfill];
    }];

    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenPerformingMultipleUpdates_withNewItemInstances_thatSectionControllersReceiveNewInstances {
    [self setupWithObjects:@[
                             genTestObject(@1, @2),
                             genTestObject(@2, @2),
                             ]];

    id object = self.dataSource.objects[0];
    IGTestDelegateController *sectionController = [self.adapter sectionControllerForObject:object];

    // test delegate controller counts the number of times it receives -didUpdateToItem:
    XCTAssertEqual(sectionController.updateCount, 1);

    self.dataSource.objects = @[
                                object, // same object instance
                                genTestObject(@3, @2),
                                ];

    XCTestExpectation *expectation = genExpectation;
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) {
        XCTAssertEqual(sectionController, [self.adapter sectionControllerForObject:[self.adapter objects][0]]);

        // should not have received -didUpdateToItem: since the instance did not change
        XCTAssertEqual(sectionController.updateCount, 1);

        self.dataSource.objects = @[
                                    genTestObject(@1, @2), // new instance but equal
                                    genTestObject(@3, @2),
                                    ];

        [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished3) {
            XCTAssertEqual(sectionController, [self.adapter sectionControllerForObject:[self.adapter objects][0]]);

            // a new instance was used, make sure the section controller was updated
            XCTAssertEqual(sectionController.updateCount, 2);

            [expectation fulfill];
        }];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenQueryingCollectionContext_withNewItemInstances_thatSectionMatchesCurrentIndex {
    [self setupWithObjects:@[
                             genTestObject(@1, @2),
                             genTestObject(@2, @2),
                             ]];

    IGTestDelegateController *sectionController = [self.adapter sectionControllerForObject:self.dataSource.objects[0]];

    XCTestExpectation *expectation = genExpectation;
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) {
        self.dataSource.objects = @[
                                    genTestObject(@2, @2),
                                    genTestObject(@1, @2), // new instance but equal
                                    genTestObject(@3, @2),
                                    ];

        __block BOOL executedUpdateBlock = NO;
        __weak __typeof__(sectionController) weakSectionController = sectionController;
        sectionController.itemUpdateBlock = ^{
            executedUpdateBlock = YES;
            XCTAssertEqual([weakSectionController.collectionContext sectionForSectionController:weakSectionController], 1);
        };

        [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished3) {
            XCTAssertTrue(executedUpdateBlock);

            [expectation fulfill];
        }];
    }];

    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenSectionControllerMutates_withReloadData_thatSectionControllerMutationIsApplied {
    [self setupWithObjects:@[
                             genTestObject(@1, @2),
                             genTestObject(@2, @2),
                             ]];
    IGTestObject *object = self.dataSource.objects[0];
    IGListSectionController<IGListSectionType> *sectionController = [self.adapter sectionControllerForObject:object];

    [sectionController.collectionContext performBatchAnimated:YES updates:^(id<IGListBatchContext> batchContext) {
        object.value = @3;
        [batchContext insertInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:2]];
    } completion:nil];

    XCTestExpectation *expectation = genExpectation;
    [self.adapter reloadDataWithCompletion:^(BOOL finished2) {
        XCTAssertEqual([self.collectionView numberOfSections], 2);

        // check that the count of items in section 0 was updated from the previous batch update block
        XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenContentOffsetChanges_withPerformUpdates_thatCollectionViewWorks {
    // this test layout changes the offset in -prepareLayout which occurs somewhere between the update block being
    // applied and the completion block
    self.collectionView.collectionViewLayout = [IGListTestOffsettingLayout new];

    [self setupWithObjects:@[
                             genTestObject(@1, @2),
                             genTestObject(@2, @2),
                             genTestObject(@3, @2),
                             ]];

    // remove the last object to check that we don't access OOB section controller when the layout changes the offset
    self.dataSource.objects = @[
                                genTestObject(@1, @2),
                                genTestObject(@2, @2),
                                ];

    XCTestExpectation *expectation = genExpectation;
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) {
        XCTAssertEqual([self.collectionView numberOfSections], 2);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenReloadingItems_withNewItemInstances_thatSectionControllersReceiveNewInstances {
    [self setupWithObjects:@[
                             genTestObject(@1, @2),
                             genTestObject(@2, @2),
                             genTestObject(@3, @2),
                             ]];

    IGTestDelegateController *sectionController1 = [self.adapter sectionControllerForObject:genTestObject(@1, @2)];
    IGTestDelegateController *sectionController2 = [self.adapter sectionControllerForObject:genTestObject(@2, @2)];

    NSArray *newObjects = @[
                            genTestObject(@1, @3),
                            genTestObject(@2, @3),
                            ];
    [self.adapter reloadObjects:newObjects];

    XCTAssertEqual(sectionController1.item, newObjects[0]);
    XCTAssertEqual(sectionController2.item, newObjects[1]);
    XCTAssertTrue([[self.adapter.sectionMap objects] indexOfObjectIdenticalTo:newObjects[0]] != NSNotFound);
    XCTAssertTrue([[self.adapter.sectionMap objects] indexOfObjectIdenticalTo:newObjects[1]] != NSNotFound);
}

- (void)test_whenReloadingItems_withPerformUpdates_thatReloadIsApplied {
    [self setupWithObjects:@[
                             genTestObject(@1, @1),
                             genTestObject(@2, @2),
                             genTestObject(@3, @3),
                             ]];

    IGTestObject *object = self.dataSource.objects[0];
    IGTestDelegateController *sectionController = [self.adapter sectionControllerForObject:object];

    // using performBatchAnimated: to mimic re-entrant item reload
    [sectionController.collectionContext performBatchAnimated:YES updates:^(id<IGListBatchContext> batchContext) {
        object.value = @4; // from @1
        [self.adapter reloadObjects:@[object]];
    } completion:nil];

    // object is moved from position 0 to 1
    // it is also mutated in the previous update block AND queued for a reload
    self.dataSource.objects = @[
                                genTestObject(@3, @3),
                                object,
                                genTestObject(@2, @2),
                                ];

    XCTestExpectation *expectation = genExpectation;
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) {
        XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3);
        XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 4); // reloaded section
        XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 2);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenSectionControllerMutates_whenThereIsNoWindow_thatCollectionViewCountsAreUpdated {
    // remove the collection view from self.window so that we use reloadData
    [self.collectionView removeFromSuperview];

    [self setupWithObjects:@[
                             genTestObject(@1, @8)
                             ]];
    IGTestObject *object = self.dataSource.objects[0];

    IGTestDelegateController *sectionController = [self.adapter sectionControllerForObject:object];

    XCTestExpectation *expectation = genExpectation;
    // using performBatchAnimated: to mimic re-entrant item reload
    [sectionController.collectionContext performBatchAnimated:YES updates:^(id<IGListBatchContext> batchContext) {
        object.value = @6; // from @1

        [batchContext reloadInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]];
        [batchContext deleteInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(5, 3)]];
        [batchContext insertInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]];

    } completion:^(BOOL finished2) {
        XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 6);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenPerformingUpdates_withoutSettingDataSource_thatCompletionBlockExecutes {
    IGListCollectionView *collectionView = [[IGListCollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:[UICollectionViewFlowLayout new]];
    [self.window addSubview:collectionView];
    IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil workingRangeSize:0];
    adapter.collectionView = collectionView;

    self.dataSource.objects = @[
                                genTestObject(@1, @1)
                                ];

    XCTestExpectation *expectation = genExpectation;

    // call -performUpdatesAnimated: before we have set the data source
    [adapter performUpdatesAnimated:YES completion:^(BOOL finished) {

        // since the data source isnt set, we complete syncronously. dispatch_async simulates setting the data source
        // in a different runloop from the completion block so it should be set by the time we make our subsequent
        // -performUpdatesAnimated: call
        dispatch_async(dispatch_get_main_queue(), ^{
            self.dataSource.objects = @[
                                        genTestObject(@1, @1),
                                        genTestObject(@2, @2)
                                        ];
            [adapter performUpdatesAnimated:YES completion:^(BOOL finished2) {
                XCTAssertEqual([collectionView numberOfSections], 2);
                [expectation fulfill];
            }];
        });
    }];

    // setting the data source immediately queries it, since the collection view is also set
    adapter.dataSource = self.dataSource;
    // simulate display reloading data on the collection view
    [collectionView layoutIfNeeded];

    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenPerformingUpdates_withItemsMovingInBlocks_thatCollectionViewWorks {
    [self setupWithObjects:@[
                             genTestObject(@1, @0),
                             genTestObject(@2, @7),
                             genTestObject(@3, @8),
                             genTestObject(@4, @8),
                             genTestObject(@5, @8),
                             genTestObject(@6, @5),
                             genTestObject(@7, @8),
                             genTestObject(@8, @8),
                             genTestObject(@9, @8),
                             ]];

    IGListCollectionView *collectionView = [[IGListCollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:[UICollectionViewFlowLayout new]];
    [self.window addSubview:collectionView];
    IGListAdapterUpdater *updater = [IGListAdapterUpdater new];
    IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil workingRangeSize:0];
    adapter.dataSource = self.dataSource;
    adapter.collectionView = collectionView;
    [collectionView layoutSubviews];

    XCTAssertEqual([collectionView numberOfSections], 9);

    self.dataSource.objects = @[
                                genTestObject(@1, @0),
                                genTestObject(@10, @5),
                                genTestObject(@11, @7),
                                genTestObject(@2, @7),
                                genTestObject(@3, @8),
                                genTestObject(@6, @5), // "moves" in front of 4, 5 but doesn't change index in array
                                genTestObject(@4, @8),
                                genTestObject(@5, @8),
                                genTestObject(@7, @8),
                                genTestObject(@8, @8),
                                ];

    XCTestExpectation *expectation = genExpectation;
    [adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
        XCTAssertEqual([collectionView numberOfSections], 10);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenReleasingObjects_thatAssertDoesntFire {
    [self setupWithObjects:@[
                             genTestObject(@1, @1)
                             ]];

    // if the adapter keeps a strong ref to self and uses an async method, this will hit asserts that a list item
    // controller is nil. the adapter should be released and the completion block never called.
    @autoreleasepool {
        IGListAdapterUpdater *updater = [[IGListAdapterUpdater alloc] init];
        IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil workingRangeSize:2];
        adapter.collectionView = self.collectionView;
        adapter.dataSource = self.dataSource;
        [adapter performUpdatesAnimated:NO completion:^(BOOL finished) {
            XCTAssertTrue(NO, @"Should not reach completion block for adapter");
        }];
    }

    self.collectionView = nil;
    self.dataSource = nil;

    // queued after perform updates
    XCTestExpectation *expectation = genExpectation;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [expectation fulfill];
    });
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenItemDeleted_withDisplayDelegate_thatDelegateReceivesDeletedItem {
    [self setupWithObjects:@[
                             genTestObject(@1, @1),
                             genTestObject(@2, @2),
                             ]];
    IGTestObject *object = self.dataSource.objects[0];

    self.dataSource.objects = @[
                                genTestObject(@2, @2),
                                ];

    id mockDisplayHandler = [OCMockObject mockForProtocol:@protocol(IGListAdapterDelegate)];
    self.adapter.delegate = mockDisplayHandler;

    [[mockDisplayHandler expect] listAdapter:self.adapter didEndDisplayingObject:object atIndex:0];

    XCTestExpectation *expectation = genExpectation;
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) {
        [mockDisplayHandler verify];
        XCTAssertTrue(finished2);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenItemReloaded_withDisplacingMutations_thatCollectionViewWorks {
    [self setupWithObjects:@[
                             genTestObject(@1, @1),
                             genTestObject(@2, @1),
                             genTestObject(@3, @1),
                             genTestObject(@4, @1),
                             genTestObject(@5, @1),
                             ]];

    self.dataSource.objects = @[
                                genTestObject(@1, @1),
                                genTestObject(@2, @2), // reloaded
                                genTestObject(@5, @2), // reloaded
                                genTestObject(@4, @2), // reloaded
                                genTestObject(@3, @1),
                                ];

    XCTestExpectation *expectation = genExpectation;
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
        XCTAssertTrue(finished);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenCollectionViewAppears_thatWillDisplayEventsAreSent {
    [self setupWithObjects:@[
                             genTestObject(@1, @1),
                             genTestObject(@2, @2),
                             ]];
    IGTestDelegateController *ic1 = [self.adapter sectionControllerForObject:self.dataSource.objects[0]];
    XCTAssertEqual(ic1.willDisplayCount, 1);
    XCTAssertEqual(ic1.didEndDisplayCount, 0);
    XCTAssertEqual([ic1.willDisplayCellIndexes countForObject:@0], 1);
    XCTAssertEqual([ic1.didEndDisplayCellIndexes countForObject:@0], 0);

    IGTestDelegateController *ic2 = [self.adapter sectionControllerForObject:self.dataSource.objects[1]];
    XCTAssertEqual(ic2.willDisplayCount, 1);
    XCTAssertEqual(ic2.didEndDisplayCount, 0);
    XCTAssertEqual([ic2.willDisplayCellIndexes countForObject:@0], 1);
    XCTAssertEqual([ic2.willDisplayCellIndexes countForObject:@1], 1);
    XCTAssertEqual([ic2.didEndDisplayCellIndexes countForObject:@0], 0);
    XCTAssertEqual([ic2.didEndDisplayCellIndexes countForObject:@1], 0);
}

- (void)test_whenAdapterUpdates_withItemUpdated_thatdidEndDisplayEventsAreSent {
    [self setupWithObjects:@[
                             genTestObject(@1, @1),
                             genTestObject(@2, @2),
                             ]];
    IGTestDelegateController *ic1 = [self.adapter sectionControllerForObject:self.dataSource.objects[0]];
    IGTestDelegateController *ic2 = [self.adapter sectionControllerForObject:self.dataSource.objects[1]];

    self.dataSource.objects = @[
                                genTestObject(@1, @1),
                                genTestObject(@2, @1), // reloaded w/ 1 cell removed
                                ];

    XCTestExpectation *expectation = genExpectation;
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
        XCTAssertEqual(ic1.willDisplayCount, 1);
        XCTAssertEqual(ic1.didEndDisplayCount, 0);
        XCTAssertEqual([ic1.willDisplayCellIndexes countForObject:@0], 1);
        XCTAssertEqual([ic1.didEndDisplayCellIndexes countForObject:@0], 0);

        XCTAssertEqual(ic2.willDisplayCount, 1);
        XCTAssertEqual(ic2.didEndDisplayCount, 0);
        XCTAssertEqual([ic2.willDisplayCellIndexes countForObject:@1], 1);
        XCTAssertEqual([ic2.didEndDisplayCellIndexes countForObject:@1], 1);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenAdapterUpdates_withItemRemoved_thatdidEndDisplayEventsAreSent {
    [self setupWithObjects:@[
                             genTestObject(@1, @1),
                             genTestObject(@2, @2),
                             ]];
    IGTestDelegateController *ic1 = [self.adapter sectionControllerForObject:self.dataSource.objects[0]];
    IGTestDelegateController *ic2 = [self.adapter sectionControllerForObject:self.dataSource.objects[1]];

    self.dataSource.objects = @[
                                genTestObject(@1, @1)
                                ];

    XCTestExpectation *expectation = genExpectation;
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
        XCTAssertEqual(ic1.willDisplayCount, 1);
        XCTAssertEqual(ic1.didEndDisplayCount, 0);
        XCTAssertEqual([ic1.willDisplayCellIndexes countForObject:@0], 1);
        XCTAssertEqual([ic1.didEndDisplayCellIndexes countForObject:@0], 0);

        XCTAssertEqual(ic2.willDisplayCount, 1);
        XCTAssertEqual(ic2.didEndDisplayCount, 1);
        XCTAssertEqual([ic2.willDisplayCellIndexes countForObject:@0], 1);
        XCTAssertEqual([ic2.willDisplayCellIndexes countForObject:@1], 1);
        XCTAssertEqual([ic2.didEndDisplayCellIndexes countForObject:@0], 1);
        XCTAssertEqual([ic2.didEndDisplayCellIndexes countForObject:@1], 1);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenAdapterUpdates_withEmptyItems_thatdidEndDisplayEventsAreSent {
    [self setupWithObjects:@[
                             genTestObject(@1, @1),
                             genTestObject(@2, @2),
                             ]];
    IGTestDelegateController *ic1 = [self.adapter sectionControllerForObject:self.dataSource.objects[0]];
    IGTestDelegateController *ic2 = [self.adapter sectionControllerForObject:self.dataSource.objects[1]];

    self.dataSource.objects = @[];

    XCTestExpectation *expectation = genExpectation;
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
        XCTAssertEqual(ic1.didEndDisplayCount, 1);
        XCTAssertEqual([ic1.didEndDisplayCellIndexes countForObject:@0], 1);

        XCTAssertEqual(ic2.didEndDisplayCount, 1);
        XCTAssertEqual([ic2.didEndDisplayCellIndexes countForObject:@0], 1);
        XCTAssertEqual([ic2.didEndDisplayCellIndexes countForObject:@1], 1);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenBatchUpdating_withCellQuery_thatCellIsNil {
    __block BOOL executed = NO;
    __weak __typeof__(self) weakSelf = self;
    void (^block)(IGTestDelegateController *) = ^(IGTestDelegateController *ic) {
        executed = YES;
        XCTAssertNil([weakSelf.adapter cellForItemAtIndex:0 sectionController:ic]);
    };
    self.dataSource.cellConfigureBlock = block;

    [self setupWithObjects:@[
                             genTestObject(@1, @1),
                             genTestObject(@2, @1),
                             genTestObject(@3, @1),
                             ]];

    // delete the last object from the original array
    self.dataSource.objects = @[
                                genTestObject(@1, @1),
                                genTestObject(@2, @1),
                                genTestObject(@4, @1),
                                genTestObject(@5, @1),
                                genTestObject(@6, @1),
                                ];
    XCTestExpectation *expectation = genExpectation;
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenBatchUpdating_withDuplicateIdentifiers_thatHaveDifferentValues_thatCollectionViewWorks {
    [self setupWithObjects:@[
                             // using string values IGTestDelegateController always returns 1 cell
                             genTestObject(@1, @"a"),
                             genTestObject(@2, @"a"),
                             genTestObject(@3, @"a"),
                             genTestObject(@4, @"b"), // problem item w/ key 4, value "b"
                             ]];

    self.dataSource.objects = @[
                                genTestObject(@1, @"a"),
                                genTestObject(@5, @"a"),
                                genTestObject(@6, @"a"),
                                genTestObject(@7, @"a"),
                                genTestObject(@4, @"a"), // key 4 but value "a", so this needs reloaded
                                genTestObject(@8, @"a"),
                                genTestObject(@4, @"b"), // key 4 but value didn't change
                                ];
    XCTestExpectation *expectation = genExpectation;

    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
        [expectation fulfill];
    }];

    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenPerformingUpdates_withWorkingRange_thatAccessingCellDoesntCrash {
    [self setupWithObjects:@[
                             genTestObject(@1, @1),
                             genTestObject(@2, @1),
                             genTestObject(@3, @1),
                             ]];

    // section controller try to access a cell in -listAdapter:sectionControllerWillEnterWorkingRange:
    // add items beyond the 100x100 frame so they access unavailable cells
    self.dataSource.objects = @[
                                genTestObject(@1, @1),
                                genTestObject(@2, @1),
                                genTestObject(@3, @1),
                                genTestObject(@4, @1),
                                genTestObject(@5, @1),
                                genTestObject(@6, @1),
                                genTestObject(@7, @1),
                                genTestObject(@8, @1),
                                genTestObject(@9, @1),
                                genTestObject(@10, @1),
                                genTestObject(@11, @1),
                                ];
    XCTestExpectation *expectation = genExpectation;

    // this will call -collectionView:performBatchUpdates:, trigger collectionView:willDisplayCell:forItemAtIndexPath:,
    // which kicks off the working range logic
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
        [expectation fulfill];
    }];

    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenReloadingItems_withDeleteAndInsertCollision_thatUpdateCanBeApplied {
    [self setupWithObjects:@[
                             genTestObject(@1, @1),
                             genTestObject(@2, @5),
                             genTestObject(@3, @1),
                             ]];

    IGListSectionController<IGListSectionType> *section = [self.adapter sectionControllerForObject:self.dataSource.objects[1]];

    XCTestExpectation *expectation = genExpectation;
    [section.collectionContext performBatchAnimated:NO updates:^(id<IGListBatchContext> batchContext) {
        [batchContext deleteInSectionController:section atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, 2)]];
        [batchContext insertInSectionController:section atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, 2)]];
        [batchContext reloadInSectionController:section atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, 4)]];
    } completion:^(BOOL finished) {
        [expectation fulfill];
    }];

    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenReloadingItems_withSectionInsertedInFront_thatUpdateCanBeApplied {
    [self setupWithObjects:@[
                             genTestObject(@1, @1),
                             genTestObject(@2, @5),
                             genTestObject(@3, @1),
                             ]];

    IGListSectionController<IGListSectionType> *section = [self.adapter sectionControllerForObject:self.dataSource.objects[1]];

    XCTestExpectation *expectation1 = genExpectation;
    [section.collectionContext performBatchAnimated:NO updates:^(id<IGListBatchContext> batchContext) {
        [batchContext reloadInSectionController:section atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, 4)]];
    } completion:^(BOOL finished) {
        [expectation1 fulfill];
    }];

    self.dataSource.objects = @[
                                genTestObject(@1, @1),
                                genTestObject(@4, @1), // insert to shift object @2
                                genTestObject(@2, @5),
                                genTestObject(@3, @1),
                                ];

    XCTestExpectation *expectation2 = genExpectation;
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
        [expectation2 fulfill];
    }];

    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenReloadingItems_withSectionDeletedInFront_thatUpdateCanBeApplied {
    [self setupWithObjects:@[
                             genTestObject(@1, @1),
                             genTestObject(@2, @5),
                             genTestObject(@3, @1),
                             ]];

    IGListSectionController<IGListSectionType> *section = [self.adapter sectionControllerForObject:self.dataSource.objects[1]];

    XCTestExpectation *expectation1 = genExpectation;
    [section.collectionContext performBatchAnimated:NO updates:^(id<IGListBatchContext> batchContext) {
        [batchContext reloadInSectionController:section atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, 4)]];
    } completion:^(BOOL finished) {
        [expectation1 fulfill];
    }];

    self.dataSource.objects = @[
                                genTestObject(@2, @5),
                                genTestObject(@3, @1),
                                ];

    XCTestExpectation *expectation2 = genExpectation;
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
        [expectation2 fulfill];
    }];

    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenDataSourceDeallocatedAfterUpdateQueued_thatUpdateSuccesfullyCompletes {
    IGTestDelegateDataSource *dataSource = [IGTestDelegateDataSource new];
    dataSource.objects = @[genTestObject(@1, @1)];
    self.adapter.collectionView = self.collectionView;
    self.adapter.dataSource = dataSource;
    [self.collectionView layoutIfNeeded];

    dataSource.objects = @[
                           genTestObject(@1, @1),
                           genTestObject(@2, @2),
                           ];

    XCTestExpectation *expectation = genExpectation;
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
        XCTAssertEqual([self.collectionView numberOfSections], 2);
        [expectation fulfill];
    }];

    dataSource = nil;

    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenQueuingUpdate_withSectionControllerBatchUpdate_thatSectionControllerNotRetained {
    __weak id weakSectionController = nil;
    @autoreleasepool {
        IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil workingRangeSize:0];
        IGTestDelegateDataSource *dataSource = [IGTestDelegateDataSource new];
        IGTestObject *object = genTestObject(@1, @2);
        dataSource.objects = @[object];
        IGListCollectionView *collectionView = [[IGListCollectionView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) collectionViewLayout:[UICollectionViewFlowLayout new]];
        adapter.collectionView = collectionView;
        adapter.dataSource = dataSource;
        [collectionView layoutIfNeeded];
        XCTAssertEqual([collectionView numberOfSections], 1);
        XCTAssertEqual([collectionView numberOfItemsInSection:0], 2);

        IGListSectionController<IGListSectionType> *section = [adapter sectionControllerForObject:object];

        [section.collectionContext performBatchAnimated:YES updates:^(id<IGListBatchContext> batchContext) {
            object.value = @3;
            [batchContext insertInSectionController:section atIndexes:[NSIndexSet indexSetWithIndex:0]];
        } completion:^(BOOL finished) {}];

        dataSource.objects = @[object, genTestObject(@2, @2)];
        [adapter performUpdatesAnimated:YES completion:^(BOOL finished) {}];

        weakSectionController = section;
        XCTAssertNotNil(weakSectionController);
    }
    XCTAssertNil(weakSectionController);
}

- (void)test_whenMovingItems_withObjectMoving_thatCollectionViewWorks {
    [self setupWithObjects:@[
                             genTestObject(@1, @2),
                             genTestObject(@2, @2),
                             genTestObject(@3, @2),
                             ]];

    __block BOOL executed = NO;
    IGListSectionController<IGListSectionType> *section = [self.adapter sectionControllerForObject:self.dataSource.objects.lastObject];
    [section.collectionContext performBatchAnimated:YES updates:^(id<IGListBatchContext> batchContext) {
        [batchContext moveInSectionController:section fromIndex:0 toIndex:1];
        executed = YES;
    } completion:nil];

    self.dataSource.objects = @[
                                genTestObject(@3, @2),
                                genTestObject(@1, @2),
                                genTestObject(@2, @2),
                                ];

    XCTestExpectation *expectation = genExpectation;
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
        XCTAssertTrue(executed);
        [expectation fulfill];
    }];

    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenMovingItems_withObjectReloaded_thatCollectionViewWorks {
    [self setupWithObjects:@[
                             genTestObject(@1, @2),
                             ]];

    __block BOOL executed = NO;
    IGListSectionController<IGListSectionType> *section = [self.adapter sectionControllerForObject:self.dataSource.objects.lastObject];
    [section.collectionContext performBatchAnimated:YES updates:^(id<IGListBatchContext> batchContext) {
        [batchContext moveInSectionController:section fromIndex:0 toIndex:1];
        executed = YES;
    } completion:nil];

    self.dataSource.objects = @[
                                genTestObject(@1, @3),
                                ];

    XCTestExpectation *expectation = genExpectation;
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
        XCTAssertTrue(executed);
        [expectation fulfill];
    }];

    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenMovingItems_withObjectDeleted_thatCollectionViewWorks {
    [self setupWithObjects:@[
                             genTestObject(@1, @2),
                             ]];

    __block BOOL executed = NO;
    IGListSectionController<IGListSectionType> *section = [self.adapter sectionControllerForObject:self.dataSource.objects.lastObject];
    [section.collectionContext performBatchAnimated:YES updates:^(id<IGListBatchContext> batchContext) {
        [batchContext moveInSectionController:section fromIndex:0 toIndex:1];
        executed = YES;
    } completion:nil];

    self.dataSource.objects = @[];

    XCTestExpectation *expectation = genExpectation;
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
        XCTAssertTrue(executed);
        [expectation fulfill];
    }];

    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenMovingItems_withObjectInsertedBefore_thatCollectionViewWorks {
    [self setupWithObjects:@[
                             genTestObject(@1, @2),
                             ]];

    __block BOOL executed = NO;
    IGListSectionController<IGListSectionType> *section = [self.adapter sectionControllerForObject:self.dataSource.objects.lastObject];
    [section.collectionContext performBatchAnimated:YES updates:^(id<IGListBatchContext> batchContext) {
        [batchContext moveInSectionController:section fromIndex:0 toIndex:1];
        executed = YES;
    } completion:nil];

    [self setupWithObjects:@[
                             genTestObject(@2, @2),
                             genTestObject(@1, @2),
                             ]];

    XCTestExpectation *expectation = genExpectation;
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
        XCTAssertTrue(executed);
        [expectation fulfill];
    }];

    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenMovingItems_thatCollectionViewWorks {
    [self setupWithObjects:@[
                             genTestObject(@1, @2),
                             ]];

    IGTestCell *cell1 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
    IGTestCell *cell2 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]];
    cell1.label.text = @"foo";
    cell2.label.text = @"bar";

    XCTestExpectation *expectation = genExpectation;
    IGListSectionController<IGListSectionType> *section = [self.adapter sectionControllerForObject:self.dataSource.objects.lastObject];
    [section.collectionContext performBatchAnimated:YES updates:^(id<IGListBatchContext> batchContext) {
        [batchContext moveInSectionController:section fromIndex:0 toIndex:1];
    } completion:^(BOOL finished) {
        IGTestCell *movedCell1 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
        IGTestCell *movedCell2 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]];
        XCTAssertEqualObjects(movedCell1.label.text, @"bar");
        XCTAssertEqualObjects(movedCell2.label.text, @"foo");

        [expectation fulfill];
    }];

    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenInvalidatingSectionController_withSizeChange_thatCellsAreSameInstance_thatCellsFrameChanged {
    [self setupWithObjects:@[
                             genTestObject(@1, @2),
                             ]];

    NSIndexPath *path1 = [NSIndexPath indexPathForItem:0 inSection:0];
    NSIndexPath *path2 = [NSIndexPath indexPathForItem:1 inSection:0];
    IGTestCell *cell1 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:path1];
    IGTestCell *cell2 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:path2];

    XCTAssertEqual(cell1.frame.size.height, 10);
    XCTAssertEqual(cell2.frame.size.height, 10);

    IGTestDelegateController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.lastObject];
    section.height = 20.0;

    XCTestExpectation *expectation = genExpectation;
    [section.collectionContext invalidateLayoutForSectionController:section completion:^(BOOL finished) {
        XCTAssertEqual(cell1, [self.collectionView cellForItemAtIndexPath:path1]);
        XCTAssertEqual(cell2, [self.collectionView cellForItemAtIndexPath:path2]);
        XCTAssertEqual(cell1.frame.size.height, 20);
        XCTAssertEqual(cell2.frame.size.height, 20);
        [expectation fulfill];
    }];

    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenAdaptersSwapCollectionViews_thatOldAdapterDoesntUpdateOldCollectionView {
    IGListAdapter *adapter1 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil workingRangeSize:0];
    IGTestDelegateDataSource *dataSource1 = [IGTestDelegateDataSource new];
    dataSource1.objects = @[genTestObject(@1, @2)];
    adapter1.dataSource = dataSource1;
    adapter1.collectionView = self.collectionView;

    [self.collectionView layoutIfNeeded];
    XCTAssertEqual([self.collectionView numberOfSections], 1);
    XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2);

    IGListAdapter *adapter2 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil workingRangeSize:0];
    IGTestDelegateDataSource *dataSource2 = [IGTestDelegateDataSource new];
    dataSource2.objects = @[genTestObject(@1, @1), genTestObject(@2, @1)];
    adapter2.dataSource = dataSource2;
    adapter2.collectionView = self.collectionView;

    XCTAssertEqual([self.collectionView numberOfSections], 2);
    XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1);
    XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1);

    dataSource1.objects = @[genTestObject(@1, @2), genTestObject(@2, @2), genTestObject(@3, @2), genTestObject(@4, @2)];
    XCTestExpectation *expectation = genExpectation;

    [adapter1 performUpdatesAnimated:YES completion:^(BOOL finished) {
        XCTAssertEqual([self.collectionView numberOfSections], 2);
        XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1);
        XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1);
        [expectation fulfill];
    }];

    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenAdaptersSwapCollectionViews_ {
    IGListAdapter *adapter1 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil workingRangeSize:0];
    IGTestDelegateDataSource *dataSource1 = [IGTestDelegateDataSource new];
    dataSource1.objects = @[genTestObject(@1, @2)];
    adapter1.dataSource = dataSource1;
    adapter1.collectionView = self.collectionView;

    [self.collectionView layoutIfNeeded];
    XCTAssertEqual([self.collectionView numberOfSections], 1);
    XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2);

    IGListAdapter *adapter2 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil workingRangeSize:0];
    IGTestDelegateDataSource *dataSource2 = [IGTestDelegateDataSource new];
    dataSource2.objects = @[genTestObject(@1, @1), genTestObject(@2, @1)];
    adapter2.dataSource = dataSource2;
    adapter2.collectionView = self.collectionView;

    [self.collectionView layoutIfNeeded];
    XCTAssertEqual([self.collectionView numberOfSections], 2);
    XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1);
    XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1);

    dataSource2.objects = @[genTestObject(@1, @2), genTestObject(@2, @1), genTestObject(@3, @1), genTestObject(@4, @1)];
    XCTestExpectation *expectation = genExpectation;

    [adapter2 performUpdatesAnimated:YES completion:^(BOOL finished) {
        XCTAssertEqual([self.collectionView numberOfSections], 4);
        XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2);
        XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1);
        XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1);
        XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1);
        [expectation fulfill];
    }];

    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenDidUpdateAsyncReloads_withBatchUpdatesInProgress_thatReloadIsExecuted {
    [self setupWithObjects:@[
                             genTestObject(@1, @1)
                             ]];
    
    IGTestDelegateController *section = [self.adapter sectionControllerForObject:self.dataSource.objects[0]];
    
    XCTestExpectation *expectation1 = genExpectation;
    __weak __typeof__(section) weakSection = section;
    section.itemUpdateBlock = ^{
        // currently inside -[IGListSectionType didUpdateToObject:], change the item (note: NEVER do this) manually
        // so that the data powering numberOfItems changes (1 to 2). dispatch_async the update to skip outside of the
        // -[UICollectionView performBatchUpdates:completion:] block execution
        [weakSection.collectionContext performBatchAnimated:NO updates:^(id<IGListBatchContext> batchContext) {
            weakSection.item = genTestObject(@1, @2);
            [batchContext reloadSectionController:weakSection];
        } completion:^(BOOL finished) {
            [expectation1 fulfill];
            XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2);
        }];
    };
    
    // add an object so that a batch update is triggered (diff result has changes)
    self.dataSource.objects = @[
                                genTestObject(@1, @1),
                                genTestObject(@2, @1)
                                ];
    
    XCTestExpectation *expectation2 = genExpectation;
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
        // verify that the section still has 2 items since this completion executes AFTER the reload block above
        XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2);
        [expectation2 fulfill];
    }];
    
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

@end
