// Copyright 2021 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/external_metrics.h"
#include "components/metrics/structured/structured_metrics_features.h"

#include <memory>
#include <numeric>

#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "build/build_config.h"
#include "components/metrics/structured/proto/event_storage.pb.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace metrics {
namespace structured {
namespace {

using testing::UnorderedElementsAre;

// Make a simple testing proto with one |uma_events| message for each id in
// |ids|.
EventsProto MakeTestingProto(const std::vector<uint64_t>& ids,
                             uint64_t project_name_hash = 0) {
  EventsProto proto;

  for (const auto id : ids) {
    auto* event = proto.add_uma_events();
    event->set_project_name_hash(project_name_hash);
    event->set_profile_event_id(id);
  }

  return proto;
}

// Check that |proto| is consistent with the proto that would be generated by
// MakeTestingProto(ids).
void AssertEqualsTestingProto(const EventsProto& proto,
                              const std::vector<uint64_t>& ids) {
  ASSERT_EQ(proto.uma_events().size(), static_cast<int>(ids.size()));
  ASSERT_TRUE(proto.events().empty());

  for (size_t i = 0; i < ids.size(); ++i) {
    const auto& event = proto.uma_events(i);
    ASSERT_EQ(event.profile_event_id(), ids[i]);
    ASSERT_FALSE(event.has_event_name_hash());
    ASSERT_TRUE(event.metrics().empty());
  }
}

}  // namespace

class ExternalMetricsTest : public testing::Test {
 public:
  void SetUp() override {
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());

    // TODO(b/181724341): Remove this when the bluetooth metrics feature is
    // enabled by default.
    scoped_feature_list_.InitWithFeatures(
        /*enabled_features=*/{},
        /*disabled_features=*/{kBluetoothSessionizedMetrics});
  }

  void Init() {
    // We don't use the scheduling feature when testing ExternalMetrics, instead
    // we just call CollectMetrics directly. So make up a time interval here
    // that we'll never reach in a test.
    const auto one_hour = base::Hours(1);
    external_metrics_ = std::make_unique<ExternalMetrics>(
        temp_dir_.GetPath(), one_hour,
        base::BindRepeating(&ExternalMetricsTest::OnEventsCollected,
                            base::Unretained(this)));

    // For most tests the recording needs to be enabled.
    EnableRecording();
  }

  void EnableRecording() { external_metrics_->EnableRecording(); }

  void DisableRecording() { external_metrics_->DisableRecording(); }

  void CollectEvents() {
    external_metrics_->CollectEvents();
    Wait();
    CHECK(proto_.has_value());
  }

  void OnEventsCollected(const EventsProto& proto) {
    proto_ = std::move(proto);
  }

  void WriteToDisk(const std::string& name, const EventsProto& proto) {
    CHECK(base::WriteFile(temp_dir_.GetPath().Append(name),
                          proto.SerializeAsString()));
  }

  void WriteToDisk(const std::string& name, const std::string& str) {
    CHECK(base::WriteFile(temp_dir_.GetPath().Append(name), str));
  }

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

  base::test::ScopedFeatureList scoped_feature_list_;
  base::ScopedTempDir temp_dir_;
  std::unique_ptr<ExternalMetrics> external_metrics_;
  std::optional<EventsProto> proto_;

  base::test::TaskEnvironment task_environment_{
      base::test::TaskEnvironment::MainThreadType::UI,
      base::test::TaskEnvironment::ThreadPoolExecutionMode::QUEUED};
  base::HistogramTester histogram_tester_;
};

TEST_F(ExternalMetricsTest, ReadOneFile) {
  // Make one proto with three events.
  WriteToDisk("myproto", MakeTestingProto({111, 222, 333}));
  Init();

  CollectEvents();

  // We should have correctly picked up the three events.
  AssertEqualsTestingProto(proto_.value(), {111, 222, 333});
  // And the directory should now be empty.
  ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
}

TEST_F(ExternalMetricsTest, ReadManyFiles) {
  // Make three protos with three events each.
  WriteToDisk("first", MakeTestingProto({111, 222, 333}));
  WriteToDisk("second", MakeTestingProto({444, 555, 666}));
  WriteToDisk("third", MakeTestingProto({777, 888, 999}));
  Init();

  CollectEvents();

  // We should have correctly picked up the nine events. Don't check for order,
  // because we can't guarantee the files will be read from disk in any
  // particular order.
  std::vector<int64_t> ids;
  for (const auto& event : proto_.value().uma_events()) {
    ids.push_back(event.profile_event_id());
  }
  ASSERT_THAT(
      ids, UnorderedElementsAre(111, 222, 333, 444, 555, 666, 777, 888, 999));

  // The directory should be empty after reading.
  ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
}

TEST_F(ExternalMetricsTest, ReadZeroFiles) {
  Init();
  CollectEvents();
  // We should have an empty proto.
  AssertEqualsTestingProto(proto_.value(), {});
  // And the directory should be empty too.
  ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
}

TEST_F(ExternalMetricsTest, CollectTwice) {
  Init();
  WriteToDisk("first", MakeTestingProto({111, 222, 333}));
  CollectEvents();
  AssertEqualsTestingProto(proto_.value(), {111, 222, 333});

  WriteToDisk("first", MakeTestingProto({444}));
  CollectEvents();
  AssertEqualsTestingProto(proto_.value(), {444});
}

TEST_F(ExternalMetricsTest, HandleCorruptFile) {
  Init();

  WriteToDisk("invalid", "surprise i'm not a proto");
  WriteToDisk("valid", MakeTestingProto({111, 222, 333}));

  CollectEvents();
  AssertEqualsTestingProto(proto_.value(), {111, 222, 333});
  // Should have deleted the invalid file too.
  ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
}

// TODO(b/181724341): Remove this when the bluetooth metrics feature is enabled
// by default.
TEST_F(ExternalMetricsTest, FilterBluetoothEvents) {
  // Event name hash for cros's BluetoothPairingStateChanged event.
  const uint64_t event_hash = UINT64_C(11839023048095184048);

  Init();

  // Use the profile_event_id as an marker of which event is which, and assign a
  // bluetooth event hash to ids > 100.
  EventsProto proto;
  for (const auto id : {101, 1, 2, 102, 103, 3, 104}) {
    auto* event = proto.add_uma_events();
    event->set_profile_event_id(id);
    if (id > 100) {
      event->set_event_name_hash(event_hash);
    }
  }
  WriteToDisk("proto", proto);

  CollectEvents();
  AssertEqualsTestingProto(proto_.value(), {1, 2, 3});
}

TEST_F(ExternalMetricsTest, FileNumberReadCappedAndDiscarded) {
  // Setup feature.
  base::test::ScopedFeatureList feature_list;
  const int file_limit = 2;
  feature_list.InitAndEnableFeatureWithParameters(
      features::kStructuredMetrics,
      {{"file_limit", base::NumberToString(file_limit)}});

  Init();

  // File limit is set to 2. Include third file to test that it is omitted and
  // deleted.
  WriteToDisk("first", MakeTestingProto({111}));
  WriteToDisk("second", MakeTestingProto({222}));
  WriteToDisk("third", MakeTestingProto({333}));

  CollectEvents();

  // Number of events should be capped to the file limit since above records one
  // event per file.
  ASSERT_EQ(proto_.value().uma_events().size(), file_limit);

  // And the directory should be empty too.
  ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
}

TEST_F(ExternalMetricsTest, FilterDisallowedProjects) {
  Init();
  external_metrics_->AddDisallowedProjectForTest(2);

  // Add 3 events with a project of 1 and 2.
  WriteToDisk("first", MakeTestingProto({111}, 1));
  WriteToDisk("second", MakeTestingProto({222}, 2));
  WriteToDisk("third", MakeTestingProto({333}, 1));

  CollectEvents();

  // The events at second should be filtered.
  ASSERT_EQ(proto_.value().uma_events().size(), 2);

  std::vector<int64_t> ids;
  for (const auto& event : proto_.value().uma_events()) {
    ids.push_back(event.profile_event_id());
  }

  // Validate that only project 1 remains.
  ASSERT_THAT(ids, UnorderedElementsAre(111, 333));

  // And the directory should be empty too.
  ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
}

TEST_F(ExternalMetricsTest, DroppedEventsWhenDisabled) {
  Init();
  DisableRecording();

  // Add 3 events with a project of 1 and 2.
  WriteToDisk("first", MakeTestingProto({111}, 1));
  WriteToDisk("second", MakeTestingProto({222}, 2));
  WriteToDisk("third", MakeTestingProto({333}, 1));

  CollectEvents();

  // No events should have been collected.
  ASSERT_EQ(proto_.value().uma_events().size(), 0);

  // And the directory should be empty too.
  ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
}

// TODO(crbug.com/40941078): Failing consistently on MSAN.
#if defined(MEMORY_SANITIZER)
#define MAYBE_ProducedAndDroppedEventMetricCollected \
  DISABLED_ProducedAndDroppedEventMetricCollected
#else
#define MAYBE_ProducedAndDroppedEventMetricCollected \
  ProducedAndDroppedEventMetricCollected
#endif

TEST_F(ExternalMetricsTest, MAYBE_ProducedAndDroppedEventMetricCollected) {
  base::test::ScopedFeatureList feature_list;
  const int file_limit = 5;
  feature_list.InitAndEnableFeatureWithParameters(
      features::kStructuredMetrics,
      {{"file_limit", base::NumberToString(file_limit)}});

  Init();

  // Wifi
  WriteToDisk("event1", MakeTestingProto({0}, UINT64_C(4320592646346933548)));
  WriteToDisk("event2", MakeTestingProto({1}, UINT64_C(4320592646346933548)));
  // Bluetooth
  WriteToDisk("event3", MakeTestingProto({2}, UINT64_C(9074739597929991885)));
  WriteToDisk("event4", MakeTestingProto({3}, UINT64_C(9074739597929991885)));
  // Cellular
  WriteToDisk("event5", MakeTestingProto({4}, UINT64_C(8206859287963243715)));
  WriteToDisk("event6", MakeTestingProto({5}, UINT64_C(8206859287963243715)));
  // WIfi
  WriteToDisk("event7", MakeTestingProto({6}, UINT64_C(4320592646346933548)));
  WriteToDisk("event8", MakeTestingProto({7}, UINT64_C(4320592646346933548)));
  // Bluetooth
  WriteToDisk("event9", MakeTestingProto({8}, UINT64_C(9074739597929991885)));
  WriteToDisk("event10", MakeTestingProto({9}, UINT64_C(9074739597929991885)));

  CollectEvents();

  ASSERT_EQ(proto_.value().uma_events().size(), file_limit);

  // Unable to guarantee the order the events are read in. Using counts to
  // verify that the number of histograms produced are what is expected.
  base::HistogramTester::CountsMap produced_map =
      histogram_tester_.GetTotalCountsForPrefix(
          "StructuredMetrics.ExternalMetricsProduced2.");
  int produced_acc = 0;
  for (const auto& hist : produced_map) {
    produced_acc += hist.second;
  }

  base::HistogramTester::CountsMap dropped_map =
      histogram_tester_.GetTotalCountsForPrefix(
          "StructuredMetrics.ExternalMetricsDropped2.");

  int dropped_acc = 0;
  for (const auto& hist : dropped_map) {
    dropped_acc += hist.second;
  }

  EXPECT_EQ(produced_acc, 3);
  EXPECT_EQ(dropped_acc, 3);
}

// TODO(crbug.com/1148168): Add a test for concurrent reading and writing here
// once we know the specifics of how the lock in cros is performed.

}  // namespace structured
}  // namespace metrics
