// Copyright 2021 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/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/test_simple_task_runner.h"
#include "build/build_config.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/hit_test_region_observer.h"
#include "net/dns/mock_host_resolver.h"
#include "ui/base/test/ui_controls.h"
#include "ui/base/ui_base_features.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/native_ui_types.h"
#include "ui/views/corewm/tooltip_aura.h"
#include "ui/views/corewm/tooltip_controller.h"
#include "ui/views/corewm/tooltip_controller_test_helper.h"
#include "ui/views/widget/any_widget_observer.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/public/tooltip_observer.h"

using content::RenderFrameHost;
using content::RenderWidgetHostView;
using content::WebContents;
using views::corewm::TooltipController;
using views::corewm::test::TooltipControllerTestHelper;

using views::corewm::TooltipAura;

class TooltipMonitor {
 public:
  TooltipMonitor() {
    observer_ = std::make_unique<views::AnyWidgetObserver>(
        views::test::AnyWidgetTestPasskey());
    observer_->set_shown_callback(base::BindRepeating(
        &TooltipMonitor::OnTooltipShown, base::Unretained(this)));
    observer_->set_closing_callback(base::BindRepeating(
        &TooltipMonitor::OnTooltipClosed, base::Unretained(this)));
  }
  TooltipMonitor(const TooltipMonitor&) = delete;
  TooltipMonitor& operator=(const TooltipMonitor&) = delete;
  ~TooltipMonitor() = default;

  bool IsWidgetActive() const { return active_widget_; }

  void WaitUntilTooltipShown() {
    if (!active_widget_) {
      run_loop_ = std::make_unique<base::RunLoop>();
      run_loop_->Run();
    }
  }

  void WaitUntilTooltipClosed() {
    if (active_widget_) {
      run_loop_ = std::make_unique<base::RunLoop>();
      run_loop_->Run();
    }
  }

  void OnTooltipShown(views::Widget* widget) {
    if (widget->GetName() == TooltipAura::kWidgetName) {
      active_widget_ = widget;
      if (run_loop_ && run_loop_->running()) {
        run_loop_->Quit();
      }
    }
  }

  void OnTooltipClosed(views::Widget* widget) {
    if (active_widget_ == widget) {
      active_widget_ = nullptr;
      if (run_loop_ && run_loop_->running()) {
        run_loop_->Quit();
      }
    }
  }

 private:
  // This attribute will help us avoid starting the |run_loop_| when the widget
  // is already shown. Otherwise, we would be waiting infinitely for an event
  // that already occurred.
  raw_ptr<views::Widget> active_widget_ = nullptr;
  std::unique_ptr<views::AnyWidgetObserver> observer_;
  std::unique_ptr<base::RunLoop> run_loop_;
};  // class TooltipMonitor

// Browser tests for tooltips on platforms that use TooltipAura to display the
// tooltip.
class TooltipBrowserTest : public InProcessBrowserTest {
 public:
  TooltipBrowserTest() = default;
  ~TooltipBrowserTest() override = default;

  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");
    ASSERT_TRUE(embedded_test_server()->Start());

    gfx::NativeWindow root_window =
        browser()->GetWindow()->GetNativeWindow()->GetRootWindow();
    event_generator_ = std::make_unique<ui::test::EventGenerator>(root_window);
    helper_ = std::make_unique<TooltipControllerTestHelper>(root_window);
    tooltip_monitor_ = std::make_unique<TooltipMonitor>();
  }

  content::WebContents* web_contents() { return web_contents_; }

 protected:
  void NavigateToURL(const std::string& relative_url) {
    ASSERT_TRUE(ui_test_utils::NavigateToURL(
        browser(), embedded_test_server()->GetURL("a.com", relative_url)));
    web_contents_ = browser()->GetTabStripModel()->GetActiveWebContents();
    rwhv_ = web_contents_->GetRenderWidgetHostView();
    content::WaitForHitTestData(web_contents_->GetPrimaryMainFrame());
  }

  void LoadCrossSitePageIntoFrame(const std::string& relative_url,
                                  const std::string& iframe_id) {
    // This function makes the iframe in the web page point to a different
    // origin, making it an OOPIF.
    GURL frame_url(embedded_test_server()->GetURL("b.com", relative_url));
    NavigateIframeToURL(web_contents_, iframe_id, frame_url);

    // Verify that the child frame is an OOPIF with a different SiteInstance.
    RenderFrameHost* child = GetChildRenderFrameHost(0);
    EXPECT_NE(web_contents_->GetSiteInstance(), child->GetSiteInstance());
    EXPECT_TRUE(child->IsCrossProcessSubframe());
    content::WaitForHitTestData(child);
  }

  RenderFrameHost* GetChildRenderFrameHost(size_t index) {
    return ChildFrameAt(web_contents_->GetPrimaryMainFrame(), index);
  }

  gfx::Point WebContentPositionToScreenCoordinate(int x, int y) {
    return gfx::Point(x, y) + rwhv_->GetViewBounds().OffsetFromOrigin();
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures,
                                    "KeyboardAccessibleTooltip");
    scoped_feature_list_.InitWithFeatures(
        {features::kKeyboardAccessibleTooltip}, {});
  }

  ui::test::EventGenerator* event_generator() { return event_generator_.get(); }
  TooltipControllerTestHelper* helper() { return helper_.get(); }
  TooltipMonitor* tooltip_monitor() { return tooltip_monitor_.get(); }

 private:
  std::unique_ptr<ui::test::EventGenerator> event_generator_ = nullptr;
  raw_ptr<RenderWidgetHostView, AcrossTasksDanglingUntriaged> rwhv_ = nullptr;
  raw_ptr<WebContents, AcrossTasksDanglingUntriaged> web_contents_ = nullptr;

  std::unique_ptr<TooltipControllerTestHelper> helper_;
  std::unique_ptr<TooltipMonitor> tooltip_monitor_ = nullptr;

  base::test::ScopedFeatureList scoped_feature_list_;
};  // class TooltipBrowserTest

// TOOD(crbug.com/40768202): Flakily fails on Windows
#if BUILDFLAG(IS_WIN)
#define MAYBE_ShowTooltipFromWebContentWithCursor \
  DISABLED_ShowTooltipFromWebContentWithCursor
#else
#define MAYBE_ShowTooltipFromWebContentWithCursor \
  ShowTooltipFromWebContentWithCursor
#endif
IN_PROC_BROWSER_TEST_F(TooltipBrowserTest,
                       MAYBE_ShowTooltipFromWebContentWithCursor) {
  NavigateToURL("/tooltip.html");
  std::u16string expected_text = u"my tooltip";

  // Trigger the tooltip from the cursor.
  // This will position the cursor right above a button with the title attribute
  // set and will show the tooltip.
  gfx::Point position = WebContentPositionToScreenCoordinate(10, 10);

  event_generator()->MoveMouseTo(position);
  tooltip_monitor()->WaitUntilTooltipShown();
  EXPECT_TRUE(helper()->IsTooltipVisible());
  EXPECT_EQ(expected_text, helper()->GetTooltipText());

  helper()->HideAndReset();
  tooltip_monitor()->WaitUntilTooltipClosed();
}

#if BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_WIN)
// https://crbug.com/40768202. Flaky on linux-chromeos-rel and other linux bots.
#define MAYBE_ShowTooltipFromWebContentWithKeyboard \
  DISABLED_ShowTooltipFromWebContentWithKeyboard
#else
#define MAYBE_ShowTooltipFromWebContentWithKeyboard \
  ShowTooltipFromWebContentWithKeyboard
#endif
IN_PROC_BROWSER_TEST_F(TooltipBrowserTest,
                       MAYBE_ShowTooltipFromWebContentWithKeyboard) {
  NavigateToURL("/tooltip.html");
  std::u16string expected_text = u"my tooltip";

  // Trigger the tooltip from the keyboard with a TAB keypress.
  event_generator()->PressKey(ui::VKEY_TAB, ui::EF_NONE);
  event_generator()->ReleaseKey(ui::VKEY_TAB, ui::EF_NONE);
  tooltip_monitor()->WaitUntilTooltipShown();
  EXPECT_TRUE(helper()->IsTooltipVisible());
  EXPECT_EQ(expected_text, helper()->GetTooltipText());

  helper()->HideAndReset();
}

// https://crbug.com/40768202. Flaky on linux-chromeos-rel.
// https://crbug.com/40786199. Flaky on Win.
IN_PROC_BROWSER_TEST_F(TooltipBrowserTest,
                       DISABLED_ShowTooltipFromIFrameWithKeyboard) {
  // There are two tooltips in this file: one above the iframe and one inside
  // the iframe.
  NavigateToURL("/tooltip_in_iframe.html");
  std::u16string expected_text = u"my tooltip";

  // Make the iframe cross-origin.
  LoadCrossSitePageIntoFrame("/tooltip.html", "iframe1");

  // Move the focus to the button outside of the iframe to get its position.
  // We'll use it in the next step to validate that the tooltip associated with
  // the button inside of the iframe is positioned, well, inside the iframe.
  event_generator()->PressKey(ui::VKEY_TAB, ui::EF_NONE);
  event_generator()->ReleaseKey(ui::VKEY_TAB, ui::EF_NONE);
  tooltip_monitor()->WaitUntilTooltipShown();
  EXPECT_TRUE(helper()->IsTooltipVisible());
  EXPECT_EQ(expected_text, helper()->GetTooltipText());
  gfx::Point first_tooltip_anchor_point = helper()->GetTooltipPosition();

  // Now that we have the anchor point of the first tooltip, move the focus to
  // the tooltip inside of the iframe.
  event_generator()->PressKey(ui::VKEY_TAB, ui::EF_NONE);
  event_generator()->ReleaseKey(ui::VKEY_TAB, ui::EF_NONE);
  tooltip_monitor()->WaitUntilTooltipShown();
  EXPECT_TRUE(helper()->IsTooltipVisible());
  EXPECT_EQ(expected_text, helper()->GetTooltipText());

  // Validate that the tooltip is correctly positioned inside the iframe.
  // Because we explicitly removed the iframe's borders and body margins, the
  // buttons should be positioned at the exact same x value as the button above,
  // and at exactly twice the y value as the one outside the iframe (because the
  // tooltip's anchor point is at the bottom-center of the button).
  int expected_y = 2 * first_tooltip_anchor_point.y();
  EXPECT_EQ(gfx::Point(first_tooltip_anchor_point.x(), expected_y),
            helper()->GetTooltipPosition());

  helper()->HideAndReset();
  tooltip_monitor()->WaitUntilTooltipClosed();
}

#if BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
// https://crbug.com/40768202. Flaky on linux-chromeos-rel.
// https://crbug.com/40786199. Flaky on Win.
#define MAYBE_HideTooltipOnKeyPressTriggeredByCursor \
  DISABLED_HideTooltipOnKeyPressTriggeredByCursor
#else
#define MAYBE_HideTooltipOnKeyPressTriggeredByCursor \
  HideTooltipOnKeyPressTriggeredByCursor
#endif
IN_PROC_BROWSER_TEST_F(TooltipBrowserTest,
                       MAYBE_HideTooltipOnKeyPressTriggeredByCursor) {
  NavigateToURL("/tooltip.html");
  std::u16string expected_text = u"my tooltip";

  // First, trigger the tooltip from the cursor.
  gfx::Point position = WebContentPositionToScreenCoordinate(10, 10);
  event_generator()->MoveMouseTo(position);
  tooltip_monitor()->WaitUntilTooltipShown();
  EXPECT_TRUE(helper()->IsTooltipVisible());
  EXPECT_EQ(expected_text, helper()->GetTooltipText());

  // Second, send a key press event to test whether the tooltip gets hidden.
  EXPECT_TRUE(tooltip_monitor()->IsWidgetActive());
  ui_controls::SendKeyPress(browser()->GetWindow()->GetNativeWindow(),
                            ui::VKEY_A, false, false, false, false);
  tooltip_monitor()->WaitUntilTooltipClosed();
  EXPECT_FALSE(helper()->IsTooltipVisible());
}

#if BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
// https://crbug.com/40768202. Flaky on linux-chromeos-rel.
// https://crbug.com/40786199. Flaky on Win.
#define MAYBE_HideTooltipOnKeyPressTriggeredByKeyboard \
  DISABLED_HideTooltipOnKeyPressTriggeredByKeyboard
#else
#define MAYBE_HideTooltipOnKeyPressTriggeredByKeyboard \
  HideTooltipOnKeyPressTriggeredByKeyboard
#endif
IN_PROC_BROWSER_TEST_F(TooltipBrowserTest,
                       MAYBE_HideTooltipOnKeyPressTriggeredByKeyboard) {
  NavigateToURL("/tooltip.html");
  std::u16string expected_text = u"my tooltip";

  // Trigger the tooltip from the keyboard with a TAB keypress.
  event_generator()->PressKey(ui::VKEY_TAB, ui::EF_NONE);
  event_generator()->ReleaseKey(ui::VKEY_TAB, ui::EF_NONE);
  tooltip_monitor()->WaitUntilTooltipShown();
  EXPECT_TRUE(helper()->IsTooltipVisible());
  EXPECT_EQ(expected_text, helper()->GetTooltipText());

  // Send a key press event to test whether the tooltip gets hidden.
  EXPECT_TRUE(tooltip_monitor()->IsWidgetActive());
  event_generator()->PressKey(ui::VKEY_A, ui::EF_NONE);
  event_generator()->ReleaseKey(ui::VKEY_A, ui::EF_NONE);
  tooltip_monitor()->WaitUntilTooltipClosed();
  EXPECT_FALSE(helper()->IsTooltipVisible());
}

#if BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
// https://crbug.com/40768202. Flaky on linux-chromeos-rel, windows, linux.
#define MAYBE_ScriptFocusHidesKeyboardTriggeredTooltip \
  DISABLED_ScriptFocusHidesKeyboardTriggeredTooltip
#else
#define MAYBE_ScriptFocusHidesKeyboardTriggeredTooltip \
  ScriptFocusHidesKeyboardTriggeredTooltip
#endif
IN_PROC_BROWSER_TEST_F(TooltipBrowserTest,
                       MAYBE_ScriptFocusHidesKeyboardTriggeredTooltip) {
  NavigateToURL("/tooltip_two_buttons.html");
  std::u16string expected_text_1 = u"my tooltip 1";
  std::u16string expected_text_2 = u"my tooltip 2";

  // Trigger the tooltip from the keyboard with a TAB keypress.
  event_generator()->PressKey(ui::VKEY_TAB, ui::EF_NONE);
  event_generator()->ReleaseKey(ui::VKEY_TAB, ui::EF_NONE);
  tooltip_monitor()->WaitUntilTooltipShown();
  EXPECT_TRUE(helper()->IsTooltipVisible());
  EXPECT_EQ(expected_text_1, helper()->GetTooltipText());
  EXPECT_TRUE(tooltip_monitor()->IsWidgetActive());

  // Validate that a blur event on another element than our focused one doesn't
  // hide the tooltip.
  std::string javascript = "document.getElementById('b2').blur();";
  EXPECT_TRUE(content::ExecJs(web_contents(), javascript));

  EXPECT_TRUE(helper()->IsTooltipVisible());
  EXPECT_EQ(expected_text_1, helper()->GetTooltipText());
  EXPECT_TRUE(tooltip_monitor()->IsWidgetActive());

  // Validate that a focus on another element will hide the tooltip.
  javascript = "document.getElementById('b2').focus();";

  EXPECT_TRUE(content::ExecJs(web_contents(), javascript));
  tooltip_monitor()->WaitUntilTooltipClosed();
  EXPECT_FALSE(tooltip_monitor()->IsWidgetActive());
  EXPECT_FALSE(helper()->IsTooltipVisible());

  // Move the focus again to the first button to test the blur on the focused
  // element.
  event_generator()->PressKey(ui::VKEY_TAB, ui::EF_SHIFT_DOWN);
  event_generator()->ReleaseKey(ui::VKEY_TAB, ui::EF_SHIFT_DOWN);
  tooltip_monitor()->WaitUntilTooltipShown();
  EXPECT_TRUE(helper()->IsTooltipVisible());
  EXPECT_EQ(expected_text_1, helper()->GetTooltipText());
  EXPECT_TRUE(tooltip_monitor()->IsWidgetActive());

  // Validate that the blur call hides the tooltip.
  javascript = "document.getElementById('b1').blur();";
  EXPECT_TRUE(content::ExecJs(web_contents(), javascript));

  EXPECT_FALSE(tooltip_monitor()->IsWidgetActive());
  EXPECT_FALSE(helper()->IsTooltipVisible());
}

// TODO(crbug.com/549855061): Flaky on Windows.
#if BUILDFLAG(IS_WIN)
#define MAYBE_ResetTooltipOnClosingWindow DISABLED_ResetTooltipOnClosingWindow
#else
#define MAYBE_ResetTooltipOnClosingWindow ResetTooltipOnClosingWindow
#endif
IN_PROC_BROWSER_TEST_F(TooltipBrowserTest, MAYBE_ResetTooltipOnClosingWindow) {
  NavigateToURL("/tooltip.html");

  // Trigger the tooltip from the cursor.
  gfx::Point position = WebContentPositionToScreenCoordinate(10, 10);
  event_generator()->MoveMouseTo(position);
  tooltip_monitor()->WaitUntilTooltipShown();
  EXPECT_TRUE(helper()->IsTooltipVisible());

  // Tooltip should be hidden on closing window.
  ui_test_utils::BrowserDestroyedObserver observer(browser());
  chrome::CloseWindow(browser());
  // Verify tooltip is closed.
  tooltip_monitor()->WaitUntilTooltipClosed();

  // Make sure Chrome won't crash during window destruction.
  observer.Wait();
}
