// Copyright 2015 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 "chrome/browser/android/contextualsearch/contextual_search_delegate.h"

#include <stddef.h>

#include "base/base64.h"
#include "base/macros.h"
#include "base/memory/scoped_ptr.h"
#include "base/message_loop/message_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/android/contextualsearch/contextual_search_context.h"
#include "chrome/browser/android/contextualsearch/resolved_search_term.h"
#include "chrome/browser/android/proto/client_discourse_context.pb.h"
#include "components/search_engines/template_url_service.h"
#include "net/base/escape.h"
#include "net/url_request/test_url_fetcher_factory.h"
#include "net/url_request/url_request_test_util.h"
#include "testing/gtest/include/gtest/gtest.h"

using base::ListValue;

namespace {

const char kSomeSpecificBasePage[] = "http://some.specific.host.name.com/";
const char kDiscourseContextHeaderName[] = "X-Additional-Discourse-Context";

}  // namespace

class ContextualSearchDelegateTest : public testing::Test {
 public:
  ContextualSearchDelegateTest() {}
  ~ContextualSearchDelegateTest() override {}

 protected:
  void SetUp() override {
    request_context_ =
        new net::TestURLRequestContextGetter(io_message_loop_.task_runner());
    template_url_service_.reset(CreateTemplateURLService());
    delegate_.reset(new ContextualSearchDelegate(
        request_context_.get(), template_url_service_.get(),
        base::Bind(
            &ContextualSearchDelegateTest::recordSearchTermResolutionResponse,
            base::Unretained(this)),
        base::Bind(&ContextualSearchDelegateTest::recordSurroundingText,
                   base::Unretained(this)),
        base::Bind(&ContextualSearchDelegateTest::recordIcingSelectionAvailable,
                   base::Unretained(this))));
  }

  void TearDown() override {
    fetcher_ = NULL;
    is_invalid_ = true;
    response_code_ = -1;
    search_term_ = "invalid";
    display_text_ = "unknown";
    context_language_ = "";
  }

  TemplateURLService* CreateTemplateURLService() {
    // Set a default search provider that supports Contextual Search.
    TemplateURLData data;
    data.SetURL("https://foobar.com/url?bar={searchTerms}");
    data.contextual_search_url = "https://foobar.com/_/contextualsearch?"
        "{google:contextualSearchVersion}{google:contextualSearchContextData}";
    TemplateURL* template_url = new TemplateURL(data);
    // Takes ownership of |template_url|.
    TemplateURLService* template_url_service = new TemplateURLService(NULL, 0);
    template_url_service->Add(template_url);
    template_url_service->SetUserSelectedDefaultSearchProvider(template_url);
    return template_url_service;
  }

  void CreateDefaultSearchContextAndRequestSearchTerm() {
    base::string16 surrounding = base::UTF8ToUTF16("Barack Obama just spoke.");
    CreateSearchContextAndRequestSearchTerm("Barack Obama", surrounding, 0, 6);
  }

  void CreateSearchContextAndRequestSearchTerm(
      const std::string& selected_text,
      const base::string16& surrounding_text,
      int start_offset,
      int end_offset) {
    test_context_ = new ContextualSearchContext(
        selected_text, true, GURL(kSomeSpecificBasePage), "utf-8");
    // ContextualSearchDelegate class takes ownership of the context.
    delegate_->set_context_for_testing(test_context_);

    test_context_->start_offset = start_offset;
    test_context_->end_offset = end_offset;
    test_context_->surrounding_text = surrounding_text;
    delegate_->ContinueSearchTermResolutionRequest();
    fetcher_ = test_factory_.GetFetcherByID(
        ContextualSearchDelegate::kContextualSearchURLFetcherID);
    ASSERT_TRUE(fetcher_);
    ASSERT_TRUE(fetcher());
  }

  void SetResponseStringAndFetch(const std::string& selected_text,
                                 const std::string& mentions_start,
                                 const std::string& mentions_end) {
    fetcher()->set_response_code(200);
      fetcher()->SetResponseString(
          ")]}'\n"
          "{\"mid\":\"/m/02mjmr\", \"search_term\":\"obama\","
          "\"info_text\":\"44th U.S. President\","
          "\"display_text\":\"Barack Obama\","
          "\"mentions\":[" + mentions_start + ","+ mentions_end + "],"
          "\"selected_text\":\"" + selected_text + "\","
          "\"resolved_term\":\"barack obama\"}");
      fetcher()->delegate()->OnURLFetchComplete(fetcher());
  }

  void SetSurroundingContext(const base::string16& surrounding_text,
                             int start_offset,
                             int end_offset) {
    test_context_ = new ContextualSearchContext(
        "Bogus", true, GURL(kSomeSpecificBasePage), "utf-8");
    test_context_->surrounding_text = surrounding_text;
    test_context_->start_offset = start_offset;
    test_context_->end_offset = end_offset;
    // ContextualSearchDelegate class takes ownership of the context.
    delegate_->set_context_for_testing(test_context_);
  }

  // Gets the Client Discourse Context proto from the request header.
  discourse_context::ClientDiscourseContext GetDiscourseContextFromRequest() {
    discourse_context::ClientDiscourseContext cdc;
    // Make sure we can get the actual raw headers from the fake fetcher.
    net::HttpRequestHeaders fetch_headers;
    fetcher()->GetExtraRequestHeaders(&fetch_headers);
    if (fetch_headers.HasHeader(kDiscourseContextHeaderName)) {
      std::string actual_header_value;
      fetch_headers.GetHeader(kDiscourseContextHeaderName,
                              &actual_header_value);

      // Unescape, since the server memoizer expects a web-safe encoding.
      std::string unescaped_header = actual_header_value;
      std::replace(unescaped_header.begin(), unescaped_header.end(), '-', '+');
      std::replace(unescaped_header.begin(), unescaped_header.end(), '_', '/');

      // Base64 decode the header.
      std::string decoded_header;
      if (base::Base64Decode(unescaped_header, &decoded_header)) {
        cdc.ParseFromString(decoded_header);
      }
    }
    return cdc;
  }

  // Gets the base-page URL from the request, or an empty string if not present.
  std::string getBasePageUrlFromRequest() {
    std::string result;
    discourse_context::ClientDiscourseContext cdc =
        GetDiscourseContextFromRequest();
    if (cdc.display_size() > 0) {
      const discourse_context::Display& first_display = cdc.display(0);
      result = first_display.uri();
    }
    return result;
  }

  net::TestURLFetcher* fetcher() { return fetcher_; }
  bool is_invalid() { return is_invalid_; }
  int response_code() { return response_code_; }
  std::string search_term() { return search_term_; }
  std::string display_text() { return display_text_; }
  std::string alternate_term() { return alternate_term_; }
  bool do_prevent_preload() { return prevent_preload_; }
  std::string after_text() { return after_text_; }
  int start_adjust() { return start_adjust_; }
  int end_adjust() { return end_adjust_; }
  std::string context_language() { return context_language_; }

  // The delegate under test.
  scoped_ptr<ContextualSearchDelegate> delegate_;

 private:
  void recordSearchTermResolutionResponse(
      const ResolvedSearchTerm& resolved_search_term) {
    is_invalid_ = resolved_search_term.is_invalid;
    response_code_ = resolved_search_term.response_code;
    search_term_ = resolved_search_term.search_term;
    display_text_ = resolved_search_term.display_text;
    alternate_term_ = resolved_search_term.alternate_term;
    prevent_preload_ = resolved_search_term.prevent_preload;
    start_adjust_ = resolved_search_term.selection_start_adjust;
    end_adjust_ = resolved_search_term.selection_end_adjust;
    context_language_ = resolved_search_term.context_language;
  }

  void recordSurroundingText(const std::string& after_text) {
    after_text_ = after_text;
  }

  void recordIcingSelectionAvailable(const std::string& encoding,
                                     const base::string16& surrounding_text,
                                     size_t start_offset,
                                     size_t end_offset) {
    // unused.
  }

  bool is_invalid_;
  int response_code_;
  std::string search_term_;
  std::string display_text_;
  std::string alternate_term_;
  bool prevent_preload_;
  int start_adjust_;
  int end_adjust_;
  std::string after_text_;
  std::string context_language_;

  base::MessageLoopForIO io_message_loop_;
  net::TestURLFetcherFactory test_factory_;
  net::TestURLFetcher* fetcher_;
  scoped_ptr<TemplateURLService> template_url_service_;
  scoped_refptr<net::TestURLRequestContextGetter> request_context_;

  // Will be owned by the delegate.
  ContextualSearchContext* test_context_;

  DISALLOW_COPY_AND_ASSIGN(ContextualSearchDelegateTest);
};

TEST_F(ContextualSearchDelegateTest, NormalFetchWithXssiEscape) {
  CreateDefaultSearchContextAndRequestSearchTerm();
  fetcher()->set_response_code(200);
  fetcher()->SetResponseString(
      ")]}'\n"
      "{\"mid\":\"/m/02mjmr\", \"search_term\":\"obama\","
      "\"info_text\":\"44th U.S. President\","
      "\"display_text\":\"Barack Obama\", \"mentions\":[0,15],"
      "\"selected_text\":\"obama\", \"resolved_term\":\"barack obama\"}");
  fetcher()->delegate()->OnURLFetchComplete(fetcher());

  EXPECT_FALSE(is_invalid());
  EXPECT_EQ(200, response_code());
  EXPECT_EQ("obama", search_term());
  EXPECT_EQ("Barack Obama", display_text());
  EXPECT_FALSE(do_prevent_preload());
}

TEST_F(ContextualSearchDelegateTest, NormalFetchWithoutXssiEscape) {
  CreateDefaultSearchContextAndRequestSearchTerm();
  fetcher()->set_response_code(200);
  fetcher()->SetResponseString(
      "{\"mid\":\"/m/02mjmr\", \"search_term\":\"obama\","
      "\"info_text\":\"44th U.S. President\","
      "\"display_text\":\"Barack Obama\", \"mentions\":[0,15],"
      "\"selected_text\":\"obama\", \"resolved_term\":\"barack obama\"}");
  fetcher()->delegate()->OnURLFetchComplete(fetcher());

  EXPECT_FALSE(is_invalid());
  EXPECT_EQ(200, response_code());
  EXPECT_EQ("obama", search_term());
  EXPECT_EQ("Barack Obama", display_text());
  EXPECT_FALSE(do_prevent_preload());
}

TEST_F(ContextualSearchDelegateTest, ResponseWithNoDisplayText) {
  CreateDefaultSearchContextAndRequestSearchTerm();
  fetcher()->set_response_code(200);
  fetcher()->SetResponseString(
      "{\"mid\":\"/m/02mjmr\",\"search_term\":\"obama\","
      "\"mentions\":[0,15]}");
  fetcher()->delegate()->OnURLFetchComplete(fetcher());

  EXPECT_FALSE(is_invalid());
  EXPECT_EQ(200, response_code());
  EXPECT_EQ("obama", search_term());
  EXPECT_EQ("obama", display_text());
  EXPECT_FALSE(do_prevent_preload());
}

TEST_F(ContextualSearchDelegateTest, ResponseWithPreventPreload) {
  CreateDefaultSearchContextAndRequestSearchTerm();
  fetcher()->set_response_code(200);
  fetcher()->SetResponseString(
      "{\"mid\":\"/m/02mjmr\",\"search_term\":\"obama\","
      "\"mentions\":[0,15],\"prevent_preload\":\"1\"}");
  fetcher()->delegate()->OnURLFetchComplete(fetcher());

  EXPECT_FALSE(is_invalid());
  EXPECT_EQ(200, response_code());
  EXPECT_EQ("obama", search_term());
  EXPECT_EQ("obama", display_text());
  EXPECT_TRUE(do_prevent_preload());
}

TEST_F(ContextualSearchDelegateTest, NonJsonResponse) {
  CreateDefaultSearchContextAndRequestSearchTerm();
  fetcher()->set_response_code(200);
  fetcher()->SetResponseString("Non-JSON Response");
  fetcher()->delegate()->OnURLFetchComplete(fetcher());

  EXPECT_FALSE(is_invalid());
  EXPECT_EQ(200, response_code());
  EXPECT_EQ("", search_term());
  EXPECT_EQ("", display_text());
  EXPECT_FALSE(do_prevent_preload());
}

TEST_F(ContextualSearchDelegateTest, InvalidResponse) {
  CreateDefaultSearchContextAndRequestSearchTerm();
  fetcher()->set_response_code(net::URLFetcher::RESPONSE_CODE_INVALID);
  fetcher()->delegate()->OnURLFetchComplete(fetcher());

  EXPECT_FALSE(do_prevent_preload());
  EXPECT_TRUE(is_invalid());
}

TEST_F(ContextualSearchDelegateTest, ExpandSelectionToEnd) {
  base::string16 surrounding = base::UTF8ToUTF16("Barack Obama just spoke.");
  std::string selected_text = "Barack";
  CreateSearchContextAndRequestSearchTerm(selected_text, surrounding, 0, 6);
  SetResponseStringAndFetch(selected_text, "0", "12");

  EXPECT_EQ(0, start_adjust());
  EXPECT_EQ(6, end_adjust());
}

TEST_F(ContextualSearchDelegateTest, ExpandSelectionToStart) {
  base::string16 surrounding = base::UTF8ToUTF16("Barack Obama just spoke.");
  std::string selected_text = "Obama";
  CreateSearchContextAndRequestSearchTerm(selected_text, surrounding, 7, 12);
  SetResponseStringAndFetch(selected_text, "0", "12");

  EXPECT_EQ(-7, start_adjust());
  EXPECT_EQ(0, end_adjust());
}

TEST_F(ContextualSearchDelegateTest, ExpandSelectionBothDirections) {
  base::string16 surrounding = base::UTF8ToUTF16("Barack Obama just spoke.");
  std::string selected_text = "Ob";
  CreateSearchContextAndRequestSearchTerm(selected_text, surrounding, 7, 9);
  SetResponseStringAndFetch(selected_text, "0", "12");

  EXPECT_EQ(-7, start_adjust());
  EXPECT_EQ(3, end_adjust());
}

TEST_F(ContextualSearchDelegateTest, ExpandSelectionInvalidRange) {
  base::string16 surrounding = base::UTF8ToUTF16("Barack Obama just spoke.");
  std::string selected_text = "Ob";
  CreateSearchContextAndRequestSearchTerm(selected_text, surrounding, 7, 9);
  SetResponseStringAndFetch(selected_text, "0", "200");

  EXPECT_EQ(0, start_adjust());
  EXPECT_EQ(0, end_adjust());
}

TEST_F(ContextualSearchDelegateTest, ExpandSelectionInvalidDistantStart) {
  base::string16 surrounding = base::UTF8ToUTF16("Barack Obama just spoke.");
  std::string selected_text = "Ob";
  CreateSearchContextAndRequestSearchTerm(selected_text, surrounding,
                                          0xffffffff, 0xffffffff - 2);
  SetResponseStringAndFetch(selected_text, "0", "12");

  EXPECT_EQ(0, start_adjust());
  EXPECT_EQ(0, end_adjust());
}

TEST_F(ContextualSearchDelegateTest, ExpandSelectionInvalidNoOverlap) {
  base::string16 surrounding = base::UTF8ToUTF16("Barack Obama just spoke.");
  std::string selected_text = "Ob";
  CreateSearchContextAndRequestSearchTerm(selected_text, surrounding, 0, 12);
  SetResponseStringAndFetch(selected_text, "12", "14");

  EXPECT_EQ(0, start_adjust());
  EXPECT_EQ(0, end_adjust());
}

TEST_F(ContextualSearchDelegateTest, ExpandSelectionInvalidDistantEndAndRange) {
  base::string16 surrounding = base::UTF8ToUTF16("Barack Obama just spoke.");
  std::string selected_text = "Ob";
  CreateSearchContextAndRequestSearchTerm(selected_text, surrounding,
                                          0xffffffff, 0xffffffff - 2);
  SetResponseStringAndFetch(selected_text, "0", "268435455");

  EXPECT_EQ(0, start_adjust());
  EXPECT_EQ(0, end_adjust());
}

TEST_F(ContextualSearchDelegateTest, ExpandSelectionLargeNumbers) {
  base::string16 surrounding = base::UTF8ToUTF16("Barack Obama just spoke.");
  std::string selected_text = "Ob";
  CreateSearchContextAndRequestSearchTerm(selected_text, surrounding,
                                          268435450, 268435455);
  SetResponseStringAndFetch(selected_text, "268435440", "268435455");

  EXPECT_EQ(-10, start_adjust());
  EXPECT_EQ(0, end_adjust());
}

TEST_F(ContextualSearchDelegateTest, ContractSelectionValid) {
  base::string16 surrounding = base::UTF8ToUTF16("Barack Obama just spoke.");
  std::string selected_text = "Barack Obama just";
  CreateSearchContextAndRequestSearchTerm(selected_text, surrounding, 0, 17);
  SetResponseStringAndFetch(selected_text, "0", "12");

  EXPECT_EQ(0, start_adjust());
  EXPECT_EQ(-5, end_adjust());
}

TEST_F(ContextualSearchDelegateTest, ContractSelectionInvalid) {
  base::string16 surrounding = base::UTF8ToUTF16("Barack Obama just spoke.");
  std::string selected_text = "Barack Obama just";
  CreateSearchContextAndRequestSearchTerm(selected_text, surrounding, 0, 17);
  SetResponseStringAndFetch(selected_text, "5", "5");

  EXPECT_EQ(0, start_adjust());
  EXPECT_EQ(0, end_adjust());
}

TEST_F(ContextualSearchDelegateTest, SurroundingTextHighMaximum) {
  base::string16 surrounding = base::ASCIIToUTF16("aa bb Bogus dd ee");
  SetSurroundingContext(surrounding, 6, 11);
  delegate_->SendSurroundingText(30);  // High maximum # of surrounding chars.
  EXPECT_EQ("dd ee", after_text());
}

TEST_F(ContextualSearchDelegateTest, SurroundingTextLowMaximum) {
  base::string16 surrounding = base::ASCIIToUTF16("aa bb Bogus dd ee");
  SetSurroundingContext(surrounding, 6, 11);
  delegate_->SendSurroundingText(3);  // Low maximum # of surrounding chars.
  // Whitespaces are trimmed.
  EXPECT_EQ("dd", after_text());
}

TEST_F(ContextualSearchDelegateTest, SurroundingTextNoBeforeText) {
  base::string16 surrounding = base::ASCIIToUTF16("Bogus ee ff gg");
  SetSurroundingContext(surrounding, 0, 5);
  delegate_->SendSurroundingText(5);
  EXPECT_EQ("ee f", after_text());
}

TEST_F(ContextualSearchDelegateTest, SurroundingTextNoAfterText) {
  base::string16 surrounding = base::ASCIIToUTF16("aa bb Bogus");
  SetSurroundingContext(surrounding, 6, 11);
  delegate_->SendSurroundingText(5);
  EXPECT_EQ("", after_text());
}

TEST_F(ContextualSearchDelegateTest, ExtractMentionsStartEnd) {
  ListValue mentions_list;
  mentions_list.AppendInteger(1);
  mentions_list.AppendInteger(2);
  int start = 0;
  int end = 0;
  delegate_->ExtractMentionsStartEnd(mentions_list, &start, &end);
  EXPECT_EQ(1, start);
  EXPECT_EQ(2, end);
}

TEST_F(ContextualSearchDelegateTest, SurroundingTextForIcing) {
  base::string16 sample = base::ASCIIToUTF16("this is Barack Obama in office.");
  int limit_each_side = 3;
  size_t start = 8;
  size_t end = 20;
  base::string16 result =
      delegate_->SurroundingTextForIcing(sample, limit_each_side, &start, &end);
  EXPECT_EQ(static_cast<size_t>(3), start);
  EXPECT_EQ(static_cast<size_t>(15), end);
  EXPECT_EQ(base::ASCIIToUTF16("is Barack Obama in"), result);
}

TEST_F(ContextualSearchDelegateTest, SurroundingTextForIcingNegativeLimit) {
  base::string16 sample = base::ASCIIToUTF16("this is Barack Obama in office.");
  int limit_each_side = -2;
  size_t start = 8;
  size_t end = 20;
  base::string16 result =
      delegate_->SurroundingTextForIcing(sample, limit_each_side, &start, &end);
  EXPECT_EQ(static_cast<size_t>(0), start);
  EXPECT_EQ(static_cast<size_t>(12), end);
  EXPECT_EQ(base::ASCIIToUTF16("Barack Obama"), result);
}

TEST_F(ContextualSearchDelegateTest, DecodeSearchTermFromJsonResponse) {
  std::string json_with_escape =
      ")]}'\n"
      "{\"mid\":\"/m/02mjmr\", \"search_term\":\"obama\","
      "\"info_text\":\"44th U.S. President\","
      "\"display_text\":\"Barack Obama\", \"mentions\":[0,15],"
      "\"selected_text\":\"obama\", \"resolved_term\":\"barack obama\"}";
  std::string search_term;
  std::string display_text;
  std::string alternate_term;
  std::string prevent_preload;
  int mention_start;
  int mention_end;
  std::string context_language;
  delegate_->DecodeSearchTermFromJsonResponse(
      json_with_escape, &search_term, &display_text, &alternate_term,
      &prevent_preload, &mention_start, &mention_end, &context_language);
  EXPECT_EQ("obama", search_term);
  EXPECT_EQ("Barack Obama", display_text);
  EXPECT_EQ("barack obama", alternate_term);
  EXPECT_EQ("", prevent_preload);
  EXPECT_EQ("", context_language);
}

TEST_F(ContextualSearchDelegateTest, ResponseWithLanguage) {
  CreateDefaultSearchContextAndRequestSearchTerm();
  fetcher()->set_response_code(200);
  fetcher()->SetResponseString(
      "{\"mid\":\"/m/02mjmr\",\"search_term\":\"obama\","
      "\"mentions\":[0,15],\"prevent_preload\":\"1\", "
      "\"lang\":\"de\"}");
  fetcher()->delegate()->OnURLFetchComplete(fetcher());

  EXPECT_FALSE(is_invalid());
  EXPECT_EQ(200, response_code());
  EXPECT_EQ("obama", search_term());
  EXPECT_EQ("obama", display_text());
  EXPECT_TRUE(do_prevent_preload());
  EXPECT_EQ("de", context_language());
}

TEST_F(ContextualSearchDelegateTest, HeaderContainsBasePageUrl) {
  CreateDefaultSearchContextAndRequestSearchTerm();
  EXPECT_EQ(kSomeSpecificBasePage, getBasePageUrlFromRequest());
}