// Copyright 2013 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/styled_label.h"

#include <stddef.h>

#include <memory>
#include <string>
#include <utility>

#include "base/command_line.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/i18n/base_i18n_switches.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/icu_test_util.h"
#include "build/build_config.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/ui_base_switches.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/text_constants.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/border.h"
#include "ui/views/controls/link.h"
#include "ui/views/controls/link_fragment.h"
#include "ui/views/style/typography.h"
#include "ui/views/test/test_layout_provider.h"
#include "ui/views/test/test_views.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/test/views_test_utils.h"
#include "ui/views/widget/widget.h"

using base::ASCIIToUTF16;

namespace views {

namespace {

using ::testing::SizeIs;

Label* LabelAt(StyledLabel* styled,
               size_t index,
               std::string expected_classname = Label::kViewClassName) {
  View* const child = styled->children()[index];
  EXPECT_EQ(expected_classname, child->GetClassName());
  return static_cast<Label*>(child);
}

int StyledLabelContentHeightForWidth(StyledLabel* styled, int w) {
  return styled->GetHeightForWidth(w) - styled->GetInsets().height();
}

}  // namespace

class StyledLabelTest : public ViewsTestBase {
 public:
  StyledLabelTest() = default;
  StyledLabelTest(const StyledLabelTest&) = delete;
  StyledLabelTest& operator=(const StyledLabelTest&) = delete;
  ~StyledLabelTest() override = default;

 protected:
  StyledLabel* InitStyledLabel(const std::string& ascii_text) {
    styled_ = std::make_unique<StyledLabel>();
    styled_->SetText(ASCIIToUTF16(ascii_text));
    return styled_.get();
  }

 private:
  std::unique_ptr<StyledLabel> styled_;
};

class StyledLabelInWidgetTest : public ViewsTestBase {
 public:
  StyledLabelInWidgetTest() = default;
  StyledLabelInWidgetTest(const StyledLabelInWidgetTest&) = delete;
  StyledLabelInWidgetTest& operator=(const StyledLabelInWidgetTest&) = delete;
  ~StyledLabelInWidgetTest() override = default;

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

    widget_ = CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET);
  }

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

  ui::MockOsSettingsProvider& os_settings_provider() {
    return os_settings_provider_;
  }
  Widget* widget() const { return widget_.get(); }

  StyledLabel* InitStyledLabel(const std::string& ascii_text) {
    View* container = widget_->SetContentsView(std::make_unique<View>());
    StyledLabel* styled =
        container->AddChildView(std::make_unique<StyledLabel>());
    styled->SetText(ASCIIToUTF16(ascii_text));
    return styled;
  }

 private:
  ui::MockOsSettingsProvider os_settings_provider_;
  std::unique_ptr<Widget> widget_;
};

TEST_F(StyledLabelTest, NoWrapping) {
  const std::string text("This is a test block of text");
  StyledLabel* styled = InitStyledLabel(text);
  Label label(ASCIIToUTF16(text));
  const gfx::Size label_preferred_size = label.GetPreferredSize({});
  EXPECT_EQ(label_preferred_size.height(),
            StyledLabelContentHeightForWidth(styled,
                                             label_preferred_size.width() * 2));
}

TEST_F(StyledLabelTest, TrailingWhitespaceiIgnored) {
  const std::string text("This is a test block of text   ");
  StyledLabel* styled = InitStyledLabel(text);

  styled->SetBounds(0, 0, 1000, 1000);
  test::RunScheduledLayout(styled);

  ASSERT_EQ(1u, styled->children().size());
  EXPECT_EQ(u"This is a test block of text", LabelAt(styled, 0)->GetText());
}

TEST_F(StyledLabelTest, RespectLeadingWhitespace) {
  const std::string text("   This is a test block of text");
  StyledLabel* styled = InitStyledLabel(text);

  styled->SetBounds(0, 0, 1000, 1000);
  test::RunScheduledLayout(styled);

  ASSERT_EQ(1u, styled->children().size());
  EXPECT_EQ(u"   This is a test block of text", LabelAt(styled, 0)->GetText());
}

TEST_F(StyledLabelTest, RespectLeadingSpacesInNonFirstLine) {
  const std::string indented_line = "  indented line";
  const std::string text(std::string("First line\n") + indented_line);
  StyledLabel* styled = InitStyledLabel(text);
  styled->SetBounds(0, 0, 1000, 1000);
  test::RunScheduledLayout(styled);
  ASSERT_EQ(2u, styled->children().size());
  EXPECT_EQ(ASCIIToUTF16(indented_line), LabelAt(styled, 1)->GetText());
}

TEST_F(StyledLabelTest, CorrectWrapAtNewline) {
  const std::string first_line = "Line one";
  const std::string second_line = "  two";
  const std::string multiline_text(first_line + "\n" + second_line);
  StyledLabel* styled = InitStyledLabel(multiline_text);
  Label label(ASCIIToUTF16(first_line));
  gfx::Size label_preferred_size = label.GetPreferredSize({});
  // Correct handling of \n and label width limit encountered at the same place
  styled->SetBounds(0, 0, label_preferred_size.width(), 1000);
  test::RunScheduledLayout(styled);
  ASSERT_EQ(2u, styled->children().size());
  EXPECT_EQ(ASCIIToUTF16(first_line), LabelAt(styled, 0)->GetText());
  const auto* label_1 = LabelAt(styled, 1);
  EXPECT_EQ(ASCIIToUTF16(second_line), label_1->GetText());
  EXPECT_EQ(styled->GetHeightForWidth(1000), label_1->bounds().bottom());
}

TEST_F(StyledLabelTest, FirstLineNotEmptyWhenLeadingWhitespaceTooLong) {
  const std::string text("                                     a");
  StyledLabel* styled = InitStyledLabel(text);

  Label label(ASCIIToUTF16(text));
  gfx::Size label_preferred_size = label.GetPreferredSize({});

  styled->SetBounds(0, 0, label_preferred_size.width() / 2, 1000);
  test::RunScheduledLayout(styled);

  ASSERT_EQ(1u, styled->children().size());
  EXPECT_EQ(u"a", LabelAt(styled, 0)->GetText());
  EXPECT_EQ(label_preferred_size.height(),
            styled->GetHeightForWidth(label_preferred_size.width() / 2));
}

TEST_F(StyledLabelTest, BasicWrapping) {
  const std::string text("This is a test block of text");
  StyledLabel* styled = InitStyledLabel(text);
  Label label(ASCIIToUTF16(text.substr(0, text.size() * 2 / 3)));
  gfx::Size label_preferred_size = label.GetPreferredSize({});
  EXPECT_EQ(
      label_preferred_size.height() * 2,
      StyledLabelContentHeightForWidth(styled, label_preferred_size.width()));

  // Also respect the border.
  styled->SetBorder(CreateEmptyBorder(3));
  styled->SetBounds(
      0, 0, styled->GetInsets().width() + label_preferred_size.width(),
      styled->GetInsets().height() + 2 * label_preferred_size.height());
  test::RunScheduledLayout(styled);
  ASSERT_EQ(2u, styled->children().size());
  EXPECT_EQ(3, styled->children()[0]->x());
  EXPECT_EQ(3, styled->children()[0]->y());
  EXPECT_EQ(styled->height() - 3, styled->children()[1]->bounds().bottom());
}

TEST_F(StyledLabelTest, AllowEmptyLines) {
  const std::string text("one");
  StyledLabel* styled = InitStyledLabel(text);
  int default_height = styled->GetHeightForWidth(1000);
  const std::string multiline_text("one\n\nthree");
  styled = InitStyledLabel(multiline_text);
  styled->SetBounds(0, 0, 1000, 1000);
  test::RunScheduledLayout(styled);
  EXPECT_EQ(3 * default_height, styled->GetHeightForWidth(1000));
  ASSERT_EQ(2u, styled->children().size());
  EXPECT_EQ(styled->GetHeightForWidth(1000),
            styled->children()[1]->bounds().bottom());
}

TEST_F(StyledLabelTest, WrapLongWords) {
  const std::string text("ThisIsTextAsASingleWord");
  StyledLabel* styled = InitStyledLabel(text);
  Label label(ASCIIToUTF16(text.substr(0, text.size() * 2 / 3)));
  gfx::Size label_preferred_size = label.GetPreferredSize({});
  EXPECT_EQ(
      label_preferred_size.height() * 2,
      StyledLabelContentHeightForWidth(styled, label_preferred_size.width()));

  styled->SetBounds(
      0, 0, styled->GetInsets().width() + label_preferred_size.width(),
      styled->GetInsets().height() + 2 * label_preferred_size.height());
  test::RunScheduledLayout(styled);

  ASSERT_EQ(2u, styled->children().size());
  ASSERT_EQ(gfx::Point(), styled->origin());
  const auto* label_0 = LabelAt(styled, 0);
  const auto* label_1 = LabelAt(styled, 1);
  EXPECT_EQ(gfx::Point(), label_0->origin());
  EXPECT_EQ(gfx::Point(0, styled->height() / 2), label_1->origin());

  EXPECT_FALSE(label_0->GetText().empty());
  EXPECT_FALSE(label_1->GetText().empty());
  EXPECT_EQ(ASCIIToUTF16(text),
            base::StrCat({label_0->GetText(), label_1->GetText()}));
}

TEST_F(StyledLabelTest, CreateLinks) {
  const std::string text("This is a test block of text.");
  StyledLabel* styled = InitStyledLabel(text);

  // Without links, there should be no focus border.
  EXPECT_TRUE(styled->GetInsets().IsEmpty());

  // Now let's add some links.
  styled->AddStyleRange(
      gfx::Range(0, 1),
      StyledLabel::RangeStyleInfo::CreateForLink(base::RepeatingClosure()));
  styled->AddStyleRange(
      gfx::Range(1, 2),
      StyledLabel::RangeStyleInfo::CreateForLink(base::RepeatingClosure()));
  styled->AddStyleRange(
      gfx::Range(10, 11),
      StyledLabel::RangeStyleInfo::CreateForLink(base::RepeatingClosure()));
  styled->AddStyleRange(
      gfx::Range(12, 13),
      StyledLabel::RangeStyleInfo::CreateForLink(base::RepeatingClosure()));

  // Insets shouldn't change when links are added, since the links indicate
  // focus by adding an underline instead.
  EXPECT_TRUE(styled->GetInsets().IsEmpty());

  // Verify layout creates the right number of children.
  styled->SetBounds(0, 0, 1000, 1000);
  test::RunScheduledLayout(styled);
  EXPECT_EQ(7u, styled->children().size());
}

TEST_F(StyledLabelTest, StyledRangeCustomFontUnderlined) {
  const std::string text("This is a test block of text, ");
  const std::string underlined_text("and this should be undelined");
  StyledLabel* styled = InitStyledLabel(text + underlined_text);
  StyledLabel::RangeStyleInfo style_info;
  style_info.tooltip = u"tooltip";
  style_info.custom_font =
      styled->GetFontList().DeriveWithStyle(gfx::Font::UNDERLINE);
  styled->AddStyleRange(
      gfx::Range(text.size(), text.size() + underlined_text.size()),
      style_info);

  styled->SetBounds(0, 0, 1000, 1000);
  test::RunScheduledLayout(styled);

  ASSERT_EQ(2u, styled->children().size());
  EXPECT_EQ(gfx::Font::UNDERLINE,
            LabelAt(styled, 1)->font_list().GetFontStyle());
}

TEST_F(StyledLabelTest, StyledRangeTextStyleBold) {
  test::TestLayoutProvider bold_provider;
  const std::string bold_text(
      "This is a block of text whose style will be set to BOLD in the test");
  const std::string text(" normal text");
  StyledLabel* styled = InitStyledLabel(bold_text + text);

  // Pretend disabled text becomes bold for testing.
  auto details =
      bold_provider.GetFontDetails(style::CONTEXT_LABEL, style::STYLE_DISABLED);
  details.weight = gfx::Font::Weight::BOLD;
  bold_provider.SetFontDetails(style::CONTEXT_LABEL, style::STYLE_DISABLED,
                               details);

  StyledLabel::RangeStyleInfo style_info;
  style_info.text_style = style::STYLE_DISABLED;
  styled->AddStyleRange(gfx::Range(0u, bold_text.size()), style_info);

  // Calculate the bold text width if it were a pure label view, both with bold
  // and normal style.
  Label label(ASCIIToUTF16(bold_text));
  const gfx::Size normal_label_size = label.GetPreferredSize({});
  label.SetFontList(
      label.font_list().DeriveWithWeight(gfx::Font::Weight::BOLD));
  const gfx::Size bold_label_size = label.GetPreferredSize({});

  ASSERT_GE(bold_label_size.width(), normal_label_size.width());

  // Set the width so |bold_text| doesn't fit on a single line with bold style,
  // but does with normal font style.
  int styled_width = (normal_label_size.width() + bold_label_size.width()) / 2;
  int pref_height = styled->GetHeightForWidth(styled_width);

  // Sanity check that |bold_text| with normal font style would fit on a single
  // line in a styled label with width |styled_width|.
  StyledLabel unstyled;
  unstyled.SetText(ASCIIToUTF16(bold_text));
  unstyled.SetBounds(0, 0, styled_width, pref_height);
  test::RunScheduledLayout(&unstyled);
  EXPECT_EQ(1u, unstyled.children().size());

  styled->SetBounds(0, 0, styled_width, pref_height);
  test::RunScheduledLayout(styled);

  ASSERT_EQ(3u, styled->children().size());

  // The bold text should be broken up into two parts.
  const auto* label_0 = LabelAt(styled, 0);
  const auto* label_1 = LabelAt(styled, 1);
  const auto* label_2 = LabelAt(styled, 2);
  EXPECT_EQ(gfx::Font::Weight::BOLD, label_0->font_list().GetFontWeight());
  EXPECT_EQ(gfx::Font::Weight::BOLD, label_1->font_list().GetFontWeight());
  EXPECT_EQ(gfx::Font::NORMAL, label_2->font_list().GetFontStyle());

  // The second bold part should start on a new line.
  EXPECT_EQ(0, label_0->x());
  EXPECT_EQ(0, label_1->x());
  EXPECT_EQ(label_1->bounds().right(), label_2->x());
}

TEST_F(StyledLabelInWidgetTest, Color) {
  const std::string text_blue("BLUE");
  const std::string text_link("link");
  const std::string text("word");
  StyledLabel* styled = InitStyledLabel(text_blue + text_link + text);

  StyledLabel::RangeStyleInfo style_info_blue;
  style_info_blue.override_color = SK_ColorBLUE;
  styled->AddStyleRange(gfx::Range(0u, text_blue.size()), style_info_blue);

  StyledLabel::RangeStyleInfo style_info_link =
      StyledLabel::RangeStyleInfo::CreateForLink(base::RepeatingClosure());
  styled->AddStyleRange(
      gfx::Range(text_blue.size(), text_blue.size() + text_link.size()),
      style_info_link);

  styled->SetBounds(0, 0, 1000, 1000);
  test::RunScheduledLayout(styled);

  auto* container = widget()->GetContentsView();
  // Obtain the default text color for a label.
  Label* label =
      container->AddChildView(std::make_unique<Label>(ASCIIToUTF16(text)));
  const SkColor kDefaultTextColor = label->GetEnabledColor();

  // Obtain the default text color for a link.
  Link* link =
      container->AddChildView(std::make_unique<Link>(ASCIIToUTF16(text_link)));
  const SkColor kDefaultLinkColor = link->GetEnabledColor();

  ASSERT_EQ(3u, styled->children().size());
  EXPECT_EQ(SK_ColorBLUE, LabelAt(styled, 0)->GetEnabledColor());
  EXPECT_EQ(
      kDefaultLinkColor,
      LabelAt(styled, 1, LinkFragment::kViewClassName)->GetEnabledColor());
  EXPECT_EQ(kDefaultTextColor, LabelAt(styled, 2)->GetEnabledColor());

  // Test adjusted color readability.
  styled->SetDisplayedOnBackgroundColor(SK_ColorBLACK);
  test::RunScheduledLayout(styled);
  label->SetBackgroundColor(SK_ColorBLACK);

  const SkColor kAdjustedTextColor = label->GetEnabledColor();
  EXPECT_NE(kAdjustedTextColor, kDefaultTextColor);
  EXPECT_EQ(kAdjustedTextColor, LabelAt(styled, 2)->GetEnabledColor());
}

TEST_F(StyledLabelInWidgetTest, SetBackgroundColor) {
  StyledLabel* styled = InitStyledLabel("test label");

  styled->SetBounds(0, 0, 1000, 1000);
  test::RunScheduledLayout(styled);

  ASSERT_THAT(styled->children(), SizeIs(1u));
  // The default background color is `ui::kColorDialogBackground`.
  EXPECT_EQ(widget()->GetColorProvider()->GetColor(ui::kColorDialogBackground),
            LabelAt(styled, 0)->GetBackgroundColor());

  styled->SetDisplayedOnBackgroundColor(SK_ColorBLUE);
  EXPECT_EQ(SK_ColorBLUE, LabelAt(styled, 0)->GetBackgroundColor());

  styled->SetDisplayedOnBackgroundColor(ui::kColorAlertHighSeverity);
  EXPECT_EQ(widget()->GetColorProvider()->GetColor(ui::kColorAlertHighSeverity),
            LabelAt(styled, 0)->GetBackgroundColor());

  // Setting a color overwrites the color id.
  styled->SetDisplayedOnBackgroundColor(SK_ColorCYAN);
  EXPECT_EQ(SK_ColorCYAN, LabelAt(styled, 0)->GetBackgroundColor());
}

TEST_F(StyledLabelInWidgetTest, SetBackgroundColorIdReactsToThemeChange) {
  StyledLabel* styled = InitStyledLabel("test label");

  styled->SetBounds(0, 0, 1000, 1000);
  test::RunScheduledLayout(styled);

  ASSERT_THAT(styled->children(), SizeIs(1u));
  os_settings_provider().SetPreferredColorScheme(
      ui::NativeTheme::PreferredColorScheme::kDark);
  EXPECT_EQ(widget()->GetColorProvider()->GetColor(ui::kColorDialogBackground),
            LabelAt(styled, 0)->GetBackgroundColor());

  os_settings_provider().SetPreferredColorScheme(
      ui::NativeTheme::PreferredColorScheme::kLight);
  EXPECT_EQ(widget()->GetColorProvider()->GetColor(ui::kColorDialogBackground),
            LabelAt(styled, 0)->GetBackgroundColor());

  styled->SetDisplayedOnBackgroundColor(ui::kColorAlertHighSeverity);
  EXPECT_EQ(widget()->GetColorProvider()->GetColor(ui::kColorAlertHighSeverity),
            LabelAt(styled, 0)->GetBackgroundColor());

  os_settings_provider().SetPreferredColorScheme(
      ui::NativeTheme::PreferredColorScheme::kDark);
  EXPECT_EQ(widget()->GetColorProvider()->GetColor(ui::kColorAlertHighSeverity),
            LabelAt(styled, 0)->GetBackgroundColor());
}

TEST_F(StyledLabelTest, StyledRangeWithTooltip) {
  const std::string text("This is a test block of text, ");
  const std::string tooltip_text("this should have a tooltip,");
  const std::string normal_text(" this should not have a tooltip, ");
  const std::string link_text("and this should be a link");

  const size_t tooltip_start = text.size();
  const size_t link_start =
      text.size() + tooltip_text.size() + normal_text.size();

  StyledLabel* styled =
      InitStyledLabel(text + tooltip_text + normal_text + link_text);
  StyledLabel::RangeStyleInfo tooltip_style;
  tooltip_style.tooltip = u"tooltip";
  styled->AddStyleRange(
      gfx::Range(tooltip_start, tooltip_start + tooltip_text.size()),
      tooltip_style);
  styled->AddStyleRange(
      gfx::Range(link_start, link_start + link_text.size()),
      StyledLabel::RangeStyleInfo::CreateForLink(base::RepeatingClosure()));

  // Break line inside the range with the tooltip.
  Label label(
      ASCIIToUTF16(text + tooltip_text.substr(0, tooltip_text.size() - 3)));
  gfx::Size label_preferred_size = label.GetPreferredSize({});
  int pref_height = styled->GetHeightForWidth(label_preferred_size.width());
  EXPECT_EQ(label_preferred_size.height() * 3,
            pref_height - styled->GetInsets().height());

  styled->SetBounds(0, 0, label_preferred_size.width(), pref_height);
  test::RunScheduledLayout(styled);

  EXPECT_EQ(label_preferred_size.width(), styled->width());

  ASSERT_EQ(6u, styled->children().size());

  // The labels shouldn't be offset to cater for focus rings.
  EXPECT_EQ(0, styled->children()[0]->x());
  EXPECT_EQ(0, styled->children()[2]->x());

  EXPECT_EQ(styled->children()[0]->bounds().right(),
            styled->children()[1]->x());
  EXPECT_EQ(styled->children()[2]->bounds().right(),
            styled->children()[3]->x());

  std::u16string tooltip =
      styled->children()[1]->GetRenderedTooltipText(gfx::Point(1, 1));
  EXPECT_EQ(u"tooltip", tooltip);
  tooltip = styled->children()[2]->GetRenderedTooltipText(gfx::Point(1, 1));
  EXPECT_EQ(u"tooltip", tooltip);
}

TEST_F(StyledLabelTest, SetTextContextAndDefaultStyle) {
  const std::string text("This is a test block of text.");
  StyledLabel* styled = InitStyledLabel(text);
  styled->SetTextContext(style::CONTEXT_DIALOG_TITLE);
  styled->SetDefaultTextStyle(style::STYLE_DISABLED);
  Label label(ASCIIToUTF16(text), style::CONTEXT_DIALOG_TITLE,
              style::STYLE_DISABLED);

  gfx::Size preferred_size = label.GetPreferredSize({});
  styled->SetBounds(0, 0, preferred_size.width(), preferred_size.height());

  // Make sure we have the same sizing as a label with the same style.
  EXPECT_EQ(preferred_size.height(), styled->height());
  EXPECT_EQ(preferred_size.width(), styled->width());

  test::RunScheduledLayout(styled);
  ASSERT_EQ(1u, styled->children().size());
  Label* sublabel = LabelAt(styled, 0);
  EXPECT_EQ(style::CONTEXT_DIALOG_TITLE, sublabel->GetTextContext());

  EXPECT_NE(SK_ColorBLACK, label.GetEnabledColor());  // Sanity check,
  EXPECT_EQ(label.GetEnabledColor(), sublabel->GetEnabledColor());
}

TEST_F(StyledLabelTest, LineHeight) {
  const std::string text("one\ntwo\nthree");
  StyledLabel* styled = InitStyledLabel(text);
  styled->SetLineHeight(18);
  EXPECT_EQ(18 * 3, styled->GetHeightForWidth(100));
}

TEST_F(StyledLabelTest, LineHeightWithBorder) {
  const std::string text("one\ntwo\nthree");
  StyledLabel* styled = InitStyledLabel(text);
  styled->SetLineHeight(18);
  styled->SetBorder(views::CreateSolidBorder(1, SK_ColorGRAY));
  EXPECT_EQ(18 * 3 + 2, styled->GetHeightForWidth(100));
}

TEST_F(StyledLabelTest, LineHeightWithLink) {
  const std::string text("one\ntwo\nthree");
  StyledLabel* styled = InitStyledLabel(text);
  styled->SetLineHeight(18);

  styled->AddStyleRange(
      gfx::Range(0, 3),
      StyledLabel::RangeStyleInfo::CreateForLink(base::RepeatingClosure()));
  styled->AddStyleRange(
      gfx::Range(4, 7),
      StyledLabel::RangeStyleInfo::CreateForLink(base::RepeatingClosure()));
  styled->AddStyleRange(
      gfx::Range(8, 13),
      StyledLabel::RangeStyleInfo::CreateForLink(base::RepeatingClosure()));
  EXPECT_EQ(18 * 3, styled->GetHeightForWidth(100));
}

TEST_F(StyledLabelTest, HandleEmptyLayout) {
  const std::string text("This is a test block of text.");
  StyledLabel* styled = InitStyledLabel(text);
  test::RunScheduledLayout(styled);
  EXPECT_EQ(0u, styled->children().size());
}

TEST_F(StyledLabelTest, CacheSize) {
  const int preferred_height = 50;
  const int preferred_width = 100;
  const std::string text("This is a test block of text.");
  const std::u16string another_text(
      u"This is a test block of text. This text is much longer than previous");

  StyledLabel* styled = InitStyledLabel(text);

  // we should be able to calculate height without any problem
  // no controls should be created
  int precalculated_height = styled->GetHeightForWidth(preferred_width);
  EXPECT_LT(0, precalculated_height);
  EXPECT_EQ(0u, styled->children().size());

  styled->SetBounds(0, 0, preferred_width, preferred_height);
  test::RunScheduledLayout(styled);

  // controls should be created after layout
  // height should be the same as precalculated
  int real_height = styled->GetHeightForWidth(styled->width());
  View* first_child_after_layout =
      styled->children().empty() ? nullptr : styled->children().front();
  EXPECT_LT(0u, styled->children().size());
  EXPECT_LT(0, real_height);
  EXPECT_EQ(real_height, precalculated_height);

  // another call to Layout should not kill and recreate all controls
  test::RunScheduledLayout(styled);
  View* first_child_after_second_layout =
      styled->children().empty() ? nullptr : styled->children().front();
  EXPECT_EQ(first_child_after_layout, first_child_after_second_layout);

  // if text is changed:
  // layout should be recalculated
  // all controls should be recreated
  styled->SetText(another_text);
  int updated_height = styled->GetHeightForWidth(styled->width());
  EXPECT_NE(updated_height, real_height);
  View* first_child_after_text_update =
      styled->children().empty() ? nullptr : styled->children().front();
  EXPECT_NE(first_child_after_text_update, first_child_after_layout);
}

TEST_F(StyledLabelTest, Border) {
  const std::string text("One line");
  StyledLabel* styled = InitStyledLabel(text);
  Label label(ASCIIToUTF16(text));
  gfx::Size label_preferred_size = label.GetPreferredSize({});
  styled->SetBorder(CreateEmptyBorder(gfx::Insets::TLBR(5, 10, 6, 20)));
  styled->SetBounds(0, 0, 1000, 0);
  test::RunScheduledLayout(styled);
  EXPECT_EQ(
      label_preferred_size.height() + 5 /*top border*/ + 6 /*bottom border*/,
      styled->GetPreferredSize(SizeBounds(1000, {})).height());
  EXPECT_EQ(
      label_preferred_size.width() + 10 /*left border*/ + 20 /*right border*/,
      styled->GetPreferredSize(SizeBounds(1000, {})).width());
}

TEST_F(StyledLabelTest, LineHeightWithShorterCustomView) {
  const std::string text("one ");
  StyledLabel* styled = InitStyledLabel(text);
  int default_height = styled->GetHeightForWidth(1000);

  const std::string custom_view_text("with custom view");
  const int less_height = 10;
  std::unique_ptr<View> custom_view = std::make_unique<StaticSizedView>(
      gfx::Size(20, default_height - less_height));
  StyledLabel::RangeStyleInfo style_info;
  style_info.custom_view = custom_view.get();
  styled = InitStyledLabel(text + custom_view_text);
  styled->AddStyleRange(
      gfx::Range(text.size(), text.size() + custom_view_text.size()),
      style_info);
  styled->AddCustomView(std::move(custom_view));
  EXPECT_EQ(default_height, styled->GetHeightForWidth(100));
}

TEST_F(StyledLabelTest, LineHeightWithTallerCustomView) {
  const std::string text("one ");
  StyledLabel* styled = InitStyledLabel(text);
  int default_height = styled->GetHeightForWidth(100);

  const std::string custom_view_text("with custom view");
  const int more_height = 10;
  std::unique_ptr<View> custom_view = std::make_unique<StaticSizedView>(
      gfx::Size(20, default_height + more_height));
  StyledLabel::RangeStyleInfo style_info;
  style_info.custom_view = custom_view.get();
  styled = InitStyledLabel(text + custom_view_text);
  styled->AddStyleRange(
      gfx::Range(text.size(), text.size() + custom_view_text.size()),
      style_info);
  styled->AddCustomView(std::move(custom_view));
  EXPECT_EQ(default_height + more_height, styled->GetHeightForWidth(100));
}

TEST_F(StyledLabelTest, LineWrapperWithCustomView) {
  const std::string text_before("one ");
  StyledLabel* styled = InitStyledLabel(text_before);
  int default_height = styled->GetHeightForWidth(100);
  const std::string custom_view_text("two with custom view ");
  const std::string text_after("three");

  int custom_view_height = 25;
  std::unique_ptr<View> custom_view =
      std::make_unique<StaticSizedView>(gfx::Size(200, custom_view_height));
  StyledLabel::RangeStyleInfo style_info;
  style_info.custom_view = custom_view.get();
  styled = InitStyledLabel(text_before + custom_view_text + text_after);
  styled->AddStyleRange(
      gfx::Range(text_before.size(),
                 text_before.size() + custom_view_text.size()),
      style_info);
  styled->AddCustomView(std::move(custom_view));
  EXPECT_EQ(default_height * 2 + custom_view_height,
            styled->GetHeightForWidth(100));
}

TEST_F(StyledLabelTest, AlignmentInLTR) {
  const std::string text("text");
  StyledLabel* styled = InitStyledLabel(text);
  styled->SetBounds(0, 0, 1000, 1000);
  test::RunScheduledLayout(styled);
  const auto& children = styled->children();
  ASSERT_EQ(1u, children.size());

  // Test the default alignment puts the text on the leading side (left).
  EXPECT_EQ(0, children.front()->bounds().x());

  // Setting |ALIGN_RIGHT| indicates the text should be aligned to the trailing
  // side, and hence its trailing side coordinates (i.e. right) should align
  // with the trailing side coordinate of the label (right).
  styled->SetHorizontalAlignment(gfx::ALIGN_RIGHT);
  test::RunScheduledLayout(styled);
  EXPECT_EQ(1000, children.front()->bounds().right());

  // Setting |ALIGN_LEFT| indicates the text should be aligned to the leading
  // side, and hence its leading side coordinates (i.e. x) should align with the
  // leading side coordinate of the label (x).
  styled->SetHorizontalAlignment(gfx::ALIGN_LEFT);
  test::RunScheduledLayout(styled);
  EXPECT_EQ(0, children.front()->bounds().x());

  styled->SetHorizontalAlignment(gfx::ALIGN_CENTER);
  test::RunScheduledLayout(styled);
  Label label(ASCIIToUTF16(text));
  EXPECT_EQ((1000 - label.GetPreferredSize({}).width()) / 2,
            children.front()->bounds().x());
}

TEST_F(StyledLabelTest, AlignmentInRTL) {
  // |g_icu_text_direction| is cached to prevent reading new commandline switch.
  // Set |g_icu_text_direction| to |UNKNOWN_DIRECTION| in order to read the new
  // commandline switch.
  base::test::ScopedRestoreICUDefaultLocale scoped_locale("en_US");
  base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
      switches::kForceUIDirection, switches::kForceDirectionRTL);

  const std::string text("text");
  StyledLabel* styled = InitStyledLabel(text);
  styled->SetBounds(0, 0, 1000, 1000);
  test::RunScheduledLayout(styled);
  const auto& children = styled->children();
  ASSERT_EQ(1u, children.size());

  // Test the default alignment puts the text on the leading side (right).
  // Note that x-coordinates in RTL place the origin (0) on the right.
  EXPECT_EQ(0, children.front()->bounds().x());

  // Setting |ALIGN_RIGHT| indicates the text should be aligned to the trailing
  // side, and hence its trailing side coordinates (i.e. right) should align
  // with the trailing side coordinate of the label (right).
  styled->SetHorizontalAlignment(gfx::ALIGN_RIGHT);
  test::RunScheduledLayout(styled);
  EXPECT_EQ(1000, children.front()->bounds().right());

  // Setting |ALIGN_LEFT| indicates the text should be aligned to the leading
  // side, and hence its leading side coordinates (i.e. x) should align with the
  // leading side coordinate of the label (x).
  styled->SetHorizontalAlignment(gfx::ALIGN_LEFT);
  test::RunScheduledLayout(styled);
  EXPECT_EQ(0, children.front()->bounds().x());

  styled->SetHorizontalAlignment(gfx::ALIGN_CENTER);
  test::RunScheduledLayout(styled);
  Label label(ASCIIToUTF16(text));
  EXPECT_EQ((1000 - label.GetPreferredSize({}).width()) / 2,
            children.front()->bounds().x());
}

TEST_F(StyledLabelTest, ViewsCenteredWithLinkAndCustomView) {
  const std::string text("This is a test block of text, ");
  const std::string link_text("and this should be a link");
  const std::string custom_view_text("And this is a custom view");
  StyledLabel* styled = InitStyledLabel(text + link_text + custom_view_text);
  styled->AddStyleRange(
      gfx::Range(text.size(), text.size() + link_text.size()),
      StyledLabel::RangeStyleInfo::CreateForLink(base::RepeatingClosure()));

  int custom_view_height = 25;
  std::unique_ptr<View> custom_view =
      std::make_unique<StaticSizedView>(gfx::Size(20, custom_view_height));
  StyledLabel::RangeStyleInfo style_info;
  style_info.custom_view = custom_view.get();
  styled->AddStyleRange(
      gfx::Range(text.size() + link_text.size(),
                 text.size() + link_text.size() + custom_view_text.size()),
      style_info);
  styled->AddCustomView(std::move(custom_view));

  styled->SetBounds(0, 0, 1000, 500);
  test::RunScheduledLayout(styled);
  const int height =
      styled->GetPreferredSize(SizeBounds(styled->size())).height();
  for (const views::View* child : styled->children()) {
    EXPECT_EQ(height / 2, child->bounds().CenterPoint().y());
  }
}

TEST_F(StyledLabelTest, ViewsCenteredForEvenAndOddSizes) {
  constexpr int kViewWidth = 30;
  for (int height : {60, 61}) {
    StyledLabel* styled = InitStyledLabel("abc");

    const auto view_heights =
        std::to_array<int>({height, height / 2, height / 2 + 1});
    for (uint32_t i = 0; i < view_heights.size(); ++i) {
      auto view = std::make_unique<StaticSizedView>(
          gfx::Size(kViewWidth, view_heights[i]));
      StyledLabel::RangeStyleInfo style_info;
      style_info.custom_view = view.get();
      styled->AddStyleRange(gfx::Range(i, i + 1), style_info);
      styled->AddCustomView(std::move(view));
    }

    styled->SetBounds(0, 0, kViewWidth * 3, height);
    test::RunScheduledLayout(styled);

    for (const views::View* child : styled->children()) {
      EXPECT_EQ(height / 2, child->bounds().CenterPoint().y());
    }
  }
}

TEST_F(StyledLabelTest, CacheSizeWithAlignment) {
  const std::string text("text");
  StyledLabel* styled = InitStyledLabel(text);
  styled->SetHorizontalAlignment(gfx::ALIGN_RIGHT);
  styled->SetBounds(0, 0, 1000, 1000);
  test::RunScheduledLayout(styled);
  ASSERT_EQ(1u, styled->children().size());
  const View* child = styled->children().front();
  EXPECT_EQ(1000, child->bounds().right());

  styled->SetSize({800, 1000});
  test::RunScheduledLayout(styled);
  ASSERT_EQ(1u, styled->children().size());
  const View* new_child = styled->children().front();
  EXPECT_EQ(child, new_child);
  EXPECT_EQ(800, new_child->bounds().right());
}

// Verifies that calling SizeToFit() on a label which requires less width still
// causes it to take the whole requested width.
TEST_F(StyledLabelTest, SizeToFit) {
  const std::string text("text");
  StyledLabel* styled = InitStyledLabel(text);
  styled->SetHorizontalAlignment(gfx::ALIGN_RIGHT);
  styled->SizeToFit(1000);
  test::RunScheduledLayout(styled);
  ASSERT_EQ(1u, styled->children().size());
  EXPECT_EQ(1000, styled->children().front()->bounds().right());
}

// Verifies that a non-empty label has a preferred size by default.
TEST_F(StyledLabelTest, PreferredSizeNonEmpty) {
  const std::string text("text");
  StyledLabel* styled = InitStyledLabel(text);
  EXPECT_FALSE(styled->GetPreferredSize({}).IsEmpty());
}

// Verifies that GetPreferredSize() respects the existing wrapping.
TEST_F(StyledLabelTest, PreferredSizeRespectsWrapping) {
  const std::string text("Long text that can be split across lines");
  StyledLabel* styled = InitStyledLabel(text);
  gfx::Size size = styled->GetPreferredSize({});
  size.set_width(size.width() / 2);
  size.set_height(styled->GetHeightForWidth(size.width()));
  styled->SetSize(size);
  test::RunScheduledLayout(styled);
  const gfx::Size new_size = styled->GetPreferredSize(SizeBounds(size));
  EXPECT_LE(new_size.width(), size.width());
  EXPECT_EQ(new_size.height(), size.height());
}

// Verifies that calling a const method does not change the preferred size.
TEST_F(StyledLabelTest, PreferredSizeAcrossConstCall) {
  const std::string text("Long text that can be split across lines");
  StyledLabel* styled = InitStyledLabel(text);
  const gfx::Size size = styled->GetPreferredSize({});
  styled->GetHeightForWidth(size.width() / 2);
  EXPECT_EQ(size, styled->GetPreferredSize({}));
}

TEST_F(StyledLabelTest, AccessibleNameAndRole) {
  const std::string text("Text");
  StyledLabel* styled = InitStyledLabel(text);

  IgnoreMissingWidgetForTestingScopedSetter a11y_ignore_missing_widget_(
      styled->GetViewAccessibility());

  EXPECT_EQ(styled->GetViewAccessibility().GetCachedName(),
            base::UTF8ToUTF16(text));
  EXPECT_EQ(styled->GetViewAccessibility().GetCachedRole(),
            ax::mojom::Role::kStaticText);

  ui::AXNodeData data;
  styled->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_EQ(data.GetStringAttribute(ax::mojom::StringAttribute::kName), text);
  EXPECT_EQ(data.role, ax::mojom::Role::kStaticText);

  styled->SetTextContext(style::CONTEXT_DIALOG_TITLE);

  EXPECT_EQ(styled->GetViewAccessibility().GetCachedName(),
            base::UTF8ToUTF16(text));
  EXPECT_EQ(styled->GetViewAccessibility().GetCachedRole(),
            ax::mojom::Role::kTitleBar);

  data = ui::AXNodeData();
  styled->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_EQ(data.GetStringAttribute(ax::mojom::StringAttribute::kName), text);
  EXPECT_EQ(data.role, ax::mojom::Role::kTitleBar);

  styled->SetText(u"New Text");
  styled->GetViewAccessibility().SetRole(ax::mojom::Role::kLink);
  EXPECT_EQ(styled->GetViewAccessibility().GetCachedName(), u"New Text");
  EXPECT_EQ(styled->GetViewAccessibility().GetCachedRole(),
            ax::mojom::Role::kLink);

  data = ui::AXNodeData();
  styled->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_EQ(data.GetString16Attribute(ax::mojom::StringAttribute::kName),
            u"New Text");
  EXPECT_EQ(data.role, ax::mojom::Role::kLink);
}

// Regression test for crbug.com/361639416.
// Tests that the child views (text fragments) are still alive after layout.
TEST_F(StyledLabelTest, OldChildViewsAreAliveAfterLayout) {
  class ViewDestroyObserser : public ViewObserver {
   public:
    MOCK_METHOD(void, OnViewIsDeleting, (View*), (override));
  };

  ViewDestroyObserser view_destroy_observer;

  const std::string text("This is a test block of text.");
  StyledLabel* styled = InitStyledLabel(text);

  styled->AddStyleRange(
      gfx::Range(0, 1),
      StyledLabel::RangeStyleInfo::CreateForLink(base::RepeatingClosure()));

  EXPECT_EQ(styled->GetFirstLinkForTesting(), nullptr);
  styled->SetBounds(0, 0, 1000, 1000);
  test::RunScheduledLayout(styled);

  views::View* link = styled->GetFirstLinkForTesting();
  EXPECT_NE(link, nullptr);
  link->AddObserver(&view_destroy_observer);
  EXPECT_CALL(view_destroy_observer, OnViewIsDeleting(testing::_)).Times(0);
  styled->SetBounds(0, 0, 10, 1000);
  test::RunScheduledLayout(styled);

  link->RemoveObserver(&view_destroy_observer);
}

TEST_F(StyledLabelTest, WrappedLinkAccessibilityAndFocus) {
  const std::u16string text = u"This is a long link that should wrap";
  StyledLabel* styled = InitStyledLabel(base::UTF16ToUTF8(text));
  styled->AddStyleRange(
      gfx::Range(0, text.size()),
      StyledLabel::RangeStyleInfo::CreateForLink(base::BindRepeating([]() {})));

  // Force wrapping by setting a small width.
  styled->SetBounds(0, 0, 50, 1000);
  test::RunScheduledLayout(styled);

  // Verify that we have multiple children and they are LinkFragments.
  ASSERT_GT(styled->children().size(), 1u);
  for (views::View* child : styled->children()) {
    EXPECT_EQ(LinkFragment::kViewClassName, child->GetClassName());
  }

  // Only the first fragment should be focusable and have the full name.
  for (size_t i = 0; i < styled->children().size(); ++i) {
    views::View* child = styled->children()[i];
    if (i == 0) {
      EXPECT_NE(views::View::FocusBehavior::NEVER, child->GetFocusBehavior());
      EXPECT_FALSE(child->GetViewAccessibility().GetIsIgnored());
      EXPECT_EQ(text, child->GetViewAccessibility().GetCachedName());
    } else {
      EXPECT_EQ(views::View::FocusBehavior::NEVER, child->GetFocusBehavior());
      EXPECT_TRUE(child->GetViewAccessibility().GetIsIgnored());
    }
  }
}

TEST_F(StyledLabelTest, AccessibleRoleReflectsInlineLink) {
  const std::string text("Please visit example.com for details");
  StyledLabel* styled = InitStyledLabel(text);

  IgnoreMissingWidgetForTestingScopedSetter a11y_ignore_missing_widget_(
      styled->GetViewAccessibility());

  EXPECT_EQ(styled->GetViewAccessibility().GetCachedRole(),
            ax::mojom::Role::kStaticText);
  styled->SetBounds(0, 0, 1000, 1000);
  test::RunScheduledLayout(styled);
  EXPECT_EQ(styled->GetViewAccessibility().GetCachedRole(),
            ax::mojom::Role::kStaticText);

  // The role reflects the styled content immediately, before layout rebuilds
  // the child views.
  const size_t link_start = text.find("example.com");
  styled->AddStyleRange(
      gfx::Range(link_start, link_start + std::string("example.com").size()),
      StyledLabel::RangeStyleInfo::CreateForLink(base::RepeatingClosure()));
  EXPECT_EQ(styled->GetViewAccessibility().GetCachedRole(),
            ax::mojom::Role::kParagraph);

  test::RunScheduledLayout(styled);
  ASSERT_NE(styled->GetFirstLinkForTesting(), nullptr);
  EXPECT_EQ(styled->GetViewAccessibility().GetCachedRole(),
            ax::mojom::Role::kParagraph);
  EXPECT_EQ(styled->GetViewAccessibility().GetCachedName(),
            base::UTF8ToUTF16(text));

  styled->ClearStyleRanges();
  EXPECT_EQ(styled->GetViewAccessibility().GetCachedRole(),
            ax::mojom::Role::kStaticText);
  test::RunScheduledLayout(styled);
  EXPECT_EQ(styled->GetFirstLinkForTesting(), nullptr);
  EXPECT_EQ(styled->GetViewAccessibility().GetCachedRole(),
            ax::mojom::Role::kStaticText);
}

TEST_F(StyledLabelTest, AccessibleRoleReflectsCustomView) {
  const std::string text("prefix ");
  const std::string custom_view_text("custom");
  auto custom_view = std::make_unique<StaticSizedView>(gfx::Size(20, 20));
  StyledLabel::RangeStyleInfo style_info;
  style_info.custom_view = custom_view.get();

  StyledLabel* styled = InitStyledLabel(text + custom_view_text);
  IgnoreMissingWidgetForTestingScopedSetter a11y_ignore_missing_widget_(
      styled->GetViewAccessibility());
  styled->AddStyleRange(
      gfx::Range(text.size(), text.size() + custom_view_text.size()),
      style_info);
  styled->AddCustomView(std::move(custom_view));

  EXPECT_EQ(styled->GetViewAccessibility().GetCachedRole(),
            ax::mojom::Role::kParagraph);

  styled->SetBounds(0, 0, 1000, 1000);
  test::RunScheduledLayout(styled);
  EXPECT_EQ(styled->GetViewAccessibility().GetCachedRole(),
            ax::mojom::Role::kParagraph);
}

TEST_F(StyledLabelTest, AccessibleRoleTitleContextUnaffectedByLink) {
  const std::string text("Title with example.com link");
  StyledLabel* styled = InitStyledLabel(text);
  styled->SetTextContext(style::CONTEXT_DIALOG_TITLE);

  IgnoreMissingWidgetForTestingScopedSetter a11y_ignore_missing_widget_(
      styled->GetViewAccessibility());

  const size_t link_start = text.find("example.com");
  styled->AddStyleRange(
      gfx::Range(link_start, link_start + std::string("example.com").size()),
      StyledLabel::RangeStyleInfo::CreateForLink(base::RepeatingClosure()));
  styled->SetBounds(0, 0, 1000, 1000);
  test::RunScheduledLayout(styled);

  ASSERT_NE(styled->GetFirstLinkForTesting(), nullptr);
  EXPECT_EQ(styled->GetViewAccessibility().GetCachedRole(),
            ax::mojom::Role::kTitleBar);
}

}  // namespace views
