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

#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/interaction/browser_elements.h"
#include "chrome/browser/ui/toolbar/app_menu_model.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "content/public/test/browser_test.h"
#include "ui/base/interaction/element_tracker.h"
#include "ui/base/interaction/expect_call_in_scope.h"
#include "ui/base/interaction/interaction_sequence.h"
#include "ui/base/page_transition_types.h"
#include "ui/base/test/ui_controls.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/interaction/mouse/interaction_test_util_mouse.h"
#include "ui/views/view_utils.h"

class InteractionTestUtilMouseUiTest
    : public InProcessBrowserTest,
      public testing::WithParamInterface<bool> {
 public:
  InteractionTestUtilMouseUiTest() = default;
  ~InteractionTestUtilMouseUiTest() override = default;

  using Mouse = views::test::InteractionTestUtilMouse;

  void SetUpOnMainThread() override {
    InProcessBrowserTest::SetUpOnMainThread();
    mouse_ = std::make_unique<Mouse>(browser()->GetWindow()->GetNativeWindow());
    CHECK(mouse_->SetTouchMode(GetParam()));
  }

  void TearDownOnMainThread() override {
    mouse_.reset();
    InProcessBrowserTest::TearDownOnMainThread();
  }

 protected:
  std::unique_ptr<Mouse> mouse_;
};

#if BUILDFLAG(IS_CHROMEOS)
INSTANTIATE_TEST_SUITE_P(TouchMode,
                         InteractionTestUtilMouseUiTest,
                         testing::Bool());
#else
INSTANTIATE_TEST_SUITE_P(TouchMode,
                         InteractionTestUtilMouseUiTest,
                         testing::Values(false));
#endif

IN_PROC_BROWSER_TEST_P(InteractionTestUtilMouseUiTest, MoveAndClick) {
  UNCALLED_MOCK_CALLBACK(ui::InteractionSequence::AbortedCallback, aborted);
  UNCALLED_MOCK_CALLBACK(ui::InteractionSequence::CompletedCallback, completed);

  auto sequence =
      ui::InteractionSequence::Builder()
          .SetContext(BrowserElements::From(browser())->GetContext())
          .SetAbortedCallback(aborted.Get())
          .SetCompletedCallback(completed.Get())
          // Find the app menu button.
          .AddStep(ui::InteractionSequence::StepBuilder()
                       .SetElementID(kToolbarAppMenuButtonElementId)
                       .SetStartCallback(base::BindLambdaForTesting(
                           [this](ui::InteractionSequence* seq,
                                  ui::TrackedElement* el) {
                             auto* const view =
                                 el->AsA<views::TrackedElementViews>()->view();
                             const gfx::Point pos =
                                 view->GetBoundsInScreen().CenterPoint();
                             // Perform the following gesture:
                             // - move to the center point of the app menu
                             // button
                             // - click the left mouse button
                             if (!mouse_->PerformGestures(
                                     Mouse::GestureParams(
                                         view->GetWidget()->GetNativeWindow()),
                                     Mouse::MoveTo(pos),
                                     Mouse::Click(ui_controls::LEFT))) {
                               seq->FailForTesting();
                             }
                           })))
          // Verify that the click opened the app menu, which should contain a
          // known menu item.
          .AddStep(ui::InteractionSequence::StepBuilder().SetElementID(
              AppMenuModel::kMoreToolsMenuItem))
          .Build();

  EXPECT_CALL_IN_SCOPE(completed, Run, sequence->RunSynchronouslyForTesting());
}

IN_PROC_BROWSER_TEST_P(InteractionTestUtilMouseUiTest, GestureAborted) {
  UNCALLED_MOCK_CALLBACK(ui::InteractionSequence::AbortedCallback, aborted);
  UNCALLED_MOCK_CALLBACK(ui::InteractionSequence::CompletedCallback, completed);

  auto cancel =
      base::BindLambdaForTesting([this]() { mouse_->CancelAllGestures(); });

  auto sequence =
      ui::InteractionSequence::Builder()
          .SetContext(BrowserElements::From(browser())->GetContext())
          .SetAbortedCallback(aborted.Get())
          .SetCompletedCallback(completed.Get())
          // Find the app menu button.
          .AddStep(ui::InteractionSequence::StepBuilder()
                       .SetElementID(kToolbarAppMenuButtonElementId)
                       .SetStartCallback(base::BindLambdaForTesting(
                           [this, &cancel](ui::TrackedElement* el) {
                             auto* const view =
                                 el->AsA<views::TrackedElementViews>()->view();
                             const gfx::Point pos =
                                 view->GetBoundsInScreen().CenterPoint();
                             // Queue a cancellation. This should execute
                             // sometime after the mouse move is sent.
                             base::SingleThreadTaskRunner::GetCurrentDefault()
                                 ->PostTask(FROM_HERE, cancel);
                             // Perform the following gesture:
                             // - move to the center point of the app menu
                             // button
                             // - click the left mouse button
                             EXPECT_FALSE(mouse_->PerformGestures(
                                 Mouse::GestureParams(
                                     view->GetWidget()->GetNativeWindow()),
                                 Mouse::MoveTo(pos),
                                 Mouse::Click(ui_controls::LEFT)));
                           })))
          .Build();

  EXPECT_CALL_IN_SCOPE(completed, Run, sequence->RunSynchronouslyForTesting());
}

IN_PROC_BROWSER_TEST_P(InteractionTestUtilMouseUiTest, Drag) {
  UNCALLED_MOCK_CALLBACK(ui::InteractionSequence::AbortedCallback, aborted);
  UNCALLED_MOCK_CALLBACK(ui::InteractionSequence::CompletedCallback, completed);

  const GURL first_url =
      browser()->tab_strip_model()->GetWebContentsAt(0)->GetURL();
  const GURL kSecondUrl("chrome://version");
  ASSERT_TRUE(AddTabAtIndex(-1, kSecondUrl, ui::PAGE_TRANSITION_LINK));

  auto sequence =
      ui::InteractionSequence::Builder()
          .SetContext(BrowserElements::From(browser())->GetContext())
          .SetAbortedCallback(aborted.Get())
          .SetCompletedCallback(completed.Get())
          // Find the tab strip.
          .AddStep(
              ui::InteractionSequence::StepBuilder()
                  .SetElementID(kTabStripElementId)
                  .SetStartCallback(base::BindLambdaForTesting(
                      [&](ui::InteractionSequence* seq,
                          ui::TrackedElement* el) {
                        auto* const tab_strip = views::AsViewClass<TabStrip>(
                            el->AsA<views::TrackedElementViews>()->view());
                        // The second tab might still be animating in, which
                        // could cause weirdness if we try to drag.
                        tab_strip->StopAnimating();

                        const gfx::Point start = tab_strip->tab_at(0)
                                                     ->GetBoundsInScreen()
                                                     .CenterPoint();
                        const gfx::Point end = tab_strip->tab_at(1)
                                                   ->GetBoundsInScreen()
                                                   .CenterPoint();
                        // Drag the first tab into the second spot.
                        if (!mouse_->PerformGestures(
                                Mouse::GestureParams(
                                    tab_strip->GetWidget()->GetNativeWindow()),
                                Mouse::MoveTo(start),
                                Mouse::DragAndRelease(end))) {
                          seq->FailForTesting();
                        }
                      })))
          // When the gesture is complete, check that the gesture succeeded.
          .AddStep(
              ui::InteractionSequence::StepBuilder()
                  .SetElementID(kTabStripElementId)
                  .SetStartCallback(base::BindLambdaForTesting(
                      [&](ui::InteractionSequence* seq,
                          ui::TrackedElement* el) {
                        // Stop any remaining animations, and verify that the
                        // tab was moved.
                        auto* const tab_strip = views::AsViewClass<TabStrip>(
                            el->AsA<views::TrackedElementViews>()->view());
                        tab_strip->StopAnimating();

                        EXPECT_EQ(kSecondUrl, browser()
                                                  ->tab_strip_model()
                                                  ->GetWebContentsAt(0)
                                                  ->GetURL());
                        EXPECT_EQ(first_url, browser()
                                                 ->tab_strip_model()
                                                 ->GetWebContentsAt(1)
                                                 ->GetURL());
                        // Clean up any drag gestures that have not yet properly
                        // cleaned up (this is a platform implementation issue
                        // for drag event handling).
                        mouse_->CancelAllGestures();
                      })))
          .Build();

  EXPECT_CALL_IN_SCOPE(completed, Run, sequence->RunSynchronouslyForTesting());
}
