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

#include "chrome/browser/ui/views/toolbar/toolbar_action_view.h"

#include <memory>
#include <string>

#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/sessions/session_tab_helper_factory.h"
#include "chrome/browser/ui/toolbar/test_toolbar_action_view_model.h"
#include "chrome/browser/ui/toolbar/toolbar_action_view_model.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/views/chrome_views_test_base.h"
#include "content/public/test/test_web_contents_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/mojom/menu_source_type.mojom-forward.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/test/event_generator.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/menu_button.h"

namespace {

// A test delegate for a toolbar action view.
class TestToolbarActionViewDelegate : public ToolbarActionView::Delegate {
 public:
  TestToolbarActionViewDelegate()
      : overflow_reference_view_(std::make_unique<views::MenuButton>()),
        web_contents_(nullptr) {}
  TestToolbarActionViewDelegate(const TestToolbarActionViewDelegate&) = delete;
  TestToolbarActionViewDelegate& operator=(
      const TestToolbarActionViewDelegate&) = delete;
  ~TestToolbarActionViewDelegate() override = default;

  // ToolbarActionView::Delegate:
  content::WebContents* GetCurrentWebContents() override {
    return web_contents_;
  }
  views::MenuButton* GetOverflowReferenceView() const override {
    return overflow_reference_view_.get();
  }
  gfx::Size GetToolbarActionSize() override { return gfx::Size(32, 32); }
  MOCK_METHOD(void,
              MovePinnedActionBy,
              (const std::string& action_id, int move_by),
              (override));
  void UpdateHoverCard(ToolbarActionView* action_view,
                       ToolbarActionHoverCardUpdateType update_type) override {}
  bool IsFocusOnExtensionAction() const override { return false; }
  void OnContextMenuShown(const std::string& action_id) override {}
  void OnContextMenuClosed(const std::string& action_id) override {}
  void WriteDragDataForView(views::View* sender,
                            const gfx::Point& press_pt,
                            ui::OSExchangeData* data) override {}
  int GetDragOperationsForView(views::View* sender,
                               const gfx::Point& p) override {
    return ui::DragDropTypes::DRAG_NONE;
  }
  bool CanStartDragForView(views::View* sender,
                           const gfx::Point& press_pt,
                           const gfx::Point& p) override {
    return false;
  }

  void set_web_contents(content::WebContents* web_contents) {
    web_contents_ = web_contents;
  }

 private:
  std::unique_ptr<views::MenuButton> overflow_reference_view_;

  raw_ptr<content::WebContents> web_contents_;
};

class OpenMenuListener : public views::ContextMenuController {
 public:
  explicit OpenMenuListener(views::View* view) : view_(view) {
    view_->set_context_menu_controller(this);
  }
  OpenMenuListener(const OpenMenuListener&) = delete;
  OpenMenuListener& operator=(const OpenMenuListener&) = delete;
  ~OpenMenuListener() override { view_->set_context_menu_controller(nullptr); }

  void ShowContextMenuForViewImpl(
      views::View* source,
      const gfx::Point& point,
      ui::mojom::MenuSourceType source_type) override {
    opened_menu_ = true;
  }

  bool opened_menu() const { return opened_menu_; }

 private:
  raw_ptr<views::View> view_;

  bool opened_menu_ = false;
};

}  // namespace

class ToolbarActionViewUnitTest : public ChromeViewsTestBase {
 public:
  ToolbarActionViewUnitTest() : widget_(nullptr) {}
  ToolbarActionViewUnitTest(const ToolbarActionViewUnitTest&) = delete;
  ToolbarActionViewUnitTest& operator=(const ToolbarActionViewUnitTest&) =
      delete;
  ~ToolbarActionViewUnitTest() override = default;

  void SetUp() override {
    ChromeViewsTestBase::SetUp();
    view_model_ = std::make_unique<TestToolbarActionViewModel>("fake model");
    action_view_delegate_ =
        std::make_unique<testing::NiceMock<TestToolbarActionViewDelegate>>();
    widget_ =
        CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
  }

  void TearDown() override {
    widget_.reset();
    ChromeViewsTestBase::TearDown();
  }

  void Reset() {
    widget_.reset();
    action_view_delegate_.reset();
    view_model_.reset();
  }

  views::Widget* widget() { return widget_.get(); }

  TestToolbarActionViewModel* view_model() { return view_model_.get(); }

  TestToolbarActionViewDelegate* action_view_delegate() {
    return action_view_delegate_.get();
  }
  testing::NiceMock<TestToolbarActionViewDelegate>&
  mock_action_view_delegate() {
    return *action_view_delegate_;
  }

 private:
  std::unique_ptr<TestToolbarActionViewModel> view_model_;
  std::unique_ptr<testing::NiceMock<TestToolbarActionViewDelegate>>
      action_view_delegate_;

  // The widget managed by this test.
  std::unique_ptr<views::Widget> widget_;
};

// A MenuButton subclass that provides access to some MenuButton internals.
class TestToolbarActionView : public ToolbarActionView {
 public:
  TestToolbarActionView(ToolbarActionViewModel* model, Delegate* delegate)
      : ToolbarActionView(model, delegate) {}
  TestToolbarActionView(const TestToolbarActionView&) = delete;
  TestToolbarActionView& operator=(const TestToolbarActionView&) = delete;
  ~TestToolbarActionView() override = default;
};

// Verifies there is no crash when a ToolbarActionView with an InkDrop is
// destroyed while holding a |pressed_lock_|.
TEST_F(ToolbarActionViewUnitTest,
       NoCrashWhenDestroyingToolbarActionViewThatHasAPressedLock) {
  TestToolbarActionViewModel* model = view_model();

  // Create a new toolbar action view.
  auto view =
      std::make_unique<ToolbarActionView>(model, action_view_delegate());
  view->SetBoundsRect(gfx::Rect(0, 0, 200, 20));
  widget()->SetContentsView(std::move(view));
  widget()->Show();

  model->ShowPopup(true);
}

// Verifies the InkDropAnimation used by the ToolbarActionView doesn't fail a
// DCHECK for an unsupported transition from ACTIVATED to ACTION_PENDING.
TEST_F(ToolbarActionViewUnitTest,
       NoCrashWhenPressingMouseOnToolbarActionViewThatHasAPressedLock) {
  TestToolbarActionViewModel* model = view_model();

  // Create a new toolbar action view.
  auto view =
      std::make_unique<ToolbarActionView>(model, action_view_delegate());
  view->SetBoundsRect(gfx::Rect(0, 0, 200, 20));
  widget()->SetContentsView(std::move(view));
  widget()->Show();

  ui::test::EventGenerator generator(GetContext(), widget()->GetNativeWindow());

  model->ShowPopup(true);
  generator.PressLeftButton();
}

// Test the basic ui of a ToolbarActionView and that it responds correctly to
// a controller's state.
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || \
    BUILDFLAG(IS_WIN)
// TODO(crbug.com/40668368): Test is flaky on Mac, Linux and Win10.
#define MAYBE_BasicToolbarActionViewTest DISABLED_BasicToolbarActionViewTest
#else
#define MAYBE_BasicToolbarActionViewTest BasicToolbarActionViewTest
#endif
TEST_F(ToolbarActionViewUnitTest, MAYBE_BasicToolbarActionViewTest) {
  TestingProfile profile;

  // ViewsTestBase initializes the aura environment, so the factory shouldn't.
  content::TestWebContentsFactory web_contents_factory;

  TestToolbarActionViewModel* model = view_model();
  TestToolbarActionViewDelegate* view_delegate = action_view_delegate();

  // Configure the test controller and delegate.
  std::u16string name = u"name";
  model->SetAccessibleName(name);
  std::u16string tooltip = u"tooltip";
  model->SetTooltip(tooltip);
  content::WebContents* web_contents =
      web_contents_factory.CreateWebContents(&profile);
  CreateSessionServiceTabHelper(web_contents);
  view_delegate->set_web_contents(web_contents);

  // Move the mouse off the not-yet-existent button.
  ui::test::EventGenerator generator(GetContext(), widget()->GetNativeWindow());
  generator.MoveMouseTo(gfx::Point(300, 300));

  // Create a new toolbar action view.
  auto owning_view = std::make_unique<ToolbarActionView>(model, view_delegate);
  owning_view->SetBoundsRect(gfx::Rect(0, 0, 200, 20));
  ToolbarActionView* view = owning_view.get();
  widget()->SetContentsView(std::move(owning_view));
  widget()->Show();

  // Check that the tooltip and accessible state of the view match the
  // controller's.
  EXPECT_EQ(tooltip, view->GetRenderedTooltipText(gfx::Point()));
  ui::AXNodeData ax_node_data;
  view->GetViewAccessibility().GetAccessibleNodeData(&ax_node_data);
  EXPECT_EQ(name, ax_node_data.GetString16Attribute(
                      ax::mojom::StringAttribute::kName));

  // The button should start in normal state, with no actions executed.
  EXPECT_EQ(views::Button::STATE_NORMAL, view->GetState());
  EXPECT_EQ(0, model->execute_action_count());

  // Click the button. This should execute it.
  generator.MoveMouseTo(gfx::Point(10, 10));
  generator.ClickLeftButton();
  EXPECT_EQ(1, model->execute_action_count());

  // Move the mouse off the button, and show a popup through a non-user action.
  // Since this was not a user action, the button should not be pressed.
  generator.MoveMouseTo(gfx::Point(300, 300));
  model->ShowPopup(false);
  EXPECT_EQ(views::Button::STATE_NORMAL, view->GetState());
  model->HidePopup();

  // Show the popup through a user action - the button should be pressed.
  model->ShowPopup(true);
  EXPECT_EQ(views::Button::STATE_PRESSED, view->GetState());
  model->HidePopup();
  EXPECT_EQ(views::Button::STATE_NORMAL, view->GetState());

  // Ensure that clicking on an otherwise-disabled action opens the
  // context menu.
  model->SetEnabled(false);
  // Even though the controller is disabled, the button remains enabled
  // because it will open the context menu.
  EXPECT_EQ(views::Button::STATE_NORMAL, view->GetState());
  int old_execute_action_count = model->execute_action_count();
  {
    OpenMenuListener menu_listener(view);
    view->Activate(nullptr);
    EXPECT_TRUE(menu_listener.opened_menu());
    EXPECT_EQ(old_execute_action_count, model->execute_action_count());
  }

  // Create an overflow button.
  views::MenuButton* overflow_button =
      view_delegate->GetOverflowReferenceView();

  // If the view isn't visible, the overflow button should be pressed for
  // popups.
  view->SetVisible(false);
  model->ShowPopup(true);
  EXPECT_EQ(views::Button::STATE_NORMAL, view->GetState());
  EXPECT_EQ(views::Button::STATE_PRESSED, overflow_button->GetState());
  model->HidePopup();
  EXPECT_EQ(views::Button::STATE_NORMAL, view->GetState());
  EXPECT_EQ(views::Button::STATE_NORMAL, overflow_button->GetState());
}

// Verifies that pressing [Ctrl|Cmd] + Left / Right triggers a call to move the
// toolbar action.
TEST_F(ToolbarActionViewUnitTest, TestKeyboardReordering) {
  TestToolbarActionViewModel* model = view_model();

  auto owned_view =
      std::make_unique<ToolbarActionView>(model, action_view_delegate());
  auto* view = owned_view.get();
  view->SetBoundsRect(gfx::Rect(0, 0, 200, 20));
  widget()->SetContentsView(std::move(owned_view));
  widget()->Show();

  auto send_key_press = [view](ui::KeyboardCode code, int flags) {
    view->OnKeyPressed(ui::KeyEvent(ui::EventType::kKeyPressed, code, flags,
                                    ui::EventTimeForNow()));
    view->OnKeyReleased(ui::KeyEvent(ui::EventType::kKeyPressed, code, flags,
                                     ui::EventTimeForNow()));
  };

  // Start by pressing just left / right. No modifier is present, so this should
  // do nothing.
  EXPECT_CALL(mock_action_view_delegate(), MovePinnedActionBy).Times(0);
  send_key_press(ui::VKEY_RIGHT, ui::EF_NONE);
  testing::Mock::VerifyAndClearExpectations(&mock_action_view_delegate());

  EXPECT_CALL(mock_action_view_delegate(), MovePinnedActionBy).Times(0);
  send_key_press(ui::VKEY_LEFT, ui::EF_NONE);
  testing::Mock::VerifyAndClearExpectations(&mock_action_view_delegate());

  // Next, do the same with the wrong modifier key. Again, this should do
  // nothing.
  constexpr int kWrongModifierFlag =
#if BUILDFLAG(IS_MAC)
      ui::EF_CONTROL_DOWN;
#else
      ui::EF_COMMAND_DOWN;
#endif

  EXPECT_CALL(mock_action_view_delegate(), MovePinnedActionBy).Times(0);
  send_key_press(ui::VKEY_RIGHT, kWrongModifierFlag);
  testing::Mock::VerifyAndClearExpectations(&mock_action_view_delegate());

  EXPECT_CALL(mock_action_view_delegate(), MovePinnedActionBy).Times(0);
  send_key_press(ui::VKEY_LEFT, kWrongModifierFlag);
  testing::Mock::VerifyAndClearExpectations(&mock_action_view_delegate());

  // Finally, use the correct modifier flag, and verify the delegate is told
  // to move the action by the correct index.
  constexpr int kCorrectModifierFlag =
#if BUILDFLAG(IS_MAC)
      ui::EF_COMMAND_DOWN;
#else
      ui::EF_CONTROL_DOWN;
#endif
  EXPECT_CALL(mock_action_view_delegate(),
              MovePinnedActionBy(model->GetId(), 1))
      .Times(1);
  send_key_press(ui::VKEY_RIGHT, kCorrectModifierFlag);
  testing::Mock::VerifyAndClearExpectations(&mock_action_view_delegate());

  EXPECT_CALL(mock_action_view_delegate(),
              MovePinnedActionBy(model->GetId(), -1))
      .Times(1);
  send_key_press(ui::VKEY_LEFT, kCorrectModifierFlag);
  testing::Mock::VerifyAndClearExpectations(&mock_action_view_delegate());

  Reset();
}
