// 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/webauthn/unexportable_key_utils.h"

#include <memory>
#include <string>

#include "ash/constants/ash_features.h"
#include "ash/public/cpp/auth/active_session_auth_controller.h"
#include "ash/public/cpp/auth/active_session_fingerprint_client.h"
#include "ash/public/cpp/webauthn_dialog_controller.h"
#include "base/containers/span.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "chromeos/ash/components/login/auth/public/user_context.h"
#include "chromeos/ash/components/osauth/impl/request/webauthn_auth_request.h"
#include "chromeos/ash/components/osauth/public/request/auth_request.h"
#include "crypto/signature_verifier.h"
#include "crypto/user_verifying_key.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/aura/test/test_windows.h"
#include "ui/aura/window.h"

namespace {

constexpr char kRpId[] = "example.com";

using testing::_;

class MockWebAuthNDialogController : public ash::WebAuthNDialogController {
 public:
  MockWebAuthNDialogController() = default;

  ~MockWebAuthNDialogController() override = default;

  MOCK_METHOD(void, SetClient, (ash::InSessionAuthDialogClient*), (override));
  MOCK_METHOD(void,
              ShowAuthenticationDialog,
              (aura::Window * source_window,
               const std::string& origin_name,
               FinishCallback finish_callback),
              (override));
  MOCK_METHOD(void, DestroyAuthenticationDialog, (), (override));
  MOCK_METHOD(void,
              AuthenticateUserWithPasswordOrPin,
              (const std::string& password,
               bool authenticated_by_pin,
               OnAuthenticateCallback callback),
              (override));
  MOCK_METHOD(void,
              AuthenticateUserWithFingerprint,
              (base::OnceCallback<void(bool, ash::FingerprintState)>),
              (override));
  MOCK_METHOD(void, OpenInSessionAuthHelpPage, (), (override));
  MOCK_METHOD(void, Cancel, (), (override));
  MOCK_METHOD(void,
              CheckAvailability,
              (FinishCallback on_availability_checked),
              (const, override));
};

class MockActiveSessionAuthController
    : public ash::ActiveSessionAuthController {
 public:
  MockActiveSessionAuthController() = default;

  ~MockActiveSessionAuthController() override = default;

  MOCK_METHOD(bool,
              ShowAuthDialog,
              (std::unique_ptr<ash::AuthRequest> auth_request),
              (override));
  MOCK_METHOD(bool, IsShown, (), (const override));
  MOCK_METHOD(void,
              SetFingerprintClient,
              (ash::ActiveSessionFingerprintClient * fp_client),
              (override));
};

class UserVerifyingKeyUtilsCrosTest
    : public ::testing::Test,
      public ::testing::WithParamInterface<bool> {
 public:
  UserVerifyingKeyUtilsCrosTest() = default;
  ~UserVerifyingKeyUtilsCrosTest() override = default;
  UserVerifyingKeyUtilsCrosTest(const UserVerifyingKeyUtilsCrosTest&) = delete;
  UserVerifyingKeyUtilsCrosTest& operator=(
      const UserVerifyingKeyUtilsCrosTest&) = delete;

  void SetUp() override {
    if (GetParam()) {
      feature_list_.InitAndEnableFeature(
          ash::features::kWebAuthNAuthDialogMerge);
      return;
    }

    feature_list_.InitAndDisableFeature(
        ash::features::kWebAuthNAuthDialogMerge);
  }

  UserVerifyingKeyProviderConfigChromeos MakeKeyProviderConfig(
      aura::Window* window) {
    if (GetParam()) {
      return UserVerifyingKeyProviderConfigChromeos{&dialog_controller_, window,
                                                    kRpId};
    }

    return UserVerifyingKeyProviderConfigChromeos{&legacy_dialog_controller_,
                                                  window, kRpId};
  }

  void AssertRequestContainsRpId(ash::AuthRequest* request) {
    ASSERT_EQ(static_cast<ash::WebAuthNAuthRequest*>(request)->GetRpId(),
              kRpId);
  }

 protected:
  MockWebAuthNDialogController legacy_dialog_controller_;
  MockActiveSessionAuthController dialog_controller_;
  base::test::TaskEnvironment task_environment_;
  base::test::ScopedFeatureList feature_list_;
};

INSTANTIATE_TEST_SUITE_P(UnexportableKeyUtils,
                         UserVerifyingKeyUtilsCrosTest,
                         testing::Bool());

TEST_P(UserVerifyingKeyUtilsCrosTest,
       UserVerifyingKeyProvider_GeneratedKeyCanBeImported) {
  std::unique_ptr<aura::Window> window =
      aura::test::CreateTestWindow({.bounds = {100, 100}, .window_id = 1});
  std::unique_ptr<crypto::UserVerifyingKeyProvider> provider =
      GetWebAuthnUserVerifyingKeyProvider(MakeKeyProviderConfig(window.get()));
  ASSERT_TRUE(provider);
  base::test::TestFuture<
      base::expected<std::unique_ptr<crypto::UserVerifyingSigningKey>,
                     crypto::UserVerifyingKeyCreationError>>
      future;
  provider->GenerateUserVerifyingSigningKey(
      base::span_from_ref(crypto::SignatureVerifier::ECDSA_SHA256),
      future.GetCallback());
  crypto::UserVerifyingSigningKey& signing_key = *future.Get().value();

  base::test::TestFuture<
      base::expected<std::unique_ptr<crypto::UserVerifyingSigningKey>,
                     crypto::UserVerifyingKeyCreationError>>
      get_future;
  provider->GetUserVerifyingSigningKey(signing_key.GetKeyLabel(),
                                       get_future.GetCallback());
  crypto::UserVerifyingSigningKey& imported_signing_key =
      *get_future.Get().value();
  EXPECT_EQ(signing_key.GetPublicKey(), imported_signing_key.GetPublicKey());
}

TEST_P(UserVerifyingKeyUtilsCrosTest,
       UserVerifyingKeyProvider_SigningShowsInSessionAuthChallenge) {
  std::unique_ptr<aura::Window> window =
      aura::test::CreateTestWindow({.bounds = {100, 100}, .window_id = 1});
  std::unique_ptr<crypto::UserVerifyingKeyProvider> provider =
      GetWebAuthnUserVerifyingKeyProvider(MakeKeyProviderConfig(window.get()));
  ASSERT_TRUE(provider);

  // Simulate successful in-session auth.
  if (GetParam()) {
    EXPECT_CALL(dialog_controller_, ShowAuthDialog(_))
        .WillOnce([this](std::unique_ptr<ash::AuthRequest> request) {
          AssertRequestContainsRpId(request.get());
          request->NotifyAuthResult(nullptr,
                                    ash::AuthRequest::AuthResult::kSuccess);
          return true;
        });
  } else {
    EXPECT_CALL(legacy_dialog_controller_,
                ShowAuthenticationDialog(window.get(), kRpId, _))
        .WillOnce([](aura::Window*, const std::string& rp_id,
                     base::OnceCallback<void(bool)> callback) {
          ASSERT_EQ(rp_id, kRpId);
          std::move(callback).Run(true);
        });
  }

  base::test::TestFuture<
      base::expected<std::unique_ptr<crypto::UserVerifyingSigningKey>,
                     crypto::UserVerifyingKeyCreationError>>
      future;
  provider->GenerateUserVerifyingSigningKey(
      base::span_from_ref(crypto::SignatureVerifier::ECDSA_SHA256),
      future.GetCallback());
  crypto::UserVerifyingSigningKey& signing_key = *future.Get().value();
  base::test::TestFuture<base::expected<std::vector<uint8_t>,
                                        crypto::UserVerifyingKeySigningError>>
      signature_future;
  signing_key.Sign({{1, 2, 3}}, signature_future.GetCallback());
  EXPECT_TRUE(signature_future.Get().has_value());
  EXPECT_FALSE(signature_future.Get()->empty());
}

TEST_P(UserVerifyingKeyUtilsCrosTest,
       UserVerifyingKeyProvider_SigningWithoutUvYieldsNullopt) {
  std::unique_ptr<aura::Window> window =
      aura::test::CreateTestWindow({.bounds = {100, 100}, .window_id = 1});
  std::unique_ptr<crypto::UserVerifyingKeyProvider> provider =
      GetWebAuthnUserVerifyingKeyProvider(MakeKeyProviderConfig(window.get()));
  ASSERT_TRUE(provider);

  // Simulate failed in-session auth.
  if (GetParam()) {
    EXPECT_CALL(dialog_controller_, ShowAuthDialog(_))
        .WillOnce([this](std::unique_ptr<ash::AuthRequest> request) -> bool {
          AssertRequestContainsRpId(request.get());
          request->NotifyAuthResult(nullptr,
                                    ash::AuthRequest::AuthResult::kAuthFailed);
          return true;
        });
  } else {
    EXPECT_CALL(legacy_dialog_controller_,
                ShowAuthenticationDialog(window.get(), kRpId, _))
        .WillOnce([](aura::Window*, const std::string& rp_id,
                     base::OnceCallback<void(bool)> callback) {
          ASSERT_EQ(rp_id, kRpId);
          std::move(callback).Run(false);
        });
  }

  base::test::TestFuture<
      base::expected<std::unique_ptr<crypto::UserVerifyingSigningKey>,
                     crypto::UserVerifyingKeyCreationError>>
      signing_key_future;
  provider->GenerateUserVerifyingSigningKey(
      base::span_from_ref(crypto::SignatureVerifier::ECDSA_SHA256),
      signing_key_future.GetCallback());
  base::test::TestFuture<base::expected<std::vector<uint8_t>,
                                        crypto::UserVerifyingKeySigningError>>
      signature_future;
  (*signing_key_future.Get())
      ->Sign({{1, 2, 3}}, signature_future.GetCallback());
  EXPECT_FALSE(signature_future.Get().has_value());
}

TEST_P(UserVerifyingKeyUtilsCrosTest, UserVerifyingKeyProvider_DeleteIsANoOp) {
  std::unique_ptr<aura::Window> w1 =
      aura::test::CreateTestWindow({.bounds = {100, 100}, .window_id = 1});
  std::unique_ptr<crypto::UserVerifyingKeyProvider> provider =
      GetWebAuthnUserVerifyingKeyProvider(MakeKeyProviderConfig(w1.get()));
  ASSERT_TRUE(provider);
  base::test::TestFuture<bool> future;
  provider->DeleteUserVerifyingKey("test key label", future.GetCallback());
  EXPECT_TRUE(future.Get());
}

}  // namespace
