chromium/chrome/browser/ash/wallpaper_handlers/sea_pen_fetcher_unittest.cc

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/wallpaper_handlers/sea_pen_fetcher.h"

#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

#include "ash/constants/ash_features.h"
#include "ash/public/cpp/wallpaper/sea_pen_image.h"
#include "ash/webui/common/mojom/sea_pen.mojom.h"
#include "base/memory/raw_ptr.h"
#include "base/no_destructor.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/protobuf_matchers.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "chrome/browser/ash/wallpaper_handlers/sea_pen_utils.h"
#include "components/manta/features.h"
#include "components/manta/manta_service_callbacks.h"
#include "components/manta/manta_status.h"
#include "components/manta/proto/manta.pb.h"
#include "components/manta/snapper_provider.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/display/test/test_screen.h"
#include "ui/gfx/codec/jpeg_codec.h"
#include "ui/gfx/image/image_unittest_util.h"

namespace ash {}
namespace wallpaper_handlers {

namespace {

constexpr uint32_t kFakeGenerationSeed = 5;

constexpr std::string_view kThumbnailsLatencyMetric =
    "Ash.SeaPen.Api.Thumbnails.Latency";
constexpr std::string_view kThumbnailsStatusCodeMetric =
    "Ash.SeaPen.Api.Thumbnails.MantaStatusCode";
constexpr std::string_view kThumbnailsTimeoutMetric =
    "Ash.SeaPen.Api.Thumbnails.Timeout";
constexpr std::string_view kThumbnailsCountMetric =
    "Ash.SeaPen.Api.Thumbnails.Count";

constexpr std::string_view kWallpaperLatencyMetric =
    "Ash.SeaPen.Api.Wallpaper.Latency";
constexpr std::string_view kWallpaperStatusCodeMetric =
    "Ash.SeaPen.Api.Wallpaper.MantaStatusCode";
constexpr std::string_view kWallpaperTimeoutMetric =
    "Ash.SeaPen.Api.Wallpaper.Timeout";
constexpr std::string_view kWallpaperHasImageMetric =
    "Ash.SeaPen.Api.Wallpaper.HasImage";

constexpr std::string_view kFreeformThumbnailsLatencyMetric =
    "Ash.SeaPen.Freeform.Api.Thumbnails.Latency";
constexpr std::string_view kFreeformThumbnailsStatusCodeMetric =
    "Ash.SeaPen.Freeform.Api.Thumbnails.MantaStatusCode";
constexpr std::string_view kFreeformThumbnailsTimeoutMetric =
    "Ash.SeaPen.Freeform.Api.Thumbnails.Timeout";
constexpr std::string_view kFreeformThumbnailsCountMetric =
    "Ash.SeaPen.Freeform.Api.Thumbnails.Count";

constexpr std::string_view kFreeformWallpaperLatencyMetric =
    "Ash.SeaPen.Freeform.Api.Wallpaper.Latency";
constexpr std::string_view kFreeformWallpaperStatusCodeMetric =
    "Ash.SeaPen.Freeform.Api.Wallpaper.MantaStatusCode";
constexpr std::string_view kFreeformWallpaperTimeoutMetric =
    "Ash.SeaPen.Freeform.Api.Wallpaper.Timeout";
constexpr std::string_view kFreeformWallpaperHasImageMetric =
    "Ash.SeaPen.Freeform.Api.Wallpaper.HasImage";

const SkBitmap CreateTestBitmap() {
  return gfx::test::CreateBitmap(1, SK_ColorMAGENTA);
}

const std::string_view GetJpgBytes() {
  static const base::NoDestructor<std::string> jpg_bytes([] {
    SkBitmap bitmap = CreateTestBitmap();
    std::vector<unsigned char> data;
    gfx::JPEGCodec::Encode(bitmap, /*quality=*/50, &data);
    return std::string(data.begin(), data.end());
  }());
  return *jpg_bytes;
}

ash::personalization_app::mojom::SeaPenQueryPtr MakeTemplateQuery() {
  return ash::personalization_app::mojom::SeaPenQuery::NewTemplateQuery(
      ash::personalization_app::mojom::SeaPenTemplateQuery::New(
          ash::personalization_app::mojom::SeaPenTemplateId::kFlower,
          ::base::flat_map<
              ash::personalization_app::mojom::SeaPenTemplateChip,
              ash::personalization_app::mojom::SeaPenTemplateOption>(
              {{ash::personalization_app::mojom::SeaPenTemplateChip::
                    kFlowerColor,
                ash::personalization_app::mojom::SeaPenTemplateOption::
                    kFlowerColorBlue},
               {ash::personalization_app::mojom::SeaPenTemplateChip::
                    kFlowerType,
                ash::personalization_app::mojom::SeaPenTemplateOption::
                    kFlowerTypeRose}}),
          ash::personalization_app::mojom::SeaPenUserVisibleQuery::New(
              "test template query", "test template title")));
}

ash::personalization_app::mojom::SeaPenQueryPtr MakeFreeformQuery() {
  return ash::personalization_app::mojom ::SeaPenQuery::NewTextQuery(
      "test query");
}

std::unique_ptr<manta::proto::Response> CreateMantaResponse(
    size_t output_data_length) {
  auto response = std::make_unique<manta::proto::Response>();
  for (size_t i = 0; i < output_data_length; i++) {
    auto* output_data = response->add_output_data();
    output_data->set_generation_seed(kFakeGenerationSeed + i);
    output_data->mutable_image()->set_serialized_bytes(
        std::string(GetJpgBytes()));
  }
  return response;
}

std::unique_ptr<manta::proto::Response> CreateMantaResponseWithFilteredReason(
    manta::proto::FilteredReason filteredReason) {
  auto response = CreateMantaResponse(0);
  auto* filtered_data = response->add_filtered_data();
  filtered_data->set_reason(filteredReason);
  return response;
}

MATCHER_P(AreJpgBytesClose, expected_bitmap, "") {
  std::unique_ptr<SkBitmap> actual_bitmap = gfx::JPEGCodec::Decode(
      reinterpret_cast<const unsigned char*>(arg.data()), arg.size());
  return actual_bitmap != nullptr &&
         gfx::test::AreBitmapsClose(expected_bitmap, *actual_bitmap,
                                    /*max_deviation=*/1);
}

testing::Matcher<ash::SeaPenImage> MatchesSeaPenImage(
    const SkBitmap& expected_bitmap,
    const uint32_t expected_id) {
  return testing::AllOf(testing::Field(&ash::SeaPenImage::id, expected_id),
                        testing::Field(&ash::SeaPenImage::jpg_bytes,
                                       AreJpgBytesClose(expected_bitmap)));
}

class MockSnapperProvider : virtual public manta::SnapperProvider {
 public:
  MockSnapperProvider() : manta::SnapperProvider(nullptr, nullptr) {}

  MockSnapperProvider(const MockSnapperProvider&) = delete;
  MockSnapperProvider& operator=(const MockSnapperProvider&) = delete;

  ~MockSnapperProvider() override = default;

  MOCK_METHOD(void,
              Call,
              (manta::proto::Request& request,
               net::NetworkTrafficAnnotationTag traffic_annotation,
               manta::MantaProtoResponseCallback done_callback),
              (override));
};

}  // namespace

class SeaPenFetcherTest : public testing::Test {
 public:
  SeaPenFetcherTest() {
    scoped_feature_list_.InitWithFeatures(
        {
            ash::features::kSeaPen,
            ash::features::kFeatureManagementSeaPen,
            manta::features::kMantaService,
        },
        {});
  }

  SeaPenFetcherTest(const SeaPenFetcherTest&) = delete;
  SeaPenFetcherTest& operator=(const SeaPenFetcherTest&) = delete;

  ~SeaPenFetcherTest() override = default;

  void SetUp() override {
    testing::Test::SetUp();
    display::Screen::SetScreenInstance(&test_screen_);
    auto mock_snapper_provider = std::make_unique<MockSnapperProvider>();
    mock_snapper_provider_ =
        static_cast<testing::StrictMock<MockSnapperProvider>*>(
            mock_snapper_provider.get());
    sea_pen_fetcher_ =
        SeaPenFetcher::MakeSeaPenFetcher(std::move(mock_snapper_provider));
  }

  void TearDown() override {
    testing::Test::TearDown();
    display::Screen::SetScreenInstance(nullptr);
    mock_snapper_provider_ = nullptr;
  }

  SeaPenFetcher* sea_pen_fetcher() { return sea_pen_fetcher_.get(); }

  base::HistogramTester& histogram_tester() { return histogram_tester_; }

  testing::StrictMock<MockSnapperProvider>& snapper_provider() {
    return *mock_snapper_provider_;
  }

  void FastForwardBy(const base::TimeDelta delta) {
    task_environment_.FastForwardBy(delta);
  }

 protected:
  base::test::ScopedFeatureList scoped_feature_list_;

 private:
  base::test::TaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
  base::HistogramTester histogram_tester_;
  display::test::TestScreen test_screen_;
  raw_ptr<testing::StrictMock<MockSnapperProvider>> mock_snapper_provider_;
  std::unique_ptr<SeaPenFetcher> sea_pen_fetcher_;
};

TEST_F(SeaPenFetcherTest, ThumbnailsCallsSnapperProvider) {
  auto query = MakeTemplateQuery();

  EXPECT_CALL(
      snapper_provider(),
      Call(base::test::EqualsProto(CreateMantaRequest(
               query, /*generation_seed=*/std::nullopt,
               /*num_outputs=*/SeaPenFetcher::kNumTemplateThumbnailsRequested,
               {880, 440}, manta::proto::FeatureName::CHROMEOS_WALLPAPER)),
           testing::_, testing::_))
      .WillOnce([](const manta::proto::Request& request,
                   net::NetworkTrafficAnnotationTag traffic_annotation,
                   manta::MantaProtoResponseCallback done_callback) {
        base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE,
            base::BindOnce(
                [](manta::MantaProtoResponseCallback delayed_callback) {
                  std::move(delayed_callback)
                      .Run(CreateMantaResponse(
                               SeaPenFetcher::kNumTemplateThumbnailsRequested),
                           {.status_code = manta::MantaStatusCode::kOk,
                            .message = std::string()});
                },
                std::move(done_callback)));
      });

  base::test::TestFuture<std::optional<std::vector<ash::SeaPenImage>>,
                         manta::MantaStatusCode>
      fetch_thumbnails_future;

  sea_pen_fetcher()->FetchThumbnails(
      manta::proto::FeatureName::CHROMEOS_WALLPAPER, query,
      fetch_thumbnails_future.GetCallback());

  EXPECT_EQ(manta::MantaStatusCode::kOk,
            fetch_thumbnails_future.Get<manta::MantaStatusCode>());

  std::vector<testing::Matcher<ash::SeaPenImage>> matchers;
  for (size_t i = 0; i < SeaPenFetcher::kNumTemplateThumbnailsRequested; i++) {
    matchers.push_back(
        MatchesSeaPenImage(CreateTestBitmap(), kFakeGenerationSeed + i));
  }
  EXPECT_THAT(fetch_thumbnails_future
                  .Get<std::optional<std::vector<ash::SeaPenImage>>>()
                  .value(),
              testing::UnorderedElementsAreArray(matchers));

  histogram_tester().ExpectTotalCount(kThumbnailsLatencyMetric, 1);
  histogram_tester().ExpectUniqueSample(kThumbnailsStatusCodeMetric,
                                        manta::MantaStatusCode::kOk, 1);
  histogram_tester().ExpectUniqueSample(kThumbnailsTimeoutMetric, false, 1);
  histogram_tester().ExpectUniqueSample(
      kThumbnailsCountMetric, SeaPenFetcher::kNumTemplateThumbnailsRequested,
      1);
}

TEST_F(SeaPenFetcherTest, FreeformThumbnailsCallsSnapperProvider) {
  auto query = MakeFreeformQuery();

  EXPECT_CALL(
      snapper_provider(),
      Call(base::test::EqualsProto(CreateMantaRequest(
               query, /*generation_seed=*/std::nullopt,
               /*num_outputs=*/SeaPenFetcher::kNumTextThumbnailsRequested,
               {880, 440}, manta::proto::FeatureName::CHROMEOS_WALLPAPER)),
           testing::_, testing::_))
      .WillOnce([](const manta::proto::Request& request,
                   net::NetworkTrafficAnnotationTag traffic_annotation,
                   manta::MantaProtoResponseCallback done_callback) {
        base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE,
            base::BindOnce(
                [](manta::MantaProtoResponseCallback delayed_callback) {
                  std::move(delayed_callback)
                      .Run(CreateMantaResponse(
                               SeaPenFetcher::kNumTextThumbnailsRequested),
                           {.status_code = manta::MantaStatusCode::kOk,
                            .message = std::string()});
                },
                std::move(done_callback)));
      });

  base::test::TestFuture<std::optional<std::vector<ash::SeaPenImage>>,
                         manta::MantaStatusCode>
      fetch_thumbnails_future;

  sea_pen_fetcher()->FetchThumbnails(
      manta::proto::FeatureName::CHROMEOS_WALLPAPER, query,
      fetch_thumbnails_future.GetCallback());

  EXPECT_EQ(manta::MantaStatusCode::kOk,
            fetch_thumbnails_future.Get<manta::MantaStatusCode>());

  std::vector<testing::Matcher<ash::SeaPenImage>> matchers;
  for (size_t i = 0; i < SeaPenFetcher::kNumTextThumbnailsRequested; i++) {
    matchers.push_back(
        MatchesSeaPenImage(CreateTestBitmap(), kFakeGenerationSeed + i));
  }
  EXPECT_THAT(fetch_thumbnails_future
                  .Get<std::optional<std::vector<ash::SeaPenImage>>>()
                  .value(),
              testing::UnorderedElementsAreArray(matchers));

  histogram_tester().ExpectTotalCount(kFreeformThumbnailsLatencyMetric, 1);
  histogram_tester().ExpectUniqueSample(kFreeformThumbnailsStatusCodeMetric,
                                        manta::MantaStatusCode::kOk, 1);
  histogram_tester().ExpectUniqueSample(kFreeformThumbnailsTimeoutMetric, false,
                                        1);
  histogram_tester().ExpectUniqueSample(
      kFreeformThumbnailsCountMetric,
      SeaPenFetcher::kNumTextThumbnailsRequested, 1);
}

TEST_F(SeaPenFetcherTest, ThumbnailsEmptyReturnsError) {
  EXPECT_CALL(snapper_provider(), Call(testing::_, testing::_, testing::_))
      .WillOnce([](const manta::proto::Request& request,
                   net::NetworkTrafficAnnotationTag traffic_annotation,
                   manta::MantaProtoResponseCallback done_callback) {
        base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE,
            base::BindOnce(
                [](manta::MantaProtoResponseCallback delayed_callback) {
                  std::move(delayed_callback)
                      .Run(CreateMantaResponse(0),
                           {.status_code = manta::MantaStatusCode::kOk,
                            .message = std::string()});
                },
                std::move(done_callback)));
      });

  base::test::TestFuture<std::optional<std::vector<ash::SeaPenImage>>,
                         manta::MantaStatusCode>
      fetch_thumbnails_future;
  sea_pen_fetcher()->FetchThumbnails(
      manta::proto::FeatureName::CHROMEOS_WALLPAPER, MakeTemplateQuery(),
      fetch_thumbnails_future.GetCallback());

  EXPECT_EQ(manta::MantaStatusCode::kGenericError,
            fetch_thumbnails_future.Get<manta::MantaStatusCode>());
  EXPECT_EQ(std::nullopt,
            fetch_thumbnails_future
                .Get<std::optional<std::vector<ash::SeaPenImage>>>());

  // Recorded an entry in the "0" thumbnail count bucket 1 time.
  histogram_tester().ExpectUniqueSample(kThumbnailsCountMetric, 0, 1);
  histogram_tester().ExpectUniqueSample(kThumbnailsStatusCodeMetric,
                                        manta::MantaStatusCode::kOk, 1);
  histogram_tester().ExpectTotalCount(kThumbnailsLatencyMetric, 1);
  histogram_tester().ExpectUniqueSample(kThumbnailsTimeoutMetric, false, 1);
}

TEST_F(SeaPenFetcherTest, FreeformThumbnailsEmptyReturnsError) {
  EXPECT_CALL(snapper_provider(), Call(testing::_, testing::_, testing::_))
      .WillOnce([](const manta::proto::Request& request,
                   net::NetworkTrafficAnnotationTag traffic_annotation,
                   manta::MantaProtoResponseCallback done_callback) {
        base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE,
            base::BindOnce(
                [](manta::MantaProtoResponseCallback delayed_callback) {
                  std::move(delayed_callback)
                      .Run(CreateMantaResponse(0),
                           {.status_code = manta::MantaStatusCode::kOk,
                            .message = std::string()});
                },
                std::move(done_callback)));
      });

  base::test::TestFuture<std::optional<std::vector<ash::SeaPenImage>>,
                         manta::MantaStatusCode>
      fetch_thumbnails_future;
  sea_pen_fetcher()->FetchThumbnails(
      manta::proto::FeatureName::CHROMEOS_WALLPAPER, MakeFreeformQuery(),
      fetch_thumbnails_future.GetCallback());

  EXPECT_EQ(manta::MantaStatusCode::kGenericError,
            fetch_thumbnails_future.Get<manta::MantaStatusCode>());
  EXPECT_EQ(std::nullopt,
            fetch_thumbnails_future
                .Get<std::optional<std::vector<ash::SeaPenImage>>>());

  // Recorded an entry in the "0" thumbnail count bucket 1 time.
  histogram_tester().ExpectUniqueSample(kFreeformThumbnailsCountMetric, 0, 1);
  histogram_tester().ExpectUniqueSample(kFreeformThumbnailsStatusCodeMetric,
                                        manta::MantaStatusCode::kOk, 1);
  histogram_tester().ExpectTotalCount(kFreeformThumbnailsLatencyMetric, 1);
  histogram_tester().ExpectUniqueSample(kFreeformThumbnailsTimeoutMetric, false,
                                        1);
}

TEST_F(SeaPenFetcherTest,
       FreeformThumbnailsEmptyReturnsErrorDueToTextBlocklist) {
  scoped_feature_list_.Reset();
  scoped_feature_list_.InitWithFeatures(
      {ash::features::kSeaPen, ash::features::kFeatureManagementSeaPen,
       manta::features::kMantaService, ash::features::kSeaPenTextInput},
      {});
  EXPECT_CALL(snapper_provider(), Call(testing::_, testing::_, testing::_))
      .WillOnce([](const manta::proto::Request& request,
                   net::NetworkTrafficAnnotationTag traffic_annotation,
                   manta::MantaProtoResponseCallback done_callback) {
        base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE,
            base::BindOnce(
                [](manta::MantaProtoResponseCallback delayed_callback) {
                  std::move(delayed_callback)
                      .Run(CreateMantaResponseWithFilteredReason(
                               manta::proto::FilteredReason::TEXT_BLOCKLIST),
                           {.status_code = manta::MantaStatusCode::kOk,
                            .message = std::string()});
                },
                std::move(done_callback)));
      });

  base::test::TestFuture<std::optional<std::vector<ash::SeaPenImage>>,
                         manta::MantaStatusCode>
      fetch_thumbnails_future;
  sea_pen_fetcher()->FetchThumbnails(
      manta::proto::FeatureName::CHROMEOS_WALLPAPER, MakeFreeformQuery(),
      fetch_thumbnails_future.GetCallback());

  EXPECT_EQ(manta::MantaStatusCode::kBlockedOutputs,
            fetch_thumbnails_future.Get<manta::MantaStatusCode>());
  EXPECT_EQ(std::nullopt,
            fetch_thumbnails_future
                .Get<std::optional<std::vector<ash::SeaPenImage>>>());

  // Recorded an entry in the "0" thumbnail count bucket 1 time.
  histogram_tester().ExpectUniqueSample(kFreeformThumbnailsCountMetric, 0, 1);
  histogram_tester().ExpectUniqueSample(kFreeformThumbnailsStatusCodeMetric,
                                        manta::MantaStatusCode::kOk, 1);
  histogram_tester().ExpectTotalCount(kFreeformThumbnailsLatencyMetric, 1);
  histogram_tester().ExpectUniqueSample(kFreeformThumbnailsTimeoutMetric, false,
                                        1);
}

TEST_F(SeaPenFetcherTest, FreeformThumbnailsEmptyReturnsErrorDueToImageSafety) {
  scoped_feature_list_.Reset();
  scoped_feature_list_.InitWithFeatures(
      {ash::features::kSeaPen, ash::features::kFeatureManagementSeaPen,
       manta::features::kMantaService, ash::features::kSeaPenTextInput},
      {});

  EXPECT_CALL(snapper_provider(), Call(testing::_, testing::_, testing::_))
      .WillOnce([](const manta::proto::Request& request,
                   net::NetworkTrafficAnnotationTag traffic_annotation,
                   manta::MantaProtoResponseCallback done_callback) {
        base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE,
            base::BindOnce(
                [](manta::MantaProtoResponseCallback delayed_callback) {
                  std::move(delayed_callback)
                      .Run(CreateMantaResponseWithFilteredReason(
                               manta::proto::FilteredReason::IMAGE_SAFETY),
                           {.status_code = manta::MantaStatusCode::kOk,
                            .message = std::string()});
                },
                std::move(done_callback)));
      });

  base::test::TestFuture<std::optional<std::vector<ash::SeaPenImage>>,
                         manta::MantaStatusCode>
      fetch_thumbnails_future;
  sea_pen_fetcher()->FetchThumbnails(
      manta::proto::FeatureName::CHROMEOS_WALLPAPER, MakeFreeformQuery(),
      fetch_thumbnails_future.GetCallback());

  EXPECT_EQ(manta::MantaStatusCode::kBlockedOutputs,
            fetch_thumbnails_future.Get<manta::MantaStatusCode>());
  EXPECT_EQ(std::nullopt,
            fetch_thumbnails_future
                .Get<std::optional<std::vector<ash::SeaPenImage>>>());

  // Recorded an entry in the "0" thumbnail count bucket 1 time.
  histogram_tester().ExpectUniqueSample(kFreeformThumbnailsCountMetric, 0, 1);
  histogram_tester().ExpectUniqueSample(kFreeformThumbnailsStatusCodeMetric,
                                        manta::MantaStatusCode::kOk, 1);
  histogram_tester().ExpectTotalCount(kFreeformThumbnailsLatencyMetric, 1);
  histogram_tester().ExpectUniqueSample(kFreeformThumbnailsTimeoutMetric, false,
                                        1);
}

TEST_F(SeaPenFetcherTest,
       FreeformThumbnailsEmptyReturnsGenericErrorDueToOtherFilterReason) {
  scoped_feature_list_.Reset();
  scoped_feature_list_.InitWithFeatures(
      {ash::features::kSeaPen, ash::features::kFeatureManagementSeaPen,
       manta::features::kMantaService, ash::features::kSeaPenTextInput},
      {});

  EXPECT_CALL(snapper_provider(), Call(testing::_, testing::_, testing::_))
      .WillOnce([](const manta::proto::Request& request,
                   net::NetworkTrafficAnnotationTag traffic_annotation,
                   manta::MantaProtoResponseCallback done_callback) {
        base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE,
            base::BindOnce(
                [](manta::MantaProtoResponseCallback delayed_callback) {
                  std::move(delayed_callback)
                      .Run(CreateMantaResponseWithFilteredReason(
                               manta::proto::FilteredReason::TEXT_LOW_QUALITY),
                           {.status_code = manta::MantaStatusCode::kOk,
                            .message = std::string()});
                },
                std::move(done_callback)));
      });

  base::test::TestFuture<std::optional<std::vector<ash::SeaPenImage>>,
                         manta::MantaStatusCode>
      fetch_thumbnails_future;
  sea_pen_fetcher()->FetchThumbnails(
      manta::proto::FeatureName::CHROMEOS_WALLPAPER, MakeFreeformQuery(),
      fetch_thumbnails_future.GetCallback());

  EXPECT_EQ(manta::MantaStatusCode::kGenericError,
            fetch_thumbnails_future.Get<manta::MantaStatusCode>());
  EXPECT_EQ(std::nullopt,
            fetch_thumbnails_future
                .Get<std::optional<std::vector<ash::SeaPenImage>>>());

  // Recorded an entry in the "0" thumbnail count bucket 1 time.
  histogram_tester().ExpectUniqueSample(kFreeformThumbnailsCountMetric, 0, 1);
  histogram_tester().ExpectUniqueSample(kFreeformThumbnailsStatusCodeMetric,
                                        manta::MantaStatusCode::kOk, 1);
  histogram_tester().ExpectTotalCount(kFreeformThumbnailsLatencyMetric, 1);
  histogram_tester().ExpectUniqueSample(kFreeformThumbnailsTimeoutMetric, false,
                                        1);
}

TEST_F(SeaPenFetcherTest, ThumbnailsTimeoutHandled) {
  EXPECT_CALL(snapper_provider(), Call(testing::_, testing::_, testing::_))
      .WillOnce([](const manta::proto::Request& request,
                   net::NetworkTrafficAnnotationTag traffic_annotation,
                   manta::MantaProtoResponseCallback done_callback) {
        // Run `done_callback` but one second too late.
        base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
            FROM_HERE,
            base::BindOnce(
                [](manta::MantaProtoResponseCallback delayed_callback) {
                  std::move(delayed_callback)
                      .Run(CreateMantaResponse(
                               SeaPenFetcher::kNumTemplateThumbnailsRequested),
                           {.status_code = manta::MantaStatusCode::kOk,
                            .message = std::string()});
                },
                std::move(done_callback)),
            SeaPenFetcher::kRequestTimeout + base::Seconds(1));
      });

  base::test::TestFuture<std::optional<std::vector<ash::SeaPenImage>>,
                         manta::MantaStatusCode>
      fetch_thumbnails_future;
  sea_pen_fetcher()->FetchThumbnails(
      manta::proto::FeatureName::CHROMEOS_WALLPAPER, MakeTemplateQuery(),
      fetch_thumbnails_future.GetCallback());

  // Trigger the timeout.
  FastForwardBy(SeaPenFetcher::kRequestTimeout + base::Milliseconds(1));

  EXPECT_EQ(manta::MantaStatusCode::kGenericError,
            fetch_thumbnails_future.Get<manta::MantaStatusCode>());
  EXPECT_EQ(std::nullopt,
            fetch_thumbnails_future
                .Get<std::optional<std::vector<ash::SeaPenImage>>>());

  // Recorded 1 timeout.
  histogram_tester().ExpectUniqueSample(kThumbnailsTimeoutMetric, true, 1);

  // Does not record following metrics on timeout.
  histogram_tester().ExpectTotalCount(kThumbnailsLatencyMetric, 0);
  histogram_tester().ExpectTotalCount(kThumbnailsStatusCodeMetric, 0);
  histogram_tester().ExpectTotalCount(kThumbnailsCountMetric, 0);
}

TEST_F(SeaPenFetcherTest, FreeformThumbnailsTimeoutHandled) {
  EXPECT_CALL(snapper_provider(), Call(testing::_, testing::_, testing::_))
      .WillOnce([](const manta::proto::Request& request,
                   net::NetworkTrafficAnnotationTag traffic_annotation,
                   manta::MantaProtoResponseCallback done_callback) {
        // Run `done_callback` but one second too late.
        base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
            FROM_HERE,
            base::BindOnce(
                [](manta::MantaProtoResponseCallback delayed_callback) {
                  std::move(delayed_callback)
                      .Run(CreateMantaResponse(
                               SeaPenFetcher::kNumTemplateThumbnailsRequested),
                           {.status_code = manta::MantaStatusCode::kOk,
                            .message = std::string()});
                },
                std::move(done_callback)),
            SeaPenFetcher::kRequestTimeout + base::Seconds(1));
      });

  base::test::TestFuture<std::optional<std::vector<ash::SeaPenImage>>,
                         manta::MantaStatusCode>
      fetch_thumbnails_future;
  sea_pen_fetcher()->FetchThumbnails(
      manta::proto::FeatureName::CHROMEOS_WALLPAPER, MakeFreeformQuery(),
      fetch_thumbnails_future.GetCallback());

  // Trigger the timeout.
  FastForwardBy(SeaPenFetcher::kRequestTimeout + base::Milliseconds(1));

  EXPECT_EQ(manta::MantaStatusCode::kGenericError,
            fetch_thumbnails_future.Get<manta::MantaStatusCode>());
  EXPECT_EQ(std::nullopt,
            fetch_thumbnails_future
                .Get<std::optional<std::vector<ash::SeaPenImage>>>());

  // Recorded 1 timeout.
  histogram_tester().ExpectUniqueSample(kFreeformThumbnailsTimeoutMetric, true,
                                        1);

  // Does not record following metrics on timeout.
  histogram_tester().ExpectTotalCount(kFreeformThumbnailsLatencyMetric, 0);
  histogram_tester().ExpectTotalCount(kFreeformThumbnailsStatusCodeMetric, 0);
  histogram_tester().ExpectTotalCount(kFreeformThumbnailsCountMetric, 0);
}

TEST_F(SeaPenFetcherTest, ThumbnailsHandlesDuplicateRequests) {
  EXPECT_CALL(snapper_provider(), Call(testing::_, testing::_, testing::_))
      .WillRepeatedly([](const manta::proto::Request& request,
                         net::NetworkTrafficAnnotationTag traffic_annotation,
                         manta::MantaProtoResponseCallback done_callback) {
        base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
            FROM_HERE,
            base::BindOnce(
                [](manta::MantaProtoResponseCallback delayed_callback) {
                  std::move(delayed_callback)
                      .Run(CreateMantaResponse(
                               SeaPenFetcher::kNumTemplateThumbnailsRequested),
                           {.status_code = manta::MantaStatusCode::kOk,
                            .message = std::string()});
                },
                std::move(done_callback)),
            SeaPenFetcher::kRequestTimeout / 2);
      });

  std::vector<base::test::TestFuture<
      std::optional<std::vector<ash::SeaPenImage>>, manta::MantaStatusCode>>
      fetch_thumbnails_futures(2);

  sea_pen_fetcher()->FetchThumbnails(
      manta::proto::FeatureName::CHROMEOS_WALLPAPER, MakeFreeformQuery(),
      fetch_thumbnails_futures.at(0).GetCallback());

  sea_pen_fetcher()->FetchThumbnails(
      manta::proto::FeatureName::CHROMEOS_WALLPAPER, MakeFreeformQuery(),
      fetch_thumbnails_futures.at(1).GetCallback());

  // First call has already returned with null images.
  EXPECT_EQ(manta::MantaStatusCode::kOk,
            fetch_thumbnails_futures.at(0).Get<manta::MantaStatusCode>());
  EXPECT_EQ(std::nullopt,
            fetch_thumbnails_futures.at(0)
                .Get<std::optional<std::vector<ash::SeaPenImage>>>());

  EXPECT_FALSE(fetch_thumbnails_futures.at(1).IsReady());

  FastForwardBy(SeaPenFetcher::kRequestTimeout / 2 + base::Milliseconds(1));

  // Second call returns with valid thumbnails.
  EXPECT_TRUE(fetch_thumbnails_futures.at(1).IsReady());
  EXPECT_EQ(manta::MantaStatusCode::kOk,
            fetch_thumbnails_futures.at(1).Get<manta::MantaStatusCode>());
  EXPECT_EQ(SeaPenFetcher::kNumTemplateThumbnailsRequested,
            fetch_thumbnails_futures.at(1)
                .Get<std::optional<std::vector<ash::SeaPenImage>>>()
                ->size());
}

TEST_F(SeaPenFetcherTest, ThumbnailsDropsInvalidJpgBytes) {
  EXPECT_CALL(snapper_provider(), Call(testing::_, testing::_, testing::_))
      .WillOnce([](const manta::proto::Request& request,
                   net::NetworkTrafficAnnotationTag traffic_annotation,
                   manta::MantaProtoResponseCallback done_callback) {
        base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE,
            base::BindOnce(
                [](manta::MantaProtoResponseCallback inner_callback) {
                  auto response = std::make_unique<manta::proto::Response>();
                  {
                    // Invalid jpg bytes.
                    auto* output_data = response->add_output_data();
                    output_data->set_generation_seed(kFakeGenerationSeed + 1);
                    output_data->mutable_image()->set_serialized_bytes(
                        "not real jpg bytes");
                  }
                  {
                    // Valid jpg bytes.
                    auto* output_data = response->add_output_data();
                    output_data->set_generation_seed(kFakeGenerationSeed);
                    output_data->mutable_image()->set_serialized_bytes(
                        std::string(GetJpgBytes()));
                  }
                  std::move(inner_callback)
                      .Run(std::move(response),
                           {.status_code = manta::MantaStatusCode::kOk,
                            .message = std::string()});
                },
                std::move(done_callback)));
      });
  base::test::TestFuture<std::optional<std::vector<ash::SeaPenImage>>,
                         manta::MantaStatusCode>
      fetch_thumbnails_future;

  sea_pen_fetcher()->FetchThumbnails(
      manta::proto::FeatureName::CHROMEOS_WALLPAPER, MakeFreeformQuery(),
      fetch_thumbnails_future.GetCallback());

  EXPECT_EQ(manta::MantaStatusCode::kOk,
            fetch_thumbnails_future.Get<manta::MantaStatusCode>());
  // Only 1 image made it. The other was dropped due to invalid jpg bytes that
  // failed decoding.
  EXPECT_EQ(1u, fetch_thumbnails_future
                    .Get<std::optional<std::vector<ash::SeaPenImage>>>()
                    ->size());
}

TEST_F(SeaPenFetcherTest, WallpaperCallsSnapperProvider) {
  auto query = MakeTemplateQuery();

  EXPECT_CALL(snapper_provider(),
              Call(base::test::EqualsProto(CreateMantaRequest(
                       query, /*generation_seed=*/kFakeGenerationSeed,
                       /*num_outputs=*/1, GetLargestDisplaySizeLandscape(),
                       manta::proto::FeatureName::CHROMEOS_WALLPAPER)),
                   testing::_, testing::_))
      .WillOnce([](const manta::proto::Request& request,
                   net::NetworkTrafficAnnotationTag traffic_annotation,
                   manta::MantaProtoResponseCallback done_callback) {
        base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE,
            base::BindOnce(
                [](manta::MantaProtoResponseCallback delayed_callback) {
                  std::move(delayed_callback)
                      .Run(CreateMantaResponse(1),
                           {.status_code = manta::MantaStatusCode::kOk,
                            .message = std::string()});
                },
                std::move(done_callback)));
      });

  base::test::TestFuture<std::optional<ash::SeaPenImage>>
      fetch_wallpaper_future;
  sea_pen_fetcher()->FetchWallpaper(
      manta::proto::FeatureName::CHROMEOS_WALLPAPER,
      ash::SeaPenImage(std::string(GetJpgBytes()), kFakeGenerationSeed), query,
      fetch_wallpaper_future.GetCallback());

  EXPECT_THAT(fetch_wallpaper_future.Get().value(),
              testing::AllOf(
                  testing::Field(&ash::SeaPenImage::id, kFakeGenerationSeed),
                  testing::Field(&ash::SeaPenImage::jpg_bytes, GetJpgBytes())));

  histogram_tester().ExpectTotalCount(kWallpaperLatencyMetric, 1);
  histogram_tester().ExpectUniqueSample(kWallpaperStatusCodeMetric,
                                        manta::MantaStatusCode::kOk, 1);
  histogram_tester().ExpectUniqueSample(kWallpaperTimeoutMetric, false, 1);
  histogram_tester().ExpectUniqueSample(kWallpaperHasImageMetric, true, 1);
}

TEST_F(SeaPenFetcherTest, FreeformWallpaperCallsSnapperProvider) {
  auto query = MakeFreeformQuery();

  EXPECT_CALL(snapper_provider(),
              Call(base::test::EqualsProto(CreateMantaRequest(
                       query, /*generation_seed=*/kFakeGenerationSeed,
                       /*num_outputs=*/1, GetLargestDisplaySizeLandscape(),
                       manta::proto::FeatureName::CHROMEOS_WALLPAPER)),
                   testing::_, testing::_))
      .WillOnce([](const manta::proto::Request& request,
                   net::NetworkTrafficAnnotationTag traffic_annotation,
                   manta::MantaProtoResponseCallback done_callback) {
        base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE,
            base::BindOnce(
                [](manta::MantaProtoResponseCallback delayed_callback) {
                  std::move(delayed_callback)
                      .Run(CreateMantaResponse(1),
                           {.status_code = manta::MantaStatusCode::kOk,
                            .message = std::string()});
                },
                std::move(done_callback)));
      });

  base::test::TestFuture<std::optional<ash::SeaPenImage>>
      fetch_wallpaper_future;
  sea_pen_fetcher()->FetchWallpaper(
      manta::proto::FeatureName::CHROMEOS_WALLPAPER,
      ash::SeaPenImage(std::string(GetJpgBytes()), kFakeGenerationSeed), query,
      fetch_wallpaper_future.GetCallback());

  EXPECT_THAT(fetch_wallpaper_future.Get().value(),
              testing::AllOf(
                  testing::Field(&ash::SeaPenImage::id, kFakeGenerationSeed),
                  testing::Field(&ash::SeaPenImage::jpg_bytes, GetJpgBytes())));

  histogram_tester().ExpectTotalCount(kFreeformWallpaperLatencyMetric, 1);
  histogram_tester().ExpectUniqueSample(kFreeformWallpaperStatusCodeMetric,
                                        manta::MantaStatusCode::kOk, 1);
  histogram_tester().ExpectUniqueSample(kFreeformWallpaperTimeoutMetric, false,
                                        1);
  histogram_tester().ExpectUniqueSample(kFreeformWallpaperHasImageMetric, true,
                                        1);
}

TEST_F(SeaPenFetcherTest, WallpaperHandlesEmptyImage) {
  EXPECT_CALL(snapper_provider(), Call(testing::_, testing::_, testing::_))
      .WillOnce([](const manta::proto::Request& request,
                   net::NetworkTrafficAnnotationTag traffic_annotation,
                   manta::MantaProtoResponseCallback done_callback) {
        base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE,
            base::BindOnce(
                [](manta::MantaProtoResponseCallback delayed_callback) {
                  std::move(delayed_callback)
                      .Run(CreateMantaResponse(0),
                           {.status_code = manta::MantaStatusCode::kOk,
                            .message = std::string()});
                },
                std::move(done_callback)));
      });

  base::test::TestFuture<std::optional<ash::SeaPenImage>>
      fetch_wallpaper_future;
  sea_pen_fetcher()->FetchWallpaper(
      manta::proto::FeatureName::CHROMEOS_WALLPAPER,
      ash::SeaPenImage(std::string(GetJpgBytes()), kFakeGenerationSeed),
      MakeTemplateQuery(), fetch_wallpaper_future.GetCallback());

  EXPECT_FALSE(fetch_wallpaper_future.Get().has_value());

  histogram_tester().ExpectTotalCount(kWallpaperLatencyMetric, 1);
  histogram_tester().ExpectUniqueSample(kWallpaperStatusCodeMetric,
                                        manta::MantaStatusCode::kOk, 1);
  histogram_tester().ExpectUniqueSample(kWallpaperTimeoutMetric, false, 1);
  histogram_tester().ExpectUniqueSample(kWallpaperHasImageMetric, false, 1);
}

TEST_F(SeaPenFetcherTest, FreeformWallpaperHandlesEmptyImage) {
  EXPECT_CALL(snapper_provider(), Call(testing::_, testing::_, testing::_))
      .WillOnce([](const manta::proto::Request& request,
                   net::NetworkTrafficAnnotationTag traffic_annotation,
                   manta::MantaProtoResponseCallback done_callback) {
        base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE,
            base::BindOnce(
                [](manta::MantaProtoResponseCallback delayed_callback) {
                  std::move(delayed_callback)
                      .Run(CreateMantaResponse(0),
                           {.status_code = manta::MantaStatusCode::kOk,
                            .message = std::string()});
                },
                std::move(done_callback)));
      });

  base::test::TestFuture<std::optional<ash::SeaPenImage>>
      fetch_wallpaper_future;
  sea_pen_fetcher()->FetchWallpaper(
      manta::proto::FeatureName::CHROMEOS_WALLPAPER,
      ash::SeaPenImage(std::string(GetJpgBytes()), kFakeGenerationSeed),
      MakeFreeformQuery(), fetch_wallpaper_future.GetCallback());

  EXPECT_FALSE(fetch_wallpaper_future.Get().has_value());

  histogram_tester().ExpectTotalCount(kFreeformWallpaperLatencyMetric, 1);
  histogram_tester().ExpectUniqueSample(kFreeformWallpaperStatusCodeMetric,
                                        manta::MantaStatusCode::kOk, 1);
  histogram_tester().ExpectUniqueSample(kFreeformWallpaperTimeoutMetric, false,
                                        1);
  histogram_tester().ExpectUniqueSample(kFreeformWallpaperHasImageMetric, false,
                                        1);
}

TEST_F(SeaPenFetcherTest, WallpaperHandlesTimeout) {
  EXPECT_CALL(snapper_provider(), Call(testing::_, testing::_, testing::_))
      .WillOnce([](const manta::proto::Request& request,
                   net::NetworkTrafficAnnotationTag traffic_annotation,
                   manta::MantaProtoResponseCallback done_callback) {
        base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
            FROM_HERE,
            base::BindOnce(
                [](manta::MantaProtoResponseCallback delayed_callback) {
                  std::move(delayed_callback)
                      .Run(CreateMantaResponse(1),
                           {.status_code = manta::MantaStatusCode::kOk,
                            .message = std::string()});
                },
                std::move(done_callback)),
            SeaPenFetcher::kRequestTimeout + base::Seconds(1));
      });

  base::test::TestFuture<std::optional<ash::SeaPenImage>>
      fetch_wallpaper_future;
  sea_pen_fetcher()->FetchWallpaper(
      manta::proto::FeatureName::CHROMEOS_WALLPAPER,
      ash::SeaPenImage(std::string(GetJpgBytes()), kFakeGenerationSeed),
      MakeTemplateQuery(), fetch_wallpaper_future.GetCallback());

  FastForwardBy(SeaPenFetcher::kRequestTimeout + base::Milliseconds(1));

  EXPECT_FALSE(fetch_wallpaper_future.Get().has_value());

  // Timeout metric records true.
  histogram_tester().ExpectUniqueSample(kWallpaperTimeoutMetric, true, 1);

  // No other metrics recorded for timeout.
  histogram_tester().ExpectTotalCount(kWallpaperLatencyMetric, 0);
  histogram_tester().ExpectTotalCount(kWallpaperStatusCodeMetric, 0);
  histogram_tester().ExpectTotalCount(kWallpaperHasImageMetric, 0);
}

TEST_F(SeaPenFetcherTest, FreeformWallpaperHandlesTimeout) {
  EXPECT_CALL(snapper_provider(), Call(testing::_, testing::_, testing::_))
      .WillOnce([](const manta::proto::Request& request,
                   net::NetworkTrafficAnnotationTag traffic_annotation,
                   manta::MantaProtoResponseCallback done_callback) {
        base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
            FROM_HERE,
            base::BindOnce(
                [](manta::MantaProtoResponseCallback delayed_callback) {
                  std::move(delayed_callback)
                      .Run(CreateMantaResponse(1),
                           {.status_code = manta::MantaStatusCode::kOk,
                            .message = std::string()});
                },
                std::move(done_callback)),
            SeaPenFetcher::kRequestTimeout + base::Seconds(1));
      });

  base::test::TestFuture<std::optional<ash::SeaPenImage>>
      fetch_wallpaper_future;
  sea_pen_fetcher()->FetchWallpaper(
      manta::proto::FeatureName::CHROMEOS_WALLPAPER,
      ash::SeaPenImage(std::string(GetJpgBytes()), kFakeGenerationSeed),
      MakeFreeformQuery(), fetch_wallpaper_future.GetCallback());

  FastForwardBy(SeaPenFetcher::kRequestTimeout + base::Milliseconds(1));

  EXPECT_FALSE(fetch_wallpaper_future.Get().has_value());

  // Timeout metric records true.
  histogram_tester().ExpectUniqueSample(kFreeformWallpaperTimeoutMetric, true,
                                        1);

  // No other metrics recorded for timeout.
  histogram_tester().ExpectTotalCount(kFreeformWallpaperLatencyMetric, 0);
  histogram_tester().ExpectTotalCount(kFreeformWallpaperStatusCodeMetric, 0);
  histogram_tester().ExpectTotalCount(kFreeformWallpaperHasImageMetric, 0);
}
}  // namespace wallpaper_handlers