// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/broadcaster/ui_bundled/chrome_broadcaster.h"

#import "ios/chrome/browser/broadcaster/ui_bundled/chrome_broadcast_observer.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#import "testing/perf/perf_test.h"
#import "testing/platform_test.h"

@interface TestObserver : NSObject <ChromeBroadcastObserver>
@property(nonatomic) BOOL lastObservedBool;
@property(nonatomic) CGFloat lastObservedCGFloat;
@property(nonatomic) CGSize lastObservedCGSize;
@property(nonatomic) UIEdgeInsets lastObservedUIEdgeInsets;
@property(nonatomic) NSInteger tabStripVisibleCallCount;
@property(nonatomic) NSInteger contentScrollOffsetCallCount;
@property(nonatomic) NSInteger scrollViewSizeCallCount;
@property(nonatomic) NSInteger contentSizeCallCount;
@property(nonatomic) NSInteger contentInsetCallCount;
@end

@implementation TestObserver
@synthesize lastObservedBool = _lastObservedBool;
@synthesize lastObservedCGFloat = _lastObservedCGFloat;
@synthesize lastObservedCGSize = _lastObservedCGSize;
@synthesize lastObservedUIEdgeInsets = _lastObservedUIEdgeInsets;
@synthesize tabStripVisibleCallCount = _tabStripVisibleCallCount;
@synthesize contentScrollOffsetCallCount = _contentScrollOffsetCallCount;
@synthesize scrollViewSizeCallCount = _scrollViewSizeCallCount;
@synthesize contentSizeCallCount = _contentSizeCallCount;
@synthesize contentInsetCallCount = _contentInsetCallCount;

- (void)broadcastScrollViewIsScrolling:(BOOL)visible {
  self.tabStripVisibleCallCount++;
  self.lastObservedBool = visible;
}

- (void)broadcastContentScrollOffset:(CGFloat)offset {
  self.contentScrollOffsetCallCount++;
  self.lastObservedCGFloat = offset;
}

- (void)broadcastScrollViewSize:(CGSize)scrollViewSize {
  self.scrollViewSizeCallCount++;
  self.lastObservedCGSize = scrollViewSize;
}

- (void)broadcastScrollViewContentSize:(CGSize)contentSize {
  self.contentSizeCallCount++;
  self.lastObservedCGSize = contentSize;
}

- (void)broadcastScrollViewContentInset:(UIEdgeInsets)contentInset {
  self.contentInsetCallCount++;
  self.lastObservedUIEdgeInsets = contentInset;
}

@end

@interface TestObservable : NSObject
@property(nonatomic) BOOL observableBool;
@property(nonatomic) CGFloat observableCGFloat;
@property(nonatomic) CGSize observableCGSize;
@property(nonatomic) UIEdgeInsets observableUIEdgeInsets;
@end
@implementation TestObservable
@synthesize observableBool = _observableBool;
@synthesize observableCGFloat = _observableCGFloat;
@synthesize observableCGSize = _observableCGSize;
@synthesize observableUIEdgeInsets = _observableUIEdgeInsets;
@end

typedef PlatformTest ChromeBroadcasterTest;

TEST_F(ChromeBroadcasterTest, TestBroadcastBoolFirst) {
  ChromeBroadcaster* broadcaster = [[ChromeBroadcaster alloc] init];
  TestObservable* observable = [[TestObservable alloc] init];
  observable.observableBool = NO;

  [broadcaster broadcastValue:@"observableBool"
                     ofObject:observable
                     selector:@selector(broadcastScrollViewIsScrolling:)];

  observable.observableBool = YES;

  TestObserver* observer = [[TestObserver alloc] init];
  EXPECT_FALSE(observer.lastObservedBool);
  EXPECT_EQ(0, observer.tabStripVisibleCallCount);
  [broadcaster addObserver:observer
               forSelector:@selector(broadcastScrollViewIsScrolling:)];
  EXPECT_EQ(1, observer.tabStripVisibleCallCount);
  EXPECT_TRUE(observer.lastObservedBool);
  observable.observableBool = NO;
  EXPECT_FALSE(observer.lastObservedBool);
  EXPECT_EQ(2, observer.tabStripVisibleCallCount);
}

TEST_F(ChromeBroadcasterTest, TestBroadcastFloatFirst) {
  ChromeBroadcaster* broadcaster = [[ChromeBroadcaster alloc] init];
  TestObservable* observable = [[TestObservable alloc] init];
  observable.observableCGFloat = 1.0;

  [broadcaster broadcastValue:@"observableCGFloat"
                     ofObject:observable
                     selector:@selector(broadcastContentScrollOffset:)];

  observable.observableCGFloat = 2.0;

  TestObserver* observer = [[TestObserver alloc] init];
  EXPECT_EQ(0.0, observer.lastObservedCGFloat);
  EXPECT_EQ(0, observer.contentScrollOffsetCallCount);
  [broadcaster addObserver:observer
               forSelector:@selector(broadcastContentScrollOffset:)];
  EXPECT_EQ(2.0, observer.lastObservedCGFloat);
  EXPECT_EQ(1, observer.contentScrollOffsetCallCount);
  observable.observableCGFloat = 3.0;
  EXPECT_EQ(3.0, observer.lastObservedCGFloat);
  EXPECT_EQ(2, observer.contentScrollOffsetCallCount);
}

TEST_F(ChromeBroadcasterTest, TestObserveBoolFirst) {
  ChromeBroadcaster* broadcaster = [[ChromeBroadcaster alloc] init];
  TestObserver* observer = [[TestObserver alloc] init];
  EXPECT_FALSE(observer.lastObservedBool);
  EXPECT_EQ(0, observer.tabStripVisibleCallCount);
  [broadcaster addObserver:observer
               forSelector:@selector(broadcastScrollViewIsScrolling:)];
  EXPECT_FALSE(observer.lastObservedBool);
  EXPECT_EQ(0, observer.tabStripVisibleCallCount);

  TestObservable* observable = [[TestObservable alloc] init];
  observable.observableBool = YES;
  EXPECT_FALSE(observer.lastObservedBool);
  EXPECT_EQ(0, observer.tabStripVisibleCallCount);

  [broadcaster broadcastValue:@"observableBool"
                     ofObject:observable
                     selector:@selector(broadcastScrollViewIsScrolling:)];
  EXPECT_TRUE(observer.lastObservedBool);
  EXPECT_EQ(1, observer.tabStripVisibleCallCount);
  observable.observableBool = NO;
  EXPECT_FALSE(observer.lastObservedBool);
  EXPECT_EQ(2, observer.tabStripVisibleCallCount);
}

TEST_F(ChromeBroadcasterTest, TestObserveFloatFirst) {
  ChromeBroadcaster* broadcaster = [[ChromeBroadcaster alloc] init];
  TestObserver* observer = [[TestObserver alloc] init];
  EXPECT_EQ(0.0, observer.lastObservedCGFloat);
  EXPECT_EQ(0, observer.contentScrollOffsetCallCount);
  [broadcaster addObserver:observer
               forSelector:@selector(broadcastContentScrollOffset:)];
  EXPECT_EQ(0.0, observer.lastObservedCGFloat);
  EXPECT_EQ(0, observer.contentScrollOffsetCallCount);

  TestObservable* observable = [[TestObservable alloc] init];
  observable.observableCGFloat = 1.0;
  EXPECT_EQ(0.0, observer.lastObservedCGFloat);
  EXPECT_EQ(0, observer.contentScrollOffsetCallCount);

  [broadcaster broadcastValue:@"observableCGFloat"
                     ofObject:observable
                     selector:@selector(broadcastContentScrollOffset:)];
  EXPECT_EQ(1.0, observer.lastObservedCGFloat);
  EXPECT_EQ(1, observer.contentScrollOffsetCallCount);

  observable.observableCGFloat = 2.0;
  EXPECT_EQ(2.0, observer.lastObservedCGFloat);
  EXPECT_EQ(2, observer.contentScrollOffsetCallCount);
}

TEST_F(ChromeBroadcasterTest, TestObserveScrollViewSizeFirst) {
  ChromeBroadcaster* broadcaster = [[ChromeBroadcaster alloc] init];
  TestObserver* observer = [[TestObserver alloc] init];
  EXPECT_TRUE(CGSizeEqualToSize(observer.lastObservedCGSize, CGSizeZero));
  EXPECT_EQ(0, observer.scrollViewSizeCallCount);
  [broadcaster addObserver:observer
               forSelector:@selector(broadcastScrollViewSize:)];
  EXPECT_TRUE(CGSizeEqualToSize(observer.lastObservedCGSize, CGSizeZero));
  EXPECT_EQ(0, observer.scrollViewSizeCallCount);

  TestObservable* observable = [[TestObservable alloc] init];
  CGSize kScrollViewSize1 = CGSizeMake(100, 100);
  observable.observableCGSize = kScrollViewSize1;
  EXPECT_TRUE(CGSizeEqualToSize(observer.lastObservedCGSize, CGSizeZero));
  EXPECT_EQ(0, observer.scrollViewSizeCallCount);

  [broadcaster broadcastValue:@"observableCGSize"
                     ofObject:observable
                     selector:@selector(broadcastScrollViewSize:)];
  EXPECT_TRUE(CGSizeEqualToSize(observer.lastObservedCGSize, kScrollViewSize1));
  EXPECT_EQ(1, observer.scrollViewSizeCallCount);

  CGSize kScrollViewSize2 = CGSizeMake(200, 200);
  observable.observableCGSize = kScrollViewSize2;
  EXPECT_TRUE(CGSizeEqualToSize(observer.lastObservedCGSize, kScrollViewSize2));
  EXPECT_EQ(2, observer.scrollViewSizeCallCount);
}

TEST_F(ChromeBroadcasterTest, TestObserveContentSizeFirst) {
  ChromeBroadcaster* broadcaster = [[ChromeBroadcaster alloc] init];
  TestObserver* observer = [[TestObserver alloc] init];
  EXPECT_TRUE(CGSizeEqualToSize(observer.lastObservedCGSize, CGSizeZero));
  EXPECT_EQ(0, observer.contentSizeCallCount);
  [broadcaster addObserver:observer
               forSelector:@selector(broadcastScrollViewContentSize:)];
  EXPECT_TRUE(CGSizeEqualToSize(observer.lastObservedCGSize, CGSizeZero));
  EXPECT_EQ(0, observer.contentSizeCallCount);

  TestObservable* observable = [[TestObservable alloc] init];
  CGSize kContentViewSize1 = CGSizeMake(100, 100);
  observable.observableCGSize = kContentViewSize1;
  EXPECT_TRUE(CGSizeEqualToSize(observer.lastObservedCGSize, CGSizeZero));
  EXPECT_EQ(0, observer.contentSizeCallCount);

  [broadcaster broadcastValue:@"observableCGSize"
                     ofObject:observable
                     selector:@selector(broadcastScrollViewContentSize:)];
  EXPECT_TRUE(
      CGSizeEqualToSize(observer.lastObservedCGSize, kContentViewSize1));
  EXPECT_EQ(1, observer.contentSizeCallCount);

  CGSize kContentViewSize2 = CGSizeMake(200, 200);
  observable.observableCGSize = kContentViewSize2;
  EXPECT_TRUE(
      CGSizeEqualToSize(observer.lastObservedCGSize, kContentViewSize2));
  EXPECT_EQ(2, observer.contentSizeCallCount);
}

TEST_F(ChromeBroadcasterTest, TestObserveContentInsetFirst) {
  ChromeBroadcaster* broadcaster = [[ChromeBroadcaster alloc] init];
  TestObserver* observer = [[TestObserver alloc] init];
  EXPECT_TRUE(UIEdgeInsetsEqualToEdgeInsets(observer.lastObservedUIEdgeInsets,
                                            UIEdgeInsetsZero));
  EXPECT_EQ(0, observer.contentInsetCallCount);
  [broadcaster addObserver:observer
               forSelector:@selector(broadcastScrollViewContentInset:)];
  EXPECT_TRUE(UIEdgeInsetsEqualToEdgeInsets(observer.lastObservedUIEdgeInsets,
                                            UIEdgeInsetsZero));
  EXPECT_EQ(0, observer.contentInsetCallCount);

  TestObservable* observable = [[TestObservable alloc] init];
  UIEdgeInsets kInsets1 = UIEdgeInsetsMake(1, 1, 1, 1);
  observable.observableUIEdgeInsets = kInsets1;
  EXPECT_TRUE(UIEdgeInsetsEqualToEdgeInsets(observer.lastObservedUIEdgeInsets,
                                            UIEdgeInsetsZero));
  EXPECT_EQ(0, observer.contentInsetCallCount);

  [broadcaster broadcastValue:@"observableUIEdgeInsets"
                     ofObject:observable
                     selector:@selector(broadcastScrollViewContentInset:)];
  EXPECT_TRUE(UIEdgeInsetsEqualToEdgeInsets(observer.lastObservedUIEdgeInsets,
                                            kInsets1));
  EXPECT_EQ(1, observer.contentInsetCallCount);

  UIEdgeInsets kInsets2 = UIEdgeInsetsMake(2, 2, 2, 2);
  observable.observableUIEdgeInsets = kInsets2;
  EXPECT_TRUE(UIEdgeInsetsEqualToEdgeInsets(observer.lastObservedUIEdgeInsets,
                                            kInsets2));
  EXPECT_EQ(2, observer.contentInsetCallCount);
}

TEST_F(ChromeBroadcasterTest, TestBroadcastManyFloats) {
  ChromeBroadcaster* broadcaster = [[ChromeBroadcaster alloc] init];
  NSMutableArray<TestObserver*>* observers = [[NSMutableArray alloc] init];
  for (size_t i = 0; i < 100; i++) {
    [observers addObject:[[TestObserver alloc] init]];
    [broadcaster addObserver:observers.lastObject
                 forSelector:@selector(broadcastContentScrollOffset:)];
  }

  TestObservable* observable = [[TestObservable alloc] init];
  observable.observableCGFloat = 1.0;
  [broadcaster broadcastValue:@"observableCGFloat"
                     ofObject:observable
                     selector:@selector(broadcastContentScrollOffset:)];
  // All observers should have the initial value set.
  for (TestObserver* observer in observers) {
    EXPECT_EQ(1.0, observer.lastObservedCGFloat);
    EXPECT_EQ(1, observer.contentScrollOffsetCallCount);
  }

  // Change the value a thousand times.
  NSDate* start = [NSDate date];
  for (size_t i = 0; i < 1000; i++) {
    observable.observableCGFloat += 1.0;
  }
  NSTimeInterval elapsed = -[start timeIntervalSinceNow] * 1000.0 /* to ms */;

  // Log the elapsed time for performance tracking.
  perf_test::PrintResult("Broadcast", "", "100 observers, 1000 updates",
                         elapsed, "ms", true /* "important" */);

  EXPECT_EQ(1001.0, observable.observableCGFloat);
  for (TestObserver* observer in observers) {
    EXPECT_EQ(1001.0, observer.lastObservedCGFloat);
    EXPECT_EQ(1001, observer.contentScrollOffsetCallCount);
  }
}

TEST_F(ChromeBroadcasterTest, TestBroadcastDuplicateFloats) {
  ChromeBroadcaster* broadcaster = [[ChromeBroadcaster alloc] init];
  TestObservable* observable = [[TestObservable alloc] init];
  observable.observableCGFloat = 1.0;

  [broadcaster broadcastValue:@"observableCGFloat"
                     ofObject:observable
                     selector:@selector(broadcastContentScrollOffset:)];

  observable.observableCGFloat = 2.0;

  TestObserver* observer = [[TestObserver alloc] init];
  [broadcaster addObserver:observer
               forSelector:@selector(broadcastContentScrollOffset:)];
  EXPECT_EQ(2.0, observer.lastObservedCGFloat);
  EXPECT_EQ(1, observer.contentScrollOffsetCallCount);
  observable.observableCGFloat = 2.0;
  EXPECT_EQ(2.0, observer.lastObservedCGFloat);
  EXPECT_EQ(1, observer.contentScrollOffsetCallCount);
  observable.observableCGFloat = 3.0;
  EXPECT_EQ(3.0, observer.lastObservedCGFloat);
  EXPECT_EQ(2, observer.contentScrollOffsetCallCount);
  observable.observableCGFloat = 3.0;
  EXPECT_EQ(3.0, observer.lastObservedCGFloat);
  EXPECT_EQ(2, observer.contentScrollOffsetCallCount);
}

TEST_F(ChromeBroadcasterTest, TestSeparateObservers) {
  ChromeBroadcaster* broadcaster = [[ChromeBroadcaster alloc] init];
  TestObserver* boolObserver = [[TestObserver alloc] init];
  TestObserver* floatObserver = [[TestObserver alloc] init];

  TestObservable* observable = [[TestObservable alloc] init];

  [broadcaster broadcastValue:@"observableBool"
                     ofObject:observable
                     selector:@selector(broadcastScrollViewIsScrolling:)];
  [broadcaster broadcastValue:@"observableCGFloat"
                     ofObject:observable
                     selector:@selector(broadcastContentScrollOffset:)];

  [broadcaster addObserver:boolObserver
               forSelector:@selector(broadcastScrollViewIsScrolling:)];
  [broadcaster addObserver:floatObserver
               forSelector:@selector(broadcastContentScrollOffset:)];
  EXPECT_FALSE(boolObserver.lastObservedBool);
  EXPECT_EQ(1, boolObserver.tabStripVisibleCallCount);
  EXPECT_EQ(0, floatObserver.tabStripVisibleCallCount);
  EXPECT_EQ(0.0, floatObserver.lastObservedCGFloat);
  EXPECT_EQ(1, floatObserver.contentScrollOffsetCallCount);
  EXPECT_EQ(0, boolObserver.contentScrollOffsetCallCount);

  observable.observableCGFloat = 5.0;
  EXPECT_EQ(5.0, floatObserver.lastObservedCGFloat);
  EXPECT_EQ(2, floatObserver.contentScrollOffsetCallCount);
  EXPECT_EQ(0.0, boolObserver.lastObservedCGFloat);
  EXPECT_EQ(0, boolObserver.contentScrollOffsetCallCount);

  observable.observableBool = YES;
  EXPECT_TRUE(boolObserver.lastObservedBool);
  EXPECT_EQ(2, boolObserver.tabStripVisibleCallCount);
  EXPECT_FALSE(floatObserver.lastObservedBool);
  EXPECT_EQ(0, floatObserver.tabStripVisibleCallCount);
}

TEST_F(ChromeBroadcasterTest, TestStopBroadcasting) {
  ChromeBroadcaster* broadcaster = [[ChromeBroadcaster alloc] init];
  TestObservable* observable = [[TestObservable alloc] init];
  observable.observableCGFloat = 1.0;

  [broadcaster broadcastValue:@"observableCGFloat"
                     ofObject:observable
                     selector:@selector(broadcastContentScrollOffset:)];

  observable.observableCGFloat = 2.0;

  TestObserver* observer = [[TestObserver alloc] init];
  [broadcaster addObserver:observer
               forSelector:@selector(broadcastContentScrollOffset:)];
  EXPECT_EQ(2.0, observer.lastObservedCGFloat);
  EXPECT_EQ(1, observer.contentScrollOffsetCallCount);
  observable.observableCGFloat = 3.0;
  EXPECT_EQ(3.0, observer.lastObservedCGFloat);
  EXPECT_EQ(2, observer.contentScrollOffsetCallCount);
  [broadcaster
      stopBroadcastingForSelector:@selector(broadcastContentScrollOffset:)];
  observable.observableCGFloat = 4.0;
  EXPECT_EQ(3.0, observer.lastObservedCGFloat);
  EXPECT_EQ(2, observer.contentScrollOffsetCallCount);
}

TEST_F(ChromeBroadcasterTest, TestStopObserving) {
  ChromeBroadcaster* broadcaster = [[ChromeBroadcaster alloc] init];
  TestObservable* observable = [[TestObservable alloc] init];
  observable.observableCGFloat = 1.0;

  [broadcaster broadcastValue:@"observableBool"
                     ofObject:observable
                     selector:@selector(broadcastScrollViewIsScrolling:)];
  [broadcaster broadcastValue:@"observableCGFloat"
                     ofObject:observable
                     selector:@selector(broadcastContentScrollOffset:)];

  observable.observableCGFloat = 2.0;
  observable.observableBool = YES;
  TestObserver* observer = [[TestObserver alloc] init];

  [broadcaster addObserver:observer
               forSelector:@selector(broadcastScrollViewIsScrolling:)];
  [broadcaster addObserver:observer
               forSelector:@selector(broadcastContentScrollOffset:)];
  EXPECT_EQ(2.0, observer.lastObservedCGFloat);
  EXPECT_EQ(1, observer.contentScrollOffsetCallCount);
  EXPECT_TRUE(observer.lastObservedBool);
  EXPECT_EQ(1, observer.tabStripVisibleCallCount);
  observable.observableCGFloat = 3.0;
  EXPECT_EQ(3.0, observer.lastObservedCGFloat);
  EXPECT_EQ(2, observer.contentScrollOffsetCallCount);
  [broadcaster removeObserver:observer
                  forSelector:@selector(broadcastContentScrollOffset:)];
  observable.observableCGFloat = 4.0;
  EXPECT_EQ(3.0, observer.lastObservedCGFloat);
  EXPECT_EQ(2, observer.contentScrollOffsetCallCount);
  observable.observableBool = NO;
  EXPECT_FALSE(observer.lastObservedBool);
  EXPECT_EQ(2, observer.tabStripVisibleCallCount);
  [broadcaster removeObserver:observer
                  forSelector:@selector(broadcastScrollViewIsScrolling:)];
  observable.observableBool = YES;
  EXPECT_FALSE(observer.lastObservedBool);
  EXPECT_EQ(2, observer.tabStripVisibleCallCount);
}
