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

#include <optional>

#include "base/test/test_future.h"
#include "build/build_config.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/test/browser_task_environment.h"
#include "crypto/apple/keychain_v2.h"
#include "crypto/apple/scoped_fake_keychain_v2.h"
#include "device/fido/mac/authenticator_config.h"
#include "device/fido/mac/credential_store.h"
#include "device/fido/public/public_key_credential_user_entity.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

using base::test::TestFuture;

static const device::PublicKeyCredentialUserEntity kUser({1, 2, 3},
                                                         "doe@example.com",
                                                         "John Doe");
constexpr char kRpId[] = "example.com";

class LocalCredentialManagementTest : public testing::Test {
 protected:
  LocalCredentialManagementTest() = default;

  content::BrowserTaskEnvironment task_environment_;
  TestingProfile profile_;
  device::fido::mac::AuthenticatorConfig config_{
      .keychain_access_group = "test-keychain-access-group",
      .metadata_secret = "TestMetadataSecret"};
  LocalCredentialManagementMac local_cred_man_{config_};
  crypto::apple::ScopedFakeKeychainV2 keychain_{config_.keychain_access_group};
  device::fido::mac::TouchIdCredentialStore store_{config_};
};

TEST_F(LocalCredentialManagementTest, NoCredentials) {
  TestFuture<bool> future;
  local_cred_man_.HasCredentials(future.GetCallback());
  EXPECT_FALSE(future.IsReady());
  EXPECT_TRUE(future.Wait());
  EXPECT_FALSE(future.Get());

  TestFuture<std::optional<std::vector<device::DiscoverableCredentialMetadata>>>
      enumerate_future;
  local_cred_man_.Enumerate(enumerate_future.GetCallback());
  EXPECT_FALSE(enumerate_future.IsReady());
  EXPECT_TRUE(enumerate_future.Wait());
  auto result = enumerate_future.Get();
  ASSERT_TRUE(result.has_value());
  EXPECT_TRUE(result->empty());
}

TEST_F(LocalCredentialManagementTest, OneCredential) {
  TestFuture<bool> future;
  auto credential = store_.CreateCredential(
      kRpId, kUser, device::fido::mac::TouchIdCredentialStore::kDiscoverable);
  EXPECT_TRUE(credential);
  local_cred_man_.HasCredentials(future.GetCallback());
  EXPECT_FALSE(future.IsReady());
  EXPECT_TRUE(future.Wait());
  EXPECT_TRUE(future.Get());

  TestFuture<std::optional<std::vector<device::DiscoverableCredentialMetadata>>>
      enumerate_future;
  local_cred_man_.Enumerate(enumerate_future.GetCallback());
  EXPECT_FALSE(enumerate_future.IsReady());
  EXPECT_TRUE(enumerate_future.Wait());
  const std::optional<std::vector<device::DiscoverableCredentialMetadata>>
      result = enumerate_future.Get();
  ASSERT_TRUE(result.has_value());
  ASSERT_EQ(result->size(), 1u);
  EXPECT_EQ(result->at(0).rp_id, kRpId);
}

TEST_F(LocalCredentialManagementTest, DeleteCredential) {
  auto credential = store_.CreateCredential(
      kRpId, kUser, device::fido::mac::TouchIdCredentialStore::kDiscoverable);
  ASSERT_TRUE(credential);
  auto credentials = store_.FindResidentCredentials(kRpId);
  ASSERT_TRUE(credentials);
  EXPECT_EQ(credentials->size(), 1u);
  {
    TestFuture<bool> future;
    local_cred_man_.Delete(credential->first.credential_id,
                           future.GetCallback());
    EXPECT_FALSE(future.IsReady());
    EXPECT_TRUE(future.Wait());
    EXPECT_TRUE(future.Get());
  }
  credentials = store_.FindResidentCredentials(kRpId);
  ASSERT_TRUE(credentials);
  EXPECT_EQ(store_.FindResidentCredentials(kRpId)->size(), 0u);

  {
    TestFuture<bool> future;
    local_cred_man_.Delete(std::vector<uint8_t>{8}, future.GetCallback());
    EXPECT_FALSE(future.IsReady());
    EXPECT_TRUE(future.Wait());
    EXPECT_FALSE(future.Get());
  }
}

TEST_F(LocalCredentialManagementTest, EditCredential) {
  TestFuture<bool> future;
  auto credential = store_.CreateCredential(
      kRpId, kUser, device::fido::mac::TouchIdCredentialStore::kDiscoverable);
  ASSERT_TRUE(credential);
  auto credentials =
      device::fido::mac::TouchIdCredentialStore::FindCredentialsForTesting(
          config_, kRpId);
  EXPECT_EQ(credentials.size(), 1u);
  local_cred_man_.Edit(credential->first.credential_id, "new-username",
                       future.GetCallback());
  EXPECT_FALSE(future.IsReady());
  EXPECT_TRUE(future.Wait());
  EXPECT_TRUE(future.Get());

  credentials =
      device::fido::mac::TouchIdCredentialStore::FindCredentialsForTesting(
          config_, kRpId);
  EXPECT_EQ(credentials.size(), 1u);
  EXPECT_EQ(credentials.front().metadata.user_name, "new-username");
}

TEST_F(LocalCredentialManagementTest, EditLongCredential) {
  TestFuture<bool> future;
  auto credential = store_.CreateCredential(
      kRpId, kUser, device::fido::mac::TouchIdCredentialStore::kDiscoverable);
  ASSERT_TRUE(credential);
  auto credentials =
      device::fido::mac::TouchIdCredentialStore::FindCredentialsForTesting(
          config_, kRpId);
  EXPECT_EQ(credentials.size(), 1u);
  local_cred_man_.Edit(
      credential->first.credential_id,
      "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
      future.GetCallback());
  EXPECT_FALSE(future.IsReady());
  EXPECT_TRUE(future.Wait());
  EXPECT_TRUE(future.Get());

  credentials =
      device::fido::mac::TouchIdCredentialStore::FindCredentialsForTesting(
          config_, kRpId);
  EXPECT_EQ(credentials.size(), 1u);
  EXPECT_EQ(
      credentials.front().metadata.user_name,
      "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA…");
}

TEST_F(LocalCredentialManagementTest, EditUnknownCredential) {
  TestFuture<bool> future;
  auto credential = store_.CreateCredential(
      kRpId, kUser, device::fido::mac::TouchIdCredentialStore::kDiscoverable);
  ASSERT_TRUE(credential);
  auto credentials =
      device::fido::mac::TouchIdCredentialStore::FindCredentialsForTesting(
          config_, kRpId);
  EXPECT_EQ(credentials.size(), 1u);
  uint8_t credential_id[] = {0xa};
  local_cred_man_.Edit(credential_id, "new-username", future.GetCallback());
  EXPECT_FALSE(future.IsReady());
  EXPECT_TRUE(future.Wait());
  EXPECT_FALSE(future.Get());
}

class ScopedMockKeychain : crypto::apple::KeychainV2 {
 public:
  ScopedMockKeychain() { SetInstanceOverride(this); }
  ~ScopedMockKeychain() override { ClearInstanceOverride(); }

  MOCK_METHOD(OSStatus,
              ItemCopyMatching,
              (CFDictionaryRef query, CFTypeRef* result),
              (override));
};

class MockKeychainLocalCredentialManagementTest : public testing::Test {
 protected:
  content::BrowserTaskEnvironment task_environment_;
  ScopedMockKeychain mock_keychain_;
  LocalCredentialManagementMac local_cred_man_{
      {.keychain_access_group = "test-keychain-access-group",
       .metadata_secret = "TestMetadataSecret"}};
};

// Regression test for crbug.com/40250693.
TEST_F(MockKeychainLocalCredentialManagementTest, KeychainError) {
  EXPECT_CALL(mock_keychain_, ItemCopyMatching)
      .WillOnce(testing::Return(errSecInternalComponent));
  TestFuture<bool> future;
  local_cred_man_.HasCredentials(future.GetCallback());
  EXPECT_FALSE(future.IsReady());
  EXPECT_TRUE(future.Wait());
  EXPECT_FALSE(future.Get());
  testing::Mock::VerifyAndClearExpectations(&mock_keychain_);

  EXPECT_CALL(mock_keychain_, ItemCopyMatching)
      .WillOnce(testing::Return(errSecInternalComponent));
  TestFuture<std::optional<std::vector<device::DiscoverableCredentialMetadata>>>
      enumerate_future;
  local_cred_man_.Enumerate(enumerate_future.GetCallback());
  EXPECT_FALSE(enumerate_future.IsReady());
  EXPECT_TRUE(enumerate_future.Wait());
  EXPECT_FALSE(enumerate_future.Get());
}

}  // namespace
