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

#include "ui/views/controls/button/label_button.h"

#include <algorithm>
#include <optional>
#include <string>
#include <utility>

#include "base/command_line.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/actions/actions.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/ui_base_switches.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "ui/gfx/text_utils.h"
#include "ui/native_theme/mock_os_settings_provider.h"
#include "ui/native_theme/native_theme.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/animation/test/ink_drop_host_test_api.h"
#include "ui/views/animation/test/test_ink_drop.h"
#include "ui/views/border.h"
#include "ui/views/buildflags.h"
#include "ui/views/controls/button/label_button_image_container.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/style/platform_style.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/test/views_test_utils.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/view_test_api.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget_utils.h"

namespace views {

using ::base::ASCIIToUTF16;

// Testing button that exposes protected methods.
class TestLabelButton : public LabelButton {
  METADATA_HEADER(TestLabelButton, LabelButton)

 public:
  explicit TestLabelButton(const std::u16string& text = std::u16string(),
                           int button_context = style::CONTEXT_BUTTON)
      : LabelButton(Button::PressedCallback(), text, button_context) {}

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

  void SetMultiLine(bool multi_line) { label()->SetMultiLine(multi_line); }

  using LabelButton::GetVisualState;
  using LabelButton::image_container_view;
  using LabelButton::label;
  using LabelButton::OnThemeChanged;
};

BEGIN_METADATA(TestLabelButton)
END_METADATA

class LabelButtonTest : public test::WidgetTest {
 public:
  // testing::Test:
  void SetUp() override {
    WidgetTest::SetUp();
    // Make a Widget to host the button. This ensures appropriate borders are
    // used (which could be derived from the Widget's NativeTheme).
    test_widget_ = CreateTopLevelPlatformWidget();

    // Ensure the Widget is active, since LabelButton appearance in inactive
    // Windows is platform-dependent.
    test_widget_->Show();

    // Place the button into a separate container view which itself does no
    // layouts. This will isolate the button from the client view which does
    // a fill layout by default.
    auto* container =
        test_widget_->client_view()->AddChildView(std::make_unique<View>());
    auto* button = container->AddChildView(std::make_unique<TestLabelButton>());
    button->SetID(1);

    // Establish the expected text colors for testing changes due to state.
    themed_normal_text_color_ =
        button->GetColorProvider()->GetColor(ui::kColorLabelForeground);

    // For styled buttons only, platforms other than Desktop Linux either ignore
    // ColorProvider and use a hardcoded black or (on Mac) have a ColorProvider
    // that reliably returns black.
    styled_normal_text_color_ = SK_ColorBLACK;
#if (BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)) && \
    BUILDFLAG(ENABLE_DESKTOP_AURA)
    // The Linux theme provides a non-black highlight text color, but it's not
    // used for styled buttons.
    styled_highlight_text_color_ = styled_normal_text_color_ =
        button->GetColorProvider()->GetColor(ui::kColorButtonForeground);
#else
    styled_highlight_text_color_ = styled_normal_text_color_;
#endif
  }

  void TearDown() override {
    test_widget_.ExtractAsDangling()->CloseNow();
    WidgetTest::TearDown();
  }

 protected:
  ui::MockOsSettingsProvider& os_settings_provider() {
    return os_settings_provider_;
  }

  TestLabelButton* button() {
    return static_cast<TestLabelButton*>(
        test_widget_->GetContentsView()->GetViewByID(1));
  }

  SkColor themed_normal_text_color_ = 0;
  SkColor styled_normal_text_color_ = 0;
  SkColor styled_highlight_text_color_ = 0;

 private:
  ui::MockOsSettingsProvider os_settings_provider_;
  raw_ptr<Widget> test_widget_ = nullptr;
};

TEST_F(LabelButtonTest, FocusBehavior) {
  EXPECT_EQ(PlatformStyle::kDefaultFocusBehavior, button()->GetFocusBehavior());
}

TEST_F(LabelButtonTest, Init) {
  const std::u16string text(u"abc");
  button()->SetText(text);

  EXPECT_TRUE(button()->GetImage(Button::STATE_NORMAL).isNull());
  EXPECT_TRUE(button()->GetImage(Button::STATE_HOVERED).isNull());
  EXPECT_TRUE(button()->GetImage(Button::STATE_PRESSED).isNull());
  EXPECT_TRUE(button()->GetImage(Button::STATE_DISABLED).isNull());

  EXPECT_EQ(text, button()->GetText());

  ui::AXNodeData accessible_node_data;
  button()->GetViewAccessibility().GetAccessibleNodeData(&accessible_node_data);
  EXPECT_EQ(ax::mojom::Role::kButton, accessible_node_data.role);
  EXPECT_EQ(text, accessible_node_data.GetString16Attribute(
                      ax::mojom::StringAttribute::kName));

  EXPECT_FALSE(button()->GetIsDefault());
  EXPECT_EQ(Button::STATE_NORMAL, button()->GetState());

  EXPECT_EQ(button()->image_container_view()->parent(), button());
  EXPECT_EQ(button()->label()->parent(), button());
}

TEST_F(LabelButtonTest, Label) {
  EXPECT_TRUE(button()->GetText().empty());

  const gfx::FontList font_list = button()->label()->font_list();
  const std::u16string short_text(u"abcdefghijklm");
  const std::u16string long_text(u"abcdefghijklmnopqrstuvwxyz");
  const int short_text_width = gfx::GetStringWidth(short_text, font_list);
  const int long_text_width = gfx::GetStringWidth(long_text, font_list);

  EXPECT_LT(button()->GetPreferredSize({}).width(), short_text_width);
  button()->SetText(short_text);
  EXPECT_GT(button()->GetPreferredSize({}).height(), font_list.GetHeight());
  EXPECT_GT(button()->GetPreferredSize({}).width(), short_text_width);
  EXPECT_LT(button()->GetPreferredSize({}).width(), long_text_width);
  button()->SetText(long_text);
  EXPECT_GT(button()->GetPreferredSize({}).width(), long_text_width);
  button()->SetText(short_text);
  EXPECT_GT(button()->GetPreferredSize({}).width(), short_text_width);
  EXPECT_LT(button()->GetPreferredSize({}).width(), long_text_width);

  // Clamp the size to a maximum value.
  button()->SetText(long_text);
  button()->SetMaxSize(gfx::Size(short_text_width, 1));
  const gfx::Size preferred_size = button()->GetPreferredSize({});
  EXPECT_LE(preferred_size.width(), short_text_width);
  EXPECT_EQ(1, preferred_size.height());

  // Clamp the size to a minimum value.
  button()->SetText(short_text);
  button()->SetMaxSize(gfx::Size());
  button()->SetMinSize(gfx::Size(long_text_width, font_list.GetHeight() * 2));
  EXPECT_EQ(button()->GetPreferredSize({}),
            gfx::Size(long_text_width, font_list.GetHeight() * 2));
}

// Tests LabelButton's usage of SetMaximumWidthSingleLine.
TEST_F(LabelButtonTest, LabelPreferredSizeWithMaxWidth) {
  const std::string text_cases[] = {
      {"The"},
      {"The quick"},
      {"The quick brown"},
      {"The quick brown fox"},
      {"The quick brown fox jumps"},
      {"The quick brown fox jumps over"},
      {"The quick brown fox jumps over the"},
      {"The quick brown fox jumps over the lazy"},
      {"The quick brown fox jumps over the lazy dog"},
  };

  const int width_cases[] = {
      10, 30, 50, 70, 90, 110, 130, 170, 200, 500,
  };

  for (bool is_multiline : {false, true}) {
    button()->SetMultiLine(is_multiline);
    for (bool set_image : {false, true}) {
      if (set_image) {
        button()->SetImageModel(Button::STATE_NORMAL,
                                ui::ImageModel::FromImageSkia(
                                    gfx::test::CreateImageSkia(/*size=*/16)));
      }

      bool preferred_size_is_sometimes_narrower_than_max = false;
      bool preferred_height_shrinks_as_max_width_grows = false;

      for (const auto& text_case : text_cases) {
        for (int width_case : width_cases) {
          const gfx::Size old_preferred_size = button()->GetPreferredSize({});

          button()->SetText(ASCIIToUTF16(text_case));
          button()->SetMaxSize(gfx::Size(width_case, 30));

          const gfx::Size preferred_size = button()->GetPreferredSize({});
          EXPECT_LE(preferred_size.width(), width_case);

          if (preferred_size.width() < width_case) {
            preferred_size_is_sometimes_narrower_than_max = true;
          }

          if (preferred_size.height() < old_preferred_size.height()) {
            preferred_height_shrinks_as_max_width_grows = true;
          }
        }
      }

      EXPECT_TRUE(preferred_size_is_sometimes_narrower_than_max);
      if (is_multiline) {
        EXPECT_TRUE(preferred_height_shrinks_as_max_width_grows);
      }
    }
  }
}

TEST_F(LabelButtonTest, LabelShrinkDown) {
  ASSERT_TRUE(button()->GetText().empty());

  const gfx::FontList font_list = button()->label()->font_list();
  const std::u16string text(u"abcdefghijklm");
  const int text_width = gfx::GetStringWidth(text, font_list);

  ASSERT_LT(button()->GetPreferredSize({}).width(), text_width);
  button()->SetText(text);
  EXPECT_GT(button()->GetPreferredSize({}).width(), text_width);
  button()->SetSize(button()->GetPreferredSize({}));

  // When shrinking, the button should report again the size with no label
  // (while keeping the label).
  button()->ShrinkDownThenClearText();
  EXPECT_EQ(button()->GetText(), text);
  EXPECT_LT(button()->GetPreferredSize({}).width(), text_width);

  // After the layout manager resizes the button to it's desired size, it's text
  // should be empty again.
  button()->SetSize(button()->GetPreferredSize({}));
  EXPECT_TRUE(button()->GetText().empty());
}

TEST_F(LabelButtonTest, LabelShrinksDownOnManualSetBounds) {
  ASSERT_TRUE(button()->GetText().empty());
  ASSERT_GT(button()->GetPreferredSize({}).width(), 1);

  const std::u16string text(u"abcdefghijklm");

  button()->SetText(text);
  EXPECT_EQ(button()->GetText(), text);
  button()->SetSize(button()->GetPreferredSize({}));
  button()->SetBoundsRect(gfx::Rect(button()->GetPreferredSize({})));

  button()->ShrinkDownThenClearText();

  // Manually setting a smaller size should also clear text.
  button()->SetBoundsRect(gfx::Rect(1, 1));
  EXPECT_TRUE(button()->GetText().empty());
}

TEST_F(LabelButtonTest, LabelShrinksDownCanceledBySettingText) {
  ASSERT_TRUE(button()->GetText().empty());

  const gfx::FontList font_list = button()->label()->font_list();
  const std::u16string text(u"abcdefghijklm");
  const int text_width = gfx::GetStringWidth(text, font_list);

  ASSERT_LT(button()->GetPreferredSize({}).width(), text_width);
  button()->SetText(text);
  EXPECT_GT(button()->GetPreferredSize({}).width(), text_width);
  button()->SetBoundsRect(gfx::Rect(button()->GetPreferredSize({})));

  // When shrinking, the button should report again the size with no label
  // (while keeping the label).
  button()->ShrinkDownThenClearText();
  EXPECT_EQ(button()->GetText(), text);
  gfx::Size shrinking_size = button()->GetPreferredSize({});
  EXPECT_LT(shrinking_size.width(), text_width);

  // When we SetText() again, the shrinking gets canceled.
  button()->SetText(text);
  EXPECT_GT(button()->GetPreferredSize({}).width(), text_width);

  // Even if the layout manager resizes the button to it's size desired for
  // shrinking, it's text does not get cleared and it still prefers having space
  // for its label.
  button()->SetSize(shrinking_size);
  EXPECT_FALSE(button()->GetText().empty());
  EXPECT_GT(button()->GetPreferredSize({}).width(), text_width);
}

TEST_F(
    LabelButtonTest,
    LabelShrinksDownImmediatelyIfAlreadySmallerThanPreferredSizeWithoutLabel) {
  button()->SetBoundsRect(gfx::Rect(1, 1));
  button()->SetText(u"abcdefghijklm");

  // Shrinking the text down when it's already shrunk down (its size is smaller
  // than preferred without label) should clear the text immediately.
  EXPECT_FALSE(button()->GetText().empty());
  button()->ShrinkDownThenClearText();
  EXPECT_TRUE(button()->GetText().empty());
}

// Test behavior of ViewAccessibility::GetAccessibleNodeData() for buttons when
// setting a label.
TEST_F(LabelButtonTest, AccessibleState) {
  ui::AXNodeData accessible_node_data;

  button()->GetViewAccessibility().GetAccessibleNodeData(&accessible_node_data);
  EXPECT_EQ(ax::mojom::Role::kButton, accessible_node_data.role);
  EXPECT_EQ(std::u16string(), accessible_node_data.GetString16Attribute(
                                  ax::mojom::StringAttribute::kName));

  // Without a label (e.g. image-only), the accessible name should automatically
  // be set from the tooltip.
  const std::u16string tooltip_text = u"abc";
  button()->SetTooltipText(tooltip_text);
  button()->GetViewAccessibility().GetAccessibleNodeData(&accessible_node_data);
  EXPECT_EQ(tooltip_text, accessible_node_data.GetString16Attribute(
                              ax::mojom::StringAttribute::kName));
  EXPECT_EQ(std::u16string(), button()->GetText());

  // Setting a label overrides the tooltip text.
  const std::u16string label_text = u"def";
  button()->SetText(label_text);
  button()->GetViewAccessibility().GetAccessibleNodeData(&accessible_node_data);
  EXPECT_EQ(label_text, accessible_node_data.GetString16Attribute(
                            ax::mojom::StringAttribute::kName));
  EXPECT_EQ(label_text, button()->GetText());
  EXPECT_EQ(tooltip_text, button()->GetRenderedTooltipText(gfx::Point()));
}

// Test ViewAccessibility::GetAccessibleNodeData() for default buttons.
TEST_F(LabelButtonTest, AccessibleDefaultState) {
  {
    // If SetIsDefault() is not called, the ax default state should not be set.
    ui::AXNodeData ax_data;
    button()->GetViewAccessibility().GetAccessibleNodeData(&ax_data);
    EXPECT_FALSE(ax_data.HasState(ax::mojom::State::kDefault));
  }

  {
    button()->SetIsDefault(true);
    ui::AXNodeData ax_data;
    button()->GetViewAccessibility().GetAccessibleNodeData(&ax_data);
    EXPECT_TRUE(ax_data.HasState(ax::mojom::State::kDefault));
  }

  {
    button()->SetIsDefault(false);
    ui::AXNodeData ax_data;
    button()->GetViewAccessibility().GetAccessibleNodeData(&ax_data);
    EXPECT_FALSE(ax_data.HasState(ax::mojom::State::kDefault));
  }
}

TEST_F(LabelButtonTest, Image) {
  const int small_size = 50, large_size = 100;
  const gfx::ImageSkia small_image = gfx::test::CreateImageSkia(small_size);
  const gfx::ImageSkia large_image = gfx::test::CreateImageSkia(large_size);

  EXPECT_LT(button()->GetPreferredSize({}).width(), small_size);
  EXPECT_LT(button()->GetPreferredSize({}).height(), small_size);
  button()->SetImageModel(Button::STATE_NORMAL,
                          ui::ImageModel::FromImageSkia(small_image));
  EXPECT_GT(button()->GetPreferredSize({}).width(), small_size);
  EXPECT_GT(button()->GetPreferredSize({}).height(), small_size);
  EXPECT_LT(button()->GetPreferredSize({}).width(), large_size);
  EXPECT_LT(button()->GetPreferredSize({}).height(), large_size);
  button()->SetImageModel(Button::STATE_NORMAL,
                          ui::ImageModel::FromImageSkia(large_image));
  EXPECT_GT(button()->GetPreferredSize({}).width(), large_size);
  EXPECT_GT(button()->GetPreferredSize({}).height(), large_size);
  button()->SetImageModel(Button::STATE_NORMAL,
                          ui::ImageModel::FromImageSkia(small_image));
  EXPECT_GT(button()->GetPreferredSize({}).width(), small_size);
  EXPECT_GT(button()->GetPreferredSize({}).height(), small_size);
  EXPECT_LT(button()->GetPreferredSize({}).width(), large_size);
  EXPECT_LT(button()->GetPreferredSize({}).height(), large_size);

  // Clamp the size to a maximum value.
  button()->SetImageModel(Button::STATE_NORMAL,
                          ui::ImageModel::FromImageSkia(large_image));
  button()->SetMaxSize(gfx::Size(large_size, 1));
  EXPECT_EQ(button()->GetPreferredSize({}), gfx::Size(large_size, 1));

  // Clamp the size to a minimum value.
  button()->SetImageModel(Button::STATE_NORMAL,
                          ui::ImageModel::FromImageSkia(small_image));
  button()->SetMaxSize(gfx::Size());
  button()->SetMinSize(gfx::Size(large_size, large_size));
  EXPECT_EQ(button()->GetPreferredSize({}), gfx::Size(large_size, large_size));
}

TEST_F(LabelButtonTest, ImageAlignmentWithMultilineLabel) {
  const std::u16string text(
      u"Some long text that would result in multiline label");
  button()->SetText(text);

  const int max_label_width = 40;
  button()->label()->SetMultiLine(true);
  button()->label()->SetMaximumWidth(max_label_width);

  const int image_size = 16;
  const gfx::ImageSkia image = gfx::test::CreateImageSkia(image_size);
  button()->SetImageModel(Button::STATE_NORMAL,
                          ui::ImageModel::FromImageSkia(image));

  button()->SetBoundsRect(gfx::Rect(button()->GetPreferredSize({})));
  views::test::RunScheduledLayout(button());
  int y_origin_centered = button()->image_container_view()->origin().y();

  button()->SetBoundsRect(gfx::Rect(button()->GetPreferredSize({})));
  button()->SetImageCentered(false);
  views::test::RunScheduledLayout(button());
  int y_origin_not_centered = button()->image_container_view()->origin().y();

  EXPECT_LT(y_origin_not_centered, y_origin_centered);
}

TEST_F(LabelButtonTest, LabelAndImage) {
  const gfx::FontList font_list = button()->label()->font_list();
  const std::u16string text(u"abcdefghijklm");
  const int text_width = gfx::GetStringWidth(text, font_list);

  const int image_size = 50;
  const gfx::ImageSkia image = gfx::test::CreateImageSkia(image_size);
  ASSERT_LT(font_list.GetHeight(), image_size);

  EXPECT_LT(button()->GetPreferredSize({}).width(), text_width);
  EXPECT_LT(button()->GetPreferredSize({}).width(), image_size);
  EXPECT_LT(button()->GetPreferredSize({}).height(), image_size);
  button()->SetText(text);
  EXPECT_GT(button()->GetPreferredSize({}).width(), text_width);
  EXPECT_GT(button()->GetPreferredSize({}).height(), font_list.GetHeight());
  EXPECT_LT(button()->GetPreferredSize({}).width(), text_width + image_size);
  EXPECT_LT(button()->GetPreferredSize({}).height(), image_size);
  button()->SetImageModel(Button::STATE_NORMAL,
                          ui::ImageModel::FromImageSkia(image));
  EXPECT_GT(button()->GetPreferredSize({}).width(), text_width + image_size);
  EXPECT_GT(button()->GetPreferredSize({}).height(), image_size);

  // Layout and ensure the image is left of the label except for ALIGN_RIGHT.
  // (A proper parent view or layout manager would Layout on its invalidations).
  // Also make sure CENTER alignment moves the label compared to LEFT alignment.
  gfx::Size button_size = button()->GetPreferredSize({});
  button_size.Enlarge(50, 0);
  button()->SetSize(button_size);
  views::test::RunScheduledLayout(button());
  EXPECT_LT(button()->image_container_view()->bounds().right(),
            button()->label()->bounds().x());
  int left_align_label_midpoint = button()->label()->bounds().CenterPoint().x();
  button()->SetHorizontalAlignment(gfx::ALIGN_CENTER);
  views::test::RunScheduledLayout(button());
  EXPECT_LT(button()->image_container_view()->bounds().right(),
            button()->label()->bounds().x());
  int center_align_label_midpoint =
      button()->label()->bounds().CenterPoint().x();
  EXPECT_LT(left_align_label_midpoint, center_align_label_midpoint);
  button()->SetHorizontalAlignment(gfx::ALIGN_RIGHT);
  views::test::RunScheduledLayout(button());
  EXPECT_LT(button()->label()->bounds().right(),
            button()->image_container_view()->bounds().x());

  button()->SetText(std::u16string());
  EXPECT_LT(button()->GetPreferredSize({}).width(), text_width + image_size);
  EXPECT_GT(button()->GetPreferredSize({}).width(), image_size);
  EXPECT_GT(button()->GetPreferredSize({}).height(), image_size);
  button()->SetImageModel(Button::STATE_NORMAL, ui::ImageModel());
  EXPECT_LT(button()->GetPreferredSize({}).width(), image_size);
  EXPECT_LT(button()->GetPreferredSize({}).height(), image_size);

  // Clamp the size to a minimum value.
  button()->SetText(text);
  button()->SetImageModel(Button::STATE_NORMAL,
                          ui::ImageModel::FromImageSkia(image));
  button()->SetMinSize(
      gfx::Size((text_width + image_size) * 2, image_size * 2));
  EXPECT_EQ(button()->GetPreferredSize({}).width(),
            (text_width + image_size) * 2);
  EXPECT_EQ(button()->GetPreferredSize({}).height(), image_size * 2);

  // Clamp the size to a maximum value.
  button()->SetMinSize(gfx::Size());
  button()->SetMaxSize(gfx::Size(1, 1));
  EXPECT_EQ(button()->GetPreferredSize({}), gfx::Size(1, 1));
}

TEST_F(LabelButtonTest, LabelWrapAndImageAlignment) {
  LayoutProvider* provider = LayoutProvider::Get();
  const gfx::FontList font_list = button()->label()->font_list();
  const std::u16string text(u"abcdefghijklm abcdefghijklm");
  const int text_wrap_width = gfx::GetStringWidth(text, font_list) / 2;
  const int image_spacing =
      provider->GetDistanceMetric(DISTANCE_RELATED_LABEL_HORIZONTAL);

  button()->SetText(text);
  button()->label()->SetMultiLine(true);

  const int image_size = font_list.GetHeight();
  const gfx::ImageSkia image = gfx::test::CreateImageSkia(image_size);
  ASSERT_EQ(font_list.GetHeight(), image.width());
  gfx::Insets button_insets = button()->GetInsets();
  const int unclamped_size_without_label =
      image.width() + image_spacing + button_insets.width();

  button()->SetImageModel(Button::STATE_NORMAL,
                          ui::ImageModel::FromImageSkia(image));
  button()->SetImageCentered(false);
  button()->SetMaxSize(
      gfx::Size(unclamped_size_without_label + text_wrap_width, 0));

  gfx::Size preferred_size = button()->GetPreferredSize({});
  preferred_size.set_height(
      button()->GetHeightForWidth(preferred_size.width()));
  button()->SetSize(preferred_size);
  views::test::RunScheduledLayout(button());

  gfx::Size label_width = button()->label()->GetPreferredSize(
      views::SizeBounds(text_wrap_width, {}));
  EXPECT_EQ(preferred_size.width(),
            unclamped_size_without_label + label_width.width());
  EXPECT_EQ(preferred_size.height(),
            font_list.GetHeight() * 2 + button_insets.height());

  // The image should be centered on the first line of the multi-line label
  EXPECT_EQ(
      button()->image_container_view()->y(),
      (font_list.GetHeight() - button()->image_container_view()->height()) / 2 +
          button_insets.top());
}

// This test was added because GetHeightForWidth and GetPreferredSize were
// inconsistent. GetPreferredSize would account for image size + insets whereas
// GetHeightForWidth wouldn't. As of writing they share a large chunk of
// logic, but this remains in place so they don't diverge as easily.
TEST_F(LabelButtonTest, GetHeightForWidthConsistentWithGetPreferredSize) {
  const std::u16string text(u"abcdefghijklm");
  constexpr int kTinyImageSize = 2;
  constexpr int kLargeImageSize = 50;
  const int font_height = button()->label()->font_list().GetHeight();
  // Parts of this test (accounting for label height) doesn't make sense if the
  // font is smaller than the tiny test image and insets.
  ASSERT_GT(font_height, button()->GetInsets().height() + kTinyImageSize);
  // Parts of this test (accounting for image insets) doesn't make sense if the
  // font is larger than the large test image.
  ASSERT_LT(font_height, kLargeImageSize);
  button()->SetText(text);

  for (int image_size : {kTinyImageSize, kLargeImageSize}) {
    SCOPED_TRACE(testing::Message() << "Image Size: " << image_size);
    // Set image and reset monotonic min size for every test iteration.
    const gfx::ImageSkia image = gfx::test::CreateImageSkia(image_size);
    button()->SetImageModel(Button::STATE_NORMAL,
                            ui::ImageModel::FromImageSkia(image));

    const gfx::Size preferred_button_size = button()->GetPreferredSize({});

    // The preferred button height should be the larger of image / label
    // heights + inset height.
    EXPECT_EQ(
        std::max(image_size, font_height) + button()->GetInsets().height(),
        preferred_button_size.height());

    // Make sure this preferred height is consistent with GetHeightForWidth().
    EXPECT_EQ(preferred_button_size.height(),
              button()->GetHeightForWidth(preferred_button_size.width()));
  }
}

// Ensure that the text used for button labels correctly adjusts in response
// to provided style::TextContext values.
TEST_F(LabelButtonTest, TextSizeFromContext) {
  constexpr style::TextContext kDefaultContext = style::CONTEXT_BUTTON;

  // Although CONTEXT_DIALOG_TITLE isn't used for buttons, picking a style with
  // a small delta risks finding a font with a different point-size but with the
  // same maximum glyph height.
  constexpr style::TextContext kAlternateContext = style::CONTEXT_DIALOG_TITLE;

  // First sanity that the TextConstants used in the test give different sizes.
  const auto get_delta = [](auto context) {
    return TypographyProvider()
               .GetFont(context, style::STYLE_PRIMARY)
               .GetFontSize() -
           gfx::FontList().GetFontSize();
  };
  TypographyProvider typography_provider;
  int default_delta = get_delta(kDefaultContext);
  int alternate_delta = get_delta(kAlternateContext);
  EXPECT_LT(default_delta, alternate_delta);

  const std::u16string text(u"abcdefghijklm");
  button()->SetText(text);
  EXPECT_EQ(default_delta, button()->label()->font_list().GetFontSize() -
                               gfx::FontList().GetFontSize());

  TestLabelButton* alternate_button =
      new TestLabelButton(text, kAlternateContext);
  button()->parent()->AddChildViewRaw(alternate_button);
  EXPECT_EQ(alternate_delta,
            alternate_button->label()->font_list().GetFontSize() -
                gfx::FontList().GetFontSize());

  // The button size increases when the font size is increased.
  EXPECT_LT(button()->GetPreferredSize({}).width(),
            alternate_button->GetPreferredSize({}).width());
  EXPECT_LT(button()->GetPreferredSize({}).height(),
            alternate_button->GetPreferredSize({}).height());
}

TEST_F(LabelButtonTest, ChangeTextSize) {
  const std::u16string text(u"abc");
  const std::u16string longer_text(u"abcdefghijklm");
  button()->SetText(text);
  button()->SizeToPreferredSize();
  gfx::Rect bounds(button()->bounds());
  const int original_width = button()->GetPreferredSize({}).width();
  EXPECT_EQ(original_width, bounds.width());

  // Reserve more space in the button.
  bounds.set_width(bounds.width() * 10);
  button()->SetBoundsRect(bounds);

  // Label view in the button is sized to short text.
  const int original_label_width = button()->label()->bounds().width();

  // The button preferred size and the label size increase when the text size
  // is increased.
  button()->SetText(longer_text);
  EXPECT_TRUE(ViewTestApi(button()).needs_layout());
  views::test::RunScheduledLayout(button());
  EXPECT_GT(button()->label()->bounds().width(), original_label_width * 2);
  EXPECT_GT(button()->GetPreferredSize({}).width(), original_width * 2);

  // The button and the label view return to its original size when the original
  // text is restored.
  button()->SetText(text);
  EXPECT_TRUE(ViewTestApi(button()).needs_layout());
  views::test::RunScheduledLayout(button());
  EXPECT_EQ(original_label_width, button()->label()->bounds().width());
  EXPECT_EQ(original_width, button()->GetPreferredSize({}).width());
}

TEST_F(LabelButtonTest, ChangeLabelImageSpacing) {
  button()->SetText(u"abc");
  button()->SetImageModel(
      Button::STATE_NORMAL,
      ui::ImageModel::FromImageSkia(gfx::test::CreateImageSkia(/*size=*/50)));

  const int kOriginalSpacing = 5;
  button()->SetImageLabelSpacing(kOriginalSpacing);
  const int original_width = button()->GetPreferredSize({}).width();

  // Increasing the spacing between the text and label should increase the size.
  button()->SetImageLabelSpacing(2 * kOriginalSpacing);
  EXPECT_GT(button()->GetPreferredSize({}).width(), original_width);

  // The button shrinks if the original spacing is restored.
  button()->SetImageLabelSpacing(kOriginalSpacing);
  EXPECT_EQ(original_width, button()->GetPreferredSize({}).width());
}

// Ensure the label gets the correct style when pressed or becoming default.
TEST_F(LabelButtonTest, HighlightedButtonStyle) {
  // The ColorProvider might not provide SK_ColorBLACK, but it should be the
  // same for normal and pressed states.
  EXPECT_EQ(themed_normal_text_color_, button()->label()->GetEnabledColor());
  button()->SetState(Button::STATE_PRESSED);
  EXPECT_EQ(themed_normal_text_color_, button()->label()->GetEnabledColor());
}

// Ensure the label resets the enabled color after LabelButton::OnThemeChanged()
// is invoked.
TEST_F(LabelButtonTest, OnThemeChanged) {
  ASSERT_NE(button()->label()->GetBackgroundColor(), SK_ColorBLACK);
  EXPECT_EQ(themed_normal_text_color_, button()->label()->GetEnabledColor());

  button()->label()->SetBackgroundColor(SK_ColorBLACK);
  button()->label()->SetAutoColorReadabilityEnabled(true);
  EXPECT_NE(themed_normal_text_color_, button()->label()->GetEnabledColor());

  button()->OnThemeChanged();
  EXPECT_EQ(themed_normal_text_color_, button()->label()->GetEnabledColor());
}

TEST_F(LabelButtonTest, SetEnabledTextColorsResetsToThemeColors) {
  constexpr SkColor kReplacementColor = SK_ColorCYAN;

  // This test doesn't make sense if the used colors are equal.
  EXPECT_NE(themed_normal_text_color_, kReplacementColor);

  // Initially the test should have the normal colors.
  EXPECT_EQ(themed_normal_text_color_, button()->label()->GetEnabledColor());

  // Setting the enabled text colors should replace the label's enabled color.
  button()->SetEnabledTextColors(kReplacementColor);
  EXPECT_EQ(kReplacementColor, button()->label()->GetEnabledColor());

  // Toggle dark mode. This should not replace the enabled text color as it's
  // been manually overridden above.
  os_settings_provider().SetPreferredColorScheme(
      ui::NativeTheme::PreferredColorScheme::kDark);
  EXPECT_EQ(kReplacementColor, button()->label()->GetEnabledColor());

  // Removing the enabled text color restore colors from the new theme, not
  // the original colors used before the theme changed.
  button()->SetEnabledTextColors(std::nullopt);
  EXPECT_NE(themed_normal_text_color_, button()->label()->GetEnabledColor());
}

TEST_F(LabelButtonTest, SetEnabledTextColorIds) {
  ASSERT_NE(ui::kColorLabelForeground, ui::kColorAccent);

  // Initially the test should have the normal colors.
  EXPECT_EQ(button()->label()->GetRequestedEnabledColor(),
            ui::kColorLabelForeground);

  // Setting the enabled text colors should replace the label's enabled color.
  button()->SetEnabledTextColors(ui::kColorAccent);
  EXPECT_EQ(button()->label()->GetRequestedEnabledColor(), ui::kColorAccent);

  // Toggle dark mode. This should not replace the enabled text color as it's
  // been manually overridden above.
  os_settings_provider().SetPreferredColorScheme(
      ui::NativeTheme::PreferredColorScheme::kDark);
  EXPECT_EQ(button()->label()->GetRequestedEnabledColor(), ui::kColorAccent);
  EXPECT_EQ(button()->label()->GetEnabledColor(),
            button()->GetColorProvider()->GetColor(ui::kColorAccent));
}

TEST_F(LabelButtonTest, ImageOrLabelGetClipped) {
  const std::u16string text(u"abc");
  button()->SetText(text);

  const gfx::FontList font_list = button()->label()->font_list();
  const int image_size = font_list.GetHeight();
  button()->SetImageModel(
      Button::STATE_NORMAL,
      ui::ImageModel::FromImageSkia(gfx::test::CreateImageSkia(image_size)));

  button()->SetBoundsRect(gfx::Rect(button()->GetPreferredSize({})));
  // The border size + the content height is more than button's preferred size.
  button()->SetBorder(CreateEmptyBorder(
      gfx::Insets::TLBR(image_size / 2, 0, image_size / 2, 0)));
  views::test::RunScheduledLayout(button());

  // Ensure that content (image and label) doesn't get clipped by the border.
  EXPECT_GE(button()->image_container_view()->height(), image_size);
  EXPECT_GE(button()->label()->height(), image_size);
}

TEST_F(LabelButtonTest, UpdateImageAfterSettingImageModel) {
  auto is_showing_image = [&](const gfx::ImageSkia& image) {
    const auto* image_view =
        AsViewClass<ImageView>(button()->image_container_view());
    CHECK(image_view);
    return image_view->GetImage().BackedBySameObjectAs(image);
  };

  auto normal_image = gfx::test::CreateImageSkia(/*size=*/16);
  button()->SetImageModel(Button::STATE_NORMAL,
                          ui::ImageModel::FromImageSkia(normal_image));
  EXPECT_TRUE(is_showing_image(normal_image));

  // When the button has no specific disabled image, changing the normal image
  // while the button is disabled should update the currently-visible image.
  normal_image = gfx::test::CreateImageSkia(/*size=*/16);
  button()->SetState(Button::STATE_DISABLED);
  button()->SetImageModel(Button::STATE_NORMAL,
                          ui::ImageModel::FromImageSkia(normal_image));
  EXPECT_TRUE(is_showing_image(normal_image));

  // Any specific disabled image should take precedence over the normal image.
  auto disabled_image = gfx::test::CreateImageSkia(/*size=*/16);
  button()->SetImageModel(Button::STATE_DISABLED,
                          ui::ImageModel::FromImageSkia(disabled_image));
  EXPECT_TRUE(is_showing_image(disabled_image));

  // An empty disabled image should take precedence over the normal image.
  auto disabled_empty_image = gfx::test::CreateImageSkia(0);
  button()->SetImageModel(Button::STATE_DISABLED,
                          ui::ImageModel::FromImageSkia(disabled_empty_image));
  EXPECT_TRUE(is_showing_image(disabled_empty_image));

  // Removing the disabled image should result in falling back to the normal
  // image again.
  button()->SetImageModel(Button::STATE_DISABLED, std::nullopt);
  EXPECT_TRUE(is_showing_image(normal_image));
}

TEST_F(LabelButtonTest, AccessibiltyDefaultState) {
  ui::AXNodeData node_data = ui::AXNodeData();
  /// Initially, kDefault should be set to false.
  EXPECT_FALSE(node_data.HasState(ax::mojom::State::kDefault));

  button()->SetIsDefault(true);
  button()->GetViewAccessibility().GetAccessibleNodeData(&node_data);
  EXPECT_TRUE(node_data.HasState(ax::mojom::State::kDefault));

  node_data = ui::AXNodeData();  // Reset the node data.
  button()->SetIsDefault(false);
  button()->GetViewAccessibility().GetAccessibleNodeData(&node_data);
  EXPECT_FALSE(node_data.HasState(ax::mojom::State::kDefault));
}

// Test fixture for a LabelButton that has an ink drop configured.
class InkDropLabelButtonTest : public ViewsTestBase {
 public:
  InkDropLabelButtonTest() = default;

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

  // ViewsTestBase:
  void SetUp() override {
    ViewsTestBase::SetUp();

    // Create a widget so that the Button can query the hover state
    // correctly.
    widget_ = std::make_unique<Widget>();
    Widget::InitParams params = CreateParams(
        Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_POPUP);
    params.bounds = gfx::Rect(0, 0, 20, 20);
    widget_->Init(std::move(params));
    widget_->Show();

    auto* button = widget_->SetContentsView(std::make_unique<LabelButton>(
        Button::PressedCallback(), std::u16string()));

    auto* test_ink_drop = new test::TestInkDrop();
    test::InkDropHostTestApi(InkDrop::Get(button))
        .SetInkDrop(base::WrapUnique(test_ink_drop));
  }

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

 protected:
  LabelButton* button() {
    return static_cast<LabelButton*>(widget_->GetContentsView());
  }

  test::TestInkDrop* test_ink_drop() {
    return static_cast<test::TestInkDrop*>(
        InkDrop::Get(button())->GetInkDrop());
  }

  // Required to host the test target.
  std::unique_ptr<Widget> widget_;
};

TEST_F(InkDropLabelButtonTest, HoverStateAfterMouseEnterAndExitEvents) {
  ui::test::EventGenerator event_generator(GetRootWindow(widget_.get()));
  const gfx::Point out_of_bounds_point(
      button()->GetBoundsInScreen().bottom_right() + gfx::Vector2d(1, 1));
  const gfx::Point in_bounds_point(button()->GetBoundsInScreen().CenterPoint());

  event_generator.MoveMouseTo(out_of_bounds_point);
  EXPECT_FALSE(test_ink_drop()->is_hovered());

  event_generator.MoveMouseTo(in_bounds_point);
  EXPECT_TRUE(test_ink_drop()->is_hovered());

  event_generator.MoveMouseTo(out_of_bounds_point);
  EXPECT_FALSE(test_ink_drop()->is_hovered());
}

// Verifies the target event handler View is the |LabelButton| and not any of
// the child Views.
TEST_F(InkDropLabelButtonTest, TargetEventHandler) {
  View* target_view = widget_->GetRootView()->GetEventHandlerForPoint(
      button()->bounds().CenterPoint());
  EXPECT_EQ(button(), target_view);
}

class LabelButtonVisualStateTest : public test::WidgetTest {
 public:
  LabelButtonVisualStateTest() = default;
  LabelButtonVisualStateTest(const LabelButtonVisualStateTest&) = delete;
  LabelButtonVisualStateTest& operator=(const LabelButtonVisualStateTest&) =
      delete;

  // testing::Test:
  void SetUp() override {
    WidgetTest::SetUp();
    test_widget_ = CreateTopLevelPlatformWidget();
    dummy_widget_ = CreateTopLevelPlatformWidget();

    MakeButtonAsContent(test_widget_)->SetID(1);
  }

  void TearDown() override {
    test_widget_.ExtractAsDangling()->CloseNow();
    dummy_widget_.ExtractAsDangling()->CloseNow();
    WidgetTest::TearDown();
  }

 protected:
  std::unique_ptr<Widget> CreateActivatableChildWidget(Widget* parent) {
    auto child = std::make_unique<Widget>();
    Widget::InitParams params = CreateParams(
        Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_POPUP);
    params.parent = parent->GetNativeView();
    params.activatable = Widget::InitParams::Activatable::kYes;
    child->Init(std::move(params));
    child->SetContentsView(std::make_unique<View>());
    return child;
  }

  TestLabelButton* MakeButtonAsContent(Widget* widget) {
    return widget->GetContentsView()->AddChildView(
        std::make_unique<TestLabelButton>());
  }

  TestLabelButton* button() {
    return static_cast<TestLabelButton*>(
        test_widget_->GetContentsView()->GetViewByID(1));
  }

  raw_ptr<Widget> test_widget_ = nullptr;
  raw_ptr<Widget> dummy_widget_ = nullptr;
  static constexpr Button::ButtonState style_of_inactive_widget_ =
      PlatformStyle::kInactiveWidgetControlsAppearDisabled
          ? Button::STATE_DISABLED
          : Button::STATE_NORMAL;
};

TEST_F(LabelButtonVisualStateTest, IndependentWidget) {
  test_widget_->ShowInactive();
  EXPECT_EQ(button()->GetVisualState(), style_of_inactive_widget_);

  test_widget_->Activate();
  EXPECT_EQ(button()->GetVisualState(), Button::STATE_NORMAL);

  auto paint_as_active_lock = test_widget_->LockPaintAsActive();
  dummy_widget_->Show();
  EXPECT_EQ(button()->GetVisualState(), Button::STATE_NORMAL);
}

TEST_F(LabelButtonVisualStateTest, ChildWidget) {
  std::unique_ptr<Widget> child_widget =
      CreateActivatableChildWidget(test_widget_);
  TestLabelButton* child_button = MakeButtonAsContent(child_widget.get());

  test_widget_->Show();
  EXPECT_EQ(button()->GetVisualState(), Button::STATE_NORMAL);
  EXPECT_EQ(child_button->GetVisualState(), Button::STATE_NORMAL);

  dummy_widget_->Show();
  EXPECT_EQ(button()->GetVisualState(), style_of_inactive_widget_);
  EXPECT_EQ(child_button->GetVisualState(), style_of_inactive_widget_);

  child_widget->Show();
#if BUILDFLAG(IS_MAC)
  // Child widget is in a key window and it will lock its parent.
  // See crrev.com/c/2048144.
  EXPECT_EQ(button()->GetVisualState(), Button::STATE_NORMAL);
#else
  EXPECT_EQ(button()->GetVisualState(), style_of_inactive_widget_);
#endif
  EXPECT_EQ(child_button->GetVisualState(), Button::STATE_NORMAL);
}

using LabelButtonActionViewInterfaceTest = ViewsTestBase;

TEST_F(LabelButtonActionViewInterfaceTest, TestActionChanged) {
  auto label_button = std::make_unique<LabelButton>();
  const std::u16string test_string = u"test_string";
  std::unique_ptr<actions::ActionItem> action_item =
      actions::ActionItem::Builder()
          .SetText(test_string)
          .SetActionId(0)
          .SetEnabled(false)
          .Build();
  label_button->GetActionViewInterface()->ActionItemChangedImpl(
      action_item.get());
  // Test some properties to ensure that the right ActionViewInterface is linked
  // to the view.
  EXPECT_EQ(test_string, label_button->GetText());
  EXPECT_FALSE(label_button->GetEnabled());
}

}  // namespace views
