// 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 "components/search_engines/template_url_parser.h"

#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/path_service.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/task_environment.h"
#include "chrome/common/chrome_paths.h"
#include "components/search_engines/search_terms_data.h"
#include "components/search_engines/template_url.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
#include "testing/gtest/include/gtest/gtest.h"

using base::ASCIIToUTF16;

// Filters any param which as an occurrence of |name_str| in its |key| or an
// occurrence of |value_str| in its |value|.
bool TestFilter(const std::string& name_str,
                const std::string& value_str,
                const std::string& key,
                const std::string& value) {
  return (name_str.empty() || key.find(name_str) == std::string::npos) &&
         (value_str.empty() || value.find(value_str) == std::string::npos);
}

class TemplateURLParserTest : public testing::Test {
 protected:
  TemplateURLParserTest();
  ~TemplateURLParserTest() override;

  void SetUp() override;

  // Parses the OpenSearch description document at file_name (relative to the
  // data dir). The TemplateURL is placed in |template_url_|.
  void ParseFile(const std::string& file_name,
                 const TemplateURLParser::ParameterFilter& filter);

  void ParseString(const std::string& data,
                   const TemplateURLParser::ParameterFilter& filter);

  // ParseFile parses the results into this template_url.
  std::unique_ptr<TemplateURL> template_url_;

 private:
  void OnTemplateURLParsed(base::OnceClosure quit_closure,
                           std::unique_ptr<TemplateURL> template_url) {
    template_url_ = std::move(template_url);
    std::move(quit_closure).Run();
  }

  base::FilePath osdd_dir_;
  base::test::TaskEnvironment task_environment_;
  data_decoder::test::InProcessDataDecoder data_decoder_;
};

TemplateURLParserTest::TemplateURLParserTest() = default;

TemplateURLParserTest::~TemplateURLParserTest() = default;

void TemplateURLParserTest::SetUp() {
  ASSERT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &osdd_dir_));
  osdd_dir_ = osdd_dir_.AppendASCII("osdd");
  ASSERT_TRUE(base::PathExists(osdd_dir_));
}

void TemplateURLParserTest::ParseFile(
    const std::string& file_name,
    const TemplateURLParser::ParameterFilter& filter) {
  base::FilePath full_path = osdd_dir_.AppendASCII(file_name);
  ASSERT_TRUE(base::PathExists(full_path));

  std::string contents;
  ASSERT_TRUE(base::ReadFileToString(full_path, &contents));
  ParseString(contents, filter);
}

void TemplateURLParserTest::ParseString(
    const std::string& data,
    const TemplateURLParser::ParameterFilter& filter) {
  base::RunLoop run_loop;
  SearchTermsData search_terms_data;
  TemplateURLParser::Parse(
      &search_terms_data, data, filter,
      base::BindOnce(&TemplateURLParserTest::OnTemplateURLParsed,
                     base::Unretained(this), run_loop.QuitClosure()));
  run_loop.Run();
}

// Actual tests ---------------------------------------------------------------

TEST_F(TemplateURLParserTest, FailOnBogusURL) {
  ASSERT_NO_FATAL_FAILURE(
      ParseFile("bogus.xml", TemplateURLParser::ParameterFilter()));
  EXPECT_FALSE(template_url_);
}

TEST_F(TemplateURLParserTest, PassOnHTTPS) {
  ASSERT_NO_FATAL_FAILURE(
      ParseFile("https.xml", TemplateURLParser::ParameterFilter()));
  EXPECT_TRUE(template_url_);
}

TEST_F(TemplateURLParserTest, FailOnPost) {
  ASSERT_NO_FATAL_FAILURE(
      ParseFile("post.xml", TemplateURLParser::ParameterFilter()));
  EXPECT_FALSE(template_url_);
}

TEST_F(TemplateURLParserTest, TestDictionary) {
  ASSERT_NO_FATAL_FAILURE(
      ParseFile("dictionary.xml", TemplateURLParser::ParameterFilter()));
  ASSERT_TRUE(template_url_);
  EXPECT_EQ(u"Dictionary.com", template_url_->short_name());
  EXPECT_EQ(GURL("http://cache.lexico.com/g/d/favicon.ico"),
            template_url_->favicon_url());
  EXPECT_TRUE(template_url_->url_ref().SupportsReplacement(SearchTermsData()));
  EXPECT_EQ("http://dictionary.reference.com/browse/{searchTerms}?r=75",
            template_url_->url());
}

TEST_F(TemplateURLParserTest, TestMSDN) {
  ASSERT_NO_FATAL_FAILURE(
      ParseFile("msdn.xml", TemplateURLParser::ParameterFilter()));
  ASSERT_TRUE(template_url_);
  EXPECT_EQ(u"Search \" MSDN", template_url_->short_name());
  EXPECT_EQ(GURL("http://search.msdn.microsoft.com/search/favicon.ico"),
            template_url_->favicon_url());
  EXPECT_TRUE(template_url_->url_ref().SupportsReplacement(SearchTermsData()));
  EXPECT_EQ("http://search.msdn.microsoft.com/search/default.aspx?"
            "Query={searchTerms}&brand=msdn&locale=en-US",
            template_url_->url());
}

TEST_F(TemplateURLParserTest, TestWikipedia) {
  ASSERT_NO_FATAL_FAILURE(
      ParseFile("wikipedia.xml", TemplateURLParser::ParameterFilter()));
  ASSERT_TRUE(template_url_);
  EXPECT_EQ(u"Wikipedia (English)", template_url_->short_name());
  EXPECT_EQ(GURL("http://en.wikipedia.org/favicon.ico"),
            template_url_->favicon_url());
  EXPECT_TRUE(template_url_->url_ref().SupportsReplacement(SearchTermsData()));
  EXPECT_EQ("http://en.wikipedia.org/w/index.php?"
            "title=Special:Search&search={searchTerms}",
            template_url_->url());
  EXPECT_TRUE(template_url_->suggestions_url_ref().SupportsReplacement(
      SearchTermsData()));
  EXPECT_EQ("http://en.wikipedia.org/w/api.php?"
            "action=opensearch&search={searchTerms}",
            template_url_->suggestions_url());
  ASSERT_EQ(2U, template_url_->input_encodings().size());
  EXPECT_EQ("UTF-8", template_url_->input_encodings()[0]);
  EXPECT_EQ("Shift_JIS", template_url_->input_encodings()[1]);
}

TEST_F(TemplateURLParserTest, NoCrashOnEmptyAttributes) {
  ASSERT_NO_FATAL_FAILURE(ParseFile("url_with_no_attributes.xml",
                                    TemplateURLParser::ParameterFilter()));
}

TEST_F(TemplateURLParserTest, TestFirefoxEbay) {
  // This file uses the Parameter extension
  // (see http://www.opensearch.org/Specifications/OpenSearch/Extensions/Parameter/1.0)
  TemplateURLParser::ParameterFilter filter =
      base::BindRepeating(&TestFilter, "ebay", "ebay");
  ASSERT_NO_FATAL_FAILURE(ParseFile("firefox_ebay.xml", filter));
  ASSERT_TRUE(template_url_);
  EXPECT_EQ(u"eBay", template_url_->short_name());
  EXPECT_TRUE(template_url_->url_ref().SupportsReplacement(SearchTermsData()));
  EXPECT_EQ("http://search.ebay.com/search/search.dll?query={searchTerms}&"
            "MfcISAPICommand=GetResult&ht=1&srchdesc=n&maxRecordsReturned=300&"
            "maxRecordsPerPage=50&SortProperty=MetaEndSort",
            template_url_->url());
  ASSERT_EQ(1U, template_url_->input_encodings().size());
  EXPECT_EQ("ISO-8859-1", template_url_->input_encodings()[0]);
  EXPECT_EQ(GURL("http://search.ebay.com/favicon.ico"),
            template_url_->favicon_url());
}

TEST_F(TemplateURLParserTest, TestFirefoxWebster) {
  // This XML file uses a namespace.
  TemplateURLParser::ParameterFilter filter =
      base::BindRepeating(&TestFilter, std::string(), "Mozilla");
  ASSERT_NO_FATAL_FAILURE(ParseFile("firefox_webster.xml", filter));
  ASSERT_TRUE(template_url_);
  EXPECT_EQ(u"Webster", template_url_->short_name());
  EXPECT_TRUE(template_url_->url_ref().SupportsReplacement(SearchTermsData()));
  EXPECT_EQ("http://www.webster.com/cgi-bin/dictionary?va={searchTerms}",
            template_url_->url());
  ASSERT_EQ(1U, template_url_->input_encodings().size());
  EXPECT_EQ("ISO-8859-1", template_url_->input_encodings()[0]);
  EXPECT_EQ(GURL("http://www.webster.com/favicon.ico"),
            template_url_->favicon_url());
}

TEST_F(TemplateURLParserTest, TestFirefoxYahoo) {
  // This XML file uses a namespace.
  TemplateURLParser::ParameterFilter filter =
      base::BindRepeating(&TestFilter, std::string(), "Mozilla");
  ASSERT_NO_FATAL_FAILURE(ParseFile("firefox_yahoo.xml", filter));
  ASSERT_TRUE(template_url_);
  EXPECT_EQ(u"Yahoo", template_url_->short_name());
  EXPECT_TRUE(template_url_->url_ref().SupportsReplacement(SearchTermsData()));
  EXPECT_EQ("http://ff.search.yahoo.com/gossip?"
            "output=fxjson&command={searchTerms}",
            template_url_->suggestions_url());
  EXPECT_EQ("http://search.yahoo.com/search?p={searchTerms}&ei=UTF-8",
            template_url_->url());
  ASSERT_EQ(1U, template_url_->input_encodings().size());
  EXPECT_EQ("UTF-8", template_url_->input_encodings()[0]);
  EXPECT_EQ(GURL("http://search.yahoo.com/favicon.ico"),
            template_url_->favicon_url());
}

// Make sure we ignore POST suggestions (this is the same XML file as
// firefox_yahoo.xml, the suggestion method was just changed to POST).
TEST_F(TemplateURLParserTest, TestPostSuggestion) {
  // This XML file uses a namespace.
  TemplateURLParser::ParameterFilter filter =
      base::BindRepeating(&TestFilter, std::string(), "Mozilla");
  ASSERT_NO_FATAL_FAILURE(ParseFile("post_suggestion.xml", filter));
  ASSERT_TRUE(template_url_);
  EXPECT_EQ(u"Yahoo", template_url_->short_name());
  EXPECT_TRUE(template_url_->url_ref().SupportsReplacement(SearchTermsData()));
  EXPECT_TRUE(template_url_->suggestions_url().empty());
  EXPECT_EQ("http://search.yahoo.com/search?p={searchTerms}&ei=UTF-8",
            template_url_->url());
  ASSERT_EQ(1U, template_url_->input_encodings().size());
  EXPECT_EQ("UTF-8", template_url_->input_encodings()[0]);
  EXPECT_EQ(GURL("http://search.yahoo.com/favicon.ico"),
            template_url_->favicon_url());
}

// <Alias> tags are parsed and used as keyword for the template URL.
TEST_F(TemplateURLParserTest, TestKeyword) {
  ASSERT_NO_FATAL_FAILURE(
      ParseFile("keyword.xml", TemplateURLParser::ParameterFilter()));
  ASSERT_TRUE(template_url_);
  EXPECT_EQ(u"Example", template_url_->short_name());
  EXPECT_EQ("https://www.example.com/search?q={searchTerms}",
            template_url_->url());
  EXPECT_EQ(u"moose", template_url_->keyword());
}

// Empty <Alias> tags are ignored and the default keyword is used instead
// (because empty keywords are not allowed).
TEST_F(TemplateURLParserTest, TestEmptyKeyword) {
  ASSERT_NO_FATAL_FAILURE(
      ParseFile("empty_keyword.xml", TemplateURLParser::ParameterFilter()));
  ASSERT_TRUE(template_url_);
  EXPECT_EQ(u"Example", template_url_->short_name());
  EXPECT_EQ("https://www.example.com/search?q={searchTerms}",
            template_url_->url());
  EXPECT_EQ(u"example.com", template_url_->keyword());
}

// An invalid template URL should not crash the parser.
// crbug.com/40089184
TEST_F(TemplateURLParserTest, InvalidInput) {
  TemplateURLParser::ParameterFilter filter = base::BindRepeating(
      [](const std::string&, const std::string&) -> bool { return true; });
  constexpr char char_data[] = R"(
    <OpenSearchDescription>
    <Url template=")R:RRR?>RRR0" type="application/x-suggestions+json">
      <Param name="name" value="value"/>
    </Url>
    </OpenSearchDescription>
  )";
  ParseString(char_data, filter);
}
