// Copyright 2018 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/metrics/metrics_reporting_state.h"

#include <memory>
#include <optional>
#include <string>
#include <string_view>

#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/json/json_file_value_serializer.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/statistics_recorder.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chrome_browser_main.h"
#include "chrome/browser/chrome_browser_main_extra_parts.h"
#include "chrome/browser/metrics/chrome_metrics_service_accessor.h"
#include "chrome/browser/metrics/testing/metrics_reporting_pref_helper.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "components/metrics/metrics_pref_names.h"
#include "components/metrics/metrics_service_accessor.h"
#include "components/policy/core/common/cloud/test/policy_builder.h"
#include "components/prefs/pref_service.h"
#include "content/public/test/browser_test.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "testing/gmock/include/gmock/gmock.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "chromeos/ash/components/install_attributes/stub_install_attributes.h"
#include "chromeos/ash/components/settings/device_settings_cache.h"
#include "components/policy/proto/chrome_device_policy.pb.h"
#include "components/policy/proto/device_management_backend.pb.h"
#endif

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

struct MetricsReportingStateTestParameterizedParams {
  bool initial_value;
  bool final_value;
};

// ChromeBrowserMainExtraParts implementation that asserts the metrics and
// reporting state matches a particular value in PreCreateThreads().
class ChromeBrowserMainExtraPartsChecker : public ChromeBrowserMainExtraParts {
 public:
  explicit ChromeBrowserMainExtraPartsChecker(
      bool expected_metrics_reporting_enabled)
      : expected_metrics_reporting_enabled_(
            expected_metrics_reporting_enabled) {}

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

  // ChromeBrowserMainExtraParts:
  void PostEarlyInitialization() override;

 private:
  // Expected value of reporting state.
  const bool expected_metrics_reporting_enabled_;
};

// Used to appropriately set up the initial value of
// IsMetricsAndCrashReportingEnabled().
class MetricsReportingStateTest : public InProcessBrowserTest {
 public:
  MetricsReportingStateTest(const MetricsReportingStateTest&) = delete;
  MetricsReportingStateTest& operator=(const MetricsReportingStateTest&) =
      delete;

  ~MetricsReportingStateTest() override = default;

  static bool IsMetricsAndCrashReportingEnabled() {
    return ChromeMetricsServiceAccessor::IsMetricsAndCrashReportingEnabled();
  }

  virtual bool IsMetricsReportingEnabledInitialValue() const = 0;

#if BUILDFLAG(IS_WIN)
  void SetUp() override {
    // Override HKCU to prevent writing to real keys. On Windows, the metrics
    // reporting consent is stored in the registry, and it is used to determine
    // the metrics reporting state when it is unset (e.g. during tests, which
    // start with fresh user data dirs). Otherwise, this may cause flakiness
    // since tests will sometimes start with metrics reporting enabled and
    // sometimes disabled.
    ASSERT_NO_FATAL_FAILURE(
        override_manager_.OverrideRegistry(HKEY_CURRENT_USER));

    InProcessBrowserTest::SetUp();
  }
#endif  // BUILDFLAG(IS_WIN)

  // InProcessBrowserTest overrides:
  bool SetUpUserDataDirectory() override {
    local_state_path_ = metrics::SetUpUserDataDirectoryForTesting(
        IsMetricsReportingEnabledInitialValue());
    return !local_state_path_.empty();
  }

  void CreatedBrowserMainParts(content::BrowserMainParts* parts) override {
    InProcessBrowserTest::CreatedBrowserMainParts(parts);
    // IsMetricsReportingEnabled() in non-official builds always returns false.
    // Enable the official build checks so that this test can work in both
    // official and non-official builds.
    ChromeMetricsServiceAccessor::SetForceIsMetricsReportingEnabledPrefLookup(
        true);
    static_cast<ChromeBrowserMainParts*>(parts)->AddParts(
        std::make_unique<ChromeBrowserMainExtraPartsChecker>(
            IsMetricsReportingEnabledInitialValue()));
  }

 protected:
  MetricsReportingStateTest() = default;

  base::FilePath local_state_path_;

#if BUILDFLAG(IS_WIN)
 private:
  registry_util::RegistryOverrideManager override_manager_;
#endif  // BUILDFLAG(IS_WIN)
};

// Used to verify the value for IsMetricsAndCrashReportingEnabled() is correctly
// written to disk when changed. The first parameter of this test corresponds
// to the initial value of IsMetricsAndCrashReportingEnabled(). The second
// parameter corresponds to what the value should change to during the test.
class MetricsReportingStateTestParameterized
    : public MetricsReportingStateTest,
      public testing::WithParamInterface<
          MetricsReportingStateTestParameterizedParams> {
 public:
  MetricsReportingStateTestParameterized() = default;

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

  ~MetricsReportingStateTestParameterized() override = default;

  bool IsMetricsReportingEnabledInitialValue() const override {
    return GetParam().initial_value;
  }

  bool is_metrics_reporting_enabled_final_value() const {
    return GetParam().final_value;
  }

  base::FilePath local_state_path() { return local_state_path_; }
};

// Used to verify that metrics collected during a session are discarded upon
// enabling metrics reporting, so that only data collected after enabling
// metrics are collected. Histogram data should only be cleared if metrics
// reporting was enabled from a settings page.
class MetricsReportingStateClearDataTest
    : public MetricsReportingStateTest,
      public testing::WithParamInterface<
          metrics::ChangeMetricsReportingStateCalledFrom> {
 public:
  // Set metrics reporting to false initially.
  bool IsMetricsReportingEnabledInitialValue() const override { return false; }
};

void ChromeBrowserMainExtraPartsChecker::PostEarlyInitialization() {
  ASSERT_EQ(expected_metrics_reporting_enabled_,
            MetricsReportingStateTest::IsMetricsAndCrashReportingEnabled());
}

// Callback from changing whether reporting is enabled.
void OnMetricsReportingStateChanged(bool* new_state_ptr,
                                    base::OnceClosure run_loop_closure,
                                    bool new_state) {
  *new_state_ptr = new_state;
  std::move(run_loop_closure).Run();
}

bool HistogramExists(std::string_view name) {
  return base::StatisticsRecorder::FindHistogram(name) != nullptr;
}

base::HistogramBase::Count32 GetHistogramDeltaTotalCount(
    std::string_view name) {
  return base::StatisticsRecorder::FindHistogram(name)
      ->SnapshotDelta()
      ->TotalCount();
}

// Verifies that metrics reporting state is correctly written to disk when set.
IN_PROC_BROWSER_TEST_P(MetricsReportingStateTestParameterized,
                       ChangeMetricsReportingState) {
  ASSERT_EQ(IsMetricsReportingEnabledInitialValue(),
            MetricsReportingStateTest::IsMetricsAndCrashReportingEnabled());

  // Update the client's metrics reporting state.
  base::RunLoop run_loop;
  bool value_after_change = false;
  metrics::ChangeMetricsReportingStateWithReply(
      is_metrics_reporting_enabled_final_value(),
      base::BindOnce(&OnMetricsReportingStateChanged, &value_after_change,
                     run_loop.QuitClosure()),
      metrics::ChangeMetricsReportingStateCalledFrom::kUiFirstRun);
  run_loop.Run();

  // Verify that the reporting state has been duly updated.
  EXPECT_EQ(is_metrics_reporting_enabled_final_value(), value_after_change);

  // Flush Local State prefs, which include |kMetricsReportingEnabled| to disk.
  base::ScopedAllowBlockingForTesting scoped_allow_blocking;
  PrefService* local_state = g_browser_process->local_state();
  base::RunLoop loop;
  local_state->CommitPendingWrite(
      base::BindOnce([](base::RunLoop* loop) { loop->Quit(); }, &loop));
  loop.Run();

  // Read the Local State file.
  JSONFileValueDeserializer deserializer(local_state_path());
  std::unique_ptr<base::Value> local_state_contents = deserializer.Deserialize(
      /*error_code=*/nullptr, /*error_message=*/nullptr);
  ASSERT_THAT(local_state_contents, ::testing::NotNull());

  // Verify that the metrics reporting state in the file is what's expected.
  std::optional<bool> metrics_reporting_state =
      local_state_contents->GetIfDict()->FindBoolByDottedPath(
          metrics::prefs::kMetricsReportingEnabled);
  EXPECT_TRUE(metrics_reporting_state.has_value());
  EXPECT_EQ(metrics_reporting_state.value(),
            is_metrics_reporting_enabled_final_value());
}

// Verifies that collected data is cleared after enabling metrics reporting.
// Histogram data should only be cleared (marked as reported) when enabling
// metrics reporting from a settings page.
IN_PROC_BROWSER_TEST_P(MetricsReportingStateClearDataTest,
                       ClearPreviouslyCollectedMetricsData) {
  // Set a stability-related count stored in Local State.
  g_browser_process->local_state()->SetInteger(
      metrics::prefs::kStabilityFileMetricsUnsentFilesCount, 1);
  // Emit to two histograms.
  ASSERT_FALSE(HistogramExists("Test.Before.Histogram"));
  ASSERT_FALSE(HistogramExists("Test.Before.StabilityHistogram"));
  base::UmaHistogramBoolean("Test.Before.Histogram", true);
  UMA_STABILITY_HISTOGRAM_BOOLEAN("Test.Before.StabilityHistogram", true);
  ASSERT_TRUE(HistogramExists("Test.Before.Histogram"));
  ASSERT_TRUE(HistogramExists("Test.Before.StabilityHistogram"));

  // Simulate enabling metrics reporting.
  metrics::ChangeMetricsReportingStateCalledFrom called_from = GetParam();
  base::RunLoop run_loop;
  bool value_after_change = false;
  metrics::ChangeMetricsReportingStateWithReply(
      true,
      base::BindOnce(&OnMetricsReportingStateChanged, &value_after_change,
                     run_loop.QuitClosure()),
      called_from);
  run_loop.Run();
  ASSERT_TRUE(value_after_change);

  // Emit to one histogram after enabling metrics reporting.
  ASSERT_FALSE(HistogramExists("Test.After.Histogram"));
  base::UmaHistogramBoolean("Test.After.Histogram", true);
  ASSERT_TRUE(HistogramExists("Test.After.Histogram"));

  // Verify that the stability-related metric in Local State was cleared.
  EXPECT_EQ(0, g_browser_process->local_state()->GetInteger(
                   metrics::prefs::kStabilityFileMetricsUnsentFilesCount));
  // Verify that histogram data that came before clearing data are not included
  // in the next snapshot if metrics reporting was enabled from a settings page.
  bool called_from_settings_page =
      (called_from ==
       metrics::ChangeMetricsReportingStateCalledFrom::kUiSettings);
  EXPECT_EQ(called_from_settings_page ? 0 : 1,
            GetHistogramDeltaTotalCount("Test.Before.Histogram"));
  EXPECT_EQ(called_from_settings_page ? 0 : 1,
            GetHistogramDeltaTotalCount("Test.Before.StabilityHistogram"));
  // Verify that histogram data that came after clearing data is included in the
  // next snapshot.
  EXPECT_EQ(1, GetHistogramDeltaTotalCount("Test.After.Histogram"));

  // Clean up histograms.
  base::StatisticsRecorder::ForgetHistogramForTesting("Test.Before.Histogram");
  base::StatisticsRecorder::ForgetHistogramForTesting(
      "Test.Before.StabilityHistogram");
  base::StatisticsRecorder::ForgetHistogramForTesting("Test.After.Histogram");
}

INSTANTIATE_TEST_SUITE_P(
    MetricsReportingStateTests,
    MetricsReportingStateTestParameterized,
    testing::ValuesIn<MetricsReportingStateTestParameterizedParams>(
        // The first param determines what is the initial state of metrics
        // reporting at the beginning of the test. The second param determines
        // what the metrics reporting state should change to during the test.
        {{false, false}, {false, true}, {true, false}, {true, true}}));

INSTANTIATE_TEST_SUITE_P(
    MetricsReportingStateTests,
    MetricsReportingStateClearDataTest,
    testing::ValuesIn<metrics::ChangeMetricsReportingStateCalledFrom>(
        {metrics::ChangeMetricsReportingStateCalledFrom::kUiSettings,
         metrics::ChangeMetricsReportingStateCalledFrom::kUiFirstRun,
         metrics::ChangeMetricsReportingStateCalledFrom::kSessionCrashedDialog,
         metrics::ChangeMetricsReportingStateCalledFrom::
             kCrosMetricsSettingsChange}));


#if BUILDFLAG(IS_CHROMEOS)
// Used to verify that managed/unmanged devices returns correct values based on
// management state.
class MetricsReportingStateManagedTest
    : public MetricsReportingStateTest,
      public testing::WithParamInterface<bool> {
 public:
  MetricsReportingStateManagedTest() = default;
  MetricsReportingStateManagedTest(const MetricsReportingStateManagedTest&) =
      delete;
  MetricsReportingStateManagedTest& operator=(
      const MetricsReportingStateManagedTest&) = delete;
  ~MetricsReportingStateManagedTest() = default;

  void SetUp() override {
    is_managed_ = GetParam();
    if (is_managed()) {
      install_attributes_ = std::make_unique<ash::ScopedStubInstallAttributes>(
          ash::StubInstallAttributes::CreateCloudManaged("domain",
                                                         "device_id"));
    } else {
      install_attributes_ = std::make_unique<ash::ScopedStubInstallAttributes>(
          ash::StubInstallAttributes::CreateConsumerOwned());
    }

    MetricsReportingStateTest::SetUp();
  }

  // Set metrics reporting initial value to false.
  bool IsMetricsReportingEnabledInitialValue() const override { return false; }

 protected:
  bool is_managed() const { return is_managed_; }

 private:
  bool is_managed_;
  std::unique_ptr<ash::ScopedStubInstallAttributes> install_attributes_;
};

IN_PROC_BROWSER_TEST_P(MetricsReportingStateManagedTest,
                       IsMetricsReportingPolicyManagedReturnsCorrectValue) {
  EXPECT_EQ(metrics::IsMetricsReportingPolicyManaged(), is_managed());
}

IN_PROC_BROWSER_TEST_P(MetricsReportingStateManagedTest,
                       MetricsReportingStateUpdatesOnCrosMetricsChange) {
  // Simulate enabling metrics reporting through OnCrosMetricsChange().
  base::RunLoop run_loop;
  bool value_after_change = false;
  metrics::ChangeMetricsReportingStateWithReply(
      true,
      base::BindOnce(&OnMetricsReportingStateChanged, &value_after_change,
                     run_loop.QuitClosure()),
      metrics::ChangeMetricsReportingStateCalledFrom::
          kCrosMetricsSettingsChange);
  run_loop.Run();

  // Value should have changed regardless whether the device is managed or not.
  ASSERT_TRUE(value_after_change);
}

INSTANTIATE_TEST_SUITE_P(MetricsReportingStateTests,
                         MetricsReportingStateManagedTest,
                         testing::Bool());

#endif
