// Copyright 2018 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/controls/hover_button.h"

#include <array>
#include <memory>
#include <string>

#include "base/strings/strcat.h"
#include "base/strings/to_string.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/test/views/chrome_views_test_base.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/text_utils.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/styled_label.h"
#include "ui/views/style/typography.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/widget/widget_utils.h"

namespace {

constexpr int kButtonWidth = 150;

struct TitleSubtitlePair {
  const std::u16string title;
  const std::u16string subtitle;
  // Whether the HoverButton is expected to have a tooltip for this text.
  bool tooltip;
};

const std::array<TitleSubtitlePair, 4> kTitleSubtitlePairs{
    // Two short strings that will fit in the space given.
    {
        {u"Clap!", u"Clap!", false},
        // First string fits, second string doesn't.
        {u"If you're happy and you know it, clap your hands!", u"Clap clap!",
         true},
        // Second string fits, first string doesn't.
        {u"Clap clap!",
         u"If you're happy and you know it, and you really want to show it,",
         true},
        // Both strings don't fit.
        {u"If you're happy and you know it, and you really want to show it,",
         u"If you're happy and you know it, clap your hands!", true},
    }};

// Returns the accessible name of `button`.
std::u16string GetAccessibleName(HoverButton& button) {
  ui::AXNodeData data;
  button.GetViewAccessibility().GetAccessibleNodeData(&data);
  return data.GetString16Attribute(ax::mojom::StringAttribute::kName);
}

}  // namespace

class HoverButtonTest : public ChromeViewsTestBase {
 public:
  HoverButtonTest() = default;

  HoverButtonTest(const HoverButtonTest&) = delete;
  HoverButtonTest& operator=(const HoverButtonTest&) = delete;

  void SetUp() override {
    ChromeViewsTestBase::SetUp();
    widget_ = CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
    generator_ = std::make_unique<ui::test::EventGenerator>(
        GetRootWindow(widget_.get()), widget_->GetNativeWindow());
  }

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

  std::unique_ptr<views::View> CreateIcon() {
    auto icon = std::make_unique<views::View>();
    icon->SetPreferredSize(gfx::Size(16, 16));
    return icon;
  }

  ui::test::EventGenerator* generator() { return generator_.get(); }
  views::Widget* widget() { return widget_.get(); }

  views::Label* GetButtonSubtitle(const HoverButton& button) {
    return button.subtitle();
  }

  views::Label* GetButtonFooter(const HoverButton& button) {
    return button.footer();
  }

  views::View* GetButtonIconWrapper(const HoverButton& button) {
    return button.icon_wrapper_;
  }

 private:
  std::unique_ptr<views::Widget> widget_;
  std::unique_ptr<ui::test::EventGenerator> generator_;
};

// Tests whether the HoverButton has the correct tooltip and accessible name.
TEST_F(HoverButtonTest, TooltipAndAccessibleName) {
  for (size_t i = 0; i < std::size(kTitleSubtitlePairs); ++i) {
    TitleSubtitlePair pair = kTitleSubtitlePairs[i];
    SCOPED_TRACE(testing::Message() << "Index: " << i << ", expected_tooltip="
                                    << base::ToString(pair.tooltip));
    auto button =
        std::make_unique<HoverButton>(views::Button::PressedCallback(),
                                      CreateIcon(), pair.title, pair.subtitle);
    views::IgnoreMissingWidgetForTestingScopedSetter ignore_missing_widget(
        button->GetViewAccessibility());

    button->SetSize(gfx::Size(kButtonWidth, 40));

    // The accessible name should always be the title and subtitle concatenated
    // by \n.
    const std::u16string expected =
        base::StrCat({pair.title, u"\n", pair.subtitle});
    EXPECT_EQ(expected, GetAccessibleName(*button));

    EXPECT_EQ(pair.tooltip ? expected : std::u16string(),
              button->GetRenderedTooltipText(gfx::Point()));
  }
}

TEST_F(HoverButtonTest, TooltipAndAccessibleNameWithFooter) {
  auto button = std::make_unique<HoverButton>(
      views::Button::PressedCallback(), CreateIcon(), u"Title", u"Subtitle",
      /*secondary_icon=*/nullptr,
      /*add_vertical_label_spacing=*/true, u"Footer");
  button->SetSize(gfx::Size(kButtonWidth, 40));
  // The accessible name should be the title, subtitle, and footer concatenated
  // by \n.
  const std::u16string expected = u"Title\nSubtitle\nFooter";

  views::IgnoreMissingWidgetForTestingScopedSetter ignore_missing_widget(
      button->GetViewAccessibility());

  EXPECT_EQ(expected, GetAccessibleName(*button));
  EXPECT_EQ(std::u16string(), button->GetRenderedTooltipText(gfx::Point()));
}

TEST_F(HoverButtonTest, TooltipAndAccessibleName_DynamicTextUpdate) {
  std::u16string original_title = u"Title";
  std::u16string original_subtitle = u"Subtitle";

  auto button = std::make_unique<HoverButton>(views::Button::PressedCallback(),
                                              CreateIcon(), original_title,
                                              original_subtitle);
  button->SetSize(gfx::Size(kButtonWidth, 40));

  views::IgnoreMissingWidgetForTestingScopedSetter ignore_missing_widget(
      button->GetViewAccessibility());

  // Verify accessible has the original title and subtitle text, and tooltip is
  // empty since text fits in the button.
  std::u16string expected =
      base::StrCat({original_title, u"\n", original_subtitle});
  EXPECT_EQ(expected, GetAccessibleName(*button));
  EXPECT_EQ(std::u16string(), button->GetTooltipText());

  // Update the title with text that still fits in the button.
  std::u16string updated_title = u"New title";
  button->title()->SetText(updated_title);

  // Verify accessible name has the updated title, and tooltip is still empty
  // since text fits in the button.
  expected = base::StrCat({updated_title, u"\n", original_subtitle});
  EXPECT_EQ(expected, GetAccessibleName(*button));
  EXPECT_EQ(std::u16string(), button->GetTooltipText());

  // Update the subtitle with text that doesn't fit in the button.
  std::u16string updated_subtitle =
      u"A very long new subtitle that should not fit in the button";
  GetButtonSubtitle(*button)->SetText(updated_subtitle);

  // Verify both accessible name and tooltip have the updated title and
  // subtitle.
  expected = base::StrCat({updated_title, u"\n", updated_subtitle});
  EXPECT_EQ(expected, GetAccessibleName(*button));
  EXPECT_EQ(expected, button->GetTooltipText());
}

// Tests that a button with a subtitle and icons can be instantiated without a
// crash.
TEST_F(HoverButtonTest, CreateButtonWithSubtitleAndIcons) {
  std::unique_ptr<views::View> primary_icon = CreateIcon();
  views::View* primary_icon_raw = primary_icon.get();
  std::unique_ptr<views::View> secondary_icon = CreateIcon();
  views::View* secondary_icon_raw = secondary_icon.get();

  HoverButton button(views::Button::PressedCallback(), std::move(primary_icon),
                     u"Title", u"Subtitle", std::move(secondary_icon));
  EXPECT_TRUE(button.Contains(primary_icon_raw));
  EXPECT_TRUE(button.Contains(secondary_icon_raw));
}

// Tests a button with a subtitle and a footer.
TEST_F(HoverButtonTest, CreateButtonWithSubtitleAndFooter) {
  std::unique_ptr<views::View> primary_icon = CreateIcon();
  views::View* primary_icon_raw = primary_icon.get();
  std::unique_ptr<views::View> secondary_icon = CreateIcon();
  views::View* secondary_icon_raw = secondary_icon.get();
  HoverButton button(views::Button::PressedCallback(), std::move(primary_icon),
                     u"Title", u"Subtitle", std::move(secondary_icon),
                     /*add_vertical_label_spacing=*/true, u"Footer");
  EXPECT_TRUE(button.Contains(primary_icon_raw));
  EXPECT_TRUE(button.Contains(secondary_icon_raw));
  EXPECT_EQ(button.title()->GetText(), u"Title");
  EXPECT_EQ(GetButtonSubtitle(button)->GetText(), u"Subtitle");
  EXPECT_EQ(GetButtonFooter(button)->GetText(), u"Footer");
}

// Tests that the button is activated on mouse release rather than mouse press.
TEST_F(HoverButtonTest, ActivatesOnMouseReleased) {
  bool clicked = false;
  HoverButton* button = widget()->SetContentsView(std::make_unique<HoverButton>(
      base::BindRepeating([](bool* clicked) { *clicked = true; }, &clicked),
      CreateIcon(), u"Title", std::u16string()));
  button->SetBoundsRect(gfx::Rect(100, 100, 200, 200));
  widget()->Show();

  // Button callback should not be called on press.
  generator()->PressLeftButton();
  EXPECT_FALSE(clicked);

  // Button callback should be called on release.
  generator()->ReleaseLeftButton();
  EXPECT_TRUE(clicked);

  widget()->Close();
}

// Test that changing the text style updates the return value of
// views::View::GetHeightForWidth().
TEST_F(HoverButtonTest, ChangingTextStyleResizesButton) {
  auto button = std::make_unique<HoverButton>(
      views::Button::PressedCallback(), CreateIcon(), u"Title", u"Subtitle");
  button->SetSubtitleTextStyle(views::style::CONTEXT_LABEL,
                               views::style::STYLE_SECONDARY);
  int height1 = button->GetHeightForWidth(100);
  button->SetSubtitleTextStyle(views::style::CONTEXT_DIALOG_TITLE,
                               views::style::STYLE_SECONDARY);
  int height2 = button->GetHeightForWidth(100);
  EXPECT_NE(height1, height2);
}

// No touch on desktop Mac.
#if !BUILDFLAG(IS_MAC) || defined(USE_AURA)

// Tests that tapping hover button does not crash if the tap handler removes the
// button from views hierarchy.
TEST_F(HoverButtonTest, TapGestureThatDeletesTheButton) {
  bool clicked = false;
  HoverButton* button = widget()->SetContentsView(std::make_unique<HoverButton>(
      base::BindRepeating(
          [](bool* clicked, views::Widget* widget) {
            *clicked = true;
            // Update the widget contents view, which deletes the hover button.
            widget->SetContentsView(std::make_unique<views::View>());
          },
          &clicked, widget()),
      CreateIcon(), u"Title", std::u16string()));
  button->SetBoundsRect(gfx::Rect(100, 100, 200, 200));
  widget()->Show();

  generator()->GestureTapAt(gfx::Point(150, 150));
  EXPECT_TRUE(clicked);

  widget()->Close();
}

TEST_F(HoverButtonTest, SetIconHorizontalMargins) {
  std::unique_ptr<views::View> primary_icon = CreateIcon();

  HoverButton button(views::Button::PressedCallback(), std::move(primary_icon),
                     u"Title");
  button.SetIconHorizontalMargins(/*left=*/3, /*right=*/4);
  gfx::Insets* margins =
      GetButtonIconWrapper(button)->GetProperty(views::kMarginsKey);
  EXPECT_EQ(margins->left(), 3);
  EXPECT_EQ(margins->right(), 4);
}

TEST_F(HoverButtonTest, AddExtraAccessibleText) {
  std::unique_ptr<views::View> primary_icon = CreateIcon();
  auto button = std::make_unique<HoverButton>(
      views::Button::PressedCallback(), std::move(primary_icon), u"Title");
  button->AddExtraAccessibleText(u"A11y text");

  views::IgnoreMissingWidgetForTestingScopedSetter ignore_missing_widget(
      button->GetViewAccessibility());
  button->SetSize(gfx::Size(kButtonWidth, 40));

  EXPECT_EQ(GetAccessibleName(*button), u"Title\nA11y text");
}

#endif  // !BUILDFLAG(IS_MAC) || defined(USE_AURA)
