// Copyright 2019 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/cdm/common/cdm_manifest.h"

#include <stdint.h>

#include <algorithm>
#include <memory>
#include <string>
#include <vector>

#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/json/json_file_value_serializer.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/values.h"
#include "base/version.h"
#include "content/public/common/cdm_info.h"
#include "media/base/cdm_capability.h"
#include "media/cdm/api/content_decryption_module.h"
#include "media/cdm/supported_cdm_versions.h"
#include "media/media_buildflags.h"
#include "testing/gtest/include/gtest/gtest.h"

using media::CdmCapability;

namespace {

// These names must match what is used in cdm_manifest.cc.
const char kCdmVersion[] = "version";
const char kCdmModuleVersionsName[] = "x-cdm-module-versions";
const char kCdmInterfaceVersionsName[] = "x-cdm-interface-versions";
const char kCdmHostVersionsName[] = "x-cdm-host-versions";
const char kCdmCodecsListName[] = "x-cdm-codecs";
const char kCdmPersistentLicenseSupportName[] =
    "x-cdm-persistent-license-support";
const char kCdmSupportedEncryptionSchemesName[] =
    "x-cdm-supported-encryption-schemes";

// Version checking does change over time. Deriving these values from constants
// in the code to ensure they change when the CDM interface changes.
// |kSupportedCdmInterfaceVersion| and |kSupportedCdmHostVersion| are the
// minimum versions supported. There may be versions after them that are also
// supported.
constexpr int kSupportedCdmModuleVersion = CDM_MODULE_VERSION;
constexpr int kSupportedCdmInterfaceVersion =
    media::kSupportedCdmInterfaceVersions[0].version;
static_assert(media::kSupportedCdmInterfaceVersions[0].enabled,
              "kSupportedCdmInterfaceVersion is not enabled by default.");
constexpr int kSupportedCdmHostVersion = media::kMinSupportedCdmHostVersion;
const char kVersion[] = "1.2.3.4";

// Make a string of the values from 0 up to and including |item|.
std::string MakeStringList(int item) {
  DCHECK_GT(item, 0);
  std::vector<std::string> parts;
  for (int i = 0; i <= item; ++i) {
    parts.push_back(base::NumberToString(i));
  }
  return base::JoinString(parts, ",");
}

base::ListValue MakeList(const std::string& item) {
  base::ListValue list;
  list.Append(item);
  return list;
}

base::ListValue MakeList(const std::string& item1, const std::string& item2) {
  base::ListValue list;
  list.Append(item1);
  list.Append(item2);
  return list;
}

// Create a default manifest with valid values for all entries.
base::DictValue DefaultManifest() {
  base::DictValue dict;
  dict.Set(kCdmCodecsListName, "vp8,vp09,av01");
  dict.Set(kCdmPersistentLicenseSupportName, true);
  dict.Set(kCdmSupportedEncryptionSchemesName, MakeList("cenc", "cbcs"));

  // The following are dependent on what the current code supports.
  EXPECT_TRUE(media::IsSupportedCdmModuleVersion(kSupportedCdmModuleVersion));
  EXPECT_TRUE(media::IsSupportedAndEnabledCdmInterfaceVersion(
      kSupportedCdmInterfaceVersion));
  EXPECT_TRUE(media::IsSupportedCdmHostVersion(kSupportedCdmHostVersion));
  dict.Set(kCdmModuleVersionsName,
           base::NumberToString(kSupportedCdmModuleVersion));
  dict.Set(kCdmInterfaceVersionsName,
           base::NumberToString(kSupportedCdmInterfaceVersion));
  dict.Set(kCdmHostVersionsName,
           base::NumberToString(kSupportedCdmHostVersion));
  dict.Set(kCdmVersion, kVersion);
  return dict;
}

void CheckVideoCodecs(const media::CdmCapability::VideoCodecMap& actual,
                      const std::vector<media::VideoCodec>& expected) {
  EXPECT_EQ(expected.size(), actual.size());
  for (const auto& [video_codec, video_codec_info] : actual) {
    EXPECT_TRUE(std::ranges::contains(expected, video_codec));

    // As the manifest only specifies codecs and not profiles, the list of
    // profiles should be empty to indicate that all profiles are supported.
    EXPECT_TRUE(video_codec_info.supported_profiles.empty());
  }
}

void CheckAudioCodecs(const base::flat_set<media::AudioCodec>& actual,
                      const std::vector<media::AudioCodec>& expected) {
  EXPECT_EQ(actual, expected);
}

void CheckEncryptionSchemes(
    const base::flat_set<media::EncryptionScheme>& actual,
    const std::vector<media::EncryptionScheme>& expected) {
  EXPECT_EQ(actual, expected);
}

void CheckSessionTypes(const base::flat_set<media::CdmSessionType>& actual,
                       const std::vector<media::CdmSessionType>& expected) {
  EXPECT_EQ(actual, expected);
}

void WriteManifestToFile(const base::ValueView manifest,
                         const base::FilePath& file_path) {
  EXPECT_FALSE(base::PathExists(file_path));
  JSONFileValueSerializer serializer(file_path);
  EXPECT_TRUE(serializer.Serialize(std::move(manifest)));
  EXPECT_TRUE(base::PathExists(file_path));
}

}  // namespace

TEST(CdmManifestTest, IsCompatibleWithChrome) {
  base::DictValue manifest(DefaultManifest());
  EXPECT_TRUE(IsCdmManifestCompatibleWithChrome(manifest));
}

TEST(CdmManifestTest, InCompatibleModuleVersion) {
  const int kUnsupportedModuleVersion = 0;
  EXPECT_FALSE(media::IsSupportedCdmModuleVersion(kUnsupportedModuleVersion));

  auto manifest = DefaultManifest();
  manifest.Set(kCdmModuleVersionsName,
               base::NumberToString(kUnsupportedModuleVersion));
  EXPECT_FALSE(IsCdmManifestCompatibleWithChrome(std::move(manifest)));
}

TEST(CdmManifestTest, InCompatibleInterfaceVersion) {
  const int kUnsupportedInterfaceVersion = kSupportedCdmInterfaceVersion - 1;
  EXPECT_FALSE(media::IsSupportedAndEnabledCdmInterfaceVersion(
      kUnsupportedInterfaceVersion));

  auto manifest = DefaultManifest();
  manifest.Set(kCdmInterfaceVersionsName,
               base::NumberToString(kUnsupportedInterfaceVersion));
  EXPECT_FALSE(IsCdmManifestCompatibleWithChrome(std::move(manifest)));
}

TEST(CdmManifestTest, InCompatibleHostVersion) {
  const int kUnsupportedHostVersion = kSupportedCdmHostVersion - 1;
  EXPECT_FALSE(media::IsSupportedCdmHostVersion(kUnsupportedHostVersion));

  auto manifest = DefaultManifest();
  manifest.Set(kCdmHostVersionsName,
               base::NumberToString(kUnsupportedHostVersion));
  EXPECT_FALSE(IsCdmManifestCompatibleWithChrome(std::move(manifest)));
}

TEST(CdmManifestTest, IsCompatibleWithMultipleValues) {
  auto manifest = DefaultManifest();
  manifest.Set(kCdmModuleVersionsName,
               MakeStringList(kSupportedCdmModuleVersion));
  manifest.Set(kCdmInterfaceVersionsName,
               MakeStringList(kSupportedCdmInterfaceVersion));
  manifest.Set(kCdmHostVersionsName, MakeStringList(kSupportedCdmHostVersion));
  EXPECT_TRUE(IsCdmManifestCompatibleWithChrome(std::move(manifest)));
}

TEST(CdmManifestTest, ValidManifest) {
  auto manifest = DefaultManifest();
  CdmCapability capability;
  EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
  CheckVideoCodecs(capability.video_codecs,
                   {media::VideoCodec::kVP8, media::VideoCodec::kVP9,
                    media::VideoCodec::kAV1});
  CheckAudioCodecs(capability.audio_codecs, {
                                                media::AudioCodec::kOpus,
                                                media::AudioCodec::kVorbis,
                                                media::AudioCodec::kFLAC,
#if BUILDFLAG(USE_PROPRIETARY_CODECS)
                                                media::AudioCodec::kAAC,
#if BUILDFLAG(ENABLE_PLATFORM_DTS_AUDIO)
                                                media::AudioCodec::kDTS,
                                                media::AudioCodec::kDTSE,
                                                media::AudioCodec::kDTSXP2,
#endif  // BUILDFLAG(ENABLE_PLATFORM_DTS_AUDIO)
#if BUILDFLAG(ENABLE_PLATFORM_AC3_EAC3_AUDIO)
                                                media::AudioCodec::kAC3,
                                                media::AudioCodec::kEAC3,
#endif  // BUILDFLAG(ENABLE_PLATFORM_AC3_EAC3_AUDIO)
#endif  // BUILDFLAG(USE_PROPRIETARY_CODECS)
                                            });
  CheckEncryptionSchemes(
      capability.encryption_schemes,
      {media::EncryptionScheme::kCenc, media::EncryptionScheme::kCbcs});
  CheckSessionTypes(capability.session_types,
                    {media::CdmSessionType::kTemporary,
                     media::CdmSessionType::kPersistentLicense});
}

TEST(CdmManifestTest, EmptyManifest) {
  base::DictValue manifest;
  CdmCapability capability;
  EXPECT_FALSE(ParseCdmManifest(manifest, &capability));

  // Manifests require a version.
  manifest.Set(kCdmVersion, kVersion);
  EXPECT_TRUE(ParseCdmManifest(manifest, &capability));

  CheckVideoCodecs(capability.video_codecs, {});
  CheckAudioCodecs(capability.audio_codecs, {
                                                media::AudioCodec::kOpus,
                                                media::AudioCodec::kVorbis,
                                                media::AudioCodec::kFLAC,
#if BUILDFLAG(USE_PROPRIETARY_CODECS)
                                                media::AudioCodec::kAAC,
#if BUILDFLAG(ENABLE_PLATFORM_DTS_AUDIO)
                                                media::AudioCodec::kDTS,
                                                media::AudioCodec::kDTSE,
                                                media::AudioCodec::kDTSXP2,
#endif  // BUILDFLAG(ENABLE_PLATFORM_DTS_AUDIO)
#if BUILDFLAG(ENABLE_PLATFORM_AC3_EAC3_AUDIO)
                                                media::AudioCodec::kAC3,
                                                media::AudioCodec::kEAC3,
#endif  // BUILDFLAG(ENABLE_PLATFORM_AC3_EAC3_AUDIO)
#endif  // BUILDFLAG(USE_PROPRIETARY_CODECS)
                                            });
  CheckEncryptionSchemes(capability.encryption_schemes,
                         {media::EncryptionScheme::kCenc});
  CheckSessionTypes(capability.session_types,
                    {media::CdmSessionType::kTemporary});
}

TEST(CdmManifestTest, ManifestCodecs) {
  auto manifest = DefaultManifest();

  // Try each valid value individually.
  {
    CdmCapability capability;
    manifest.Set(kCdmCodecsListName, "vp8");
    EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
    CheckVideoCodecs(capability.video_codecs, {media::VideoCodec::kVP8});
  }
  {
    CdmCapability capability;
    manifest.Set(kCdmCodecsListName, "vp09");
    EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
    CheckVideoCodecs(capability.video_codecs, {media::VideoCodec::kVP9});
  }
  {
    CdmCapability capability;
    manifest.Set(kCdmCodecsListName, "av01");
    EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
    CheckVideoCodecs(capability.video_codecs, {media::VideoCodec::kAV1});
  }
#if BUILDFLAG(USE_PROPRIETARY_CODECS)
  {
    CdmCapability capability;
    manifest.Set(kCdmCodecsListName, "avc1");
    EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
    CheckVideoCodecs(capability.video_codecs, {media::VideoCodec::kH264});
  }
#endif
  {
    // Try list of everything (except proprietary codecs).
    CdmCapability capability;
    manifest.Set(kCdmCodecsListName, "vp8,vp09,av01");
    EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
    CheckVideoCodecs(capability.video_codecs,
                     {media::VideoCodec::kVP8, media::VideoCodec::kVP9,
                      media::VideoCodec::kAV1});
  }
  {
    // Empty codecs list result in empty list.
    CdmCapability capability;
    manifest.Set(kCdmCodecsListName, "");
    EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
    CheckVideoCodecs(capability.video_codecs, {});
  }
  {
    // Note that invalid codec values are simply skipped.
    CdmCapability capability;
    manifest.Set(kCdmCodecsListName, "invalid,av01");
    EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
    CheckVideoCodecs(capability.video_codecs, {media::VideoCodec::kAV1});
  }
  {
    // Legacy: "vp9.0" was used to support VP9 profile 0 (no profile 2 support).
    // Now this has been deprecated and "vp9.0" becomes an invalid codec value.
    CdmCapability capability;
    manifest.Set(kCdmCodecsListName, "vp9.0");
    EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
    CheckVideoCodecs(capability.video_codecs, {});
  }
  {
    // Wrong types are an error.
    CdmCapability capability;
    manifest.Set(kCdmCodecsListName, true);
    EXPECT_FALSE(ParseCdmManifest(manifest, &capability));
  }
  {
    // Missing entry is OK, but list is empty.
    CdmCapability capability;
    EXPECT_TRUE(manifest.Remove(kCdmCodecsListName));
    EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
    CheckVideoCodecs(capability.video_codecs, {});
  }
}

TEST(CdmManifestTest, ManifestEncryptionSchemes) {
  auto manifest = DefaultManifest();

  // Try each valid value individually.
  {
    CdmCapability capability;
    manifest.Set(kCdmSupportedEncryptionSchemesName, MakeList("cenc"));
    EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
    CheckEncryptionSchemes(capability.encryption_schemes,
                           {media::EncryptionScheme::kCenc});
  }
  {
    CdmCapability capability;
    manifest.Set(kCdmSupportedEncryptionSchemesName, MakeList("cbcs"));
    EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
    CheckEncryptionSchemes(capability.encryption_schemes,
                           {media::EncryptionScheme::kCbcs});
  }
  {
    // Try multiple valid entries.
    CdmCapability capability;
    manifest.Set(kCdmSupportedEncryptionSchemesName, MakeList("cenc", "cbcs"));
    EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
    CheckEncryptionSchemes(
        capability.encryption_schemes,
        {media::EncryptionScheme::kCenc, media::EncryptionScheme::kCbcs});
  }
  {
    // Invalid encryption schemes are ignored. However, if value specified then
    // there must be at least 1 valid value.
    CdmCapability capability;
    manifest.Set(kCdmSupportedEncryptionSchemesName, MakeList("invalid"));
    EXPECT_FALSE(ParseCdmManifest(manifest, &capability));
  }
  {
    CdmCapability capability;
    manifest.Set(kCdmSupportedEncryptionSchemesName,
                 MakeList("invalid", "cenc"));
    EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
    CheckEncryptionSchemes(capability.encryption_schemes,
                           {media::EncryptionScheme::kCenc});
  }
  {
    // Wrong types are an error.
    CdmCapability capability;
    manifest.Set(kCdmSupportedEncryptionSchemesName, true);
    EXPECT_FALSE(ParseCdmManifest(manifest, &capability));
  }
  {
    // Missing values default to "cenc".
    CdmCapability capability;
    EXPECT_TRUE(manifest.Remove(kCdmSupportedEncryptionSchemesName));
    EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
    CheckEncryptionSchemes(capability.encryption_schemes,
                           {media::EncryptionScheme::kCenc});
  }
}

TEST(CdmManifestTest, ManifestSessionTypes) {
  auto manifest = DefaultManifest();

  {
    // Try false (persistent license not supported).
    CdmCapability capability;
    manifest.Set(kCdmPersistentLicenseSupportName, false);
    EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
    CheckSessionTypes(capability.session_types,
                      {media::CdmSessionType::kTemporary});
  }
  {
    // Try true (persistent license is supported).
    CdmCapability capability;
    manifest.Set(kCdmPersistentLicenseSupportName, true);
    EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
    CheckSessionTypes(capability.session_types,
                      {media::CdmSessionType::kTemporary,
                       media::CdmSessionType::kPersistentLicense});
  }
  {
    // Wrong types are an error.
    CdmCapability capability;
    manifest.Set(kCdmPersistentLicenseSupportName, "true");
    EXPECT_FALSE(ParseCdmManifest(manifest, &capability));
  }
  {
    // Missing values default to "temporary".
    CdmCapability capability;
    EXPECT_TRUE(manifest.Remove(kCdmPersistentLicenseSupportName));
    EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
    CheckSessionTypes(capability.session_types,
                      {media::CdmSessionType::kTemporary});
  }
}

TEST(CdmManifestTest, FileManifest) {
  base::ScopedTempDir temp_dir;
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
  auto manifest_path = temp_dir.GetPath().AppendASCII("manifest.json");

  // Manifests read from a file also need a version.
  auto manifest = DefaultManifest();
  WriteManifestToFile(manifest, manifest_path);

  CdmCapability capability;
  EXPECT_TRUE(ParseCdmManifestFromPath(manifest_path, &capability));
  EXPECT_TRUE(capability.version.IsValid());
  EXPECT_EQ(capability.version.GetString(), kVersion);
  CheckVideoCodecs(capability.video_codecs,
                   {media::VideoCodec::kVP8, media::VideoCodec::kVP9,
                    media::VideoCodec::kAV1});
  CheckEncryptionSchemes(
      capability.encryption_schemes,
      {media::EncryptionScheme::kCenc, media::EncryptionScheme::kCbcs});
  CheckSessionTypes(capability.session_types,
                    {media::CdmSessionType::kTemporary,
                     media::CdmSessionType::kPersistentLicense});
}

TEST(CdmManifestTest, FileManifestNoVersion) {
  base::ScopedTempDir temp_dir;
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
  auto manifest_path = temp_dir.GetPath().AppendASCII("manifest.json");

  auto manifest = DefaultManifest();
  manifest.Remove(kCdmVersion);
  WriteManifestToFile(manifest, manifest_path);

  CdmCapability capability;
  EXPECT_FALSE(ParseCdmManifestFromPath(manifest_path, &capability));
}

TEST(CdmManifestTest, FileManifestBadVersion) {
  base::ScopedTempDir temp_dir;
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
  auto manifest_path = temp_dir.GetPath().AppendASCII("manifest.json");

  auto manifest = DefaultManifest();
  manifest.Set(kCdmVersion, "bad version");
  WriteManifestToFile(manifest, manifest_path);

  CdmCapability capability;
  EXPECT_FALSE(ParseCdmManifestFromPath(manifest_path, &capability));
}

TEST(CdmManifestTest, FileManifestDoesNotExist) {
  base::ScopedTempDir temp_dir;
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
  auto manifest_path = temp_dir.GetPath().AppendASCII("manifest.json");

  CdmCapability capability;
  EXPECT_FALSE(ParseCdmManifestFromPath(manifest_path, &capability));
}

TEST(CdmManifestTest, FileManifestEmpty) {
  base::ScopedTempDir temp_dir;
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
  auto manifest_path = temp_dir.GetPath().AppendASCII("manifest.json");

  base::DictValue manifest;
  WriteManifestToFile(manifest, manifest_path);

  CdmCapability capability;
  EXPECT_FALSE(ParseCdmManifestFromPath(manifest_path, &capability));
}

TEST(CdmManifestTest, FileManifestLite) {
  base::ScopedTempDir temp_dir;
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
  auto manifest_path = temp_dir.GetPath().AppendASCII("manifest.json");

  // Only a version plus fields to satisfy compatibility are required in the
  // manifest to parse correctly.
  base::DictValue manifest;
  manifest.Set(kCdmVersion, "1.2.3.4");
  manifest.Set(kCdmModuleVersionsName,
               base::NumberToString(kSupportedCdmModuleVersion));
  manifest.Set(kCdmInterfaceVersionsName,
               base::NumberToString(kSupportedCdmInterfaceVersion));
  manifest.Set(kCdmHostVersionsName,
               base::NumberToString(kSupportedCdmHostVersion));
  WriteManifestToFile(manifest, manifest_path);

  CdmCapability capability;
  EXPECT_TRUE(ParseCdmManifestFromPath(manifest_path, &capability));
  CheckVideoCodecs(capability.video_codecs, {});
  CheckEncryptionSchemes(capability.encryption_schemes,
                         {media::EncryptionScheme::kCenc});
  CheckSessionTypes(capability.session_types,
                    {media::CdmSessionType::kTemporary});
}

TEST(CdmManifestTest, FileManifestNotDictionary) {
  base::ScopedTempDir temp_dir;
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
  auto manifest_path = temp_dir.GetPath().AppendASCII("manifest.json");

  base::Value manifest("not a dictionary");
  WriteManifestToFile(manifest, manifest_path);

  CdmCapability capability;
  EXPECT_FALSE(ParseCdmManifestFromPath(manifest_path, &capability));
}
