// 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 "components/trusted_vault/recovery_key_store_connection.h"

#include <memory>
#include <string>
#include <utility>

#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram.h"
#include "base/run_loop.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/test/protobuf_matchers.h"
#include "base/test/simple_test_clock.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/types/expected.h"
#include "components/signin/public/identity_manager/access_token_info.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/trusted_vault/proto/recovery_key_store.pb.h"
#include "components/trusted_vault/proto_string_bytes_conversion.h"
#include "components/trusted_vault/recovery_key_store_connection_impl.h"
#include "components/trusted_vault/securebox.h"
#include "components/trusted_vault/test/fake_trusted_vault_access_token_fetcher.h"
#include "components/trusted_vault/test/recovery_key_store_certificate_test_util.h"
#include "components/trusted_vault/trusted_vault_histograms.h"
#include "net/base/net_errors.h"
#include "net/http/http_status_code.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "services/network/test/test_url_loader_factory.h"
#include "services/network/test/test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace trusted_vault {

namespace {

using base::test::EqualsProto;
using testing::Eq;
using testing::HasSubstr;
using testing::IsNull;
using testing::Not;
using testing::NotNull;
using testing::UnorderedElementsAre;
using ResponseMatchFlags = network::TestURLLoaderFactory::ResponseMatchFlags;

using UpdateRecoveryKeyStoreFuture =
    base::test::TestFuture<RecoveryKeyStoreStatus>;
using ListVaultsFuture = base::test::TestFuture<
    base::expected<std::vector<RecoveryKeyStoreEntry>, RecoveryKeyStoreStatus>>;
using FetchCertCallback = base::MockCallback<
    RecoveryKeyStoreConnection::FetchRecoveryKeyStoreCertificatesCallback>;
using FetchCertFuture = base::test::TestFuture<
    base::expected<RecoveryKeyStoreCertificate,
                   RecoveryKeyStoreCertificateFetchStatus>>;

constexpr char kPasskeysApplicationKeyName[] =
    "security_domain_member_key_encrypted_locally";

constexpr std::string_view kBackendPublicKey1 = "backend-public-key-1";
constexpr std::string_view kVaultHandle1 = "vault-handle-1";
constexpr std::string_view kBackendPublicKey2 = "backend-public-key-2";
constexpr std::string_view kVaultHandle2 = "vault-handle-2";

class RecoveryKeyStoreConnectionImplTest : public testing::Test {
 protected:
  static constexpr char kUpdateVaultUrl[] =
      "https://cryptauthvault.googleapis.com/v1/vaults/0?alt=proto";
  static constexpr char kListVaultsUrl[] =
      "https://cryptauthvault.googleapis.com/v1/vaults";
  static constexpr char kRecoveryKeyStoreCertFileUrl[] =
      "https://www.gstatic.com/cryptauthvault/v0/cert.xml";
  static constexpr char kRecoveryKeyStoreSigFileUrl[] =
      "https://www.gstatic.com/cryptauthvault/v0/cert.sig.xml";

  RecoveryKeyStoreConnectionImplTest() {
    clock_.SetNow(test_certs::kValidCertificateDate);
  }
  ~RecoveryKeyStoreConnectionImplTest() override = default;

  RecoveryKeyStoreConnection* connection() { return connection_.get(); }

  std::unique_ptr<RecoveryKeyStoreConnection::Request> UpdateRecoveryKeyStore(
      const CoreAccountInfo& account_info,
      const trusted_vault_pb::Vault& request,
      RecoveryKeyStoreConnection::UpdateRecoveryKeyStoreCallback callback) {
    return connection()->UpdateRecoveryKeyStore(account_info, request,
                                                std::move(callback));
  }

  trusted_vault_pb::Vault MakeRequest() {
    trusted_vault_pb::Vault vault;
    vault.add_application_keys()->set_key_name(kPasskeysApplicationKeyName);
    vault.mutable_chrome_os_metadata()->set_device_id("test device id");
    return vault;
  }

  bool SimulateResponse(std::string_view url,
                        net::HttpStatusCode response_http_code,
                        std::string_view content = "") {
    return test_url_loader_factory_.SimulateResponseForPendingRequest(
        url, content, response_http_code,
        static_cast<ResponseMatchFlags>(ResponseMatchFlags::kUrlMatchPrefix |
                                        ResponseMatchFlags::kWaitForRequest));
  }

  bool SimulateNetworkError(std::string_view url, net::Error err) {
    return test_url_loader_factory_.SimulateResponseForPendingRequest(
        GURL(url), network::URLLoaderCompletionStatus(err),
        network::mojom::URLResponseHead::New(),
        /*content=*/"",
        static_cast<ResponseMatchFlags>(ResponseMatchFlags::kUrlMatchPrefix |
                                        ResponseMatchFlags::kWaitForRequest));
  }

  network::TestURLLoaderFactory::PendingRequest* GetPendingHTTPRequest() {
    test_url_loader_factory_.WaitForRequest(
        GURL(""), ResponseMatchFlags::kUrlMatchPrefix);
    CHECK_EQ(test_url_loader_factory_.pending_requests()->size(), 1u);
    return test_url_loader_factory_.GetPendingRequest(/*index=*/0);
  }

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

  network::TestURLLoaderFactory test_url_loader_factory_;

  std::unique_ptr<RecoveryKeyStoreConnection> connection_ =
      std::make_unique<RecoveryKeyStoreConnectionImpl>(
          base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
              &test_url_loader_factory_)
              ->Clone(),
          std::make_unique<FakeTrustedVaultAccessTokenFetcher>(
              signin::AccessTokenInfo(
                  "test access token",
                  /*expiration_time_param=*/base::Time::Max(),
                  /*id_token=*/"")));

  base::SimpleTestClock clock_;
};

TEST_F(RecoveryKeyStoreConnectionImplTest,
       ShouldUpdateRecoveryKeyStoreAndHandleSuccess) {
  const trusted_vault_pb::Vault request_proto = MakeRequest();

  base::MockCallback<RecoveryKeyStoreConnection::UpdateRecoveryKeyStoreCallback>
      callback;

  std::unique_ptr<TrustedVaultConnection::Request> request =
      UpdateRecoveryKeyStore(CoreAccountInfo(), request_proto, callback.Get());
  EXPECT_THAT(request, NotNull());

  const network::TestURLLoaderFactory::PendingRequest* pending_request =
      GetPendingHTTPRequest();
  ASSERT_THAT(pending_request, NotNull());
  const network::ResourceRequest& resource_request = pending_request->request;
  EXPECT_THAT(resource_request.method, Eq("PATCH"));
  EXPECT_THAT(resource_request.url, Eq(kUpdateVaultUrl));

  trusted_vault_pb::Vault deserialized_body;
  EXPECT_TRUE(deserialized_body.ParseFromString(
      network::GetUploadData(resource_request)));
  EXPECT_THAT(network::GetUploadData(resource_request),
              Eq(request_proto.SerializeAsString()));

  EXPECT_CALL(callback, Run(Eq(RecoveryKeyStoreStatus::kSuccess)));
  SimulateResponse(kUpdateVaultUrl, net::HttpStatusCode::HTTP_OK);
}

TEST_F(RecoveryKeyStoreConnectionImplTest,
       ShouldHandleAccessTokenFetchFailureDuringUpdateRecoveryKeyStore) {
  // Simulate a persistent failure during access token fetching when making an
  // RecoveryKeyStoreConnection requests.
  auto connection = std::make_unique<RecoveryKeyStoreConnectionImpl>(
      base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
          &test_url_loader_factory_)
          ->Clone(),
      std::make_unique<FakeTrustedVaultAccessTokenFetcher>(
          base::unexpected(TrustedVaultAccessTokenFetcher::FetchingError::
                               kPersistentAuthError)));

  UpdateRecoveryKeyStoreFuture future;
  std::unique_ptr<TrustedVaultConnection::Request> request =
      connection->UpdateRecoveryKeyStore(CoreAccountInfo(), MakeRequest(),
                                         future.GetCallback());
  ASSERT_THAT(request, NotNull());
  EXPECT_EQ(future.Get(),
            RecoveryKeyStoreStatus::kPersistentAccessTokenFetchError);

  // No network requests should be made.
  EXPECT_EQ(test_url_loader_factory_.total_requests(), 0u);
}

TEST_F(RecoveryKeyStoreConnectionImplTest,
       ShouldHandleFailedUpdateRecoveryKeyStoreRequest) {
  for (const net::HttpStatusCode http_status :
       {net::HTTP_BAD_REQUEST, net::HTTP_NOT_FOUND, net::HTTP_CONFLICT,
        net::HTTP_INTERNAL_SERVER_ERROR}) {
    base::MockCallback<
        RecoveryKeyStoreConnection::UpdateRecoveryKeyStoreCallback>
        callback;
    std::unique_ptr<TrustedVaultConnection::Request> request =
        connection()->UpdateRecoveryKeyStore(CoreAccountInfo(), MakeRequest(),
                                             callback.Get());
    ASSERT_THAT(request, NotNull());

    EXPECT_CALL(callback, Run(Eq(RecoveryKeyStoreStatus::kOtherError)));
    SimulateResponse(kUpdateVaultUrl, http_status);
  }
}

TEST_F(RecoveryKeyStoreConnectionImplTest,
       ShouldHandleNetworkErrorDuringUpdateRecoveryKeyStore) {
  base::MockCallback<RecoveryKeyStoreConnection::UpdateRecoveryKeyStoreCallback>
      callback;
  std::unique_ptr<TrustedVaultConnection::Request> request =
      connection()->UpdateRecoveryKeyStore(CoreAccountInfo(), MakeRequest(),
                                           callback.Get());
  ASSERT_THAT(request, NotNull());

  EXPECT_CALL(callback, Run(Eq(RecoveryKeyStoreStatus::kNetworkError)));
  SimulateNetworkError(kUpdateVaultUrl, net::ERR_FAILED);
}

TEST_F(RecoveryKeyStoreConnectionImplTest, ShouldListVaults) {
  ListVaultsFuture future;
  std::unique_ptr<TrustedVaultConnection::Request> request =
      connection()->ListRecoveryKeyStores(CoreAccountInfo(),
                                          future.GetCallback());
  ASSERT_THAT(request, NotNull());

  trusted_vault_pb::ListVaultsResponse response;
  trusted_vault_pb::Vault* vault = response.add_vaults();
  vault->mutable_vault_parameters()->set_backend_public_key(kBackendPublicKey1);
  vault->mutable_vault_parameters()->set_vault_handle(kVaultHandle1);
  const network::ResourceRequest& url_request =
      GetPendingHTTPRequest()->request;
  EXPECT_EQ(url_request.method, "GET");
  EXPECT_THAT(url_request.url.GetQuery(), Not(HasSubstr("page_token")));
  EXPECT_THAT(url_request.url.GetQuery(), HasSubstr("use_case=13"));
  EXPECT_THAT(url_request.url.GetQuery(),
              HasSubstr("challenge_not_required=1"));
  SimulateResponse(kListVaultsUrl, net::HTTP_OK, response.SerializeAsString());

  auto result = future.Take();
  ASSERT_TRUE(result.has_value());
  EXPECT_EQ(result.value().size(), 1u);
  EXPECT_EQ(result.value().at(0).backend_public_key,
            ProtoStringToBytes(kBackendPublicKey1));
  EXPECT_EQ(result.value().at(0).vault_handle,
            ProtoStringToBytes(kVaultHandle1));
}

TEST_F(RecoveryKeyStoreConnectionImplTest, ShouldListVaultsWithPagination) {
  ListVaultsFuture future;
  std::unique_ptr<TrustedVaultConnection::Request> request =
      connection()->ListRecoveryKeyStores(CoreAccountInfo(),
                                          future.GetCallback());
  ASSERT_THAT(request, NotNull());
  {
    trusted_vault_pb::ListVaultsResponse response;
    trusted_vault_pb::Vault* vault = response.add_vaults();
    vault->mutable_vault_parameters()->set_backend_public_key(
        kBackendPublicKey1);
    vault->mutable_vault_parameters()->set_vault_handle(kVaultHandle1);
    response.set_next_page_token("next-page-token");
    EXPECT_THAT(GetPendingHTTPRequest()->request.url.GetQuery(),
                Not(HasSubstr("page_token")));
    SimulateResponse(kListVaultsUrl, net::HTTP_OK,
                     response.SerializeAsString());
    ASSERT_FALSE(future.IsReady());
  }
  {
    trusted_vault_pb::ListVaultsResponse response;
    trusted_vault_pb::Vault* vault = response.add_vaults();
    vault->mutable_vault_parameters()->set_backend_public_key(
        kBackendPublicKey2);
    vault->mutable_vault_parameters()->set_vault_handle(kVaultHandle2);
    EXPECT_THAT(GetPendingHTTPRequest()->request.url.GetQuery(),
                HasSubstr("page_token=next-page-token"));
    SimulateResponse(kListVaultsUrl, net::HTTP_OK,
                     response.SerializeAsString());
  }
  auto result = future.Take();
  ASSERT_TRUE(result.has_value());
  EXPECT_EQ(result.value().size(), 2u);
  EXPECT_EQ(result.value().at(0).backend_public_key,
            ProtoStringToBytes(kBackendPublicKey1));
  EXPECT_EQ(result.value().at(0).vault_handle,
            ProtoStringToBytes(kVaultHandle1));
  EXPECT_EQ(result.value().at(1).backend_public_key,
            ProtoStringToBytes(kBackendPublicKey2));
  EXPECT_EQ(result.value().at(1).vault_handle,
            ProtoStringToBytes(kVaultHandle2));
}

TEST_F(RecoveryKeyStoreConnectionImplTest,
       ShouldHandleHttpErrorDuringListVaults) {
  base::MockCallback<RecoveryKeyStoreConnection::ListRecoveryKeyStoresCallback>
      callback;
  std::unique_ptr<TrustedVaultConnection::Request> request =
      connection()->ListRecoveryKeyStores(CoreAccountInfo(), callback.Get());
  ASSERT_THAT(request, NotNull());

  EXPECT_CALL(callback,
              Run(Eq(base::unexpected(RecoveryKeyStoreStatus::kOtherError))));
  SimulateResponse(kListVaultsUrl, net::HTTP_BAD_REQUEST);
}

TEST_F(RecoveryKeyStoreConnectionImplTest,
       ShouldHandleNetworkErrorDuringListVaults) {
  base::MockCallback<RecoveryKeyStoreConnection::ListRecoveryKeyStoresCallback>
      callback;
  std::unique_ptr<TrustedVaultConnection::Request> request =
      connection()->ListRecoveryKeyStores(CoreAccountInfo(), callback.Get());
  ASSERT_THAT(request, NotNull());

  EXPECT_CALL(callback,
              Run(Eq(base::unexpected(RecoveryKeyStoreStatus::kNetworkError))));
  SimulateNetworkError(kListVaultsUrl, net::ERR_FAILED);
}

TEST_F(RecoveryKeyStoreConnectionImplTest,
       ShouldHandleBadProtoResponseDuringListVaults) {
  base::MockCallback<RecoveryKeyStoreConnection::ListRecoveryKeyStoresCallback>
      callback;
  std::unique_ptr<TrustedVaultConnection::Request> request =
      connection()->ListRecoveryKeyStores(CoreAccountInfo(), callback.Get());
  ASSERT_THAT(request, NotNull());

  EXPECT_CALL(callback,
              Run(Eq(base::unexpected(RecoveryKeyStoreStatus::kOtherError))));
  SimulateResponse(kListVaultsUrl, net::HTTP_OK, "This is not a proto");
}

TEST_F(RecoveryKeyStoreConnectionImplTest,
       ShouldHandleNetworkErrorDuringFetch) {
  for (const std::string_view error_url :
       {kRecoveryKeyStoreSigFileUrl, kRecoveryKeyStoreCertFileUrl}) {
    base::HistogramTester histogram_tester;
    FetchCertCallback callback;
    std::unique_ptr<TrustedVaultConnection::Request> request =
        connection()->FetchRecoveryKeyStoreCertificates(&clock_,
                                                        callback.Get());
    ASSERT_THAT(request, NotNull());
    EXPECT_CALL(callback,
                Run(Eq(base::unexpected(
                    RecoveryKeyStoreCertificateFetchStatus::kNetworkError))));
    SimulateNetworkError(error_url, net::ERR_FAILED);
    histogram_tester.ExpectUniqueSample(
        "TrustedVault.RecoveryKeyStoreCertificatesFetchStatus",
        RecoveryKeyStoreCertificatesFetchStatusForUMA::kNetworkError, 1);
  }
}

TEST_F(RecoveryKeyStoreConnectionImplTest,
       ShouldHandleParseErrorDuringFetchCerts) {
  base::HistogramTester histogram_tester;
  FetchCertCallback callback;
  std::unique_ptr<TrustedVaultConnection::Request> request =
      connection()->FetchRecoveryKeyStoreCertificates(&clock_, callback.Get());
  ASSERT_THAT(request, NotNull());
  EXPECT_CALL(callback,
              Run(Eq(base::unexpected(
                  RecoveryKeyStoreCertificateFetchStatus::kParseError))));
  SimulateResponse(kRecoveryKeyStoreCertFileUrl, net::HTTP_OK,
                   "This is not a certificate file");
  SimulateResponse(kRecoveryKeyStoreSigFileUrl, net::HTTP_OK,
                   test_certs::kSigXml);
  histogram_tester.ExpectUniqueSample(
      "TrustedVault.RecoveryKeyStoreCertificatesFetchStatus",
      RecoveryKeyStoreCertificatesFetchStatusForUMA::kParseError, 1);
}

TEST_F(RecoveryKeyStoreConnectionImplTest, ShouldFetchAndParseCertificates) {
  base::HistogramTester histogram_tester;
  FetchCertFuture future;
  std::unique_ptr<TrustedVaultConnection::Request> request =
      connection()->FetchRecoveryKeyStoreCertificates(&clock_,
                                                      future.GetCallback());
  SimulateResponse(kRecoveryKeyStoreCertFileUrl, net::HTTP_OK,
                   test_certs::kCertXml);
  SimulateResponse(kRecoveryKeyStoreSigFileUrl, net::HTTP_OK,
                   test_certs::kSigXml);
  base::expected<RecoveryKeyStoreCertificate,
                 RecoveryKeyStoreCertificateFetchStatus>
      result = future.Take();
  ASSERT_TRUE(result.has_value());
  EXPECT_EQ(result->endpoint_public_keys().size(), 3u);
  histogram_tester.ExpectUniqueSample(
      "TrustedVault.RecoveryKeyStoreCertificatesFetchStatus",
      RecoveryKeyStoreCertificatesFetchStatusForUMA::kSuccess, 1);
}

}  // namespace

}  // namespace trusted_vault
