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

#import "base/ios/crb_protocol_observers.h"

#include "base/ios/weak_nsobject.h"
#include "base/logging.h"
#include "base/mac/scoped_nsobject.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.h"
#include "testing/platform_test.h"

@protocol TestObserver

@required
- (void)requiredMethod;
- (void)reset;

@optional
- (void)optionalMethod;
- (void)mutateByAddingObserver:(id<TestObserver>)observer;
- (void)mutateByRemovingObserver:(id<TestObserver>)observer;
- (void)nestedMutateByAddingObserver:(id<TestObserver>)observer;
- (void)nestedMutateByRemovingObserver:(id<TestObserver>)observer;

@end

// Implements only the required methods in the TestObserver protocol.
@interface TestPartialObserver : NSObject<TestObserver>
@property(nonatomic, readonly) BOOL requiredMethodInvoked;
@end

// Implements all the methods in the TestObserver protocol.
@interface TestCompleteObserver : TestPartialObserver<TestObserver>
@property(nonatomic, readonly) BOOL optionalMethodInvoked;
@end

@interface TestMutateObserver : TestCompleteObserver
- (instancetype)initWithObserver:(CRBProtocolObservers*)observer
    NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
@end

namespace {

class CRBProtocolObserversTest : public PlatformTest {
 public:
  CRBProtocolObserversTest() {}

 protected:
  void SetUp() override {
    PlatformTest::SetUp();

    observers_.reset([[CRBProtocolObservers observersWithProtocol:
        @protocol(TestObserver)] retain]);

    partial_observer_.reset([[TestPartialObserver alloc] init]);
    EXPECT_FALSE([partial_observer_ requiredMethodInvoked]);

    complete_observer_.reset([[TestCompleteObserver alloc] init]);
    EXPECT_FALSE([complete_observer_ requiredMethodInvoked]);
    EXPECT_FALSE([complete_observer_ optionalMethodInvoked]);

    mutate_observer_.reset(
        [[TestMutateObserver alloc] initWithObserver:observers_.get()]);
    EXPECT_FALSE([mutate_observer_ requiredMethodInvoked]);
  }

  base::scoped_nsobject<id> observers_;
  base::scoped_nsobject<TestPartialObserver> partial_observer_;
  base::scoped_nsobject<TestCompleteObserver> complete_observer_;
  base::scoped_nsobject<TestMutateObserver> mutate_observer_;
};

// Verifies basic functionality of -[CRBProtocolObservers addObserver:] and
// -[CRBProtocolObservers removeObserver:].
TEST_F(CRBProtocolObserversTest, AddRemoveObserver) {
  // Add an observer and verify that the CRBProtocolObservers instance forwards
  // an invocation to it.
  [observers_ addObserver:partial_observer_];
  [observers_ requiredMethod];
  EXPECT_TRUE([partial_observer_ requiredMethodInvoked]);

  [partial_observer_ reset];
  EXPECT_FALSE([partial_observer_ requiredMethodInvoked]);

  // Remove the observer and verify that the CRBProtocolObservers instance no
  // longer forwards an invocation to it.
  [observers_ removeObserver:partial_observer_];
  [observers_ requiredMethod];
  EXPECT_FALSE([partial_observer_ requiredMethodInvoked]);
}

// Verifies that CRBProtocolObservers correctly forwards the invocation of a
// required method in the protocol.
TEST_F(CRBProtocolObserversTest, RequiredMethods) {
  [observers_ addObserver:partial_observer_];
  [observers_ addObserver:complete_observer_];
  [observers_ requiredMethod];
  EXPECT_TRUE([partial_observer_ requiredMethodInvoked]);
  EXPECT_TRUE([complete_observer_ requiredMethodInvoked]);
}

// Verifies that CRBProtocolObservers correctly forwards the invocation of an
// optional method in the protocol.
TEST_F(CRBProtocolObserversTest, OptionalMethods) {
  [observers_ addObserver:partial_observer_];
  [observers_ addObserver:complete_observer_];
  [observers_ optionalMethod];
  EXPECT_FALSE([partial_observer_ requiredMethodInvoked]);
  EXPECT_FALSE([complete_observer_ requiredMethodInvoked]);
  EXPECT_TRUE([complete_observer_ optionalMethodInvoked]);
}

// Verifies that CRBProtocolObservers only holds a weak reference to an
// observer.
TEST_F(CRBProtocolObserversTest, WeakReference) {
  base::WeakNSObject<TestPartialObserver> weak_observer(
      partial_observer_);
  EXPECT_TRUE(weak_observer);

  [observers_ addObserver:partial_observer_];

  // Need an autorelease pool here, because
  // -[CRBProtocolObservers forwardInvocation:] creates a temporary
  // autoreleased array that holds all the observers.
  @autoreleasepool {
    [observers_ requiredMethod];
    EXPECT_TRUE([partial_observer_ requiredMethodInvoked]);
  }

  partial_observer_.reset();
  EXPECT_FALSE(weak_observer.get());
}

// Verifies that an observer can safely remove itself as observer while being
// notified.
TEST_F(CRBProtocolObserversTest, SelfMutateObservers) {
  [observers_ addObserver:mutate_observer_];
  EXPECT_FALSE([observers_ empty]);

  [observers_ requiredMethod];
  EXPECT_TRUE([mutate_observer_ requiredMethodInvoked]);

  [mutate_observer_ reset];

  [observers_ nestedMutateByRemovingObserver:mutate_observer_];
  EXPECT_FALSE([mutate_observer_ requiredMethodInvoked]);

  [observers_ addObserver:partial_observer_];

  [observers_ requiredMethod];
  EXPECT_FALSE([mutate_observer_ requiredMethodInvoked]);
  EXPECT_TRUE([partial_observer_ requiredMethodInvoked]);

  [observers_ removeObserver:partial_observer_];
  EXPECT_TRUE([observers_ empty]);
}

// Verifies that - [CRBProtocolObservers addObserver:] and
// - [CRBProtocolObservers removeObserver:] can be called while methods are
// being forwarded.
TEST_F(CRBProtocolObserversTest, MutateObservers) {
  // Indirectly add an observer while forwarding an observer method.
  [observers_ addObserver:mutate_observer_];

  [observers_ mutateByAddingObserver:partial_observer_];
  EXPECT_FALSE([partial_observer_ requiredMethodInvoked]);

  // Check that methods are correctly forwared to the indirectly added observer.
  [mutate_observer_ reset];
  [observers_ requiredMethod];
  EXPECT_TRUE([mutate_observer_ requiredMethodInvoked]);
  EXPECT_TRUE([partial_observer_ requiredMethodInvoked]);

  [mutate_observer_ reset];
  [partial_observer_ reset];

  // Indirectly remove an observer while forwarding an observer method.
  [observers_ mutateByRemovingObserver:partial_observer_];

  // Check that method is not forwared to the indirectly removed observer.
  [observers_ requiredMethod];
  EXPECT_TRUE([mutate_observer_ requiredMethodInvoked]);
  EXPECT_FALSE([partial_observer_ requiredMethodInvoked]);
}

// Verifies that - [CRBProtocolObservers addObserver:] and
// - [CRBProtocolObservers removeObserver:] can be called while methods are
// being forwarded with a nested invocation depth > 0.
TEST_F(CRBProtocolObserversTest, NestedMutateObservers) {
  // Indirectly add an observer while forwarding an observer method.
  [observers_ addObserver:mutate_observer_];

  [observers_ nestedMutateByAddingObserver:partial_observer_];
  EXPECT_FALSE([partial_observer_ requiredMethodInvoked]);

  // Check that methods are correctly forwared to the indirectly added observer.
  [mutate_observer_ reset];
  [observers_ requiredMethod];
  EXPECT_TRUE([mutate_observer_ requiredMethodInvoked]);
  EXPECT_TRUE([partial_observer_ requiredMethodInvoked]);

  [mutate_observer_ reset];
  [partial_observer_ reset];

  // Indirectly remove an observer while forwarding an observer method.
  [observers_ nestedMutateByRemovingObserver:partial_observer_];

  // Check that method is not forwared to the indirectly removed observer.
  [observers_ requiredMethod];
  EXPECT_TRUE([mutate_observer_ requiredMethodInvoked]);
  EXPECT_FALSE([partial_observer_ requiredMethodInvoked]);
}

}  // namespace

@implementation TestPartialObserver {
  BOOL _requiredMethodInvoked;
}

- (BOOL)requiredMethodInvoked {
  return _requiredMethodInvoked;
}

- (void)requiredMethod {
  _requiredMethodInvoked = YES;
}

- (void)reset {
  _requiredMethodInvoked = NO;
}

@end

@implementation TestCompleteObserver {
  BOOL _optionalMethodInvoked;
}

- (BOOL)optionalMethodInvoked {
  return _optionalMethodInvoked;
}

- (void)optionalMethod {
  _optionalMethodInvoked = YES;
}

- (void)reset {
  [super reset];
  _optionalMethodInvoked = NO;
}

@end

@implementation TestMutateObserver {
  id _observers;  // weak
}

- (instancetype)initWithObserver:(CRBProtocolObservers*)observers {
  self = [super init];
  if (self) {
    _observers = observers;
  }
  return self;
}

- (instancetype)init {
  NOTREACHED();
  return nil;
}

- (void)mutateByAddingObserver:(id<TestObserver>)observer {
  [_observers addObserver:observer];
}

- (void)mutateByRemovingObserver:(id<TestObserver>)observer {
  [_observers removeObserver:observer];
}

- (void)nestedMutateByAddingObserver:(id<TestObserver>)observer {
  [_observers mutateByAddingObserver:observer];
}

- (void)nestedMutateByRemovingObserver:(id<TestObserver>)observer {
  [_observers mutateByRemovingObserver:observer];
}

@end
