/**
 * 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 <IGListKit/IGListKit.h>

#import "IGTestDiffingDataSource.h"
#import "IGTestDiffingObject.h"
#import "IGTestDiffingSectionController.h"
#import "IGTestStringBindableCell.h"
#import "IGTestNumberBindableCell.h"
#import "IGListAdapterInternal.h"
#import "IGTestObject.h"
#import "IGTestCell.h"

@interface IGListBindingSectionControllerTests : XCTestCase

@property (nonatomic, strong) IGListCollectionView *collectionView;
@property (nonatomic, strong) IGListAdapter *adapter;
@property (nonatomic, strong) IGTestDiffingDataSource *dataSource;
@property (nonatomic, strong) UICollectionViewFlowLayout *layout;
@property (nonatomic, strong) UIWindow *window;

@end

@implementation IGListBindingSectionControllerTests

- (void)setUp {
    [super setUp];

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

    UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
    self.collectionView = [[IGListCollectionView alloc] initWithFrame:self.window.bounds collectionViewLayout:layout];

    [self.window addSubview:self.collectionView];

    self.dataSource = [IGTestDiffingDataSource new];
    self.adapter = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new]
                                           viewController:nil
                                         workingRangeSize:0];
}

- (void)tearDown {
    [super tearDown];

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

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

- (id)cellAtSection:(NSInteger)section item:(NSInteger)item {
    return [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:item inSection:section]];
}

- (void)test_whenInitialLoad_withEmptyViewModels_thatCollectionViewEmpty {
    [self setupWithObjects:@[
                             [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[]]
                             ]];
    XCTAssertEqual([self.collectionView numberOfSections], 1);
    XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 0);
}

- (void)test_whenInitialLoad_withMultipleViewModels_thatCellsMappedAndConfigured {
    [self setupWithObjects:@[
                             [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]],
                             [[IGTestDiffingObject alloc] initWithKey:@2 objects:@[@"foo", @"bar", @42]],
                             [[IGTestDiffingObject alloc] initWithKey:@3 objects:@[]],
                             ]];

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

    IGTestNumberBindableCell *cell00 = [self cellAtSection:0 item:0];
    IGTestStringBindableCell *cell01 = [self cellAtSection:0 item:1];
    IGTestStringBindableCell *cell10 = [self cellAtSection:1 item:0];
    IGTestStringBindableCell *cell11 = [self cellAtSection:1 item:1];
    IGTestNumberBindableCell *cell12 = [self cellAtSection:1 item:2];

    XCTAssertEqualObjects(cell00.textField.text, @"7");
    XCTAssertEqualObjects(cell01.label.text, @"seven");
    XCTAssertEqualObjects(cell10.label.text, @"foo");
    XCTAssertEqualObjects(cell11.label.text, @"bar");
    XCTAssertEqualObjects(cell12.textField.text, @"42");
}

- (void)test_whenUpdating_withAddedModels_thatCellsCorrectAndConfigured {
    [self setupWithObjects:@[
                             [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]],
                             ]];

    self.dataSource.objects = @[
                                [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven", @8, @"eight"]],
                                ];

    XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)];
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
        XCTAssertEqual([self.collectionView numberOfSections], 1);
        XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 4);

        IGTestNumberBindableCell *cell00 = [self cellAtSection:0 item:0];
        IGTestStringBindableCell *cell01 = [self cellAtSection:0 item:1];
        IGTestNumberBindableCell *cell02 = [self cellAtSection:0 item:2];
        IGTestStringBindableCell *cell03 = [self cellAtSection:0 item:3];

        XCTAssertEqualObjects(cell00.textField.text, @"7");
        XCTAssertEqualObjects(cell01.label.text, @"seven");
        XCTAssertEqualObjects(cell02.textField.text, @"8");
        XCTAssertEqualObjects(cell03.label.text, @"eight");

        [expectation fulfill];
    }];

    [self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenSelectingCell_thatCorrectViewModelSelected {
    [self setupWithObjects:@[
                             [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]],
                             ]];
    [self.adapter collectionView:self.collectionView didSelectItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]];
    IGTestDiffingSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject];
    XCTAssertEqualObjects(section.selectedViewModel, @"seven");
}

- (void)test_whenAdapterReloadsObjects_thatSectionUpdated {
    [self setupWithObjects:@[
                             [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]],
                             ]];
    [self.adapter reloadObjects:@[[[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@"four", @4, @"seven", @7]]]];
    
    IGTestNumberBindableCell *cell00 = [self cellAtSection:0 item:0];
    IGTestStringBindableCell *cell01 = [self cellAtSection:0 item:1];
    
    XCTAssertEqualObjects(cell00.textField.text, @"7");
    XCTAssertEqualObjects(cell01.label.text, @"seven");
    XCTAssertNil([self cellAtSection:0 item:2]);
    XCTAssertNil([self cellAtSection:0 item:3]);
    
    // "fake" batch updates to make sure that calling reload triggers a diffed batch update
    XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)];
    [self.adapter performBatchAnimated:YES updates:^(id<IGListBatchContext> batchContext){} completion:^(BOOL finished) {
        IGTestStringBindableCell *batchedCell00 = [self cellAtSection:0 item:0];
        IGTestNumberBindableCell *batchedCell01 = [self cellAtSection:0 item:1];
        IGTestStringBindableCell *batchedCell02 = [self cellAtSection:0 item:2];
        IGTestNumberBindableCell *batchedCell03 = [self cellAtSection:0 item:3];
        
        XCTAssertEqualObjects(batchedCell00.label.text, @"four");
        XCTAssertEqualObjects(batchedCell01.textField.text, @"4");
        XCTAssertEqualObjects(batchedCell02.label.text, @"seven");
        XCTAssertEqualObjects(batchedCell03.textField.text, @"7");
        
        [expectation fulfill];
    }];
    
    [self waitForExpectationsWithTimeout:16 handler:nil];
}

- (void)test_whenUpdating_withViewModelMovesAndReloads_thatCellUpdatedAndInstanceSame {
    NSArray *initObjects = @[
                             @"foo",
                             @"bar",
                             [[IGTestObject alloc] initWithKey:@42 value:@"baz"]
                             ];
    [self setupWithObjects:@[
                             [[IGTestDiffingObject alloc] initWithKey:@1 objects:initObjects]
                             ]];
    
    XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3);
    
    IGTestStringBindableCell *cell00 = [self cellAtSection:0 item:0];
    IGTestStringBindableCell *cell01 = [self cellAtSection:0 item:1];
    IGTestCell *cell02 = [self cellAtSection:0 item:2];
    
    XCTAssertEqualObjects(cell00.label.text, @"foo");
    XCTAssertEqualObjects(cell01.label.text, @"bar");
    XCTAssertEqualObjects(cell02.label.text, @"baz");

    NSArray *newObjects = @[
                            [[IGTestObject alloc] initWithKey:@42 value:@"bang"], // moved to section 0 and value changed
                            @"foo",
                            @"bar",
                            ];
    self.dataSource.objects = @[
                                [[IGTestDiffingObject alloc] initWithKey:@1 objects:newObjects]
                                ];
    
    XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)];
    [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
        IGTestCell *batchedCell00 = [self cellAtSection:0 item:0];
        IGTestStringBindableCell *batchedCell01 = [self cellAtSection:0 item:1];
        IGTestStringBindableCell *batchedCell02 = [self cellAtSection:0 item:2];
        
        XCTAssertEqualObjects(batchedCell00.label.text, @"bang");
        XCTAssertEqualObjects(batchedCell01.label.text, @"foo");
        XCTAssertEqualObjects(batchedCell02.label.text, @"bar");
        
        XCTAssertEqual(cell00, batchedCell01);
        XCTAssertEqual(cell01, batchedCell02);
        XCTAssertEqual(cell02, batchedCell00);
        
        [expectation fulfill];
    }];
    
    [self waitForExpectationsWithTimeout:16 handler:nil];
}

- (void)test_whenUpdatingManually_with2Updates_thatBothCompletionBlocksCalled {
    [self setupWithObjects:@[
                             [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]],
                             ]];
    IGTestDiffingSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects[0]];
    
    XCTestExpectation *expectation1 = [self expectationWithDescription:NSStringFromSelector(_cmd)];
    [section updateAnimated:YES completion:^(BOOL updated) {
        XCTAssertTrue(updated);
        [expectation1 fulfill];
    }];
    
    XCTestExpectation *expectation2 = [self expectationWithDescription:NSStringFromSelector(_cmd)];
    [section updateAnimated:YES completion:^(BOOL updated) {
        // queued second, shouldn't execute update block
        XCTAssertFalse(updated);
        [expectation2 fulfill];
    }];
    
    [self waitForExpectationsWithTimeout:15 handler:nil];
}

@end
