// 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 "chrome/browser/permissions/crowd_deny_preload_data.h"

#include <string_view>

#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/numerics/safe_conversions.h"
#include "base/test/task_environment.h"
#include "base/version.h"
#include "chrome/browser/permissions/crowd_deny.pb.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace {

constexpr char kTestDomainAlpha[] = "alpha.com";
constexpr char kTestDomainBeta[] = "beta.com";
constexpr char kTestDomainGamma[] = "gamma.com";
constexpr char kTestDomainDelta[] = "delta.com";
constexpr char kTestSubdomainOfDelta1[] = "one.delta.com";
constexpr char kTestDomainEpsilon[] = "epsilon.com";

constexpr char kTestOriginAlpha[] = "https://alpha.com";
constexpr char kTestOriginSubdomainOfAlpha[] = "https://foo.alpha.com";
constexpr char kTestOriginBeta[] = "https://beta.com";
constexpr char kTestOriginGamma[] = "https://gamma.com";
constexpr char kTestOriginDelta[] = "https://delta.com";
constexpr char kTestOriginSubdomainOfDelta1[] = "https://one.delta.com";
constexpr char kTestOriginSubdomainOfDelta2[] = "https://foo.two.delta.com";
constexpr char kTestOriginNotSubdomainOfDelta[] = "https://notdelta.com";
constexpr char kTestOriginEpsilon[] = "https://epsilon.com";
constexpr char kTestOriginZeta[] = "https://zeta.com";

constexpr const char* kAllTestingOrigins[] = {
    kTestOriginAlpha, kTestOriginBeta, kTestOriginGamma, kTestOriginDelta,
    kTestOriginEpsilon};

}  // namespace

class CrowdDenyPreloadDataTest : public testing::Test {
 public:
  using SiteReputation = chrome_browser_crowd_deny::SiteReputation;

  CrowdDenyPreloadDataTest() = default;

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

  ~CrowdDenyPreloadDataTest() override = default;

 protected:
  void SetUp() override { ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir()); }

  base::test::TaskEnvironment* task_environment() { return &task_environment_; }
  CrowdDenyPreloadData* preload_data() { return &preload_data_; }

  base::FilePath GetPathInTempDir(
      base::FilePath::StringViewType filename) const {
    return scoped_temp_dir_.GetPath().Append(filename);
  }

  void SerializeTestRawData(std::string_view raw_data, base::FilePath path) {
    ASSERT_TRUE(base::WriteFile(path, raw_data));
  }

  void SerializeTestPreloadData(
      chrome_browser_crowd_deny::PreloadData preload_data,
      base::FilePath path) {
    std::string binary_preload_data;
    ASSERT_TRUE(preload_data.SerializeToString(&binary_preload_data));
    ASSERT_NO_FATAL_FAILURE(SerializeTestRawData(binary_preload_data, path));
  }

  void LoadTestDataAndWait(base::FilePath path) {
    preload_data()->LoadFromDisk(path, base::Version());
    task_environment()->RunUntilIdle();
  }

  void SerializeAndLoadTestData(
      chrome_browser_crowd_deny::PreloadData test_data) {
    const base::FilePath temp_path =
        GetPathInTempDir(FILE_PATH_LITERAL("Preload Data"));
    ASSERT_NO_FATAL_FAILURE(
        SerializeTestPreloadData(std::move(test_data), temp_path));
    LoadTestDataAndWait(temp_path);
  }

  void SerializeAndLoadCannedTestData() {
    chrome_browser_crowd_deny::PreloadData test_data;

    auto* alpha_site_reputation = test_data.add_site_reputations();
    alpha_site_reputation->set_domain(kTestDomainAlpha);
    alpha_site_reputation->set_notification_ux_quality(SiteReputation::UNKNOWN);

    auto* beta_site_reputation = test_data.add_site_reputations();
    beta_site_reputation->set_domain(kTestDomainBeta);
    beta_site_reputation->set_include_subdomains(false);
    beta_site_reputation->set_notification_ux_quality(
        SiteReputation::ACCEPTABLE);

    auto* gamma_site_reputation = test_data.add_site_reputations();
    gamma_site_reputation->set_domain(kTestDomainGamma);
    gamma_site_reputation->set_warning_only(false);
    gamma_site_reputation->set_notification_ux_quality(
        SiteReputation::UNSOLICITED_PROMPTS);

    auto* delta_site_reputation = test_data.add_site_reputations();
    delta_site_reputation->set_domain(kTestDomainDelta);
    delta_site_reputation->set_include_subdomains(true);
    delta_site_reputation->set_warning_only(true);
    delta_site_reputation->set_notification_ux_quality(
        SiteReputation::ABUSIVE_PROMPTS);

    auto* epsilon_site_reputation = test_data.add_site_reputations();
    epsilon_site_reputation->set_domain(kTestDomainEpsilon);
    // No |notification_ux_quality| field.
    // No |include_subdomains| field.
    // No |warning_only| field.

    ASSERT_NO_FATAL_FAILURE(SerializeAndLoadTestData(std::move(test_data)));
  }

  void ExpectEmptyPreloadData() {
    for (const char* origin_string : kAllTestingOrigins) {
      SCOPED_TRACE(origin_string);
      EXPECT_FALSE(preload_data()->GetReputationDataForSite(
          url::Origin::Create(GURL(origin_string))));
    }
  }

  const SiteReputation* GetReputationDataForSite(const url::Origin& origin) {
    return preload_data()->GetReputationDataForSite(origin);
  }

 private:
  base::test::TaskEnvironment task_environment_;
  base::ScopedTempDir scoped_temp_dir_;
  CrowdDenyPreloadData preload_data_;
};

TEST_F(CrowdDenyPreloadDataTest, NoData) {
  ExpectEmptyPreloadData();
}

TEST_F(CrowdDenyPreloadDataTest, MissingFile) {
  LoadTestDataAndWait(GetPathInTempDir(FILE_PATH_LITERAL("NonExistentFile")));
  ExpectEmptyPreloadData();
}

TEST_F(CrowdDenyPreloadDataTest, EmptyData) {
  const base::FilePath empty_file_path =
      GetPathInTempDir(FILE_PATH_LITERAL("EmptyFile"));
  SerializeTestRawData(std::string_view(), empty_file_path);
  LoadTestDataAndWait(empty_file_path);
  ExpectEmptyPreloadData();
}

TEST_F(CrowdDenyPreloadDataTest, BadData) {
  const base::FilePath bad_data_path =
      GetPathInTempDir(FILE_PATH_LITERAL("BadFile"));
  SerializeTestRawData("This is not a proto.", bad_data_path);
  LoadTestDataAndWait(bad_data_path);
  ExpectEmptyPreloadData();
}

TEST_F(CrowdDenyPreloadDataTest, DataIntegrityAndDefaults) {
  ASSERT_NO_FATAL_FAILURE(SerializeAndLoadCannedTestData());

  const auto* data =
      GetReputationDataForSite(url::Origin::Create(GURL(kTestOriginAlpha)));
  ASSERT_TRUE(data);
  EXPECT_EQ(kTestDomainAlpha, data->domain());
  EXPECT_EQ(SiteReputation::UNKNOWN, data->notification_ux_quality());
  EXPECT_FALSE(data->include_subdomains());
  EXPECT_FALSE(data->warning_only());

  data = GetReputationDataForSite(url::Origin::Create(GURL(kTestOriginBeta)));
  ASSERT_TRUE(data);
  EXPECT_EQ(kTestDomainBeta, data->domain());
  EXPECT_EQ(SiteReputation::ACCEPTABLE, data->notification_ux_quality());
  EXPECT_FALSE(data->include_subdomains());
  EXPECT_FALSE(data->warning_only());

  data = GetReputationDataForSite(url::Origin::Create(GURL(kTestOriginGamma)));
  ASSERT_TRUE(data);
  EXPECT_EQ(kTestDomainGamma, data->domain());
  EXPECT_EQ(SiteReputation::UNSOLICITED_PROMPTS,
            data->notification_ux_quality());
  EXPECT_FALSE(data->include_subdomains());
  EXPECT_FALSE(data->warning_only());

  data = GetReputationDataForSite(url::Origin::Create(GURL(kTestOriginDelta)));
  ASSERT_TRUE(data);
  EXPECT_EQ(kTestDomainDelta, data->domain());
  EXPECT_EQ(SiteReputation::ABUSIVE_PROMPTS, data->notification_ux_quality());
  EXPECT_TRUE(data->include_subdomains());
  EXPECT_TRUE(data->warning_only());

  data =
      GetReputationDataForSite(url::Origin::Create(GURL(kTestOriginEpsilon)));
  ASSERT_TRUE(data);
  EXPECT_EQ(kTestDomainEpsilon, data->domain());
  EXPECT_EQ(SiteReputation::UNKNOWN, data->notification_ux_quality());
  EXPECT_FALSE(data->include_subdomains());
  EXPECT_FALSE(data->warning_only());

  data = GetReputationDataForSite(url::Origin::Create(GURL(kTestOriginZeta)));
  EXPECT_FALSE(data);
}

TEST_F(CrowdDenyPreloadDataTest, GetReputationReturnsNullForNonHttpsOrigins) {
  const char* kNonHttpsOrigins[] = {
      "http://alpha.com",
      "wss://alpha.com",
      "ftp://alpha.com",
      "file:///alpha.com",
  };

  ASSERT_NO_FATAL_FAILURE(SerializeAndLoadCannedTestData());
  EXPECT_TRUE(
      GetReputationDataForSite(url::Origin::Create(GURL(kTestOriginAlpha))));

  for (const char* non_https_origin : kNonHttpsOrigins) {
    SCOPED_TRACE(non_https_origin);
    EXPECT_FALSE(
        GetReputationDataForSite(url::Origin::Create(GURL(non_https_origin))));
  }
}

TEST_F(CrowdDenyPreloadDataTest, GetReputationIgnoresPort) {
  ASSERT_NO_FATAL_FAILURE(SerializeAndLoadCannedTestData());

  EXPECT_TRUE(GetReputationDataForSite(
      url::Origin::Create(GURL("https://alpha.com:443"))));
  EXPECT_TRUE(GetReputationDataForSite(
      url::Origin::Create(GURL("https://alpha.com:1234"))));
}

TEST_F(CrowdDenyPreloadDataTest, GetReputationWithSubdomainMatching) {
  ASSERT_NO_FATAL_FAILURE(SerializeAndLoadCannedTestData());

  const auto* data =
      GetReputationDataForSite(url::Origin::Create(GURL(kTestOriginDelta)));
  ASSERT_TRUE(data);
  EXPECT_EQ(kTestDomainDelta, data->domain());
  ASSERT_TRUE(data->include_subdomains());

  data = GetReputationDataForSite(
      url::Origin::Create(GURL(kTestOriginSubdomainOfDelta1)));
  ASSERT_TRUE(data);
  EXPECT_EQ(kTestDomainDelta, data->domain());

  data = GetReputationDataForSite(
      url::Origin::Create(GURL(kTestOriginSubdomainOfDelta2)));
  ASSERT_TRUE(data);
  EXPECT_EQ(kTestDomainDelta, data->domain());

  data = GetReputationDataForSite(
      url::Origin::Create(GURL(kTestOriginNotSubdomainOfDelta)));
  EXPECT_FALSE(data);

  data = GetReputationDataForSite(url::Origin::Create(GURL(kTestOriginAlpha)));
  ASSERT_TRUE(data);
  EXPECT_EQ(kTestDomainAlpha, data->domain());
  ASSERT_FALSE(data->include_subdomains());

  // Should not return `alpha.com` because |include_subdomains| is not set.
  data = GetReputationDataForSite(
      url::Origin::Create(GURL(kTestOriginSubdomainOfAlpha)));
  EXPECT_FALSE(data);
}

TEST_F(CrowdDenyPreloadDataTest, SubdomainSpecificOverride) {
  chrome_browser_crowd_deny::PreloadData test_data;

  auto* delta_site_reputation = test_data.add_site_reputations();
  delta_site_reputation->set_domain(kTestDomainDelta);
  delta_site_reputation->set_include_subdomains(true);
  delta_site_reputation->set_notification_ux_quality(
      SiteReputation::UNSOLICITED_PROMPTS);

  auto* subdomain_site_reputation = test_data.add_site_reputations();
  subdomain_site_reputation->set_domain(kTestSubdomainOfDelta1);
  subdomain_site_reputation->set_include_subdomains(true);
  subdomain_site_reputation->set_notification_ux_quality(
      SiteReputation::ACCEPTABLE);

  ASSERT_NO_FATAL_FAILURE(SerializeAndLoadTestData(std::move(test_data)));

  const auto* data = GetReputationDataForSite(
      url::Origin::Create(GURL(kTestOriginSubdomainOfDelta1)));
  ASSERT_TRUE(data);
  EXPECT_EQ(kTestSubdomainOfDelta1, data->domain());
  EXPECT_EQ(SiteReputation::ACCEPTABLE, data->notification_ux_quality());

  data = GetReputationDataForSite(
      url::Origin::Create(GURL(kTestOriginSubdomainOfDelta2)));
  ASSERT_TRUE(data);
  EXPECT_EQ(kTestDomainDelta, data->domain());
  EXPECT_EQ(SiteReputation::UNSOLICITED_PROMPTS,
            data->notification_ux_quality());
}

TEST_F(CrowdDenyPreloadDataTest, Update) {
  ASSERT_NO_FATAL_FAILURE(SerializeAndLoadCannedTestData());

  // Prepare and load an updated payload version, which updates the entry for
  // `delta.com`, adds an entry for `epsilon.com`, and removes all others.
  chrome_browser_crowd_deny::PreloadData test_data_v2;

  auto* delta_site_reputation = test_data_v2.add_site_reputations();
  delta_site_reputation->set_domain(kTestDomainDelta);
  delta_site_reputation->set_notification_ux_quality(
      SiteReputation::UNSOLICITED_PROMPTS);

  auto* epsilon_site_reputation = test_data_v2.add_site_reputations();
  epsilon_site_reputation->set_domain(kTestDomainEpsilon);
  epsilon_site_reputation->set_notification_ux_quality(
      SiteReputation::ACCEPTABLE);

  ASSERT_NO_FATAL_FAILURE(SerializeAndLoadTestData(std::move(test_data_v2)));

  // Check that the updated preload data is visible.
  EXPECT_FALSE(
      GetReputationDataForSite(url::Origin::Create(GURL(kTestOriginAlpha))));
  EXPECT_FALSE(
      GetReputationDataForSite(url::Origin::Create(GURL(kTestOriginBeta))));
  EXPECT_FALSE(
      GetReputationDataForSite(url::Origin::Create(GURL(kTestOriginGamma))));

  const auto* data =
      GetReputationDataForSite(url::Origin::Create(GURL(kTestOriginDelta)));
  ASSERT_TRUE(data);
  EXPECT_EQ(kTestDomainDelta, data->domain());
  EXPECT_EQ(SiteReputation::UNSOLICITED_PROMPTS,
            data->notification_ux_quality());

  data =
      GetReputationDataForSite(url::Origin::Create(GURL(kTestOriginEpsilon)));
  ASSERT_TRUE(data);
  EXPECT_EQ(kTestDomainEpsilon, data->domain());
  EXPECT_EQ(SiteReputation::ACCEPTABLE, data->notification_ux_quality());
}

TEST_F(CrowdDenyPreloadDataTest, UpdateToMissingFileWipesInMemoryState) {
  ASSERT_NO_FATAL_FAILURE(SerializeAndLoadCannedTestData());

  LoadTestDataAndWait(GetPathInTempDir(FILE_PATH_LITERAL("NonExistentFile")));
  ExpectEmptyPreloadData();
}

TEST_F(CrowdDenyPreloadDataTest, UpdateToEmptyFileWipesInMemoryState) {
  ASSERT_NO_FATAL_FAILURE(SerializeAndLoadCannedTestData());

  const base::FilePath empty_file_path =
      GetPathInTempDir(FILE_PATH_LITERAL("EmptyFile"));
  SerializeTestRawData(std::string_view(), empty_file_path);
  LoadTestDataAndWait(empty_file_path);
  ExpectEmptyPreloadData();
}

TEST_F(CrowdDenyPreloadDataTest, UpdateToBadFileWipesInMemoryState) {
  ASSERT_NO_FATAL_FAILURE(SerializeAndLoadCannedTestData());

  const base::FilePath bad_data_path =
      GetPathInTempDir(FILE_PATH_LITERAL("BadFile"));
  SerializeTestRawData("This is not a proto.", bad_data_path);
  LoadTestDataAndWait(bad_data_path);
  ExpectEmptyPreloadData();
}

// During start-up congestion, it is possible that a new version of the
// component becomes available while the old version is pending being loaded.
// Ensure that when things settle down, the last version loaded will prevail,
// and nothing explodes on the way.
TEST_F(CrowdDenyPreloadDataTest, LastOneSurvivesFromUpdatesInQuickSuccession) {
  ASSERT_NO_FATAL_FAILURE(SerializeAndLoadCannedTestData());

  // Prepare and load two updated versions, each twice, in a quick success.
  chrome_browser_crowd_deny::PreloadData test_data_v2;
  auto* delta_site_reputation = test_data_v2.add_site_reputations();
  delta_site_reputation->set_domain(kTestDomainDelta);

  chrome_browser_crowd_deny::PreloadData test_data_v3;
  auto* epsilon_site_reputation = test_data_v3.add_site_reputations();
  epsilon_site_reputation->set_domain(kTestDomainEpsilon);
  epsilon_site_reputation->set_notification_ux_quality(
      SiteReputation::ACCEPTABLE);

  const base::FilePath data_path_v2 =
      GetPathInTempDir(FILE_PATH_LITERAL("DataV2"));
  const base::FilePath data_path_v3 =
      GetPathInTempDir(FILE_PATH_LITERAL("DataV3"));
  SerializeTestPreloadData(std::move(test_data_v2), data_path_v2);
  SerializeTestPreloadData(std::move(test_data_v3), data_path_v3);

  // Trigger three loads without pumping the message loop.
  //
  // TODO(crbug.com/40109238): Think about making this test stronger. Even if
  // the ordering were random, given the generous retry policy in continuous
  // build, the test would still pass most of the time.
  preload_data()->LoadFromDisk(data_path_v2, base::Version());
  preload_data()->LoadFromDisk(data_path_v3, base::Version());
  task_environment()->RunUntilIdle();

  // Expect the new version to have become visible.
  const auto* data =
      GetReputationDataForSite(url::Origin::Create(GURL(kTestOriginEpsilon)));
  ASSERT_TRUE(data);
  EXPECT_EQ(kTestDomainEpsilon, data->domain());
  EXPECT_EQ(SiteReputation::ACCEPTABLE, data->notification_ux_quality());

  EXPECT_FALSE(
      GetReputationDataForSite(url::Origin::Create(GURL(kTestOriginDelta))));
}
