// Copyright 2023 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/autofill/popup/popup_row_factory_utils.h"

#include <memory>
#include <utility>
#include <vector>

#include "base/test/scoped_feature_list.h"
#include "chrome/browser/ui/autofill/mock_autofill_popup_controller.h"
#include "chrome/browser/ui/views/autofill/payments/bnpl_issuer_linked_pill.h"
#include "chrome/browser/ui/views/autofill/popup/mock_accessibility_selection_delegate.h"
#include "chrome/browser/ui/views/autofill/popup/mock_selection_delegate.h"
#include "chrome/browser/ui/views/autofill/popup/popup_row_content_view.h"
#include "chrome/browser/ui/views/autofill/popup/popup_row_view.h"
#include "chrome/browser/ui/views/autofill/popup/popup_row_with_button_view.h"
#include "chrome/test/views/chrome_views_test_base.h"
#include "components/autofill/core/browser/at_memory/at_memory_manager.h"
#include "components/autofill/core/browser/metrics/autofill_metrics.h"
#include "components/autofill/core/browser/suggestions/suggestion.h"
#include "components/autofill/core/browser/suggestions/suggestion_type.h"
#include "components/strings/grit/components_strings.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/layer.h"
#include "ui/events/test/event_generator.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/throbber.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget_utils.h"

using ::testing::IsNull;
using ::testing::NotNull;
using ::testing::Return;

namespace autofill {

namespace {
constexpr float kDisabledBnplOpacity = 0.38f;

// Helper function to recursively find a `views::Label` with matching text
// inside `view`.
views::Label* FindLabelWithText(views::View* view, const std::u16string& text) {
  for (views::View* child : view->children()) {
    if (auto* label = views::AsViewClass<views::Label>(child)) {
      if (label->GetText() == text) {
        return label;
      }
    }
    if (auto* found = FindLabelWithText(child, text)) {
      return found;
    }
  }
  return nullptr;
}
}  // namespace

class PopupRowFactoryUtilsTest : public ChromeViewsTestBase {
 public:
  void SetUp() override {
    ChromeViewsTestBase::SetUp();
    widget_ =
        CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
    generator_ = std::make_unique<ui::test::EventGenerator>(
        GetRootWindow(widget_.get()));
  }

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

 protected:
  void ShowSuggestion(Suggestion suggestion) {
    // Show the button.
    controller().set_suggestions({std::move(suggestion)});
    row_view_ = widget().SetContentsView(
        CreatePopupRowView(controller().GetWeakPtr(), a11y_selection_delegate(),
                           selection_delegate(), 0));
    widget().Show();
  }

  MockAutofillPopupController& controller() { return controller_; }
  MockAccessibilitySelectionDelegate& a11y_selection_delegate() {
    return mock_a11y_selection_delegate_;
  }
  MockSelectionDelegate& selection_delegate() {
    return mock_selection_delegate_;
  }
  ui::test::EventGenerator& generator() { return *generator_; }
  PopupRowView& row_view() { return *row_view_; }
  views::Widget& widget() { return *widget_; }

 private:
  std::unique_ptr<views::Widget> widget_;
  std::unique_ptr<ui::test::EventGenerator> generator_;
  MockAutofillPopupController controller_;
  MockAccessibilitySelectionDelegate mock_a11y_selection_delegate_;
  MockSelectionDelegate mock_selection_delegate_;
  raw_ptr<PopupRowView> row_view_ = nullptr;
};

// A test fixture for testing the creation of rows that contain a button.
class PopupRowFactoryUtilsRowWithButtonTest : public PopupRowFactoryUtilsTest {
 public:
  void ShowAutocompleteSuggestion() {
    ShowSuggestion(
        Suggestion(u"Some entry", SuggestionType::kAutocompleteEntry));
  }

 protected:
  PopupRowWithButtonView& row_with_button_view() {
    CHECK(views::IsViewClass<PopupRowWithButtonView>(&row_view()));
    return static_cast<PopupRowWithButtonView&>(row_view());
  }
};

TEST_F(PopupRowFactoryUtilsRowWithButtonTest,
       AutocompleteDeleteInvokesController) {
  ShowAutocompleteSuggestion();
  views::ImageButton* button = row_with_button_view().GetButtonForTest();
  row_with_button_view().SetSelectedCell(PopupRowView::CellType::kContent);
  // In test env we have to manually set the bounds when a view becomes visible.
  button->parent()->SetBoundsRect(gfx::Rect(0, 0, 30, 30));

  EXPECT_CALL(
      controller(),
      RemoveSuggestion(
          0, AutofillMetrics::SingleEntryRemovalMethod::kDeleteButtonClicked))
      .WillOnce(Return(true));

  generator().MoveMouseTo(button->GetBoundsInScreen().CenterPoint());
  generator().ClickLeftButton();
  task_environment()->RunUntilIdle();
}

TEST_F(PopupRowFactoryUtilsRowWithButtonTest,
       AutocompleteDeleteButtonHasTooltip) {
  ShowAutocompleteSuggestion();
  views::ImageButton* button = row_with_button_view().GetButtonForTest();
  EXPECT_EQ(button->GetTooltipText(),
            l10n_util::GetStringUTF16(
                IDS_AUTOFILL_DELETE_AUTOCOMPLETE_SUGGESTION_TOOLTIP));
}

TEST_F(PopupRowFactoryUtilsRowWithButtonTest,
       AutocompleteDeleteButtonSetsAccessibility) {
  ShowAutocompleteSuggestion();
  views::ImageButton* button = row_with_button_view().GetButtonForTest();

  views::IgnoreMissingWidgetForTestingScopedSetter ignore_missing_widget(
      button->GetViewAccessibility());
  ui::AXNodeData node_data;
  button->GetViewAccessibility().GetAccessibleNodeData(&node_data);

  EXPECT_EQ(node_data.role, ax::mojom::Role::kMenuItem);
  EXPECT_EQ(
      l10n_util::GetStringFUTF16(
          IDS_AUTOFILL_DELETE_AUTOCOMPLETE_SUGGESTION_A11Y_HINT, u"Some entry"),
      node_data.GetString16Attribute(ax::mojom::StringAttribute::kName));
}

class PasswordPopupRowViewTest : public PopupRowFactoryUtilsTest {
 public:
  void ShowPasswordSuggestionWithLoadingState(bool is_loading) {
    Suggestion suggestion(u"ortiler", SuggestionType::kPasswordEntry);
    suggestion.labels = {{Suggestion::Text(u"password")}};
    suggestion.is_loading = Suggestion::IsLoading(is_loading);
    ShowSuggestion(suggestion);
  }
};

TEST_F(PasswordPopupRowViewTest, LoadingSuggestionShowsThrobber) {
  ShowPasswordSuggestionWithLoadingState(true);

  EXPECT_TRUE(views::IsViewClass<views::Throbber>(
      row_view().GetContentView().children().at(0)));
}

TEST_F(PasswordPopupRowViewTest, NonLoadingSuggestionDoesNotShowThrobber) {
  ShowPasswordSuggestionWithLoadingState(false);

  EXPECT_FALSE(views::IsViewClass<views::Throbber>(
      row_view().GetContentView().children().at(0)));
}

class BnplPopupRowViewTest : public PopupRowFactoryUtilsTest {
 public:
  BnplPopupRowViewTest() {
    scoped_feature_list_.InitAndEnableFeature(
        features::kAutofillEnablePayNowPayLaterTabs);
  }
  ~BnplPopupRowViewTest() override = default;

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

TEST_F(BnplPopupRowViewTest, LinkedPill) {
  Suggestion suggestion(u"Bnpl", SuggestionType::kBnplEntry);
  BnplIssuer linked_issuer(/*instrument_id=*/1234,
                           BnplIssuer::IssuerId::kBnplZip, {});
  suggestion.payload = Suggestion::BnplIssuer(linked_issuer);

  ShowSuggestion(suggestion);

  views::View* pill = row_view().GetContentView().GetViewByElementId(
      payments::BnplLinkedIssuerPill::kBnplLinkedPillElementId);
  ASSERT_THAT(pill, NotNull());
  EXPECT_TRUE(pill->GetEnabled());
  EXPECT_FLOAT_EQ(1.0f, pill->layer()->opacity());
}

TEST_F(BnplPopupRowViewTest, LinkedPill_Deactivated) {
  Suggestion suggestion(u"Bnpl", SuggestionType::kBnplEntry);
  BnplIssuer linked_issuer(/*instrument_id=*/1234,
                           BnplIssuer::IssuerId::kBnplZip, {});
  suggestion.payload = Suggestion::BnplIssuer(linked_issuer);
  suggestion.acceptability =
      Suggestion::Acceptability::kUnselectableAndUnacceptable;

  ShowSuggestion(suggestion);

  views::View* pill = row_view().GetContentView().GetViewByElementId(
      payments::BnplLinkedIssuerPill::kBnplLinkedPillElementId);
  ASSERT_THAT(pill, NotNull());
  EXPECT_FALSE(pill->GetEnabled());
  EXPECT_FLOAT_EQ(kDisabledBnplOpacity, pill->layer()->opacity());
}

TEST_F(BnplPopupRowViewTest, Deactivated_IconOpacity) {
  Suggestion suggestion(u"Bnpl", SuggestionType::kBnplEntry);
  suggestion.icon = Suggestion::Icon::kBnplGeneric;
  suggestion.acceptability =
      Suggestion::Acceptability::kUnselectableAndUnacceptable;

  ShowSuggestion(suggestion);

  views::ImageView* icon_view = nullptr;
  for (const auto& child : row_view().GetContentView().children()) {
    if (auto* img = views::AsViewClass<views::ImageView>(child.get())) {
      icon_view = img;
      break;
    }
  }

  ASSERT_THAT(icon_view, NotNull());
  EXPECT_TRUE(icon_view->layer());
  EXPECT_FLOAT_EQ(kDisabledBnplOpacity, icon_view->layer()->opacity());
}

TEST_F(BnplPopupRowViewTest, UnlinkedIssuer_NoLinkedPill) {
  Suggestion suggestion(u"Bnpl", SuggestionType::kBnplEntry);
  BnplIssuer unlinked_issuer(/*instrument_id=*/std::nullopt,
                             BnplIssuer::IssuerId::kBnplZip, {});
  suggestion.payload = Suggestion::BnplIssuer(unlinked_issuer);

  ShowSuggestion(suggestion);

  views::View* pill = row_view().GetContentView().GetViewByElementId(
      payments::BnplLinkedIssuerPill::kBnplLinkedPillElementId);
  EXPECT_THAT(pill, IsNull());
}

// Tests that AtMemory suggestions ignore filter match bolding even when a
// filter match is provided to CreatePopupRowView.
TEST_F(PopupRowFactoryUtilsTest, AtMemorySuggestionIgnoresFilterMatchBolding) {
  Suggestion atmemory_suggestion(u"@memory query text search",
                                 SuggestionType::kAtMemorySearchAffordance);
  AutofillPopupController::SuggestionFilterMatch filter_match{
      .main_text_match = gfx::Range(0, 25)};

  // Create content view directly WITH filter_match applied (bolded).
  std::unique_ptr<PopupRowContentView> content_view_with_bolding =
      CreatePopupRowContentView(atmemory_suggestion,
                                /*show_new_badge=*/std::nullopt,
                                FillingProduct::kAtMemory, filter_match);
  views::Label* bolded_label = FindLabelWithText(
      content_view_with_bolding.get(), u"@memory query text search");
  ASSERT_THAT(bolded_label, NotNull());
  int bolded_width = bolded_label->GetPreferredSize().width();

  // Populate controller suggestions so CreatePopupRowView can query index 0
  // for the AtMemory suggestion when creating the row view with filter_match.
  controller().set_suggestions({atmemory_suggestion});
  auto row_view =
      CreatePopupRowView(controller().GetWeakPtr(), a11y_selection_delegate(),
                         selection_delegate(), 0, filter_match);
  views::Label* atmemory_label = FindLabelWithText(
      &row_view->GetContentView(), u"@memory query text search");
  ASSERT_THAT(atmemory_label, NotNull());
  int atmemory_label_width = atmemory_label->GetPreferredSize().width();

  // Verify that AtMemory main text label is narrower than the bolded version
  // because filter_match bolding was ignored.
  EXPECT_LT(atmemory_label_width, bolded_width);
}

// Tests that kAtMemorySourceAttribution uses Body 4 text style and
// onSurfaceSubtle text color.
TEST_F(PopupRowFactoryUtilsTest, AtMemorySourceAttributionStyle) {
  Suggestion suggestion = AtMemoryManager::CreateSourceAttributionSuggestion();
  ShowSuggestion(suggestion);

  std::u16string expected_text = l10n_util::GetStringUTF16(
      IDS_AUTOFILL_AT_MEMORY_SOURCE_ATTRIBUTION_PERSONAL_INTELLIGENCE);
  views::Label* label =
      FindLabelWithText(&row_view().GetContentView(), expected_text);
  ASSERT_THAT(label, NotNull());
  EXPECT_EQ(label->GetTextStyle(), views::style::STYLE_BODY_4);
  EXPECT_EQ(label->GetEnabledColor(), row_view().GetColorProvider()->GetColor(
                                          ui::kColorSysOnSurfaceSubtle));
}

TEST_F(PopupRowFactoryUtilsTest, RemoveAutofillAiRowView) {
  Suggestion suggestion(u"Remove this info", SuggestionType::kRemoveAutofillAi);
  suggestion.icon = Suggestion::Icon::kClose;
  ShowSuggestion(suggestion);

  views::Label* label =
      FindLabelWithText(&row_view().GetContentView(), u"Remove this info");
  ASSERT_THAT(label, NotNull());
  EXPECT_EQ(label->GetHorizontalAlignment(), gfx::ALIGN_TO_HEAD);

  ASSERT_FALSE(row_view().GetContentView().children().empty());
  EXPECT_TRUE(views::IsViewClass<views::ImageView>(
      row_view().GetContentView().children().front()));
}

// Tests that when a `Suggestion` has `kAtMemorySearchResult` type, the
// custom horizontal spacing between labels in the same row is applied.
TEST_F(PopupRowFactoryUtilsTest, AtMemorySearchResultLabelsHorizontalSpacing) {
  Suggestion suggestion(u"Main", SuggestionType::kAtMemorySearchResult);
  suggestion.labels = {
      {Suggestion::Text(u"Part1"), Suggestion::Text(u"Part2")}};
  ShowSuggestion(suggestion);

  views::Label* label1 =
      FindLabelWithText(&row_view().GetContentView(), u"Part1");
  ASSERT_THAT(label1, NotNull());

  auto* container = views::AsViewClass<views::BoxLayoutView>(label1->parent());
  ASSERT_THAT(container, NotNull());
  EXPECT_EQ(container->GetBetweenChildSpacing(), 4);
}

// Tests that `kAtMemorySearchResult` suggestions have a multiline main text
// label with max 2 lines and extra vertical padding when labels are present so
// the row height isn't crammed.
TEST_F(PopupRowFactoryUtilsTest, AtMemorySearchResultMultiLineAndHeight) {
  EXPECT_CALL(controller(), GetMainFillingProduct())
      .WillRepeatedly(testing::Return(FillingProduct::kAddress));

  Suggestion suggestion(
      u"Very long search result text that spans multiple lines",
      SuggestionType::kAtMemorySearchResult);
  suggestion.labels = {{Suggestion::Text(u"Label text")}};
  ShowSuggestion(suggestion);

  views::Label* label = FindLabelWithText(
      &row_view().GetContentView(),
      u"Very long search result text that spans multiple lines");
  ASSERT_THAT(label, NotNull());
  EXPECT_TRUE(label->GetMultiLine());
  EXPECT_EQ(label->GetMaxLines(), 2u);
  EXPECT_EQ(label->GetMaximumWidth(), 236);
  EXPECT_EQ(label->GetHorizontalAlignment(), gfx::ALIGN_TO_HEAD);

  gfx::Insets insets = row_view().GetContentView().GetInsideBorderInsets();
  EXPECT_EQ(insets.top(), 8);
  EXPECT_EQ(insets.bottom(), 8);
}

// Tests that `kAtMemorySearchResult` suggestions with short main text do not
// get extra vertical padding.
TEST_F(PopupRowFactoryUtilsTest, AtMemorySearchResultShortTextNoExtraPadding) {
  EXPECT_CALL(controller(), GetMainFillingProduct())
      .WillRepeatedly(testing::Return(FillingProduct::kAddress));

  Suggestion suggestion(u"Paris", SuggestionType::kAtMemorySearchResult);
  suggestion.labels = {{Suggestion::Text(u"Label text")}};
  ShowSuggestion(suggestion);

  views::Label* label =
      FindLabelWithText(&row_view().GetContentView(), u"Paris");
  ASSERT_THAT(label, NotNull());
  EXPECT_TRUE(label->GetMultiLine());
  EXPECT_EQ(label->GetMaxLines(), 2u);

  gfx::Insets insets = row_view().GetContentView().GetInsideBorderInsets();
  EXPECT_EQ(insets.top(), 0);
  EXPECT_EQ(insets.bottom(), 0);
}

}  // namespace autofill
