// Copyright 2022 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/media/media_foundation_service_monitor.h"

#include "base/check.h"
#include "base/files/file_path.h"
#include "base/json/values_util.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_file_util.h"
#include "base/time/time.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/browser/service_process_info.h"
#include "media/base/media_switches.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace {
inline constexpr char kSite1[] = "https://site1.com";
inline constexpr char kSite2[] = "https://site2.com";
inline constexpr char kSite3[] = "https://subdomain1.site1.com";
inline constexpr char kMediaFoundationServiceProcessName[] =
    "media.mojom.MediaFoundationServiceBroker";
}  // namespace

class MediaFoundationServiceMonitorTest
    : public ChromeRenderViewHostTestHarness {
 public:
  void SetUp() override {
    // Set up a testing profile manager.
    ChromeRenderViewHostTestHarness::SetUp();
    profile_manager_ = std::make_unique<TestingProfileManager>(
        TestingBrowserProcess::GetGlobal());

    // Create a new temporary directory, and store the path
    const base::FilePath temp_user_data_dir =
        base::CreateUniqueTempDirectoryScopedToTest();
    ASSERT_TRUE(profile_manager_->SetUp(temp_user_data_dir));

    monitor_ = MediaFoundationServiceMonitor::GetInstance();
    CHECK(monitor_);

    // Tests could write to local state and profile. Must clear them.
    ClearLocalState();
    ClearProfile();
    // MediaFoundationServiceMonitor is a singleton and holds state between
    // test. Must clear it.
    monitor_->ResetForTesting();
  }

  void TearDown() override {
    TestingBrowserProcess::GetGlobal()->SetProfileManager(nullptr);
    ChromeRenderViewHostTestHarness::TearDown();
  }

 protected:
  // Test the `days_since_last_disabling_date` against `disabled_dates`, both
  // of which are integer(s) number of days since an arbitrary base time.
  void TestEarliestEnableDate(std::vector<int> disabled_dates,
                              int days_since_last_disabling_date) {
    // An arbitrary base time for the tests.
    base::Time base_time;
    EXPECT_TRUE(base::Time::FromString("22 Sep 2022 12:23 GMT", &base_time));

    std::vector<base::Time> disabled_times;
    for (const auto& days : disabled_dates) {
      disabled_times.push_back(base_time + base::Days(days));
    }
    auto enable_time =
        MediaFoundationServiceMonitor::GetEarliestEnableTime(disabled_times);
    auto expected_time =
        disabled_times.back() + base::Days(days_since_last_disabling_date);

    // Expect the `enable_time` to be in a range to avoid testing rounding
    // logic.
    EXPECT_LE(enable_time, expected_time + base::Days(1));
    EXPECT_GE(enable_time, expected_time - base::Days(1));
  }

  // Creates an entry for `site` in profile.
  void InitProfileForSite(const GURL& site) {
    auto* user_prefs =
        profile_manager_->profile_manager()->GetLastUsedProfile()->GetPrefs();
    ScopedDictPrefUpdate update(user_prefs, prefs::kMediaCdmOriginData);

    update.Get()
        .EnsureDict(url::Origin::Create(site).Serialize())
        ->EnsureList(prefs::kHardwareSecureDecryptionDisabledTimes);
  }

  // Clears user profile.
  void ClearProfile() {
    auto* user_prefs =
        profile_manager_->profile_manager()->GetLastUsedProfile()->GetPrefs();
    ScopedDictPrefUpdate update(user_prefs, prefs::kMediaCdmOriginData);
    for (auto [key, value] : update.Get()) {
      ASSERT_TRUE(update.Get().Remove(key));
    }
  }

  // Clears local state.
  void ClearLocalState() {
    auto* user_prefs =
        profile_manager_->profile_manager()->GetLastUsedProfile()->GetPrefs();
    ScopedDictPrefUpdate update(user_prefs, prefs::kMediaCdmOriginData);
    TestingBrowserProcess::GetGlobal()->local_state()->ClearPref(
        prefs::kGlobalHardwareSecureDecryptionDisabledTimes);
  }

  bool IsAllowedForSite(GURL site) {
    return MediaFoundationServiceMonitor::
        IsHardwareSecureDecryptionAllowedForSite(site);
  }

  std::unique_ptr<TestingProfileManager> profile_manager_;
  raw_ptr<MediaFoundationServiceMonitor> monitor_;
};

TEST_F(MediaFoundationServiceMonitorTest, GetEarliestEnableTime_Default) {
  // One disabling event will cause the feature to be disabled for 30 days,
  // which is the minimum disabling days.
  TestEarliestEnableDate({0}, 30);
  TestEarliestEnableDate({1}, 30);
  TestEarliestEnableDate({10}, 30);

  // Two close disabling events will cause the feature to be disabled for 180
  // days, which is the maximum disabling days.
  TestEarliestEnableDate({10, 10}, 180);
  TestEarliestEnableDate({10, 20}, 180);
  TestEarliestEnableDate({10, 40}, 180);

  // The closer the two disabling events are, the longer the feature will be
  // disabled.
  TestEarliestEnableDate({10, 50}, 142);
  TestEarliestEnableDate({10, 100}, 80);
  TestEarliestEnableDate({10, 1000}, 34);

  // Two far apart disabling events will cause the feature to be disabled for 30
  // days, which is the minimum disabling days.
  TestEarliestEnableDate({10, 10000}, 30);

  // The third disabling event time doesn't matter.
  TestEarliestEnableDate({10, 50}, 142);
  TestEarliestEnableDate({1, 10, 50}, 142);
}

TEST_F(MediaFoundationServiceMonitorTest, GetEarliestEnableTime_Overridden) {
  // Ensure we take any base::Feature overrides into account.
  base::test::ScopedFeatureList features;
  features.InitAndEnableFeatureWithParameters(
      media::kHardwareSecureDecryptionFallback,
      {{"min_disabling_days", "10"}, {"max_disabling_days", "60"}});

  // One disabling event will cause the feature to be disabled for 30 days,
  // which is the minimum disabling days.
  TestEarliestEnableDate({0}, 10);
  TestEarliestEnableDate({1}, 10);
  TestEarliestEnableDate({10}, 10);

  // Two close disabling events will cause the feature to be disabled for 60
  // days, which is the maximum disabling days.
  TestEarliestEnableDate({10, 10}, 60);
  TestEarliestEnableDate({10, 20}, 60);

  // The closer the two disabling events are, the longer the feature will be
  // disabled.
  TestEarliestEnableDate({10, 40}, 26);
  TestEarliestEnableDate({10, 50}, 22);
  TestEarliestEnableDate({10, 100}, 15);

  // Two far apart disabling events will cause the feature to be disabled for 10
  // days, which is the minimum disabling days.
  TestEarliestEnableDate({10, 1000}, 10);
  TestEarliestEnableDate({10, 10000}, 10);

  // The third disabling event time doesn't matter.
  TestEarliestEnableDate({10, 50}, 22);
  TestEarliestEnableDate({1, 10, 50}, 22);
}

TEST_F(MediaFoundationServiceMonitorTest, CrashInSiteShouldDisable) {
  content::ServiceProcessInfo crashed_info(
      kMediaFoundationServiceProcessName, GURL(kSite1),
      content::ServiceProcessId(), base::Process::Current());
  // Creation of user profile happens after media foundation cdm is created. We
  // need to force creating an entry to user profile first for testing.
  InitProfileForSite(crashed_info.site().value());
  EXPECT_TRUE(IsAllowedForSite(crashed_info.site().value()));

  // First crash still allows the site to play as first crash could be
  // transient.
  monitor_->OnServiceProcessCrashed(crashed_info);
  EXPECT_TRUE(IsAllowedForSite(crashed_info.site().value()));

  // Second crash should disallow hardware secure decryption.
  monitor_->OnServiceProcessCrashed(crashed_info);
  EXPECT_FALSE(IsAllowedForSite(crashed_info.site().value()));
}

TEST_F(MediaFoundationServiceMonitorTest, CrashInSiteShouldNotAffectOtherSite) {
  content::ServiceProcessInfo crashed_info(
      kMediaFoundationServiceProcessName, GURL(kSite1),
      content::ServiceProcessId(), base::Process::Current());
  content::ServiceProcessInfo ok_info(kMediaFoundationServiceProcessName,
                                      GURL(kSite2), content::ServiceProcessId(),
                                      base::Process::Current());
  // Creation of user profile happens after media foundation cdm is created. We
  // need to force creating an entry to user profile first for testing.
  InitProfileForSite(crashed_info.site().value());
  InitProfileForSite(ok_info.site().value());
  EXPECT_TRUE(IsAllowedForSite(crashed_info.site().value()));
  EXPECT_TRUE(IsAllowedForSite(ok_info.site().value()));

  // First crash still allows the site to play as first crash could be
  // transient.
  monitor_->OnServiceProcessCrashed(crashed_info);
  EXPECT_TRUE(IsAllowedForSite(crashed_info.site().value()));
  EXPECT_TRUE(IsAllowedForSite(ok_info.site().value()));

  // Second crash should disallow hardware secure decryption.
  monitor_->OnServiceProcessCrashed(crashed_info);
  EXPECT_FALSE(IsAllowedForSite(crashed_info.site().value()));
  EXPECT_TRUE(IsAllowedForSite(ok_info.site().value()));
}

TEST_F(MediaFoundationServiceMonitorTest, NewSiteShouldUseGlobalDisable) {
  content::ServiceProcessInfo crashed_info(
      kMediaFoundationServiceProcessName, GURL(kSite1),
      content::ServiceProcessId(), base::Process::Current());
  // Creation of user profile happens after media foundation cdm is created. We
  // need to force creating an entry to user profile first for testing.
  InitProfileForSite(crashed_info.site().value());
  EXPECT_TRUE(IsAllowedForSite(crashed_info.site().value()));

  // First crash still allows the site to play as first crash could be
  // transient.
  monitor_->OnServiceProcessCrashed(crashed_info);
  EXPECT_TRUE(IsAllowedForSite(crashed_info.site().value()));

  monitor_->OnServiceProcessCrashed(crashed_info);
  // Second crash should disallow hardware secure decryption.
  monitor_->OnServiceProcessCrashed(crashed_info);
  EXPECT_FALSE(IsAllowedForSite(crashed_info.site().value()));

  // New site should not be allowed to use hardware secure decryption.
  content::ServiceProcessInfo new_info(
      kMediaFoundationServiceProcessName, GURL(kSite2),
      content::ServiceProcessId(), base::Process::Current());
  EXPECT_FALSE(IsAllowedForSite(new_info.site().value()));
  EXPECT_TRUE(MediaFoundationServiceMonitor::
                  IsHardwareSecureDecryptionDisabledByPref());
}

TEST_F(MediaFoundationServiceMonitorTest, CrashInSiteCausesGlobalDisable) {
  content::ServiceProcessInfo crashed_info(
      kMediaFoundationServiceProcessName, GURL(kSite1),
      content::ServiceProcessId(), base::Process::Current());
  // Creation of user profile happens after media foundation cdm is created. We
  // need to force creating an entry to user profile first for testing.
  InitProfileForSite(crashed_info.site().value());
  EXPECT_TRUE(IsAllowedForSite(crashed_info.site().value()));

  // First crash still allows the site to play as first crash could be
  // transient.
  monitor_->OnServiceProcessCrashed(crashed_info);
  EXPECT_TRUE(IsAllowedForSite(crashed_info.site().value()));

  // Second crash should disallow hardware secure decryption.
  monitor_->OnServiceProcessCrashed(crashed_info);
  EXPECT_FALSE(IsAllowedForSite(crashed_info.site().value()));

  // Crash should disallow hardware secure decryption globally.
  EXPECT_TRUE(MediaFoundationServiceMonitor::
                  IsHardwareSecureDecryptionDisabledByPref());
}

TEST_F(MediaFoundationServiceMonitorTest, CrashedSiteAffectAllSameOrigins) {
  content::ServiceProcessInfo crashed_info(
      kMediaFoundationServiceProcessName, GURL(kSite1),
      content::ServiceProcessId(), base::Process::Current());
  content::ServiceProcessInfo ok_info(kMediaFoundationServiceProcessName,
                                      GURL(kSite2), content::ServiceProcessId(),
                                      base::Process::Current());
  content::ServiceProcessInfo same_origin_info(
      kMediaFoundationServiceProcessName, GURL(kSite3),
      content::ServiceProcessId(), base::Process::Current());
  // Creation of user profile happens after media foundation cdm is created. We
  // need to force creating an entry to user profile first for testing.
  InitProfileForSite(crashed_info.site().value());
  InitProfileForSite(ok_info.site().value());
  InitProfileForSite(same_origin_info.site().value());
  EXPECT_TRUE(IsAllowedForSite(crashed_info.site().value()));
  EXPECT_TRUE(IsAllowedForSite(ok_info.site().value()));
  EXPECT_TRUE(IsAllowedForSite(same_origin_info.site().value()));

  // First crash still allows the site to play as first crash could be
  // transient.
  monitor_->OnServiceProcessCrashed(crashed_info);
  EXPECT_TRUE(IsAllowedForSite(crashed_info.site().value()));

  // Second crash should disallow hardware secure decryption.
  monitor_->OnServiceProcessCrashed(crashed_info);
  EXPECT_FALSE(IsAllowedForSite(crashed_info.site().value()));

  // Site with same origin should also be disabled.
  EXPECT_FALSE(IsAllowedForSite(same_origin_info.site().value()));

  // Site with different origin should be allowed.
  EXPECT_TRUE(IsAllowedForSite(ok_info.site().value()));
}
