// 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 "chrome/browser/metrics/structured/test/structured_metrics_mixin.h"

#include <memory>

#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/test_timeouts.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/metrics/chrome_metrics_service_accessor.h"
#include "components/metrics/log_decoder.h"
#include "components/metrics/metrics_switches.h"
#include "components/metrics/structured/structured_metrics_features.h"
#include "components/metrics/structured/structured_metrics_service.h"
#include "components/metrics/structured/test/test_event_storage.h"
#include "components/metrics/structured/test/test_key_data_provider.h"
#include "components/metrics_services_manager/metrics_services_manager.h"

namespace {

// Static hwid used for tests to populate the system profile proto.
constexpr char kHwid[] = "hwid";

class TestSystemProfileProvider : public metrics::MetricsProvider {
 public:
  TestSystemProfileProvider() = default;
  TestSystemProfileProvider(const TestSystemProfileProvider& recorder) = delete;
  TestSystemProfileProvider& operator=(
      const TestSystemProfileProvider& recorder) = delete;
  ~TestSystemProfileProvider() override = default;

  void ProvideSystemProfileMetrics(
      metrics::SystemProfileProto* proto) override {
    proto->mutable_hardware()->set_full_hardware_class(kHwid);
  }
};

}  // namespace

namespace metrics::structured {

StructuredMetricsMixin::StructuredMetricsMixin(
    InProcessBrowserTestMixinHost* host,
    bool setup_profile)
    : InProcessBrowserTestMixin(host), setup_profile_(setup_profile) {
  EXPECT_TRUE(temp_dir_.CreateUniqueTempDir());
}

StructuredMetricsMixin::~StructuredMetricsMixin() {
  ChromeMetricsServiceAccessor::SetMetricsAndCrashReportingForTesting(nullptr);
}

void StructuredMetricsMixin::SetUpOnMainThread() {
  InProcessBrowserTestMixin::SetUpOnMainThread();

  ChromeMetricsServiceAccessor::SetMetricsAndCrashReportingForTesting(
      &recording_state_);

  base::FilePath device_keys_path =
      temp_dir_.GetPath()
          .Append(FILE_PATH_LITERAL("structured"))
          .Append(FILE_PATH_LITERAL("device_keys"));
  base::FilePath profile_path =
      temp_dir_.GetPath().Append(FILE_PATH_LITERAL("profile"));

  auto key_data_provider =
      std::make_unique<TestKeyDataProvider>(device_keys_path);

  if (setup_profile_) {
    // Setup test profile directory immediately so that recording can happen.
    key_data_provider->OnProfileAdded(profile_path);
  }

  // Create test key data provider and initialize key data provider.
  // TODO(andrewbregger) make sure that all tests that rely on the persistent
  // storage are moved.
  auto recorder = std::make_unique<StructuredMetricsRecorder>(
      std::move(key_data_provider), std::make_unique<TestEventStorage>());

  g_browser_process->GetMetricsServicesManager()
      ->GetStructuredMetricsService()
      ->SetRecorderForTest(std::move(recorder));
}

StructuredMetricsRecorder* StructuredMetricsMixin::GetRecorder() {
  return GetService()->recorder();
}

StructuredMetricsService* StructuredMetricsMixin::GetService() {
  return g_browser_process->GetMetricsServicesManager()
      ->GetStructuredMetricsService();
}

std::optional<StructuredEventProto> StructuredMetricsMixin::FindEvent(
    uint64_t project_name_hash,
    uint64_t event_name_hash) {
  std::vector<StructuredEventProto> events =
      FindEvents(project_name_hash, event_name_hash);
  if (events.size() > 0) {
    return events[0];
  }
  return std::nullopt;
}

std::vector<StructuredEventProto> StructuredMetricsMixin::FindEvents(
    uint64_t project_name_hash,
    uint64_t event_name_hash) {
  std::vector<StructuredEventProto> events_vector;

  // TODO(andrewbregger): Create an API to allow events to be iterated over
  // without copying.
  const EventStorage<StructuredEventProto>* storage = GetEventStorage();

  EventsProto events;
  storage->CopyEvents(&events);

  for (const auto& event : events.events()) {
    if (event.project_name_hash() == project_name_hash &&
        event.event_name_hash() == event_name_hash) {
      events_vector.push_back(event);
    }
  }

  return events_vector;
}

void StructuredMetricsMixin::WaitUntilEventRecorded(uint64_t project_name_hash,
                                                    uint64_t event_name_hash) {
  // Check if event already exists.
  std::optional<StructuredEventProto> event =
      FindEvent(project_name_hash, event_name_hash);
  if (event.has_value()) {
    return;
  }

  // Wait for event since it does not exist yet.
  record_run_loop_ = std::make_unique<base::RunLoop>();
  base::RepeatingClosure callback =
      base::BindLambdaForTesting([project_name_hash, event_name_hash, this]() {
        std::optional<StructuredEventProto> event =
            FindEvent(project_name_hash, event_name_hash);

        if (event.has_value()) {
          record_run_loop_->Quit();
        }
      });
  GetRecorder()->SetEventRecordCallbackForTest(std::move(callback));

  // The timeout for this is set to about 3 seconds (scaled accordingly for
  // debug and sanitizer builds) because this should be ample time for the event
  // to show up after Event::Record() has been called. There is normally a delay
  // between flushes but this delay has been set to 0 for testing.
  base::test::ScopedRunLoopTimeout shortened_timeout{
      FROM_HERE, TestTimeouts::action_timeout() / 3};
  record_run_loop_->Run();
}

void StructuredMetricsMixin::WaitUntilKeysReady() {
  keys_run_loop_ = std::make_unique<base::RunLoop>();
  base::RepeatingClosure callback =
      base::BindLambdaForTesting([this]() { keys_run_loop_->Quit(); });
  GetRecorder()->SetOnReadyToRecord(std::move(callback));
  keys_run_loop_->Run();
}

void StructuredMetricsMixin::UpdateRecordingState(bool state) {
  recording_state_ = state;

  // Triggers rechecking of metrics state.
  g_browser_process->GetMetricsServicesManager()->UpdateUploadPermissions();
}

std::unique_ptr<ChromeUserMetricsExtension>
StructuredMetricsMixin::GetUmaProto() {
  StructuredMetricsService* service = GetService();
  auto* log_store = service->reporting_service_->log_store();

  if (!log_store->has_unsent_logs()) {
    return nullptr;
  }

  if (log_store->has_staged_log()) {
    // For testing purposes, we examine the content of a staged log without
    // ever sending the log, so discard any previously staged log.
    log_store->DiscardStagedLog();
  }

  log_store->StageNextLog();
  if (!log_store->has_staged_log()) {
    return nullptr;
  }

  std::unique_ptr<ChromeUserMetricsExtension> uma_proto =
      std::make_unique<ChromeUserMetricsExtension>();
  if (!metrics::DecodeLogDataToProto(log_store->staged_log(),
                                     uma_proto.get())) {
    return nullptr;
  }
  return uma_proto;
}

EventStorage<StructuredEventProto>* StructuredMetricsMixin::GetEventStorage() {
  return GetRecorder()->event_storage();
}

}  // namespace metrics::structured
