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

#include "media/blink/watch_time_component.h"

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/platform/web_media_player.h"

namespace media {

class WatchTimeInterceptor : public mojom::WatchTimeRecorder {
 public:
  WatchTimeInterceptor() = default;
  ~WatchTimeInterceptor() override = default;

  // mojom::WatchTimeRecorder implementation:
  MOCK_METHOD2(RecordWatchTime, void(WatchTimeKey, base::TimeDelta));
  MOCK_METHOD1(FinalizeWatchTime, void(const std::vector<WatchTimeKey>&));
  MOCK_METHOD1(OnError, void(PipelineStatus));
  MOCK_METHOD1(UpdateUnderflowCount, void(int32_t count));
  MOCK_METHOD1(
      UpdateSecondaryProperties,
      void(mojom::SecondaryPlaybackPropertiesPtr secondary_properties));
  MOCK_METHOD1(SetAutoplayInitiated, void(bool));
  MOCK_METHOD1(OnDurationChanged, void(base::TimeDelta));
};

class WatchTimeComponentTest : public testing::Test {
 public:
  WatchTimeComponentTest() = default;
  ~WatchTimeComponentTest() override = default;

 protected:
  template <typename T>
  std::unique_ptr<WatchTimeComponent<T>> CreateComponent(
      T initial_value,
      std::vector<WatchTimeKey> keys_to_finalize,
      typename WatchTimeComponent<T>::ValueToKeyCB value_to_key_cb) {
    return std::make_unique<WatchTimeComponent<T>>(
        initial_value, std::move(keys_to_finalize), std::move(value_to_key_cb),
        base::BindRepeating(&WatchTimeComponentTest::GetMediaTime,
                            base::Unretained(this)),
        &recorder_);
  }

  MOCK_METHOD0(GetMediaTime, base::TimeDelta(void));

  // Usage of StrictMock is intentional here. This ensures all mock method calls
  // are accounted for in tests.
  testing::StrictMock<WatchTimeInterceptor> recorder_;

 private:
  DISALLOW_COPY_AND_ASSIGN(WatchTimeComponentTest);
};

// Components should be key agnostic so just choose an arbitrary key for running
// most of the tests.
constexpr WatchTimeKey kTestKey = WatchTimeKey::kAudioAll;

// This is a test of the standard flow for most components. Most components will
// be created, be enabled, start reporting, record watch time, be disabled,
// report a finalize, and then record watch time again.
TEST_F(WatchTimeComponentTest, BasicFlow) {
  auto test_component = CreateComponent<bool>(
      false, {kTestKey}, WatchTimeComponent<bool>::ValueToKeyCB());
  EXPECT_FALSE(test_component->current_value_for_testing());
  EXPECT_FALSE(test_component->NeedsFinalize());
  EXPECT_EQ(test_component->end_timestamp(), kNoTimestamp);

  // Simulate flag enabled after construction, but before timer is running; this
  // should set the current value immediately.
  test_component->SetCurrentValue(true);
  EXPECT_TRUE(test_component->current_value_for_testing());
  EXPECT_FALSE(test_component->NeedsFinalize());
  EXPECT_EQ(test_component->end_timestamp(), kNoTimestamp);

  // Notify the start of reporting to set the starting timestamp.
  const base::TimeDelta kStartTime = base::TimeDelta::FromSeconds(1);
  test_component->OnReportingStarted(kStartTime);
  EXPECT_TRUE(test_component->current_value_for_testing());
  EXPECT_FALSE(test_component->NeedsFinalize());
  EXPECT_EQ(test_component->end_timestamp(), kNoTimestamp);

  // Simulate a single recording tick.
  const base::TimeDelta kWatchTime = base::TimeDelta::FromSeconds(2);
  EXPECT_CALL(recorder_, RecordWatchTime(kTestKey, kWatchTime - kStartTime));
  test_component->RecordWatchTime(kWatchTime);
  EXPECT_TRUE(test_component->current_value_for_testing());
  EXPECT_FALSE(test_component->NeedsFinalize());
  EXPECT_EQ(test_component->end_timestamp(), kNoTimestamp);

  // Simulate the flag being flipped to false while the timer is running; which
  // should trigger a finalize, but not yet set the current value.
  const base::TimeDelta kFinalWatchTime = base::TimeDelta::FromSeconds(3);
  EXPECT_CALL(*this, GetMediaTime()).WillOnce(testing::Return(kFinalWatchTime));
  test_component->SetPendingValue(false);
  EXPECT_TRUE(test_component->current_value_for_testing());
  EXPECT_TRUE(test_component->NeedsFinalize());
  EXPECT_EQ(test_component->end_timestamp(), kFinalWatchTime);

  // If record is called again it should use the finalize timestamp instead of
  // whatever timestamp we provide.
  EXPECT_CALL(recorder_,
              RecordWatchTime(kTestKey, kFinalWatchTime - kStartTime));
  test_component->RecordWatchTime(base::TimeDelta::FromSeconds(1234));
  EXPECT_TRUE(test_component->current_value_for_testing());
  EXPECT_TRUE(test_component->NeedsFinalize());
  EXPECT_EQ(test_component->end_timestamp(), kFinalWatchTime);

  // Calling it twice or more should not change anything; nor even generate a
  // report since that time has already been recorded.
  for (int i = 0; i < 2; ++i) {
    test_component->RecordWatchTime(base::TimeDelta::FromSeconds(1234 + i));
    EXPECT_TRUE(test_component->current_value_for_testing());
    EXPECT_TRUE(test_component->NeedsFinalize());
    EXPECT_EQ(test_component->end_timestamp(), kFinalWatchTime);
  }

  // Trigger finalize which should transition the pending value to the current
  // value as well as clear the finalize.
  std::vector<WatchTimeKey> finalize_keys;
  test_component->Finalize(&finalize_keys);
  EXPECT_FALSE(test_component->current_value_for_testing());
  EXPECT_FALSE(test_component->NeedsFinalize());
  EXPECT_EQ(test_component->end_timestamp(), kNoTimestamp);
  ASSERT_EQ(finalize_keys.size(), 1u);
  EXPECT_EQ(finalize_keys[0], kTestKey);

  // The start timestamps should be equal to the previous end timestamp now, so
  // if we call RecordWatchTime again, the value should be relative.
  const base::TimeDelta kNewWatchTime = base::TimeDelta::FromSeconds(4);
  EXPECT_CALL(recorder_,
              RecordWatchTime(kTestKey, kNewWatchTime - kFinalWatchTime));
  test_component->RecordWatchTime(kNewWatchTime);
  EXPECT_FALSE(test_component->current_value_for_testing());
  EXPECT_FALSE(test_component->NeedsFinalize());
  EXPECT_EQ(test_component->end_timestamp(), kNoTimestamp);
}

TEST_F(WatchTimeComponentTest, SetCurrentValue) {
  auto test_component = CreateComponent<bool>(
      true, {kTestKey}, WatchTimeComponent<bool>::ValueToKeyCB());
  EXPECT_TRUE(test_component->current_value_for_testing());
  EXPECT_FALSE(test_component->NeedsFinalize());
  EXPECT_EQ(test_component->end_timestamp(), kNoTimestamp);

  // An update when the timer isn't running should take effect immediately.
  test_component->SetCurrentValue(false);
  EXPECT_FALSE(test_component->current_value_for_testing());
  EXPECT_FALSE(test_component->NeedsFinalize());
  EXPECT_EQ(test_component->end_timestamp(), kNoTimestamp);

  test_component->SetCurrentValue(true);
  EXPECT_TRUE(test_component->current_value_for_testing());
  EXPECT_FALSE(test_component->NeedsFinalize());
  EXPECT_EQ(test_component->end_timestamp(), kNoTimestamp);
}

TEST_F(WatchTimeComponentTest, RecordDuringFinalizeRespectsCurrentTime) {
  auto test_component = CreateComponent<bool>(
      true, {kTestKey}, WatchTimeComponent<bool>::ValueToKeyCB());
  EXPECT_TRUE(test_component->current_value_for_testing());
  EXPECT_FALSE(test_component->NeedsFinalize());
  EXPECT_EQ(test_component->end_timestamp(), kNoTimestamp);

  // Simulate the flag being flipped to false while the timer is running; which
  // should trigger a finalize, but not yet set the current value.
  const base::TimeDelta kWatchTime1 = base::TimeDelta::FromSeconds(3);
  EXPECT_CALL(*this, GetMediaTime()).WillOnce(testing::Return(kWatchTime1));
  test_component->SetPendingValue(false);
  EXPECT_TRUE(test_component->current_value_for_testing());
  EXPECT_TRUE(test_component->NeedsFinalize());
  EXPECT_EQ(test_component->end_timestamp(), kWatchTime1);

  // Now issue a RecordWatchTime() call with a media time before the finalize
  // time. This can happen when the TimeDelta provided to RecordWatchTime has
  // been clamped for some reason (e.g., a superseding finalize).
  const base::TimeDelta kWatchTime2 = base::TimeDelta::FromSeconds(2);
  EXPECT_CALL(recorder_, RecordWatchTime(kTestKey, kWatchTime2));
  test_component->RecordWatchTime(kWatchTime2);
}

TEST_F(WatchTimeComponentTest, SetPendingValue) {
  auto test_component = CreateComponent<bool>(
      true, {kTestKey}, WatchTimeComponent<bool>::ValueToKeyCB());
  EXPECT_TRUE(test_component->current_value_for_testing());
  EXPECT_FALSE(test_component->NeedsFinalize());
  EXPECT_EQ(test_component->end_timestamp(), kNoTimestamp);

  // A change when running should trigger a finalize.
  const base::TimeDelta kFinalWatchTime = base::TimeDelta::FromSeconds(1);
  EXPECT_CALL(*this, GetMediaTime()).WillOnce(testing::Return(kFinalWatchTime));
  test_component->SetPendingValue(false);
  EXPECT_TRUE(test_component->current_value_for_testing());
  EXPECT_TRUE(test_component->NeedsFinalize());
  EXPECT_EQ(test_component->end_timestamp(), kFinalWatchTime);

  // Issuing the same property change again should do nothing since there's a
  // pending finalize already.
  test_component->SetPendingValue(false);
  EXPECT_TRUE(test_component->current_value_for_testing());
  EXPECT_TRUE(test_component->NeedsFinalize());
  EXPECT_EQ(test_component->end_timestamp(), kFinalWatchTime);

  // Changing the value back, should cancel the finalize.
  test_component->SetPendingValue(true);
  EXPECT_TRUE(test_component->current_value_for_testing());
  EXPECT_FALSE(test_component->NeedsFinalize());
  EXPECT_EQ(test_component->end_timestamp(), kNoTimestamp);
}

// Tests RecordWatchTime() behavior when a ValueToKeyCB is provided.
TEST_F(WatchTimeComponentTest, WithValueToKeyCB) {
  using DisplayType = blink::WebMediaPlayer::DisplayType;

  const std::vector<WatchTimeKey> finalize_keys = {
      WatchTimeKey::kAudioVideoDisplayInline,
      WatchTimeKey::kAudioVideoDisplayFullscreen,
      WatchTimeKey::kAudioVideoDisplayPictureInPicture};
  auto test_component = CreateComponent<DisplayType>(
      DisplayType::kFullscreen, finalize_keys,
      base::BindRepeating([](DisplayType display_type) {
        switch (display_type) {
          case DisplayType::kInline:
            return WatchTimeKey::kAudioVideoDisplayInline;
          case DisplayType::kFullscreen:
            return WatchTimeKey::kAudioVideoDisplayFullscreen;
          case DisplayType::kPictureInPicture:
            return WatchTimeKey::kAudioVideoDisplayPictureInPicture;
        }
      }));
  EXPECT_EQ(test_component->current_value_for_testing(),
            DisplayType::kFullscreen);
  EXPECT_FALSE(test_component->NeedsFinalize());
  EXPECT_EQ(test_component->end_timestamp(), kNoTimestamp);

  // Notify the start of reporting to set the starting timestamp.
  const base::TimeDelta kStartTime = base::TimeDelta::FromSeconds(1);
  test_component->OnReportingStarted(kStartTime);
  EXPECT_EQ(test_component->current_value_for_testing(),
            DisplayType::kFullscreen);
  EXPECT_FALSE(test_component->NeedsFinalize());
  EXPECT_EQ(test_component->end_timestamp(), kNoTimestamp);

  // Record and verify the key recorded too matches the callback provided.
  const base::TimeDelta kWatchTime1 = base::TimeDelta::FromSeconds(2);
  EXPECT_CALL(recorder_,
              RecordWatchTime(WatchTimeKey::kAudioVideoDisplayFullscreen,
                              kWatchTime1 - kStartTime));
  test_component->RecordWatchTime(kWatchTime1);
  EXPECT_EQ(test_component->current_value_for_testing(),
            DisplayType::kFullscreen);
  EXPECT_FALSE(test_component->NeedsFinalize());
  EXPECT_EQ(test_component->end_timestamp(), kNoTimestamp);

  // Change property while saying the timer isn't running to avoid finalize.
  const base::TimeDelta kWatchTime2 = base::TimeDelta::FromSeconds(3);
  test_component->SetCurrentValue(DisplayType::kInline);
  EXPECT_CALL(recorder_, RecordWatchTime(WatchTimeKey::kAudioVideoDisplayInline,
                                         kWatchTime2 - kStartTime));
  test_component->RecordWatchTime(kWatchTime2);
  EXPECT_EQ(test_component->current_value_for_testing(), DisplayType::kInline);
  EXPECT_FALSE(test_component->NeedsFinalize());
  EXPECT_EQ(test_component->end_timestamp(), kNoTimestamp);

  // Cycle through all three properties...
  const base::TimeDelta kWatchTime3 = base::TimeDelta::FromSeconds(4);
  test_component->SetCurrentValue(DisplayType::kPictureInPicture);
  EXPECT_CALL(recorder_,
              RecordWatchTime(WatchTimeKey::kAudioVideoDisplayPictureInPicture,
                              kWatchTime3 - kStartTime));
  test_component->RecordWatchTime(kWatchTime3);
  EXPECT_EQ(test_component->current_value_for_testing(),
            DisplayType::kPictureInPicture);
  EXPECT_FALSE(test_component->NeedsFinalize());
  EXPECT_EQ(test_component->end_timestamp(), kNoTimestamp);

  // Verify finalize sends all three keys.
  std::vector<WatchTimeKey> actual_finalize_keys;
  const base::TimeDelta kFinalWatchTime = base::TimeDelta::FromSeconds(5);
  EXPECT_CALL(*this, GetMediaTime()).WillOnce(testing::Return(kFinalWatchTime));
  test_component->SetPendingValue(DisplayType::kFullscreen);
  test_component->Finalize(&actual_finalize_keys);
  ASSERT_EQ(actual_finalize_keys.size(), finalize_keys.size());
  for (size_t i = 0; i < finalize_keys.size(); ++i)
    EXPECT_EQ(actual_finalize_keys[i], finalize_keys[i]);
}

// Unlike WatchTimeReporter, WatchTimeComponents have no automatic finalization
// so creating and destroying one without calls, should do nothing.
TEST_F(WatchTimeComponentTest, NoAutomaticFinalize) {
  auto test_component = CreateComponent<bool>(
      false, {kTestKey}, WatchTimeComponent<bool>::ValueToKeyCB());
}

}  // namespace media
