// Copyright 2016 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/previews/content/previews_ui_service.h"

#include <memory>

#include "base/memory/ref_counted.h"
#include "base/run_loop.h"
#include "base/single_thread_task_runner.h"
#include "base/test/scoped_task_environment.h"
#include "base/time/default_clock.h"
#include "components/blacklist/opt_out_blacklist/opt_out_blacklist_data.h"
#include "components/previews/content/previews_decider_impl.h"
#include "components/previews/core/previews_black_list.h"
#include "components/previews/core/previews_experiments.h"
#include "components/previews/core/previews_logger.h"
#include "services/network/test/test_network_quality_tracker.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace previews {

namespace {

// Dummy method for creating TestPreviewsUIService.
bool MockedPreviewsIsEnabled(previews::PreviewsType type) {
  return true;
}

class TestPreviewsUIService : public PreviewsUIService {
 public:
  TestPreviewsUIService(
      std::unique_ptr<PreviewsDeciderImpl> previews_decider_impl,
      std::unique_ptr<blacklist::OptOutStore> previews_opt_out_store,
      std::unique_ptr<PreviewsOptimizationGuide> previews_opt_guide,
      std::unique_ptr<PreviewsLogger> logger,
      network::TestNetworkQualityTracker* test_network_quality_tracker)
      : PreviewsUIService(std::move(previews_decider_impl),
                          std::move(previews_opt_out_store),
                          std::move(previews_opt_guide),
                          base::BindRepeating(&MockedPreviewsIsEnabled),
                          std::move(logger),
                          blacklist::BlacklistData::AllowedTypesAndVersions(),
                          test_network_quality_tracker) {}
  ~TestPreviewsUIService() override {}
};

// Mock class of PreviewsLogger for checking passed in parameters.
class TestPreviewsLogger : public PreviewsLogger {
 public:
  TestPreviewsLogger()
      : decision_page_id_(0),
        navigation_opt_out_(false),
        user_blacklisted_(false),
        blacklist_ignored_(false) {}

  // PreviewsLogger:
  void LogPreviewNavigation(const GURL& url,
                            PreviewsType type,
                            bool opt_out,
                            base::Time time,
                            uint64_t page_id) override {
    navigation_url_ = url;
    navigation_opt_out_ = opt_out;
    navigation_type_ = type;
    navigation_time_ = base::Time(time);
    navigation_page_id_ = page_id;
  }

  void LogPreviewDecisionMade(
      PreviewsEligibilityReason reason,
      const GURL& url,
      base::Time time,
      PreviewsType type,
      std::vector<PreviewsEligibilityReason>&& passed_reasons,
      uint64_t page_id) override {
    decision_reason_ = reason;
    decision_url_ = GURL(url);
    decision_time_ = time;
    decision_type_ = type;
    decision_passed_reasons_ = std::move(passed_reasons);
    decision_page_id_ = page_id;
  }

  void OnNewBlacklistedHost(const std::string& host, base::Time time) override {
    host_blacklisted_ = host;
    host_blacklisted_time_ = time;
  }

  void OnUserBlacklistedStatusChange(bool blacklisted) override {
    user_blacklisted_ = blacklisted;
  }

  void OnBlacklistCleared(base::Time time) override {
    blacklist_cleared_time_ = time;
  }

  void OnIgnoreBlacklistDecisionStatusChanged(bool ignored) override {
    blacklist_ignored_ = ignored;
  }

  // Return the passed in LogPreviewDecision parameters.
  PreviewsEligibilityReason decision_reason() const { return decision_reason_; }
  GURL decision_url() const { return decision_url_; }
  PreviewsType decision_type() const { return decision_type_; }
  base::Time decision_time() const { return decision_time_; }
  const std::vector<PreviewsEligibilityReason>& decision_passed_reasons()
      const {
    return decision_passed_reasons_;
  }
  uint64_t decision_page_id() const { return decision_page_id_; }

  // Return the passed in LogPreviewNavigation parameters.
  GURL navigation_url() const { return navigation_url_; }
  bool navigation_opt_out() const { return navigation_opt_out_; }
  base::Time navigation_time() const { return navigation_time_; }
  PreviewsType navigation_type() const { return navigation_type_; }
  uint64_t navigation_page_id() const { return navigation_page_id_; }

  // Return the passed in OnBlacklist events.
  std::string host_blacklisted() const { return host_blacklisted_; }
  base::Time host_blacklisted_time() const { return host_blacklisted_time_; }
  bool user_blacklisted() const { return user_blacklisted_; }
  base::Time blacklist_cleared_time() const { return blacklist_cleared_time_; }

  // Return the status of blacklist ignored.
  bool blacklist_ignored() const { return blacklist_ignored_; }

 private:
  // Passed in LogPreviewDecision parameters.
  PreviewsEligibilityReason decision_reason_;
  GURL decision_url_;
  PreviewsType decision_type_;
  base::Time decision_time_;
  std::vector<PreviewsEligibilityReason> decision_passed_reasons_;
  uint64_t decision_page_id_;

  // Passed in LogPreviewsNavigation parameters.
  GURL navigation_url_;
  bool navigation_opt_out_;
  base::Time navigation_time_;
  PreviewsType navigation_type_;
  uint64_t navigation_page_id_;

  // Passed in OnBlacklist events.
  std::string host_blacklisted_;
  base::Time host_blacklisted_time_;
  bool user_blacklisted_;
  base::Time blacklist_cleared_time_;

  // Passed in blacklist ignored status.
  bool blacklist_ignored_;
};

class TestPreviewsDeciderImpl : public PreviewsDeciderImpl {
 public:
  TestPreviewsDeciderImpl()
      : PreviewsDeciderImpl(base::DefaultClock::GetInstance()),
        blacklist_ignored_(false) {}

  // PreviewsDeciderImpl:
  void SetIgnorePreviewsBlacklistDecision(bool ignored) override {
    blacklist_ignored_ = ignored;
  }
  bool GetResourceLoadingHints(
      const GURL& url,
      std::vector<std::string>* out_resource_patterns_to_block) const override {
    if (url.host() == "blockresources.com") {
      out_resource_patterns_to_block->push_back("BlockMe");
      return true;
    }
    return false;
  }

  // Exposed the status of blacklist decisions ignored for testing
  // PreviewsUIService.
  bool blacklist_ignored() const { return blacklist_ignored_; }

 private:
  // Whether the blacklist decisions are ignored or not.
  bool blacklist_ignored_;
};

class PreviewsUIServiceTest : public testing::Test {
 public:
  PreviewsUIServiceTest() {}

  ~PreviewsUIServiceTest() override {}

  void SetUp() override {
    std::unique_ptr<TestPreviewsLogger> logger =
        std::make_unique<TestPreviewsLogger>();

    // Use to testing logger data.
    logger_ptr_ = logger.get();

    std::unique_ptr<TestPreviewsDeciderImpl> previews_decider_impl =
        std::make_unique<TestPreviewsDeciderImpl>();
    previews_decider_impl_ = previews_decider_impl.get();

    ui_service_ = std::make_unique<TestPreviewsUIService>(
        std::move(previews_decider_impl), nullptr /* previews_opt_out_store */,
        nullptr /* previews_opt_guide */, std::move(logger),
        &test_network_quality_tracker_);
  }

  TestPreviewsDeciderImpl* previews_decider_impl() {
    return previews_decider_impl_;
  }

  TestPreviewsUIService* ui_service() { return ui_service_.get(); }

 protected:
  // Run this test on a single thread.
  base::test::ScopedTaskEnvironment task_environment_;
  TestPreviewsLogger* logger_ptr_;
  network::TestNetworkQualityTracker test_network_quality_tracker_;

 private:
  TestPreviewsDeciderImpl* previews_decider_impl_;
  std::unique_ptr<TestPreviewsUIService> ui_service_;
};

}  // namespace

TEST_F(PreviewsUIServiceTest, TestInitialization) {
  // After the outstanding posted tasks have run, SetIOData should have been
  // called for |ui_service_|.
  EXPECT_TRUE(ui_service()->previews_decider_impl());
}

TEST_F(PreviewsUIServiceTest, TestLogPreviewNavigationPassInCorrectParams) {
  const GURL url_a = GURL("http://www.url_a.com/url_a");
  const PreviewsType type_a = PreviewsType::LOFI;
  const bool opt_out_a = true;
  const base::Time time_a = base::Time::Now();
  const uint64_t page_id_a = 1234;

  ui_service()->LogPreviewNavigation(url_a, type_a, opt_out_a, time_a,
                                     page_id_a);

  EXPECT_EQ(url_a, logger_ptr_->navigation_url());
  EXPECT_EQ(type_a, logger_ptr_->navigation_type());
  EXPECT_EQ(opt_out_a, logger_ptr_->navigation_opt_out());
  EXPECT_EQ(time_a, logger_ptr_->navigation_time());
  EXPECT_EQ(page_id_a, logger_ptr_->navigation_page_id());

  const GURL url_b = GURL("http://www.url_b.com/url_b");
  const PreviewsType type_b = PreviewsType::OFFLINE;
  const bool opt_out_b = false;
  const base::Time time_b = base::Time::Now();
  const uint64_t page_id_b = 4321;

  ui_service()->LogPreviewNavigation(url_b, type_b, opt_out_b, time_b,
                                     page_id_b);

  EXPECT_EQ(url_b, logger_ptr_->navigation_url());
  EXPECT_EQ(type_b, logger_ptr_->navigation_type());
  EXPECT_EQ(opt_out_b, logger_ptr_->navigation_opt_out());
  EXPECT_EQ(time_b, logger_ptr_->navigation_time());
  EXPECT_EQ(page_id_b, logger_ptr_->navigation_page_id());
}

TEST_F(PreviewsUIServiceTest, TestLogPreviewDecisionMadePassesCorrectParams) {
  PreviewsEligibilityReason reason_a =
      PreviewsEligibilityReason::BLACKLIST_UNAVAILABLE;
  const GURL url_a("http://www.url_a.com/url_a");
  const base::Time time_a = base::Time::Now();
  PreviewsType type_a = PreviewsType::OFFLINE;
  std::vector<PreviewsEligibilityReason> passed_reasons_a = {
      PreviewsEligibilityReason::NETWORK_NOT_SLOW,
      PreviewsEligibilityReason::USER_RECENTLY_OPTED_OUT,
      PreviewsEligibilityReason::RELOAD_DISALLOWED,
  };
  const std::vector<PreviewsEligibilityReason> expected_passed_reasons_a(
      passed_reasons_a);
  const uint64_t page_id_a = 1234;

  ui_service()->LogPreviewDecisionMade(reason_a, url_a, time_a, type_a,
                                       std::move(passed_reasons_a), page_id_a);

  EXPECT_EQ(reason_a, logger_ptr_->decision_reason());
  EXPECT_EQ(url_a, logger_ptr_->decision_url());
  EXPECT_EQ(time_a, logger_ptr_->decision_time());
  EXPECT_EQ(type_a, logger_ptr_->decision_type());
  EXPECT_EQ(expected_passed_reasons_a, logger_ptr_->decision_passed_reasons());
  EXPECT_EQ(page_id_a, logger_ptr_->decision_page_id());

  auto actual_passed_reasons_a = logger_ptr_->decision_passed_reasons();
  EXPECT_EQ(3UL, actual_passed_reasons_a.size());
  for (size_t i = 0; i < actual_passed_reasons_a.size(); i++) {
    EXPECT_EQ(expected_passed_reasons_a[i], actual_passed_reasons_a[i]);
  }

  PreviewsEligibilityReason reason_b =
      PreviewsEligibilityReason::NETWORK_NOT_SLOW;
  const GURL url_b("http://www.url_b.com/url_b");
  const base::Time time_b = base::Time::Now();
  PreviewsType type_b = PreviewsType::LOFI;
  std::vector<PreviewsEligibilityReason> passed_reasons_b = {
      PreviewsEligibilityReason::HOST_NOT_WHITELISTED_BY_SERVER,
      PreviewsEligibilityReason::NETWORK_QUALITY_UNAVAILABLE,
  };
  const std::vector<PreviewsEligibilityReason> expected_passed_reasons_b(
      passed_reasons_b);
  const uint64_t page_id_b = 4321;

  ui_service()->LogPreviewDecisionMade(reason_b, url_b, time_b, type_b,
                                       std::move(passed_reasons_b), page_id_b);

  EXPECT_EQ(reason_b, logger_ptr_->decision_reason());
  EXPECT_EQ(url_b, logger_ptr_->decision_url());
  EXPECT_EQ(type_b, logger_ptr_->decision_type());
  EXPECT_EQ(time_b, logger_ptr_->decision_time());
  EXPECT_EQ(page_id_b, logger_ptr_->decision_page_id());

  auto actual_passed_reasons_b = logger_ptr_->decision_passed_reasons();
  EXPECT_EQ(2UL, actual_passed_reasons_b.size());
  for (size_t i = 0; i < actual_passed_reasons_b.size(); i++) {
    EXPECT_EQ(expected_passed_reasons_b[i], actual_passed_reasons_b[i]);
  }
}

TEST_F(PreviewsUIServiceTest, TestOnNewBlacklistedHostPassesCorrectParams) {
  const std::string expected_host = "example.com";
  const base::Time expected_time = base::Time::Now();
  ui_service()->OnNewBlacklistedHost(expected_host, expected_time);

  EXPECT_EQ(expected_host, logger_ptr_->host_blacklisted());
  EXPECT_EQ(expected_time, logger_ptr_->host_blacklisted_time());
}

TEST_F(PreviewsUIServiceTest, TestOnUserBlacklistedPassesCorrectParams) {
  ui_service()->OnUserBlacklistedStatusChange(true /* blacklisted */);
  EXPECT_TRUE(logger_ptr_->user_blacklisted());

  ui_service()->OnUserBlacklistedStatusChange(false /* blacklisted */);
  EXPECT_FALSE(logger_ptr_->user_blacklisted());
}

TEST_F(PreviewsUIServiceTest, TestOnBlacklistClearedPassesCorrectParams) {
  const base::Time expected_time = base::Time::Now();
  ui_service()->OnBlacklistCleared(expected_time);

  EXPECT_EQ(expected_time, logger_ptr_->blacklist_cleared_time());
}

TEST_F(PreviewsUIServiceTest,
       TestSetIgnorePreviewsBlacklistDecisionPassesCorrectParams) {
  ui_service()->SetIgnorePreviewsBlacklistDecision(true /* ignored */);
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(previews_decider_impl()->blacklist_ignored());

  ui_service()->SetIgnorePreviewsBlacklistDecision(false /* ignored */);
  base::RunLoop().RunUntilIdle();
  EXPECT_FALSE(previews_decider_impl()->blacklist_ignored());
}

TEST_F(PreviewsUIServiceTest, TestOnIgnoreBlacklistDecisionStatusChanged) {
  ui_service()->OnIgnoreBlacklistDecisionStatusChanged(true /* ignored */);
  EXPECT_TRUE(logger_ptr_->blacklist_ignored());

  ui_service()->OnIgnoreBlacklistDecisionStatusChanged(false /* ignored */);
  EXPECT_FALSE(logger_ptr_->blacklist_ignored());
}

TEST_F(PreviewsUIServiceTest,
       TestGetResourceLoadingHintsResourcePatternsToBlock) {
  EXPECT_TRUE(ui_service()
                  ->GetResourceLoadingHintsResourcePatternsToBlock(
                      GURL("https://www.somedomain.org/"))
                  .empty());

  std::vector<std::string> patterns_to_block =
      ui_service()->GetResourceLoadingHintsResourcePatternsToBlock(
          GURL("https://blockresources.com/"));
  EXPECT_EQ(1ul, patterns_to_block.size());
  EXPECT_EQ("BlockMe", patterns_to_block[0]);
}

}  // namespace previews
