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

#include "components/omnibox/browser/titled_url_match_utils.h"

#include <algorithm>
#include <memory>
#include <string_view>

#include "base/memory/scoped_refptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/to_string.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "components/bookmarks/browser/titled_url_match.h"
#include "components/bookmarks/browser/titled_url_node.h"
#include "components/omnibox/browser/autocomplete_input.h"
#include "components/omnibox/browser/autocomplete_match.h"
#include "components/omnibox/browser/autocomplete_provider.h"
#include "components/omnibox/browser/fake_autocomplete_provider.h"
#include "components/omnibox/browser/test_scheme_classifier.h"
#include "components/omnibox/common/omnibox_features.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/metrics_proto/omnibox_event.pb.h"
#include "url/gurl.h"

using bookmarks::TitledUrlMatchToAutocompleteMatch;

namespace {

class MockTitledUrlNode : public bookmarks::TitledUrlNode {
 public:
  MockTitledUrlNode(const std::u16string& title,
                    const GURL& url,
                    std::vector<std::u16string> ancestors = {})
      : title_(title), url_(url), ancestors_(ancestors) {}

  // TitledUrlNode
  const std::u16string& GetTitledUrlNodeTitle() const override {
    return title_;
  }
  const GURL& GetTitledUrlNodeUrl() const override { return url_; }
  std::vector<std::u16string_view> GetTitledUrlNodeAncestorTitles()
      const override {
    std::vector<std::u16string_view> ancestors;
    std::ranges::transform(
        ancestors_, std::back_inserter(ancestors),
        [](auto& ancestor) { return std::u16string_view(ancestor); });
    return ancestors;
  }

 private:
  std::u16string title_;
  GURL url_;
  std::vector<std::u16string> ancestors_;
};

std::string ACMatchClassificationsAsString(
    const ACMatchClassifications& classifications) {
  std::string position_string("{");
  for (auto classification : classifications) {
    position_string +=
        "{offset " + base::NumberToString(classification.offset) + ", style " +
        base::NumberToString(classification.style) + "}, ";
  }
  position_string += "}\n";
  return position_string;
}

// Use a test fixture to ensure that any scoped settings that are set during the
// test are cleared after the test is terminated.
class TitledUrlMatchUtilsTest : public testing::Test {
 protected:
  void TearDown() override {
    RichAutocompletionParams::ClearParamsForTesting();
  }
};

}  // namespace

TEST_F(TitledUrlMatchUtilsTest, TitledUrlMatchToAutocompleteMatch) {
  std::u16string input_text(u"goo");
  std::u16string match_title(u"Google Search");
  GURL match_url("https://www.google.com/");
  AutocompleteMatchType::Type type = AutocompleteMatchType::BOOKMARK_TITLE;
  int relevance = 123;
  int bookmark_count = 3;

  MockTitledUrlNode node(match_title, match_url);
  bookmarks::TitledUrlMatch titled_url_match;
  titled_url_match.node = &node;
  titled_url_match.title_match_positions = {{0, 3}};
  titled_url_match.url_match_positions = {{12, 15}};

  scoped_refptr<FakeAutocompleteProvider> provider =
      new FakeAutocompleteProvider(AutocompleteProvider::Type::TYPE_BOOKMARK);
  TestSchemeClassifier classifier;
  AutocompleteInput input(input_text, metrics::OmniboxEventProto::NTP,
                          classifier);
  const std::u16string fixed_up_input(input_text);

  AutocompleteMatch autocomplete_match = TitledUrlMatchToAutocompleteMatch(
      titled_url_match, type, relevance, bookmark_count, provider.get(),
      classifier, input, fixed_up_input);

  ACMatchClassifications expected_contents_class = {
      {0, ACMatchClassification::URL | ACMatchClassification::MATCH},
      {3, ACMatchClassification::URL},
  };
  ACMatchClassifications expected_description_class = {
      {0, ACMatchClassification::MATCH},
      {3, ACMatchClassification::NONE},
  };
  std::u16string expected_inline_autocompletion(u"gle.com");

  EXPECT_EQ(provider.get(), autocomplete_match.provider);
  EXPECT_EQ(type, autocomplete_match.type);
  EXPECT_EQ(relevance, autocomplete_match.relevance);
  EXPECT_EQ(match_url, autocomplete_match.destination_url);
  EXPECT_EQ(u"google.com", autocomplete_match.contents);
  EXPECT_TRUE(std::ranges::equal(expected_contents_class,
                                 autocomplete_match.contents_class))
      << "EXPECTED: " << ACMatchClassificationsAsString(expected_contents_class)
      << "ACTUAL:   "
      << ACMatchClassificationsAsString(autocomplete_match.contents_class);
  EXPECT_EQ(match_title, autocomplete_match.description);
  EXPECT_TRUE(std::ranges::equal(expected_description_class,
                                 autocomplete_match.description_class));
  EXPECT_EQ(u"https://www.google.com", autocomplete_match.fill_into_edit);
  EXPECT_TRUE(autocomplete_match.allowed_to_be_default_match);
  EXPECT_EQ(expected_inline_autocompletion,
            autocomplete_match.inline_autocompletion);
}

AutocompleteMatch BuildTestAutocompleteMatch(
    scoped_refptr<FakeAutocompleteProvider> provider,
    const std::string& input_text_s,
    const GURL& match_url,
    const bookmarks::TitledUrlMatch::MatchPositions& match_positions) {
  std::u16string input_text(base::ASCIIToUTF16(input_text_s));
  std::u16string match_title(u"The Facebook");
  AutocompleteMatchType::Type type = AutocompleteMatchType::BOOKMARK_TITLE;
  int relevance = 123;
  int bookmark_count = 3;

  MockTitledUrlNode node(match_title, match_url);
  bookmarks::TitledUrlMatch titled_url_match;
  titled_url_match.node = &node;
  titled_url_match.title_match_positions = {{0, 3}};
  // Don't capture the scheme, so that it doesn't match.
  titled_url_match.url_match_positions = match_positions;

  TestSchemeClassifier classifier;
  AutocompleteInput input(input_text, metrics::OmniboxEventProto::NTP,
                          classifier);
  const std::u16string fixed_up_input(input_text);

  return TitledUrlMatchToAutocompleteMatch(titled_url_match, type, relevance,
                                           bookmark_count, provider.get(),
                                           classifier, input, fixed_up_input);
}

TEST_F(TitledUrlMatchUtilsTest, DoTrimHttpScheme) {
  scoped_refptr<FakeAutocompleteProvider> provider =
      base::MakeRefCounted<FakeAutocompleteProvider>(
          AutocompleteProvider::Type::TYPE_BOOKMARK);
  GURL match_url("http://www.facebook.com/");
  AutocompleteMatch autocomplete_match =
      BuildTestAutocompleteMatch(provider, "face", match_url, {{11, 15}});

  ACMatchClassifications expected_contents_class = {
      {0, ACMatchClassification::URL | ACMatchClassification::MATCH},
      {4, ACMatchClassification::URL},
  };
  std::u16string expected_contents(u"facebook.com");

  EXPECT_EQ(match_url, autocomplete_match.destination_url);
  EXPECT_EQ(expected_contents, autocomplete_match.contents);
  EXPECT_TRUE(std::ranges::equal(expected_contents_class,
                                 autocomplete_match.contents_class))
      << "EXPECTED: " << ACMatchClassificationsAsString(expected_contents_class)
      << "ACTUAL:   "
      << ACMatchClassificationsAsString(autocomplete_match.contents_class);
  EXPECT_TRUE(autocomplete_match.allowed_to_be_default_match);
}

TEST_F(TitledUrlMatchUtilsTest, DontTrimHttpSchemeIfInputHasScheme) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndDisableFeature({omnibox::kRichAutocompletion});
  RichAutocompletionParams::ClearParamsForTesting();

  scoped_refptr<FakeAutocompleteProvider> provider =
      base::MakeRefCounted<FakeAutocompleteProvider>(
          AutocompleteProvider::Type::TYPE_BOOKMARK);
  GURL match_url("http://www.facebook.com/");
  AutocompleteMatch autocomplete_match = BuildTestAutocompleteMatch(
      provider, "http://face", match_url, {{11, 15}});

  ACMatchClassifications expected_contents_class = {
      {0, ACMatchClassification::URL | ACMatchClassification::MATCH},
      {11, ACMatchClassification::URL},
  };
  std::u16string expected_contents(u"http://facebook.com");

  EXPECT_EQ(match_url, autocomplete_match.destination_url);
  EXPECT_EQ(expected_contents, autocomplete_match.contents);
  EXPECT_TRUE(std::ranges::equal(expected_contents_class,
                                 autocomplete_match.contents_class))
      << "EXPECTED: " << ACMatchClassificationsAsString(expected_contents_class)
      << "ACTUAL:   "
      << ACMatchClassificationsAsString(autocomplete_match.contents_class);
  EXPECT_FALSE(autocomplete_match.allowed_to_be_default_match);
}

TEST_F(TitledUrlMatchUtilsTest, DoTrimHttpsScheme) {
  scoped_refptr<FakeAutocompleteProvider> provider =
      base::MakeRefCounted<FakeAutocompleteProvider>(
          AutocompleteProvider::Type::TYPE_BOOKMARK);
  GURL match_url("https://www.facebook.com/");
  AutocompleteMatch autocomplete_match =
      BuildTestAutocompleteMatch(provider, "face", match_url, {{12, 16}});

  ACMatchClassifications expected_contents_class = {
      {0, ACMatchClassification::URL | ACMatchClassification::MATCH},
      {4, ACMatchClassification::URL},
  };
  std::u16string expected_contents(u"facebook.com");

  EXPECT_EQ(match_url, autocomplete_match.destination_url);
  EXPECT_EQ(expected_contents, autocomplete_match.contents);
  EXPECT_TRUE(std::ranges::equal(expected_contents_class,
                                 autocomplete_match.contents_class))
      << "EXPECTED: " << ACMatchClassificationsAsString(expected_contents_class)
      << "ACTUAL:   "
      << ACMatchClassificationsAsString(autocomplete_match.contents_class);
  EXPECT_TRUE(autocomplete_match.allowed_to_be_default_match);
}

TEST_F(TitledUrlMatchUtilsTest, DontTrimHttpsSchemeIfInputHasScheme) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndDisableFeature({omnibox::kRichAutocompletion});
  RichAutocompletionParams::ClearParamsForTesting();

  scoped_refptr<FakeAutocompleteProvider> provider =
      base::MakeRefCounted<FakeAutocompleteProvider>(
          AutocompleteProvider::Type::TYPE_BOOKMARK);
  GURL match_url("https://www.facebook.com/");
  AutocompleteMatch autocomplete_match = BuildTestAutocompleteMatch(
      provider, "https://face", match_url, {{12, 16}});

  ACMatchClassifications expected_contents_class = {
      {0, ACMatchClassification::URL | ACMatchClassification::MATCH},
      {12, ACMatchClassification::URL},
  };
  std::u16string expected_contents(u"https://facebook.com");

  EXPECT_EQ(match_url, autocomplete_match.destination_url);
  EXPECT_EQ(expected_contents, autocomplete_match.contents);
  EXPECT_TRUE(std::ranges::equal(expected_contents_class,
                                 autocomplete_match.contents_class))
      << "EXPECTED: " << ACMatchClassificationsAsString(expected_contents_class)
      << "ACTUAL:   "
      << ACMatchClassificationsAsString(autocomplete_match.contents_class);
  EXPECT_FALSE(autocomplete_match.allowed_to_be_default_match);
}

TEST_F(TitledUrlMatchUtilsTest, EmptyInlineAutocompletion) {
  // Since there is no URL prefix match, the inline autocompletion string will
  // be empty.
  std::u16string input_text(u"goo");
  std::u16string match_title(u"Email by Google");
  GURL match_url("http://www.gmail.com/google");
  AutocompleteMatchType::Type type = AutocompleteMatchType::BOOKMARK_TITLE;
  int relevance = 123;
  int bookmark_count = 3;

  MockTitledUrlNode node(match_title, match_url);
  bookmarks::TitledUrlMatch titled_url_match;
  titled_url_match.node = &node;
  titled_url_match.title_match_positions = {{9, 12}};
  titled_url_match.url_match_positions = {{21, 24}};
  titled_url_match.has_ancestor_match = false;

  scoped_refptr<FakeAutocompleteProvider> provider =
      new FakeAutocompleteProvider(AutocompleteProvider::Type::TYPE_BOOKMARK);
  TestSchemeClassifier classifier;
  AutocompleteInput input(input_text, metrics::OmniboxEventProto::NTP,
                          classifier);
  const std::u16string fixed_up_input(input_text);

  AutocompleteMatch autocomplete_match = TitledUrlMatchToAutocompleteMatch(
      titled_url_match, type, relevance, bookmark_count, provider.get(),
      classifier, input, fixed_up_input);

  // 'goo' in 'gmail.com/google'
  ACMatchClassifications expected_contents_class = {
      {0, ACMatchClassification::URL},
      {10, ACMatchClassification::URL | ACMatchClassification::MATCH},
      {13, ACMatchClassification::URL},
  };
  // 'goo' in 'Email by Google'
  ACMatchClassifications expected_description_class = {
      {0, ACMatchClassification::NONE},
      {9, ACMatchClassification::MATCH},
      {12, ACMatchClassification::NONE},
  };

  EXPECT_EQ(provider.get(), autocomplete_match.provider);
  EXPECT_EQ(type, autocomplete_match.type);
  EXPECT_EQ(relevance, autocomplete_match.relevance);
  EXPECT_EQ(match_url, autocomplete_match.destination_url);
  EXPECT_EQ(u"gmail.com/google", autocomplete_match.contents);
  EXPECT_TRUE(std::ranges::equal(expected_contents_class,
                                 autocomplete_match.contents_class))
      << "EXPECTED: " << ACMatchClassificationsAsString(expected_contents_class)
      << "ACTUAL:   "
      << ACMatchClassificationsAsString(autocomplete_match.contents_class);
  EXPECT_EQ(match_title, autocomplete_match.description);
  EXPECT_TRUE(std::ranges::equal(expected_description_class,
                                 autocomplete_match.description_class));
  EXPECT_EQ(u"www.gmail.com/google", autocomplete_match.fill_into_edit);
  EXPECT_FALSE(autocomplete_match.allowed_to_be_default_match);
  EXPECT_TRUE(autocomplete_match.inline_autocompletion.empty());
}

TEST_F(TitledUrlMatchUtilsTest, PathsInContentsAndDescription) {
  scoped_refptr<FakeAutocompleteProvider> provider =
      new FakeAutocompleteProvider(AutocompleteProvider::Type::TYPE_BOOKMARK);
  TestSchemeClassifier classifier;
  std::vector<std::u16string> ancestors = {u"parent", u"grandparent"};

  // Verifies contents and description of the AutocompleteMatch returned from
  // |bookmarks::TitledUrlMatchToAutocompleteMatch()|.
  auto test = [&](std::string title, std::string url, bool has_url_match,
                  bool has_ancestor_match, std::string expected_contents,
                  std::string expected_description) {
    SCOPED_TRACE("title [" + title + "], url [" + url + "], has_url_match [" +
                 base::ToString(has_url_match) + "], has_ancestor_match [" +
                 base::ToString(has_ancestor_match) + "].");
    MockTitledUrlNode node(base::UTF8ToUTF16(title), GURL(url), ancestors);
    bookmarks::TitledUrlMatch titled_url_match;
    titled_url_match.node = &node;
    if (has_url_match)
      titled_url_match.url_match_positions.push_back(
          {8, 8});  // 8 in order to be after 'https://'
    titled_url_match.has_ancestor_match = has_ancestor_match;
    AutocompleteInput input(std::u16string(), metrics::OmniboxEventProto::NTP,
                            classifier);
    AutocompleteMatch autocomplete_match = TitledUrlMatchToAutocompleteMatch(
        titled_url_match, AutocompleteMatchType::BOOKMARK_TITLE, 1,
        /*bookmark_count=*/3, provider.get(), classifier, input,
        std::u16string());
    EXPECT_EQ(base::UTF16ToUTF8(autocomplete_match.contents),
              expected_contents);
    EXPECT_EQ(base::UTF16ToUTF8(autocomplete_match.description),
              expected_description);
  };

  test("title", "https://url.com", false, false, "grandparent/parent", "title");
  test("title", "https://url.com", true, false, "url.com", "title");
  test("title", "https://url.com", false, true, "grandparent/parent", "title");
  test("title", "https://url.com", true, true, "grandparent/parent", "title");
}
