// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "crypto/apple/keychain_secitem.h"

#import <Foundation/Foundation.h>

#import "base/apple/bridging.h"
#import "base/apple/foundation_util.h"
#import "base/apple/scoped_cftyperef.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/scoped_feature_list.h"
#import "crypto/features.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/platform_test.h"

namespace {

using base::apple::CFToNSPtrCast;

// Use unique names for the test item to avoid conflicts.
const char kTestServiceName[] = "KeychainAccessibilityMigrationTest.Service";
const char kTestAccountName[] = "KeychainAccessibilityMigrationTest.Account";
const std::vector<uint8_t> kTestPassword = {'s', 'e', 'c', 'r', 'e', 't'};

}  // namespace

class KeychainAccessibilityMigrationIOSTest : public PlatformTest {
 public:
  KeychainAccessibilityMigrationIOSTest() = default;

  void SetUp() override {
    PlatformTest::SetUp();

    // Get current test name to distinguish PRE_ phase.
    const std::string& test_name =
        testing::UnitTest::GetInstance()->current_test_info()->name();

    if (test_name.rfind("PRE_", 0) == 0) {
      feature_list_.InitAndDisableFeature(
          crypto::features::kMigrateIOSKeychainAccessibility);
    } else {
      feature_list_.InitAndEnableFeature(
          crypto::features::kMigrateIOSKeychainAccessibility);
    }
  }

  static void TearDownTestSuite() {
    // Clean up the keychain item after all tests in the suite have run.
    DeleteGenericPasswordForTest(kTestServiceName, kTestAccountName);
  }

 protected:
  // Gets the accessibility attribute of the test keychain item.
  base::expected<std::string, OSStatus> GetKeychainItemAccessibility() {
    NSDictionary* query = @{
      CFToNSPtrCast(kSecClass) : CFToNSPtrCast(kSecClassGenericPassword),
      CFToNSPtrCast(kSecAttrService) :
          base::SysUTF8ToNSString(kTestServiceName),
      CFToNSPtrCast(kSecAttrAccount) :
          base::SysUTF8ToNSString(kTestAccountName),
      CFToNSPtrCast(kSecReturnAttributes) : @YES,
    };
    base::apple::ScopedCFTypeRef<CFTypeRef> result;
    OSStatus status = SecItemCopyMatching(base::apple::NSToCFPtrCast(query),
                                          result.InitializeInto());
    if (status != noErr) {
      return base::unexpected(status);
    }
    CFDictionaryRef result_dict =
        base::apple::CFCast<CFDictionaryRef>(result.get());
    return base::SysCFStringRefToUTF8(
        base::apple::GetValueFromDictionary<CFStringRef>(result_dict,
                                                         kSecAttrAccessible));
  }

  static void DeleteGenericPasswordForTest(std::string_view service_name,
                                           std::string_view account_name) {
    NSDictionary* query = @{
      CFToNSPtrCast(kSecClass) : CFToNSPtrCast(kSecClassGenericPassword),
      CFToNSPtrCast(kSecAttrService) : base::SysUTF8ToNSString(service_name),
      CFToNSPtrCast(kSecAttrAccount) : base::SysUTF8ToNSString(account_name),
    };
    SecItemDelete(base::apple::NSToCFPtrCast(query));
  }

  crypto::apple::KeychainSecItem keychain_;
  base::test::ScopedFeatureList feature_list_;
};

// Phase 1: Add keychain item with kMigrateIOSKeychainAccessibility disabled.
// The item will be stored using kSecAttrAccessibleWhenUnlocked.
TEST_F(KeychainAccessibilityMigrationIOSTest, PRE_MigrateItem) {
  // Verify that the keychain item doesn't already exist. This is a check
  // that the test suite's cleanup is working correctly.
  base::expected<std::vector<uint8_t>, OSStatus> result_before =
      keychain_.FindGenericPassword(kTestServiceName, kTestAccountName);
  ASSERT_FALSE(result_before.has_value())
      << "Keychain item existed before test. Prior test state not cleaned up.";

  // Verify feature is disabled as configured in SetUp.
  ASSERT_FALSE(base::FeatureList::IsEnabled(
      crypto::features::kMigrateIOSKeychainAccessibility));

  // Add a password to the keychain.
  OSStatus status = keychain_.AddGenericPassword(
      kTestServiceName, kTestAccountName, kTestPassword);
  ASSERT_EQ(status, noErr)
      << "PRE_MigrateItem: AddGenericPassword failed with OSStatus " << status;

  // Sanity check: ensure the password can be retrieved immediately.
  base::expected<std::vector<uint8_t>, OSStatus> result =
      keychain_.FindGenericPassword(kTestServiceName, kTestAccountName);
  ASSERT_TRUE(result.has_value())
      << "PRE_MigrateItem: FindGenericPassword failed. OSStatus: "
      << result.error();
  EXPECT_EQ(result.value(), kTestPassword);
}

// Phase 2: Read keychain item with kMigrateIOSKeychainAccessibility enabled.
// This should find the item and trigger an update of its accessibility
// attribute to kSecAttrAccessibleAfterFirstUnlock within FindGenericPassword.
TEST_F(KeychainAccessibilityMigrationIOSTest, MigrateItem) {
  // Verify feature is enabled as configured in SetUp.
  ASSERT_TRUE(base::FeatureList::IsEnabled(
      crypto::features::kMigrateIOSKeychainAccessibility));

  // Check that the accessibility is `kSecAttrAccessibleWhenUnlocked` before
  // migration.
  base::expected<std::string, OSStatus> accessibility_before =
      GetKeychainItemAccessibility();
  ASSERT_TRUE(accessibility_before.has_value());
  EXPECT_EQ(accessibility_before.value(),
            base::SysCFStringRefToUTF8(kSecAttrAccessibleWhenUnlocked));

  // Attempt to find the password. This call should succeed and trigger the
  // migration.
  base::expected<std::vector<uint8_t>, OSStatus> result =
      keychain_.FindGenericPassword(kTestServiceName, kTestAccountName);
  ASSERT_TRUE(result.has_value())
      << "MigrateItem: FindGenericPassword failed. OSStatus: "
      << result.error();
  EXPECT_EQ(result.value(), kTestPassword);

  // Check that the accessibility is `kSecAttrAccessibleAfterFirstUnlock` after
  // migration.
  base::expected<std::string, OSStatus> accessibility_after =
      GetKeychainItemAccessibility();
  ASSERT_TRUE(accessibility_after.has_value());
  EXPECT_EQ(accessibility_after.value(),
            base::SysCFStringRefToUTF8(kSecAttrAccessibleAfterFirstUnlock));
}
