// Copyright 2017 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.

#include "ui/base/cocoa/bubble_closer.h"

#include "base/bind.h"
#import "ui/base/test/cocoa_helper.h"
#import "ui/base/test/menu_test_observer.h"
#import "ui/events/test/cocoa_test_event_utils.h"

namespace ui {

class BubbleCloserTest : public CocoaTest {
 public:
  enum Button { LEFT, RIGHT };
  enum InOrOut { INSIDE, OUTSIDE };

  BubbleCloserTest() = default;

  void SetUp() override {
    CocoaTest::SetUp();
    bubble_window_.reset([[NSWindow alloc]
        initWithContentRect:NSMakeRect(100, 100, 320, 200)
                  styleMask:NSBorderlessWindowMask
                    backing:NSBackingStoreBuffered
                      defer:NO]);
    [bubble_window_ setReleasedWhenClosed:NO];  // Owned by scoped_nsobject.
    [bubble_window_ makeKeyAndOrderFront:nil];
    bubble_closer_ = std::make_unique<BubbleCloser>(
        bubble_window_,
        base::BindRepeating([](int* i) { *i += 1; }, &click_outside_count_));
  }

  void TearDown() override {
    [bubble_window_ close];
    bubble_closer_ = nullptr;
    bubble_window_.reset();
    CocoaTest::TearDown();
  }

  void SendClick(Button left_or_right, InOrOut in_or_out) {
    NSWindow* window = in_or_out == INSIDE ? bubble_window_ : test_window();
    NSEvent* event =
        left_or_right == LEFT
            ? cocoa_test_event_utils::LeftMouseDownAtPointInWindow(
                  NSMakePoint(10, 10), window)
            : cocoa_test_event_utils::RightMouseDownAtPointInWindow(
                  NSMakePoint(10, 10), window);
    [NSApp sendEvent:event];
  }

  void ResetCloser() { bubble_closer_ = nullptr; }

 protected:
  int click_outside_count() const { return click_outside_count_; }
  NSWindow* bubble_window() { return bubble_window_; }
  bool IsCloserReset() const { return !bubble_closer_; }

 private:
  base::scoped_nsobject<NSWindow> bubble_window_;
  std::unique_ptr<BubbleCloser> bubble_closer_;
  int click_outside_count_ = 0;

  DISALLOW_COPY_AND_ASSIGN(BubbleCloserTest);
};

// Test for lifetime issues around NSEvent monitors.
TEST_F(BubbleCloserTest, SecondBubbleCloser) {
  auto resetter = [](BubbleCloserTest* me) { me->ResetCloser(); };
  auto deleter = std::make_unique<BubbleCloser>(
      bubble_window(), base::BindRepeating(resetter, this));
  SendClick(LEFT, OUTSIDE);

  // The order is non-deterministic, so click_outside_count() may not change.
  // But either way, the closer should have been reset.
  EXPECT_TRUE(IsCloserReset());
}

// Test that clicking outside the window fires the callback and clicking inside
// does not.
TEST_F(BubbleCloserTest, ClickInsideAndOut) {
  EXPECT_EQ(0, click_outside_count());
  SendClick(LEFT, OUTSIDE);
  EXPECT_EQ(1, click_outside_count());
  SendClick(RIGHT, OUTSIDE);
  EXPECT_EQ(2, click_outside_count());
  SendClick(LEFT, INSIDE);
  EXPECT_EQ(2, click_outside_count());
  SendClick(RIGHT, INSIDE);
  EXPECT_EQ(2, click_outside_count());
}

// Test that right-clicking the window to display a context menu works.
TEST_F(BubbleCloserTest, RightClickOutsideClosesWithContextMenu) {
  base::scoped_nsobject<NSMenu> context_menu(
      [[NSMenu alloc] initWithTitle:@""]);
  [context_menu addItemWithTitle:@"ContextMenuTest"
                          action:nil
                   keyEquivalent:@""];

  // Set the menu as the contextual menu of contentView of test_window().
  [[test_window() contentView] setMenu:context_menu];

  base::scoped_nsobject<MenuTestObserver> menu_observer(
      [[MenuTestObserver alloc] initWithMenu:context_menu]);
  [menu_observer setCloseAfterOpening:YES];
  [menu_observer setOpenCallback:^(MenuTestObserver* observer) {
    // Verify click is seen when contextual menu is open.
    EXPECT_TRUE([observer isOpen]);
    EXPECT_EQ(1, click_outside_count());
  }];

  EXPECT_FALSE([menu_observer isOpen]);
  EXPECT_FALSE([menu_observer didOpen]);

  EXPECT_EQ(0, click_outside_count());

  // RightMouseDown in test_window() would close the bubble window and then
  // display the contextual menu.
  SendClick(RIGHT, OUTSIDE);

  // When we got here, menu has already run its RunLoop.
  EXPECT_EQ(1, click_outside_count());

  EXPECT_FALSE([menu_observer isOpen]);
  EXPECT_TRUE([menu_observer didOpen]);
}

}  // namespace ui
