// Copyright 2018 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/themes/ntp_background_service.h"

#include <optional>
#include <string>
#include <string_view>

#include "base/barrier_closure.h"
#include "base/check_deref.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/observer_list.h"
#include "base/strings/strcat.h"
#include "base/time/time.h"
#include "components/application_locale_storage/application_locale_storage.h"
#include "components/search/ntp_features.h"
#include "components/themes/ntp_background.pb.h"
#include "components/version_info/version_info.h"
#include "net/base/load_flags.h"
#include "net/http/http_status_code.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"

namespace {

// Command line param to override the collections base URL, e.g. for testing.
constexpr char kCollectionsBaseUrlCmdlineSwitch[] = "collections-base-url";

// The default base URL to download prod collections.
constexpr char kCollectionsBaseUrl[] = "https://clients3.google.com";

// The default base URL to download alpha collections.
constexpr char kAlphaCollectionsBaseUrl[] = "https://clients5.google.com";

// The MIME type of the POST data sent to the server.
constexpr char kProtoMimeType[] = "application/x-protobuf";

// The path relative to kCollectionsBaseUrl to download the proto of the
// complete list of wallpaper collections.
constexpr char kCollectionsPath[] =
    "/cast/chromecast/home/wallpaper/collections?rt=b";
// The path relative to kCollectionsBaseUrl to download the metadata of the
// images in a collection.
constexpr char kCollectionImagesPath[] =
    "/cast/chromecast/home/wallpaper/collection-images?rt=b";
// The path relative to kCollectionsBaseUrl to download the metadata of the
// 'next' image in a collection.
constexpr char kNextCollectionImagePath[] =
    "/cast/chromecast/home/wallpaper/image?rt=b";

// Returns the configured collections base URL with |path| appended.
GURL GetUrl(std::string_view path) {
  return GURL(base::CommandLine::ForCurrentProcess()->HasSwitch(
                  kCollectionsBaseUrlCmdlineSwitch)
                  ? base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
                        kCollectionsBaseUrlCmdlineSwitch)
              : base::FeatureList::IsEnabled(
                    ntp_features::kNtpAlphaBackgroundCollections)
                  ? kAlphaCollectionsBaseUrl
                  : kCollectionsBaseUrl)
      .Resolve(path);
}

}  // namespace

NtpBackgroundService::NtpBackgroundService(
    ApplicationLocaleStorage* application_locale_storage,
    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
    : application_locale_storage_(CHECK_DEREF(application_locale_storage)),
      url_loader_factory_(url_loader_factory) {
  default_image_options_ = GetImageOptions();
  thumbnail_image_options_ = GetThumbnailImageOptions();
  collections_api_url_ = GetUrl(kCollectionsPath);
  collection_images_api_url_ = GetUrl(kCollectionImagesPath);
  next_image_api_url_ = GetUrl(kNextCollectionImagePath);
}

NtpBackgroundService::~NtpBackgroundService() = default;

void NtpBackgroundService::Shutdown() {
  for (auto& observer : observers_) {
    observer.OnNtpBackgroundServiceShuttingDown();
  }
  DCHECK(observers_.empty());
}

void NtpBackgroundService::FetchCollectionInfo(
    const std::string& filtering_label) {
  // If a request is currently in progress, drop the new request.
  if (collections_loader_ != nullptr) {
    return;
  }
  collection_error_info_.ClearError();

  net::NetworkTrafficAnnotationTag traffic_annotation =
      net::DefineNetworkTrafficAnnotation("backdrop_collection_names_download",
                                          R"(
        semantics {
          sender: "Desktop NTP Background Selector"
          description:
            "The Chrome Desktop New Tab Page background selector displays a "
            "rich set of wallpapers for users to choose from. Each wallpaper "
            "belongs to a collection (e.g. Arts, Landscape etc.). The list of "
            "all available collections is obtained from the Backdrop wallpaper "
            "service."
          trigger:
            "Clicking the customize (pencil) icon on the New Tab page."
          data:
            "The Backdrop protocol buffer messages. No user data is included."
          destination: GOOGLE_OWNED_SERVICE
          internal {
            contacts {
            email: "chrome-desktop-ntp@google.com"
            }
          }
          user_data {
            type: NONE
          }
          last_reviewed: "2023-06-13"
        }
        policy {
          cookies_allowed: NO
          setting:
            "Users can control this feature by selecting a non-Google default "
            "search engine in Chrome settings under 'Search Engine'."
          chrome_policy {
            DefaultSearchProviderEnabled {
              policy_options {mode: MANDATORY}
              DefaultSearchProviderEnabled: false
            }
          }
        })");

  ntp::background::GetCollectionsRequest request;
  // The language field may include the country code (e.g. "en-US").
  request.set_language(application_locale_storage_->Get());
  request.add_filtering_label(filtering_label);
  // Add some extra filtering information in case we need to target a specific
  // milestone post release.
  request.add_filtering_label(base::StrCat(
      {filtering_label, ".M", version_info::GetMajorVersionNumber()}));
  // Add filtering for Panorama feature.
  request.add_filtering_label(base::StrCat({filtering_label, ".panorama"}));
  request.add_filtering_label(base::StrCat({filtering_label, ".gm3"}));
  if (base::FeatureList::IsEnabled(
          ntp_features::kNtpBackgroundImageErrorDetection)) {
    request.add_filtering_label(
        base::StrCat({filtering_label, ".error_detection"}));
  }

  std::string serialized_proto;
  request.SerializeToString(&serialized_proto);

  auto resource_request = std::make_unique<network::ResourceRequest>();
  resource_request->url = collections_api_url_;
  resource_request->method = "POST";
  resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;

  collections_loader_ = network::SimpleURLLoader::Create(
      std::move(resource_request), traffic_annotation);
  collections_loader_->AttachStringForUpload(serialized_proto, kProtoMimeType);
  collections_loader_->DownloadToString(
      url_loader_factory_.get(),
      base::BindOnce(&NtpBackgroundService::OnCollectionInfoFetchComplete,
                     base::Unretained(this)),
      1024 * 1024);
}

void NtpBackgroundService::FetchCollectionInfo() {
  // Calls `FetchCollectionInfo` method using the default filter.
  FetchCollectionInfo(GetFilteringLabel());
}

void NtpBackgroundService::OnCollectionInfoFetchComplete(
    std::optional<std::string> response_body) {
  collection_info_.clear();
  // The loader will be deleted when the request is handled.
  std::unique_ptr<network::SimpleURLLoader> loader_deleter(
      std::move(collections_loader_));

  if (!response_body) {
    // This represents network errors (i.e. the server did not provide a
    // response).
    DVLOG(1) << "Request failed with error: " << loader_deleter->NetError();
    collection_error_info_.error_type = ErrorType::NET_ERROR;
    collection_error_info_.net_error = loader_deleter->NetError();
    NotifyObservers(FetchComplete::COLLECTION_INFO);
    return;
  }

  ntp::background::GetCollectionsResponse collections_response;
  if (!collections_response.ParseFromString(*response_body)) {
    DVLOG(1) << "Deserializing Backdrop wallpaper proto for collection info "
                "failed.";
    collection_error_info_.error_type = ErrorType::SERVICE_ERROR;
    NotifyObservers(FetchComplete::COLLECTION_INFO);
    return;
  }

  for (int i = 0; i < collections_response.collections_size(); ++i) {
    ntp::background::Collection collection =
        collections_response.collections(i);
    if (collection.preview_size() > 0 &&
        collection.preview(0).has_image_url()) {
      collection_info_.push_back(CollectionInfo::CreateFromProto(
          collection, /*preview_image_url=*/AddOptionsToImageURL(
              collection.preview(0).image_url(), thumbnail_image_options_)));
    } else {
      collection_info_.push_back(CollectionInfo::CreateFromProto(
          collection, /*preview_image_url=*/std::nullopt));
    }
  }
  NotifyObservers(FetchComplete::COLLECTION_INFO);
}

void NtpBackgroundService::FetchCollectionImageInfo(
    const std::string& collection_id,
    FetchCollectionImageCallback callback) {
  FetchCollectionImageInfoInternal(
      collection_id,
      base::BindOnce(&NtpBackgroundService::OnCollectionImageInfoFetchComplete,
                     base::Unretained(this), collection_id,
                     std::move(callback)));
}

void NtpBackgroundService::FetchCollectionImageInfo(
    const std::string& collection_id) {
  collection_images_error_info_.ClearError();
  // Ignore subsequent requests to fetch collection image info.
  // TODO(crbug.com/40916951): Prioritize the latest request to fetch collection
  // images.
  if (!requested_collection_id_.empty()) {
    return;
  }
  requested_collection_id_ = collection_id;
  pending_image_url_header_loaders_.clear();
  FetchCollectionImageInfoInternal(
      collection_id,
      base::BindOnce(
          &NtpBackgroundService::OnCollectionImageInfoFetchComplete,
          base::Unretained(this), collection_id,
          base::BindOnce(&NtpBackgroundService::OnCollectionImageInfoReceived,
                         base::Unretained(this))));
}

void NtpBackgroundService::OnCollectionImageInfoFetchComplete(
    const std::string& collection_id,
    FetchCollectionImageCallback callback,
    ntp::background::GetImagesInCollectionResponse images_response,
    ErrorType error_type) {
  std::vector<CollectionImage> collection_images;

  if (error_type == ErrorType::NET_ERROR) {
    // This represents network errors (i.e. the server did not provide
    // a response).
    DVLOG(1) << "Request failed with error: "
             << collection_images_error_info_.net_error;
  } else if (error_type == ErrorType::SERVICE_ERROR) {
    DVLOG(1) << "Deserializing Backdrop wallpaper proto for image "
                "info failed.";
  } else {
    for (int i = 0; i < images_response.images_size(); ++i) {
      const ntp::background::Image image = images_response.images(i);
      collection_images.push_back(CollectionImage::CreateFromProto(
          collection_id, image,
          /*default_image_url=*/
          AddOptionsToImageURL(image.image_url(), default_image_options_),
          /*thumbnail_image_url=*/
          AddOptionsToImageURL(image.image_url(), thumbnail_image_options_)));
    }
  }

  // Execute callback with the fetched collection images data.
  std::move(callback).Run(collection_images, error_type);
}

void NtpBackgroundService::OnCollectionImageInfoReceived(
    const std::vector<CollectionImage>& collection_images,
    ErrorType error_type) {
  collection_images_.clear();
  if (error_type != ErrorType::NONE) {
    collection_images_error_info_.error_type = error_type;
    NotifyObservers(FetchComplete::COLLECTION_IMAGE_INFO);
    return;
  }

  collection_images_ = collection_images;
  NotifyObservers(FetchComplete::COLLECTION_IMAGE_INFO);
}

void NtpBackgroundService::VerifyImageURL(
    const GURL& url,
    base::OnceCallback<void(int)> image_url_headers_received_callback) {
  constexpr net::NetworkTrafficAnnotationTag traffic_annotation =
      net::DefineNetworkTrafficAnnotation("ntp_image_url_verification",
                                          R"(
        semantics {
          sender: "Desktop NTP Background Selector"
          description:
            "The Chrome Desktop New Tab Page background selector displays a "
            "rich set of wallpapers for users to choose from. Each wallpaper "
            "belongs to a collection (e.g. Arts, Landscape etc.). "
            "This fetches a wallpaper image's link to make sure its "
            "resource is reachable."
          trigger:
            "When the user has a non-uploaded background image set and "
            "opens the New Tab Page, clicks the customize (pencil) icon "
            "on the New Tab page, or clicks a theme "
            "collection on the New Tab Page."
          data:
            "The HTTP headers for a NTP background image."
          destination: GOOGLE_OWNED_SERVICE
          internal {
            contacts {
            email: "chrome-desktop-ntp@google.com"
            }
          }
          user_data {
            type: NONE
          }
          last_reviewed: "2023-07-13"
        }
        policy {
          cookies_allowed: NO
          setting:
            "Users can control this feature by selecting a non-Google default "
            "search engine in Chrome settings under 'Search Engine'."
          chrome_policy {
            DefaultSearchProviderEnabled {
              policy_options {mode: MANDATORY}
              DefaultSearchProviderEnabled: false
            }
          }
        })");
  auto resource_request = std::make_unique<network::ResourceRequest>();
  resource_request->method = "GET";
  resource_request->url = url;
  resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
  std::unique_ptr<network::SimpleURLLoader> image_url_header_loader =
      network::SimpleURLLoader::Create(std::move(resource_request),
                                       traffic_annotation);
  network::SimpleURLLoader* const image_url_header_loader_ptr =
      image_url_header_loader.get();
  image_url_header_loader_ptr->SetRetryOptions(
      /*max_retries=*/3,
      network::SimpleURLLoader::RetryMode::RETRY_ON_5XX |
          network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE |
          network::SimpleURLLoader::RETRY_ON_NAME_NOT_RESOLVED);
  auto it = pending_image_url_header_loaders_.insert(
      pending_image_url_header_loaders_.begin(),
      std::move(image_url_header_loader));
  image_url_header_loader_ptr->DownloadHeadersOnly(
      url_loader_factory_.get(),
      base::BindOnce(&NtpBackgroundService::OnImageURLHeadersFetchComplete,
                     base::Unretained(this), std::move(it),
                     std::move(image_url_headers_received_callback),
                     base::TimeTicks::Now()));
}

void NtpBackgroundService::OnImageURLHeadersFetchComplete(
    URLLoaderList::iterator it,
    base::OnceCallback<void(int)> image_url_headers_received_callback,
    base::TimeTicks request_start,
    scoped_refptr<net::HttpResponseHeaders> headers) {
  if (pending_image_url_header_loaders_.empty()) {
    return;
  }
  pending_image_url_header_loaders_.erase(it);
  int headers_response_code =
      headers ? headers->response_code() : net::ERR_FAILED;

  UMA_HISTOGRAM_SPARSE("NewTabPage.BackgroundService.Images.Headers.StatusCode",
                       headers_response_code);
  auto request_time = base::TimeTicks::Now() - request_start;
  UMA_HISTOGRAM_TIMES(
      "NewTabPage.BackgroundService.Images.Headers.RequestLatency",
      request_time);
  if (headers_response_code == net::HTTP_OK) {
    UMA_HISTOGRAM_TIMES(
        "NewTabPage.BackgroundService.Images.Headers.RequestLatency.Ok",
        request_time);
  } else if (headers_response_code == net::HTTP_NOT_FOUND) {
    UMA_HISTOGRAM_TIMES(
        "NewTabPage.BackgroundService.Images.Headers.RequestLatency.NotFound",
        request_time);
  } else {
    UMA_HISTOGRAM_TIMES(
        "NewTabPage.BackgroundService.Images.Headers.RequestLatency.Other",
        request_time);
  }

  std::move(image_url_headers_received_callback).Run(headers_response_code);
}

void NtpBackgroundService::FetchReplacementCollectionPreviewImage(
    const std::string& collection_id,
    FetchReplacementImageCallback fetch_replacement_image_callback) {
  FetchCollectionImageInfoInternal(
      collection_id,
      base::BindOnce(&NtpBackgroundService::
                         OnFetchReplacementCollectionPreviewImageComplete,
                     base::Unretained(this),
                     std::move(fetch_replacement_image_callback)));
}

void NtpBackgroundService::OnFetchReplacementCollectionPreviewImageComplete(
    FetchReplacementImageCallback fetch_replacement_image_callback,
    ntp::background::GetImagesInCollectionResponse images_response,
    ErrorType error_type) {
  if (error_type != ErrorType::NONE || images_response.images_size() == 0) {
    std::move(fetch_replacement_image_callback).Run(std::nullopt);
    return;
  }
  // Attempt to find an image URL in the collection that works. We start with
  // the first image's index, and increment, if needed, in the callback
  // |OnReplacementCollectionPreviewImageHeadersReceived|.
  const int replacement_image_index = 0;
  const GURL replacement_image_url = AddOptionsToImageURL(
      images_response.images(replacement_image_index).image_url(),
      thumbnail_image_options_);
  VerifyImageURL(
      replacement_image_url,
      base::BindOnce(
          &NtpBackgroundService::
              OnReplacementCollectionPreviewImageHeadersReceived,
          base::Unretained(this), std::move(fetch_replacement_image_callback),
          images_response, replacement_image_index, replacement_image_url));
}

void NtpBackgroundService::OnReplacementCollectionPreviewImageHeadersReceived(
    FetchReplacementImageCallback fetch_replacement_image_callback,
    ntp::background::GetImagesInCollectionResponse images_response,
    int replacement_image_index,
    const GURL& replacement_image_url,
    int headers_response_code) {
  if (headers_response_code == net::HTTP_OK) {
    std::move(fetch_replacement_image_callback).Run(replacement_image_url);
  } else if (replacement_image_index == images_response.images_size() - 1) {
    std::move(fetch_replacement_image_callback).Run(std::nullopt);
  } else {
    replacement_image_index++;
    const GURL next_replacement_image_url = AddOptionsToImageURL(
        images_response.images(replacement_image_index).image_url(),
        thumbnail_image_options_);
    VerifyImageURL(
        next_replacement_image_url,
        base::BindOnce(&NtpBackgroundService::
                           OnReplacementCollectionPreviewImageHeadersReceived,
                       base::Unretained(this),
                       std::move(fetch_replacement_image_callback),
                       images_response, replacement_image_index,
                       next_replacement_image_url));
  }
}

void NtpBackgroundService::FetchNextCollectionImage(
    const std::string& collection_id,
    const std::optional<std::string>& resume_token) {
  next_image_error_info_.ClearError();
  if (next_image_loader_ != nullptr) {
    return;
  }

  net::NetworkTrafficAnnotationTag traffic_annotation =
      net::DefineNetworkTrafficAnnotation("backdrop_next_image_download",
                                          R"(
        semantics {
          sender: "Desktop NTP Background Selector"
          description:
            "The Chrome Desktop New Tab Page background selector displays a "
            "rich set of wallpapers for users to choose from. Each wallpaper "
            "belongs to a collection (e.g. Arts, Landscape etc.). The list of "
            "all available collections is obtained from the Backdrop wallpaper "
            "service."
          trigger:
            "Clicking the customize (pencil) icon on the New Tab page."
          data:
            "The Backdrop protocol buffer messages. ApplicationLocale is "
            "included in the request."
          destination: GOOGLE_OWNED_SERVICE
          internal {
            contacts {
            email: "chrome-desktop-ntp@google.com"
            }
          }
          user_data {
            type: NONE
          }
          last_reviewed: "2023-06-13"
        }
        policy {
          cookies_allowed: NO
          setting:
            "Users can control this feature by selecting a non-Google default "
            "search engine in Chrome settings under 'Search Engine'."
          chrome_policy {
            DefaultSearchProviderEnabled {
              policy_options {mode: MANDATORY}
              DefaultSearchProviderEnabled: false
            }
          }
        })");

  requested_next_image_collection_id_ = collection_id;
  requested_next_image_resume_token_ = resume_token.value_or("");
  ntp::background::GetImageFromCollectionRequest request;
  *request.add_collection_ids() = requested_next_image_collection_id_;
  request.set_resume_token(requested_next_image_resume_token_);
  // The language field may include the country code (e.g. "en-US").
  request.set_language(application_locale_storage_->Get());
  std::string serialized_proto;
  request.SerializeToString(&serialized_proto);

  auto resource_request = std::make_unique<network::ResourceRequest>();
  resource_request->url = next_image_api_url_;
  resource_request->method = "POST";
  resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;

  next_image_loader_ = network::SimpleURLLoader::Create(
      std::move(resource_request), traffic_annotation);
  next_image_loader_->AttachStringForUpload(serialized_proto, kProtoMimeType);
  next_image_loader_->DownloadToString(
      url_loader_factory_.get(),
      base::BindOnce(&NtpBackgroundService::OnNextImageInfoFetchComplete,
                     base::Unretained(this)),
      1024 * 1024);
}

void NtpBackgroundService::OnNextImageInfoFetchComplete(
    std::optional<std::string> response_body) {
  // The loader will be deleted when the request is handled.
  std::unique_ptr<network::SimpleURLLoader> loader_deleter(
      std::move(next_image_loader_));

  if (!response_body) {
    // This represents network errors (i.e. the server did not provide a
    // response).
    DVLOG(1) << "Request failed with error: " << loader_deleter->NetError();
    next_image_error_info_.error_type = ErrorType::NET_ERROR;
    next_image_error_info_.net_error = loader_deleter->NetError();
    NotifyObservers(FetchComplete::NEXT_IMAGE_INFO);
    return;
  }

  ntp::background::GetImageFromCollectionResponse image_response;
  if (!image_response.ParseFromString(*response_body)) {
    DVLOG(1) << "Deserializing Backdrop wallpaper proto for next image failed.";
    next_image_error_info_.error_type = ErrorType::SERVICE_ERROR;
    NotifyObservers(FetchComplete::NEXT_IMAGE_INFO);
    return;
  }

  const ntp::background::Image image = image_response.image();
  next_image_ = CollectionImage::CreateFromProto(
      requested_next_image_collection_id_, image,
      /*default_image_url=*/
      AddOptionsToImageURL(image.image_url(), default_image_options_),
      /*thumbnail_image_url=*/
      AddOptionsToImageURL(image.image_url(), thumbnail_image_options_));
  next_image_resume_token_ = image_response.resume_token();

  NotifyObservers(FetchComplete::NEXT_IMAGE_INFO);
}

void NtpBackgroundService::FetchCollectionImageInfoInternal(
    const std::string& collection_id,
    base::OnceCallback<void(ntp::background::GetImagesInCollectionResponse,
                            ErrorType)> collection_images_received_callback) {
  collection_images_error_info_.ClearError();

  net::NetworkTrafficAnnotationTag traffic_annotation =
      net::DefineNetworkTrafficAnnotation("backdrop_collection_images_download",
                                          R"(
        semantics {
          sender: "Desktop NTP Background Selector"
          description:
            "The Chrome Desktop New Tab Page background selector displays a "
            "rich set of wallpapers for users to choose from. Each wallpaper "
            "belongs to a collection (e.g. Arts, Landscape etc.). The list of "
            "all available collections is obtained from the Backdrop wallpaper "
            "service."
          trigger:
            "Clicking the customize (pencil) icon on the New Tab page."
          data:
            "The Backdrop protocol buffer messages. No user data is included."
          destination: GOOGLE_OWNED_SERVICE
          internal {
            contacts {
            email: "chrome-desktop-ntp@google.com"
            }
          }
          user_data {
            type: NONE
          }
          last_reviewed: "2023-06-13"
        }
        policy {
          cookies_allowed: NO
          setting:
            "Users can control this feature by selecting a non-Google default "
            "search engine in Chrome settings under 'Search Engine'."
          chrome_policy {
            DefaultSearchProviderEnabled {
              policy_options {mode: MANDATORY}
              DefaultSearchProviderEnabled: false
            }
          }
        })");
  auto resource_request = std::make_unique<network::ResourceRequest>();
  resource_request->url = collection_images_api_url_;
  resource_request->method = "POST";
  resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
  std::unique_ptr<network::SimpleURLLoader> collection_image_info_loader =
      network::SimpleURLLoader::Create(std::move(resource_request),
                                       traffic_annotation);
  network::SimpleURLLoader* const collection_image_info_loader_ptr =
      collection_image_info_loader.get();
  collection_image_info_loader->SetRetryOptions(
      /*max_retries=*/3,
      network::SimpleURLLoader::RetryMode::RETRY_ON_5XX |
          network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE |
          network::SimpleURLLoader::RETRY_ON_NAME_NOT_RESOLVED);
  auto it = pending_collection_image_info_loaders_.insert(
      pending_collection_image_info_loaders_.begin(),
      std::move(collection_image_info_loader));

  ntp::background::GetImagesInCollectionRequest request;
  request.set_collection_id(collection_id);
  // The language field may include the country code (e.g. "en-US").
  request.set_language(application_locale_storage_->Get());
  std::string serialized_proto;
  request.SerializeToString(&serialized_proto);
  collection_image_info_loader_ptr->AttachStringForUpload(serialized_proto,
                                                          kProtoMimeType);
  collection_image_info_loader_ptr->DownloadToString(
      url_loader_factory_.get(),
      base::BindOnce(
          [](URLLoaderList& pending_collection_image_info_loaders,
             URLLoaderList::iterator it,
             base::OnceCallback<void(
                 ntp::background::GetImagesInCollectionResponse, ErrorType)>
                 collection_images_received_callback,
             std::optional<std::string> response_body) {
            if (pending_collection_image_info_loaders.empty()) {
              return;
            }
            pending_collection_image_info_loaders.erase(it);

            ntp::background::GetImagesInCollectionResponse images_response;
            if (!response_body) {
              std::move(collection_images_received_callback)
                  .Run(std::move(images_response), ErrorType::NET_ERROR);
            } else if (!images_response.ParseFromString(*response_body)) {
              std::move(collection_images_received_callback)
                  .Run(std::move(images_response), ErrorType::SERVICE_ERROR);
            } else {
              std::move(collection_images_received_callback)
                  .Run(std::move(images_response), ErrorType::NONE);
            }
          },
          std::ref(pending_collection_image_info_loaders_), std::move(it),
          std::move(collection_images_received_callback)),
      1024 * 1024);
}

const std::vector<CollectionInfo>& NtpBackgroundService::collection_info()
    const {
  return collection_info_;
}

const std::vector<CollectionImage>& NtpBackgroundService::collection_images()
    const {
  return collection_images_;
}

void NtpBackgroundService::AddObserver(NtpBackgroundServiceObserver* observer) {
  observers_.AddObserver(observer);
}

void NtpBackgroundService::RemoveObserver(
    NtpBackgroundServiceObserver* observer) {
  observers_.RemoveObserver(observer);
}

bool NtpBackgroundService::IsValidBackdropUrl(const GURL& url) const {
  for (auto& image : collection_images_) {
    if (image.image_url == url) {
      return true;
    }
  }
  return false;
}

bool NtpBackgroundService::IsValidBackdropCollection(
    const std::string& collection_id) const {
  for (auto& collection : collection_info_) {
    if (collection.collection_id == collection_id) {
      return true;
    }
  }
  return false;
}

void NtpBackgroundService::AddValidBackdropUrlForTesting(const GURL& url) {
  CollectionImage image;
  image.image_url = url;
  collection_images_.push_back(image);
}

void NtpBackgroundService::AddValidBackdropCollectionForTesting(
    const std::string& collection_id) {
  CollectionInfo collection;
  collection.collection_id = collection_id;
  collection_info_.push_back(collection);
}

void NtpBackgroundService::AddValidBackdropUrlWithThumbnailForTesting(
    const GURL& url,
    const GURL& thumbnail_url) {
  CollectionImage image;
  image.image_url = url;
  image.thumbnail_image_url = thumbnail_url;
  collection_images_.push_back(image);
}

void NtpBackgroundService::SetNextCollectionImageForTesting(
    const CollectionImage& image) {
  next_image_ = image;
}

const GURL& NtpBackgroundService::GetThumbnailUrl(const GURL& image_url) {
  for (auto& image : collection_images_) {
    if (image.image_url == image_url) {
      return image.thumbnail_image_url;
    }
  }
  return GURL::EmptyGURL();
}

void NtpBackgroundService::NotifyObservers(FetchComplete fetch_complete) {
  for (auto& observer : observers_) {
    switch (fetch_complete) {
      case FetchComplete::COLLECTION_INFO:
        observer.OnCollectionInfoAvailable();
        break;
      case FetchComplete::COLLECTION_IMAGE_INFO:
        requested_collection_id_.clear();
        observer.OnCollectionImagesAvailable();
        break;
      case FetchComplete::NEXT_IMAGE_INFO:
        observer.OnNextCollectionImageAvailable();
        break;
    }
  }
}

GURL NtpBackgroundService::GetCollectionsLoadURLForTesting() const {
  return collections_api_url_;
}

GURL NtpBackgroundService::GetImagesURLForTesting() const {
  return collection_images_api_url_;
}

GURL NtpBackgroundService::GetNextImageURLForTesting() const {
  return next_image_api_url_;
}
