// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/ukm/ukm_service.h"

#include <algorithm>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "base/containers/flat_map.h"
#include "base/containers/flat_set.h"
#include "base/containers/to_vector.h"
#include "base/functional/bind.h"
#include "base/hash/hash.h"
#include "base/metrics/metrics_hashes.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/test_simple_task_runner.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "components/metrics/cloned_install_detector.h"
#include "components/metrics/log_decoder.h"
#include "components/metrics/metrics_features.h"
#include "components/metrics/metrics_log.h"
#include "components/metrics/metrics_log_uploader.h"
#include "components/metrics/metrics_pref_names.h"
#include "components/metrics/metrics_reporting_choice_service.h"
#include "components/metrics/test/test_metrics_provider.h"
#include "components/metrics/test/test_metrics_service_client.h"
#include "components/metrics/ukm_demographic_metrics_provider.h"
#include "components/metrics/unsent_log_store.h"
#include "components/prefs/testing_pref_service.h"
#include "components/ukm/observers/ukm_consent_state_observer.h"
#include "components/ukm/test_ukm_recorder.h"
#include "components/ukm/ukm_pref_names.h"
#include "components/ukm/ukm_recorder_impl.h"
#include "components/ukm/ukm_recorder_observer.h"
#include "components/ukm/unsent_log_store_metrics_impl.h"
#include "services/metrics/public/cpp/mojo_ukm_recorder.h"
#include "services/metrics/public/cpp/test_recording_helper.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_entry_builder.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "services/metrics/public/cpp/ukm_source.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "services/metrics/ukm_recorder_factory_impl.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/metrics_proto/ukm/report.pb.h"
#include "third_party/metrics_proto/ukm/source.pb.h"
#include "third_party/metrics_proto/ukm/web_features.pb.h"
#include "third_party/metrics_proto/user_demographics.pb.h"

namespace ukm {

// Some arbitrary events used in tests.
using TestEvent1 = builders::PageLoad;
const char* kTestEvent1Metric1 =
    TestEvent1::kPaintTiming_NavigationToFirstContentfulPaintName;
const char* kTestEvent1Metric2 = TestEvent1::kNet_CacheBytes2Name;
using TestEvent2 = builders::Memory_Experimental;
const char* kTestEvent2Metric1 = TestEvent2::kArrayBufferName;
const char* kTestEvent2Metric2 = TestEvent2::kBlinkGCName;
using TestEvent3 = builders::PageWithPassword;
using TestProviderEvent = builders::ScreenBrightness;
const int32_t kWebDXFeature1 = 1;
const int32_t kWebDXFeature3 = 3;
const size_t kWebDXFeatureNumberOfFeaturesForTesting = 5;

SourceId ConvertSourceIdToAllowlistedType(SourceId id, SourceIdType type) {
  return ukm::SourceIdObj::FromOtherId(id, type).ToInt64();
}

class TestMetricsServiceClientWithClonedInstallDetector
    : public metrics::TestMetricsServiceClient {
 public:
  TestMetricsServiceClientWithClonedInstallDetector() = default;

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

  ~TestMetricsServiceClientWithClonedInstallDetector() override = default;

  // metrics::MetricsServiceClient:
  base::CallbackListSubscription AddOnClonedInstallDetectedCallback(
      base::OnceClosure callback) override {
    return cloned_install_detector_.AddOnClonedInstallDetectedCallback(
        std::move(callback));
  }

  metrics::ClonedInstallDetector* cloned_install_detector() {
    return &cloned_install_detector_;
  }

 private:
  metrics::ClonedInstallDetector cloned_install_detector_;
};

namespace {

bool TestIsWebstoreExtension(std::string_view id) {
  return (id == "bhcnanendmgjjeghamaccjnochlnhcgj");
}

int GetPersistedLogCount(TestingPrefServiceSimple& prefs) {
  return prefs.GetList(prefs::kUkmUnsentLogStore).size();
}

Report GetPersistedReport(TestingPrefServiceSimple& prefs) {
  EXPECT_GE(GetPersistedLogCount(prefs), 1);
  metrics::UnsentLogStore result_unsent_log_store(
      std::make_unique<UnsentLogStoreMetricsImpl>(), &prefs,
      prefs::kUkmUnsentLogStore, /*metadata_pref_name=*/nullptr,
      // Set to 3 so logs are not dropped in the test.
      metrics::UnsentLogStore::UnsentLogStoreLimits{
          .min_log_count = 3,
      },
      /*signing_key=*/std::string(),
      /*logs_event_manager=*/nullptr);

  result_unsent_log_store.LoadPersistedUnsentLogs();
  result_unsent_log_store.StageNextLog();

  Report report;
  EXPECT_TRUE(metrics::DecodeLogDataToProto(
      result_unsent_log_store.staged_log(), &report));
  return report;
}

metrics::LogMetadata GetPersistedLogMetadata(TestingPrefServiceSimple& prefs) {
  EXPECT_GE(GetPersistedLogCount(prefs), 1);
  metrics::UnsentLogStore result_unsent_log_store(
      std::make_unique<UnsentLogStoreMetricsImpl>(), &prefs,
      prefs::kUkmUnsentLogStore, /*metadata_pref_name=*/nullptr,
      // Set to 3 so logs are not dropped in the test.
      metrics::UnsentLogStore::UnsentLogStoreLimits{
          .min_log_count = 3,
      },
      /*signing_key=*/std::string(),
      /*logs_event_manager=*/nullptr);

  result_unsent_log_store.LoadPersistedUnsentLogs();
  result_unsent_log_store.StageNextLog();

  return result_unsent_log_store.staged_log_metadata();
}

void AddSourceToReport(Report& report,
                       int64_t other_id,
                       SourceIdType id_type,
                       std::string url) {
  Source* proto_source = report.add_sources();
  SourceId source_id = ConvertToSourceId(other_id, id_type);
  proto_source->set_id(source_id);
  proto_source->add_urls()->set_url(url);
  // Add entry for the source.
  Entry* entry = report.add_entries();
  entry->set_source_id(source_id);
}

bool WebDXFeaturesStrictlyContains(const HighLevelWebFeatures& actual_features,
                                   const std::set<int32_t>& expected_features) {
  BitSet bitset(kWebDXFeatureNumberOfFeaturesForTesting,
                actual_features.bit_vector());
  for (size_t i = 0; i < kWebDXFeatureNumberOfFeaturesForTesting; ++i) {
    if (bitset.Contains(i) != expected_features.contains(i)) {
      return false;
    }
  }
  return true;
}

class ScopedUkmFeatureParams {
 public:
  explicit ScopedUkmFeatureParams(const base::FieldTrialParams& params) {
    scoped_feature_list_.InitAndEnableFeatureWithParameters(kUkmFeature,
                                                            params);
  }

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

  ~ScopedUkmFeatureParams() = default;

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

class MockDemographicMetricsProvider
    : public metrics::UkmDemographicMetricsProvider {
 public:
  ~MockDemographicMetricsProvider() override = default;

  // DemographicMetricsProvider:
  MOCK_METHOD1(ProvideSyncedUserNoisedBirthYearAndGenderToReport,
               void(Report* report));
};

class MockUkmRecorderObserver : public UkmRecorder::Observer {
 public:
  MOCK_METHOD0(OnStartingShutdown, void());
};

// A simple Provider that emits a 'TestProviderEvent' on session close (i.e. a
// Report being emitted).
class UkmTestMetricsProvider : public metrics::TestMetricsProvider {
 public:
  explicit UkmTestMetricsProvider(UkmRecorder* test_recording_helper)
      : test_recording_helper_(test_recording_helper) {}

  void ProvideCurrentSessionUKMData() override {
    // An Event emitted during a Provider will frequently not not associated
    // with a URL.
    SourceId id = ukm::NoURLSourceId();
    TestProviderEvent(id).Record(test_recording_helper_);
  }

 private:
  raw_ptr<UkmRecorder> test_recording_helper_;
};

class UkmServiceTest : public testing::Test {
 public:
  UkmServiceTest()
      : task_runner_(new base::TestSimpleTaskRunner),
        task_runner_current_default_handle_(task_runner_) {
    UkmService::RegisterPrefs(prefs_.registry());
    ClearPrefs();
  }

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

  void ClearPrefs() {
    prefs_.ClearPref(prefs::kUkmClientId);
    prefs_.ClearPref(prefs::kUkmSessionId);
    prefs_.ClearPref(prefs::kUkmUnsentLogStore);
  }

  int GetPersistedLogCount() { return ukm::GetPersistedLogCount(prefs_); }

  Report GetPersistedReport() { return ukm::GetPersistedReport(prefs_); }

  metrics::LogMetadata GetPersistedLogMetadata() {
    return ukm::GetPersistedLogMetadata(prefs_);
  }

  static SourceId GetAllowlistedSourceId(int64_t id) {
    return ConvertToSourceId(id, SourceIdType::NAVIGATION_ID);
  }

  static SourceId GetAppIDSourceId(int64_t id) {
    return ConvertToSourceId(id, SourceIdType::APP_ID);
  }

  static SourceId GetNonAllowlistedSourceId(int64_t id) {
    return ConvertToSourceId(id, SourceIdType::DEFAULT);
  }

 protected:
  TestingPrefServiceSimple prefs_;
  metrics::TestMetricsServiceClient client_;

  scoped_refptr<base::TestSimpleTaskRunner> task_runner_;
  base::SingleThreadTaskRunner::CurrentDefaultHandle
      task_runner_current_default_handle_;
};

class UkmReduceAddEntryIpcTest : public testing::Test {
 public:
  UkmReduceAddEntryIpcTest() {
    UkmService::RegisterPrefs(prefs_.registry());
    ClearPrefs();
    scoped_feature_list_.InitAndEnableFeature(ukm::kUkmReduceAddEntryIPC);
  }

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

  void ClearPrefs() {
    prefs_.ClearPref(prefs::kUkmClientId);
    prefs_.ClearPref(prefs::kUkmSessionId);
    prefs_.ClearPref(prefs::kUkmUnsentLogStore);
  }

 protected:
  TestingPrefServiceSimple prefs_;
  metrics::TestMetricsServiceClient client_;
  base::test::ScopedFeatureList scoped_feature_list_;

 private:
  base::test::TaskEnvironment task_environment;
};

struct UrlTruncationTestCase {
  std::string name;
  std::string url;
  std::string expected_url;
  ukm::TruncationStatus expected_status;
};

std::vector<UrlTruncationTestCase> GetTruncationTestCases() {
  std::vector<UrlTruncationTestCase> cases;

  const std::string path_string =
      "https://example.com/" + std::string(10000, 'a');
  cases.push_back({"LongUrlWithoutQuery", path_string,
                   path_string.substr(0, 2048),
                   ukm::TRUNCATED_AT_URL_WITHOUT_QUERY});

  const std::string query_string =
      "https://example.com/a?q=" + std::string(10000, 'a');
  cases.push_back({"LongQuery", query_string, query_string.substr(0, 2048),
                   ukm::TRUNCATED_AT_FULL_PATH});

  cases.push_back({"ShortURL", "https://example.com/short",
                   "https://example.com/short", ukm::NOT_TRUNCATED});

  const std::string origin_string =
      "https://" + std::string(2500, 'a') + ".com/";
  cases.push_back({"LongOrigin", origin_string, origin_string.substr(0, 2048),
                   ukm::TRUNCATED_AT_ORIGIN});

  const std::string expected_origin_boundary =
      "https://" + std::string(2036, 'a') + ".com";
  const std::string origin_boundary_string = expected_origin_boundary + "/";
  cases.push_back({"OriginBoundary", origin_boundary_string,
                   expected_origin_boundary, ukm::TRUNCATED_AT_ORIGIN});

  const std::string exactly_limit_string =
      "https://example.com/" + std::string(2028, 'a');
  cases.push_back({"ExactlyLimit", exactly_limit_string, exactly_limit_string,
                   ukm::NOT_TRUNCATED});

  const std::string just_above_limit_string =
      "https://example.com/" + std::string(2030, 'a');
  cases.push_back({"JustAboveLimit", just_above_limit_string,
                   just_above_limit_string.substr(0, 2048),
                   ukm::TRUNCATED_AT_URL_WITHOUT_QUERY});

  const std::string expected_cutoff_after =
      "https://example.com/" + std::string(2027, 'a') + "?";
  const std::string cutoff_after_delimiter_string =
      expected_cutoff_after + "q=c";
  cases.push_back({"CutoffAfterDelimiter", cutoff_after_delimiter_string,
                   expected_cutoff_after, ukm::TRUNCATED_AT_FULL_PATH});

  const std::string expected_cutoff_at =
      "https://example.com/" + std::string(2028, 'a');
  const std::string cutoff_at_delimiter_string = expected_cutoff_at + "?q=c";
  cases.push_back({"CutoffAtDelimiter", cutoff_at_delimiter_string,
                   expected_cutoff_at, ukm::TRUNCATED_AT_FULL_PATH});

  const std::string expected_cutoff_before =
      "https://example.com/" + std::string(2028, 'a');
  const std::string cutoff_before_delimiter_string =
      expected_cutoff_before + "b?q=c";
  cases.push_back({"CutoffBeforeDelimiter", cutoff_before_delimiter_string,
                   expected_cutoff_before,
                   ukm::TRUNCATED_AT_URL_WITHOUT_QUERY});

  return cases;
}
}  // namespace

TEST_F(UkmServiceTest, ClientIdClonedInstall) {
  prefs_.SetUint64(prefs::kUkmClientId, 123);
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());

  EXPECT_FALSE(client_.ShouldResetClientIdsOnClonedInstall());
  client_.set_should_reset_client_ids_on_cloned_install(true);
  EXPECT_TRUE(client_.ShouldResetClientIdsOnClonedInstall());

  uint64_t original_id = prefs_.GetUint64(prefs::kUkmClientId);
  service.Initialize();
  uint64_t new_id = prefs_.GetUint64(prefs::kUkmClientId);
  EXPECT_NE(original_id, new_id);
}

TEST_F(UkmServiceTest, EnableDisableSchedule) {
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  EXPECT_FALSE(task_runner_->HasPendingTask());
  service.Initialize();
  EXPECT_FALSE(task_runner_->HasPendingTask());
  service.UpdateRecording({UkmConsentType::MSBB});
  service.EnableReporting();
  EXPECT_TRUE(task_runner_->HasPendingTask());
  service.DisableReporting();
  task_runner_->RunPendingTasks();
  EXPECT_FALSE(task_runner_->HasPendingTask());
}

TEST_F(UkmServiceTest, PersistAndPurge) {
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  EXPECT_EQ(GetPersistedLogCount(), 0);
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB});
  service.EnableReporting();

  SourceId id = GetAllowlistedSourceId(0);
  recorder.UpdateSourceURL(id, GURL("https://google.com/foobar"));
  // Should init, generate a log, and start an upload for source.
  task_runner_->RunPendingTasks();
  EXPECT_TRUE(client_.uploader()->is_uploading());
  // Flushes the generated log to disk and generates a new entry.
  TestEvent1(id).Record(&service);
  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(GetPersistedLogCount(), 2);
  service.Purge();
  EXPECT_EQ(GetPersistedLogCount(), 0);
}

TEST_F(UkmServiceTest, Purge) {
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  EXPECT_EQ(GetPersistedLogCount(), 0);
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB});
  service.EnableReporting();

  // Record some data
  auto id = GetAllowlistedSourceId(0);
  recorder.UpdateSourceURL(id, GURL("https://google.com/foobar1"));
  TestEvent1(id).Record(&service);

  // Purge should delete data, so there shouldn't be anything left to upload.
  service.Purge();
  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(0, GetPersistedLogCount());
}

TEST_F(UkmServiceTest, PurgeExtensionDataFromUnsentLogStore) {
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  auto* unsent_log_store = service.reporting_service_.ukm_log_store();

  // Initialize a Report to be saved to the log store.
  Report report;
  report.set_client_id(1);
  report.set_session_id(1);
  report.set_report_id(1);
  report.mutable_system_profile()->set_app_version(client_.GetVersionString());

  std::string non_extension_url = "https://www.google.ca";
  std::string extension_url =
      "chrome-extension://bmnlcjabgnpnenekpadlanbbkooimhnj/manifest.json";

  // Add both extension- and non-extension-related sources to the Report.
  Source* proto_source_1 = report.add_sources();
  SourceId source_id_1 = ConvertToSourceId(1, SourceIdType::NAVIGATION_ID);
  proto_source_1->set_id(source_id_1);
  proto_source_1->add_urls()->set_url(non_extension_url);
  Source* proto_source_2 = report.add_sources();
  SourceId source_id_2 = ConvertToSourceId(2, SourceIdType::NAVIGATION_ID);
  proto_source_2->set_id(source_id_2);
  proto_source_2->add_urls()->set_url(extension_url);

  // Add entries related to the sources.
  Entry* entry_1 = report.add_entries();
  entry_1->set_source_id(source_id_2);
  Entry* entry_2 = report.add_entries();
  entry_2->set_source_id(source_id_1);
  Entry* entry_3 = report.add_entries();
  entry_3->set_source_id(source_id_1);

  // Add web features related to the sources.
  HighLevelWebFeatures* features_1 = report.add_web_features();
  features_1->set_source_id(source_id_1);
  HighLevelWebFeatures* features_2 = report.add_web_features();
  features_2->set_source_id(source_id_2);

  // Save the Report to the store.
  std::string serialized_log;
  report.SerializeToString(&serialized_log);
  // Makes sure that the serialized ukm report can be parsed.
  ASSERT_TRUE(UkmService::LogCanBeParsed(serialized_log));
  metrics::LogMetadata log_metadata;
  unsent_log_store->StoreLog(
      serialized_log, log_metadata,
      metrics::MetricsLogsEventManager::CreateReason::kUnknown);

  // Do extension purging.
  service.PurgeExtensionsData();

  // Get the Report in the log store and verify extension-related data have been
  // filtered.
  unsent_log_store->StageNextLog();
  const std::string& compressed_log_data = unsent_log_store->staged_log();

  Report filtered_report;
  ASSERT_TRUE(
      metrics::DecodeLogDataToProto(compressed_log_data, &filtered_report));

  // The logs app version and the client_ version string should be the same,
  // so the log_written_by_app_version shouldn't be set.
  ASSERT_FALSE(
      filtered_report.system_profile().has_log_written_by_app_version());
  // Only proto_source_1 with non-extension URL is kept.
  EXPECT_EQ(1, filtered_report.sources_size());
  EXPECT_EQ(source_id_1, filtered_report.sources(0).id());
  EXPECT_EQ(non_extension_url, filtered_report.sources(0).urls(0).url());

  // Only entry_2 and entry_3 from the non-extension source is kept.
  EXPECT_EQ(2, filtered_report.entries_size());
  EXPECT_EQ(source_id_1, filtered_report.entries(0).source_id());
  EXPECT_EQ(source_id_1, filtered_report.entries(1).source_id());

  // Only features_1 from the non-extension source is kept (features_2 was
  // filtered out due to being associated with an extension source).
  EXPECT_EQ(1, filtered_report.web_features_size());
  EXPECT_EQ(source_id_1, filtered_report.web_features(0).source_id());
}

TEST_F(UkmServiceTest, PurgeExtensionDataFromUnsentLogStoreWithVersionChange) {
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  auto* unsent_log_store = service.reporting_service_.ukm_log_store();

  // Initialize a Report to be saved to the log store.
  Report report;
  report.set_client_id(1);
  report.set_session_id(1);
  report.set_report_id(1);
  report.mutable_system_profile()->set_app_version("0.0.0.0");

  std::string non_extension_url = "https://www.google.ca";
  std::string extension_url =
      "chrome-extension://bmnlcjabgnpnenekpadlanbbkooimhnj/manifest.json";

  // Add both extension- and non-extension-related sources to the Report.
  Source* proto_source_1 = report.add_sources();
  SourceId source_id_1 = ConvertToSourceId(1, SourceIdType::NAVIGATION_ID);
  proto_source_1->set_id(source_id_1);
  proto_source_1->add_urls()->set_url(non_extension_url);
  Source* proto_source_2 = report.add_sources();
  SourceId source_id_2 = ConvertToSourceId(2, SourceIdType::NAVIGATION_ID);
  proto_source_2->set_id(source_id_2);
  proto_source_2->add_urls()->set_url(extension_url);

  // Add entries related to the sources.
  Entry* entry_1 = report.add_entries();
  entry_1->set_source_id(source_id_2);
  Entry* entry_2 = report.add_entries();
  entry_2->set_source_id(source_id_1);
  Entry* entry_3 = report.add_entries();
  entry_3->set_source_id(source_id_1);

  // Add web features related to the sources.
  HighLevelWebFeatures* features_1 = report.add_web_features();
  features_1->set_source_id(source_id_1);
  HighLevelWebFeatures* features_2 = report.add_web_features();
  features_2->set_source_id(source_id_2);

  // Save the Report to the store.
  std::string serialized_log;
  report.SerializeToString(&serialized_log);
  // Makes sure that the serialized ukm report can be parsed.
  ASSERT_TRUE(UkmService::LogCanBeParsed(serialized_log));
  metrics::LogMetadata log_metadata;
  unsent_log_store->StoreLog(
      serialized_log, log_metadata,
      metrics::MetricsLogsEventManager::CreateReason::kUnknown);

  // Do extension purging.
  service.PurgeExtensionsData();

  // Get the Report in the log store and verify extension-related data have been
  // filtered.
  unsent_log_store->StageNextLog();
  const std::string& compressed_log_data = unsent_log_store->staged_log();

  Report filtered_report;
  ASSERT_TRUE(
      metrics::DecodeLogDataToProto(compressed_log_data, &filtered_report));

  // The logs app version and the client_ version string should be different,
  // so the log_written_by_app_version should be set to the current client_
  // version.
  ASSERT_EQ(filtered_report.system_profile().log_written_by_app_version(),
            client_.GetVersionString());
  // Only proto_source_1 with non-extension URL is kept.
  EXPECT_EQ(1, filtered_report.sources_size());
  EXPECT_EQ(source_id_1, filtered_report.sources(0).id());
  EXPECT_EQ(non_extension_url, filtered_report.sources(0).urls(0).url());

  // Only entry_2 and entry_3 from the non-extension source is kept.
  EXPECT_EQ(2, filtered_report.entries_size());
  EXPECT_EQ(source_id_1, filtered_report.entries(0).source_id());
  EXPECT_EQ(source_id_1, filtered_report.entries(1).source_id());

  // Only features_1 from the non-extension source is kept (features_2 was
  // filtered out due to being associated with an extension source).
  EXPECT_EQ(1, filtered_report.web_features_size());
  EXPECT_EQ(source_id_1, filtered_report.web_features(0).source_id());
}

TEST_F(UkmServiceTest, PurgeAppDataFromUnsentLogStore) {
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  auto* unsent_log_store = service.reporting_service_.ukm_log_store();

  // Initialize a Report to be saved to the log store.
  Report report;
  report.set_client_id(1);
  report.set_session_id(1);
  report.set_report_id(1);

  // A URL from browser navigation.
  std::string non_app_url = "https://www.google.ca";
  // A URL with app:// scheme.
  std::string app_url = "app://mgndgikekgjfcpckkfioiadnlibdjbkf";
  // OS Settings is an app on ChromeOS without the app:// scheme.
  std::string os_settings_url = "chrome://os-settings";

  // Add sources to the Report.
  // Non-app source.
  Source* proto_source_1 = report.add_sources();
  SourceId source_id_1 = ConvertToSourceId(1, SourceIdType::NAVIGATION_ID);
  proto_source_1->set_id(source_id_1);
  proto_source_1->add_urls()->set_url(non_app_url);
  // App source with app:// URL.
  Source* proto_source_2 = report.add_sources();
  SourceId source_id_2 = ConvertToSourceId(2, SourceIdType::APP_ID);
  proto_source_2->set_id(source_id_2);
  proto_source_2->add_urls()->set_url(app_url);
  // App source with non-app:// URL.
  Source* proto_source_3 = report.add_sources();
  SourceId source_id_3 = ConvertToSourceId(3, SourceIdType::APP_ID);
  proto_source_3->set_id(source_id_3);
  proto_source_3->add_urls()->set_url(os_settings_url);
  // Non-app source with app:// URL. This shouldn't happen in practice, but
  // if it does, this source should be purged when app data are purged.
  Source* proto_source_4 = report.add_sources();
  SourceId source_id_4 = ConvertToSourceId(4, SourceIdType::NAVIGATION_ID);
  proto_source_4->set_id(source_id_4);
  proto_source_4->add_urls()->set_url(app_url);

  // Add entries to each of the sources.
  Entry* entry_1 = report.add_entries();
  entry_1->set_source_id(source_id_2);
  Entry* entry_2 = report.add_entries();
  entry_2->set_source_id(source_id_1);
  Entry* entry_3 = report.add_entries();
  entry_3->set_source_id(source_id_2);
  Entry* entry_4 = report.add_entries();
  entry_4->set_source_id(source_id_3);
  Entry* entry_5 = report.add_entries();
  entry_5->set_source_id(source_id_4);

  // Save the Report to the store.
  std::string serialized_log;
  report.SerializeToString(&serialized_log);
  // Make sure that the serialized ukm report can be parsed.
  ASSERT_TRUE(UkmService::LogCanBeParsed(serialized_log));
  metrics::LogMetadata log_metadata;
  unsent_log_store->StoreLog(
      serialized_log, log_metadata,
      metrics::MetricsLogsEventManager::CreateReason::kUnknown);

  // Do app data purging.
  service.PurgeAppsData();

  // Get the Report in the log store and verify app-related data have been
  // filtered.
  unsent_log_store->StageNextLog();
  const std::string& compressed_log_data = unsent_log_store->staged_log();

  Report filtered_report;
  ASSERT_TRUE(
      metrics::DecodeLogDataToProto(compressed_log_data, &filtered_report));

  // Only proto_source_1 with non-app URL is kept.
  EXPECT_EQ(1, filtered_report.sources_size());
  EXPECT_EQ(source_id_1, filtered_report.sources(0).id());
  EXPECT_EQ(non_app_url, filtered_report.sources(0).urls(0).url());

  // Only entry_2 from the non-app source is kept.
  EXPECT_EQ(1, filtered_report.entries_size());
  EXPECT_EQ(source_id_1, filtered_report.entries(0).source_id());
}

TEST_F(UkmServiceTest, PurgeMsbbDataFromUnsentLogStore) {
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  auto* unsent_log_store = service.reporting_service_.ukm_log_store();

  // Initialize a Report to be saved to the log store.
  Report report;
  report.set_client_id(1);
  report.set_session_id(1);
  report.set_report_id(1);

  // A URL from browser navigation.
  std::string non_app_url = "https://www.google.ca";
  std::string non_app_url2 = "https://www.google.com";
  // A URL with app:// scheme.
  std::string app_url = "app://mgndgikekgjfcpckkfioiadnlibdjbkf";
  // OS Settings is an app on ChromeOS without the app:// scheme.
  std::string os_settings_url = "chrome://os-settings";

  // Add sources to the Report.
  // Non-app source.
  Source* proto_source_1 = report.add_sources();
  SourceId source_id_1 = ConvertToSourceId(1, SourceIdType::NAVIGATION_ID);
  proto_source_1->set_id(source_id_1);
  proto_source_1->add_urls()->set_url(non_app_url);
  // Non-app source 2.
  Source* proto_source_2 = report.add_sources();
  SourceId source_id_2 = ConvertToSourceId(2, SourceIdType::NAVIGATION_ID);
  proto_source_2->set_id(source_id_2);
  proto_source_2->add_urls()->set_url(non_app_url);
  // App source with app:// URL.
  Source* proto_source_3 = report.add_sources();
  SourceId source_id_3 = ConvertToSourceId(3, SourceIdType::APP_ID);
  proto_source_3->set_id(source_id_3);
  proto_source_3->add_urls()->set_url(app_url);
  // App source with non-app:// URL.
  Source* proto_source_4 = report.add_sources();
  SourceId source_id_4 = ConvertToSourceId(4, SourceIdType::APP_ID);
  proto_source_4->set_id(source_id_4);
  proto_source_4->add_urls()->set_url(os_settings_url);
  // Non-app source with app:// URL. This shouldn't happen in practice, but
  // if it does, this source should be purged when app data are purged.
  Source* proto_source_5 = report.add_sources();
  SourceId source_id_5 = ConvertToSourceId(5, SourceIdType::NAVIGATION_ID);
  proto_source_5->set_id(source_id_5);
  proto_source_5->add_urls()->set_url(app_url);

  // Add entries to each of the sources.
  Entry* entry_1 = report.add_entries();
  entry_1->set_source_id(source_id_2);
  Entry* entry_2 = report.add_entries();
  entry_2->set_source_id(source_id_1);
  Entry* entry_3 = report.add_entries();
  entry_3->set_source_id(source_id_2);
  Entry* entry_4 = report.add_entries();
  entry_4->set_source_id(source_id_3);
  Entry* entry_5 = report.add_entries();
  entry_5->set_source_id(source_id_4);
  Entry* entry_6 = report.add_entries();
  entry_6->set_source_id(source_id_5);

  // Save the Report to the store.
  std::string serialized_log;
  report.SerializeToString(&serialized_log);
  // Make sure that the serialized ukm report can be parsed.
  ASSERT_TRUE(UkmService::LogCanBeParsed(serialized_log));
  metrics::LogMetadata log_metadata;
  unsent_log_store->StoreLog(
      serialized_log, log_metadata,
      metrics::MetricsLogsEventManager::CreateReason::kUnknown);

  // Purge MSBB data.
  service.PurgeMsbbData();

  // Get the Report in the log store and verify non-app-related data have been
  // filtered.
  unsent_log_store->StageNextLog();
  const std::string& compressed_log_data = unsent_log_store->staged_log();

  Report filtered_report;
  ASSERT_TRUE(
      metrics::DecodeLogDataToProto(compressed_log_data, &filtered_report));

  // Source proto_source_1 with app URL is kept.
  EXPECT_EQ(2, filtered_report.sources_size());
  EXPECT_EQ(source_id_4, filtered_report.sources(0).id());
  EXPECT_EQ(os_settings_url, filtered_report.sources(0).urls(0).url());

  EXPECT_EQ(source_id_3, filtered_report.sources(1).id());
  EXPECT_EQ(app_url, filtered_report.sources(1).urls(0).url());

  // Entry entry_2 from the app source is kept.
  EXPECT_EQ(2, filtered_report.entries_size());
  EXPECT_EQ(source_id_4, filtered_report.entries(0).source_id());
  EXPECT_EQ(source_id_3, filtered_report.entries(1).source_id());
}

TEST_F(UkmServiceTest, PurgeAppDataLogMetadataUpdate) {
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  auto* unsent_log_store = service.reporting_service_.ukm_log_store();

  // Initialize a Report to be saved to the log store.
  Report report;
  report.set_client_id(1);
  report.set_session_id(1);
  report.set_report_id(1);

  // A URL from browser navigation.
  std::string non_app_url = "https://www.google.ca";
  // A URL with app:// scheme.
  std::string app_url = "app://mgndgikekgjfcpckkfioiadnlibdjbkf";
  // OS Settings is an app on ChromeOS without the app:// scheme.
  std::string os_settings_url = "chrome://os-settings";

  // Add sources to the Report.
  AddSourceToReport(report, 1, SourceIdType::NAVIGATION_ID, non_app_url);
  AddSourceToReport(report, 2, SourceIdType::APP_ID, app_url);
  AddSourceToReport(report, 3, SourceIdType::APP_ID, os_settings_url);
  AddSourceToReport(report, 4, SourceIdType::NAVIGATION_ID, app_url);

  // Save the Report to the store.
  std::string serialized_log;
  report.SerializeToString(&serialized_log);

  // Make sure that the serialized ukm report can be parsed.
  ASSERT_TRUE(UkmService::LogCanBeParsed(serialized_log));

  metrics::LogMetadata log_metadata;
  log_metadata.log_source_type = metrics::UkmLogSourceType::BOTH_UKM_AND_APPKM;
  unsent_log_store->StoreLog(
      serialized_log, log_metadata,
      metrics::MetricsLogsEventManager::CreateReason::kUnknown);

  // Do app data purging.
  service.PurgeAppsData();

  // Get the Report in the log store and verify log metadata is updated.
  unsent_log_store->StageNextLog();
  const metrics::LogMetadata updated_log_metadata =
      unsent_log_store->staged_log_metadata();
  EXPECT_EQ(updated_log_metadata.log_source_type,
            metrics::UkmLogSourceType::UKM_ONLY);
}

TEST_F(UkmServiceTest, SourceSerialization) {
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  EXPECT_EQ(GetPersistedLogCount(), 0);
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB});
  service.EnableReporting();

  UkmSource::NavigationData navigation_data;
  navigation_data.urls = {GURL("https://google.com/initial"),
                          GURL("https://google.com/final")};

  SourceId id = GetAllowlistedSourceId(0);
  recorder.RecordNavigation(id, navigation_data);

  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(GetPersistedLogCount(), 1);

  Report proto_report = GetPersistedReport();
  EXPECT_EQ(1, proto_report.sources_size());
  EXPECT_TRUE(proto_report.has_session_id());
  const Source& proto_source = proto_report.sources(0);

  EXPECT_EQ(id, proto_source.id());
  EXPECT_EQ(GURL("https://google.com/final").spec(),
            proto_source.urls(1).url());
}

TEST_F(UkmServiceTest, SourceSerializationForAllowlistedButNonNavigationType) {
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  EXPECT_EQ(GetPersistedLogCount(), 0);
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB});
  service.EnableReporting();

  const GURL kURL("https://example.com/");

  SourceId id = ConvertToSourceId(0, SourceIdType::NOTIFICATION_ID);
  recorder.UpdateSourceURL(id, kURL);

  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(GetPersistedLogCount(), 1);

  Report proto_report = GetPersistedReport();
  ASSERT_EQ(1, proto_report.sources_size());
  EXPECT_TRUE(proto_report.has_session_id());
  const Source& proto_source = proto_report.sources(0);

  EXPECT_EQ(id, proto_source.id());
  EXPECT_EQ(static_cast<int>(SourceIdType::NOTIFICATION_ID), proto_source.type());
  ASSERT_EQ(1, proto_source.urls_size());
  EXPECT_EQ(kURL.spec(), proto_source.urls(0).url());
}

TEST_F(UkmServiceTest, LogMetadataOnlyAppKMSourceType) {
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  EXPECT_EQ(GetPersistedLogCount(), 0);
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::APPS});
  service.EnableReporting();
  const GURL kAppURL("app://google.com/foobar");

  SourceId id = GetAppIDSourceId(0);
  recorder.UpdateSourceURL(id, kAppURL);

  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(GetPersistedLogCount(), 1);

  metrics::LogMetadata log_metadata = GetPersistedLogMetadata();
  EXPECT_TRUE(log_metadata.log_source_type.has_value());
  EXPECT_TRUE(log_metadata.log_source_type.value() ==
              metrics::UkmLogSourceType::APPKM_ONLY);
}

TEST_F(UkmServiceTest, LogMetadataOnlyUKMSourceType) {
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  EXPECT_EQ(GetPersistedLogCount(), 0);
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB, UkmConsentType::APPS});
  service.EnableReporting();
  const GURL kURL("https://google.com/foobar");

  SourceId id = GetAllowlistedSourceId(0);
  recorder.UpdateSourceURL(id, kURL);

  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(GetPersistedLogCount(), 1);

  metrics::LogMetadata log_metadata = GetPersistedLogMetadata();
  EXPECT_TRUE(log_metadata.log_source_type.has_value());
  EXPECT_TRUE(log_metadata.log_source_type.value() ==
              metrics::UkmLogSourceType::UKM_ONLY);
}

TEST_F(UkmServiceTest, LogMetadataBothSourceType) {
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  EXPECT_EQ(GetPersistedLogCount(), 0);
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB, UkmConsentType::APPS});
  service.EnableReporting();

  const GURL kAppURL("app://google.com/foobar");

  SourceId app_id = GetAppIDSourceId(0);
  recorder.UpdateSourceURL(app_id, kAppURL);

  const GURL kURL("https://google.com/foobar");

  SourceId id = GetAllowlistedSourceId(0);
  recorder.UpdateSourceURL(id, kURL);

  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(GetPersistedLogCount(), 1);

  metrics::LogMetadata log_metadata = GetPersistedLogMetadata();
  EXPECT_TRUE(log_metadata.log_source_type.has_value() &&
              log_metadata.log_source_type.value() ==
                  metrics::UkmLogSourceType::BOTH_UKM_AND_APPKM);
}

TEST_F(UkmServiceTest, AddEntryWithEmptyMetrics) {
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  ASSERT_EQ(0, GetPersistedLogCount());
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB});
  service.EnableReporting();

  SourceId id = GetAllowlistedSourceId(0);
  recorder.UpdateSourceURL(id, GURL("https://google.com/foobar"));

  TestEvent1(id).Record(&service);
  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  ASSERT_EQ(1, GetPersistedLogCount());
  Report proto_report = GetPersistedReport();
  EXPECT_EQ(1, proto_report.entries_size());
}

TEST_F(UkmServiceTest, MetricsProviderTest) {
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);

  auto* provider = new UkmTestMetricsProvider(&service);
  service.RegisterMetricsProvider(
      std::unique_ptr<metrics::MetricsProvider>(provider));

  service.Initialize();

  // Providers have not supplied system profile information yet.
  EXPECT_FALSE(provider->provide_system_profile_metrics_called());

  task_runner_->RunUntilIdle();
  service.UpdateRecording({MSBB});
  service.EnableReporting();

  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(GetPersistedLogCount(), 1);

  Report proto_report = GetPersistedReport();
  // We should have an Event from a Provider provided metric, however, it is not
  // attached to a Source (which should be typical for a Provider metric).
  EXPECT_EQ(proto_report.sources_size(), 0);

  // Providers have now supplied system profile.
  EXPECT_TRUE(provider->provide_system_profile_metrics_called());
  // Providers has also supplied a UKM Event.
  const Entry& entry = proto_report.entries(0);
  EXPECT_EQ(base::HashMetricName(TestProviderEvent::kEntryName),
            entry.event_hash());
}

// Currently just testing brand is set, would be good to test other core
// system profile fields.
TEST_F(UkmServiceTest, SystemProfileTest) {
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);

  service.Initialize();

  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB});
  service.EnableReporting();

  SourceId id = GetAllowlistedSourceId(0);
  recorder.UpdateSourceURL(id, GURL("https://google.com/foobar"));
  TestEvent1(id).Record(&service);
  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(GetPersistedLogCount(), 1);

  Report proto_report = GetPersistedReport();
  EXPECT_EQ(metrics::TestMetricsServiceClient::kBrandForTesting,
            proto_report.system_profile().brand_code());
}

TEST_F(UkmServiceTest, RecordingEnabledWithoutConsentRestructure) {
  // 1. Consent restructure is NOT enabled.
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitAndDisableFeature(
      metrics::features::kRestructureMetricsConsentSettings);

  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  service.Initialize();

  // MSBB consented, APPS not consented.
  service.UpdateRecording({MSBB});
  service.EnableRecording();
  EXPECT_TRUE(service.recording_enabled(MSBB));
  EXPECT_FALSE(service.recording_enabled(APPS));
}

TEST_F(UkmServiceTest, RecordingEnabledWithConsentRestructure) {
  // 2. Consent restructure is enabled.
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitAndEnableFeature(
      metrics::features::kRestructureMetricsConsentSettings);

  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  service.Initialize();

  // Even if only MSBB is consented, both should be enabled if service
  // recording is enabled.
  service.UpdateRecording({MSBB});
  service.EnableRecording();
  EXPECT_TRUE(service.recording_enabled(MSBB));
  EXPECT_TRUE(service.recording_enabled(APPS));

  // If service recording is disabled, both should be disabled.
  service.DisableRecording();
  EXPECT_FALSE(service.recording_enabled(MSBB));
  EXPECT_FALSE(service.recording_enabled(APPS));
}

TEST_F(UkmServiceTest, AddUserDemograhicsWhenAvailableAndFeatureEnabled) {
  int number_of_invocations = 0;
  int test_birth_year = 1983;
  metrics::UserDemographicsProto::Gender test_gender =
      metrics::UserDemographicsProto::GENDER_FEMALE;

  auto provider = std::make_unique<MockDemographicMetricsProvider>();
  // Expect the synced user's noised birth year and gender to be added 2 times
  // to the UKM report: on the event trigger and when tearing down the UKM
  // service.
  EXPECT_CALL(*provider,
              ProvideSyncedUserNoisedBirthYearAndGenderToReport(testing::_))
      .Times(2)
      .WillRepeatedly([&number_of_invocations, test_gender,
                       test_birth_year](Report* report) {
        report->mutable_user_demographics()->set_birth_year(test_birth_year);
        report->mutable_user_demographics()->set_gender(test_gender);
        ++number_of_invocations;
      });

  UkmService service(&prefs_, &client_, std::move(provider));
  TestRecordingHelper recorder(&service);

  service.Initialize();

  // Verify that the provider hasn't provided the synced user's noised birth
  // year and gender yet.
  EXPECT_EQ(0, number_of_invocations);

  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB});
  service.EnableReporting();

  SourceId id = GetAllowlistedSourceId(0);
  recorder.UpdateSourceURL(id, GURL("https://google.com/foobar"));
  TestEvent1(id).Record(&service);
  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(1, GetPersistedLogCount());

  // Verify that the synced user's noised birth year and gender were added to
  // the UKM report.
  Report proto_report = GetPersistedReport();
  EXPECT_EQ(test_birth_year, proto_report.user_demographics().birth_year());
  EXPECT_EQ(test_gender, proto_report.user_demographics().gender());

  // Verify that the provider's method was only called once before the
  // destruction of the service.
  EXPECT_EQ(1, number_of_invocations);
}

TEST_F(UkmServiceTest,
       DontAddUserDemograhicsWhenNotAvailableAndFeatureEnabled) {
  auto provider = std::make_unique<MockDemographicMetricsProvider>();
  EXPECT_CALL(*provider,
              ProvideSyncedUserNoisedBirthYearAndGenderToReport(testing::_))
      .Times(2)
      .WillRepeatedly([](Report* report) {});

  UkmService service(&prefs_, &client_, std::move(provider));
  TestRecordingHelper recorder(&service);
  service.Initialize();

  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB});
  service.EnableReporting();

  SourceId id = GetAllowlistedSourceId(0);
  recorder.UpdateSourceURL(id, GURL("https://google.com/foobar"));
  TestEvent1(id).Record(&service);
  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(1, GetPersistedLogCount());

  // Verify that the synced user's noised birth year and gender are not in the
  // report when they are not available.
  Report proto_report = GetPersistedReport();
  EXPECT_FALSE(proto_report.has_user_demographics());
}

TEST_F(UkmServiceTest, DontAddUserDemograhicsWhenFeatureDisabled) {
  base::test::ScopedFeatureList local_feature;
  local_feature.InitAndDisableFeature(kReportUserNoisedUserBirthYearAndGender);

  // The demographics provider should not be called.
  auto provider = std::make_unique<MockDemographicMetricsProvider>();
  EXPECT_CALL(*provider,
              ProvideSyncedUserNoisedBirthYearAndGenderToReport(testing::_))
      .Times(0);

  UkmService service(&prefs_, &client_, std::move(provider));
  TestRecordingHelper recorder(&service);

  service.Initialize();

  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB});
  service.EnableReporting();

  SourceId id = GetAllowlistedSourceId(0);
  recorder.UpdateSourceURL(id, GURL("https://google.com/foobar"));
  TestEvent1(id).Record(&service);
  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(1, GetPersistedLogCount());

  // Verify that the synced user's noised birth year and gender are not in the
  // report when they are not available.
  Report proto_report = GetPersistedReport();
  EXPECT_FALSE(proto_report.has_user_demographics());
}

TEST_F(UkmServiceTest, LogsRotation) {
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  EXPECT_EQ(GetPersistedLogCount(), 0);
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB});
  service.EnableReporting();

  EXPECT_EQ(0, service.report_count());

  // Log rotation should generate a log.
  const SourceId id = GetAllowlistedSourceId(0);
  recorder.UpdateSourceURL(id, GURL("https://google.com/foobar"));
  task_runner_->RunPendingTasks();
  EXPECT_EQ(1, service.report_count());
  EXPECT_TRUE(client_.uploader()->is_uploading());

  // Rotation shouldn't generate a log due to one being pending.
  recorder.UpdateSourceURL(id, GURL("https://google.com/foobar"));
  task_runner_->RunPendingTasks();
  EXPECT_EQ(1, service.report_count());
  EXPECT_TRUE(client_.uploader()->is_uploading());

  // Completing the upload should clear pending log, then log rotation should
  // generate another log.
  client_.uploader()->CompleteUpload(200);
  task_runner_->RunPendingTasks();
  EXPECT_EQ(2, service.report_count());

  // Check that rotations keep working.
  for (int i = 3; i < 6; i++) {
    task_runner_->RunPendingTasks();
    client_.uploader()->CompleteUpload(200);
    recorder.UpdateSourceURL(id, GURL("https://google.com/foobar"));
    task_runner_->RunPendingTasks();
    EXPECT_EQ(i, service.report_count());
  }
}

TEST_F(UkmServiceTest, LogsUploadedOnlyWhenHavingData) {
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  EXPECT_EQ(GetPersistedLogCount(), 0);
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB});
  service.EnableReporting();

  EXPECT_TRUE(task_runner_->HasPendingTask());
  // Neither rotation or Flush should generate logs
  task_runner_->RunPendingTasks();
  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(GetPersistedLogCount(), 0);

  SourceId id = GetAllowlistedSourceId(0);
  recorder.UpdateSourceURL(id, GURL("https://google.com/foobar"));
  // Includes a Source, so will persist.
  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(GetPersistedLogCount(), 1);

  TestEvent1(id).Record(&service);
  // Includes an Entry, so will persist.
  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(GetPersistedLogCount(), 2);

  recorder.UpdateSourceURL(id, GURL("https://google.com/foobar"));
  TestEvent1(id).Record(&service);
  // Do not keep the source in the recorder after the current log.
  recorder.MarkSourceForDeletion(id);
  // Includes a Source and an Entry, so will persist.
  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(GetPersistedLogCount(), 3);

  // The recorder contains no data (Sources, Entries, nor web features), thus
  // will not create a new log.
  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(GetPersistedLogCount(), 3);

  recorder.RecordWebDXFeatures(id, {kWebDXFeature1},
                               kWebDXFeatureNumberOfFeaturesForTesting);
  // Includes web features data, so will persist.
  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(GetPersistedLogCount(), 4);

  // The recorder contains no data (Sources, Entries, nor web features), thus
  // will not create a new log.
  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(GetPersistedLogCount(), 4);
}

TEST_F(UkmServiceTest, GetNewSourceID) {
  SourceId id1 = UkmRecorder::GetNewSourceID();
  SourceId id2 = UkmRecorder::GetNewSourceID();
  SourceId id3 = UkmRecorder::GetNewSourceID();
  EXPECT_NE(id1, id2);
  EXPECT_NE(id1, id3);
  EXPECT_NE(id2, id3);
}

TEST_F(UkmServiceTest, RecordRedirectedUrl) {
  ClearPrefs();
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  EXPECT_EQ(GetPersistedLogCount(), 0);
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB});
  service.EnableReporting();

  SourceId id = GetAllowlistedSourceId(0);
  UkmSource::NavigationData navigation_data;
  navigation_data.urls = {GURL("https://google.com/initial"),
                          GURL("https://google.com/final")};
  recorder.RecordNavigation(id, navigation_data);

  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(GetPersistedLogCount(), 1);

  Report proto_report = GetPersistedReport();
  EXPECT_EQ(1, proto_report.sources_size());
  const Source& proto_source = proto_report.sources(0);

  EXPECT_EQ(id, proto_source.id());
  EXPECT_EQ(GURL("https://google.com/initial").spec(),
            proto_source.urls(0).url());
  EXPECT_EQ(GURL("https://google.com/final").spec(),
            proto_source.urls(1).url());
}

TEST_F(UkmServiceTest, RecordSessionId) {
  ClearPrefs();
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  EXPECT_EQ(0, GetPersistedLogCount());
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB});
  service.EnableReporting();

  auto id = GetAllowlistedSourceId(0);
  recorder.UpdateSourceURL(id, GURL("https://google.com/foobar"));

  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(1, GetPersistedLogCount());

  auto proto_report = GetPersistedReport();
  EXPECT_TRUE(proto_report.has_session_id());
  EXPECT_EQ(1, proto_report.report_id());
}

TEST_F(UkmServiceTest, SourceSize) {
  ClearPrefs();
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  EXPECT_EQ(0, GetPersistedLogCount());
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB});
  service.EnableReporting();

  // Add a large number of sources, more than the hardcoded max.
  for (int i = 0; i < 1000; ++i) {
    auto id = GetAllowlistedSourceId(i);
    recorder.UpdateSourceURL(id, GURL("https://google.com/foobar"));
  }

  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(1, GetPersistedLogCount());

  auto proto_report = GetPersistedReport();
  // Note, 500 instead of 1000 sources, since 500 is the maximum.
  EXPECT_EQ(500, proto_report.sources_size());
}

TEST_F(UkmServiceTest, PurgeMidUpload) {
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  EXPECT_EQ(GetPersistedLogCount(), 0);
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB});
  service.EnableReporting();

  auto id = GetAllowlistedSourceId(0);
  recorder.UpdateSourceURL(id, GURL("https://google.com/foobar1"));
  // Should init, generate a log, and start an upload.
  task_runner_->RunPendingTasks();
  EXPECT_TRUE(client_.uploader()->is_uploading());
  // Purge should delete all logs, including the one being sent.
  service.Purge();
  // Upload succeeds after logs was deleted.
  client_.uploader()->CompleteUpload(200);
  EXPECT_EQ(GetPersistedLogCount(), 0);
  EXPECT_FALSE(client_.uploader()->is_uploading());
}

class UkmServiceSourceURLLengthTest
    : public UkmServiceTest,
      public testing::WithParamInterface<UrlTruncationTestCase> {};

TEST_P(UkmServiceSourceURLLengthTest, Truncation) {
  const auto& test_case = GetParam();
  EXPECT_LE(test_case.expected_url.size(), 2048u);
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  EXPECT_EQ(0, GetPersistedLogCount());
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB});
  service.EnableReporting();

  SourceId source_id = GetAllowlistedSourceId(0);
  recorder.UpdateSourceURL(source_id, GURL(test_case.url));

  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(1, GetPersistedLogCount());

  auto proto_report = GetPersistedReport();
  ASSERT_EQ(1, proto_report.sources_size());
  const Source& proto_source = proto_report.sources(0);

  EXPECT_EQ(test_case.expected_url, proto_source.urls(0).url());
  EXPECT_EQ(test_case.expected_status,
            proto_source.urls(0).truncation_status());
}

INSTANTIATE_TEST_SUITE_P(
    UkmServiceSourceURLLengthTestGroup,
    UkmServiceSourceURLLengthTest,
    testing::ValuesIn(GetTruncationTestCases()),
    [](const testing::TestParamInfo<UkmServiceSourceURLLengthTest::ParamType>&
           info) { return info.param.name; });

TEST_F(UkmServiceTest, UnreferencedNonAllowlistedSources) {
  const GURL kURL("https://google.com/foobar");

  ClearPrefs();
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  EXPECT_EQ(0, GetPersistedLogCount());
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB});
  service.EnableReporting();

  // Record with Allowlisted ID to allowlist the URL.
  // Use a larger ID to make it last in the proto.
  SourceId allowlisted_id = GetAllowlistedSourceId(100);
  recorder.UpdateSourceURL(allowlisted_id, kURL);

  std::vector<SourceId> ids;
  base::TimeTicks last_time = base::TimeTicks::Now();
  for (int i = 1; i < 7; ++i) {
    // Wait until base::TimeTicks::Now() no longer equals |last_time|. This
    // ensures each source has a unique timestamp to avoid flakes. Should take
    // between 1-15ms per documented resolution of base::TimeTicks.
    while (base::TimeTicks::Now() == last_time) {
      base::PlatformThread::Sleep(base::Milliseconds(1));
    }

    ids.push_back(GetNonAllowlistedSourceId(i));
    recorder.UpdateSourceURL(ids.back(), kURL);
    last_time = base::TimeTicks::Now();
  }

  TestEvent1(ids[0]).Record(&service);
  TestEvent2(ids[2]).Record(&service);
  TestEvent3(ids[2]).Record(&service);
  TestEvent3(ids[3]).Record(&service);

  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(1, GetPersistedLogCount());
  auto proto_report = GetPersistedReport();

  // 1 allowlisted source and 6 non-allowlisted source.
  EXPECT_EQ(7, proto_report.source_counts().observed());
  // The one allowlisted source is of navigation type.
  EXPECT_EQ(1, proto_report.source_counts().navigation_sources());
  EXPECT_EQ(0, proto_report.source_counts().unmatched_sources());

  EXPECT_EQ(4, proto_report.source_counts().deferred_sources());
  EXPECT_EQ(0, proto_report.source_counts().carryover_sources());

  ASSERT_EQ(4, proto_report.sources_size());
  EXPECT_EQ(ids[0], proto_report.sources(0).id());
  EXPECT_EQ(kURL.spec(), proto_report.sources(0).urls(0).url());
  EXPECT_EQ(ids[2], proto_report.sources(1).id());
  EXPECT_EQ(kURL.spec(), proto_report.sources(1).urls(0).url());
  // Since MaxKeptSources is 3, only Sources 5, 4, 3 should be retained.
  // Log entries under 0, 1, 3 and 4. Log them in reverse order - which
  // shouldn't affect source ordering in the output.
  //  - Source 0 should not be re-transmitted since it was sent before.
  //  - Source 1 should not be transmitted due to MaxKeptSources param.
  //  - Sources 3 and 4 should be transmitted since they were not sent before.
  TestEvent1(ids[4]).Record(&service);
  TestEvent1(ids[3]).Record(&service);
  TestEvent1(ids[1]).Record(&service);
  TestEvent1(ids[0]).Record(&service);

  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(2, GetPersistedLogCount());
  proto_report = GetPersistedReport();

  EXPECT_EQ(0, proto_report.source_counts().observed());
  EXPECT_EQ(0, proto_report.source_counts().navigation_sources());
  EXPECT_EQ(0, proto_report.source_counts().unmatched_sources());

  EXPECT_EQ(2, proto_report.source_counts().deferred_sources());

  EXPECT_EQ(4, proto_report.source_counts().carryover_sources());
  ASSERT_EQ(3, proto_report.sources_size());
}

TEST_F(UkmServiceTest, NonAllowlistedUrls) {
  // URL to be manually allowlisted using allowlisted source type.
  const GURL kURL("https://google.com/foobar");
  struct {
    GURL url;
    bool expect_in_report;
  } test_cases[] = {
      {GURL("https://google.com/foobar"), true},
      // For origin-only URLs, only the origin needs to be matched.
      {GURL("https://google.com"), true},
      {GURL("https://google.com/foobar2"), false},
      {GURL("https://other.com"), false},
  };

  for (const auto& test : test_cases) {
    ClearPrefs();
    UkmService service(&prefs_, &client_,
                       std::make_unique<MockDemographicMetricsProvider>());
    TestRecordingHelper recorder(&service);

    ASSERT_EQ(GetPersistedLogCount(), 0);
    service.Initialize();
    task_runner_->RunUntilIdle();
    service.UpdateRecording({UkmConsentType::MSBB});
    service.EnableReporting();

    // Record with allowlisted ID to allowlist the URL.
    SourceId allowlist_id = GetAllowlistedSourceId(1);
    recorder.UpdateSourceURL(allowlist_id, kURL);

    // Record non allowlisted ID with an entry.
    SourceId nonallowlist_id = GetNonAllowlistedSourceId(100);
    recorder.UpdateSourceURL(nonallowlist_id, test.url);
    TestEvent1(nonallowlist_id).Record(&service);

    service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
    ASSERT_EQ(1, GetPersistedLogCount());
    auto proto_report = GetPersistedReport();

    EXPECT_EQ(2, proto_report.source_counts().observed());
    EXPECT_EQ(1, proto_report.source_counts().navigation_sources());

    // If the source id is not allowlisted, don't send it unless it has
    // associated entries and the URL matches that of the allowlisted source.
    if (test.expect_in_report) {
      EXPECT_EQ(0, proto_report.source_counts().unmatched_sources());
      ASSERT_EQ(2, proto_report.sources_size());
      EXPECT_EQ(allowlist_id, proto_report.sources(0).id());
      EXPECT_EQ(kURL, proto_report.sources(0).urls(0).url());
      EXPECT_EQ(nonallowlist_id, proto_report.sources(1).id());
      EXPECT_EQ(test.url, proto_report.sources(1).urls(0).url());
    } else {
      EXPECT_EQ(1, proto_report.source_counts().unmatched_sources());
      ASSERT_EQ(1, proto_report.sources_size());
      EXPECT_EQ(allowlist_id, proto_report.sources(0).id());
      EXPECT_EQ(kURL, proto_report.sources(0).urls(0).url());
    }

    // Do a log rotation again, with the same test URL associated to a new
    // source id. Since the previous source id of the test case is of
    // non-allowlisted type, the carryover URLs list is expected to remain
    // be unchanged, thus the the report should still contain the same numbers
    // of sources as before, that is, non-allowlisted URLs should not have
    // allowlisted themselves during the previous log rotation.
    SourceId nonallowlist_id2 = GetNonAllowlistedSourceId(101);
    recorder.UpdateSourceURL(nonallowlist_id2, test.url);
    TestEvent1(nonallowlist_id2).Record(&service);
    service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
    ASSERT_EQ(2, GetPersistedLogCount());
    proto_report = GetPersistedReport();

    if (test.expect_in_report) {
      EXPECT_EQ(0, proto_report.source_counts().unmatched_sources());
      ASSERT_EQ(2, proto_report.sources_size());
      EXPECT_EQ(allowlist_id, proto_report.sources(0).id());
      EXPECT_EQ(kURL, proto_report.sources(0).urls(0).url());
      EXPECT_EQ(nonallowlist_id2, proto_report.sources(1).id());
      EXPECT_EQ(test.url, proto_report.sources(1).urls(0).url());
    } else {
      EXPECT_EQ(1, proto_report.source_counts().unmatched_sources());
      ASSERT_EQ(1, proto_report.sources_size());
      EXPECT_EQ(allowlist_id, proto_report.sources(0).id());
      EXPECT_EQ(kURL, proto_report.sources(0).urls(0).url());
    }
  }
}

TEST_F(UkmServiceTest, AllowlistIdType) {
  std::map<SourceIdType, bool> source_id_type_allowlisted = {
      {SourceIdType::DEFAULT, false},  {SourceIdType::NAVIGATION_ID, true},
      {SourceIdType::APP_ID, true},    {SourceIdType::HISTORY_ID, true},
      {SourceIdType::WEBAPK_ID, true},
  };

  for (std::pair<SourceIdType, bool> type : source_id_type_allowlisted) {
    ClearPrefs();
    UkmService service(&prefs_, &client_,
                       std::make_unique<MockDemographicMetricsProvider>());
    TestRecordingHelper recorder(&service);
    EXPECT_EQ(0, GetPersistedLogCount());
    service.Initialize();
    task_runner_->RunUntilIdle();
    service.UpdateRecording({UkmConsentType::MSBB, UkmConsentType::APPS});
    service.EnableReporting();

    SourceId id = ConvertSourceIdToAllowlistedType(
        1, static_cast<SourceIdType>(type.first));
    ASSERT_EQ(GetSourceIdType(id), type.first);

    recorder.UpdateSourceURL(id, GURL("https://google.com/foobar1"));

    TestEvent1(id).Record(&service);

    service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
    EXPECT_EQ(1, GetPersistedLogCount());
    Report proto_report = GetPersistedReport();

    if (type.second) {
      // Verify we've added one source.
      EXPECT_EQ(1, proto_report.sources_size());
      EXPECT_EQ(GURL("https://google.com/foobar1").spec(),
                proto_report.sources(0).urls(0).url());
    } else {
      // No source added when id is not allowlisted type.
      EXPECT_EQ(0, proto_report.sources_size());
    }

    // We've added the entry whether source is added or not.
    ASSERT_EQ(1, proto_report.entries_size());
    const Entry& proto_entry_a = proto_report.entries(0);
    EXPECT_EQ(id, proto_entry_a.source_id());
    EXPECT_EQ(base::HashMetricName(TestEvent1::kEntryName),
              proto_entry_a.event_hash());
  }
}

TEST_F(UkmServiceTest, SupportedSchemes) {
  struct {
    const char* url;
    bool expected_kept;
  } test_cases[] = {
      {"http://google.ca/", true},
      {"https://google.ca/", true},
      {"about:blank", true},
      {"chrome://version/", true},
      {"app://play/abcdefghijklmnopqrstuvwxyzabcdef/", true},
      {"isolated-app://"
       "pl2ctdpnkf7ltse22mpjdb376etd3ydo7s72lgspuopgzcwl5tkqaaic/",
       true},
      // chrome-extension are controlled by TestIsWebstoreExtension, above.
      {"chrome-extension://bhcnanendmgjjeghamaccjnochlnhcgj/", true},
      {"chrome-extension://abcdefghijklmnopqrstuvwxyzabcdef/", false},
      {"ftp://google.ca/", false},
      {"file:///tmp/", false},
      {"abc://google.ca/", false},
      {"www.google.ca/", false},
  };

  ScopedUkmFeatureParams params({});
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  service.SetIsWebstoreExtensionCallback(
      base::BindRepeating(&TestIsWebstoreExtension));

  EXPECT_EQ(GetPersistedLogCount(), 0);
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB, UkmConsentType::EXTENSIONS});
  service.EnableReporting();

  int64_t id_counter = 1;
  int expected_kept_count = 0;
  for (const auto& test : test_cases) {
    auto source_id = GetAllowlistedSourceId(id_counter++);
    recorder.UpdateSourceURL(source_id, GURL(test.url));
    TestEvent1(source_id).Record(&service);
    if (test.expected_kept) {
      ++expected_kept_count;
    }
  }

  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(GetPersistedLogCount(), 1);
  Report proto_report = GetPersistedReport();

  EXPECT_EQ(expected_kept_count, proto_report.sources_size());
  for (const auto& test : test_cases) {
    bool found = false;
    for (int i = 0; i < proto_report.sources_size(); ++i) {
      if (proto_report.sources(i).urls(0).url() == test.url) {
        found = true;
        break;
      }
    }
    EXPECT_EQ(test.expected_kept, found) << test.url;
  }
}

TEST_F(UkmServiceTest, SupportedSchemesNoExtensions) {
  struct {
    const char* url;
    bool expected_kept;
  } test_cases[] = {
      {"http://google.ca/", true},
      {"https://google.ca/", true},
      {"about:blank", true},
      {"chrome://version/", true},
      {"app://play/abcdefghijklmnopqrstuvwxyzabcdef/", true},
      {"chrome-extension://bhcnanendmgjjeghamaccjnochlnhcgj/", false},
      {"chrome-extension://abcdefghijklmnopqrstuvwxyzabcdef/", false},
      {"ftp://google.ca/", false},
      {"file:///tmp/", false},
      {"abc://google.ca/", false},
      {"www.google.ca/", false},
  };

  ScopedUkmFeatureParams params({});
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);

  EXPECT_EQ(GetPersistedLogCount(), 0);
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB});
  service.EnableReporting();

  int64_t id_counter = 1;
  int expected_kept_count = 0;
  for (const auto& test : test_cases) {
    auto source_id = GetAllowlistedSourceId(id_counter++);
    recorder.UpdateSourceURL(source_id, GURL(test.url));
    TestEvent1(source_id).Record(&service);
    if (test.expected_kept) {
      ++expected_kept_count;
    }
  }

  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(GetPersistedLogCount(), 1);
  Report proto_report = GetPersistedReport();

  EXPECT_EQ(expected_kept_count, proto_report.sources_size());
  for (const auto& test : test_cases) {
    bool found = false;
    for (int i = 0; i < proto_report.sources_size(); ++i) {
      if (proto_report.sources(i).urls(0).url() == test.url) {
        found = true;
        break;
      }
    }
    EXPECT_EQ(test.expected_kept, found) << test.url;
  }
}

TEST_F(UkmServiceTest, SanitizeUrlAuthParams) {
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  EXPECT_EQ(0, GetPersistedLogCount());
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB});
  service.EnableReporting();

  auto id = GetAllowlistedSourceId(0);
  recorder.UpdateSourceURL(id, GURL("https://username:password@example.com/"));

  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(1, GetPersistedLogCount());

  auto proto_report = GetPersistedReport();
  ASSERT_EQ(1, proto_report.sources_size());
  const Source& proto_source = proto_report.sources(0);
  EXPECT_EQ("https://example.com/", proto_source.urls(0).url());
}

TEST_F(UkmServiceTest, SanitizeChromeUrlParams) {
  struct {
    const char* url;
    const char* expected_url;
  } test_cases[] = {
      {"chrome://version/?foo=bar", "chrome://version/"},
      {"about:blank?foo=bar", "about:blank"},
      {"chrome://histograms/Variations", "chrome://histograms/Variations"},
      {"http://google.ca/?foo=bar", "http://google.ca/?foo=bar"},
      {"https://google.ca/?foo=bar", "https://google.ca/?foo=bar"},
      {"chrome-extension://bhcnanendmgjjeghamaccjnochlnhcgj/foo.html?a=b",
       "chrome-extension://bhcnanendmgjjeghamaccjnochlnhcgj/"},
  };

  for (const auto& test : test_cases) {
    ClearPrefs();

    UkmService service(&prefs_, &client_,
                       std::make_unique<MockDemographicMetricsProvider>());
    TestRecordingHelper recorder(&service);
    service.SetIsWebstoreExtensionCallback(
        base::BindRepeating(&TestIsWebstoreExtension));

    EXPECT_EQ(0, GetPersistedLogCount());
    service.Initialize();
    task_runner_->RunUntilIdle();
    service.UpdateRecording({UkmConsentType::MSBB, UkmConsentType::EXTENSIONS});
    service.EnableReporting();

    auto id = GetAllowlistedSourceId(0);
    recorder.UpdateSourceURL(id, GURL(test.url));

    service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
    EXPECT_EQ(1, GetPersistedLogCount());

    auto proto_report = GetPersistedReport();
    ASSERT_EQ(1, proto_report.sources_size());
    const Source& proto_source = proto_report.sources(0);
    EXPECT_EQ(test.expected_url, proto_source.urls(0).url());
  }
}

TEST_F(UkmServiceTest, MarkSourceForDeletion) {
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  EXPECT_EQ(0, GetPersistedLogCount());
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB});
  service.EnableReporting();

  // Seed some dummy sources.
  SourceId id0 = GetAllowlistedSourceId(0);
  recorder.UpdateSourceURL(id0, GURL("https://www.example0.com/"));
  SourceId id1 = GetAllowlistedSourceId(1);
  recorder.UpdateSourceURL(id1, GURL("https://www.example1.com/"));
  SourceId id2 = GetAllowlistedSourceId(2);
  recorder.UpdateSourceURL(id2, GURL("https://www.example2.com/"));

  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  int logs_count = 0;
  EXPECT_EQ(++logs_count, GetPersistedLogCount());

  // All sources are present in the report.
  Report proto_report = GetPersistedReport();
  ASSERT_EQ(3, proto_report.sources_size());
  EXPECT_EQ(id0, proto_report.sources(0).id());
  EXPECT_EQ(id1, proto_report.sources(1).id());
  EXPECT_EQ(id2, proto_report.sources(2).id());

  // Mark source 1 for deletion. Next report will still contain source 1 because
  // we might have associated entries. It will no longer be in further report at
  // the following cycle.
  service.MarkSourceForDeletion(id1);
  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(++logs_count, GetPersistedLogCount());

  proto_report = GetPersistedReport();
  ASSERT_EQ(3, proto_report.sources_size());

  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(++logs_count, GetPersistedLogCount());

  proto_report = GetPersistedReport();
  ASSERT_EQ(2, proto_report.sources_size());
  EXPECT_EQ(id0, proto_report.sources(0).id());
  EXPECT_EQ(id2, proto_report.sources(1).id());
}

// Verifies that sources of some types are deleted at the end of reporting
// cycle.
TEST_F(UkmServiceTest, PurgeNonCarriedOverSources) {
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  EXPECT_EQ(0, GetPersistedLogCount());
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording(
      {UkmConsentType::MSBB, UkmConsentType::APPS, UkmConsentType::EXTENSIONS});
  service.EnableReporting();
  service.SetIsWebstoreExtensionCallback(
      base::BindRepeating(&TestIsWebstoreExtension));

  // Seed some fake sources.
  SourceId ukm_id = ConvertToSourceId(0, SourceIdType::DEFAULT);
  recorder.UpdateSourceURL(ukm_id, GURL("https://www.example0.com/"));
  SourceId navigation_id =
      ConvertSourceIdToAllowlistedType(1, SourceIdType::NAVIGATION_ID);
  recorder.UpdateSourceURL(navigation_id, GURL("https://www.example1.com/"));
  SourceId app_id = ConvertSourceIdToAllowlistedType(2, SourceIdType::APP_ID);
  recorder.UpdateSourceURL(app_id, GURL("https://www.example2.com/"));
  SourceId history_id =
      ConvertSourceIdToAllowlistedType(3, SourceIdType::HISTORY_ID);
  recorder.UpdateSourceURL(history_id, GURL("https://www.example3.com/"));
  SourceId webapk_id =
      ConvertSourceIdToAllowlistedType(4, SourceIdType::WEBAPK_ID);
  recorder.UpdateSourceURL(webapk_id, GURL("https://www.example4.com/"));
  SourceId payment_app_id =
      ConvertSourceIdToAllowlistedType(5, SourceIdType::PAYMENT_APP_ID);
  recorder.UpdateSourceURL(payment_app_id, GURL("https://www.example5.com/"));
  SourceId web_identity_id =
      ConvertSourceIdToAllowlistedType(6, SourceIdType::WEB_IDENTITY_ID);
  recorder.UpdateSourceURL(web_identity_id, GURL("https://www.example6.com/"));
  SourceId extension_id =
      ConvertSourceIdToAllowlistedType(7, SourceIdType::EXTENSION_ID);
  recorder.UpdateSourceURL(
      extension_id,
      GURL("chrome-extension://bhcnanendmgjjeghamaccjnochlnhcgj"));

  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  int logs_count = 0;
  EXPECT_EQ(++logs_count, GetPersistedLogCount());

  // All sources are present except ukm_id of non-allowlisted UKM type.
  Report proto_report = GetPersistedReport();
  ASSERT_EQ(7, proto_report.sources_size());
  EXPECT_EQ(navigation_id, proto_report.sources(0).id());
  EXPECT_EQ(app_id, proto_report.sources(1).id());
  EXPECT_EQ(history_id, proto_report.sources(2).id());
  EXPECT_EQ(webapk_id, proto_report.sources(3).id());
  EXPECT_EQ(payment_app_id, proto_report.sources(4).id());
  EXPECT_EQ(web_identity_id, proto_report.sources(5).id());
  EXPECT_EQ(extension_id, proto_report.sources(6).id());

  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(++logs_count, GetPersistedLogCount());

  // Sources of HISTORY_ID, WEBAPK_ID, PAYMENT_APP_ID, WEB_IDENTITY_ID,and
  // EXTENSION_ID types are not kept between reporting cycles, thus only 2
  // sources remain.
  proto_report = GetPersistedReport();
  ASSERT_EQ(2, proto_report.sources_size());
  EXPECT_EQ(navigation_id, proto_report.sources(0).id());
  EXPECT_EQ(app_id, proto_report.sources(1).id());
}

TEST_F(UkmServiceTest, PruneOldSources) {
  const GURL kURL("https://google.com/foobar");

  // Set the 'MaxKeptSources' value to 3 so it is easier to test.
  ScopedUkmFeatureParams params({{"MaxKeptSources", "3"}});

  ClearPrefs();
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  EXPECT_EQ(0, GetPersistedLogCount());
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB});
  service.EnableReporting();

  // Create 5 allowlisted ids. Allowlisted ids (like APP_ID) will not be
  // automatically removed when they emit events. They're only removed via the
  // pruning mechanism. Note that the are added in order, so 4 is the
  // youngest/newest.
  std::vector<SourceId> ids;
  base::TimeTicks last_time = base::TimeTicks::Now();
  for (int i = 0; i < 5; ++i) {
    // Wait until base::TimeTicks::Now() no longer equals |last_time|. This
    // ensures each source has a unique timestamp to avoid flakes. Should take
    // between 1-15ms per documented resolution of base::TimeTicks.
    while (base::TimeTicks::Now() == last_time) {
      base::PlatformThread::Sleep(base::Milliseconds(1));
    }
    ids.push_back(GetAllowlistedSourceId(i));
    recorder.UpdateSourceURL(ids.back(), kURL);
    last_time = base::TimeTicks::Now();
  }

  // Events on 0 and 4. This doesn't affect the pruning.
  TestEvent1(ids[0]).Record(&service);
  TestEvent1(ids[4]).Record(&service);

  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(1, GetPersistedLogCount());
  auto proto_report = GetPersistedReport();

  EXPECT_EQ(5, proto_report.source_counts().observed());
  // All are navigation sources.
  EXPECT_EQ(5, proto_report.source_counts().navigation_sources());
  EXPECT_EQ(0, proto_report.source_counts().unmatched_sources());

  // In all cases, 3 will be deferred since that is our max allowed.
  EXPECT_EQ(3, proto_report.source_counts().deferred_sources());
  // This is from last time, so none there.
  EXPECT_EQ(0, proto_report.source_counts().carryover_sources());

  // All 5 sources will be included in this first report.
  ASSERT_EQ(5, proto_report.sources_size());
  EXPECT_EQ(ids[0], proto_report.sources(0).id());
  EXPECT_EQ(ids[1], proto_report.sources(1).id());
  EXPECT_EQ(ids[2], proto_report.sources(2).id());
  EXPECT_EQ(ids[3], proto_report.sources(3).id());
  EXPECT_EQ(ids[4], proto_report.sources(4).id());

  // We have MaxKeptSources=3, and we keep by age, so we should keep 2,3,4.

  // New events on 0,2,4. This actually doesn't matter with respect to what
  // sources are emitted here, as some sources are already pruned.
  TestEvent1(ids[0]).Record(&service);
  TestEvent1(ids[2]).Record(&service);
  TestEvent1(ids[4]).Record(&service);

  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(2, GetPersistedLogCount());
  proto_report = GetPersistedReport();

  // No new sources observed.
  EXPECT_EQ(0, proto_report.source_counts().observed());
  // 0 again, as this is for newly observed ones.
  EXPECT_EQ(0, proto_report.source_counts().navigation_sources());
  EXPECT_EQ(0, proto_report.source_counts().unmatched_sources());

  // Since no new sources added, we still are keeping the same 3. So all 3 are
  // kept and retained, in both cases.
  EXPECT_EQ(3, proto_report.source_counts().deferred_sources());
  EXPECT_EQ(3, proto_report.source_counts().carryover_sources());
  ASSERT_EQ(3, proto_report.sources_size());

  // 2, 3, 4 as these are the 3 newest.
  EXPECT_EQ(ids[2], proto_report.sources(0).id());
  EXPECT_EQ(ids[3], proto_report.sources(1).id());
  EXPECT_EQ(ids[4], proto_report.sources(2).id());
}

// Verifies that when a cloned install is detected, logs are purged.
TEST_F(UkmServiceTest, PurgeLogsOnClonedInstallDetected) {
  TestMetricsServiceClientWithClonedInstallDetector client;
  UkmService service(&prefs_, &client,
                     std::make_unique<MockDemographicMetricsProvider>());
  service.Initialize();

  // Store various logs.
  metrics::UnsentLogStore* test_log_store =
      service.reporting_service()->ukm_log_store();
  test_log_store->StoreLog(
      "dummy log data", metrics::LogMetadata(),
      metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  test_log_store->StageNextLog();
  test_log_store->StoreLog(
      "more dummy log data", metrics::LogMetadata(),
      metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_TRUE(test_log_store->has_staged_log());
  EXPECT_TRUE(test_log_store->has_unsent_logs());

  metrics::ClonedInstallDetector* cloned_install_detector =
      client.cloned_install_detector();
  cloned_install_detector->RegisterPrefs(prefs_.registry());

  static constexpr char kTestRawId[] = "test";
  // Hashed machine id for |kTestRawId|.
  static constexpr int kTestHashedId = 2216819;

  // Save a machine id that will not cause a clone to be detected.
  prefs_.SetInteger(metrics::prefs::kMetricsMachineId, kTestHashedId);
  cloned_install_detector->SaveMachineIdForTesting(&prefs_, kTestRawId);
  // Verify that the logs are still present.
  EXPECT_TRUE(test_log_store->has_staged_log());
  EXPECT_TRUE(test_log_store->has_unsent_logs());

  // Save a machine id that will cause a clone to be detected.
  prefs_.SetInteger(metrics::prefs::kMetricsMachineId, kTestHashedId + 1);
  cloned_install_detector->SaveMachineIdForTesting(&prefs_, kTestRawId);
  // Verify that the logs were purged.
  EXPECT_FALSE(test_log_store->has_staged_log());
  EXPECT_FALSE(test_log_store->has_unsent_logs());
}

TEST_F(UkmServiceTest, WebDXFeatures) {
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording({UkmConsentType::MSBB});
  service.EnableReporting();

  // Record some web features data, create a report, and verify that the data in
  // it matches what was recorded.
  auto id0 = GetAllowlistedSourceId(0);
  recorder.UpdateSourceURL(id0, GURL("https://google.com/foobar0"));
  recorder.RecordWebDXFeatures(id0, {kWebDXFeature1},
                               kWebDXFeatureNumberOfFeaturesForTesting);
  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  ASSERT_EQ(GetPersistedLogCount(), 1);
  Report proto_report = GetPersistedReport();
  ASSERT_EQ(proto_report.web_features_size(), 1);
  EXPECT_EQ(proto_report.web_features(0).source_id(), id0);
  EXPECT_TRUE(WebDXFeaturesStrictlyContains(proto_report.web_features(0),
                                            {kWebDXFeature1}));

  // Record some more web features data, create a report, and verify that the
  // data in it matches what was recorded. The web features data from the
  // previous report should not appear.
  auto id1 = GetAllowlistedSourceId(1);
  recorder.UpdateSourceURL(id1, GURL("https://google.com/foobar1"));
  recorder.RecordWebDXFeatures(id1, {kWebDXFeature1, kWebDXFeature3},
                               kWebDXFeatureNumberOfFeaturesForTesting);
  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  ASSERT_EQ(GetPersistedLogCount(), 2);
  proto_report = GetPersistedReport();
  ASSERT_EQ(proto_report.web_features_size(), 1);
  EXPECT_EQ(proto_report.web_features(0).source_id(), id1);
  EXPECT_TRUE(WebDXFeaturesStrictlyContains(proto_report.web_features(0),
                                            {kWebDXFeature1, kWebDXFeature3}));

  // Create a report without recording any web features data. Verify that it
  // contains no web features data, as the data from the previous reports should
  // not appear.
  auto id2 = GetAllowlistedSourceId(2);
  recorder.UpdateSourceURL(id2, GURL("https://google.com/foobar2"));
  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  ASSERT_EQ(GetPersistedLogCount(), 3);
  proto_report = GetPersistedReport();
  EXPECT_EQ(proto_report.web_features_size(), 0);

  // Record some more web features data, purge the data, and then try to create
  // a report. Nothing should be created, and all the previous logs should also
  // be gone.
  auto id3 = GetAllowlistedSourceId(3);
  recorder.UpdateSourceURL(id3, GURL("https://google.com/foobar3"));
  recorder.RecordWebDXFeatures(id3, {kWebDXFeature3},
                               kWebDXFeatureNumberOfFeaturesForTesting);
  service.Purge();
  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(GetPersistedLogCount(), 0);
}

#if BUILDFLAG(IS_CHROMEOS)
TEST_F(UkmServiceTest, NotifyObserverOnShutdown) {
  MockUkmRecorderObserver observer;
  {
    UkmService service(&prefs_, &client_,
                       std::make_unique<MockDemographicMetricsProvider>());
    ukm::UkmRecorder::Get()->AddObserver(&observer);
    EXPECT_CALL(observer, OnStartingShutdown()).Times(1);
  }
  ukm::UkmRecorder::Get()->RemoveObserver(&observer);
}

namespace {

class UkmServiceTestWithIndependentAppKM
    : public testing::TestWithParam<UkmConsentType> {
 public:
  UkmServiceTestWithIndependentAppKM()
      : task_runner_(new base::TestSimpleTaskRunner),
        task_runner_current_default_handle_(task_runner_) {
    UkmService::RegisterPrefs(prefs_.registry());

    prefs_.ClearPref(prefs::kUkmClientId);
    prefs_.ClearPref(prefs::kUkmSessionId);
    prefs_.ClearPref(prefs::kUkmUnsentLogStore);
  }

  int GetPersistedLogCount() { return ukm::GetPersistedLogCount(prefs_); }

  Report GetPersistedReport() { return ukm::GetPersistedReport(prefs_); }

 protected:
  TestingPrefServiceSimple prefs_;
  metrics::TestMetricsServiceClient client_;
  scoped_refptr<base::TestSimpleTaskRunner> task_runner_;
  base::SingleThreadTaskRunner::CurrentDefaultHandle
      task_runner_current_default_handle_;
};

}  // namespace

TEST_P(UkmServiceTestWithIndependentAppKM, RejectWhenNotConsented) {
  const GURL kURL("https://google.com/foobar");
  const GURL kAppURL("app://google.com/foobar");

  // Setup test constants from param.
  const auto consent = GetParam();
  const std::vector<int> app_indices = {1, 4};
  const std::vector<int> url_indices = {0, 2, 3};
  const std::vector<int>& expected_source_indices =
      (consent == UkmConsentType::APPS ? app_indices : url_indices);

  const int expected_result = expected_source_indices.size();

  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  EXPECT_EQ(0, GetPersistedLogCount());
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording({consent});
  service.EnableReporting();

  std::vector<SourceId> source_ids;
  for (int i = 0; i < 5; ++i) {
    if (std::ranges::contains(app_indices, i)) {
      source_ids.push_back(UkmServiceTest::GetAppIDSourceId(i));
      recorder.UpdateSourceURL(source_ids.back(), kAppURL);
    } else {
      source_ids.push_back(UkmServiceTest::GetAllowlistedSourceId(i));
      recorder.UpdateSourceURL(source_ids.back(), kURL);
    }

    TestEvent1(source_ids.back()).Record(&service);
  }

  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  EXPECT_EQ(1, GetPersistedLogCount());

  // Has the sources and entries associated with AppIDs.
  Report report = GetPersistedReport();
  EXPECT_EQ(expected_result, report.sources_size());
  EXPECT_EQ(expected_result, report.entries_size());

  for (int i = 0; i < expected_result; ++i) {
    EXPECT_EQ(source_ids[expected_source_indices[i]], report.sources(i).id());
    EXPECT_EQ(source_ids[expected_source_indices[i]],
              report.entries(i).source_id());
  }
}

INSTANTIATE_TEST_SUITE_P(
    UkmServiceTestWithIndependentAppKMGroup,
    UkmServiceTestWithIndependentAppKM,
    testing::Values(UkmConsentType::APPS, UkmConsentType::MSBB),
    [](const testing::TestParamInfo<
        UkmServiceTestWithIndependentAppKM::ParamType>& info) {
      if (info.param == UkmConsentType::APPS) {
        return "TestApps";
      } else {
        return "TestMSBB";
      }
    });

namespace {

class UkmServiceTestWithIndependentAppKMFullConsent
    : public testing::TestWithParam<bool> {
 public:
  UkmServiceTestWithIndependentAppKMFullConsent()
      : task_runner_(new base::TestSimpleTaskRunner),
        task_runner_current_default_handle_(task_runner_) {
    UkmService::RegisterPrefs(prefs_.registry());

    prefs_.ClearPref(prefs::kUkmClientId);
    prefs_.ClearPref(prefs::kUkmSessionId);
    prefs_.ClearPref(prefs::kUkmUnsentLogStore);
  }

  int GetPersistedLogCount() { return ukm::GetPersistedLogCount(prefs_); }

  Report GetPersistedReport() { return ukm::GetPersistedReport(prefs_); }

 protected:
  TestingPrefServiceSimple prefs_;
  metrics::TestMetricsServiceClient client_;
  scoped_refptr<base::TestSimpleTaskRunner> task_runner_;
  base::SingleThreadTaskRunner::CurrentDefaultHandle
      task_runner_current_default_handle_;
};

}  // namespace

TEST_P(UkmServiceTestWithIndependentAppKMFullConsent, VerifyAllAndNoneConsent) {
  const GURL kURL("https://google.com/foobar");
  const GURL kAppURL("app://google.com/foobar");
  const int kExpectedResultWithConsent = 5;

  // Setup test constants from param.
  const auto has_consent = GetParam();
  const auto consent_state =
      (has_consent ? UkmConsentState::All() : UkmConsentState());

  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  TestRecordingHelper recorder(&service);
  EXPECT_EQ(0, GetPersistedLogCount());
  service.Initialize();
  task_runner_->RunUntilIdle();
  service.UpdateRecording(consent_state);
  service.EnableReporting();

  const std::vector<int> app_indices = {1, 4};
  const std::vector<int> url_indices = {0, 2, 3};

  std::vector<SourceId> source_ids;
  for (int i = 0; i < 5; ++i) {
    if (std::ranges::contains(app_indices, i)) {
      source_ids.push_back(UkmServiceTest::GetAppIDSourceId(i));
      recorder.UpdateSourceURL(source_ids.back(), kAppURL);
    } else {
      source_ids.push_back(UkmServiceTest::GetAllowlistedSourceId(i));
      recorder.UpdateSourceURL(source_ids.back(), kURL);
    }

    TestEvent1(source_ids.back()).Record(&service);
  }

  service.Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);

  EXPECT_EQ(GetPersistedLogCount(), static_cast<int>(has_consent));

  if (has_consent) {
    const auto report = GetPersistedReport();

    EXPECT_EQ(report.sources_size(), kExpectedResultWithConsent);
    EXPECT_EQ(report.entries_size(), kExpectedResultWithConsent);

    for (int i = 0; i < kExpectedResultWithConsent; ++i) {
      EXPECT_EQ(source_ids[i], report.sources(i).id());
      EXPECT_EQ(source_ids[i], report.entries(i).source_id());
    }
  }
}

INSTANTIATE_TEST_SUITE_P(
    UkmServiceTestWithIndependentAppKMFullConsentGroup,
    UkmServiceTestWithIndependentAppKMFullConsent,
    testing::Values(true, false),
    [](const testing::TestParamInfo<
        UkmServiceTestWithIndependentAppKMFullConsent::ParamType>& info) {
      if (info.param) {
        return "TestAllConsent";
      } else {
        return "TestNoConsent";
      }
    });

#endif  // BUILDFLAG(IS_CHROMEOS)

class MockUkmRecorder : public ukm::UkmRecorder {
 public:
  MockUkmRecorder() = default;
  ~MockUkmRecorder() override = default;

  MOCK_METHOD(void, AddEntry, (mojom::UkmEntryPtr entry), (override));
  MOCK_METHOD(void,
              RecordWebDXFeatures,
              (SourceId source_id,
               const std::set<int32_t>& features,
               const size_t max_feature_value),
              (override));
  MOCK_METHOD(void,
              UpdateSourceURL,
              (SourceId source_id, const GURL& url),
              (override));

  MOCK_METHOD(void,
              UpdateAppURL,
              (SourceId source_id, const GURL& url, const AppType app_type),
              (override));

  MOCK_METHOD(void,
              RecordNavigation,
              (SourceId source_id,
               const UkmSource::NavigationData& navigation_data),
              (override));

  MOCK_METHOD(void,
              MarkSourceForDeletion,
              (ukm::SourceId source_id),
              (override));
};

TEST_F(UkmReduceAddEntryIpcTest, RecordingEnabled) {
  ASSERT_TRUE(base::FeatureList::IsEnabled(ukm::kUkmReduceAddEntryIPC));

  base::RunLoop run_loop;
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  // Initialize UkmService.
  service.Initialize();

  ukm::UkmEntryBuilder builder(ukm::NoURLSourceId(),
                               "Event.ScrollUpdate.Touch");
  builder.SetMetric("TimeToScrollUpdateSwapBegin", 17);

  // Custom UkmRecorder to intercept messages to and from UkmService to clients.
  MockUkmRecorder mock_recorder;
  mojo::Remote<ukm::mojom::UkmRecorderFactory> factory;
  metrics::UkmRecorderFactoryImpl::Create(&mock_recorder,
                                          factory.BindNewPipeAndPassReceiver());

  // MojoUkmRecorder (client).
  auto mojo_recorder = MojoUkmRecorder::Create(*factory);

  service.EnableRecording();
  run_loop.RunUntilIdle();

  // Since UkmObservers list is empty, the final decision regarding sending the
  // AddEntry IPC from clients to UkmService depends on recording being
  // enabled/disabled.
  EXPECT_CALL(mock_recorder, AddEntry).Times(1);

  builder.Record(mojo_recorder.get());
  run_loop.RunUntilIdle();
}

TEST_F(UkmReduceAddEntryIpcTest, RecordingDisabled) {
  ASSERT_TRUE(base::FeatureList::IsEnabled(ukm::kUkmReduceAddEntryIPC));

  base::RunLoop run_loop;
  ukm::UkmEntryBuilder builder(ukm::NoURLSourceId(),
                               "Event.ScrollUpdate.Touch");
  builder.SetMetric("TimeToScrollUpdateSwapBegin", 17);

  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  // Initialize UkmService.
  service.Initialize();

  // Custom UkmRecorder to intercept messages to and from UkmService to clients.
  MockUkmRecorder mock_recorder;
  mojo::Remote<ukm::mojom::UkmRecorderFactory> factory;
  metrics::UkmRecorderFactoryImpl::Create(&mock_recorder,
                                          factory.BindNewPipeAndPassReceiver());

  // MojoUkmRecorder (client).
  auto mojo_recorder = MojoUkmRecorder::Create(*factory);

  service.DisableRecording();
  run_loop.RunUntilIdle();

  // Since UkmObservers list is empty, the final decision regarding sending the
  // AddEntry IPC from clients to UkmService depends on recording being
  // enabled/disabled.
  EXPECT_CALL(mock_recorder, AddEntry).Times(0);

  builder.Record(mojo_recorder.get());
  run_loop.RunUntilIdle();
}

TEST_F(UkmReduceAddEntryIpcTest, AddRemoveUkmObserver) {
  ASSERT_TRUE(base::FeatureList::IsEnabled(ukm::kUkmReduceAddEntryIPC));

  base::RunLoop run_loop;
  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  // Initialize UkmService.
  service.Initialize();

  // Custom UkmRecorder to intercept messages to and from UkmService to clients.
  MockUkmRecorder mock_recorder;
  mojo::Remote<ukm::mojom::UkmRecorderFactory> factory;
  metrics::UkmRecorderFactoryImpl::Create(&mock_recorder,
                                          factory.BindNewPipeAndPassReceiver());

  // MojoUkmRecorder (client).
  auto mojo_recorder = MojoUkmRecorder::Create(*factory);

  // Recording Disabled.
  service.DisableRecording();
  run_loop.RunUntilIdle();

  mojom::UkmEntryPtr observed_ukm_entry;

  // UkmRecorderObservers with different event hashes. If an entry is seen at
  // client where event_hash matches with one from observers, we need to send
  // AddEntry IPC even if recording is disabled.
  UkmRecorderObserver obs1, obs2;
  base::flat_set<uint64_t> events1 = {
      base::HashMetricName("Event.ScrollUpdate.Touch")};
  service.AddUkmRecorderObserver(events1, &obs1);
  base::flat_set<uint64_t> events2 = {
      base::HashMetricName("Event.ScrollBegin.Wheel")};
  service.AddUkmRecorderObserver(events2, &obs2);
  run_loop.RunUntilIdle();

  {
    // This event is being observed by UkmRecorderObserver obs1. Hence, even
    // when UKM recording has been disabled, the event needs to be sent to
    // browser process.
    ukm::UkmEntryBuilder builder(ukm::NoURLSourceId(),
                                 "Event.ScrollUpdate.Touch");
    builder.SetMetric("TimeToScrollUpdateSwapBegin", 17);
    builder.SetMetric("IsMainThread", 21);
    auto expected_ukm_entry = builder.GetEntryForTesting();
    // We expect this event will not be filtered at client,i.e., MojoUkmRecorder
    // and the mock method AddEntry would be called once.
    EXPECT_CALL(mock_recorder, AddEntry)
        .Times(1)
        .WillOnce([&](mojom::UkmEntryPtr entry) {
          observed_ukm_entry = std::move(entry);
        });

    builder.Record(mojo_recorder.get());
    run_loop.RunUntilIdle();
    // Expects the UkmEntry seen at both sent(MojoUkmRecorder) and
    // receive(MockUkmRecorder) to be same.
    EXPECT_EQ(expected_ukm_entry, observed_ukm_entry);
  }
  {
    // This event is not being observed by any of UkmRecorderObservers and since
    // UKM recording has been disabled, the event will not be sent to browser
    // process.
    ukm::UkmEntryBuilder builder2(ukm::NoURLSourceId(), "Download.Interrupted");
    builder2.SetMetric("BytesWasted", 10);
    // Expect 0 calls to MockUkmRecorer::AddEntry since the UKM event will be
    // filtered out at the client.
    EXPECT_CALL(mock_recorder, AddEntry).Times(0);
    builder2.Record(mojo_recorder.get());
    run_loop.RunUntilIdle();
  }
  {
    // This event is being observed by UkmRecorderObserver obs2. Hence, even
    // when UKM recording has been disabled, the event needs to be sent to
    // browser process.
    ukm::UkmEntryBuilder builder3(ukm::NoURLSourceId(),
                                  "Event.ScrollBegin.Wheel");
    builder3.SetMetric("TimeToScrollUpdateSwapBegin", 25);
    auto expected_ukm_entry = builder3.GetEntryForTesting();

    EXPECT_CALL(mock_recorder, AddEntry)
        .Times(1)
        .WillOnce([&](mojom::UkmEntryPtr entry) {
          observed_ukm_entry = std::move(entry);
        });
    builder3.Record(mojo_recorder.get());
    run_loop.RunUntilIdle();
    EXPECT_EQ(expected_ukm_entry, observed_ukm_entry);
  }
  // Remove UkmRecorderObserver obs1.
  service.RemoveUkmRecorderObserver(&obs1);
  run_loop.RunUntilIdle();
  {
    // This event is not being observed by any of the UkmRecorderObservers now,
    // and since UKM recording is disabled, it will be filtered out.
    ukm::UkmEntryBuilder builder4(ukm::NoURLSourceId(),
                                  "Event.ScrollUpdate.Touch");
    builder4.SetMetric("TimeToScrollUpdateSwapBegin", 17);
    // Expect 0 calls to MockUkmRecorer::AddEntry since the UKM event will be
    // filtered out at the client, because of UKM recording being disabled.
    EXPECT_CALL(mock_recorder, AddEntry).Times(0);
    builder4.Record(mojo_recorder.get());
    run_loop.RunUntilIdle();
  }
}

TEST_F(UkmReduceAddEntryIpcTest, MultipleDelegates) {
  ASSERT_TRUE(base::FeatureList::IsEnabled(ukm::kUkmReduceAddEntryIPC));

  base::RunLoop run_loop;
  ukm::UkmEntryBuilder builder(ukm::NoURLSourceId(),
                               "Event.ScrollUpdate.Touch");
  builder.SetMetric("TimeToScrollUpdateSwapBegin", 17);

  UkmService service(&prefs_, &client_,
                     std::make_unique<MockDemographicMetricsProvider>());
  // Initialize UkmService.
  service.Initialize();
  MockUkmRecorder mock_recorder;
  mojo::Remote<ukm::mojom::UkmRecorderFactory> factory;
  metrics::UkmRecorderFactoryImpl::Create(&mock_recorder,
                                          factory.BindNewPipeAndPassReceiver());

  // MojoUkmRecorder (client).
  auto mojo_recorder = MojoUkmRecorder::Create(*factory);

  // Disabled recording but having multiple delegates will default clients
  // sending all AddEntry IPCs to the browser.
  service.DisableRecording();
  run_loop.RunUntilIdle();

  ukm::TestAutoSetUkmRecorder test_recorder;
  run_loop.RunUntilIdle();

  EXPECT_CALL(mock_recorder, AddEntry).Times(1);

  builder.Record(mojo_recorder.get());
  run_loop.RunUntilIdle();
}
}  // namespace ukm
