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

#include "device/fido/mac/fake_icloud_keychain_sys.h"

#import <AuthenticationServices/AuthenticationServices.h>
#import <objc/message.h>

#include <vector>

#include "base/containers/to_vector.h"
#include "base/functional/callback.h"
#include "base/strings/sys_string_conversions.h"
#include "device/fido/ctap_get_assertion_request.h"
#include "device/fido/ctap_make_credential_request.h"

// Fake subclass to simulate a largeBlob registration output.

API_AVAILABLE(macos(14.0))
@interface FakeLargeBlobRegistrationOutput
    : ASAuthorizationPublicKeyCredentialLargeBlobRegistrationOutput
@property(nonatomic) BOOL isSupported;
@end

@implementation FakeLargeBlobRegistrationOutput
@synthesize isSupported = _isSupported;
- (BOOL)isSupported {
  return _isSupported;
}
@end

// Fake subclass to simulate a largeBlob assertion output.

API_AVAILABLE(macos(14.0))
@interface FakeLargeBlobAssertionOutput
    : ASAuthorizationPublicKeyCredentialLargeBlobAssertionOutput
@property(nonatomic) BOOL didWrite;
@property(nonatomic, copy) NSData* readData;
@end

@implementation FakeLargeBlobAssertionOutput
@synthesize didWrite = _didWrite;
@synthesize readData = _readData;
@end

// A number of AuthenticationServices objects are subclassed so that the
// values of readonly properties can be overridden in tests.

API_AVAILABLE(macos(13.3))
@interface FakeBrowserPlatformPublicKeyCredential
    : ASAuthorizationWebBrowserPlatformPublicKeyCredential
@property(nonatomic, copy) NSData* credentialID;
@property(nonatomic, copy) NSString* name;
@property(nonatomic, copy) NSString* relyingParty;
@property(nonatomic, copy) NSData* userHandle;
@property(nonatomic, copy) NSString* providerName;
@end

@implementation FakeBrowserPlatformPublicKeyCredential
@synthesize credentialID = _credentialID;
@synthesize name = _name;
@synthesize relyingParty = _relyingParty;
@synthesize userHandle = _userHandle;
@synthesize providerName = _providerName;
@end

API_AVAILABLE(macos(13.3))
@interface FakeASAuthorization : ASAuthorization
@property(nonatomic, strong) id<ASAuthorizationCredential> credential;
@end

@implementation FakeASAuthorization
@synthesize credential = _credential;
@end

API_AVAILABLE(macos(13.3))
@interface FakeASAuthorizationPublicKeyCredentialRegistration
    : ASAuthorizationPlatformPublicKeyCredentialRegistration
@property(nonatomic, copy) NSData* rawAttestationObject;
@property(nonatomic, copy) NSData* rawClientDataJSON;
@property(nonatomic, copy) NSData* credentialID;
@property(nonatomic, strong) API_AVAILABLE(macos(14.0))
    FakeLargeBlobRegistrationOutput* largeBlob;
@end

@implementation FakeASAuthorizationPublicKeyCredentialRegistration
@synthesize rawAttestationObject = _rawAttestationObject;
@synthesize rawClientDataJSON = _rawClientDataJSON;
@synthesize credentialID = _credentialID;
@synthesize largeBlob = _largeBlob;

+ (BOOL)supportsSecureCoding {
  return NO;
}

- (instancetype)initWithCoder:(NSCoder*)aDecoder {
  NOTREACHED();
}

- (void)encodeWithCoder:(NSCoder*)aCoder {
  NOTREACHED();
}

- (id)copyWithZone:(NSZone*)zone {
  NOTREACHED();
}
@end

API_AVAILABLE(macos(13.3))
@interface FakeASAuthorizationPublicKeyCredentialAssertion
    : ASAuthorizationPlatformPublicKeyCredentialAssertion
@property(nonatomic, copy) NSData* rawAuthenticatorData;
@property(nonatomic, copy) NSData* signature;
@property(nonatomic, copy) NSData* userID;
@property(nonatomic, copy) NSData* credentialID;
@property(nonatomic, copy) NSData* rawClientDataJSON;
@property(nonatomic, strong) API_AVAILABLE(macos(14.0))
    FakeLargeBlobAssertionOutput* largeBlob;
@end

@implementation FakeASAuthorizationPublicKeyCredentialAssertion
@synthesize rawAuthenticatorData = _rawAuthenticatorData;
@synthesize signature = _signature;
@synthesize userID = _userID;
@synthesize credentialID = _credentialID;
@synthesize rawClientDataJSON = _rawClientDataJSON;
@synthesize largeBlob = _largeBlob;

+ (BOOL)supportsSecureCoding {
  return NO;
}

- (instancetype)initWithCoder:(NSCoder*)aDecoder {
  NOTREACHED();
}

- (void)encodeWithCoder:(NSCoder*)aCoder {
  NOTREACHED();
}

- (id)copyWithZone:(NSZone*)zone {
  NOTREACHED();
}
@end

namespace device::fido::icloud_keychain {

namespace {
NSData* ToNSData(base::span<const uint8_t> data) {
  return [NSData dataWithBytes:data.data() length:data.size()];
}
}  // namespace

FakeSystemInterface::FakeSystemInterface() = default;

void FakeSystemInterface::set_auth_state(AuthState auth_state) {
  auth_state_ = auth_state;
}

void FakeSystemInterface::set_next_auth_state(AuthState next_auth_state) {
  next_auth_state_ = next_auth_state;
}

void FakeSystemInterface::SetMakeCredentialCallback(
    base::RepeatingCallback<void(const CtapMakeCredentialRequest&)> callback) {
  create_callback_ = std::move(callback);
}

void FakeSystemInterface::SetMakeCredentialResult(
    base::span<const uint8_t> attestation_object_bytes,
    base::span<const uint8_t> credential_id) {
  CHECK(!make_credential_error_code_);
  make_credential_attestation_object_bytes_ =
      base::ToVector(attestation_object_bytes);
  make_credential_credential_id_ = base::ToVector(credential_id);
}

void FakeSystemInterface::SetMakeCredentialError(int code) {
  CHECK(!make_credential_attestation_object_bytes_);
  make_credential_error_code_ = code;
}

void FakeSystemInterface::SetGetAssertionCallback(
    base::RepeatingCallback<void(const CtapGetAssertionRequest&)> callback) {
  get_callback_ = std::move(callback);
}

void FakeSystemInterface::SetGetAssertionResult(
    base::span<const uint8_t> authenticator_data,
    base::span<const uint8_t> signature,
    base::span<const uint8_t> user_id,
    base::span<const uint8_t> credential_id) {
  get_assertion_authenticator_data_ = base::ToVector(authenticator_data);
  get_assertion_signature_ = base::ToVector(signature);
  get_assertion_user_id_ = base::ToVector(user_id);
  get_assertion_credential_id_ = base::ToVector(credential_id);
}

void FakeSystemInterface::SetGetAssertionError(int code, std::string msg) {
  get_assertion_error_ = std::make_pair(code, std::move(msg));
}

void FakeSystemInterface::SetCredentials(
    std::vector<DiscoverableCredentialMetadata> creds) {
  creds_ = std::move(creds);
}

bool FakeSystemInterface::IsAvailable() const {
  return true;
}

SystemInterface::AuthState FakeSystemInterface::GetAuthState() {
  return auth_state_;
}

void FakeSystemInterface::AuthorizeAndContinue(
    base::OnceCallback<void()> callback) {
  CHECK(next_auth_state_.has_value());
  auth_state_ = *next_auth_state_;
  std::move(callback).Run();
}

void FakeSystemInterface::GetPlatformCredentials(
    const std::string& rp_id,
    void (^block)(
        NSArray<ASAuthorizationWebBrowserPlatformPublicKeyCredential*>*)) {
  NSMutableArray<ASAuthorizationWebBrowserPlatformPublicKeyCredential*>* ret =
      [[NSMutableArray alloc] init];
  for (const auto& cred_values : creds_) {
    // `init` on `ASAuthorizationWebBrowserPlatformPublicKeyCredential` is
    // marked as `NS_UNAVAILABLE` and so is not called.
    FakeBrowserPlatformPublicKeyCredential* cred =
        [FakeBrowserPlatformPublicKeyCredential alloc];
    cred.credentialID = ToNSData(cred_values.cred_id);
    cred.userHandle = ToNSData(cred_values.user.id);
    cred.relyingParty = base::SysUTF8ToNSString(rp_id);
    cred.name = base::SysUTF8ToNSString(cred_values.user.name.value_or(""));
    [ret addObject:cred];
    cred.providerName = base::SysUTF8ToNSString(
        cred_values.provider_name.value_or("(Not provided)"));
  }

  block(ret);
}

void FakeSystemInterface::MakeCredential(
    NSWindow* window,
    CtapMakeCredentialRequest request,
    MakeCredentialOptions options,
    base::OnceCallback<void(ASAuthorization*, NSError*)> callback) {
  if (create_callback_) {
    create_callback_.Run(request);
  }

  auto attestation_object_bytes =
      std::move(make_credential_attestation_object_bytes_);
  make_credential_attestation_object_bytes_.reset();
  auto credential_id = std::move(make_credential_credential_id_);
  make_credential_credential_id_.reset();
  auto error_code = std::move(make_credential_error_code_);
  make_credential_error_code_.reset();

  if (error_code) {
    std::move(callback).Run(nullptr,
                            [[NSError alloc] initWithDomain:@"WKErrorDomain"
                                                       code:error_code.value()
                                                   userInfo:nullptr]);
  } else if (!attestation_object_bytes) {
    // Error code 1001 is a arbitrary number that doesn't trigger any special
    // processing.
    std::move(callback).Run(nullptr, [[NSError alloc] initWithDomain:@""
                                                                code:1001
                                                            userInfo:nullptr]);
  } else {
    FakeASAuthorizationPublicKeyCredentialRegistration* result =
        [FakeASAuthorizationPublicKeyCredentialRegistration alloc];
    result.rawAttestationObject = ToNSData(*attestation_object_bytes);
    result.credentialID = ToNSData(*credential_id);
    if (@available(macOS 14.0, *)) {
      if (options.large_blob_support != LargeBlobSupport::kNotRequested) {
        CHECK(options.large_blob_support != LargeBlobSupport::kRequired ||
              large_blob_support_state_ !=
                  LargeBlobSupportState::kNotSupported);
        FakeLargeBlobRegistrationOutput* blob_output =
            [[FakeLargeBlobRegistrationOutput alloc] init];
        blob_output.isSupported = large_blob_support_state_ ==
                                  LargeBlobSupportState::kSupportedAndEnabled;
        result.largeBlob = blob_output;
      }
    }
    FakeASAuthorization* authorization = [FakeASAuthorization alloc];
    authorization.credential = result;

    std::move(callback).Run(authorization, nullptr);
  }
}

void FakeSystemInterface::GetAssertion(
    NSWindow* window,
    CtapGetAssertionRequest request,
    LargeBlobAssertionInputs large_blob_inputs,
    base::OnceCallback<void(ASAuthorization*, NSError*)> callback) {
  if (get_callback_) {
    get_callback_.Run(request);
  }

  if (get_assertion_error_) {
    NSError* error = [[NSError alloc]
        initWithDomain:@""
                  code:get_assertion_error_->first
              userInfo:@{
                NSLocalizedDescriptionKey : base::SysUTF8ToNSString(
                    get_assertion_error_->second.c_str())
              }];
    get_assertion_error_.reset();
    std::move(callback).Run(nullptr, error);
    return;
  }

  if (!get_assertion_authenticator_data_) {
    std::move(callback).Run(nullptr, [[NSError alloc] initWithDomain:@""
                                                                code:1001
                                                            userInfo:nullptr]);
    return;
  }

  FakeASAuthorizationPublicKeyCredentialAssertion* result =
      [FakeASAuthorizationPublicKeyCredentialAssertion alloc];
  result.rawAuthenticatorData = ToNSData(*get_assertion_authenticator_data_);
  result.signature = ToNSData(*get_assertion_signature_);
  result.userID = ToNSData(*get_assertion_user_id_);
  result.credentialID = ToNSData(*get_assertion_credential_id_);

  CHECK(!large_blob_inputs.read || !large_blob_inputs.write);
  if (@available(macOS 14.0, *)) {
    if (large_blob_inputs.read || large_blob_inputs.write) {
      FakeLargeBlobAssertionOutput* large_blob_out =
          [FakeLargeBlobAssertionOutput alloc];

      if (large_blob_inputs.write) {
        large_blob_out.didWrite = large_blob_write_success_;
      }

      if (large_blob_inputs.read && large_blob_read_data_) {
        large_blob_out.readData = ToNSData(*large_blob_read_data_);
      }
      result.largeBlob = large_blob_out;
    }
  }

  large_blob_read_data_.reset();
  get_assertion_authenticator_data_.reset();
  get_assertion_signature_.reset();
  get_assertion_user_id_.reset();
  get_assertion_credential_id_.reset();

  FakeASAuthorization* authorization = [FakeASAuthorization alloc];
  authorization.credential = result;

  std::move(callback).Run(authorization, nullptr);
}

void FakeSystemInterface::Cancel() {
  cancel_count_++;
}

void FakeSystemInterface::set_large_blob_read_data(std::vector<uint8_t> data) {
  large_blob_read_data_ = std::move(data);
}

FakeSystemInterface::~FakeSystemInterface() = default;

}  // namespace device::fido::icloud_keychain
