// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/memory/scoped_ptr.h"
#include "base/message_loop.h"
#include "base/string_number_conversions.h"
#include "base/string_util.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/autocomplete/autocomplete.h"
#include "chrome/browser/autocomplete/autocomplete_match.h"
#include "chrome/browser/autocomplete/keyword_provider.h"
#include "chrome/browser/autocomplete/search_provider.h"
#include "chrome/browser/search_engines/template_url.h"
#include "chrome/browser/search_engines/template_url_model.h"
#include "chrome/test/testing_browser_process.h"
#include "chrome/test/testing_browser_process_test.h"
#include "chrome/test/testing_profile.h"
#include "content/common/notification_observer.h"
#include "content/common/notification_registrar.h"
#include "content/common/notification_service.h"
#include "testing/gtest/include/gtest/gtest.h"

static std::ostream& operator<<(std::ostream& os,
                                const AutocompleteResult::const_iterator& it) {
  return os << static_cast<const AutocompleteMatch*>(&(*it));
}

namespace {

const size_t num_results_per_provider = 3;

// Autocomplete provider that provides known results. Note that this is
// refcounted so that it can also be a task on the message loop.
class TestProvider : public AutocompleteProvider {
 public:
  TestProvider(int relevance, const string16& prefix)
      : AutocompleteProvider(NULL, NULL, ""),
        relevance_(relevance),
        prefix_(prefix) {
  }

  virtual void Start(const AutocompleteInput& input,
                     bool minimal_changes);

  void set_listener(ACProviderListener* listener) {
    listener_ = listener;
  }

 private:
  ~TestProvider() {}

  void Run();

  void AddResults(int start_at, int num);

  int relevance_;
  const string16 prefix_;
};

void TestProvider::Start(const AutocompleteInput& input,
                         bool minimal_changes) {
  if (minimal_changes)
    return;

  matches_.clear();

  // Generate one result synchronously, the rest later.
  AddResults(0, 1);

  if (input.matches_requested() == AutocompleteInput::ALL_MATCHES) {
    done_ = false;
    MessageLoop::current()->PostTask(FROM_HERE, NewRunnableMethod(
        this, &TestProvider::Run));
  }
}

void TestProvider::Run() {
  DCHECK_GT(num_results_per_provider, 0U);
  AddResults(1, num_results_per_provider);
  done_ = true;
  DCHECK(listener_);
  listener_->OnProviderUpdate(true);
}

void TestProvider::AddResults(int start_at, int num) {
  for (int i = start_at; i < num; i++) {
    AutocompleteMatch match(this, relevance_ - i, false,
                            AutocompleteMatch::URL_WHAT_YOU_TYPED);

    match.fill_into_edit = prefix_ + UTF8ToUTF16(base::IntToString(i));
    match.destination_url = GURL(UTF16ToUTF8(match.fill_into_edit));

    match.contents = match.fill_into_edit;
    match.contents_class.push_back(
        ACMatchClassification(0, ACMatchClassification::NONE));
    match.description = match.fill_into_edit;
    match.description_class.push_back(
        ACMatchClassification(0, ACMatchClassification::NONE));

    matches_.push_back(match);
  }
}

class AutocompleteProviderTest : public testing::Test,
                                 public NotificationObserver {
 protected:
  void ResetControllerWithTestProviders(bool same_destinations);

  // Runs a query on the input "a", and makes sure both providers' input is
  // properly collected.
  void RunTest();

  void ResetControllerWithTestProvidersWithKeywordAndSearchProviders();
  void RunExactKeymatchTest(bool allow_exact_keyword_match);

  // These providers are owned by the controller once it's created.
  ACProviders providers_;

  AutocompleteResult result_;

 private:
  // NotificationObserver
  virtual void Observe(NotificationType type,
                       const NotificationSource& source,
                       const NotificationDetails& details);

  ScopedTestingBrowserProcess browser_process_;

  MessageLoopForUI message_loop_;
  scoped_ptr<AutocompleteController> controller_;
  NotificationRegistrar registrar_;
  TestingProfile profile_;
};

void AutocompleteProviderTest::ResetControllerWithTestProviders(
    bool same_destinations) {
  // Forget about any existing providers.  The controller owns them and will
  // Release() them below, when we delete it during the call to reset().
  providers_.clear();

  // Construct two new providers, with either the same or different prefixes.
  TestProvider* providerA = new TestProvider(num_results_per_provider,
                                             ASCIIToUTF16("http://a"));
  providerA->AddRef();
  providers_.push_back(providerA);

  TestProvider* providerB = new TestProvider(num_results_per_provider * 2,
      same_destinations ? ASCIIToUTF16("http://a") : ASCIIToUTF16("http://b"));
  providerB->AddRef();
  providers_.push_back(providerB);

  // Reset the controller to contain our new providers.
  AutocompleteController* controller = new AutocompleteController(providers_);
  controller_.reset(controller);
  providerA->set_listener(controller);
  providerB->set_listener(controller);

  // The providers don't complete synchronously, so listen for "result updated"
  // notifications.
  registrar_.Add(this, NotificationType::AUTOCOMPLETE_CONTROLLER_RESULT_READY,
                 NotificationService::AllSources());
}

void AutocompleteProviderTest::
    ResetControllerWithTestProvidersWithKeywordAndSearchProviders() {
  profile_.CreateTemplateURLModel();

  // Reset the default TemplateURL.
  TemplateURL* default_t_url = new TemplateURL();
  default_t_url->SetURL("http://defaultturl/{searchTerms}", 0, 0);
  TemplateURLModel* turl_model = profile_.GetTemplateURLModel();
  turl_model->Add(default_t_url);
  turl_model->SetDefaultSearchProvider(default_t_url);
  TemplateURLID default_provider_id = default_t_url->id();
  ASSERT_NE(0, default_provider_id);

  // Create another TemplateURL for KeywordProvider.
  TemplateURL* keyword_t_url = new TemplateURL();
  keyword_t_url->set_short_name(ASCIIToUTF16("k"));
  keyword_t_url->set_keyword(ASCIIToUTF16("k"));
  keyword_t_url->SetURL("http://keyword/{searchTerms}", 0, 0);
  profile_.GetTemplateURLModel()->Add(keyword_t_url);
  ASSERT_NE(0, keyword_t_url->id());

  // Forget about any existing providers.  The controller owns them and will
  // Release() them below, when we delete it during the call to reset().
  providers_.clear();

  // Create both a keyword and search provider, and add them in that order.
  // (Order is important; see comments in RunExactKeymatchTest().)
  AutocompleteProvider* keyword_provider = new KeywordProvider(NULL,
                                                                &profile_);
  keyword_provider->AddRef();
  providers_.push_back(keyword_provider);
  AutocompleteProvider* search_provider = new SearchProvider(NULL, &profile_);
  search_provider->AddRef();
  providers_.push_back(search_provider);

  AutocompleteController* controller = new AutocompleteController(providers_);
  controller_.reset(controller);
}

void AutocompleteProviderTest::RunTest() {
  result_.Reset();
  controller_->Start(ASCIIToUTF16("a"), string16(), true, false, true,
                     AutocompleteInput::ALL_MATCHES);

  // The message loop will terminate when all autocomplete input has been
  // collected.
  MessageLoop::current()->Run();
}

void AutocompleteProviderTest::RunExactKeymatchTest(
    bool allow_exact_keyword_match) {
  // Send the controller input which exactly matches the keyword provider we
  // created in ResetControllerWithKeywordAndSearchProviders().  The default
  // match should thus be a keyword match iff |allow_exact_keyword_match| is
  // true.
  controller_->Start(ASCIIToUTF16("k test"), string16(), true, false,
                     allow_exact_keyword_match,
                     AutocompleteInput::SYNCHRONOUS_MATCHES);
  EXPECT_TRUE(controller_->done());
  // ResetControllerWithKeywordAndSearchProviders() adds the keyword provider
  // first, then the search provider.  So if the default match is a keyword
  // match, it will come from provider 0, otherwise from provider 1.
  EXPECT_EQ(providers_[allow_exact_keyword_match ? 0 : 1],
      controller_->result().default_match()->provider);
}

void AutocompleteProviderTest::Observe(NotificationType type,
                                       const NotificationSource& source,
                                       const NotificationDetails& details) {
  if (controller_->done()) {
    result_.CopyFrom(controller_->result());
    MessageLoop::current()->Quit();
  }
}

// Tests that the default selection is set properly when updating results.
TEST_F(AutocompleteProviderTest, Query) {
  ResetControllerWithTestProviders(false);
  RunTest();

  // Make sure the default match gets set to the highest relevance match.  The
  // highest relevance matches should come from the second provider.
  EXPECT_EQ(num_results_per_provider * 2, result_.size());  // two providers
  ASSERT_NE(result_.end(), result_.default_match());
  EXPECT_EQ(providers_[1], result_.default_match()->provider);
}

TEST_F(AutocompleteProviderTest, RemoveDuplicates) {
  ResetControllerWithTestProviders(true);
  RunTest();

  // Make sure all the first provider's results were eliminated by the second
  // provider's.
  EXPECT_EQ(num_results_per_provider, result_.size());
  for (AutocompleteResult::const_iterator i(result_.begin());
       i != result_.end(); ++i)
    EXPECT_EQ(providers_[1], i->provider);
}

TEST_F(AutocompleteProviderTest, AllowExactKeywordMatch) {
  ResetControllerWithTestProvidersWithKeywordAndSearchProviders();
  RunExactKeymatchTest(true);
  RunExactKeymatchTest(false);
}

typedef TestingBrowserProcessTest AutocompleteTest;

TEST_F(AutocompleteTest, InputType) {
  struct test_data {
    const string16 input;
    const AutocompleteInput::Type type;
  } input_cases[] = {
    { ASCIIToUTF16(""), AutocompleteInput::INVALID },
    { ASCIIToUTF16("?"), AutocompleteInput::FORCED_QUERY },
    { ASCIIToUTF16("?foo"), AutocompleteInput::FORCED_QUERY },
    { ASCIIToUTF16("?foo bar"), AutocompleteInput::FORCED_QUERY },
    { ASCIIToUTF16("?http://foo.com/bar"), AutocompleteInput::FORCED_QUERY },
    { ASCIIToUTF16("foo"), AutocompleteInput::UNKNOWN },
    { ASCIIToUTF16("foo.c"), AutocompleteInput::UNKNOWN },
    { ASCIIToUTF16("foo.com"), AutocompleteInput::URL },
    { ASCIIToUTF16("-.com"), AutocompleteInput::UNKNOWN },
    { ASCIIToUTF16("foo/bar"), AutocompleteInput::URL },
    { ASCIIToUTF16("foo;bar"), AutocompleteInput::QUERY },
    { ASCIIToUTF16("foo/bar baz"), AutocompleteInput::UNKNOWN },
    { ASCIIToUTF16("foo bar.com"), AutocompleteInput::QUERY },
    { ASCIIToUTF16("foo bar"), AutocompleteInput::QUERY },
    { ASCIIToUTF16("foo+bar"), AutocompleteInput::QUERY },
    { ASCIIToUTF16("foo+bar.com"), AutocompleteInput::UNKNOWN },
    { ASCIIToUTF16("\"foo:bar\""), AutocompleteInput::QUERY },
    { ASCIIToUTF16("link:foo.com"), AutocompleteInput::UNKNOWN },
    { ASCIIToUTF16("foo:81"), AutocompleteInput::URL },
    { ASCIIToUTF16("www.foo.com:81"), AutocompleteInput::URL },
    { ASCIIToUTF16("localhost:8080"), AutocompleteInput::URL },
    { ASCIIToUTF16("foo.com:123456"), AutocompleteInput::QUERY },
    { ASCIIToUTF16("foo.com:abc"), AutocompleteInput::QUERY },
    { ASCIIToUTF16("1.2.3.4:abc"), AutocompleteInput::QUERY },
    { ASCIIToUTF16("user@foo.com"), AutocompleteInput::UNKNOWN },
    { ASCIIToUTF16("user:pass@"), AutocompleteInput::UNKNOWN },
    { ASCIIToUTF16("user:pass@!foo.com"), AutocompleteInput::UNKNOWN },
    { ASCIIToUTF16("user:pass@foo"), AutocompleteInput::URL },
    { ASCIIToUTF16("user:pass@foo.c"), AutocompleteInput::URL },
    { ASCIIToUTF16("user:pass@foo.com"), AutocompleteInput::URL },
    { ASCIIToUTF16("user:pass@foo.com:81"), AutocompleteInput::URL },
    { ASCIIToUTF16("user:pass@foo:81"), AutocompleteInput::URL },
    { ASCIIToUTF16("1.2"), AutocompleteInput::UNKNOWN },
    { ASCIIToUTF16("1.2/45"), AutocompleteInput::UNKNOWN },
    { ASCIIToUTF16("1.2:45"), AutocompleteInput::UNKNOWN },
    { ASCIIToUTF16("user@1.2:45"), AutocompleteInput::UNKNOWN },
    { ASCIIToUTF16("user:pass@1.2:45"), AutocompleteInput::URL },
    { ASCIIToUTF16("ps/2 games"), AutocompleteInput::UNKNOWN },
    { ASCIIToUTF16("en.wikipedia.org/wiki/James Bond"),
        AutocompleteInput::URL },
    // In Chrome itself, mailto: will get handled by ShellExecute, but in
    // unittest mode, we don't have the data loaded in the external protocol
    // handler to know this.
    // { ASCIIToUTF16("mailto:abuse@foo.com"), AutocompleteInput::URL },
    { ASCIIToUTF16("view-source:http://www.foo.com/"), AutocompleteInput::URL },
    { ASCIIToUTF16("javascript:alert(\"Hey there!\");"),
        AutocompleteInput::URL },
#if defined(OS_WIN)
    { ASCIIToUTF16("C:\\Program Files"), AutocompleteInput::URL },
    { ASCIIToUTF16("\\\\Server\\Folder\\File"), AutocompleteInput::URL },
#endif  // defined(OS_WIN)
    { ASCIIToUTF16("http:foo"), AutocompleteInput::URL },
    { ASCIIToUTF16("http://foo"), AutocompleteInput::URL },
    { ASCIIToUTF16("http://foo.c"), AutocompleteInput::URL },
    { ASCIIToUTF16("http://foo.com"), AutocompleteInput::URL },
    { ASCIIToUTF16("http://foo_bar.com"), AutocompleteInput::URL },
    { ASCIIToUTF16("http://foo/bar baz"), AutocompleteInput::URL },
    { ASCIIToUTF16("http://-.com"), AutocompleteInput::UNKNOWN },
    { ASCIIToUTF16("http://_foo_.com"), AutocompleteInput::UNKNOWN },
    { ASCIIToUTF16("http://foo.com:abc"), AutocompleteInput::QUERY },
    { ASCIIToUTF16("http://foo.com:123456"), AutocompleteInput::QUERY },
    { ASCIIToUTF16("http://1.2.3.4:abc"), AutocompleteInput::QUERY },
    { ASCIIToUTF16("http:user@foo.com"), AutocompleteInput::URL },
    { ASCIIToUTF16("http://user@foo.com"), AutocompleteInput::URL },
    { ASCIIToUTF16("http:user:pass@foo.com"), AutocompleteInput::URL },
    { ASCIIToUTF16("http://user:pass@foo.com"), AutocompleteInput::URL },
    { ASCIIToUTF16("http://1.2"), AutocompleteInput::URL },
    { ASCIIToUTF16("http://1.2/45"), AutocompleteInput::URL },
    { ASCIIToUTF16("http:ps/2 games"), AutocompleteInput::URL },
    { ASCIIToUTF16("http://ps/2 games"), AutocompleteInput::URL },
    { ASCIIToUTF16("https://foo.com"), AutocompleteInput::URL },
    { ASCIIToUTF16("127.0.0.1"), AutocompleteInput::URL },
    { ASCIIToUTF16("127.0.1"), AutocompleteInput::UNKNOWN },
    { ASCIIToUTF16("127.0.1/"), AutocompleteInput::UNKNOWN },
    { ASCIIToUTF16("browser.tabs.closeButtons"), AutocompleteInput::UNKNOWN },
    { WideToUTF16(L"\u6d4b\u8bd5"), AutocompleteInput::UNKNOWN },
    { ASCIIToUTF16("[2001:]"), AutocompleteInput::QUERY },  // Not a valid IP
    { ASCIIToUTF16("[2001:dB8::1]"), AutocompleteInput::URL },
    { ASCIIToUTF16("192.168.0.256"),
        AutocompleteInput::QUERY },  // Invalid IPv4 literal.
    { ASCIIToUTF16("[foo.com]"),
        AutocompleteInput::QUERY },  // Invalid IPv6 literal.
  };

  for (size_t i = 0; i < ARRAYSIZE_UNSAFE(input_cases); ++i) {
    SCOPED_TRACE(input_cases[i].input);
    AutocompleteInput input(input_cases[i].input, string16(), true, false,
                            true, AutocompleteInput::ALL_MATCHES);
    EXPECT_EQ(input_cases[i].type, input.type());
  }
}

TEST_F(AutocompleteTest, InputTypeWithDesiredTLD) {
  struct test_data {
    const string16 input;
    const AutocompleteInput::Type type;
  } input_cases[] = {
    { ASCIIToUTF16("401k"), AutocompleteInput::REQUESTED_URL },
    { ASCIIToUTF16("999999999999999"), AutocompleteInput::REQUESTED_URL },
  };

  for (size_t i = 0; i < ARRAYSIZE_UNSAFE(input_cases); ++i) {
    AutocompleteInput input(input_cases[i].input, ASCIIToUTF16("com"), true,
                            false, true, AutocompleteInput::ALL_MATCHES);
    EXPECT_EQ(input_cases[i].type, input.type()) << "Input: " <<
        input_cases[i].input;
  }
}

// This tests for a regression where certain input in the omnibox caused us to
// crash. As long as the test completes without crashing, we're fine.
TEST_F(AutocompleteTest, InputCrash) {
  AutocompleteInput input(WideToUTF16(L"\uff65@s"), string16(), true, false,
                          true, AutocompleteInput::ALL_MATCHES);
}

// Test comparing matches relevance.
TEST(AutocompleteMatch, MoreRelevant) {
  struct RelevantCases {
    int r1;
    int r2;
    bool expected_result;
  } cases[] = {
    {  10,   0, true  },
    {  10,  -5, true  },
    {  -5,  10, false },
    {   0,  10, false },
    { -10,  -5, false  },
    {  -5, -10, true },
  };

  AutocompleteMatch m1(NULL, 0, false, AutocompleteMatch::URL_WHAT_YOU_TYPED);
  AutocompleteMatch m2(NULL, 0, false, AutocompleteMatch::URL_WHAT_YOU_TYPED);

  for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) {
    m1.relevance = cases[i].r1;
    m2.relevance = cases[i].r2;
    EXPECT_EQ(cases[i].expected_result,
              AutocompleteMatch::MoreRelevant(m1, m2));
  }
}

TEST(AutocompleteInput, ParseForEmphasizeComponent) {
  using url_parse::Component;
  Component kInvalidComponent(0, -1);
  struct test_data {
    const string16 input;
    const Component scheme;
    const Component host;
  } input_cases[] = {
    { ASCIIToUTF16(""), kInvalidComponent, kInvalidComponent },
    { ASCIIToUTF16("?"), kInvalidComponent, kInvalidComponent },
    { ASCIIToUTF16("?http://foo.com/bar"), kInvalidComponent,
        kInvalidComponent },
    { ASCIIToUTF16("foo/bar baz"), kInvalidComponent, Component(0, 3) },
    { ASCIIToUTF16("http://foo/bar baz"), Component(0, 4), Component(7, 3) },
    { ASCIIToUTF16("link:foo.com"), Component(0, 4), kInvalidComponent },
    { ASCIIToUTF16("www.foo.com:81"), kInvalidComponent, Component(0, 11) },
    { WideToUTF16(L"\u6d4b\u8bd5"), kInvalidComponent, Component(0, 2) },
    { ASCIIToUTF16("view-source:http://www.foo.com/"), Component(12, 4),
        Component(19, 11) },
    { ASCIIToUTF16("view-source:https://example.com/"),
      Component(12, 5), Component(20, 11) },
    { ASCIIToUTF16("view-source:www.foo.com"), kInvalidComponent,
        Component(12, 11) },
    { ASCIIToUTF16("view-source:"), Component(0, 11), kInvalidComponent },
    { ASCIIToUTF16("view-source:garbage"), kInvalidComponent,
        Component(12, 7) },
    { ASCIIToUTF16("view-source:http://http://foo"), Component(12, 4),
        Component(19, 4) },
    { ASCIIToUTF16("view-source:view-source:http://example.com/"),
        Component(12, 11), kInvalidComponent }
  };

  ScopedTestingBrowserProcess browser_process;

  for (size_t i = 0; i < ARRAYSIZE_UNSAFE(input_cases); ++i) {
    Component scheme, host;
    AutocompleteInput::ParseForEmphasizeComponents(input_cases[i].input,
                                                   string16(),
                                                   &scheme,
                                                   &host);
    AutocompleteInput input(input_cases[i].input, string16(), true, false,
                            true, AutocompleteInput::ALL_MATCHES);
    EXPECT_EQ(input_cases[i].scheme.begin, scheme.begin) << "Input: " <<
        input_cases[i].input;
    EXPECT_EQ(input_cases[i].scheme.len, scheme.len) << "Input: " <<
        input_cases[i].input;
    EXPECT_EQ(input_cases[i].host.begin, host.begin) << "Input: " <<
        input_cases[i].input;
    EXPECT_EQ(input_cases[i].host.len, host.len) << "Input: " <<
        input_cases[i].input;
  }
}

}  // namespace