// Copyright 2015 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/safe_browsing/incident_reporting/state_store.h"

#include <stdint.h>

#include "base/files/scoped_temp_dir.h"
#include "base/json/json_file_value_serializer.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/test_simple_task_runner.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/prefs/browser_prefs.h"
#include "chrome/browser/safe_browsing/incident_reporting/incident.h"
#include "chrome/browser/safe_browsing/incident_reporting/platform_state_store.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/in_memory_pref_store.h"
#include "components/safe_browsing/core/common/safe_browsing_prefs.h"
#include "components/sync_preferences/pref_service_syncable.h"
#include "components/sync_preferences/pref_service_syncable_factory.h"
#include "content/public/test/browser_task_environment.h"
#include "extensions/browser/quota_service.h"
#include "testing/gtest/include/gtest/gtest.h"

#if BUILDFLAG(IS_WIN)
#include "base/test/test_reg_util_win.h"
#endif

namespace safe_browsing {

#if BUILDFLAG(IS_WIN)

// A base test fixture that redirects HKCU for testing the platform state store
// backed by the Windows registry to prevent interference with existing Chrome
// installs or other tests.
class PlatformStateStoreTestBase : public ::testing::Test {
 protected:
  void SetUp() override {
    ::testing::Test::SetUp();
    ASSERT_NO_FATAL_FAILURE(
        registry_override_manager_.OverrideRegistry(HKEY_CURRENT_USER));
  }

 private:
  registry_util::RegistryOverrideManager registry_override_manager_;
};

#else  // BUILDFLAG(IS_WIN)

using PlatformStateStoreTestBase = ::testing::Test;

#endif  // BUILDFLAG(IS_WIN)

// A test fixture with a testing profile that writes its user prefs to a json
// file.
class StateStoreTest : public PlatformStateStoreTestBase {
 protected:
  struct TestData {
    IncidentType type;
    const char* key;
    uint32_t digest;
  };

  StateStoreTest()
      : profile_(nullptr),
        task_runner_(new base::TestSimpleTaskRunner()),
        profile_manager_(TestingBrowserProcess::GetGlobal()) {}

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

  void SetUp() override {
    PlatformStateStoreTestBase::SetUp();
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
    ASSERT_TRUE(profile_manager_.SetUp());
    CreateProfile();
  }

  void DeleteProfile() {
    if (profile_) {
      profile_ = nullptr;
      profile_manager_.DeleteTestingProfile(kProfileName_);
    }
  }

  // Commits a pending write (which posts a task to task_runner_) and waits for
  // it to finish.
  void CommitWrite() {
    ASSERT_NE(nullptr, profile_);
    profile_->GetPrefs()->CommitPendingWrite();
    task_runner_->RunUntilIdle();
  }

  // Removes the safebrowsing.incidents_sent preference from the profile's pref
  // store.
  void TrimPref() {
    ASSERT_EQ(nullptr, profile_.get());
    std::unique_ptr<base::Value> prefs(JSONFileValueDeserializer(GetPrefsPath())
                                           .Deserialize(nullptr, nullptr));
    ASSERT_NE(nullptr, prefs.get());
    base::DictValue* dict = prefs->GetIfDict();
    ASSERT_TRUE(dict);
    ASSERT_TRUE(dict->RemoveByDottedPath(prefs::kSafeBrowsingIncidentsSent));
    ASSERT_TRUE(JSONFileValueSerializer(GetPrefsPath()).Serialize(*dict));
  }

  void CreateProfile() {
    ASSERT_EQ(nullptr, profile_.get());
    // Create the testing profile with a file-backed user pref store.
    sync_preferences::PrefServiceSyncableFactory factory;
    factory.SetUserPrefsFile(GetPrefsPath(), task_runner_.get());
    factory.SetAccountPrefStore(base::MakeRefCounted<InMemoryPrefStore>());
    user_prefs::PrefRegistrySyncable* pref_registry =
        new user_prefs::PrefRegistrySyncable();
    RegisterUserProfilePrefs(pref_registry);
    profile_ = profile_manager_.CreateTestingProfile(
        kProfileName_, factory.CreateSyncable(pref_registry),
        base::UTF8ToUTF16(kProfileName_), 0,
        TestingProfile::TestingFactories());
  }

  static const char kProfileName_[];
  static const TestData kTestData_[];
  content::BrowserTaskEnvironment task_environment_;
  raw_ptr<TestingProfile, DanglingUntriaged> profile_;
  scoped_refptr<base::TestSimpleTaskRunner> task_runner_;

 private:
  base::FilePath GetPrefsPath() {
    return temp_dir_.GetPath().AppendASCII("prefs");
  }

  extensions::QuotaService::ScopedDisablePurgeForTesting
      disable_purge_for_testing_;
  base::ScopedTempDir temp_dir_;
  TestingProfileManager profile_manager_;
};

// static
const char StateStoreTest::kProfileName_[] = "test_profile";
const StateStoreTest::TestData StateStoreTest::kTestData_[] = {
    {IncidentType::TRACKED_PREFERENCE, "tp_one", 1},
    {IncidentType::TRACKED_PREFERENCE, "tp_two", 2},
    {IncidentType::TRACKED_PREFERENCE, "tp_three", 3},
    {IncidentType::BINARY_INTEGRITY, "bi", 0},
};

TEST_F(StateStoreTest, MarkAsAndHasBeenReported) {
  StateStore state_store(profile_);

  for (const auto& data : kTestData_)
    ASSERT_FALSE(state_store.HasBeenReported(data.type, data.key, data.digest));

  {
    StateStore::Transaction transaction(&state_store);
    for (const auto& data : kTestData_) {
      transaction.MarkAsReported(data.type, data.key, data.digest);
      ASSERT_TRUE(
          state_store.HasBeenReported(data.type, data.key, data.digest));
    }
  }

  for (const auto& data : kTestData_)
    ASSERT_TRUE(state_store.HasBeenReported(data.type, data.key, data.digest));
}

TEST_F(StateStoreTest, ClearForType) {
  StateStore state_store(profile_);

  {
    StateStore::Transaction transaction(&state_store);
    for (const auto& data : kTestData_)
      transaction.MarkAsReported(data.type, data.key, data.digest);
  }

  for (const auto& data : kTestData_)
    ASSERT_TRUE(state_store.HasBeenReported(data.type, data.key, data.digest));

  const IncidentType removed_type = IncidentType::TRACKED_PREFERENCE;
  StateStore::Transaction(&state_store).ClearForType(removed_type);

  for (const auto& data : kTestData_) {
    if (data.type == removed_type) {
      ASSERT_FALSE(
          state_store.HasBeenReported(data.type, data.key, data.digest));
    } else {
      ASSERT_TRUE(
          state_store.HasBeenReported(data.type, data.key, data.digest));
    }
  }
}

TEST_F(StateStoreTest, ClearAll) {
  StateStore state_store(profile_);
  // Write some state to the store.
  {
    StateStore::Transaction transaction(&state_store);
    for (const auto& data : kTestData_)
      transaction.MarkAsReported(data.type, data.key, data.digest);
  }

  StateStore::Transaction(&state_store).ClearAll();

  for (const auto& data : kTestData_) {
    ASSERT_FALSE(state_store.HasBeenReported(data.type, data.key, data.digest));
  }

  // Write prefs out to the JsonPrefStore.
  CommitWrite();

  // Delete the profile.
  DeleteProfile();

  // Recreate the profile.
  CreateProfile();

  StateStore store_2(profile_);
  for (const auto& data : kTestData_) {
    // Verify that the state did not survive through the Platform State Store.
    ASSERT_FALSE(store_2.HasBeenReported(data.type, data.key, data.digest));
  }
}

TEST_F(StateStoreTest, Persistence) {
  // Write some state to the store.
  {
    StateStore state_store(profile_);
    StateStore::Transaction transaction(&state_store);
    for (const auto& data : kTestData_)
      transaction.MarkAsReported(data.type, data.key, data.digest);
  }

  // Run tasks to write prefs out to the JsonPrefStore.
  CommitWrite();

  // Delete the profile.
  DeleteProfile();

  // Recreate the profile.
  CreateProfile();

  // Verify that the state survived.
  StateStore state_store(profile_);
  for (const auto& data : kTestData_)
    ASSERT_TRUE(state_store.HasBeenReported(data.type, data.key, data.digest));
}

TEST_F(StateStoreTest, PersistenceWithStoreDelete) {
  // Write some state to the store.
  {
    StateStore state_store(profile_);
    StateStore::Transaction transaction(&state_store);
    for (const auto& data : kTestData_)
      transaction.MarkAsReported(data.type, data.key, data.digest);
  }

  // Write prefs out to the JsonPrefStore.
  CommitWrite();

  // Delete the profile.
  DeleteProfile();

  // Delete the state pref.
  TrimPref();

  // Recreate the profile.
  CreateProfile();

  StateStore state_store(profile_);
  for (const auto& data : kTestData_) {
#if defined(USE_PLATFORM_STATE_STORE)
    // Verify that the state survived.
    ASSERT_TRUE(state_store.HasBeenReported(data.type, data.key, data.digest));
#else
    // Verify that the state did not survive.
    ASSERT_FALSE(state_store.HasBeenReported(data.type, data.key, data.digest));
#endif
  }
}

TEST_F(StateStoreTest, ClearAfterTransaction) {
  StateStore state_store(profile_);

  // Perform a mutation in a first transaction.
  {
    StateStore::Transaction transaction(&state_store);
    transaction.MarkAsReported(kTestData_[0].type, kTestData_[0].key,
                               kTestData_[0].digest);
  }

  // Clear the preference in PrefService. This deallocates the previous
  // dictionary from the user pref store.
  profile_->GetPrefs()->ClearPref(prefs::kSafeBrowsingIncidentsSent);

  // A subsequent transaction clearing the incident must safely read and update
  // the preference rather than dereferencing a dangling incidents_sent_
  // pointer to the deallocated dictionary.
  {
    StateStore::Transaction transaction(&state_store);
    transaction.Clear(kTestData_[0].type, kTestData_[0].key);
  }

  EXPECT_FALSE(state_store.HasBeenReported(
      kTestData_[0].type, kTestData_[0].key, kTestData_[0].digest));
}

TEST_F(StateStoreTest, ClearAfterConstructorMutation) {
  // Populate the platform state store so that StateStore's constructor performs
  // a mutation (calling ReplacePrefDict).
  base::DictValue initial_state;
  initial_state.SetByDottedPath(
      base::StrCat({base::NumberToString(static_cast<int>(kTestData_[0].type)),
                    ".", kTestData_[0].key}),
      base::NumberToString(kTestData_[0].digest));
  platform_state_store::Store(profile_, initial_state);

  StateStore state_store(profile_);

  // Clear the preference in PrefService to deallocate the old dictionary from
  // the user pref store.
  profile_->GetPrefs()->ClearPref(prefs::kSafeBrowsingIncidentsSent);

  // Clear the incident in a subsequent transaction.
  {
    StateStore::Transaction transaction(&state_store);
    transaction.Clear(kTestData_[0].type, kTestData_[0].key);
  }

  EXPECT_FALSE(state_store.HasBeenReported(
      kTestData_[0].type, kTestData_[0].key, kTestData_[0].digest));
}

}  // namespace safe_browsing
