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

#include "components/ntp_snippets/remote/cached_image_fetcher.h"

#include <memory>

#include "base/bind.h"
#include "base/callback.h"
#include "base/files/file_path.h"
#include "base/files/scoped_temp_dir.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_task_environment.h"
#include "components/image_fetcher/core/image_decoder.h"
#include "components/image_fetcher/core/image_fetcher.h"
#include "components/image_fetcher/core/image_fetcher_impl.h"
#include "components/ntp_snippets/remote/proto/ntp_snippets.pb.h"
#include "components/ntp_snippets/remote/remote_suggestions_database.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/testing_pref_service.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 testing::_;
using testing::Eq;
using testing::Property;

namespace ntp_snippets {

namespace {

const char kImageData[] = "data";
const char kImageURL[] = "http://image.test/test.png";
const char kSnippetID[] = "http://localhost";
const ContentSuggestion::ID kSuggestionID(
    Category::FromKnownCategory(KnownCategories::ARTICLES),
    kSnippetID);

// Always decodes a valid image for all non-empty input.
class FakeImageDecoder : public image_fetcher::ImageDecoder {
 public:
  void DecodeImage(
      const std::string& image_data,
      const gfx::Size& desired_image_frame_size,
      const image_fetcher::ImageDecodedCallback& callback) override {
    ASSERT_TRUE(enabled_);
    gfx::Image image;
    if (!image_data.empty()) {
      ASSERT_EQ(kImageData, image_data);
      image = gfx::test::CreateImage();
    }
    base::SequencedTaskRunnerHandle::Get()->PostTask(
        FROM_HERE, base::BindRepeating(callback, image));
  }
  void SetEnabled(bool enabled) { enabled_ = enabled; }

 private:
  bool enabled_ = true;
};

enum class TestType {
  kImageCallback,
  kImageDataCallback,
  kBothCallbacks,
};
}  // namespace

// This test is parameterized to run all tests in the three configurations:
// both callbacks used, only image_callback used, only image_data_callback used.
class CachedImageFetcherTest : public testing::TestWithParam<TestType> {
 public:
  CachedImageFetcherTest() {
    EXPECT_TRUE(database_dir_.CreateUniqueTempDir());

    RequestThrottler::RegisterProfilePrefs(pref_service_.registry());
    database_ =
        std::make_unique<RemoteSuggestionsDatabase>(database_dir_.GetPath());

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

    auto decoder = std::make_unique<FakeImageDecoder>();
    fake_image_decoder_ = decoder.get();
    cached_image_fetcher_ = std::make_unique<ntp_snippets::CachedImageFetcher>(
        std::make_unique<image_fetcher::ImageFetcherImpl>(std::move(decoder),
                                                          shared_factory_),
        &pref_service_, database_.get());
    RunUntilIdle();
    EXPECT_TRUE(database_->IsInitialized());
  }

  ~CachedImageFetcherTest() override {
    cached_image_fetcher_.reset();
    database_.reset();
    // We need to run until idle after deleting the database, because
    // ProtoDatabaseImpl deletes the actual LevelDB asynchronously.
    RunUntilIdle();
  }

  void Fetch(std::string expected_image_data, bool expect_image) {
    fake_image_decoder()->SetEnabled(GetParam() !=
                                     TestType::kImageDataCallback);
    base::MockCallback<ImageFetchedCallback> image_callback;
    base::MockCallback<ImageDataFetchedCallback> image_data_callback;
    switch (GetParam()) {
      case TestType::kImageCallback: {
        EXPECT_CALL(image_callback,
                    Run(Property(&gfx::Image::IsEmpty, Eq(!expect_image))));
        cached_image_fetcher()->FetchSuggestionImage(
            kSuggestionID, GURL(kImageURL), ImageDataFetchedCallback(),
            image_callback.Get());

      } break;
      case TestType::kImageDataCallback: {
        EXPECT_CALL(image_data_callback, Run(expected_image_data));
        cached_image_fetcher()->FetchSuggestionImage(
            kSuggestionID, GURL(kImageURL), image_data_callback.Get(),
            ImageFetchedCallback());
      } break;
      case TestType::kBothCallbacks: {
        EXPECT_CALL(image_data_callback, Run(expected_image_data));
        EXPECT_CALL(image_callback,
                    Run(Property(&gfx::Image::IsEmpty, Eq(!expect_image))));
        cached_image_fetcher()->FetchSuggestionImage(
            kSuggestionID, GURL(kImageURL), image_data_callback.Get(),
            image_callback.Get());
      } break;
    }
    RunUntilIdle();
  }

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

  RemoteSuggestionsDatabase* database() { return database_.get(); }
  FakeImageDecoder* fake_image_decoder() { return fake_image_decoder_; }
  network::TestURLLoaderFactory* test_url_loader_factory() {
    return &test_url_loader_factory_;
  }
  CachedImageFetcher* cached_image_fetcher() {
    return cached_image_fetcher_.get();
  }

 private:
  network::TestURLLoaderFactory test_url_loader_factory_;
  scoped_refptr<network::SharedURLLoaderFactory> shared_factory_;
  std::unique_ptr<CachedImageFetcher> cached_image_fetcher_;
  std::unique_ptr<RemoteSuggestionsDatabase> database_;
  base::ScopedTempDir database_dir_;
  FakeImageDecoder* fake_image_decoder_;

  TestingPrefServiceSimple pref_service_;
  base::test::ScopedTaskEnvironment scoped_task_environment_;

  DISALLOW_COPY_AND_ASSIGN(CachedImageFetcherTest);
};

TEST_P(CachedImageFetcherTest, FetchImageFromCache) {
  // Save the image in the database.
  database()->SaveImage(kSnippetID, kImageData);
  RunUntilIdle();

  // Do not provide any URL responses and expect that the image is fetched (from
  // cache).
  Fetch(kImageData, true);
}

TEST_P(CachedImageFetcherTest, FetchImagePopulatesCache) {
  // Expect the image to be fetched by URL.
  {
    test_url_loader_factory()->AddResponse(kImageURL, kImageData);
    Fetch(kImageData, true);
  }
  // Fetch again. The cache should be populated, no network request is needed.
  {
    test_url_loader_factory()->ClearResponses();
    Fetch(kImageData, true);
  }
}

TEST_P(CachedImageFetcherTest, FetchNonExistingImage) {
  const std::string kErrorResponse = "error-response";
  test_url_loader_factory()->AddResponse(kImageURL, kErrorResponse,
                                         net::HTTP_NOT_FOUND);
  // Expect an empty image is fetched if the URL cannot be requested.
  Fetch("", false);
}

INSTANTIATE_TEST_CASE_P(,
                        CachedImageFetcherTest,
                        testing::Values(TestType::kImageCallback,
                                        TestType::kImageDataCallback,
                                        TestType::kBothCallbacks));

}  // namespace ntp_snippets
