// Copyright 2014 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 "components/search_provider_logos/logo_service_impl.h"

#include <stddef.h>
#include <stdint.h>

#include <memory>
#include <vector>

#include "base/base64.h"
#include "base/bind.h"
#include "base/callback.h"
#include "base/files/file_path.h"
#include "base/json/json_writer.h"
#include "base/memory/ptr_util.h"
#include "base/memory/ref_counted.h"
#include "base/run_loop.h"
#include "base/strings/string_piece.h"
#include "base/strings/stringprintf.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_task_environment.h"
#include "base/test/simple_test_clock.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "base/values.h"
#include "build/build_config.h"
#include "components/image_fetcher/core/image_decoder.h"
#include "components/search_engines/template_url.h"
#include "components/search_engines/template_url_data.h"
#include "components/search_engines/template_url_service.h"
#include "components/search_provider_logos/features.h"
#include "components/search_provider_logos/fixed_logo_api.h"
#include "components/search_provider_logos/google_logo_api.h"
#include "components/search_provider_logos/logo_cache.h"
#include "components/search_provider_logos/logo_tracker.h"
#include "components/signin/core/browser/account_tracker_service.h"
#include "components/signin/core/browser/fake_gaia_cookie_manager_service.h"
#include "components/signin/core/browser/fake_profile_oauth2_token_service.h"
#include "components/signin/core/browser/test_signin_client.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "net/base/url_util.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "net/url_request/test_url_fetcher_factory.h"
#include "net/url_request/url_request_test_util.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/image/image.h"

using ::testing::_;
using ::testing::AnyNumber;
using ::testing::AtMost;
using ::testing::Eq;
using ::testing::InSequence;
using ::testing::Invoke;
using ::testing::Mock;
using ::testing::Ne;
using ::testing::NiceMock;
using ::testing::Not;
using ::testing::NotNull;
using ::testing::Pointee;
using ::testing::StrictMock;
using ::testing::Return;

using sync_preferences::TestingPrefServiceSyncable;

namespace search_provider_logos {

namespace {

using MockLogoCallback = base::MockCallback<LogoCallback>;
using MockEncodedLogoCallback = base::MockCallback<EncodedLogoCallback>;

scoped_refptr<base::RefCountedString> EncodeBitmapAsPNG(
    const SkBitmap& bitmap) {
  scoped_refptr<base::RefCountedMemory> png_bytes =
      gfx::Image::CreateFrom1xBitmap(bitmap).As1xPNGBytes();
  scoped_refptr<base::RefCountedString> str = new base::RefCountedString();
  str->data().assign(png_bytes->front_as<char>(), png_bytes->size());
  return str;
}

std::string EncodeBitmapAsPNGBase64(const SkBitmap& bitmap) {
  scoped_refptr<base::RefCountedString> png_bytes = EncodeBitmapAsPNG(bitmap);
  std::string encoded_image_base64;
  base::Base64Encode(png_bytes->data(), &encoded_image_base64);
  return encoded_image_base64;
}

SkBitmap MakeBitmap(int width, int height) {
  SkBitmap bitmap;
  bitmap.allocN32Pixels(width, height);
  bitmap.eraseColor(SK_ColorBLUE);
  return bitmap;
}

EncodedLogo EncodeLogo(const Logo& logo) {
  EncodedLogo encoded_logo;
  encoded_logo.encoded_image = EncodeBitmapAsPNG(logo.image);
  encoded_logo.metadata = logo.metadata;
  return encoded_logo;
}

Logo DecodeLogo(const EncodedLogo& encoded_logo) {
  Logo logo;
  logo.image =
      gfx::Image::CreateFrom1xPNGBytes(encoded_logo.encoded_image->front(),
                                       encoded_logo.encoded_image->size())
          .AsBitmap();
  logo.metadata = encoded_logo.metadata;
  return logo;
}

Logo GetSampleLogo(const GURL& logo_url, base::Time response_time) {
  Logo logo;
  logo.image = MakeBitmap(2, 5);
  logo.metadata.can_show_after_expiration = false;
  logo.metadata.expiration_time =
      response_time + base::TimeDelta::FromHours(19);
  logo.metadata.fingerprint = "8bc33a80";
  logo.metadata.source_url =
      AppendPreliminaryParamsToDoodleURL(false, logo_url);
  logo.metadata.on_click_url = GURL("https://www.google.com/search?q=potato");
  logo.metadata.alt_text = "A logo about potatoes";
  logo.metadata.animated_url = GURL("https://www.google.com/logos/doodle.png");
  logo.metadata.mime_type = "image/png";
  return logo;
}

Logo GetSampleLogo2(const GURL& logo_url, base::Time response_time) {
  Logo logo;
  logo.image = MakeBitmap(4, 3);
  logo.metadata.can_show_after_expiration = true;
  logo.metadata.expiration_time = base::Time();
  logo.metadata.fingerprint = "71082741021409127";
  logo.metadata.source_url =
      AppendPreliminaryParamsToDoodleURL(false, logo_url);
  logo.metadata.on_click_url = GURL("https://example.com/page25");
  logo.metadata.alt_text = "The logo for example.com";
  logo.metadata.mime_type = "image/jpeg";
  return logo;
}

std::string MakeServerResponse(const SkBitmap& image,
                               const std::string& on_click_url,
                               const std::string& alt_text,
                               const std::string& animated_url,
                               const std::string& mime_type,
                               const std::string& fingerprint,
                               base::TimeDelta time_to_live) {
  base::DictionaryValue dict;

  std::string data_uri = "data:";
  data_uri += mime_type;
  data_uri += ";base64,";
  data_uri += EncodeBitmapAsPNGBase64(image);

  dict.SetString("ddljson.target_url", on_click_url);
  dict.SetString("ddljson.alt_text", alt_text);
  if (animated_url.empty()) {
    dict.SetString("ddljson.doodle_type", "SIMPLE");
    if (!image.isNull())
      dict.SetString("ddljson.data_uri", data_uri);
  } else {
    dict.SetString("ddljson.doodle_type", "ANIMATED");
    dict.SetBoolean("ddljson.large_image.is_animated_gif", true);
    dict.SetString("ddljson.large_image.url", animated_url);
    if (!image.isNull())
      dict.SetString("ddljson.cta_data_uri", data_uri);
  }
  dict.SetString("ddljson.fingerprint", fingerprint);
  if (time_to_live != base::TimeDelta())
    dict.SetInteger("ddljson.time_to_live_ms",
                    static_cast<int>(time_to_live.InMilliseconds()));

  std::string output;
  base::JSONWriter::Write(dict, &output);
  return output;
}

std::string MakeServerResponse(const Logo& logo, base::TimeDelta time_to_live) {
  return MakeServerResponse(
      logo.image, logo.metadata.on_click_url.spec(), logo.metadata.alt_text,
      logo.metadata.animated_url.spec(), logo.metadata.mime_type,
      logo.metadata.fingerprint, time_to_live);
}

template <typename Arg, typename Matcher>
bool Match(const Arg& arg,
           const Matcher& matcher,
           ::testing::MatchResultListener* result_listener) {
  return ::testing::Matcher<Arg>(matcher).MatchAndExplain(arg, result_listener);
}

MATCHER_P(DecodesTo, decoded_logo, "") {
  return Match(DecodeLogo(arg), Eq(decoded_logo), result_listener);
}

class MockLogoCache : public LogoCache {
 public:
  MockLogoCache() : LogoCache(base::FilePath()) {
    // Delegate actions to the *Internal() methods by default.
    ON_CALL(*this, UpdateCachedLogoMetadata(_))
        .WillByDefault(
            Invoke(this, &MockLogoCache::UpdateCachedLogoMetadataInternal));
    ON_CALL(*this, GetCachedLogoMetadata())
        .WillByDefault(
            Invoke(this, &MockLogoCache::GetCachedLogoMetadataInternal));
    ON_CALL(*this, SetCachedLogo(_))
        .WillByDefault(Invoke(this, &MockLogoCache::SetCachedLogoInternal));
  }

  MOCK_METHOD1(UpdateCachedLogoMetadata, void(const LogoMetadata& metadata));
  MOCK_METHOD0(GetCachedLogoMetadata, const LogoMetadata*());
  MOCK_METHOD1(SetCachedLogo, void(const EncodedLogo* logo));
  // GetCachedLogo() can't be mocked since it returns a scoped_ptr, which is
  // non-copyable. Instead create a method that's pinged when GetCachedLogo() is
  // called.
  MOCK_METHOD0(OnGetCachedLogo, void());

  void EncodeAndSetCachedLogo(const Logo& logo) {
    EncodedLogo encoded_logo = EncodeLogo(logo);
    SetCachedLogo(&encoded_logo);
  }

  void ExpectSetCachedLogo(const Logo* expected_logo) {
    Mock::VerifyAndClearExpectations(this);
    if (expected_logo) {
      EXPECT_CALL(*this, SetCachedLogo(Pointee(DecodesTo(*expected_logo))));
    } else {
      EXPECT_CALL(*this, SetCachedLogo(nullptr));
    }
  }

  void UpdateCachedLogoMetadataInternal(const LogoMetadata& metadata) {
    ASSERT_TRUE(logo_.get());
    ASSERT_TRUE(metadata_.get());
    EXPECT_EQ(metadata_->fingerprint, metadata.fingerprint);
    metadata_.reset(new LogoMetadata(metadata));
    logo_->metadata = metadata;
  }

  const LogoMetadata* GetCachedLogoMetadataInternal() {
    return metadata_.get();
  }

  void SetCachedLogoInternal(const EncodedLogo* logo) {
    logo_ = logo ? std::make_unique<EncodedLogo>(*logo) : nullptr;
    metadata_ = logo ? std::make_unique<LogoMetadata>(logo->metadata) : nullptr;
  }

  std::unique_ptr<EncodedLogo> GetCachedLogo() override {
    OnGetCachedLogo();
    return logo_ ? std::make_unique<EncodedLogo>(*logo_) : nullptr;
  }

 private:
  std::unique_ptr<LogoMetadata> metadata_;
  std::unique_ptr<EncodedLogo> logo_;
};

class FakeImageDecoder : public image_fetcher::ImageDecoder {
 public:
  void DecodeImage(
      const std::string& image_data,
      const gfx::Size& desired_image_frame_size,
      const image_fetcher::ImageDecodedCallback& callback) override {
    gfx::Image image = gfx::Image::CreateFrom1xPNGBytes(
        reinterpret_cast<const uint8_t*>(image_data.data()), image_data.size());
    base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE,
                                                  base::Bind(callback, image));
  }
};

// A helper class that wraps around all the dependencies required to simulate
// signing in/out.
class SigninHelper {
 public:
  explicit SigninHelper(base::test::ScopedTaskEnvironment* task_environment)
      : task_environment_(task_environment),
        signin_client_(&pref_service_),
        cookie_service_(&token_service_, "test_source", &signin_client_) {
    // GaiaCookieManagerService calls static methods of AccountTrackerService
    // which access prefs.
    AccountTrackerService::RegisterPrefs(pref_service_.registry());
  }

  GaiaCookieManagerService* cookie_service() { return &cookie_service_; }

  void SignIn() {
    cookie_service_.SetListAccountsResponseOneAccount("user@gmail.com",
                                                      "gaia_id");
    cookie_service_.TriggerListAccounts("test_source");
    task_environment_->RunUntilIdle();
  }

  void SignOut() {
    cookie_service_.SetListAccountsResponseNoAccounts();
    cookie_service_.TriggerListAccounts("test_source");
    task_environment_->RunUntilIdle();
  }

 private:
  base::test::ScopedTaskEnvironment* task_environment_;

  TestingPrefServiceSyncable pref_service_;
  TestSigninClient signin_client_;
  FakeProfileOAuth2TokenService token_service_;
  FakeGaiaCookieManagerService cookie_service_;
};

class LogoServiceImplTest : public ::testing::Test {
 protected:
  LogoServiceImplTest()
      : template_url_service_(nullptr, 0),
        logo_cache_(new NiceMock<MockLogoCache>()),
        shared_factory_(
            base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
                &test_url_loader_factory_)),
        signin_helper_(&task_environment_),
        use_gray_background_(false) {
    test_url_loader_factory_.SetInterceptor(base::BindRepeating(
        &LogoServiceImplTest::CapturingInterceptor, base::Unretained(this)));

    // Default search engine with logo. All 3P doodle_urls use ddljson API.
    AddSearchEngine("ex", "Logo Example",
                    "https://example.com/?q={searchTerms}",
                    GURL("https://example.com/logo.json"),
                    /*make_default=*/true);

    test_clock_.SetNow(base::Time::FromJsTime(INT64_C(1388686828000)));
    logo_service_ = std::make_unique<LogoServiceImpl>(
        base::FilePath(), signin_helper_.cookie_service(),
        &template_url_service_, std::make_unique<FakeImageDecoder>(),
        shared_factory_,
        base::BindRepeating(&LogoServiceImplTest::use_gray_background,
                            base::Unretained(this)));
    logo_service_->SetClockForTests(&test_clock_);
    logo_service_->SetLogoCacheForTests(base::WrapUnique(logo_cache_));
  }

  void TearDown() override {
    // |logo_service_|'s LogoTracker owns |logo_cache_|, which gets destroyed on
    // a background sequence after the LogoTracker's destruction. Ensure that
    // |logo_cache_| is actually destroyed before the test ends to make gmock
    // happy.
    logo_service_.reset();
    task_environment_.RunUntilIdle();
  }

  // Returns the response that the server would send for the given logo.
  std::string ServerResponse(const Logo& logo);

  // Sets the response to be returned when the LogoTracker fetches the logo.
  void SetServerResponse(const std::string& response,
                         int error_code = net::OK,
                         net::HttpStatusCode response_code = net::HTTP_OK);

  // Sets the response to be returned when the LogoTracker fetches the logo and
  // provides the given fingerprint.
  void SetServerResponseWhenFingerprint(
      const std::string& fingerprint,
      const std::string& response_when_fingerprint,
      int error_code = net::OK,
      net::HttpStatusCode response_code = net::HTTP_OK);

  const GURL& DoodleURL() const;

  // Calls logo_service_->GetLogo() and waits for the asynchronous response(s).
  void GetLogo(LogoCallbacks callbacks);
  void GetDecodedLogo(LogoCallback cached, LogoCallback fresh);
  void GetEncodedLogo(EncodedLogoCallback cached, EncodedLogoCallback fresh);

  void AddSearchEngine(base::StringPiece keyword,
                       base::StringPiece short_name,
                       const std::string& url,
                       GURL doodle_url,
                       bool make_default);

  void CapturingInterceptor(const network::ResourceRequest& request);

  bool use_gray_background() const { return use_gray_background_; }

  base::test::ScopedTaskEnvironment task_environment_;
  TemplateURLService template_url_service_;
  base::SimpleTestClock test_clock_;
  NiceMock<MockLogoCache>* logo_cache_;

  // Used for mocking |logo_service_| URLs.
  network::TestURLLoaderFactory test_url_loader_factory_;
  scoped_refptr<network::SharedURLLoaderFactory> shared_factory_;
  std::unique_ptr<LogoServiceImpl> logo_service_;

  SigninHelper signin_helper_;

  GURL latest_url_;
  bool use_gray_background_;
};

void LogoServiceImplTest::CapturingInterceptor(
    const network::ResourceRequest& request) {
  latest_url_ = request.url;
}

std::string LogoServiceImplTest::ServerResponse(const Logo& logo) {
  base::TimeDelta time_to_live;
  if (!logo.metadata.expiration_time.is_null())
    time_to_live = logo.metadata.expiration_time - test_clock_.Now();
  return MakeServerResponse(logo, time_to_live);
}

void LogoServiceImplTest::SetServerResponse(const std::string& response,
                                            int error_code,
                                            net::HttpStatusCode response_code) {
  SetServerResponseWhenFingerprint(std::string(), response, error_code,
                                   response_code);
}

void LogoServiceImplTest::SetServerResponseWhenFingerprint(
    const std::string& fingerprint,
    const std::string& response_when_fingerprint,
    int error_code,
    net::HttpStatusCode response_code) {
  GURL url_with_fp = AppendFingerprintParamToDoodleURL(
      AppendPreliminaryParamsToDoodleURL(false, DoodleURL()), fingerprint);

  network::ResourceResponseHead head;
  std::string headers(base::StringPrintf(
      "HTTP/1.1 %d %s\nContent-type: text/html\n\n",
      static_cast<int>(response_code), GetHttpReasonPhrase(response_code)));
  head.headers = new net::HttpResponseHeaders(
      net::HttpUtil::AssembleRawHeaders(headers.c_str(), headers.size()));
  head.mime_type = "text/html";
  network::URLLoaderCompletionStatus status;
  status.error_code = error_code;
  status.decoded_body_length = response_when_fingerprint.size();

  test_url_loader_factory_.AddResponse(url_with_fp, head,
                                       response_when_fingerprint, status);
}

const GURL& LogoServiceImplTest::DoodleURL() const {
  return template_url_service_.GetDefaultSearchProvider()->doodle_url();
}

void LogoServiceImplTest::GetLogo(LogoCallbacks callbacks) {
  logo_service_->GetLogo(std::move(callbacks));
  task_environment_.RunUntilIdle();
}

void LogoServiceImplTest::GetDecodedLogo(LogoCallback cached,
                                         LogoCallback fresh) {
  LogoCallbacks callbacks;
  callbacks.on_cached_decoded_logo_available = std::move(cached);
  callbacks.on_fresh_decoded_logo_available = std::move(fresh);
  GetLogo(std::move(callbacks));
}

void LogoServiceImplTest::GetEncodedLogo(EncodedLogoCallback cached,
                                         EncodedLogoCallback fresh) {
  LogoCallbacks callbacks;
  callbacks.on_cached_encoded_logo_available = std::move(cached);
  callbacks.on_fresh_encoded_logo_available = std::move(fresh);
  GetLogo(std::move(callbacks));
}

void LogoServiceImplTest::AddSearchEngine(base::StringPiece keyword,
                                          base::StringPiece short_name,
                                          const std::string& url,
                                          GURL doodle_url,
                                          bool make_default) {
  TemplateURLData search_url;
  search_url.SetKeyword(base::ASCIIToUTF16(keyword));
  search_url.SetShortName(base::ASCIIToUTF16(short_name));
  search_url.SetURL(url);
  search_url.doodle_url = doodle_url;

  TemplateURL* template_url =
      template_url_service_.Add(std::make_unique<TemplateURL>(search_url));
  if (make_default) {
    template_url_service_.SetUserSelectedDefaultSearchProvider(template_url);
  }
}

// Tests -----------------------------------------------------------------------

TEST_F(LogoServiceImplTest, CTARequestedBackgroundCanUpdate) {
  std::string response =
      ServerResponse(GetSampleLogo(DoodleURL(), test_clock_.Now()));
  GURL query_with_gray_background = AppendFingerprintParamToDoodleURL(
      AppendPreliminaryParamsToDoodleURL(true, DoodleURL()), std::string());
  GURL query_without_gray_background = AppendFingerprintParamToDoodleURL(
      AppendPreliminaryParamsToDoodleURL(false, DoodleURL()), std::string());

  use_gray_background_ = false;
  test_url_loader_factory_.ClearResponses();
  test_url_loader_factory_.AddResponse(query_without_gray_background.spec(),
                                       response, net::HTTP_OK);
  {
    StrictMock<MockLogoCallback> fresh;
    EXPECT_CALL(fresh, Run(_, _));
    LogoCallbacks callbacks;
    callbacks.on_fresh_decoded_logo_available = fresh.Get();
    logo_service_->GetLogo(std::move(callbacks));
    task_environment_.RunUntilIdle();
  }
  EXPECT_EQ(latest_url_.query().find("graybg:1"), std::string::npos);

  use_gray_background_ = true;
  test_url_loader_factory_.ClearResponses();
  test_url_loader_factory_.AddResponse(query_with_gray_background.spec(),
                                       response, net::HTTP_OK);
  {
    StrictMock<MockLogoCallback> fresh;
    EXPECT_CALL(fresh, Run(_, _));
    LogoCallbacks callbacks;
    callbacks.on_fresh_decoded_logo_available = fresh.Get();
    logo_service_->GetLogo(std::move(callbacks));
    task_environment_.RunUntilIdle();
  }
  EXPECT_NE(latest_url_.query().find("graybg:1"), std::string::npos);
}

TEST_F(LogoServiceImplTest, DownloadAndCacheLogo) {
  StrictMock<MockLogoCallback> cached;
  StrictMock<MockLogoCallback> fresh;
  Logo logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
  SetServerResponse(ServerResponse(logo));
  logo_cache_->ExpectSetCachedLogo(&logo);
  EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));
  EXPECT_CALL(fresh, Run(LogoCallbackReason::DETERMINED, Eq(logo)));
  GetDecodedLogo(cached.Get(), fresh.Get());
}

TEST_F(LogoServiceImplTest, DownloadAndCacheEncodedLogo) {
  StrictMock<MockEncodedLogoCallback> cached;
  StrictMock<MockEncodedLogoCallback> fresh;
  Logo logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
  EncodedLogo encoded_logo = EncodeLogo(logo);
  SetServerResponse(ServerResponse(logo));
  logo_cache_->ExpectSetCachedLogo(&logo);
  EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));
  EXPECT_CALL(fresh, Run(LogoCallbackReason::DETERMINED, Eq(encoded_logo)));
  GetEncodedLogo(cached.Get(), fresh.Get());
}

TEST_F(LogoServiceImplTest, ShouldReturnDisabledWhenDSEHasNoLogo) {
  AddSearchEngine("cr", "Chromium", "https://www.chromium.org/?q={searchTerms}",
                  GURL(/* logo disabled */), /*make_default=*/true);

  {
    StrictMock<MockLogoCallback> cached;
    StrictMock<MockLogoCallback> fresh;
    EXPECT_CALL(cached, Run(LogoCallbackReason::DISABLED, Eq(base::nullopt)));
    EXPECT_CALL(fresh, Run(LogoCallbackReason::DISABLED, Eq(base::nullopt)));
    GetDecodedLogo(cached.Get(), fresh.Get());
  }

  {
    StrictMock<MockEncodedLogoCallback> cached;
    StrictMock<MockEncodedLogoCallback> fresh;
    EXPECT_CALL(cached, Run(LogoCallbackReason::DISABLED, Eq(base::nullopt)));
    EXPECT_CALL(fresh, Run(LogoCallbackReason::DISABLED, Eq(base::nullopt)));
    GetEncodedLogo(cached.Get(), fresh.Get());
  }
}

TEST_F(LogoServiceImplTest, EmptyCacheAndFailedDownload) {
  EXPECT_CALL(*logo_cache_, UpdateCachedLogoMetadata(_)).Times(0);
  EXPECT_CALL(*logo_cache_, SetCachedLogo(_)).Times(0);
  EXPECT_CALL(*logo_cache_, SetCachedLogo(nullptr)).Times(AnyNumber());

  {
    StrictMock<MockLogoCallback> cached;
    StrictMock<MockLogoCallback> fresh;
    SetServerResponse("server is borked");
    EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));
    EXPECT_CALL(fresh, Run(LogoCallbackReason::REVALIDATED, Eq(base::nullopt)));
    GetDecodedLogo(cached.Get(), fresh.Get());
  }

  {
    StrictMock<MockLogoCallback> cached;
    StrictMock<MockLogoCallback> fresh;
    SetServerResponse("", net::ERR_FAILED, net::HTTP_OK);
    EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));
    EXPECT_CALL(fresh, Run(LogoCallbackReason::FAILED, Eq(base::nullopt)));
    GetDecodedLogo(cached.Get(), fresh.Get());
  }

  {
    StrictMock<MockLogoCallback> cached;
    StrictMock<MockLogoCallback> fresh;
    SetServerResponse("", net::OK, net::HTTP_BAD_GATEWAY);
    EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));
    EXPECT_CALL(fresh, Run(LogoCallbackReason::FAILED, Eq(base::nullopt)));
    GetDecodedLogo(cached.Get(), fresh.Get());
  }
}

TEST_F(LogoServiceImplTest, AcceptMinimalLogoResponse) {
  Logo logo;
  logo.image = MakeBitmap(1, 2);
  logo.metadata.source_url =
      AppendPreliminaryParamsToDoodleURL(false, DoodleURL());
  logo.metadata.can_show_after_expiration = true;
  logo.metadata.mime_type = "image/png";

  std::string response =
      ")]}' {\"ddljson\":{\"data_uri\":\"data:image/png;base64," +
      EncodeBitmapAsPNGBase64(logo.image) + "\"}}";

  SetServerResponse(response);

  StrictMock<MockLogoCallback> cached;
  StrictMock<MockLogoCallback> fresh;
  EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));
  EXPECT_CALL(fresh, Run(LogoCallbackReason::DETERMINED, Eq(logo)));

  GetDecodedLogo(cached.Get(), fresh.Get());
}

TEST_F(LogoServiceImplTest, ReturnCachedLogo) {
  Logo cached_logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
  logo_cache_->EncodeAndSetCachedLogo(cached_logo);
  SetServerResponseWhenFingerprint(cached_logo.metadata.fingerprint, "",
                                   net::ERR_FAILED, net::HTTP_OK);

  EXPECT_CALL(*logo_cache_, UpdateCachedLogoMetadata(_)).Times(0);
  EXPECT_CALL(*logo_cache_, SetCachedLogo(_)).Times(0);
  EXPECT_CALL(*logo_cache_, OnGetCachedLogo()).Times(AtMost(1));

  StrictMock<MockLogoCallback> cached;
  StrictMock<MockLogoCallback> fresh;
  EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(cached_logo)));
  EXPECT_CALL(fresh, Run(LogoCallbackReason::FAILED, Eq(base::nullopt)));

  GetDecodedLogo(cached.Get(), fresh.Get());
}

TEST_F(LogoServiceImplTest, ValidateCachedLogo) {
  Logo cached_logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
  logo_cache_->EncodeAndSetCachedLogo(cached_logo);

  // During revalidation, the image data and mime_type are absent.
  Logo fresh_logo = cached_logo;
  fresh_logo.image.reset();
  fresh_logo.metadata.mime_type.clear();
  fresh_logo.metadata.expiration_time =
      test_clock_.Now() + base::TimeDelta::FromDays(8);
  SetServerResponseWhenFingerprint(fresh_logo.metadata.fingerprint,
                                   ServerResponse(fresh_logo));

  {
    EXPECT_CALL(*logo_cache_, UpdateCachedLogoMetadata(_)).Times(1);
    EXPECT_CALL(*logo_cache_, SetCachedLogo(_)).Times(0);
    EXPECT_CALL(*logo_cache_, OnGetCachedLogo()).Times(AtMost(1));

    StrictMock<MockLogoCallback> cached;
    StrictMock<MockLogoCallback> fresh;
    EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(cached_logo)));
    EXPECT_CALL(fresh, Run(LogoCallbackReason::REVALIDATED, Eq(base::nullopt)));
    GetDecodedLogo(cached.Get(), fresh.Get());
  }

  ASSERT_TRUE(logo_cache_->GetCachedLogoMetadata());
  EXPECT_EQ(fresh_logo.metadata.expiration_time,
            logo_cache_->GetCachedLogoMetadata()->expiration_time);

  {
    // Ensure that cached logo is still returned correctly on subsequent
    // requests. In particular, the metadata should stay valid.
    // https://crbug.com/480090
    EXPECT_CALL(*logo_cache_, UpdateCachedLogoMetadata(_)).Times(1);
    EXPECT_CALL(*logo_cache_, SetCachedLogo(_)).Times(0);
    EXPECT_CALL(*logo_cache_, OnGetCachedLogo()).Times(AtMost(1));

    StrictMock<MockLogoCallback> cached;
    StrictMock<MockLogoCallback> fresh;
    EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(cached_logo)));
    EXPECT_CALL(fresh, Run(LogoCallbackReason::REVALIDATED, Eq(base::nullopt)));
    GetDecodedLogo(cached.Get(), fresh.Get());
  }
}

TEST_F(LogoServiceImplTest, UpdateCachedLogoMetadata) {
  Logo cached_logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
  logo_cache_->EncodeAndSetCachedLogo(cached_logo);

  Logo fresh_logo = cached_logo;
  fresh_logo.image.reset();
  fresh_logo.metadata.mime_type.clear();
  fresh_logo.metadata.on_click_url = GURL("https://new.onclick.url");
  fresh_logo.metadata.alt_text = "new alt text";
  fresh_logo.metadata.animated_url = GURL("https://new.animated.url");
  fresh_logo.metadata.expiration_time =
      test_clock_.Now() + base::TimeDelta::FromDays(8);
  SetServerResponseWhenFingerprint(fresh_logo.metadata.fingerprint,
                                   ServerResponse(fresh_logo));

  // On the first request, the cached logo should be used.
  {
    StrictMock<MockLogoCallback> cached;
    StrictMock<MockLogoCallback> fresh;
    EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(cached_logo)));
    EXPECT_CALL(fresh, Run(LogoCallbackReason::REVALIDATED, Eq(base::nullopt)));
    GetDecodedLogo(cached.Get(), fresh.Get());
  }

  // Subsequently, the cached image should be returned along with the updated
  // metadata.
  {
    Logo expected_logo = fresh_logo;
    expected_logo.image = cached_logo.image;
    expected_logo.metadata.mime_type = cached_logo.metadata.mime_type;
    StrictMock<MockLogoCallback> cached;
    StrictMock<MockLogoCallback> fresh;
    EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(expected_logo)));
    EXPECT_CALL(fresh, Run(LogoCallbackReason::REVALIDATED, Eq(base::nullopt)));
    GetDecodedLogo(cached.Get(), fresh.Get());
  }
}

TEST_F(LogoServiceImplTest, UpdateCachedLogo) {
  Logo cached_logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
  logo_cache_->EncodeAndSetCachedLogo(cached_logo);

  Logo fresh_logo = GetSampleLogo2(DoodleURL(), test_clock_.Now());
  SetServerResponseWhenFingerprint(cached_logo.metadata.fingerprint,
                                   ServerResponse(fresh_logo));

  logo_cache_->ExpectSetCachedLogo(&fresh_logo);
  EXPECT_CALL(*logo_cache_, UpdateCachedLogoMetadata(_)).Times(0);
  EXPECT_CALL(*logo_cache_, OnGetCachedLogo()).Times(AtMost(1));

  StrictMock<MockLogoCallback> cached;
  StrictMock<MockLogoCallback> fresh;
  EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(cached_logo)));
  EXPECT_CALL(fresh, Run(LogoCallbackReason::DETERMINED, Eq(fresh_logo)));
  GetDecodedLogo(cached.Get(), fresh.Get());
}

TEST_F(LogoServiceImplTest, InvalidateCachedLogo) {
  Logo cached_logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
  logo_cache_->EncodeAndSetCachedLogo(cached_logo);

  // This response means there's no current logo.
  SetServerResponseWhenFingerprint(cached_logo.metadata.fingerprint,
                                   ")]}' {\"update\":{}}");

  logo_cache_->ExpectSetCachedLogo(nullptr);
  EXPECT_CALL(*logo_cache_, UpdateCachedLogoMetadata(_)).Times(0);
  EXPECT_CALL(*logo_cache_, OnGetCachedLogo()).Times(AtMost(1));

  StrictMock<MockLogoCallback> cached;
  StrictMock<MockLogoCallback> fresh;
  EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(cached_logo)));
  EXPECT_CALL(fresh, Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));

  GetDecodedLogo(cached.Get(), fresh.Get());
}

TEST_F(LogoServiceImplTest, DeleteCachedLogoFromOldUrl) {
  SetServerResponse("", net::ERR_FAILED, net::HTTP_OK);
  Logo cached_logo =
      GetSampleLogo(GURL("https://oldsearchprovider.com"), test_clock_.Now());
  logo_cache_->EncodeAndSetCachedLogo(cached_logo);

  EXPECT_CALL(*logo_cache_, UpdateCachedLogoMetadata(_)).Times(0);
  EXPECT_CALL(*logo_cache_, SetCachedLogo(_)).Times(0);
  EXPECT_CALL(*logo_cache_, SetCachedLogo(nullptr)).Times(AnyNumber());
  EXPECT_CALL(*logo_cache_, OnGetCachedLogo()).Times(AtMost(1));

  StrictMock<MockLogoCallback> cached;
  StrictMock<MockLogoCallback> fresh;
  EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));
  EXPECT_CALL(fresh, Run(LogoCallbackReason::FAILED, Eq(base::nullopt)));

  GetDecodedLogo(cached.Get(), fresh.Get());
}

TEST_F(LogoServiceImplTest, LogoWithTTLCannotBeShownAfterExpiration) {
  Logo logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
  base::TimeDelta time_to_live = base::TimeDelta::FromDays(3);
  logo.metadata.expiration_time = test_clock_.Now() + time_to_live;
  SetServerResponse(ServerResponse(logo));
  LogoCallbacks callbacks;
  callbacks.on_fresh_decoded_logo_available = base::Bind(
      [](LogoCallbackReason type, const base::Optional<Logo>& logo) {});
  GetLogo(std::move(callbacks));

  const LogoMetadata* cached_metadata = logo_cache_->GetCachedLogoMetadata();
  ASSERT_TRUE(cached_metadata);
  EXPECT_FALSE(cached_metadata->can_show_after_expiration);
  EXPECT_EQ(test_clock_.Now() + time_to_live, cached_metadata->expiration_time);
}

TEST_F(LogoServiceImplTest, LogoWithoutTTLCanBeShownAfterExpiration) {
  Logo logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
  base::TimeDelta time_to_live = base::TimeDelta();
  SetServerResponse(MakeServerResponse(logo, time_to_live));
  LogoCallbacks callbacks;
  callbacks.on_fresh_decoded_logo_available = base::Bind(
      [](LogoCallbackReason type, const base::Optional<Logo>& logo) {});
  GetLogo(std::move(callbacks));

  const LogoMetadata* cached_metadata = logo_cache_->GetCachedLogoMetadata();
  ASSERT_TRUE(cached_metadata);
  EXPECT_TRUE(cached_metadata->can_show_after_expiration);
  EXPECT_EQ(test_clock_.Now() + base::TimeDelta::FromDays(30),
            cached_metadata->expiration_time);
}

TEST_F(LogoServiceImplTest, UseSoftExpiredCachedLogo) {
  SetServerResponse("", net::ERR_FAILED, net::HTTP_OK);
  Logo cached_logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
  cached_logo.metadata.expiration_time =
      test_clock_.Now() - base::TimeDelta::FromSeconds(1);
  cached_logo.metadata.can_show_after_expiration = true;
  logo_cache_->EncodeAndSetCachedLogo(cached_logo);

  EXPECT_CALL(*logo_cache_, UpdateCachedLogoMetadata(_)).Times(0);
  EXPECT_CALL(*logo_cache_, SetCachedLogo(_)).Times(0);
  EXPECT_CALL(*logo_cache_, OnGetCachedLogo()).Times(AtMost(1));

  StrictMock<MockLogoCallback> cached;
  StrictMock<MockLogoCallback> fresh;
  EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(cached_logo)));
  EXPECT_CALL(fresh, Run(LogoCallbackReason::FAILED, Eq(base::nullopt)));

  GetDecodedLogo(cached.Get(), fresh.Get());
}

TEST_F(LogoServiceImplTest, RerequestSoftExpiredCachedLogo) {
  Logo cached_logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
  cached_logo.metadata.expiration_time =
      test_clock_.Now() - base::TimeDelta::FromDays(5);
  cached_logo.metadata.can_show_after_expiration = true;
  logo_cache_->EncodeAndSetCachedLogo(cached_logo);

  Logo fresh_logo = GetSampleLogo2(DoodleURL(), test_clock_.Now());
  SetServerResponse(ServerResponse(fresh_logo));

  logo_cache_->ExpectSetCachedLogo(&fresh_logo);
  EXPECT_CALL(*logo_cache_, UpdateCachedLogoMetadata(_)).Times(0);
  EXPECT_CALL(*logo_cache_, OnGetCachedLogo()).Times(AtMost(1));

  StrictMock<MockLogoCallback> cached;
  StrictMock<MockLogoCallback> fresh;
  EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(cached_logo)));
  EXPECT_CALL(fresh, Run(LogoCallbackReason::DETERMINED, Eq(fresh_logo)));

  GetDecodedLogo(cached.Get(), fresh.Get());
}

TEST_F(LogoServiceImplTest, DeleteAncientCachedLogo) {
  SetServerResponse("", net::ERR_FAILED, net::HTTP_OK);
  Logo cached_logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
  cached_logo.metadata.expiration_time =
      test_clock_.Now() - base::TimeDelta::FromDays(200);
  cached_logo.metadata.can_show_after_expiration = true;
  logo_cache_->EncodeAndSetCachedLogo(cached_logo);

  EXPECT_CALL(*logo_cache_, UpdateCachedLogoMetadata(_)).Times(0);
  EXPECT_CALL(*logo_cache_, SetCachedLogo(_)).Times(0);
  EXPECT_CALL(*logo_cache_, SetCachedLogo(nullptr)).Times(AnyNumber());
  EXPECT_CALL(*logo_cache_, OnGetCachedLogo()).Times(AtMost(1));

  StrictMock<MockLogoCallback> cached;
  StrictMock<MockLogoCallback> fresh;
  EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));
  EXPECT_CALL(fresh, Run(LogoCallbackReason::FAILED, Eq(base::nullopt)));

  GetDecodedLogo(cached.Get(), fresh.Get());
}

TEST_F(LogoServiceImplTest, DeleteExpiredCachedLogo) {
  SetServerResponse("", net::ERR_FAILED, net::HTTP_OK);
  Logo cached_logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
  cached_logo.metadata.expiration_time =
      test_clock_.Now() - base::TimeDelta::FromSeconds(1);
  cached_logo.metadata.can_show_after_expiration = false;
  logo_cache_->EncodeAndSetCachedLogo(cached_logo);

  EXPECT_CALL(*logo_cache_, UpdateCachedLogoMetadata(_)).Times(0);
  EXPECT_CALL(*logo_cache_, SetCachedLogo(_)).Times(0);
  EXPECT_CALL(*logo_cache_, SetCachedLogo(nullptr)).Times(AnyNumber());
  EXPECT_CALL(*logo_cache_, OnGetCachedLogo()).Times(AtMost(1));

  StrictMock<MockLogoCallback> cached;
  StrictMock<MockLogoCallback> fresh;
  EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));
  EXPECT_CALL(fresh, Run(LogoCallbackReason::FAILED, Eq(base::nullopt)));

  GetDecodedLogo(cached.Get(), fresh.Get());
}

TEST_F(LogoServiceImplTest, ClearLogoOnSignOut) {
  // Sign in and setup a logo response.
  signin_helper_.SignIn();
  Logo logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
  SetServerResponse(ServerResponse(logo));

  // Request the logo so it gets fetched and cached.
  logo_cache_->ExpectSetCachedLogo(&logo);
  StrictMock<MockLogoCallback> cached;
  StrictMock<MockLogoCallback> fresh;
  EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));
  EXPECT_CALL(fresh, Run(LogoCallbackReason::DETERMINED, Eq(logo)));
  GetDecodedLogo(cached.Get(), fresh.Get());

  // Signing out should clear the cached logo immediately.
  logo_cache_->ExpectSetCachedLogo(nullptr);
  signin_helper_.SignOut();
}

// Tests that deal with multiple listeners.

void EnqueueCallbacks(LogoServiceImpl* logo_service,
                      std::vector<LogoCallback>* cached_callbacks,
                      std::vector<LogoCallback>* fresh_callbacks,
                      size_t start_index) {
  DCHECK_EQ(cached_callbacks->size(), fresh_callbacks->size());
  if (start_index >= cached_callbacks->size())
    return;

  LogoCallbacks callbacks;
  callbacks.on_cached_decoded_logo_available =
      std::move((*cached_callbacks)[start_index]);
  callbacks.on_fresh_decoded_logo_available =
      std::move((*fresh_callbacks)[start_index]);
  logo_service->GetLogo(std::move(callbacks));
  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE, base::Bind(&EnqueueCallbacks, logo_service, cached_callbacks,
                            fresh_callbacks, start_index + 1));
}

TEST_F(LogoServiceImplTest, SupportOverlappingLogoRequests) {
  Logo cached_logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
  logo_cache_->EncodeAndSetCachedLogo(cached_logo);
  ON_CALL(*logo_cache_, SetCachedLogo(_)).WillByDefault(Return());

  Logo fresh_logo = GetSampleLogo2(DoodleURL(), test_clock_.Now());
  std::string response = ServerResponse(fresh_logo);
  SetServerResponse(response);
  SetServerResponseWhenFingerprint(cached_logo.metadata.fingerprint, response);

  const int kNumListeners = 10;
  std::vector<std::unique_ptr<MockLogoCallback>> mocks;
  std::vector<LogoCallback> cached_callbacks;
  std::vector<LogoCallback> fresh_callbacks;
  for (int i = 0; i < kNumListeners; ++i) {
    mocks.push_back(std::make_unique<MockLogoCallback>());
    EXPECT_CALL(*mocks.back(),
                Run(LogoCallbackReason::DETERMINED, Eq(cached_logo)));
    cached_callbacks.push_back(mocks.back()->Get());

    mocks.push_back(std::make_unique<MockLogoCallback>());
    EXPECT_CALL(*mocks.back(),
                Run(LogoCallbackReason::DETERMINED, Eq(fresh_logo)));
    fresh_callbacks.push_back(mocks.back()->Get());
  }
  EXPECT_CALL(*logo_cache_, SetCachedLogo(_)).Times(AtMost(3));
  EXPECT_CALL(*logo_cache_, OnGetCachedLogo()).Times(AtMost(3));

  EnqueueCallbacks(logo_service_.get(), &cached_callbacks, &fresh_callbacks, 0);

  task_environment_.RunUntilIdle();
}

TEST_F(LogoServiceImplTest, DeleteCallbacksWhenLogoURLChanged) {
  StrictMock<MockLogoCallback> first_cached;
  StrictMock<MockLogoCallback> first_fresh;
  EXPECT_CALL(first_cached,
              Run(LogoCallbackReason::CANCELED, Eq(base::nullopt)));
  EXPECT_CALL(first_fresh,
              Run(LogoCallbackReason::CANCELED, Eq(base::nullopt)));
  LogoCallbacks first_callbacks;
  first_callbacks.on_cached_decoded_logo_available = first_cached.Get();
  first_callbacks.on_fresh_decoded_logo_available = first_fresh.Get();
  logo_service_->GetLogo(std::move(first_callbacks));

  // Change default search engine; new DSE has a doodle URL.
  AddSearchEngine("cr", "Chromium", "https://www.chromium.org/?q={searchTerms}",
                  GURL("https://chromium.org/logo.json"),
                  /*make_default=*/true);

  Logo logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
  SetServerResponse(ServerResponse(logo));

  StrictMock<MockLogoCallback> second_cached;
  StrictMock<MockLogoCallback> second_fresh;
  EXPECT_CALL(second_cached,
              Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));
  EXPECT_CALL(second_fresh, Run(LogoCallbackReason::DETERMINED, Eq(logo)));
  LogoCallbacks second_callbacks;
  second_callbacks.on_cached_decoded_logo_available = second_cached.Get();
  second_callbacks.on_fresh_decoded_logo_available = second_fresh.Get();
  logo_service_->GetLogo(std::move(second_callbacks));

  task_environment_.RunUntilIdle();
}

}  // namespace

bool operator==(const Logo& a, const Logo& b) {
  return (a.image.width() == b.image.width()) &&
         (a.image.height() == b.image.height()) &&
         (a.metadata.on_click_url == b.metadata.on_click_url) &&
         (a.metadata.source_url == b.metadata.source_url) &&
         (a.metadata.animated_url == b.metadata.animated_url) &&
         (a.metadata.alt_text == b.metadata.alt_text) &&
         (a.metadata.mime_type == b.metadata.mime_type) &&
         (a.metadata.fingerprint == b.metadata.fingerprint) &&
         (a.metadata.can_show_after_expiration ==
          b.metadata.can_show_after_expiration);
}

bool operator==(const EncodedLogo& a, const EncodedLogo& b) {
  return DecodeLogo(a) == DecodeLogo(b);
}

void PrintTo(const Logo& logo, std::ostream* ostr) {
  *ostr << "image size: " << logo.image.width() << "x" << logo.image.height()
        << "\non_click_url: " << logo.metadata.on_click_url
        << "\nsource_url: " << logo.metadata.source_url
        << "\nanimated_url: " << logo.metadata.animated_url
        << "\nalt_text: " << logo.metadata.alt_text
        << "\nmime_type: " << logo.metadata.mime_type
        << "\nfingerprint: " << logo.metadata.fingerprint
        << "\ncan_show_after_expiration: "
        << logo.metadata.can_show_after_expiration;
}

void PrintTo(const EncodedLogo& logo, std::ostream* ostr) {
  PrintTo(DecodeLogo(logo), ostr);
}

}  // namespace search_provider_logos
