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

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

#include <memory>
#include <string_view>
#include <vector>

#include "base/base64.h"
#include "base/byte_size.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/json/json_writer.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/run_loop.h"
#include "base/strings/string_view_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/mock_callback.h"
#include "base/test/simple_test_clock.h"
#include "base/test/task_environment.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/search_engines_test_environment.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/fixed_logo_api.h"
#include "components/search_provider_logos/google_logo_api.h"
#include "components/search_provider_logos/logo_cache.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "net/base/url_util.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "net/http/http_util.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/url_response_head.mojom.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::Return;
using ::testing::StrictMock;

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();
  return base::MakeRefCounted<base::RefCountedString>(
      std::string(base::as_string_view(*png_bytes)));
}

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

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

SkBitmap MakeBitmap2(int width, int height) {
  SkBitmap bitmap;
  bitmap.allocN32Pixels(width, height);
  bitmap.eraseColor(SK_ColorRED);
  return bitmap;
}

EncodedLogo EncodeLogo(const Logo& logo) {
  EncodedLogo encoded_logo;
  encoded_logo.encoded_image = EncodeBitmapAsPNG(logo.image);
  encoded_logo.dark_encoded_image = EncodeBitmapAsPNG(logo.dark_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).AsBitmap();
  if (encoded_logo.dark_encoded_image) {
    logo.dark_image =
        gfx::Image::CreateFrom1xPNGBytes(encoded_logo.dark_encoded_image)
            .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.dark_image = MakeBitmap2(20, 50);
  logo.metadata.can_show_after_expiration = false;
  logo.metadata.expiration_time = response_time + base::Hours(19);
  logo.metadata.fingerprint = "8bc33a80";
  logo.metadata.source_url =
      AppendPreliminaryParamsToDoodleURL(false, false, 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.dark_animated_url =
      GURL("https://www.google.com/logos/dark_doodle.png");
  logo.metadata.mime_type = "image/png";
  logo.metadata.dark_mime_type = "image/png";
  return logo;
}

Logo GetSampleLogoWithoutDarkImage(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::Hours(19);
  logo.metadata.fingerprint = "8bc33a80";
  logo.metadata.source_url =
      AppendPreliminaryParamsToDoodleURL(false, false, 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, false, 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 SkBitmap& dark_image,
                               const std::string& on_click_url,
                               const std::string& alt_text,
                               const std::string& animated_url,
                               const std::string& dark_animated_url,
                               const std::string& mime_type,
                               const std::string& dark_mime_type,
                               const std::string& fingerprint,
                               base::TimeDelta time_to_live) {
  base::DictValue dict;

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

  std::string dark_data_uri = "data:";
  dark_data_uri += dark_mime_type;
  dark_data_uri += ";base64,";
  dark_data_uri += EncodeBitmapAsPNGBase64(dark_image);

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

  return base::WriteJson(dict).value_or("");
}

std::string MakeServerResponse(const Logo& logo, base::TimeDelta time_to_live) {
  return MakeServerResponse(
      logo.image, logo.dark_image, logo.metadata.on_click_url.spec(),
      logo.metadata.alt_text, logo.metadata.animated_url.spec(),
      logo.metadata.dark_animated_url.spec(), logo.metadata.mime_type,
      logo.metadata.dark_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_ = std::make_unique<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,
                   data_decoder::DataDecoder* data_decoder,
                   image_fetcher::ImageDecodedCallback callback) override {
    gfx::Image image =
        gfx::Image::CreateFrom1xPNGBytes(base::as_byte_span(image_data));
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), image));
  }
};

// A helper class that wraps around all the dependencies required to simulate
// signing in/out.
class SigninHelper {
 public:
  explicit SigninHelper() : identity_test_env_(&test_url_loader_factory_) {}

  signin::IdentityManager* identity_manager() {
    return identity_test_env_.identity_manager();
  }

  void SignIn() {
    std::string email("user@gmail.com");
    identity_test_env_.SetCookieAccounts(
        {{email, signin::GetTestGaiaIdForEmail(email)}});
  }

  void SignOut() {
    identity_test_env_.SetCookieAccounts({});
  }

 private:
  network::TestURLLoaderFactory test_url_loader_factory_;
  signin::IdentityTestEnvironment identity_test_env_;
};

class LogoServiceImplTest : public ::testing::Test {
 protected:
  LogoServiceImplTest()
      : logo_cache_(new NiceMock<MockLogoCache>()),
        shared_factory_(
            base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
                &test_url_loader_factory_)),
        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::FromMillisecondsSinceUnixEpoch(INT64_C(1388686828000)));
    logo_service_ = std::make_unique<LogoServiceImpl>(
        base::FilePath(), signin_helper_.identity_manager(),
        &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_.get()));
  }

  void TearDown() override {
    // |logo_service_|'s owns |logo_cache_|, which gets destroyed on
    // a background sequence after the LogoService's destruction. Ensure that
    // |logo_cache_| is actually destroyed before the test ends to make gmock
    // happy.
    logo_cache_ = nullptr;
    logo_service_->Shutdown();
    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 LogoService 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 LogoService 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(std::string_view keyword,
                       std::string_view 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_; }

  TemplateURLService& template_url_service() {
    return *search_engines_test_environment_.template_url_service();
  }
  const TemplateURLService& template_url_service() const {
    return *search_engines_test_environment_.template_url_service();
  }

  base::test::TaskEnvironment task_environment_;
  search_engines::SearchEnginesTestEnvironment search_engines_test_environment_;
  base::SimpleTestClock test_clock_;
  raw_ptr<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, false, false, DoodleURL()),
      fingerprint);

  auto head = network::mojom::URLResponseHead::New();
  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 = base::MakeRefCounted<net::HttpResponseHeaders>(
      net::HttpUtil::AssembleRawHeaders(headers));
  head->mime_type = "text/html";
  network::URLLoaderCompletionStatus status;
  status.error_code = error_code;
  status.decoded_body_length = base::ByteSize(response_when_fingerprint.size());

  test_url_loader_factory_.AddResponse(url_with_fp, std::move(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), /*for_webui_ntp=*/false,
                         /*enable_animated_logo=*/false);
  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(std::string_view keyword,
                                          std::string_view 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, false, false, DoodleURL()),
      std::string());
  GURL query_without_gray_background = AppendFingerprintParamToDoodleURL(
      AppendPreliminaryParamsToDoodleURL(false, false, 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), /*for_webui_ntp=*/false,
                           /*enable_animated_logo=*/false);
    task_environment_.RunUntilIdle();
  }
  EXPECT_EQ(latest_url_.GetQuery().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), /*for_webui_ntp=*/false,
                           /*enable_animated_logo=*/false);
    task_environment_.RunUntilIdle();
  }
  EXPECT_NE(latest_url_.GetQuery().find("graybg:1"), std::string::npos);
}

TEST_F(LogoServiceImplTest, AnimatedLogoRequestedCanUpdate) {
  // Arrange
  std::string response =
      ServerResponse(GetSampleLogo(DoodleURL(), test_clock_.Now()));
  GURL query_with_animated_logo = AppendFingerprintParamToDoodleURL(
      AppendPreliminaryParamsToDoodleURL(false, false, true, DoodleURL()),
      std::string());
  GURL query_without_animated_logo = AppendFingerprintParamToDoodleURL(
      AppendPreliminaryParamsToDoodleURL(false, false, false, DoodleURL()),
      std::string());

  // Act - No animated logo.
  test_url_loader_factory_.ClearResponses();
  test_url_loader_factory_.AddResponse(query_without_animated_logo.spec(),
                                       response, net::HTTP_OK);
  {
    base::RunLoop run_loop;
    StrictMock<MockLogoCallback> fresh;
    EXPECT_CALL(fresh, Run(_, _))
        .WillOnce(testing::InvokeWithoutArgs(&run_loop, &base::RunLoop::Quit));
    LogoCallbacks callbacks;
    callbacks.on_fresh_decoded_logo_available = fresh.Get();
    logo_service_->GetLogo(std::move(callbacks), /*for_webui_ntp=*/false,
                           /*enable_animated_logo=*/false);
    run_loop.Run();
  }

  // Assert - No animated logo.
  EXPECT_EQ(latest_url_.GetQuery().find("anim:1"), std::string::npos);

  // Act - Animated logo.
  test_url_loader_factory_.ClearResponses();
  test_url_loader_factory_.AddResponse(query_with_animated_logo.spec(),
                                       response, net::HTTP_OK);
  {
    base::RunLoop run_loop;
    StrictMock<MockLogoCallback> fresh;
    EXPECT_CALL(fresh, Run(_, _))
        .WillOnce(testing::InvokeWithoutArgs(&run_loop, &base::RunLoop::Quit));
    LogoCallbacks callbacks;
    callbacks.on_fresh_decoded_logo_available = fresh.Get();
    logo_service_->GetLogo(std::move(callbacks), /*for_webui_ntp=*/false,
                           /*enable_animated_logo=*/true);
    run_loop.Run();
  }

  // Assert - Animated logo.
  EXPECT_NE(latest_url_.GetQuery().find("anim: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(std::nullopt)));
  EXPECT_CALL(fresh, Run(LogoCallbackReason::DETERMINED, Eq(logo)));
  GetDecodedLogo(cached.Get(), fresh.Get());
}

TEST_F(LogoServiceImplTest, DownloadAndCacheLogoWithoutDarkImage) {
  StrictMock<MockLogoCallback> cached;
  StrictMock<MockLogoCallback> fresh;
  Logo logo = GetSampleLogoWithoutDarkImage(DoodleURL(), test_clock_.Now());
  SetServerResponse(ServerResponse(logo));
  logo_cache_->ExpectSetCachedLogo(&logo);
  EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(std::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(std::nullopt)));
  EXPECT_CALL(fresh, Run(LogoCallbackReason::DETERMINED, Eq(encoded_logo)));
  GetEncodedLogo(cached.Get(), fresh.Get());
}

TEST_F(LogoServiceImplTest, DownloadAndCacheEncodedLogoWithoutDarkImage) {
  StrictMock<MockEncodedLogoCallback> cached;
  StrictMock<MockEncodedLogoCallback> fresh;
  Logo logo = GetSampleLogoWithoutDarkImage(DoodleURL(), test_clock_.Now());
  EncodedLogo encoded_logo = EncodeLogo(logo);
  SetServerResponse(ServerResponse(logo));
  logo_cache_->ExpectSetCachedLogo(&logo);
  EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(std::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(std::nullopt)));
    EXPECT_CALL(fresh, Run(LogoCallbackReason::DISABLED, Eq(std::nullopt)));
    GetDecodedLogo(cached.Get(), fresh.Get());
  }

  {
    StrictMock<MockEncodedLogoCallback> cached;
    StrictMock<MockEncodedLogoCallback> fresh;
    EXPECT_CALL(cached, Run(LogoCallbackReason::DISABLED, Eq(std::nullopt)));
    EXPECT_CALL(fresh, Run(LogoCallbackReason::DISABLED, Eq(std::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(std::nullopt)));
    EXPECT_CALL(fresh, Run(LogoCallbackReason::REVALIDATED, Eq(std::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(std::nullopt)));
    EXPECT_CALL(fresh, Run(LogoCallbackReason::FAILED, Eq(std::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(std::nullopt)));
    EXPECT_CALL(fresh, Run(LogoCallbackReason::FAILED, Eq(std::nullopt)));
    GetDecodedLogo(cached.Get(), fresh.Get());
  }
}

TEST_F(LogoServiceImplTest, AcceptMinimalLogoResponse) {
  Logo logo;
  logo.image = MakeBitmap(1, 2);
  logo.metadata.source_url =
      AppendPreliminaryParamsToDoodleURL(false, false, 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(std::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(std::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.dark_image.reset();
  fresh_logo.metadata.mime_type.clear();
  fresh_logo.metadata.dark_mime_type.clear();
  fresh_logo.metadata.expiration_time = test_clock_.Now() + base::Days(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(std::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(std::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::Days(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(std::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(std::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(std::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(std::nullopt)));
  EXPECT_CALL(fresh, Run(LogoCallbackReason::FAILED, Eq(std::nullopt)));

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

TEST_F(LogoServiceImplTest, LogoWithTTLCannotBeShownAfterExpiration) {
  Logo logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
  base::TimeDelta time_to_live = base::Days(3);
  logo.metadata.expiration_time = test_clock_.Now() + time_to_live;
  SetServerResponse(ServerResponse(logo));
  LogoCallbacks callbacks;
  callbacks.on_fresh_decoded_logo_available = base::BindOnce(
      [](LogoCallbackReason type, const std::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::BindOnce(
      [](LogoCallbackReason type, const std::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::Days(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::Seconds(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(std::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::Days(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::Days(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(std::nullopt)));
  EXPECT_CALL(fresh, Run(LogoCallbackReason::FAILED, Eq(std::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::Seconds(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(std::nullopt)));
  EXPECT_CALL(fresh, Run(LogoCallbackReason::FAILED, Eq(std::nullopt)));

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

TEST_F(LogoServiceImplTest, ClearLogoOnSignOut) {
  // Sign in and setup a logo response.
  signin_helper_.SignIn();
  // |SetCachedLogo(nullptr)| task might not have run.
  task_environment_.RunUntilIdle();
  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(std::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), /*for_webui_ntp=*/false,
                        /*enable_animated_logo=*/false);
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(&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(std::nullopt)));
  EXPECT_CALL(first_fresh, Run(LogoCallbackReason::CANCELED, Eq(std::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), /*for_webui_ntp=*/false,
                         /*enable_animated_logo=*/false);

  // 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(std::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), /*for_webui_ntp=*/false,
                         /*enable_animated_logo=*/false);

  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.dark_image.width() == b.dark_image.width()) &&
         (a.dark_image.height() == b.dark_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.dark_animated_url == b.metadata.dark_animated_url) &&
         (a.metadata.alt_text == b.metadata.alt_text) &&
         (a.metadata.mime_type == b.metadata.mime_type) &&
         (a.metadata.dark_mime_type == b.metadata.dark_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
