// 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 <map>
#include <memory>

#include "base/check.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "components/leveldb_proto/testing/fake_db.h"
#include "media/base/media_switches.h"
#include "media/base/test_data_util.h"
#include "media/base/video_codecs.h"
#include "media/capabilities/video_decode_stats.pb.h"
#include "media/capabilities/video_decode_stats_db_impl.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/geometry/size.h"

using leveldb_proto::test::FakeDB;
using testing::_;
using testing::Eq;
using testing::Pointee;

namespace media {

class VideoDecodeStatsDBImplTest : public ::testing::Test {
 public:
  using VideoDescKey = VideoDecodeStatsDB::VideoDescKey;
  using DecodeStatsEntry = VideoDecodeStatsDB::DecodeStatsEntry;

  VideoDecodeStatsDBImplTest()
      : kStatsKeyVp9(VideoDescKey::MakeBucketedKey(VP9PROFILE_PROFILE3,
                                                   gfx::Size(1024, 768),
                                                   60,
                                                   "com.widevine.alpha",
                                                   true)),
        kStatsKeyAvc(VideoDescKey::MakeBucketedKey(H264PROFILE_MIN,
                                                   gfx::Size(1024, 768),
                                                   60,
                                                   "",
                                                   false)) {
    bool parsed_time = base::Time::FromString(
        VideoDecodeStatsDBImpl::kDefaultWriteTime, &kDefaultWriteTime);
    DCHECK(parsed_time);

    // Fake DB simply wraps a std::map with the LevelDB interface. We own the
    // map and will delete it in TearDown().
    fake_db_map_ = std::make_unique<FakeDB<DecodeStatsProto>::EntryMap>();
    // |stats_db_| will own this pointer, but we hold a reference to control
    // its behavior.
    auto db = std::make_unique<FakeDB<DecodeStatsProto>>(fake_db_map_.get());
    fake_db_ = db.get();

    // Wrap the fake proto DB with our interface.
    stats_db_ = base::WrapUnique(new VideoDecodeStatsDBImpl(std::move(db)));
  }

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

  ~VideoDecodeStatsDBImplTest() override {
    // Tests should always complete any pending operations
    VerifyNoPendingOps();
  }

  void VerifyOnePendingOp(std::string_view op_name) {
    EXPECT_EQ(stats_db_->pending_operations_.get_pending_ops_for_test().size(),
              1u);
    PendingOperations::PendingOperation* pending_op =
        stats_db_->pending_operations_.get_pending_ops_for_test()
            .begin()
            ->second.get();
    EXPECT_TRUE(pending_op->uma_str_.ends_with(op_name));
  }

  void VerifyNoPendingOps() {
    EXPECT_TRUE(
        stats_db_->pending_operations_.get_pending_ops_for_test().empty());
  }

  int GetMaxFramesPerBuffer() {
    return VideoDecodeStatsDBImpl::GetMaxFramesPerBuffer();
  }

  int GetMaxDaysToKeepStats() {
    return VideoDecodeStatsDBImpl::GetMaxDaysToKeepStats();
  }

  bool GetEnableUnweightedEntries() {
    return VideoDecodeStatsDBImpl::GetEnableUnweightedEntries();
  }

  static base::FieldTrialParams GetFieldTrialParams() {
    return VideoDecodeStatsDBImpl::GetFieldTrialParams();
  }

  void SetDBClock(base::Clock* clock) {
    stats_db_->set_wall_clock_for_test(clock);
  }

  void InitializeDB() {
    stats_db_->Initialize(base::BindOnce(
        &VideoDecodeStatsDBImplTest::OnInitialize, base::Unretained(this)));
    EXPECT_CALL(*this, OnInitialize(true));
    fake_db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK);
    testing::Mock::VerifyAndClearExpectations(this);
  }

  void AppendStats(const VideoDescKey& key, const DecodeStatsEntry& entry) {
    EXPECT_CALL(*this, MockAppendDecodeStatsCb(true));
    stats_db_->AppendDecodeStats(
        key, entry,
        base::BindOnce(&VideoDecodeStatsDBImplTest::MockAppendDecodeStatsCb,
                       base::Unretained(this)));
    VerifyOnePendingOp("Read");
    fake_db_->GetCallback(true);
    VerifyOnePendingOp("Write");
    fake_db_->UpdateCallback(true);
    testing::Mock::VerifyAndClearExpectations(this);
  }

  void VerifyReadStats(const VideoDescKey& key,
                       const DecodeStatsEntry& expected) {
    EXPECT_CALL(*this, MockGetDecodeStatsCb(true, Pointee(Eq(expected))));
    stats_db_->GetDecodeStats(
        key, base::BindOnce(&VideoDecodeStatsDBImplTest::GetDecodeStatsCb,
                            base::Unretained(this)));
    VerifyOnePendingOp("Read");
    fake_db_->GetCallback(true);
    testing::Mock::VerifyAndClearExpectations(this);
  }

  void VerifyEmptyStats(const VideoDescKey& key) {
    EXPECT_CALL(*this, MockGetDecodeStatsCb(true, nullptr));
    stats_db_->GetDecodeStats(
        key, base::BindOnce(&VideoDecodeStatsDBImplTest::GetDecodeStatsCb,
                            base::Unretained(this)));
    VerifyOnePendingOp("Read");
    fake_db_->GetCallback(true);
    testing::Mock::VerifyAndClearExpectations(this);
  }

  // Unwraps move-only parameters to pass to the mock function.
  void GetDecodeStatsCb(bool success, std::unique_ptr<DecodeStatsEntry> entry) {
    MockGetDecodeStatsCb(success, entry.get());
  }

  void AppendToProtoDB(const VideoDescKey& key,
                       const DecodeStatsProto* const proto) {
    base::RunLoop run_loop;
    base::OnceCallback<void(bool)> update_done_cb = base::BindOnce(
        [](base::RunLoop* run_loop, bool success) {
          ASSERT_TRUE(success);
          run_loop->Quit();
        },
        Unretained(&run_loop));

    using DBType = leveldb_proto::ProtoDatabase<DecodeStatsProto>;
    std::unique_ptr<DBType::KeyEntryVector> entries =
        std::make_unique<DBType::KeyEntryVector>();
    entries->emplace_back(key.Serialize(), *proto);

    fake_db_->UpdateEntries(std::move(entries),
                            std::make_unique<leveldb_proto::KeyVector>(),
                            std::move(update_done_cb));

    fake_db_->UpdateCallback(true);
    run_loop.Run();
  }

  MOCK_METHOD1(OnInitialize, void(bool success));

  MOCK_METHOD2(MockGetDecodeStatsCb,
               void(bool success, DecodeStatsEntry* entry));

  MOCK_METHOD1(MockAppendDecodeStatsCb, void(bool success));

  MOCK_METHOD0(MockClearStatsCb, void());

 protected:
  base::test::TaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};

  const VideoDescKey kStatsKeyVp9;
  const VideoDescKey kStatsKeyAvc;

  // Const in practice, but not marked const for compatibility with
  // base::Time::FromString.
  base::Time kDefaultWriteTime;

  // See documentation in constructor.
  std::unique_ptr<FakeDB<DecodeStatsProto>::EntryMap> fake_db_map_;
  std::unique_ptr<VideoDecodeStatsDBImpl> stats_db_;
  raw_ptr<FakeDB<DecodeStatsProto>> fake_db_;
};

TEST_F(VideoDecodeStatsDBImplTest, InitializeFailed) {
  stats_db_->Initialize(base::BindOnce(
      &VideoDecodeStatsDBImplTest::OnInitialize, base::Unretained(this)));
  EXPECT_CALL(*this, OnInitialize(false));
  fake_db_.ExtractAsDangling()->InitStatusCallback(
      leveldb_proto::Enums::InitStatus::kError);
}

TEST_F(VideoDecodeStatsDBImplTest, InitializeTimedOut) {
  // Queue up an Initialize.
  stats_db_->Initialize(base::BindOnce(
      &VideoDecodeStatsDBImplTest::OnInitialize, base::Unretained(this)));
  VerifyOnePendingOp("Initialize");

  // Move time forward enough to trigger timeout.
  EXPECT_CALL(*this, OnInitialize(_)).Times(0);
  task_environment_.FastForwardBy(base::Seconds(100));
  task_environment_.RunUntilIdle();

  // Verify we didn't get an init callback and task is no longer considered
  // pending (because it timed out).
  testing::Mock::VerifyAndClearExpectations(this);
  VerifyNoPendingOps();

  // Verify callback still works if init completes very late.
  EXPECT_CALL(*this, OnInitialize(false));
  fake_db_.ExtractAsDangling()->InitStatusCallback(
      leveldb_proto::Enums::InitStatus::kError);
}

TEST_F(VideoDecodeStatsDBImplTest, ReadExpectingNothing) {
  InitializeDB();
  VerifyEmptyStats(kStatsKeyVp9);
}

TEST_F(VideoDecodeStatsDBImplTest, WriteReadAndClear) {
  InitializeDB();

  // Append and read back some VP9 stats.
  DecodeStatsEntry entry(1000, 2, 10);
  AppendStats(kStatsKeyVp9, entry);
  VerifyReadStats(kStatsKeyVp9, entry);

  // Reading with the wrong key (different codec) should still return nothing.
  VerifyEmptyStats(kStatsKeyAvc);

  // Appending same VP9 stats should read back as 2x the initial entry.
  AppendStats(kStatsKeyVp9, entry);
  DecodeStatsEntry aggregate_entry(2000, 4, 20);
  VerifyReadStats(kStatsKeyVp9, aggregate_entry);

  // Clear all stats from the DB.
  EXPECT_CALL(*this, MockClearStatsCb);
  stats_db_->ClearStats(base::BindOnce(
      &VideoDecodeStatsDBImplTest::MockClearStatsCb, base::Unretained(this)));
  VerifyOnePendingOp("Clear");
  fake_db_->UpdateCallback(true);

  // Database is now empty. Expect null entry.
  VerifyEmptyStats(kStatsKeyVp9);
}

TEST_F(VideoDecodeStatsDBImplTest, ConfigureMaxFramesPerBuffer) {
  // Setup field trial to use a tiny window of 1 decoded frame.

  base::test::ScopedFeatureList scoped_feature_list;
  std::unique_ptr<base::FieldTrialList> field_trial_list;

  int previous_max_frames_per_buffer = GetMaxFramesPerBuffer();
  int new_max_frames_per_buffer = 1;

  ASSERT_LT(new_max_frames_per_buffer, previous_max_frames_per_buffer);

  // Override field trial.
  base::FieldTrialParams params;
  params[VideoDecodeStatsDBImpl::kMaxFramesPerBufferParamName] =
      base::NumberToString(new_max_frames_per_buffer);

  scoped_feature_list.InitAndEnableFeatureWithParameters(
      media::kMediaCapabilitiesWithParameters, params);

  EXPECT_EQ(GetFieldTrialParams(), params);

  EXPECT_EQ(new_max_frames_per_buffer, GetMaxFramesPerBuffer());

  // Now verify the configured window is used by writing a frame and then
  // writing another.

  InitializeDB();

  // Append single frame which was, sadly, dropped and not efficient.
  DecodeStatsEntry entry(1, 1, 0);
  AppendStats(kStatsKeyVp9, entry);
  VerifyReadStats(kStatsKeyVp9, entry);

  // Appending another frame which is *not* dropped and *is* efficient.
  // Verify that only this last entry is still in the buffer (no aggregation).
  DecodeStatsEntry second_entry(1, 0, 1);
  AppendStats(kStatsKeyVp9, second_entry);
  VerifyReadStats(kStatsKeyVp9, second_entry);
}

namespace {

class DBClockScope {
 public:
  DBClockScope(VideoDecodeStatsDBImplTest* test_class, base::Clock* clock)
      : test_class_(test_class) {
    test_class_->SetDBClock(clock);
  }
  ~DBClockScope() { test_class_->SetDBClock(nullptr); }

 private:
  const raw_ptr<VideoDecodeStatsDBImplTest> test_class_;
};

}  // namespace

TEST_F(VideoDecodeStatsDBImplTest, ConfigureExpireDays) {
  base::test::ScopedFeatureList scoped_feature_list;
  std::unique_ptr<base::FieldTrialList> field_trial_list;

  int previous_max_days_to_keep_stats = GetMaxDaysToKeepStats();
  int new_max_days_to_keep_stats = 4;

  ASSERT_LT(new_max_days_to_keep_stats, previous_max_days_to_keep_stats);

  // Override field trial.
  base::FieldTrialParams params;
  params[VideoDecodeStatsDBImpl::kMaxDaysToKeepStatsParamName] =
      base::NumberToString(new_max_days_to_keep_stats);

  scoped_feature_list.InitAndEnableFeatureWithParameters(
      media::kMediaCapabilitiesWithParameters, params);

  EXPECT_EQ(GetFieldTrialParams(), params);

  EXPECT_EQ(new_max_days_to_keep_stats, GetMaxDaysToKeepStats());

  InitializeDB();

  // Inject a test clock and initialize with the current time.
  base::SimpleTestClock clock;
  DBClockScope clock_scope(this, &clock);
  clock.SetNow(base::Time::Now());

  // Append and verify read-back.
  AppendStats(kStatsKeyVp9, DecodeStatsEntry(200, 20, 2));
  VerifyReadStats(kStatsKeyVp9, DecodeStatsEntry(200, 20, 2));

  // Some simple math to avoid troubles of integer division.
  int half_days_to_keep_stats = new_max_days_to_keep_stats / 2;
  int remaining_days_to_keep_stats =
      new_max_days_to_keep_stats - half_days_to_keep_stats;

  // Advance time half way through grace period. Verify stats not expired.
  clock.Advance(base::Days(half_days_to_keep_stats));
  VerifyReadStats(kStatsKeyVp9, DecodeStatsEntry(200, 20, 2));

  // Advance time 1 day beyond grace period, verify stats are expired.
  clock.Advance(base::Days((remaining_days_to_keep_stats) + 1));
  VerifyEmptyStats(kStatsKeyVp9);

  // Advance the clock 100 extra days. Verify stats still expired.
  clock.Advance(base::Days(100));
  VerifyEmptyStats(kStatsKeyVp9);
}

TEST_F(VideoDecodeStatsDBImplTest, FailedWrite) {
  InitializeDB();

  // Expect the callback to indicate success = false when the write fails.
  EXPECT_CALL(*this, MockAppendDecodeStatsCb(false));

  // Append stats, but fail the internal DB update.
  stats_db_->AppendDecodeStats(
      kStatsKeyVp9, DecodeStatsEntry(1000, 2, 10),
      base::BindOnce(&VideoDecodeStatsDBImplTest::MockAppendDecodeStatsCb,
                     base::Unretained(this)));
  fake_db_->GetCallback(true);
  fake_db_->UpdateCallback(false);
}

TEST_F(VideoDecodeStatsDBImplTest, FillBufferInMixedIncrements) {
  InitializeDB();

  // Setup DB entry that half fills the buffer with 10% of frames dropped and
  // 50% of frames power efficient.
  const int kNumFramesEntryA = GetMaxFramesPerBuffer() / 2;
  DecodeStatsEntry entryA(kNumFramesEntryA, std::round(0.1 * kNumFramesEntryA),
                          std::round(0.5 * kNumFramesEntryA));
  const double kDropRateA =
      static_cast<double>(entryA.frames_dropped) / entryA.frames_decoded;
  const double kEfficientRateA =
      static_cast<double>(entryA.frames_power_efficient) /
      entryA.frames_decoded;

  // Append entryA to half fill the buffer and verify read. Verify read.
  AppendStats(kStatsKeyVp9, entryA);
  VerifyReadStats(kStatsKeyVp9, entryA);

  // Append same entryA again to completely fill the buffer. Verify read gives
  // out aggregated stats (2x the initial entryA)
  AppendStats(kStatsKeyVp9, entryA);
  VerifyReadStats(
      kStatsKeyVp9,
      DecodeStatsEntry(GetMaxFramesPerBuffer(),
                       std::round(kDropRateA * GetMaxFramesPerBuffer()),
                       std::round(kEfficientRateA * GetMaxFramesPerBuffer())));

  // This row in the DB is now "full" (appended frames >= kMaxFramesPerBuffer)!
  //
  // Future appends will not increase the total count of decoded frames. The
  // ratios of dropped and power efficient frames will be a weighted average.
  // The weight of new appends is determined by how much of the buffer they
  // fill, i.e.
  //
  //   new_append_weight = min(1, new_append_frame_count / kMaxFramesPerBuffer);
  //   old_data_weight = 1 - new_append_weight;
  //
  // The calculation for dropped ratios (same for power efficient) then becomes:
  //
  //   aggregate_drop_ratio = old_drop_ratio * old_drop_weight +
  //                          new_drop_ratio * new_data_weight;
  //
  // See implementation for more details.

  // Append same entryA a 3rd time. Verify we still only get the aggregated
  // stats from above (2x entryA) because the buffer is full and the ratio of
  // dropped and efficient frames is the same.
  AppendStats(kStatsKeyVp9, entryA);
  VerifyReadStats(
      kStatsKeyVp9,
      DecodeStatsEntry(GetMaxFramesPerBuffer(),
                       std::round(kDropRateA * GetMaxFramesPerBuffer()),
                       std::round(kEfficientRateA * GetMaxFramesPerBuffer())));

  // Append entryB that will fill just 10% of the buffer. The new entry has
  // different rates of dropped and power efficient frames to help verify that
  // it is given proper weight as it mixes with existing data in the buffer.
  const int kNumFramesEntryB = std::round(.1 * GetMaxFramesPerBuffer());
  DecodeStatsEntry entryB(kNumFramesEntryB, std::round(0.25 * kNumFramesEntryB),
                          std::round(1 * kNumFramesEntryB));
  const double kDropRateB =
      static_cast<double>(entryB.frames_dropped) / entryB.frames_decoded;
  const double kEfficientRateB =
      static_cast<double>(entryB.frames_power_efficient) /
      entryB.frames_decoded;
  AppendStats(kStatsKeyVp9, entryB);
  // Verify that buffer is still full, but dropped and power efficient frame
  // rates are now higher according to entryB's impact (10%) on the full buffer.
  double mixed_drop_rate = .1 * kDropRateB + .9 * kDropRateA;
  double mixed_effiency_rate = .1 * kEfficientRateB + .9 * kEfficientRateA;
  VerifyReadStats(
      kStatsKeyVp9,
      DecodeStatsEntry(
          GetMaxFramesPerBuffer(),
          std::round(GetMaxFramesPerBuffer() * mixed_drop_rate),
          std::round(GetMaxFramesPerBuffer() * mixed_effiency_rate)));

  // After appending entryB again, verify aggregate ratios behave according to
  // the formula above (appending repeated entryB brings ratios closer to those
  // in entryB, further from entryA).
  AppendStats(kStatsKeyVp9, entryB);
  mixed_drop_rate = .1 * kDropRateB + .9 * mixed_drop_rate;
  mixed_effiency_rate = .1 * kEfficientRateB + .9 * mixed_effiency_rate;
  VerifyReadStats(
      kStatsKeyVp9,
      DecodeStatsEntry(
          GetMaxFramesPerBuffer(),
          std::round(GetMaxFramesPerBuffer() * mixed_drop_rate),
          std::round(GetMaxFramesPerBuffer() * mixed_effiency_rate)));

  // Appending entry*A* again, verify aggregate ratios behave according to
  // the formula above (ratio's move back in direction of those in entryA).
  // Since entryA fills half the buffer it gets a higher weight than entryB did
  // above.
  AppendStats(kStatsKeyVp9, entryA);
  mixed_drop_rate = .5 * kDropRateA + .5 * mixed_drop_rate;
  mixed_effiency_rate = .5 * kEfficientRateA + .5 * mixed_effiency_rate;
  VerifyReadStats(
      kStatsKeyVp9,
      DecodeStatsEntry(
          GetMaxFramesPerBuffer(),
          std::round(GetMaxFramesPerBuffer() * mixed_drop_rate),
          std::round(GetMaxFramesPerBuffer() * mixed_effiency_rate)));

  // Now append entryC with a frame count of 2x the buffer max. Verify entryC
  // gets 100% of the weight, erasing the mixed stats from earlier appends.
  const int kNumFramesEntryC = 2 * GetMaxFramesPerBuffer();
  DecodeStatsEntry entryC(kNumFramesEntryC, std::round(0.3 * kNumFramesEntryC),
                          std::round(0.25 * kNumFramesEntryC));
  const double kDropRateC =
      static_cast<double>(entryC.frames_dropped) / entryC.frames_decoded;
  const double kEfficientRateC =
      static_cast<double>(entryC.frames_power_efficient) /
      entryC.frames_decoded;
  AppendStats(kStatsKeyVp9, entryC);
  VerifyReadStats(
      kStatsKeyVp9,
      DecodeStatsEntry(GetMaxFramesPerBuffer(),
                       std::round(GetMaxFramesPerBuffer() * kDropRateC),
                       std::round(GetMaxFramesPerBuffer() * kEfficientRateC)));
}

// Overfilling an empty buffer triggers the codepath to compute weighted dropped
// and power efficient ratios under a circumstance where the existing counts are
// all zero. This test ensures that we don't do any dividing by zero with that
// empty data.
TEST_F(VideoDecodeStatsDBImplTest, OverfillEmptyBuffer) {
  InitializeDB();

  // Setup DB entry that overflows the buffer max (by 1) with 10% of frames
  // dropped and 50% of frames power efficient.
  const int kNumFramesOverfill = GetMaxFramesPerBuffer() + 1;
  DecodeStatsEntry entryA(kNumFramesOverfill,
                          std::round(0.1 * kNumFramesOverfill),
                          std::round(0.5 * kNumFramesOverfill));

  // Append entry to completely fill the buffer and verify read.
  AppendStats(kStatsKeyVp9, entryA);
  // Read-back stats should have same ratios, but scaled such that
  // frames_decoded = GetMaxFramesPerBuffer().
  DecodeStatsEntry readBackEntryA(GetMaxFramesPerBuffer(),
                                  std::round(0.1 * GetMaxFramesPerBuffer()),
                                  std::round(0.5 * GetMaxFramesPerBuffer()));
  VerifyReadStats(kStatsKeyVp9, readBackEntryA);

  // Append another entry that again overfills with different dropped and power
  // efficient ratios. Verify that read-back only reflects latest entry.
  DecodeStatsEntry entryB(kNumFramesOverfill,
                          std::round(0.2 * kNumFramesOverfill),
                          std::round(0.6 * kNumFramesOverfill));
  AppendStats(kStatsKeyVp9, entryB);
  // Read-back stats should have same ratios, but scaled such that
  // frames_decoded = GetMaxFramesPerBuffer().
  DecodeStatsEntry readBackEntryB(GetMaxFramesPerBuffer(),
                                  std::round(0.2 * GetMaxFramesPerBuffer()),
                                  std::round(0.6 * GetMaxFramesPerBuffer()));
  VerifyReadStats(kStatsKeyVp9, readBackEntryB);
}

TEST_F(VideoDecodeStatsDBImplTest, NoWriteDateReadAndExpire) {
  InitializeDB();

  // Seed the fake proto DB with an old-style entry lacking a write date. This
  // will cause the DB to use kDefaultWriteTime.
  DecodeStatsProto proto_lacking_date;
  proto_lacking_date.set_frames_decoded(100);
  proto_lacking_date.set_frames_dropped(10);
  proto_lacking_date.set_frames_power_efficient(1);
  fake_db_map_->emplace(kStatsKeyVp9.Serialize(), proto_lacking_date);

  // Set "now" to be *before* the default write date. This will be the common
  // case when the proto update (adding last_write_date) first ships (i.e. we
  // don't want to immediately expire all the existing data).
  base::SimpleTestClock clock;
  DBClockScope clock_scope(this, &clock);
  clock.SetNow(kDefaultWriteTime - base::Days(10));
  // Verify the stats are readable (not expired).
  VerifyReadStats(kStatsKeyVp9, DecodeStatsEntry(100, 10, 1));

  // Set "now" to be in the middle of the grace period. Verify stats are still
  // readable (not expired).
  clock.SetNow(kDefaultWriteTime + base::Days(GetMaxDaysToKeepStats() / 2));
  VerifyReadStats(kStatsKeyVp9, DecodeStatsEntry(100, 10, 1));

  // Set the clock 1 day beyond the expiry date. Verify stats are no longer
  // readable due to expiration.
  clock.SetNow(kDefaultWriteTime + base::Days(GetMaxDaysToKeepStats() + 1));
  VerifyEmptyStats(kStatsKeyVp9);

  // Write some stats to the entry. Verify we get back exactly what's written
  // without summing with the expired stats.
  AppendStats(kStatsKeyVp9, DecodeStatsEntry(50, 5, 0));
  VerifyReadStats(kStatsKeyVp9, DecodeStatsEntry(50, 5, 0));
}

TEST_F(VideoDecodeStatsDBImplTest, NoWriteDateAppendReadAndExpire) {
  InitializeDB();

  // Seed the fake proto DB with an old-style entry lacking a write date. This
  // will cause the DB to use kDefaultWriteTime.
  DecodeStatsProto proto_lacking_date;
  proto_lacking_date.set_frames_decoded(100);
  proto_lacking_date.set_frames_dropped(10);
  proto_lacking_date.set_frames_power_efficient(1);
  fake_db_map_->emplace(kStatsKeyVp9.Serialize(), proto_lacking_date);

  // Set "now" to be *before* the default write date. This will be the common
  // case when the proto update (adding last_write_date) first ships (i.e. we
  // don't want to immediately expire all the existing data).
  base::SimpleTestClock clock;
  DBClockScope clock_scope(this, &clock);
  clock.SetNow(kDefaultWriteTime - base::Days(10));
  // Verify the stats are readable (not expired).
  VerifyReadStats(kStatsKeyVp9, DecodeStatsEntry(100, 10, 1));

  // Append some stats and verify the aggregate math is correct. This will
  // update the last_write_date to the current clock time.
  AppendStats(kStatsKeyVp9, DecodeStatsEntry(200, 20, 2));
  VerifyReadStats(kStatsKeyVp9, DecodeStatsEntry(300, 30, 3));

  // Set "now" to be in the middle of the grace period. Verify stats are still
  // readable (not expired).
  clock.SetNow(kDefaultWriteTime + base::Days(GetMaxDaysToKeepStats() / 2));
  VerifyReadStats(kStatsKeyVp9, DecodeStatsEntry(300, 30, 3));

  // Set the clock 1 day beyond the expiry date. Verify stats are no longer
  // readable due to expiration.
  clock.SetNow(kDefaultWriteTime + base::Days(GetMaxDaysToKeepStats() + 1));
  VerifyEmptyStats(kStatsKeyVp9);
}

TEST_F(VideoDecodeStatsDBImplTest, AppendAndExpire) {
  InitializeDB();

  // Inject a test clock and initialize with the current time.
  base::SimpleTestClock clock;
  DBClockScope clock_scope(this, &clock);
  clock.SetNow(base::Time::Now());

  // Append and verify read-back.
  AppendStats(kStatsKeyVp9, DecodeStatsEntry(200, 20, 2));
  VerifyReadStats(kStatsKeyVp9, DecodeStatsEntry(200, 20, 2));

  // Advance time half way through grace period. Verify stats not expired.
  clock.Advance(base::Days(GetMaxDaysToKeepStats() / 2));
  VerifyReadStats(kStatsKeyVp9, DecodeStatsEntry(200, 20, 2));

  // Advance time 1 day beyond grace period, verify stats are expired.
  clock.Advance(base::Days((GetMaxDaysToKeepStats() / 2) + 1));
  VerifyEmptyStats(kStatsKeyVp9);

  // Advance the clock 100 days. Verify stats still expired.
  clock.Advance(base::Days(100));
  VerifyEmptyStats(kStatsKeyVp9);
}

TEST_F(VideoDecodeStatsDBImplTest, EnableUnweightedEntries) {
  base::test::ScopedFeatureList scoped_feature_list;
  std::unique_ptr<base::FieldTrialList> field_trial_list;

  // Default is false.
  EXPECT_FALSE(GetEnableUnweightedEntries());

  // Override field trial.
  base::FieldTrialParams params;
  params[VideoDecodeStatsDBImpl::kEnableUnweightedEntriesParamName] = "true";
  scoped_feature_list.InitAndEnableFeatureWithParameters(
      media::kMediaCapabilitiesWithParameters, params);

  EXPECT_EQ(GetFieldTrialParams(), params);

  // Confirm field trial overridden.
  EXPECT_TRUE(GetMaxDaysToKeepStats());

  InitializeDB();

  // Append 200 frames with 10% dropped, 1% efficient.
  AppendStats(kStatsKeyVp9, DecodeStatsEntry(200, 0.10 * 200, 0.01 * 200));
  // Use real doubles to keep track of these things to make sure the precision
  // math for repeating decimals works out with what's done internally.
  int num_appends = 1;
  double unweighted_smoothness_avg = 0.10;
  double unweighted_efficiency_avg = 0.01;

  // NOTE, the members of DecodeStatsEntry have a different meaning when using
  // unweighted DB entries. The denominator is 100,000 * the number of appends
  // and the numerator is whatever value achieves the correct unweighted ratio
  // for those appends. See detailed comment in
  // VideoDecodeStatsDBImpl::OnGotDecodeStats();
  const int kNumAppendScale = 100000;
  int expected_denominator = kNumAppendScale * num_appends;
  VerifyReadStats(
      kStatsKeyVp9,
      DecodeStatsEntry(expected_denominator,
                       unweighted_smoothness_avg * expected_denominator,
                       unweighted_efficiency_avg * expected_denominator));

  // Append 20K frames with 5% dropped and 10% efficient.
  AppendStats(kStatsKeyVp9,
              DecodeStatsEntry(20000, 0.05 * 20000, 0.10 * 20000));
  num_appends++;
  unweighted_smoothness_avg = (0.10 + 0.05) / num_appends;
  unweighted_efficiency_avg = (0.01 + 0.10) / num_appends;

  // While new record had 100x more frames than the previous append, the ratios
  // should be an unweighted average of the two records (7.5% dropped and
  // 5.5% efficient).
  expected_denominator = kNumAppendScale * num_appends;
  VerifyReadStats(
      kStatsKeyVp9,
      DecodeStatsEntry(expected_denominator,
                       unweighted_smoothness_avg * expected_denominator,
                       unweighted_efficiency_avg * expected_denominator));

  // Append 1M frames with 3.4567% dropped and 3.4567% efficient.
  AppendStats(kStatsKeyVp9, DecodeStatsEntry(1000000, 0.012345 * 1000000,
                                             0.034567 * 1000000));
  num_appends++;
  unweighted_smoothness_avg = (0.10 + 0.05 + 0.012345) / num_appends;
  unweighted_efficiency_avg = (0.01 + 0.10 + 0.034567) / num_appends;

  // Here, the ratios should still be averaged in the unweighted fashion, but
  // truncated after the 3rd decimal place of the percentage (e.g. 1.234%
  // or the 5th decimal place when represented as a fraction of 1 (0.01234)).
  expected_denominator = kNumAppendScale * num_appends;
  VerifyReadStats(
      kStatsKeyVp9,
      DecodeStatsEntry(expected_denominator,
                       unweighted_smoothness_avg * expected_denominator,
                       unweighted_efficiency_avg * expected_denominator));
}

TEST_F(VideoDecodeStatsDBImplTest, DiscardCorruptedDBData) {
  InitializeDB();

  // Inject a test clock and initialize with the current time.
  base::SimpleTestClock clock;
  DBClockScope clock_scope(this, &clock);
  clock.SetNow(base::Time::Now());

  // Construct several distinct key values for storing/retrieving the corrupted
  // data. The details of the keys are not important.
  const auto keyA = VideoDescKey::MakeBucketedKey(
      VP9PROFILE_PROFILE0, gfx::Size(1024, 768), 60, "", false);
  const auto keyB = VideoDescKey::MakeBucketedKey(
      VP9PROFILE_PROFILE1, gfx::Size(1024, 768), 60, "", false);
  const auto keyC = VideoDescKey::MakeBucketedKey(
      VP9PROFILE_PROFILE2, gfx::Size(1024, 768), 60, "", false);
  const auto keyD = VideoDescKey::MakeBucketedKey(
      VP9PROFILE_PROFILE3, gfx::Size(1024, 768), 60, "", false);
  const auto keyE = VideoDescKey::MakeBucketedKey(
      H264PROFILE_BASELINE, gfx::Size(1024, 768), 60, "", false);
  const auto keyF = VideoDescKey::MakeBucketedKey(
      H264PROFILE_MAIN, gfx::Size(1024, 768), 60, "", false);
  const auto keyG = VideoDescKey::MakeBucketedKey(
      H264PROFILE_EXTENDED, gfx::Size(1024, 768), 60, "", false);

  // Start with a proto that represents a valid uncorrupted and unexpired entry.
  DecodeStatsProto protoA;
  protoA.set_frames_decoded(100);
  protoA.set_frames_dropped(15);
  protoA.set_frames_power_efficient(50);
  protoA.set_last_write_date(clock.Now().InMillisecondsFSinceUnixEpoch());
  protoA.set_unweighted_average_frames_dropped(15.0 / 100);
  protoA.set_unweighted_average_frames_efficient(50.0 / 100);
  protoA.set_num_unweighted_playbacks(1);

  // Append it and read it back without issue.
  AppendToProtoDB(keyA, &protoA);
  VerifyReadStats(keyA, DecodeStatsEntry(100, 15, 50));

  // Make the valid proto invalid with more dropped frames than decoded. Verify
  // you can't read it back (filtered for corruption).
  DecodeStatsProto protoB(protoA);
  protoB.set_frames_dropped(150);
  AppendToProtoDB(keyB, &protoB);
  VerifyEmptyStats(keyB);

  // Make an invalid proto with more power efficient frames than decoded. Verify
  // you can't read it back (filtered for corruption).
  DecodeStatsProto protoC(protoA);
  protoC.set_frames_power_efficient(150);
  AppendToProtoDB(keyC, &protoC);
  VerifyEmptyStats(keyC);

  // Make an invalid proto with an unweighted average dropped ratio > 1.
  DecodeStatsProto protoD(protoA);
  protoD.set_unweighted_average_frames_dropped(2.0);
  AppendToProtoDB(keyD, &protoD);
  VerifyEmptyStats(keyD);

  // Make an invalid proto with an unweighted average efficient ratio > 1.
  DecodeStatsProto protoE(protoA);
  protoE.set_unweighted_average_frames_efficient(2.0);
  AppendToProtoDB(keyE, &protoE);
  VerifyEmptyStats(keyE);

  // Make an invalid proto with a negative last write date.
  DecodeStatsProto protoF(protoA);
  protoF.set_last_write_date(-1.0);
  AppendToProtoDB(keyF, &protoF);
  VerifyEmptyStats(keyF);

  // Make an invalid  proto with a last write date in the future.
  DecodeStatsProto protoG(protoA);
  protoG.set_last_write_date(
      (clock.Now() + base::Days(1)).InMillisecondsFSinceUnixEpoch());
  AppendToProtoDB(keyG, &protoG);
  VerifyEmptyStats(keyG);
}

}  // namespace media
