// Copyright (c) 2011 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 <Cocoa/Cocoa.h>

#import "chrome/browser/ui/cocoa/toolbar/reload_button.h"

#include "base/mac/scoped_nsobject.h"
#include "chrome/app/chrome_command_ids.h"
#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
#import "chrome/browser/ui/cocoa/image_button_cell.h"
#import "testing/gtest_mac.h"
#include "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "ui/events/test/cocoa_test_event_utils.h"

@interface ReloadButton (Testing)
+ (void)setPendingReloadTimeout:(NSTimeInterval)seconds;
@end

@protocol TargetActionMock <NSObject>
- (void)anAction:(id)sender;
@end

namespace {

class ReloadButtonTest : public CocoaTest {
 public:
  ReloadButtonTest() {
    NSRect frame = NSMakeRect(0, 0, 20, 20);
    base::scoped_nsobject<ReloadButton> button(
        [[ReloadButton alloc] initWithFrame:frame]);
    button_ = button.get();

    // Set things up so unit tests have a reliable baseline.
    [button_ setTag:IDC_RELOAD];
    [button_ awakeFromNib];

    [[test_window() contentView] addSubview:button_];
  }

  bool IsMouseInside() {
    return [[button_ cell] isMouseInside];
  }

  void MouseEnter() {
    [[button_ cell] mouseEntered:nil];
  }

  void MouseExit() {
    [[button_ cell] mouseExited:nil];
  }

  ReloadButton* button_;
};

TEST_VIEW(ReloadButtonTest, button_)

// Test that mouse-tracking is setup and does the right thing.
TEST_F(ReloadButtonTest, IsMouseInside) {
  EXPECT_FALSE(IsMouseInside());
  MouseEnter();
  EXPECT_TRUE(IsMouseInside());
  MouseExit();
}

// Verify that multiple clicks do not result in multiple messages to
// the target.
TEST_F(ReloadButtonTest, IgnoredMultiClick) {
  id mock_target = [OCMockObject mockForProtocol:@protocol(TargetActionMock)];
  [button_ setTarget:mock_target];
  [button_ setAction:@selector(anAction:)];

  // Expect the action once.
  [[mock_target expect] anAction:button_];

  const std::pair<NSEvent*,NSEvent*> click_one =
      cocoa_test_event_utils::MouseClickInView(button_, 1);
  const std::pair<NSEvent*,NSEvent*> click_two =
      cocoa_test_event_utils::MouseClickInView(button_, 2);
  [NSApp postEvent:click_one.second atStart:YES];
  [button_ mouseDown:click_one.first];
  [NSApp postEvent:click_two.second atStart:YES];
  [button_ mouseDown:click_two.first];

  [button_ setTarget:nil];
}

TEST_F(ReloadButtonTest, UpdateTag) {
  [button_ setTag:IDC_STOP];

  [button_ updateTag:IDC_RELOAD];
  EXPECT_EQ(IDC_RELOAD, [button_ tag]);
  NSString* const reloadToolTip = [button_ toolTip];

  [button_ updateTag:IDC_STOP];
  EXPECT_EQ(IDC_STOP, [button_ tag]);
  NSString* const stopToolTip = [button_ toolTip];
  EXPECT_NSNE(reloadToolTip, stopToolTip);

  [button_ updateTag:IDC_RELOAD];
  EXPECT_EQ(IDC_RELOAD, [button_ tag]);
  EXPECT_NSEQ(reloadToolTip, [button_ toolTip]);
}

// Test that when forcing the mode, it takes effect immediately,
// regardless of whether the mouse is hovering.
TEST_F(ReloadButtonTest, SetIsLoadingForce) {
  EXPECT_FALSE(IsMouseInside());
  EXPECT_EQ(IDC_RELOAD, [button_ tag]);

  // Changes to stop immediately.
  [button_ setIsLoading:YES force:YES];
  EXPECT_EQ(IDC_STOP, [button_ tag]);

  // Changes to reload immediately.
  [button_ setIsLoading:NO force:YES];
  EXPECT_EQ(IDC_RELOAD, [button_ tag]);

  // Changes to stop immediately when the mouse is hovered, and
  // doesn't change when the mouse exits.
  MouseEnter();
  EXPECT_TRUE(IsMouseInside());
  [button_ setIsLoading:YES force:YES];
  EXPECT_EQ(IDC_STOP, [button_ tag]);
  MouseExit();
  EXPECT_FALSE(IsMouseInside());
  EXPECT_EQ(IDC_STOP, [button_ tag]);

  // Changes to reload immediately when the mouse is hovered, and
  // doesn't change when the mouse exits.
  MouseEnter();
  EXPECT_TRUE(IsMouseInside());
  [button_ setIsLoading:NO force:YES];
  EXPECT_EQ(IDC_RELOAD, [button_ tag]);
  MouseExit();
  EXPECT_FALSE(IsMouseInside());
  EXPECT_EQ(IDC_RELOAD, [button_ tag]);
}

// Test that without force, stop mode is set immediately, but reload
// is affected by the hover status.
TEST_F(ReloadButtonTest, SetIsLoadingNoForceUnHover) {
  EXPECT_FALSE(IsMouseInside());
  EXPECT_EQ(IDC_RELOAD, [button_ tag]);

  // Changes to stop immediately when the mouse is not hovering.
  [button_ setIsLoading:YES force:NO];
  EXPECT_EQ(IDC_STOP, [button_ tag]);

  // Changes to reload immediately when the mouse is not hovering.
  [button_ setIsLoading:NO force:NO];
  EXPECT_EQ(IDC_RELOAD, [button_ tag]);

  // Changes to stop immediately when the mouse is hovered, and
  // doesn't change when the mouse exits.
  MouseEnter();
  EXPECT_TRUE(IsMouseInside());
  [button_ setIsLoading:YES force:NO];
  EXPECT_EQ(IDC_STOP, [button_ tag]);
  MouseExit();
  EXPECT_FALSE(IsMouseInside());
  EXPECT_EQ(IDC_STOP, [button_ tag]);

  // Does not change to reload immediately when the mouse is hovered,
  // changes when the mouse exits.
  MouseEnter();
  EXPECT_TRUE(IsMouseInside());
  [button_ setIsLoading:NO force:NO];
  EXPECT_EQ(IDC_STOP, [button_ tag]);
  MouseExit();
  EXPECT_FALSE(IsMouseInside());
  EXPECT_EQ(IDC_RELOAD, [button_ tag]);
}

// Test that without force, stop mode is set immediately, and reload
// will be set after a timeout.
// TODO(shess): Reenable, http://crbug.com/61485
TEST_F(ReloadButtonTest, DISABLED_SetIsLoadingNoForceTimeout) {
  // When the event loop first spins, some delayed tracking-area setup
  // is done, which causes -mouseExited: to be called.  Spin it at
  // least once, and dequeue any pending events.
  // TODO(shess): It would be more reasonable to have an MockNSTimer
  // factory for the class to use, which this code could fire
  // directly.
  while ([NSApp nextEventMatchingMask:NSAnyEventMask
                            untilDate:nil
                               inMode:NSDefaultRunLoopMode
                              dequeue:YES]) {
  }

  const NSTimeInterval kShortTimeout = 0.1;
  [ReloadButton setPendingReloadTimeout:kShortTimeout];

  EXPECT_FALSE(IsMouseInside());
  EXPECT_EQ(IDC_RELOAD, [button_ tag]);

  // Move the mouse into the button and press it.
  MouseEnter();
  EXPECT_TRUE(IsMouseInside());
  [button_ setIsLoading:YES force:NO];
  EXPECT_EQ(IDC_STOP, [button_ tag]);

  // Does not change to reload immediately when the mouse is hovered.
  EXPECT_TRUE(IsMouseInside());
  [button_ setIsLoading:NO force:NO];
  EXPECT_TRUE(IsMouseInside());
  EXPECT_EQ(IDC_STOP, [button_ tag]);
  EXPECT_TRUE(IsMouseInside());

  // Spin event loop until the timeout passes.
  NSDate* pastTimeout = [NSDate dateWithTimeIntervalSinceNow:2 * kShortTimeout];
  [NSApp nextEventMatchingMask:NSAnyEventMask
                     untilDate:pastTimeout
                        inMode:NSDefaultRunLoopMode
                       dequeue:NO];

  // Mouse is still hovered, button is in reload mode.  If the mouse
  // is no longer hovered, see comment at top of function.
  EXPECT_TRUE(IsMouseInside());
  EXPECT_EQ(IDC_RELOAD, [button_ tag]);
}

// Test that pressing stop after reload mode has been requested
// doesn't forward the stop message.
TEST_F(ReloadButtonTest, StopAfterReloadSet) {
  id mock_target = [OCMockObject mockForProtocol:@protocol(TargetActionMock)];
  [button_ setTarget:mock_target];
  [button_ setAction:@selector(anAction:)];

  EXPECT_FALSE(IsMouseInside());

  // Get to stop mode.
  [button_ setIsLoading:YES force:YES];
  EXPECT_EQ(IDC_STOP, [button_ tag]);
  EXPECT_TRUE([button_ isEnabled]);

  // Expect the action once.
  [[mock_target expect] anAction:button_];

  // Clicking in stop mode should send the action and transition to
  // reload mode.
  const std::pair<NSEvent*,NSEvent*> click =
      cocoa_test_event_utils::MouseClickInView(button_, 1);
  [NSApp postEvent:click.second atStart:YES];
  [button_ mouseDown:click.first];
  EXPECT_EQ(IDC_RELOAD, [button_ tag]);
  EXPECT_TRUE([button_ isEnabled]);

  // Get back to stop mode.
  [button_ setIsLoading:YES force:YES];
  EXPECT_EQ(IDC_STOP, [button_ tag]);
  EXPECT_TRUE([button_ isEnabled]);

  // If hover prevented reload mode immediately taking effect, clicks should do
  // nothing, because the button should be disabled.
  MouseEnter();
  EXPECT_TRUE(IsMouseInside());
  [button_ setIsLoading:NO force:NO];
  EXPECT_EQ(IDC_STOP, [button_ tag]);
  EXPECT_FALSE([button_ isEnabled]);
  [NSApp postEvent:click.second atStart:YES];
  [button_ mouseDown:click.first];
  EXPECT_EQ(IDC_STOP, [button_ tag]);

  [button_ setTarget:nil];
}

}  // namespace
