// Copyright 2024 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/policy/cloud/cloud_policy_invalidator.h"

#include <stdint.h>

#include <memory>
#include <optional>
#include <string>
#include <utility>

#include "base/metrics/histogram_base.h"
#include "base/metrics/histogram_samples.h"
#include "base/metrics/sample_map.h"
#include "base/metrics/statistics_recorder.h"
#include "base/run_loop.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/simple_test_clock.h"
#include "base/test/task_environment.h"
#include "base/test/test_simple_task_runner.h"
#include "base/time/time.h"
#include "base/values.h"
#include "build/build_config.h"
#include "components/invalidation/test_support/fake_invalidation_listener.h"
#include "components/policy/core/common/cloud/cloud_policy_constants.h"
#include "components/policy/core/common/cloud/cloud_policy_core.h"
#include "components/policy/core/common/cloud/cloud_policy_refresh_scheduler.h"
#include "components/policy/core/common/cloud/enterprise_metrics.h"
#include "components/policy/core/common/cloud/mock_cloud_policy_client.h"
#include "components/policy/core/common/cloud/mock_cloud_policy_store.h"
#include "components/policy/core/common/cloud/policy_invalidation_scope.h"
#include "components/policy/core/common/cloud/policy_invalidation_util.h"
#include "components/policy/core/common/policy_types.h"
#include "components/policy/policy_constants.h"
#include "components/policy/proto/device_management_backend.pb.h"
#include "services/network/test/test_network_connection_tracker.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using ::testing::_;
using ::testing::WithArg;

namespace em = enterprise_management;

namespace policy {

namespace {

// Fake policy values which are alternated to cause the store to report a
// changed policy.
constexpr char kPolicyValueA[] = "policyValueA";
constexpr char kPolicyValueB[] = "policyValueB";

constexpr char kDeviceLocalAccountId[] = "test_account";

}  // namespace

class CloudPolicyInvalidatorTestBase : public testing::Test {
 protected:
  CloudPolicyInvalidatorTestBase();
  ~CloudPolicyInvalidatorTestBase() override;

  // Starts the invalidator which will be tested.
  void StartInvalidator();

  const CloudPolicyInvalidator* invalidator() const {
    return invalidator_.get();
  }

  // Connects the cloud policy core.
  void ConnectCore();

  // Starts the refresh scheduler.
  void StartRefreshScheduler();

  // Disconnects the cloud policy core.
  void DisconnectCore();

  // Simulates storing a new policy to the policy store.
  // `policy_changed` determines whether a policy value different from the
  // current value will be stored.
  // `time` determines the timestamp the store will report.
  void StorePolicy(int64_t invalidation_version,
                   bool policy_changed,
                   const base::Time& time);
  void StorePolicy(int64_t invalidation_version, bool policy_changed) {
    StorePolicy(invalidation_version, policy_changed, Now() - base::Minutes(5));
  }
  void StorePolicy(int64_t invalidation_version) {
    StorePolicy(invalidation_version, false);
  }
  void StorePolicy() { StorePolicy(0); }

  // Disables the invalidation service. It is disabled by default.
  void DisableInvalidationListener();

  // Enables the invalidation service. It is disabled by default.
  void EnableInvalidationListener();

  // Causes the invalidation service to fire an invalidation.
  invalidation::DirectInvalidation FireInvalidation(int64_t version,
                                                    const std::string& payload);

  // Returns true if the invalidation info of the `core_`'s client matches the
  // passed invalidation's version and payload.
  bool ClientInvalidationInfoMatches(
      const invalidation::DirectInvalidation& invalidation);

  // Returns true if the invalidation info of the `core_`'s client is unset.
  bool ClientInvalidationInfoIsUnset();

  // Returns the number of policy refreshes that have occurred since the last
  // call to this method and resets the counter to 0.
  int GetPolicyRefreshCountAndReset();

  // Returns the invalidations enabled state set by the invalidator on the
  // refresh scheduler.
  bool InvalidationsEnabled();

  // Determines if the invalidator has registered as an observer with the
  // invalidation service.
  bool IsInvalidatorRegistered();

  // Advance the test clock by the given `delta`.
  void FastForwardBy(base::TimeDelta delta);
  // Advance the test clock to trigger invalidation handling.
  void FastForwardByInvalidationDelay();

  // Get the current time on the test clock.
  base::Time Now();

  // Translate a version number into an appropriate invalidation version (which
  // is based on the start time).
  int64_t V(int version);

  // Get an invalidation version for the given time.
  int64_t GetVersion(base::Time time);

  // Get the invalidation scope that the `invalidator_` is responsible for.
  virtual PolicyInvalidationScope GetPolicyInvalidationScope() const;
  std::string GetPolicyInvalidationType() const;

 private:
  base::test::SingleThreadTaskEnvironment task_environment_{
      base::test::SingleThreadTaskEnvironment::TimeSource::MOCK_TIME};
  const base::Time start_time{task_environment_.GetMockClock()->Now()};

  // Objects the invalidator depends on.
  testing::NiceMock<MockCloudPolicyStore> store_{
      dm_protocol::GetChromeUserPolicyType()};
  CloudPolicyCore core_{dm_protocol::GetChromeUserPolicyType(),
                        std::string(),
                        &store_,
                        task_environment_.GetMainThreadTaskRunner(),
                        network::TestNetworkConnectionTracker::CreateGetter()};
  int policy_refresh_count_ = 0;

  invalidation::FakeInvalidationListener invalidation_listener_;

  // The currently used policy value.
  std::string policy_value_cur_{kPolicyValueA};

  // The invalidator which will be tested.
  std::unique_ptr<CloudPolicyInvalidator> invalidator_;
};

CloudPolicyInvalidatorTestBase::CloudPolicyInvalidatorTestBase() = default;

CloudPolicyInvalidatorTestBase::~CloudPolicyInvalidatorTestBase() {
  core_.Disconnect();
}

void CloudPolicyInvalidatorTestBase::StartInvalidator() {
  invalidator_ = std::make_unique<CloudPolicyInvalidator>(
      GetPolicyInvalidationScope(), &invalidation_listener_, &core_,
      task_environment_.GetMainThreadTaskRunner(),
      task_environment_.GetMockClock(), kDeviceLocalAccountId);
}

void CloudPolicyInvalidatorTestBase::ConnectCore() {
  std::unique_ptr<testing::NiceMock<MockCloudPolicyClient>> client =
      std::make_unique<testing::NiceMock<MockCloudPolicyClient>>();

  ON_CALL(*client, FetchPolicy(PolicyFetchReason::kInvalidation))
      .WillByDefault([this]() { ++policy_refresh_count_; });

  client->SetDMToken("dm");
  core_.Connect(std::move(client));
}

void CloudPolicyInvalidatorTestBase::StartRefreshScheduler() {
  core_.StartRefreshScheduler();
}

void CloudPolicyInvalidatorTestBase::DisconnectCore() {
  core_.Disconnect();
}

void CloudPolicyInvalidatorTestBase::StorePolicy(int64_t invalidation_version,
                                                 bool policy_changed,
                                                 const base::Time& time) {
  auto data = std::make_unique<em::PolicyData>();
  data->set_timestamp(time.InMillisecondsSinceUnixEpoch());
  // Swap the policy value if a policy change is desired.
  if (policy_changed) {
    policy_value_cur_ =
        policy_value_cur_ == kPolicyValueA ? kPolicyValueB : kPolicyValueA;
  }
  data->set_policy_value(policy_value_cur_);
  store_.invalidation_version_ = invalidation_version;
  store_.set_policy_data_for_testing(std::move(data));
  base::DictValue policies;
  // MaxInvalidationFetchDelay policy is not supported on Android.
#if !BUILDFLAG(IS_ANDROID)
  policies.Set(key::kMaxInvalidationFetchDelay,
               static_cast<int>(
                   CloudPolicyInvalidator::kMaxFetchDelayMin.InMilliseconds()));
#endif  // !BUILDFLAG(IS_ANDROID)
  store_.policy_map_.LoadFrom(policies, POLICY_LEVEL_MANDATORY,
                              POLICY_SCOPE_MACHINE, POLICY_SOURCE_CLOUD);
  store_.NotifyStoreLoaded();
}

void CloudPolicyInvalidatorTestBase::DisableInvalidationListener() {
  invalidation_listener_.Shutdown();
}

void CloudPolicyInvalidatorTestBase::EnableInvalidationListener() {
  invalidation_listener_.Start();
}

invalidation::DirectInvalidation
CloudPolicyInvalidatorTestBase::FireInvalidation(int64_t version,
                                                 const std::string& payload) {
  invalidation::DirectInvalidation invalidation(GetPolicyInvalidationType(),
                                                version, payload);
  invalidation_listener_.FireInvalidation(invalidation);
  return invalidation;
}

bool CloudPolicyInvalidatorTestBase::ClientInvalidationInfoIsUnset() {
  MockCloudPolicyClient* client =
      static_cast<MockCloudPolicyClient*>(core_.client());
  if (!client) {
    return false;
  }

  return client->invalidation_version_ == 0 &&
         client->invalidation_payload_.empty();
}

int CloudPolicyInvalidatorTestBase::GetPolicyRefreshCountAndReset() {
  return std::exchange(policy_refresh_count_, 0);
}

bool CloudPolicyInvalidatorTestBase::ClientInvalidationInfoMatches(
    const invalidation::DirectInvalidation& invalidation) {
  MockCloudPolicyClient* client =
      static_cast<MockCloudPolicyClient*>(core_.client());
  if (!client) {
    return false;
  }

  return invalidation.version() == client->invalidation_version_ &&
         invalidation.payload() == client->invalidation_payload_;
}

bool CloudPolicyInvalidatorTestBase::InvalidationsEnabled() {
  return core_.refresh_scheduler()->invalidations_available();
}

bool CloudPolicyInvalidatorTestBase::IsInvalidatorRegistered() {
  return invalidator_ && invalidation_listener_.HasObserver(invalidator_.get());
}

void CloudPolicyInvalidatorTestBase::FastForwardBy(base::TimeDelta delta) {
  task_environment_.FastForwardBy(delta);
}

void CloudPolicyInvalidatorTestBase::FastForwardByInvalidationDelay() {
  // MaxInvalidationFetchDelay policy is not supported on Android.
#if !BUILDFLAG(IS_ANDROID)
  const auto* delay_policy_value = store_.policy_map().GetValue(
      key::kMaxInvalidationFetchDelay, base::Value::Type::INTEGER);
  const base::TimeDelta max_delay =
      delay_policy_value ? base::Milliseconds(delay_policy_value->GetInt())
                         : CloudPolicyInvalidator::kMaxFetchDelayMax;
#else
  const base::TimeDelta max_delay =
      CloudPolicyInvalidator::kMaxFetchDelayDefault;
#endif  // !BUILDFLAG(IS_ANDROID)
  FastForwardBy(max_delay);
}

base::Time CloudPolicyInvalidatorTestBase::Now() {
  return task_environment_.GetMockClock()->Now();
}

int64_t CloudPolicyInvalidatorTestBase::V(int version) {
  return GetVersion(start_time) + version;
}

int64_t CloudPolicyInvalidatorTestBase::GetVersion(base::Time time) {
  return (time - base::Time::UnixEpoch()).InMicroseconds();
}

PolicyInvalidationScope
CloudPolicyInvalidatorTestBase::GetPolicyInvalidationScope() const {
  return PolicyInvalidationScope::kUser;
}

std::string CloudPolicyInvalidatorTestBase::GetPolicyInvalidationType() const {
  switch (GetPolicyInvalidationScope()) {
    case PolicyInvalidationScope::kUser:
      return "USER_POLICY_FETCH";
    case PolicyInvalidationScope::kDevice:
      return "DEVICE_POLICY_FETCH";
    case PolicyInvalidationScope::kDeviceLocalAccount:
      return "PUBLIC_ACCOUNT_POLICY_FETCH-test_account";
    case PolicyInvalidationScope::kCBCM:
      return "BROWSER_POLICY_FETCH";
  }
}

class CloudPolicyInvalidatorTest : public CloudPolicyInvalidatorTestBase {};

TEST_F(CloudPolicyInvalidatorTest, DoesNotRegisterWhenCoreIsNotConnected) {
  StartInvalidator();
  EnableInvalidationListener();

  FireInvalidation(V(1), "test");
  FastForwardByInvalidationDelay();

  EXPECT_FALSE(IsInvalidatorRegistered());
  EXPECT_EQ(GetPolicyRefreshCountAndReset(), 0);
}

TEST_F(CloudPolicyInvalidatorTest,
       DoesNotRegisterWhenRefreshSchedulerNotStarted) {
  StartInvalidator();
  EnableInvalidationListener();
  ConnectCore();

  FireInvalidation(V(1), "test");
  FastForwardByInvalidationDelay();

  EXPECT_FALSE(IsInvalidatorRegistered());
  EXPECT_EQ(GetPolicyRefreshCountAndReset(), 0);
}

TEST_F(CloudPolicyInvalidatorTest, RegistersWhenCoreIsReady) {
  StartInvalidator();
  EnableInvalidationListener();
  ConnectCore();
  StartRefreshScheduler();

  FireInvalidation(V(1), "test");
  FastForwardByInvalidationDelay();

  EXPECT_TRUE(IsInvalidatorRegistered());
  EXPECT_TRUE(InvalidationsEnabled());
  EXPECT_EQ(GetPolicyRefreshCountAndReset(), 1);
}

TEST_F(CloudPolicyInvalidatorTest,
       UnregistersWhenCoreDisconnectsAndRegistersWhenConnected) {
  StartInvalidator();
  EnableInvalidationListener();
  ConnectCore();
  StartRefreshScheduler();

  EXPECT_TRUE(IsInvalidatorRegistered());
  EXPECT_TRUE(InvalidationsEnabled());

  DisconnectCore();
  EXPECT_FALSE(IsInvalidatorRegistered());

  ConnectCore();
  EXPECT_FALSE(IsInvalidatorRegistered());

  StartRefreshScheduler();
  EXPECT_TRUE(IsInvalidatorRegistered());
  EXPECT_TRUE(InvalidationsEnabled());
}

TEST_F(CloudPolicyInvalidatorTest,
       UpdatesInvalidationStatusWhenInvalidationListenerStarts) {
  StartInvalidator();
  ConnectCore();
  StartRefreshScheduler();

  EXPECT_TRUE(IsInvalidatorRegistered());
  EXPECT_FALSE(InvalidationsEnabled());

  EnableInvalidationListener();

  EXPECT_TRUE(IsInvalidatorRegistered());
  EXPECT_TRUE(InvalidationsEnabled());

  DisableInvalidationListener();

  EXPECT_TRUE(IsInvalidatorRegistered());
  EXPECT_FALSE(InvalidationsEnabled());
}

TEST_F(CloudPolicyInvalidatorTest, HandlesInvalidation) {
  StorePolicy();
  StartInvalidator();
  ConnectCore();
  StartRefreshScheduler();
  EnableInvalidationListener();

  const invalidation::DirectInvalidation inv =
      FireInvalidation(V(12), "test_payload");

  // Verify that invalidation is not yet handled as we did not pass random
  // delay.
  EXPECT_TRUE(ClientInvalidationInfoIsUnset());
  EXPECT_EQ(GetPolicyRefreshCountAndReset(), 0);

  FastForwardByInvalidationDelay();

  // Verify that the invalidation is handled after the delay.
  EXPECT_TRUE(ClientInvalidationInfoMatches(inv));
  EXPECT_EQ(GetPolicyRefreshCountAndReset(), 1);
  EXPECT_EQ(0, invalidator()->highest_handled_invalidation_version());

  StorePolicy(V(12));
  EXPECT_TRUE(ClientInvalidationInfoIsUnset());
  EXPECT_EQ(V(12), invalidator()->highest_handled_invalidation_version());
}

TEST_F(CloudPolicyInvalidatorTest, HandlesInvalidationWithoutPayload) {
  StorePolicy();
  StartInvalidator();
  ConnectCore();
  StartRefreshScheduler();
  EnableInvalidationListener();

  // Fire an invalidation and check that it triggered a policy refresh only
  // after a random delay.
  const invalidation::DirectInvalidation inv = FireInvalidation(V(12), "");

  // Verify that invalidation is not yet handled as we did not pass random
  // delay.
  EXPECT_TRUE(ClientInvalidationInfoIsUnset());
  EXPECT_EQ(GetPolicyRefreshCountAndReset(), 0);
  EXPECT_EQ(0, invalidator()->highest_handled_invalidation_version());

  FastForwardByInvalidationDelay();

  EXPECT_TRUE(ClientInvalidationInfoMatches(inv));
  EXPECT_EQ(GetPolicyRefreshCountAndReset(), 1);
  EXPECT_EQ(0, invalidator()->highest_handled_invalidation_version());

  StorePolicy(V(12));
  EXPECT_TRUE(ClientInvalidationInfoIsUnset());
  EXPECT_EQ(V(12), invalidator()->highest_handled_invalidation_version());
}

TEST_F(CloudPolicyInvalidatorTest, HandlesInvalidationBeforePolicyLoaded) {
  StartInvalidator();
  ConnectCore();
  StartRefreshScheduler();
  EnableInvalidationListener();

  const invalidation::DirectInvalidation inv =
      FireInvalidation(V(12), "test_payload");
  FastForwardByInvalidationDelay();

  EXPECT_TRUE(ClientInvalidationInfoMatches(inv));
  EXPECT_EQ(GetPolicyRefreshCountAndReset(), 1);
  EXPECT_EQ(0, invalidator()->highest_handled_invalidation_version());

  StorePolicy(V(12));

  EXPECT_TRUE(ClientInvalidationInfoIsUnset());
  EXPECT_EQ(V(12), invalidator()->highest_handled_invalidation_version());
}

TEST_F(CloudPolicyInvalidatorTest, HandlesMultipleInvalidations) {
  StorePolicy();
  StartInvalidator();
  ConnectCore();
  StartRefreshScheduler();
  EnableInvalidationListener();

  // Fire invalidations out of order.
  const invalidation::DirectInvalidation inv2 = FireInvalidation(V(2), "test1");
  const invalidation::DirectInvalidation inv1 = FireInvalidation(V(1), "test2");
  const invalidation::DirectInvalidation inv3 = FireInvalidation(V(3), "test3");
  FastForwardByInvalidationDelay();

  // Make sure the policy is refreshed once.
  EXPECT_TRUE(ClientInvalidationInfoMatches(inv3));
  EXPECT_EQ(GetPolicyRefreshCountAndReset(), 1);
  EXPECT_EQ(0, invalidator()->highest_handled_invalidation_version());

  // Make sure that the invalidation data is only removed from the client after
  // the store is loaded with the latest version.
  StorePolicy(V(1));
  EXPECT_TRUE(ClientInvalidationInfoMatches(inv3));
  EXPECT_EQ(V(1), invalidator()->highest_handled_invalidation_version());

  StorePolicy(V(2));
  EXPECT_TRUE(ClientInvalidationInfoMatches(inv3));
  EXPECT_EQ(V(2), invalidator()->highest_handled_invalidation_version());

  StorePolicy(V(3));
  EXPECT_TRUE(ClientInvalidationInfoIsUnset());
  EXPECT_EQ(V(3), invalidator()->highest_handled_invalidation_version());
}

TEST_F(CloudPolicyInvalidatorTest, IgnoresOldInvalidations) {
  StorePolicy();
  ConnectCore();
  StartRefreshScheduler();
  EnableInvalidationListener();
  StartInvalidator();

  const invalidation::DirectInvalidation inv0 = FireInvalidation(V(2), "test2");
  FastForwardByInvalidationDelay();
  EXPECT_EQ(GetPolicyRefreshCountAndReset(), 1);
  EXPECT_TRUE(ClientInvalidationInfoMatches(inv0));
  StorePolicy(V(2));
  EXPECT_EQ(V(2), invalidator()->highest_handled_invalidation_version());

  // Check that an invalidation whose version is lower than the highest handled
  // so far is acknowledged but ignored otherwise.
  const invalidation::DirectInvalidation inv1 = FireInvalidation(V(1), "test1");
  FastForwardByInvalidationDelay();

  EXPECT_EQ(GetPolicyRefreshCountAndReset(), 0);
  EXPECT_TRUE(ClientInvalidationInfoIsUnset());
  EXPECT_EQ(V(2), invalidator()->highest_handled_invalidation_version());

  // Check that an invalidation whose version matches the highest handled so far
  // is acknowledged but ignored otherwise.
  const invalidation::DirectInvalidation inv2 = FireInvalidation(V(2), "test2");
  FastForwardByInvalidationDelay();

  EXPECT_EQ(GetPolicyRefreshCountAndReset(), 0);
  EXPECT_TRUE(ClientInvalidationInfoIsUnset());
  EXPECT_EQ(V(2), invalidator()->highest_handled_invalidation_version());

  // Check that an invalidation whose version is higher than the highest handled
  // so far is handled, causing a policy refresh.
  const invalidation::DirectInvalidation inv3 = FireInvalidation(V(3), "test3");
  FastForwardByInvalidationDelay();

  EXPECT_EQ(GetPolicyRefreshCountAndReset(), 1);
  EXPECT_TRUE(ClientInvalidationInfoMatches(inv3));

  StorePolicy(V(3));
  EXPECT_TRUE(ClientInvalidationInfoIsUnset());
  EXPECT_EQ(V(3), invalidator()->highest_handled_invalidation_version());
}

TEST_F(CloudPolicyInvalidatorTest, NoticesRegularPolicyRefresh) {
  StorePolicy();
  ConnectCore();
  StartRefreshScheduler();
  EnableInvalidationListener();
  StartInvalidator();

  EXPECT_EQ(0, invalidator()->highest_handled_invalidation_version());

  StorePolicy(V(2));

  EXPECT_EQ(V(2), invalidator()->highest_handled_invalidation_version());
}

class CloudPolicyInvalidatorOwnerNameTest
    : public CloudPolicyInvalidatorTestBase {
 protected:
  PolicyInvalidationScope GetPolicyInvalidationScope() const override {
    return scope_;
  }

  PolicyInvalidationScope scope_;
};

TEST_F(CloudPolicyInvalidatorOwnerNameTest, GetTypeForUserScope) {
  scope_ = PolicyInvalidationScope::kUser;
  StartInvalidator();
  ASSERT_TRUE(invalidator());
  EXPECT_EQ("USER_POLICY_FETCH", invalidator()->GetType());
}

TEST_F(CloudPolicyInvalidatorOwnerNameTest, GetTypeForDeviceScope) {
  scope_ = PolicyInvalidationScope::kDevice;
  StartInvalidator();
  ASSERT_TRUE(invalidator());
  EXPECT_EQ("DEVICE_POLICY_FETCH", invalidator()->GetType());
}

TEST_F(CloudPolicyInvalidatorOwnerNameTest, GetTypeForDeviceLocalAccountScope) {
  scope_ = PolicyInvalidationScope::kDeviceLocalAccount;
  StartInvalidator();
  ASSERT_TRUE(invalidator());
  EXPECT_EQ("PUBLIC_ACCOUNT_POLICY_FETCH-test_account",
            invalidator()->GetType());
}

TEST_F(CloudPolicyInvalidatorOwnerNameTest, GetTypeForCbcmScope) {
  scope_ = PolicyInvalidationScope::kCBCM;
  StartInvalidator();
  ASSERT_TRUE(invalidator());
  EXPECT_EQ("BROWSER_POLICY_FETCH", invalidator()->GetType());
}

class CloudPolicyInvalidatorUserTypedTest
    : public CloudPolicyInvalidatorTestBase,
      public testing::WithParamInterface<PolicyInvalidationScope> {
 public:
  CloudPolicyInvalidatorUserTypedTest(
      const CloudPolicyInvalidatorUserTypedTest&) = delete;
  CloudPolicyInvalidatorUserTypedTest& operator=(
      const CloudPolicyInvalidatorUserTypedTest&) = delete;

 protected:
  CloudPolicyInvalidatorUserTypedTest();

  base::HistogramBase::Count32 GetCount(MetricPolicyRefresh metric);
  base::HistogramBase::Count32 GetInvalidationCount(
      PolicyInvalidationType type);

 private:
  // CloudPolicyInvalidatorTest:
  PolicyInvalidationScope GetPolicyInvalidationScope() const override;

  base::HistogramTester histogram_tester_;
};

CloudPolicyInvalidatorUserTypedTest::CloudPolicyInvalidatorUserTypedTest() {
  ConnectCore();
  StartRefreshScheduler();
}

base::HistogramBase::Count32 CloudPolicyInvalidatorUserTypedTest::GetCount(
    MetricPolicyRefresh metric) {
  const char* metric_name = CloudPolicyInvalidator::GetPolicyRefreshMetricName(
      GetPolicyInvalidationScope());
  return histogram_tester_.GetHistogramSamplesSinceCreation(metric_name)
      ->GetCount(metric);
}

base::HistogramBase::Count32
CloudPolicyInvalidatorUserTypedTest::GetInvalidationCount(
    PolicyInvalidationType type) {
  const char* metric_name =
      CloudPolicyInvalidator::GetPolicyInvalidationMetricName(
          GetPolicyInvalidationScope());
  return histogram_tester_.GetHistogramSamplesSinceCreation(metric_name)
      ->GetCount(type);
}

PolicyInvalidationScope
CloudPolicyInvalidatorUserTypedTest::GetPolicyInvalidationScope() const {
  return GetParam();
}

TEST_P(CloudPolicyInvalidatorUserTypedTest,
       RefreshMetricsInvalidationsDisabled) {
  StartInvalidator();
  DisableInvalidationListener();

  StorePolicy(0, /*policy_changed=*/false);
  StorePolicy(0, /*policy_changed=*/true);

  EXPECT_EQ(0, GetCount(METRIC_POLICY_REFRESH_CHANGED));
  EXPECT_EQ(2, GetCount(METRIC_POLICY_REFRESH_CHANGED_NO_INVALIDATIONS));
  EXPECT_EQ(0, GetCount(METRIC_POLICY_REFRESH_UNCHANGED));
  EXPECT_EQ(0, GetCount(METRIC_POLICY_REFRESH_INVALIDATED_CHANGED));
  EXPECT_EQ(0, GetCount(METRIC_POLICY_REFRESH_INVALIDATED_UNCHANGED));

  EXPECT_EQ(0, invalidator()->highest_handled_invalidation_version());
}

TEST_P(CloudPolicyInvalidatorUserTypedTest, RefreshMetricsNoInvalidations) {
  // Store loads should be differentiated depending on whether the invalidation
  // service was enabled or not.
  StorePolicy();
  StartInvalidator();
  EnableInvalidationListener();

  // Initially, invalidations have not been enabled past the grace period, so
  // invalidations are OFF.
  StorePolicy(0, /*policy_changed=*/false);
  StorePolicy(0, /*policy_changed=*/true);
  EXPECT_EQ(1, GetCount(METRIC_POLICY_REFRESH_UNCHANGED));
  EXPECT_EQ(1, GetCount(METRIC_POLICY_REFRESH_CHANGED_NO_INVALIDATIONS));

  // If the clock advances less than the grace period, invalidations are OFF.
  FastForwardBy(base::Seconds(1));
  StorePolicy(0, /*policy_changed=*/false);
  StorePolicy(0, /*policy_changed=*/true);
  EXPECT_EQ(2, GetCount(METRIC_POLICY_REFRESH_UNCHANGED));
  EXPECT_EQ(2, GetCount(METRIC_POLICY_REFRESH_CHANGED_NO_INVALIDATIONS));

  // After the grace period elapses, invalidations are ON.
  FastForwardBy(CloudPolicyInvalidator::kInvalidationGracePeriod);
  StorePolicy(0, /*policy_changed=*/false);
  StorePolicy(0, /*policy_changed=*/true);
  EXPECT_EQ(3, GetCount(METRIC_POLICY_REFRESH_UNCHANGED));
  EXPECT_EQ(1, GetCount(METRIC_POLICY_REFRESH_CHANGED));

  // After the invalidation service is disabled, invalidations are OFF.
  DisableInvalidationListener();
  StorePolicy(0, /*policy_changed=*/false);
  StorePolicy(0, /*policy_changed=*/true);
  EXPECT_EQ(4, GetCount(METRIC_POLICY_REFRESH_UNCHANGED));
  EXPECT_EQ(3, GetCount(METRIC_POLICY_REFRESH_CHANGED_NO_INVALIDATIONS));

  // Enabling the invalidation service results in a new grace period, so
  // invalidations are OFF.
  EnableInvalidationListener();
  StorePolicy(0, /*policy_changed=*/false);
  StorePolicy(0, /*policy_changed=*/true);
  EXPECT_EQ(5, GetCount(METRIC_POLICY_REFRESH_UNCHANGED));
  EXPECT_EQ(4, GetCount(METRIC_POLICY_REFRESH_CHANGED_NO_INVALIDATIONS));

  // After the grace period elapses, invalidations are ON.
  FastForwardBy(CloudPolicyInvalidator::kInvalidationGracePeriod);
  StorePolicy(0, /*policy_changed=*/false);
  StorePolicy(0, /*policy_changed=*/true);

  EXPECT_EQ(2, GetCount(METRIC_POLICY_REFRESH_CHANGED));
  EXPECT_EQ(4, GetCount(METRIC_POLICY_REFRESH_CHANGED_NO_INVALIDATIONS));
  EXPECT_EQ(6, GetCount(METRIC_POLICY_REFRESH_UNCHANGED));
  EXPECT_EQ(0, GetCount(METRIC_POLICY_REFRESH_INVALIDATED_CHANGED));
  EXPECT_EQ(0, GetCount(METRIC_POLICY_REFRESH_INVALIDATED_UNCHANGED));

  EXPECT_EQ(0, invalidator()->highest_handled_invalidation_version());
}

TEST_P(CloudPolicyInvalidatorUserTypedTest, RefreshMetricsInvalidation) {
  StartInvalidator();
  EnableInvalidationListener();

  StorePolicy();
  EXPECT_EQ(1, GetCount(METRIC_POLICY_REFRESH_CHANGED_NO_INVALIDATIONS));

  FastForwardBy(CloudPolicyInvalidator::kInvalidationGracePeriod);
  FireInvalidation(V(5), "test");
  FastForwardByInvalidationDelay();

  StorePolicy(0, /*policy_changed=*/false);
  EXPECT_EQ(1, GetCount(METRIC_POLICY_REFRESH_UNCHANGED));

  StorePolicy(0, /*policy_changed=*/true);
  EXPECT_EQ(1, GetCount(METRIC_POLICY_REFRESH_CHANGED));
  EXPECT_EQ(0, invalidator()->highest_handled_invalidation_version());

  StorePolicy(V(5), true /* policy_changed */);
  EXPECT_EQ(1, GetCount(METRIC_POLICY_REFRESH_INVALIDATED_CHANGED));
  EXPECT_EQ(V(5), invalidator()->highest_handled_invalidation_version());

  // Store loads after the invalidation is complete are not counted as
  // invalidated.
  StorePolicy(V(5), /*policy_changed=*/false);
  StorePolicy(V(6), /*policy_changed=*/true);
  StorePolicy(V(6), /*policy_changed=*/false);
  StorePolicy(V(7), /*policy_changed=*/true);
  StorePolicy(V(7), /*policy_changed=*/false);
  StorePolicy(V(8), /*policy_changed=*/true);
  StorePolicy(V(8), /*policy_changed=*/false);

  EXPECT_EQ(4, GetCount(METRIC_POLICY_REFRESH_CHANGED));
  EXPECT_EQ(1, GetCount(METRIC_POLICY_REFRESH_CHANGED_NO_INVALIDATIONS));
  EXPECT_EQ(5, GetCount(METRIC_POLICY_REFRESH_UNCHANGED));
  EXPECT_EQ(1, GetCount(METRIC_POLICY_REFRESH_INVALIDATED_CHANGED));
  EXPECT_EQ(0, GetCount(METRIC_POLICY_REFRESH_INVALIDATED_UNCHANGED));

  EXPECT_EQ(V(8), invalidator()->highest_handled_invalidation_version());
}

TEST_P(CloudPolicyInvalidatorUserTypedTest, ExpiredInvalidations) {
  FastForwardBy(base::Hours(1));
  const auto policy_store_timestamp = Now();
  StorePolicy(0, false, policy_store_timestamp);
  StartInvalidator();
  EnableInvalidationListener();

  // Invalidations fired before the last fetch time (adjusted by max time delta)
  // should be ignored (and count as expired).
  const base::Time expired_invalidation_timestamp =
      policy_store_timestamp -
      invalidation_timeouts::kMaxInvalidationTimeDelta - base::Seconds(1);

  // Fire expired invalidation with a payload.
  invalidation::DirectInvalidation inv =
      FireInvalidation(GetVersion(expired_invalidation_timestamp), "test");
  FastForwardByInvalidationDelay();
  EXPECT_TRUE(ClientInvalidationInfoIsUnset());
  EXPECT_EQ(GetPolicyRefreshCountAndReset(), 0);

  // Fire expired invalidation without a payload.
  inv = FireInvalidation(GetVersion(expired_invalidation_timestamp), "");
  FastForwardByInvalidationDelay();
  EXPECT_EQ(GetPolicyRefreshCountAndReset(), 0);

  // Invalidations fired after the last fetch should not be ignored.
  const base::Time non_expired_invalidaiton_timestamp =
      expired_invalidation_timestamp + base::Seconds(1);

  // Fire a fine invalidation without a payload.
  inv = FireInvalidation(GetVersion(non_expired_invalidaiton_timestamp), "");
  FastForwardByInvalidationDelay();
  EXPECT_TRUE(ClientInvalidationInfoMatches(inv));
  EXPECT_EQ(GetPolicyRefreshCountAndReset(), 1);

  // Fire three fine invalidations with a payload.
  inv = FireInvalidation(
      GetVersion(non_expired_invalidaiton_timestamp + base::Minutes(10)),
      "test");
  FastForwardByInvalidationDelay();
  EXPECT_TRUE(ClientInvalidationInfoMatches(inv));
  EXPECT_EQ(GetPolicyRefreshCountAndReset(), 1);

  inv = FireInvalidation(
      GetVersion(non_expired_invalidaiton_timestamp + base::Minutes(20)),
      "test");
  FastForwardByInvalidationDelay();
  EXPECT_TRUE(ClientInvalidationInfoMatches(inv));
  EXPECT_EQ(GetPolicyRefreshCountAndReset(), 1);

  inv = FireInvalidation(
      GetVersion(non_expired_invalidaiton_timestamp + base::Minutes(30)),
      "test");
  FastForwardByInvalidationDelay();
  EXPECT_TRUE(ClientInvalidationInfoMatches(inv));
  EXPECT_EQ(GetPolicyRefreshCountAndReset(), 1);

  // Verify that received invalidations metrics are correct.
  EXPECT_EQ(1, GetInvalidationCount(POLICY_INVALIDATION_TYPE_NO_PAYLOAD));
  EXPECT_EQ(3, GetInvalidationCount(POLICY_INVALIDATION_TYPE_NORMAL));
  EXPECT_EQ(1,
            GetInvalidationCount(POLICY_INVALIDATION_TYPE_NO_PAYLOAD_EXPIRED));
  EXPECT_EQ(1, GetInvalidationCount(POLICY_INVALIDATION_TYPE_EXPIRED));

  // New policies never stored, verify invalidation handling did not finished.
  EXPECT_EQ(0, invalidator()->highest_handled_invalidation_version());
}

INSTANTIATE_TEST_SUITE_P(
    CloudPolicyInvalidatorUserTypedTestInstance,
    CloudPolicyInvalidatorUserTypedTest,
    testing::Values(PolicyInvalidationScope::kUser,
                    PolicyInvalidationScope::kDevice,
                    PolicyInvalidationScope::kDeviceLocalAccount));

}  // namespace policy
