// Copyright 2019 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/image_fetcher/core/reduced_mode_image_fetcher.h"

#include <map>
#include <memory>
#include <string>
#include <utility>

#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/test/simple_test_clock.h"
#include "base/test/task_environment.h"
#include "components/image_fetcher/core/cache/image_cache.h"
#include "components/image_fetcher/core/cache/image_data_store_disk.h"
#include "components/image_fetcher/core/cache/image_metadata_store_leveldb.h"
#include "components/image_fetcher/core/cache/proto/cached_image_metadata.pb.h"
#include "components/image_fetcher/core/cached_image_fetcher.h"
#include "components/image_fetcher/core/fake_image_decoder.h"
#include "components/image_fetcher/core/image_fetcher_impl.h"
#include "components/image_fetcher/core/image_fetcher_metrics_reporter.h"
#include "components/image_fetcher/core/image_fetcher_types.h"
#include "components/leveldb_proto/testing/fake_db.h"
#include "components/prefs/testing_pref_service.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_unittest_util.h"

using leveldb_proto::test::FakeDB;
using testing::_;

namespace image_fetcher {

namespace {

constexpr char kUmaClientName[] = "TestUma";
constexpr char kImageData[] = "data";

const char kImageFetcherEventHistogramName[] = "ImageFetcher.Events";

}  // namespace

class ReducedModeImageFetcherTest : public testing::Test {
 public:
  ReducedModeImageFetcherTest() = default;

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

  ~ReducedModeImageFetcherTest() override {
    reduced_mode_image_fetcher_.reset();
    // We need to run until idle after deleting the database, because
    // ProtoDatabase deletes the actual LevelDB asynchronously.
    RunUntilIdle();
  }

  void SetUp() override {
    ASSERT_TRUE(data_dir_.CreateUniqueTempDir());
    ImageCache::RegisterProfilePrefs(test_prefs_.registry());
    CreateReducedModeImageFetcher();
  }

  void CreateReducedModeImageFetcher() {
    auto db =
        std::make_unique<FakeDB<CachedImageMetadataProto>>(&metadata_store_);
    db_ = db.get();

    auto metadata_store =
        std::make_unique<ImageMetadataStoreLevelDB>(std::move(db), &clock_);
    auto data_store = std::make_unique<ImageDataStoreDisk>(
        data_dir_.GetPath(), base::SequencedTaskRunner::GetCurrentDefault());

    image_cache_ = base::MakeRefCounted<ImageCache>(
        std::move(data_store), std::move(metadata_store), &test_prefs_, &clock_,
        base::SequencedTaskRunner::GetCurrentDefault());

    // Use an initial request to start the cache up.
    image_cache_->SaveImage(kImageUrl.spec(), kImageData,
                            /* needs_transcoding */ false,
                            /* expiration_interval */ std::nullopt);
    RunUntilIdle();
    db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK);
    image_cache_->DeleteImage(kImageUrl.spec());
    RunUntilIdle();

    shared_factory_ =
        base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
            &test_url_loader_factory_);

    auto decoder = std::make_unique<FakeImageDecoder>();
    fake_image_decoder_ = decoder.get();
    image_fetcher_ = std::make_unique<image_fetcher::ImageFetcherImpl>(
        std::move(decoder), shared_factory_);
    cached_image_fetcher_ = std::make_unique<CachedImageFetcher>(
        image_fetcher_.get(), image_cache_, /* read_only */ false);
    reduced_mode_image_fetcher_ =
        std::make_unique<ReducedModeImageFetcher>(cached_image_fetcher_.get());

    RunUntilIdle();
  }

  void RunUntilIdle() { task_environment_.RunUntilIdle(); }

  void PerformFetch(ImageDataFetcherCallback callback) {
    reduced_mode_image_fetcher()->FetchImageAndData(
        kImageUrl, std::move(callback), ImageFetcherCallback(),
        ImageFetcherParams(TRAFFIC_ANNOTATION_FOR_TESTS, kUmaClientName));
    db()->LoadCallback(true);
    RunUntilIdle();
  }

  void VerifyCacheHit() {
    RunUntilIdle();

    base::MockCallback<ImageDataFetcherCallback> data_callback;

    EXPECT_CALL(data_callback, Run(kImageData, _));
    reduced_mode_image_fetcher()->FetchImageAndData(
        kImageUrl, data_callback.Get(), ImageFetcherCallback(),
        ImageFetcherParams(TRAFFIC_ANNOTATION_FOR_TESTS, kUmaClientName));
    db()->LoadCallback(true);
    RunUntilIdle();

    histogram_tester().ExpectBucketCount(kImageFetcherEventHistogramName,
                                         ImageFetcherEvent::kImageRequest, 1);
    histogram_tester().ExpectBucketCount(kImageFetcherEventHistogramName,
                                         ImageFetcherEvent::kCacheHit, 1);
  }

  ReducedModeImageFetcher* reduced_mode_image_fetcher() {
    return reduced_mode_image_fetcher_.get();
  }
  scoped_refptr<ImageCache> image_cache() { return image_cache_; }
  network::TestURLLoaderFactory* test_url_loader_factory() {
    return &test_url_loader_factory_;
  }
  base::HistogramTester& histogram_tester() { return histogram_tester_; }
  FakeDB<CachedImageMetadataProto>* db() { return db_; }
  std::map<std::string, CachedImageMetadataProto>& metadata_store() {
    return metadata_store_;
  }

  MOCK_METHOD(void, OnImageLoaded, (bool, std::string), ());

 protected:
  GURL kImageUrl{"http://gstatic.img.com/foo.jpg"};

 private:
  std::unique_ptr<ImageFetcher> image_fetcher_;
  raw_ptr<FakeImageDecoder> fake_image_decoder_;
  std::unique_ptr<ImageFetcher> cached_image_fetcher_;
  std::unique_ptr<ReducedModeImageFetcher> reduced_mode_image_fetcher_;
  network::TestURLLoaderFactory test_url_loader_factory_;
  scoped_refptr<network::SharedURLLoaderFactory> shared_factory_;

  scoped_refptr<ImageCache> image_cache_;
  base::SimpleTestClock clock_;
  TestingPrefServiceSimple test_prefs_;
  base::ScopedTempDir data_dir_;
  raw_ptr<FakeDB<CachedImageMetadataProto>> db_;
  std::map<std::string, CachedImageMetadataProto> metadata_store_;

  base::test::TaskEnvironment task_environment_;
  base::HistogramTester histogram_tester_;
};

TEST_F(ReducedModeImageFetcherTest, FetchNeedsTranscodingImageFromCache) {
  // Save the image that needs transcoding in the database.
  image_cache()->SaveImage(kImageUrl.spec(), kImageData,
                           /* needs_transcoding */ true,
                           /* expiration_interval */ std::nullopt);
  VerifyCacheHit();
}

TEST_F(ReducedModeImageFetcherTest, FetchImageFromCache) {
  // Save the image that doesn't need transcoding in the database.
  image_cache()->SaveImage(kImageUrl.spec(), kImageData,
                           /* needs_transcoding */ false,
                           /* expiration_interval */ std::nullopt);
  VerifyCacheHit();
}

TEST_F(ReducedModeImageFetcherTest, FetchImageFromNetworkAndCacheReadBack) {
  std::string test_data = "raw_image_data";
  test_url_loader_factory()->AddResponse(kImageUrl.spec(), test_data);

  // Confirm cache is empty at first.
  ASSERT_EQ(metadata_store().size(), 0u);

  // Fetch image (should go to network because cache is empty).
  base::MockCallback<ImageDataFetcherCallback> data_callback1;
  // Verify that the fetched data matches the raw image data exactly.
  EXPECT_CALL(data_callback1, Run(test_data, _));
  PerformFetch(data_callback1.Get());

  // Confirm there is 1 element in the cache after fetch.
  EXPECT_EQ(metadata_store().size(), 1u);

  // Clear responses to ensure the next fetch doesn't hit the network.
  test_url_loader_factory()->ClearResponses();

  // Fetch image again (should hit cache).
  base::MockCallback<ImageDataFetcherCallback> data_callback2;
  // Verify that the cached data returned is identical to the original raw data.
  EXPECT_CALL(data_callback2, Run(test_data, _));
  PerformFetch(data_callback2.Get());

  // Verify that the cache was hit.
  histogram_tester().ExpectBucketCount(kImageFetcherEventHistogramName,
                                       ImageFetcherEvent::kCacheHit, 1);
}

}  // namespace image_fetcher
