// Copyright 2023 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/metrics/structured/structured_metrics_service.h"

#include <memory>
#include <utility>

#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/test/task_environment.h"
#include "base/test/test_simple_task_runner.h"
#include "components/metrics/log_decoder.h"
#include "components/metrics/metrics_provider.h"
#include "components/metrics/structured/recorder.h"
#include "components/metrics/structured/reporting/structured_metrics_reporting_service.h"
#include "components/metrics/structured/structured_events.h"
#include "components/metrics/structured/structured_metrics_client.h"
#include "components/metrics/structured/structured_metrics_prefs.h"
#include "components/metrics/structured/structured_metrics_recorder.h"
#include "components/metrics/structured/test/test_event_storage.h"
#include "components/metrics/structured/test/test_key_data_provider.h"
#include "components/metrics/test/test_metrics_service_client.h"
#include "components/metrics/unsent_log_store.h"
#include "components/metrics/unsent_log_store_metrics_impl.h"
#include "components/prefs/testing_pref_service.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace metrics::structured {
namespace {

using events::v2::test_project_one::TestEventOne;
using events::v2::test_project_six::TestEventSeven;
using ::testing::ElementsAre;
using ::testing::SizeIs;

// The name hash of "TestProjectOne".
constexpr uint64_t kProjectOneHash = UINT64_C(16881314472396226433);
// The name hash of "TestProjectThree".
constexpr uint64_t kProjectThreeHash = UINT64_C(10860358748803291132);

class TestRecorder : public StructuredMetricsClient::RecordingDelegate {
 public:
  TestRecorder() = default;
  TestRecorder(const TestRecorder& recorder) = delete;
  TestRecorder& operator=(const TestRecorder& recorder) = delete;
  ~TestRecorder() override = default;

  void RecordEvent(Event&& event) override {
    Recorder::GetInstance()->RecordEvent(std::move(event));
  }

  bool IsReadyToRecord() const override { return true; }
};

class TestMetricsProvider : public metrics::MetricsProvider {
 public:
  TestMetricsProvider() = default;
  ~TestMetricsProvider() override = default;

  void ProvideSystemProfileMetrics(
      SystemProfileProto* system_profile) override {}

  void ProvideSystemProfileMetricsWithLogCreationTime(
      base::TimeTicks log_creation_time,
      SystemProfileProto* system_profile) override {
    system_profile->add_field_trial()->set_name_id(1);
    system_profile->add_field_trial()->set_name_id(2);
  }
};
}  // namespace

class StructuredMetricsServiceTest : public testing::Test {
 public:
  StructuredMetricsServiceTest() {
    reporting::StructuredMetricsReportingService::RegisterPrefs(
        prefs_.registry());
  }

  ~StructuredMetricsServiceTest() override = default;

  void SetUp() override {
    Recorder::GetInstance()->SetUiTaskRunner(
        task_environment_.GetMainThreadTaskRunner());
    StructuredMetricsClient::Get()->SetDelegate(&test_recorder_);

    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());

    WriteTestingDeviceKeys();

    WriteTestingProfileKeys();
  }

  void TearDown() override {
    StructuredMetricsClient::Get()->UnsetDelegate();
  }

  void Init() {
    auto key_data_provider =
        std::make_unique<TestKeyDataProvider>(DeviceKeyFilePath());
    TestKeyDataProvider* test_key_data_provider = key_data_provider.get();
    auto recorder = std::make_unique<StructuredMetricsRecorder>(
        std::move(key_data_provider), std::make_unique<TestEventStorage>());

    service_ = std::make_unique<StructuredMetricsService>(&client_, &prefs_,
                                                          std::move(recorder));
    // Register the profile with the key data provider.
    test_key_data_provider->OnProfileAdded(ProfileKeyFilePath());
    Wait();
  }

  void EnableRecording() { service_->EnableRecording(); }
  void EnableReporting() { service_->EnableReporting(); }

  void DisableRecording() { service_->DisableRecording(); }
  void DisableReporting() { service_->DisableReporting(); }

  base::FilePath ProfileKeyFilePath() {
    return temp_dir_.GetPath()
        .Append(FILE_PATH_LITERAL("structured_metrics"))
        .Append(FILE_PATH_LITERAL("keys"));
  }

  base::FilePath DeviceKeyFilePath() {
    return temp_dir_.GetPath()
        .Append(FILE_PATH_LITERAL("structured_metrics"))
        .Append(FILE_PATH_LITERAL("device_keys"));
  }

  base::FilePath DeviceEventsFilePath() {
    return temp_dir_.GetPath()
        .Append(FILE_PATH_LITERAL("structured_metrics"))
        .Append(FILE_PATH_LITERAL("events"));
  }

  void WriteTestingProfileKeys() {
    const int today = (base::Time::Now() - base::Time::UnixEpoch()).InDays();

    KeyDataProto proto;
    KeyProto& key_one = (*proto.mutable_keys())[kProjectOneHash];
    key_one.set_key("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
    key_one.set_last_rotation(today);
    key_one.set_rotation_period(90);

    KeyProto& key_three = (*proto.mutable_keys())[kProjectThreeHash];
    key_three.set_key("cccccccccccccccccccccccccccccccc");
    key_three.set_last_rotation(today);
    key_three.set_rotation_period(90);

    base::CreateDirectory(ProfileKeyFilePath().DirName());
    ASSERT_TRUE(
        base::WriteFile(ProfileKeyFilePath(), proto.SerializeAsString()));
    Wait();
  }

  void WriteTestingDeviceKeys() {
    base::CreateDirectory(DeviceKeyFilePath().DirName());
    ASSERT_TRUE(base::WriteFile(DeviceKeyFilePath(),
                                KeyDataProto().SerializeAsString()));
    Wait();
  }

  int GetPersistedLogCount() {
    return prefs_.GetList(prefs::kLogStoreName).size();
  }

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

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

    ChromeUserMetricsExtension uma_proto;
    EXPECT_TRUE(metrics::DecodeLogDataToProto(
        result_unsent_log_store.staged_log(), &uma_proto));
    return uma_proto;
  }

  StructuredMetricsService& service() { return *service_.get(); }

  void Wait() { task_environment_.RunUntilIdle(); }

  void AdvanceClock(int hours) {
    task_environment_.AdvanceClock(base::Hours(hours));
  }

 private:
  // Test resources must be declared before TaskEnvironment so they outlive
  // the tasks that TaskEnvironment flushes during its destruction.
  base::ScopedTempDir temp_dir_;
  TestingPrefServiceSimple prefs_;

 protected:
  // The TaskEnvironment is the manager for the test's threads and tasks. It
  // must be declared after the resources that its tasks may access. Its
  // destructor will block and flush all tasks, ensuring they can safely use
  // the resources above before they are destroyed.
  base::test::TaskEnvironment task_environment_{
      base::test::TaskEnvironment::MainThreadType::UI,
      base::test::TaskEnvironment::ThreadPoolExecutionMode::QUEUED,
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};

  // TestRecorder is a delegate for the process-wide StructuredMetricsClient
  // singleton. Its lifetime is managed by Set/UnsetDelegate in
  // SetUp/TearDown.
  TestRecorder test_recorder_;

  // The service holds a raw pointer to this client, so `client_` must be
  // destroyed after `service_`.
  metrics::TestMetricsServiceClient client_;

  // The service under test. Declared last so it is destroyed first, before
  // its dependencies and before the TaskEnvironment that manages its tasks.
  std::unique_ptr<StructuredMetricsService> service_;
};

TEST_F(StructuredMetricsServiceTest, PurgeInMemory) {
  Init();

  EnableRecording();
  EnableReporting();

  StructuredMetricsClient::Record(
      std::move(TestEventOne().SetTestMetricTwo(1)));
  StructuredMetricsClient::Record(
      std::move(TestEventSeven().SetTestMetricSeven(1.0)));
  Wait();

  service_->Purge();
  service_->Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);

  // Nothing should be stored.
  EXPECT_THAT(GetPersistedLogCount(), 0);
}

TEST_F(StructuredMetricsServiceTest, PurgePersisted) {
  Init();

  EnableRecording();
  EnableReporting();

  StructuredMetricsClient::Record(
      std::move(TestEventOne().SetTestMetricTwo(1)));
  StructuredMetricsClient::Record(
      std::move(TestEventSeven().SetTestMetricSeven(1.0)));
  Wait();

  service_->Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);

  service_->Purge();

  // Need to make sure there is a log to read.
  service_->Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);

  // Nothing should be stored.
  EXPECT_THAT(GetPersistedLogCount(), 0);
}

TEST_F(StructuredMetricsServiceTest, RotateLogs) {
  Init();

  EnableRecording();
  EnableReporting();

  StructuredMetricsClient::Record(
      std::move(TestEventOne().SetTestMetricTwo(1)));
  StructuredMetricsClient::Record(
      std::move(TestEventSeven().SetTestMetricSeven(1)));
  Wait();

  service_->Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);

  const auto uma_proto = GetPersistedLog();
  EXPECT_THAT(uma_proto.structured_data().events().size(), 2);
  service_.reset();
}

TEST_F(StructuredMetricsServiceTest, SystemProfileFilled) {
  Init();

  EnableRecording();
  EnableReporting();

  StructuredMetricsClient::Record(
      std::move(TestEventOne().SetTestMetricTwo(1)));
  StructuredMetricsClient::Record(
      std::move(TestEventSeven().SetTestMetricSeven(1)));
  Wait();

  service_->Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);

  const auto uma_proto = GetPersistedLog();
  EXPECT_THAT(uma_proto.structured_data().events().size(), 2);
  EXPECT_TRUE(uma_proto.has_system_profile());

  const SystemProfileProto& system_profile = uma_proto.system_profile();
  EXPECT_EQ(system_profile.channel(), client_.GetChannel());
  EXPECT_EQ(system_profile.app_version(), client_.GetVersionString());
}

TEST_F(StructuredMetricsServiceTest, MetricsProviderDataInSystemProfile) {
  Init();

  service().RegisterMetricsProvider(std::make_unique<TestMetricsProvider>());

  EnableRecording();
  EnableReporting();

  StructuredMetricsClient::Record(
      std::move(TestEventOne().SetTestMetricTwo(1)));
  Wait();

  service_->Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);

  const auto uma_proto = GetPersistedLog();
  EXPECT_TRUE(uma_proto.has_system_profile());
  EXPECT_THAT(uma_proto.system_profile().field_trial(), SizeIs(2));
  EXPECT_EQ(uma_proto.system_profile().field_trial(0).name_id(), 1u);
  EXPECT_EQ(uma_proto.system_profile().field_trial(1).name_id(), 2u);
}

TEST_F(StructuredMetricsServiceTest, DoesNotRecordWhenRecordingDisabled) {
  Init();
  EnableRecording();
  EnableReporting();

  StructuredMetricsClient::Record(
      std::move(TestEventOne().SetTestMetricTwo(1)));
  StructuredMetricsClient::Record(
      std::move(TestEventSeven().SetTestMetricSeven(1)));
  Wait();

  DisableRecording();

  StructuredMetricsClient::Record(
      std::move(TestEventOne().SetTestMetricTwo(1)));
  StructuredMetricsClient::Record(
      std::move(TestEventSeven().SetTestMetricSeven(1)));
  Wait();

  EnableRecording();

  service_->Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);

  const auto uma_proto = GetPersistedLog();
  EXPECT_THAT(uma_proto.structured_data().events().size(), 2);
}

TEST_F(StructuredMetricsServiceTest, FlushOnShutdown) {
  Init();
  EnableRecording();
  EnableReporting();

  StructuredMetricsClient::Record(
      std::move(TestEventOne().SetTestMetricTwo(1)));
  StructuredMetricsClient::Record(
      std::move(TestEventSeven().SetTestMetricSeven(1)));
  Wait();

  // Will flush the log.
  service_.reset();

  const auto uma_proto = GetPersistedLog();
  EXPECT_THAT(uma_proto.structured_data().events().size(), 2);
}

TEST_F(StructuredMetricsServiceTest, FlushWithNoEvents) {
  Init();

  EnableRecording();
  EnableReporting();

  service_->Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  Wait();

  EXPECT_THAT(GetPersistedLogCount(), 0);
}

TEST_F(StructuredMetricsServiceTest, TimerRestartsAfterEmptyRotation) {
  Init();
  EnableRecording();
  EnableReporting();
  Wait();
  ASSERT_TRUE(service_->recorder()->CanProvideMetrics());

  // 1. Advance clock by 1 minute (initial delay) to trigger the first rotation
  // (empty).
  task_environment_.FastForwardBy(base::Minutes(1));
  Wait();

  // 2. Record an event.
  StructuredMetricsClient::Record(
      std::move(TestEventOne().SetTestMetricTwo(1)));
  Wait();

  // Verify event reached storage.
  ASSERT_TRUE(service_->recorder()->event_storage()->HasEvents());

  // 3. Advance clock far enough to trigger the second rotation.
  // Delay is 10 minutes. We already spent 1 minute for the initial delay.
  // Let's advance another 15 minutes to be safe.
  task_environment_.FastForwardBy(base::Minutes(15));
  Wait();

  // 4. The second rotation should have happened and stored the log even though
  // the first rotation was empty.
  service_->reporting_service_->log_store()->TrimAndPersistUnsentLogs(true);
  EXPECT_EQ(GetPersistedLogCount(), 1);
}

TEST_F(StructuredMetricsServiceTest, RotateLogsWithUnsentLogsExtractsEvents) {
  Init();
  EnableRecording();
  EnableReporting();
  Wait();
  ASSERT_TRUE(service_->recorder()->CanProvideMetrics());

  // 1. Manually add a log to the store by recording and flushing.
  StructuredMetricsClient::Record(
      std::move(TestEventOne().SetTestMetricTwo(1)));
  Wait();
  ASSERT_TRUE(service_->recorder()->event_storage()->HasEvents());

  service_->Flush(metrics::MetricsLogsEventManager::CreateReason::kUnknown);
  Wait();
  ASSERT_EQ(GetPersistedLogCount(), 1);
  ASSERT_FALSE(service_->recorder()->event_storage()->HasEvents());

  // 2. Record a SECOND event.
  StructuredMetricsClient::Record(
      std::move(TestEventOne().SetTestMetricTwo(2)));
  Wait();
  ASSERT_TRUE(service_->recorder()->event_storage()->HasEvents());

  // 3. Call RotateLogsAndSend directly (as if triggered by timer).
  service_->RotateLogsAndSend();
  Wait();

  // 4. We should now have 2 logs because the second event was extracted even
  // though the store already had a log.
  service_->reporting_service_->log_store()->TrimAndPersistUnsentLogs(true);
  EXPECT_EQ(GetPersistedLogCount(), 2);
}

}  // namespace metrics::structured
