// Copyright 2024 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/ui/ash/focus_mode/certificate_manager.h"

#include <memory>

#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "chrome/browser/ash/settings/scoped_testing_cros_settings.h"
#include "chrome/browser/ash/settings/stub_cros_settings_provider.h"
#include "chrome/browser/ui/ash/focus_mode/test/test_certificate.h"
#include "chromeos/ash/components/attestation/attestation_flow.h"
#include "chromeos/ash/components/attestation/fake_certificate.h"
#include "chromeos/ash/components/attestation/mock_attestation_flow.h"
#include "chromeos/ash/components/dbus/attestation/fake_attestation_client.h"
#include "chromeos/ash/components/dbus/constants/attestation_constants.h"
#include "chromeos/ash/components/settings/cros_settings_names.h"
#include "components/account_id/account_id.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

using testing::_;
using testing::Eq;
using testing::Ne;

const AccountId kTestAccount = AccountId::FromUserEmail("user@example.com");
const base::TimeDelta kTestBuffer = base::Hours(1);

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

  void SetUp() override {
    auto mock_attestation =
        std::make_unique<ash::attestation::MockAttestationFlow>();
    mock_attestation_flow_ = mock_attestation.get();
    certificate_manager_ = CertificateManager::CreateForTesting(
        kTestAccount, kTestBuffer, std::move(mock_attestation),
        &fake_attestation_client_);
  }

  void TearDown() override {
    mock_attestation_flow_ = nullptr;
    certificate_manager_.reset();
  }

  ash::StubCrosSettingsProvider* cros_settings() {
    return test_cros_settings_.device_settings();
  }

  CertificateManager* certificate_manager() {
    return certificate_manager_.get();
  }

  ash::attestation::MockAttestationFlow* mock_attestation_flow() {
    return mock_attestation_flow_;
  }

 private:
  base::test::SingleThreadTaskEnvironment task_environment_;

  ash::ScopedTestingCrosSettings test_cros_settings_;
  raw_ptr<ash::attestation::MockAttestationFlow> mock_attestation_flow_;
  ash::FakeAttestationClient fake_attestation_client_;
  std::unique_ptr<CertificateManager> certificate_manager_;
};

// Verifies that if content protection is disabled, no requests are made.
TEST_F(CertificateManagerTest, GetCertificate_PolicyDenied) {
  cros_settings()->SetBoolean(ash::kAttestationForContentProtectionEnabled,
                              false);

  // If policy is not allowed, `GetCertificate()` returns immediately and does
  // not invoke the attestation flow.
  EXPECT_CALL(*mock_attestation_flow(),
              GetCertificate(/*profile=*/_,
                             /*account_id=*/_,
                             /*request_origin=*/_,
                             /*force_new_key=*/_,
                             /*key_crypto_type=*/_,
                             /*key_name=*/_,
                             /*profile_specific_data=*/_,
                             /*callback=*/_))
      .Times(0);

  // `GetCertificate()` does nothing and returns false if the policy is
  // disabled.
  EXPECT_FALSE(certificate_manager()->GetCertificate(false, base::DoNothing()));
}

// Verifies that a certificate is requested via the attestation flow.
TEST_F(CertificateManagerTest, GetCertificate) {
  cros_settings()->SetBoolean(ash::kAttestationForContentProtectionEnabled,
                              true);

  EXPECT_CALL(
      *mock_attestation_flow(),
      GetCertificate(ash::attestation::PROFILE_CONTENT_PROTECTION_CERTIFICATE,
                     /*account_id=*/_,
                     /*request_origin=*/"youtubemediaconnect.googleapis.com",
                     /*force_new_key=*/false,
                     /*key_crypto_type=*/::attestation::KEY_TYPE_ECC,
                     /*key_name=*/_,
                     /*profile_specific_data=*/_,
                     /*callback=*/_));

  ASSERT_TRUE(certificate_manager()->GetCertificate(false, base::DoNothing()));
}

// Request signing but denied by policy.
TEST_F(CertificateManagerTest, Sign_Denied) {
  cros_settings()->SetBoolean(ash::kAttestationForContentProtectionEnabled,
                              false);

  // Pick an expiration arbitrarily far in the future.
  base::Time expiration = base::Time::Now() + base::Days(14);
  CertificateManager::Key key("CrOSFocusMode", expiration);

  auto status =
      certificate_manager()->Sign(key, "TEST_PAYLOAD", base::DoNothing());
  EXPECT_THAT(status,
              Eq(CertificateManager::CertificateResult::kDisallowedByPolicy));
}

// Request signing with an expired certificate.
TEST_F(CertificateManagerTest, Sign_Expired) {
  cros_settings()->SetBoolean(ash::kAttestationForContentProtectionEnabled,
                              true);

  // Pick an expiration in the past.
  base::Time expiration = base::Time::Now() - base::Days(14);
  CertificateManager::Key key("CrOSFocusMode", expiration);

  auto status =
      certificate_manager()->Sign(key, "TEST_PAYLOAD", base::DoNothing());
  EXPECT_THAT(status,
              Eq(CertificateManager::CertificateResult::kCertificateExpired));
}

// Request signing with a key that is not from `GetCertificate()`.
TEST_F(CertificateManagerTest, Sign_InvalidKey) {
  cros_settings()->SetBoolean(ash::kAttestationForContentProtectionEnabled,
                              true);

  // Pick an arbitrary date in the future that does not match the cached
  // certificate.
  base::Time expiration = base::Time::Now() + base::Days(14);
  CertificateManager::Key key("CrOSFocusMode", expiration);

  auto status =
      certificate_manager()->Sign(key, "TEST_PAYLOAD", base::DoNothing());
  EXPECT_THAT(status, Eq(CertificateManager::CertificateResult::kInvalidKey));
}

// Request for signing is fulfilled.
TEST_F(CertificateManagerTest, Sign) {
  ash::attestation::AttestationFlow::CertificateCallback certificate_callback;
  EXPECT_CALL(*mock_attestation_flow(), GetCertificate)
      .WillOnce(
          [&](ash::attestation::AttestationCertificateProfile, const AccountId&,
              const std::string&, bool, ::attestation::KeyType,
              const std::string&,
              const std::optional<
                  ash::attestation::AttestationFlow::CertProfileSpecificData>&,
              ash::attestation::AttestationFlow::CertificateCallback callback) {
            certificate_callback = std::move(callback);
          });

  std::optional<CertificateManager::Key> certificate_key;
  bool cert_status = certificate_manager()->GetCertificate(
      false, base::BindLambdaForTesting(
                 [&](const std::optional<CertificateManager::Key>& key) {
                   // Check that the key is not null since retrieval should be
                   // successful.
                   ASSERT_THAT(key, Ne(std::nullopt));
                   certificate_key.emplace(*key);
                 }));
  ASSERT_TRUE(cert_status);

  std::string certificate;
  ASSERT_TRUE(
      ash::attestation::GetFakeCertificatePEM(base::Days(30), &certificate));

  // Fulfill the request for a certificate.
  std::move(certificate_callback)
      .Run(ash::attestation::AttestationStatus::ATTESTATION_SUCCESS,
           certificate);

  // Verify that we received a key.
  ASSERT_THAT(certificate_key, Ne(std::nullopt));

  base::RunLoop run_loop;
  auto status = certificate_manager()->Sign(
      *certificate_key, "TEST_PAYLOAD",
      base::IgnoreArgs<bool, const std::string&, const std::string&,
                       const std::vector<std::string>&>(
          run_loop.QuitClosure()));
  EXPECT_THAT(status, Eq(CertificateManager::CertificateResult::kSuccess));

  // Wait for the `FakeAttestationClient` to finish.
  run_loop.Run();
}

// Verifies that if the `CertificateManager` finds that the currently cached
// certificate requires upgrade, we attempt upgrade one time (and only once to
// prevent overwhelming the server).
TEST_F(CertificateManagerTest, CertificateUpgradeRequired_Failed) {
  // A certificate signed with SHA-1 that's expected to be rejected.
  std::string test_certificate = ash::ReadSha1TestCertificate();

  testing::InSequence sequence;
  EXPECT_CALL(*mock_attestation_flow(),
              GetCertificate(_, _, _, /*force_refresh=*/false, _, _, _, _))
      .WillOnce(
          [&](ash::attestation::AttestationCertificateProfile, const AccountId&,
              const std::string&, bool, ::attestation::KeyType,
              const std::string&,
              const std::optional<
                  ash::attestation::AttestationFlow::CertProfileSpecificData>&,
              ash::attestation::AttestationFlow::CertificateCallback callback) {
            std::move(callback).Run(
                ash::attestation::AttestationStatus::ATTESTATION_SUCCESS,
                test_certificate);
          });

  EXPECT_CALL(*mock_attestation_flow(),
              GetCertificate(_, _, _, /*force_refresh=*/true, _, _, _, _))
      .WillOnce(
          [&](ash::attestation::AttestationCertificateProfile, const AccountId&,
              const std::string&, bool, ::attestation::KeyType,
              const std::string&,
              const std::optional<
                  ash::attestation::AttestationFlow::CertProfileSpecificData>&,
              ash::attestation::AttestationFlow::CertificateCallback callback) {
            std::move(callback).Run(
                ash::attestation::AttestationStatus::ATTESTATION_SUCCESS,
                test_certificate);
          });

  base::RunLoop run_loop;
  bool cert_status = certificate_manager()->GetCertificate(
      false, base::BindLambdaForTesting(
                 [&](const std::optional<CertificateManager::Key>& key) {
                   // Since an appropriate certificate is not available, this
                   // fails.
                   EXPECT_THAT(key, Eq(std::nullopt));
                   run_loop.Quit();
                 }));
  ASSERT_TRUE(cert_status);

  run_loop.Run();

  // Get Certificate should have been called exactly twice. New requests for
  // certificates should fail.
  cert_status = certificate_manager()->GetCertificate(false, base::DoNothing());
  EXPECT_THAT(cert_status, Eq(false));
}

TEST_F(CertificateManagerTest, CertificateUpgraded) {
  testing::InSequence sequence;
  EXPECT_CALL(*mock_attestation_flow(),
              GetCertificate(_, _, _, /*force_refresh=*/false, _, _, _, _))
      .WillOnce(
          [&](ash::attestation::AttestationCertificateProfile, const AccountId&,
              const std::string&, bool, ::attestation::KeyType,
              const std::string&,
              const std::optional<
                  ash::attestation::AttestationFlow::CertProfileSpecificData>&,
              ash::attestation::AttestationFlow::CertificateCallback callback) {
            // A certificate signed with SHA-1 that's expected to be rejected.
            std::string test_certificate = ash::ReadSha1TestCertificate();
            std::move(callback).Run(
                ash::attestation::AttestationStatus::ATTESTATION_SUCCESS,
                test_certificate);
          });

  EXPECT_CALL(*mock_attestation_flow(),
              GetCertificate(_, _, _, /*force_refresh=*/true, _, _, _, _))
      .WillOnce(
          [&](ash::attestation::AttestationCertificateProfile, const AccountId&,
              const std::string&, bool, ::attestation::KeyType,
              const std::string&,
              const std::optional<
                  ash::attestation::AttestationFlow::CertProfileSpecificData>&,
              ash::attestation::AttestationFlow::CertificateCallback callback) {
            // On refresh, provide a SHA-256 signed certificate that is
            // accepted.
            std::string certificate;
            ASSERT_TRUE(ash::attestation::GetFakeCertificatePEM(base::Days(30),
                                                                &certificate));
            std::move(callback).Run(
                ash::attestation::AttestationStatus::ATTESTATION_SUCCESS,
                certificate);
          });

  base::RunLoop run_loop;
  bool cert_status = certificate_manager()->GetCertificate(
      false, base::BindLambdaForTesting(
                 [&](const std::optional<CertificateManager::Key>& key) {
                   // Since a valid certificate was provided in the second
                   // request, we get the key for that certificate.
                   EXPECT_THAT(key, Ne(std::nullopt));
                   run_loop.Quit();
                 }));
  ASSERT_TRUE(cert_status);

  run_loop.Run();
}

}  // namespace
