/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

final class GraphRequestPiggybackManager: _GraphRequestPiggybackManaging {

  private struct AggregatePermissions {
    var granted = Set<Permission>()
    var declined = Set<Permission>()
    var expired = Set<Permission>()
  }

  private enum Keys {
    static let data = "data"
    static let accessToken = "access_token"
    static let accessTokenPath = "oauth/access_token"
    static let grantType = "grant_type"
    static let fields = "fields"
    static let clientId = "client_id"
    static let expiresAt = "expires_at"
    static let dataAccessExpirationTime = "data_access_expiration_time"
    static let permission = "permission"
    static let status = "status"
    static let granted = "granted"
    static let declined = "declined"
    static let expired = "expired"
  }

  enum Values {
    static let tokenRefreshThresholdInSeconds: TimeInterval = 24 * 60 * 60 // one day
    static let tokenRefreshRetryInSeconds: TimeInterval = 60 * 60 // one hour
    static let extendSSOToken = "fb_extend_sso_token"
    static let accessTokenRefreshFields = ""
    static let permissionsPath = "me/permissions"
    static let permissionsRefreshFields = ""
  }

  var lastRefreshTry = Date.distantPast

  var configuredDependencies: ObjectDependencies?

  var defaultDependencies: ObjectDependencies? = .init(
    tokenWallet: AccessToken.self,
    settings: Settings.shared,
    serverConfigurationProvider: _ServerConfigurationManager.shared,
    graphRequestFactory: GraphRequestFactory()
  )

  func addPiggybackRequests(_ connection: GraphRequestConnecting) {
    guard
      let dependencies = try? getDependencies(),
      let appID = dependencies.settings.appID,
      !appID.isEmpty
    else {
      return
    }

    // There's a circular dependency with GraphRequestConnecting and GraphRequestMetadata
    // that prevents the type from being available correctly due to forward declaration.
    // This will go away once everything is in Swift but for now we can just cast it.
    let isSafeForPiggyback = connection.requests.allSatisfy { metaData in
      guard let request = (metaData as? GraphRequestMetadata)?.request else {
        return false
      }

      return isRequestSafeForPiggyback(request)
    }

    if isSafeForPiggyback {
      addRefreshPiggybackIfStale(to: connection)
      addServerConfigurationPiggyback(to: connection)
    }
  }

  func addRefreshPiggyback(
    _ connection: GraphRequestConnecting,
    permissionHandler: GraphRequestCompletion?
  ) {
    guard
      let dependencies = try? getDependencies(),
      let expectedToken = dependencies.tokenWallet.current else { return }

    var permissions = AggregatePermissions(
      granted: expectedToken.permissions,
      declined: expectedToken.declinedPermissions,
      expired: expectedToken.expiredPermissions
    )
    var tokenString: String?
    var expirationDateNumber: NSNumber?
    var dataAccessExpirationDateNumber: NSNumber?
    var expectingCallbacksCount = 2

    let expectingCallbackComplete = {
      expectingCallbacksCount -= 1
      if expectingCallbacksCount == 0 {
        guard let currentToken = dependencies.tokenWallet.current else {
          return
        }

        var expirationDate = currentToken.expirationDate
        if let expirationDateNumber = expirationDateNumber {
          expirationDate = expirationDateNumber.doubleValue > 0
            ? Date(timeIntervalSince1970: expirationDateNumber.doubleValue)
            : .distantFuture
        }

        var dataExpirationDate = currentToken.dataAccessExpirationDate
        if let dataAccessExpirationDateNumber = dataAccessExpirationDateNumber {
          dataExpirationDate = dataAccessExpirationDateNumber.doubleValue > 0
            ? Date(timeIntervalSince1970: dataAccessExpirationDateNumber.doubleValue)
            : .distantFuture
        }

        let granted = permissions.granted.map(\.name)
        let declined = permissions.declined.map(\.name)
        let expired = permissions.expired.map(\.name)

        let refreshedToken = AccessToken(
          tokenString: tokenString ?? currentToken.tokenString,
          permissions: granted,
          declinedPermissions: declined,
          expiredPermissions: expired,
          appID: currentToken.appID,
          userID: currentToken.userID,
          expirationDate: expirationDate,
          refreshDate: Date(),
          dataAccessExpirationDate: dataExpirationDate
        )

        if expectedToken == currentToken {
          dependencies.tokenWallet.current = refreshedToken
        }
      }
    }

    let extensionRequest = dependencies.graphRequestFactory.createGraphRequest(
      withGraphPath: Keys.accessTokenPath,
      parameters: [
        Keys.grantType: Values.extendSSOToken,
        Keys.fields: Values.accessTokenRefreshFields,
        Keys.clientId: expectedToken.appID,
      ],
      flags: .disableErrorRecovery
    )

    connection.add(extensionRequest) { _, potentialResult, _ in
      defer { expectingCallbackComplete() }

      guard let result = potentialResult as? [String: Any] else {
        return
      }

      tokenString = result[Keys.accessToken] as? String
      expirationDateNumber = result[Keys.expiresAt] as? NSNumber
      dataAccessExpirationDateNumber = result[Keys.dataAccessExpirationTime] as? NSNumber
    }

    let permissionsRequest = dependencies.graphRequestFactory.createGraphRequest(
      withGraphPath: Values.permissionsPath,
      parameters: [Keys.fields: Values.permissionsRefreshFields],
      flags: .disableErrorRecovery
    )

    connection.add(permissionsRequest) { [self] innerConnection, potentialResult, potentialError in
      defer {
        expectingCallbackComplete()
        permissionHandler?(innerConnection, potentialResult, potentialError)
      }

      if let result = potentialResult as? [String: Any] {
        let extractedPermissions = extractPermissions(
          fromResponse: result,
          priorPermissions: permissions
        )
        permissions = extractedPermissions
      } else if potentialError != nil {
        return
      } else {
        permissions = AggregatePermissions()
        return
      }
    }
  }

  private func extractPermissions(
    fromResponse response: [String: Any],
    priorPermissions: AggregatePermissions
  ) -> AggregatePermissions {
    guard let resultData = response[Keys.data] as? [[String: Any]] else {
      return priorPermissions
    }

    var permissions = AggregatePermissions()

    resultData.forEach { permissionsDictionary in
      guard
        let name = permissionsDictionary[Keys.permission] as? String,
        let status = permissionsDictionary[Keys.status] as? String
      else {
        return
      }

      let permission = Permission(stringLiteral: name)
      switch status {
      case Keys.granted: permissions.granted.insert(permission)
      case Keys.declined: permissions.declined.insert(permission)
      case Keys.expired: permissions.expired.insert(permission)
      default: break
      }
    }

    return permissions
  }

  func isRequestSafeForPiggyback(_ request: GraphRequestProtocol) -> Bool {
    guard let dependencies = try? getDependencies() else { return false }

    return (request.version == dependencies.settings.graphAPIVersion) && !request.hasAttachments
  }

  func addRefreshPiggybackIfStale(to connection: GraphRequestConnecting) {
    guard let dependencies = try? getDependencies() else { return }

    // don't piggy back more than once an hour as a cheap way of
    // retrying in cases of errors and preventing duplicate refreshes.
    // obviously this is not foolproof but is simple and sufficient.
    let now = Date()

    guard
      let refreshDate = dependencies.tokenWallet.current?.refreshDate,
      now.timeIntervalSince(lastRefreshTry) > Values.tokenRefreshRetryInSeconds,
      now.timeIntervalSince(refreshDate) > Values.tokenRefreshThresholdInSeconds
    else {
      return
    }

    addRefreshPiggyback(connection, permissionHandler: nil)
    lastRefreshTry = Date()
  }

  func addServerConfigurationPiggyback(to connection: GraphRequestConnecting) {
    guard let dependencies = try? getDependencies() else { return }

    let isDefaults = dependencies.serverConfigurationProvider.cachedServerConfiguration().isDefaults

    // This is bad. The pre-conversion implementation resolved to NaN and then checked if
    // NaN was less than FBSDK_SERVER_CONFIGURATION_MANAGER_CACHE_TIMEOUT.
    // This resolved to false. The Swift version also needs to default to false.
    var isCacheTimedOut = false
    if let cachedConfigurationTimeStamp = dependencies.serverConfigurationProvider
      .cachedServerConfiguration()
      .timestamp {
      let timeout = TimeInterval(FBSDK_SERVER_CONFIGURATION_MANAGER_CACHE_TIMEOUT)

      isCacheTimedOut = Date().timeIntervalSince(cachedConfigurationTimeStamp) < timeout
    }

    guard
      isDefaults || !isCacheTimedOut,
      let appID = dependencies.settings.appID,
      let request = dependencies.serverConfigurationProvider.request(toLoadServerConfiguration: appID)
    else { return }

    connection.add(request) { _, potentialResult, error in
      guard let result = potentialResult else { return }

      dependencies.serverConfigurationProvider.processLoadRequestResponse(
        result,
        error: error,
        appID: appID
      )
    }
  }
}

extension GraphRequestPiggybackManager: DependentAsObject {
  struct ObjectDependencies {
    let tokenWallet: _AccessTokenProviding.Type
    let settings: SettingsProtocol
    let serverConfigurationProvider: _ServerConfigurationProviding
    let graphRequestFactory: GraphRequestFactoryProtocol
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation

/**
 Internal Type exposed to facilitate transition to Swift.
 API Subject to change or removal without warning. Do not use.

 @warning INTERNAL - DO NOT USE
 */
@objcMembers
@objc(FBSDKDialogConfigurationMapBuilder)
public final class _DialogConfigurationMapBuilder: NSObject, _DialogConfigurationMapBuilding {

  private enum Keys {
    static let name = "name"
    static let url = "url"
    static let versions = "versions"
  }

  public func buildDialogConfigurationMap(
    from rawConfigurations: [[String: Any]]
  ) -> [String: _DialogConfiguration] {

    var configurations = [String: _DialogConfiguration]()

    rawConfigurations.forEach { configuration in
      guard
        let name = configuration[Keys.name] as? String,
        !name.isEmpty,
        let rawURL = configuration[Keys.url] as? String,
        let url = URL(string: rawURL),
        let appVersions = configuration[Keys.versions] as? [Any],
        appVersions is [String] || appVersions is [Int],
        !appVersions.isEmpty
      else {
        return
      }

      configurations[name] = _DialogConfiguration(name: name, url: url, appVersions: appVersions)
    }

    return configurations
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

final class TokenCache: TokenCaching {

  private enum Keys {
    static let accessTokenUserDefaults = "com.facebook.sdk.v4.FBSDKAccessTokenInformationKey"
    static let accessTokenKeychain = "com.facebook.sdk.v4.FBSDKAccessTokenInformationKeychainKey"
    static let authenticationTokenUserDefaults = "com.facebook.sdk.v9.FBSDKAuthenticationTokenInformationKey"
    static let authenticationTokenKeychain = "com.facebook.sdk.v9.FBSDKAuthenticationTokenInformationKeychainKey"
    static let tokenUUID = "tokenUUID"
    static let encodedToken = "tokenEncoded"
  }

  var configuredDependencies: ObjectDependencies?
  var defaultDependencies: ObjectDependencies?

  var accessToken: AccessToken? {
    get {
      guard let dependencies = try? getDependencies() else {
        return nil
      }

      let uuid = dependencies.dataStore.fb_string(forKey: Keys.accessTokenUserDefaults)
      let keychainStoreRepresentation = dependencies.keychainStore.dictionary(forKey: Keys.accessTokenKeychain)

      if dependencies.settings.shouldUseTokenOptimizations {
        if uuid == nil,
           keychainStoreRepresentation == nil {
          return nil
        }

        if uuid == nil {
          clearAccessTokenCache()
          return nil
        }

        if keychainStoreRepresentation == nil {
          dependencies.dataStore.fb_removeObject(forKey: Keys.accessTokenUserDefaults)
          return nil
        }
      }

      guard
        let keychainStoreRepresentation = keychainStoreRepresentation,
        let tokenUUID = keychainStoreRepresentation[Keys.tokenUUID] as? String,
        // We check if the UUID from the defaults and the keychain match to see if the token has
        // changed or missing which could mean the app was uninstalled.
        tokenUUID == uuid,
        let tokenData = keychainStoreRepresentation[Keys.encodedToken] as? Data,
        let unarchiver = try? createUnarchiver(for: tokenData),
        let unarchivedToken = unarchiver.decodeObject(of: AccessToken.self, forKey: NSKeyedArchiveRootObjectKey)
      else {
        clearAccessTokenCache()
        return nil
      }

      return unarchivedToken
    }

    set {
      guard
        let dependencies = try? getDependencies(),
        let token = newValue
      else { return clearAccessTokenCache() }

      var uuid = dependencies.dataStore.fb_object(forKey: Keys.accessTokenUserDefaults) as? String
      if uuid == nil {
        uuid = UUID().uuidString
        dependencies.dataStore.fb_setObject(
          uuid as Any,
          forKey: Keys.accessTokenUserDefaults
        )
      }

      let tokenData = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: false)
      let keychainRepresentation = [
        Keys.tokenUUID: uuid as Any,
        Keys.encodedToken: tokenData as Any,
      ] as [String: Any]

      dependencies.keychainStore.setDictionary(
        keychainRepresentation,
        forKey: Keys.accessTokenKeychain,
        accessibility: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
      )
    }
  }

  var authenticationToken: AuthenticationToken? {
    get {
      guard let dependencies = try? getDependencies() else { return nil }

      let uuid = dependencies.dataStore.fb_object(forKey: Keys.authenticationTokenUserDefaults) as? String
      let keychainRepresentation = dependencies.keychainStore.dictionary(forKey: Keys.authenticationTokenKeychain)

      if dependencies.settings.shouldUseTokenOptimizations {
        if uuid == nil,
           keychainRepresentation == nil {
          return nil
        }

        if uuid == nil {
          clearAuthenticationTokenCache()
          return nil
        }

        if keychainRepresentation == nil {
          dependencies.dataStore.fb_removeObject(forKey: Keys.authenticationTokenUserDefaults)
          return nil
        }
      }

      guard
        let keychainRepresentation = keychainRepresentation,
        let tokenUUID = keychainRepresentation[Keys.tokenUUID] as? String,
        // We check if the UUID from the defaults and the keychain match to see if the token has
        // changed or missing which could mean the app was uninstalled.
        tokenUUID == uuid
      else {
        clearAuthenticationTokenCache()
        return nil
      }

      guard
        let tokenData = keychainRepresentation[Keys.encodedToken] as? Data,
        let unarchiver = try? createUnarchiver(for: tokenData),
        let unarchivedToken = unarchiver.decodeObject(of: AuthenticationToken.self, forKey: NSKeyedArchiveRootObjectKey)
      else {
        return nil
      }

      return unarchivedToken
    }

    set {
      guard
        let dependencies = try? getDependencies(),
        let token = newValue
      else {
        clearAuthenticationTokenCache()
        return
      }

      var uuid = dependencies.dataStore.fb_object(forKey: Keys.authenticationTokenUserDefaults) as? String

      if uuid == nil {
        uuid = UUID().uuidString
        dependencies.dataStore.fb_setObject(
          uuid as Any,
          forKey: Keys.authenticationTokenUserDefaults
        )
      }

      let tokenData = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: false)

      let keychainRepresentation = [
        Keys.tokenUUID: uuid as Any,
        Keys.encodedToken: tokenData as Any,
      ] as [String: Any]

      dependencies.keychainStore.setDictionary(
        keychainRepresentation,
        forKey: Keys.authenticationTokenKeychain,
        accessibility: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
      )
    }
  }

  private func clearAuthenticationTokenCache() {
    let dependencies = try? getDependencies()
    dependencies?.keychainStore.setDictionary(nil, forKey: Keys.authenticationTokenKeychain, accessibility: nil)
    UserDefaults.standard.removeObject(forKey: Keys.authenticationTokenUserDefaults)
  }

  private func clearAccessTokenCache() {
    let dependencies = try? getDependencies()
    dependencies?.keychainStore.setDictionary(nil, forKey: Keys.accessTokenKeychain, accessibility: nil)
    UserDefaults.standard.removeObject(forKey: Keys.accessTokenUserDefaults)
  }

  private func createUnarchiver(for data: Data) throws -> NSKeyedUnarchiver {
    let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data)
    unarchiver.requiresSecureCoding = true
    return unarchiver
  }
}

extension TokenCache: DependentAsObject {
  struct ObjectDependencies {
    let settings: SettingsProtocol
    let keychainStore: KeychainStoreProtocol
    let dataStore: DataPersisting
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit_Basics
import Foundation

/**
 Internal Type exposed to facilitate transition to Swift.
 API Subject to change or removal without warning. Do not use.

 @warning INTERNAL - DO NOT USE
 */
@objcMembers
@objc(_FBSDKAccessTokenExpirer)
public final class _AccessTokenExpirer: NSObject, _AccessTokenExpiring {
  let notificationCenter: _NotificationPosting & NotificationDelivering
  private var timer: Timer?

  public init(notificationCenter: _NotificationPosting & NotificationDelivering) {
    self.notificationCenter = notificationCenter
    super.init()
    notificationCenter.fb_addObserver(
      self,
      selector: #selector(checkAccessTokenExpirationDate),
      name: .AccessTokenDidChange,
      object: nil
    )
    notificationCenter.fb_addObserver(
      self,
      selector: #selector(checkAccessTokenExpirationDate),
      name: .FBSDKApplicationDidBecomeActive,
      object: nil
    )
    checkAccessTokenExpirationDate()
  }

  deinit {
    timer?.invalidate()
    timer = nil
  }

  func checkAccessTokenExpirationDate() {
    timer?.invalidate()
    timer = nil

    guard let accessToken = AccessToken.current,
          !accessToken.isExpired
    else {
      return
    }

    timer = Timer(
      timeInterval: accessToken.expirationDate.timeIntervalSinceNow,
      target: self,
      selector: #selector(timerDidFire),
      userInfo: nil,
      repeats: false
    )
  }

  func timerDidFire() {
    let accessToken = AccessToken.current
    let userInfo: [String: Any] = [
      AccessTokenChangeNewKey: accessToken as Any,
      AccessTokenChangeOldKey: accessToken as Any,
      AccessTokenDidExpireKey: true,
    ]
    notificationCenter.fb_post(
      name: .AccessTokenDidChange,
      object: AccessToken.self,
      userInfo: userInfo
    )
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

// This is an empty protocol used to mark an abstraction for a type that
// must be retained in order to continue monitoring for the expiration of
// access tokens.

/**
 Internal Type exposed to facilitate transition to Swift.
 API Subject to change or removal without warning. Do not use.

 @warning INTERNAL - DO NOT USE
 */
@objc(_FBSDKAccessTokenExpiring)
public protocol _AccessTokenExpiring {}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation

/**
 Internal Type exposed to facilitate transition to Swift.
 API Subject to change or removal without warning. Do not use.

 @warning INTERNAL - DO NOT USE
 */
@objcMembers
@objc(FBSDKIcon)
open class FBIcon: NSObject {

  open func path(with size: CGSize) -> CGPath? { nil }

  public func image(size: CGSize) -> UIImage? {
    image(size: size, scale: UIScreen.main.scale, color: UIColor.white)
  }

  func image(size: CGSize, scale: CGFloat) -> UIImage? {
    image(size: size, scale: scale, color: UIColor.white)
  }

  public func image(size: CGSize, color: UIColor) -> UIImage? {
    image(size: size, scale: UIScreen.main.scale, color: color)
  }

  public func image(size: CGSize, scale: CGFloat, color: UIColor) -> UIImage? {
    guard
      size.width != 0,
      size.height != 0
    else {
      return nil
    }

    UIGraphicsBeginImageContextWithOptions(size, false, scale)
    guard
      let context = UIGraphicsGetCurrentContext(),
      let path = path(with: size)
    else { return nil }

    context.addPath(path)
    context.setFillColor(color.cgColor)
    context.fillPath()
    let image = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return image
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation

/**
 Internal Type exposed to facilitate transition to Swift.
 API Subject to change or removal without warning. Do not use.

 @warning INTERNAL - DO NOT USE
 */

@objcMembers
@objc(FBSDKCloseIcon)
public final class _FBCloseIcon: NSObject {

  public func image(size: CGSize) -> UIImage? {
    image(size: size, primaryColor: .white, secondaryColor: .black, scale: UIScreen.main.scale)
  }

  func image(size: CGSize, primaryColor: UIColor, secondaryColor: UIColor, scale: CGFloat) -> UIImage? {
    UIGraphicsBeginImageContextWithOptions(size, false, scale)
    guard let context = UIGraphicsGetCurrentContext() else { return nil }

    let iconSize = min(size.width, size.height)

    var rect = CGRect(
      x: (size.width - iconSize) / 2,
      y: (size.height - iconSize) / 2,
      width: iconSize,
      height: iconSize
    )

    let step = iconSize / 12

    // shadow
    rect = rect.insetBy(dx: step, dy: step).integral
    let colors = [
      UIColor(white: 0, alpha: 0.7).cgColor,
      UIColor(white: 0, alpha: 0.3).cgColor,
      UIColor(white: 0, alpha: 0.1).cgColor,
      UIColor(white: 0, alpha: 0.0).cgColor,
    ]

    let locations: [CGFloat] = [
      0.70,
      0.80,
      0.90,
      1.0,
    ]
    let colorSpace = CGColorSpaceCreateDeviceGray()
    guard let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: locations) else {
      return nil
    }
    let center = CGPoint(x: rect.midX - step / 6, y: rect.midY + step / 4)
    context.drawRadialGradient(
      gradient,
      startCenter: center,
      startRadius: 0,
      endCenter: center,
      endRadius: (rect.width - step / 2) / 2,
      options: []
    )

    // outer circle
    rect = rect.insetBy(dx: step, dy: step).integral
    primaryColor.setFill()
    context.fillEllipse(in: rect)

    // inner circle
    rect = rect.insetBy(dx: step, dy: step).integral
    secondaryColor.setFill()
    context.fillEllipse(in: rect)

    // cross
    rect = rect.insetBy(dx: step, dy: step).integral
    let lineWidth = step * 5 / 4
    rect.origin.y = rect.midY - lineWidth / 2
    rect.size.height = lineWidth
    primaryColor.setFill()

    context.translateBy(x: size.width / 2, y: size.height / 2)
    context.rotate(by: .pi / 4)
    context.translateBy(x: -size.width / 2, y: -size.height / 2)
    context.fill(rect)
    context.translateBy(x: size.width / 2, y: size.height / 2)
    context.rotate(by: .pi / 2)
    context.translateBy(x: -size.width / 2, y: -size.height / 2)
    context.fill(rect)

    let image = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return image
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation

/**
 Internal Type exposed to facilitate transition to Swift.
 API Subject to change or removal without warning. Do not use.

 @warning INTERNAL - DO NOT USE
 */
@objcMembers
@objc(FBSDKLogo)
public final class _FBLogo: FBIcon {
  public override func path(with size: CGSize) -> CGPath? {
    let originalCanvasWidth: CGFloat = 1366
    let originalCanvasHeight: CGFloat = 1366

    let transformValue = CGAffineTransform(
      scaleX: size.width / originalCanvasWidth,
      y: size.height / originalCanvasHeight
    )

    let path = UIBezierPath()
    path.move(to: CGPoint(x: 1365.33, y: 682.67))
    path.addCurve(
      to: CGPoint(x: 682.67, y: -0),
      controlPoint1: CGPoint(x: 1365.33, y: 305.64),
      controlPoint2: CGPoint(x: 1059.69, y: -0)
    )
    path.addCurve(
      to: CGPoint(x: 0, y: 682.67),
      controlPoint1: CGPoint(x: 305.64, y: -0),
      controlPoint2: CGPoint(x: 0, y: 305.64)
    )
    path.addCurve(
      to: CGPoint(x: 576, y: 1357.04),
      controlPoint1: CGPoint(x: 0, y: 1023.41),
      controlPoint2: CGPoint(x: 249.64, y: 1305.83)
    )

    path.addLine(to: CGPoint(x: 576, y: 880))
    path.addLine(to: CGPoint(x: 402.67, y: 880))
    path.addLine(to: CGPoint(x: 402.67, y: 682.67))
    path.addLine(to: CGPoint(x: 576, y: 682.67))
    path.addLine(to: CGPoint(x: 576, y: 532.27))

    path.addCurve(
      to: CGPoint(x: 833.85, y: 266.67),
      controlPoint1: CGPoint(x: 576, y: 361.17),
      controlPoint2: CGPoint(x: 677.92, y: 266.67)
    )
    path.addCurve(
      to: CGPoint(x: 986.67, y: 280),
      controlPoint1: CGPoint(x: 908.54, y: 266.67),
      controlPoint2: CGPoint(x: 986.67, y: 280)
    )

    path.addLine(to: CGPoint(x: 986.67, y: 448))
    path.addLine(to: CGPoint(x: 900.58, y: 448))

    path.addCurve(
      to: CGPoint(x: 789.33, y: 554.61),
      controlPoint1: CGPoint(x: 815.78, y: 448),
      controlPoint2: CGPoint(x: 789.33, y: 500.62)
    )

    path.addLine(to: CGPoint(x: 789.33, y: 682.67))
    path.addLine(to: CGPoint(x: 978.67, y: 682.67))
    path.addLine(to: CGPoint(x: 948.4, y: 880))
    path.addLine(to: CGPoint(x: 789.33, y: 880))
    path.addLine(to: CGPoint(x: 789.33, y: 1357.04))

    path.addCurve(
      to: CGPoint(x: 1365.33, y: 682.67),
      controlPoint1: CGPoint(x: 1115.69, y: 1305.83),
      controlPoint2: CGPoint(x: 1365.33, y: 1023.41)
    )

    path.close()
    path.apply(transformValue)
    return path.cgPath
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation

/**
 Internal Type exposed to facilitate transition to Swift.
 API Subject to change or removal without warning. Do not use.

 @warning INTERNAL - DO NOT USE
 */
@objcMembers
@objc(FBSDKHumanSilhouetteIcon)
public final class _HumanSilhouetteIcon: FBIcon {
  public override func path(with size: CGSize) -> CGPath? {
    let transform = CGAffineTransform(scaleX: size.width / 158.783, y: size.height / 158.783)

    let path = CGMutablePath()
    path.move(to: CGPoint(x: 158.783, y: 158.783), transform: transform)

    path.addCurve(
      to: CGPoint(x: 105.607, y: 117.32),
      control1: CGPoint(x: 156.39, y: 131.441),
      control2: CGPoint(x: 144.912, y: 136.964),
      transform: transform
    )

    path.addCurve(
      to: CGPoint(x: 103.013, y: 107.4781),
      control1: CGPoint(x: 103.811, y: 113.941),
      control2: CGPoint(x: 103.348, y: 108.8965),
      transform: transform
    )

    path.addLine(to: CGPoint(x: 100.434, y: 106.7803), transform: transform)

    path.addCurve(
      to: CGPoint(x: 106.006, y: 75.2188),
      control1: CGPoint(x: 97.2363, y: 82.7701),
      control2: CGPoint(x: 100.67, y: 101.5845),
      transform: transform
    )

    path.addCurve(
      to: CGPoint(x: 108.971, y: 66.5743),
      control1: CGPoint(x: 107.949, y: 76.2959),
      control2: CGPoint(x: 108.268, y: 70.7417),
      transform: transform
    )

    path.addCurve(
      to: CGPoint(x: 107.139, y: 58.9082),
      control1: CGPoint(x: 109.673, y: 62.4068),
      control2: CGPoint(x: 110.864, y: 58.9082),
      transform: transform
    )

    path.addCurve(
      to: CGPoint(x: 101.335, y: 23.3072),
      control1: CGPoint(x: 107.94, y: 42.7652),
      control2: CGPoint(x: 110.299, y: 31.3848),
      transform: transform
    )

    path.addCurve(
      to: CGPoint(x: 95.0483, y: 9.6036128),
      control1: CGPoint(x: 92.3808, y: 15.23781),
      control2: CGPoint(x: 87.874, y: 15.52349),
      transform: transform
    )

    path.addCurve(
      to: CGPoint(x: 57.4487, y: 23.3072),
      control1: CGPoint(x: 91.2319, y: 8.892613),
      control2: CGPoint(x: 70.2036, y: 12.01861),
      transform: transform
    )

    path.addCurve(
      to: CGPoint(x: 51.6445, y: 58.9082),
      control1: CGPoint(x: 48.4121, y: 31.3042),
      control2: CGPoint(x: 50.8437, y: 42.7652),
      transform: transform
    )

    path.addCurve(
      to: CGPoint(x: 49.813, y: 66.5743),
      control1: CGPoint(x: 47.9194, y: 58.9082),
      control2: CGPoint(x: 49.1108, y: 62.4068),
      transform: transform
    )

    path.addCurve(
      to: CGPoint(x: 52.7778, y: 75.2188),
      control1: CGPoint(x: 50.5156, y: 70.7417),
      control2: CGPoint(x: 50.8349, y: 76.2959),
      transform: transform
    )

    path.addCurve(
      to: CGPoint(x: 58.3501, y: 106.7803),
      control1: CGPoint(x: 58.1138, y: 110.1135),
      control2: CGPoint(x: 61.5478, y: 82.7701),
      transform: transform
    )

    path.addLine(to: CGPoint(x: 55.7705, y: 107.4781), transform: transform)

    path.addCurve(
      to: CGPoint(x: 53.1767, y: 117.32),
      control1: CGPoint(x: 55.4355, y: 108.8965),
      control2: CGPoint(x: 54.9722, y: 113.941),
      transform: transform
    )

    path.addCurve(
      to: CGPoint(x: 0.0, y: 158.783),
      control1: CGPoint(x: 13.8711, y: 136.964),
      control2: CGPoint(x: 2.3945, y: 131.441),
      transform: transform
    )

    path.addLine(to: CGPoint(x: 158.783, y: 158.783), transform: transform)

    return path
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import FBSDKCoreKit_Basics
import UIKit

/**
 Internal Type exposed to facilitate transition to Swift.
 API Subject to change or removal without warning. Do not use.

 @warning INTERNAL - DO NOT USE
 */
@objcMembers
@objc(FBSDKViewImpressionLogger)
public final class _ViewImpressionLogger: NSObject, ImpressionLogging {
  static var impressionTrackers = [AppEvents.Name: Any]()
  let eventName: AppEvents.Name
  var trackedImpressions = Set<[String: String]>()

  @objc(initWithEventName:)
  public init(eventName: AppEvents.Name) {
    self.eventName = eventName

    super.init()

    guard let notificationDeliverer = try? Self.getDependencies().notificationDeliverer else {
      return
    }

    notificationDeliverer.fb_addObserver(
      self,
      selector: #selector(applicationDidEnterBackground(_:)),
      name: UIApplication.didEnterBackgroundNotification,
      object: UIApplication.shared
    )
  }

  public static func retrieveLogger(with eventName: AppEvents.Name) -> _ViewImpressionLogger {
    if let impressionLogger = Self.impressionTrackers[eventName] as? _ViewImpressionLogger {
      return impressionLogger
    }
    let impressionLogger = _ViewImpressionLogger(eventName: eventName)
    Self.impressionTrackers[eventName] = impressionLogger
    return impressionLogger
  }

  func applicationDidEnterBackground(_ notification: Notification) {
    trackedImpressions.removeAll()
  }

  public func logImpression(withIdentifier identifier: String, parameters: [AppEvents.ParameterName: Any]?) {
    guard let dependencies = try? Self.getDependencies() else {
      return
    }

    var keys = [String: String]()
    keys["__view_impression_identifier__"] = identifier

    if let parameters = parameters as? [AppEvents.ParameterName: String] {
      parameters.forEach { key, value in keys[key.rawValue] = value }
    }

    // Ensure that each impression is only tracked once
    if trackedImpressions.contains(keys) {
      return
    }

    trackedImpressions.insert(keys)
    dependencies.eventLogger.logInternalEvent(
      eventName,
      parameters: parameters,
      isImplicitlyLogged: true,
      accessToken: dependencies.tokenWallet.current
    )
  }
}

extension _ViewImpressionLogger: DependentAsType {
  struct TypeDependencies {
    var graphRequestFactory: GraphRequestFactoryProtocol
    var eventLogger: EventLogging
    var notificationDeliverer: NotificationDelivering
    var tokenWallet: _AccessTokenProviding.Type
  }

  static var configuredDependencies: TypeDependencies?

  static var defaultDependencies: TypeDependencies? = TypeDependencies(
    graphRequestFactory: GraphRequestFactory(),
    eventLogger: AppEvents.shared,
    notificationDeliverer: NotificationCenter.default,
    tokenWallet: AccessToken.self
  )
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation

/**
 Internal Type exposed to facilitate transition to Swift.
 API Subject to change or removal without warning. Do not use.

 @warning INTERNAL - DO NOT USE
 */

@objcMembers
@objc(FBSDKWebDialog)
public final class _WebDialog: NSObject {
  public var shouldDeferVisibility = false
  public weak var delegate: WebDialogDelegate?
  var name: String
  var webViewFrame: CGRect
  var parameters: [String: String]?
  var backgroundView: UIView?
  var dialogView: FBWebDialogView?
  var path: String?

  private enum AnimationDuration {
    static let show = 0.2
    static let dismiss = 0.3
  }

  private enum URLParameterKeys {
    static let display = "display"
    static let sdk = "sdk"
    static let redirectURI = "redirect_uri"
    static let appID = "app_id"
    static let accessToken = "access_token"
  }

  private enum URLParameterValues {
    static let touch = "touch"
    static let sdkVersion = "ios-\(Settings.shared.sdkVersion)"
    static let success = "fbconnect://success"
  }

  public init(
    name: String,
    parameters: [String: String]?,
    webViewFrame: CGRect = .zero,
    path: String? = nil
  ) {
    self.name = name
    self.parameters = parameters
    self.webViewFrame = webViewFrame
    self.path = path
  }

  public convenience init(name: String) {
    self.init(name: name, parameters: nil, webViewFrame: .zero)
  }

  public func show() {
    do {
      let url = try generateURL()
      guard (try? Self.getDependencies().windowFinder.findWindow()) != nil else {
        _Logger.singleShotLogEntry(
          .developerErrors,
          logEntry: "There are no valid windows in which to present this web dialog"
        )
        let error = try Self.getDependencies().errorFactory.unknownError(
          message: "There are no valid windows in which to present this web dialog"
        )
        fail(with: error)
        return
      }

      let frame = webViewFrame.isEmpty ? applicationFrameForOrientation() : webViewFrame
      dialogView = FBWebDialogView(frame: frame)
      dialogView?.delegate = self
      dialogView?.load(url)

      if !shouldDeferVisibility {
        showWebView()
      }
    } catch {
      fail(with: error)
    }
  }

  func addObservers() {
    NotificationCenter.default.addObserver(
      self,
      selector: #selector(deviceOrientationDidChangeNotification(_:)),
      name: UIDevice.orientationDidChangeNotification,
      object: nil
    )
  }

  func deviceOrientationDidChangeNotification(_ notification: Notification) {
    if let animated = notification.userInfo?["UIDeviceOrientationRotateAnimatedUserInfoKey"] as? Bool {
      let animationDuration = animated ? CATransaction.animationDuration() : 0
      updateView(scale: 1.0, alpha: 1.0, animationDuration: animationDuration) { finished in
        if finished {
          self.dialogView?.setNeedsLayout()
        }
      }
    }
  }

  func removeObservers() {
    NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
  }

  func cancel() {
    delegate?.webDialogDidCancel(self)
    dismiss(animated: true)
  }

  func complete(with results: [String: Any]) {
    delegate?.webDialog(self, didCompleteWithResults: results)
    dismiss(animated: true)
  }

  func dismiss(animated: Bool) {
    removeObservers()

    let didDismiss: (Bool) -> Void = { _ in
      self.backgroundView?.removeFromSuperview()
      self.dialogView?.removeFromSuperview()
    }

    if animated {
      UIView.animate(
        withDuration: 0.3,
        animations: {
          self.dialogView?.alpha = 0.0
          self.backgroundView?.alpha = 0.0
        },
        completion: didDismiss
      )
    } else {
      didDismiss(true)
    }
  }

  func fail(with error: Error) {
    delegate?.webDialog(self, didFailWithError: error)
    dismiss(animated: true)
  }

  func generateURL() throws -> URL {
    var urlParameters = [String: String]()
    urlParameters[URLParameterKeys.display] = URLParameterValues.touch
    urlParameters[URLParameterKeys.sdk] = URLParameterValues.sdkVersion
    urlParameters[URLParameterKeys.redirectURI] = URLParameterValues.success
    urlParameters[URLParameterKeys.appID] = Settings.shared.appID
    urlParameters[URLParameterKeys.accessToken] = AccessToken.current?.tokenString

    if let parameters = parameters {
      urlParameters = parameters.merging(urlParameters) { _, last in last }
    }
    return try InternalUtility.shared.facebookURL(
      hostPrefix: "m",
      path: path ?? "/dialog/\(name)",
      queryParameters: urlParameters
    )
  }

  func showWebView() {
    guard let window = try? Self.getDependencies().windowFinder.findWindow() else {
      let message = "There are no valid windows in which to present this web dialog"
      _Logger.singleShotLogEntry(
        LoggingBehavior.developerErrors, logEntry: message
      )
      if let error = try? Self.getDependencies().errorFactory.unknownError(
        message: message
      ) {
        fail(with: error)
      }
      return
    }

    addObservers()
    backgroundView = UIView(frame: window.bounds)
    backgroundView?.alpha = 0
    backgroundView?.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    backgroundView?.backgroundColor = UIColor(white: 0.3, alpha: 0.8)

    guard let dialogView = dialogView,
          let backgroundView = backgroundView
    else {
      let message = "dialog or background view has not been created or set"
      _Logger.singleShotLogEntry(
        LoggingBehavior.developerErrors, logEntry: message
      )
      if let error = try? Self.getDependencies().errorFactory.unknownError(
        message: message
      ) {
        fail(with: error)
      }
      return
    }

    window.addSubview(backgroundView)
    window.addSubview(dialogView)
    dialogView.becomeFirstResponder()

    updateView(scale: 0.001, alpha: 0.0, animationDuration: 0.0)
    updateView(scale: 1.1, alpha: 1.0, animationDuration: AnimationDuration.show) { _ in
      self.updateView(scale: 0.9, alpha: 1.0, animationDuration: AnimationDuration.show) { _ in
        self.updateView(scale: 1.0, alpha: 1.0, animationDuration: AnimationDuration.show)
      }
    }
  }

  func applicationFrameForOrientation() -> CGRect {
    var applicationFrame = dialogView?.window?.screen.bounds
    guard var insets = dialogView?.window?.safeAreaInsets else {
      return .zero
    }

    if insets.top == 0 {
      insets.top = UIApplication.shared.statusBarFrame.size.height
    }

    applicationFrame?.origin.x += insets.left
    applicationFrame?.origin.y += insets.top
    applicationFrame?.size.width -= insets.left + insets.right
    applicationFrame?.size.height -= insets.top + insets.bottom
    return applicationFrame ?? .zero
  }

  func updateView(
    scale: CGFloat,
    alpha: CGFloat,
    animationDuration: TimeInterval,
    completion: ((Bool) -> Void)? = nil
  ) {
    let transform = dialogView?.transform
    let applicationFrame = webViewFrame.isEmpty ? applicationFrameForOrientation() : webViewFrame
    if scale == 1 {
      dialogView?.transform = .identity
      dialogView?.frame = applicationFrame
      dialogView?.transform = transform ?? .identity
    }

    let updateBlock = { [self] in
      dialogView?.transform = transform ?? .identity
      dialogView?.center = CGPoint(x: applicationFrame.midX, y: applicationFrame.midY)
      dialogView?.alpha = alpha
      backgroundView?.alpha = alpha
    }

    if animationDuration == 0 {
      updateBlock()
    } else {
      UIView.animate(withDuration: animationDuration, animations: updateBlock, completion: completion)
    }
  }
}

// MARK: - WebDialogViewDelegate

extension _WebDialog: WebDialogViewDelegate {

  public func webDialogView(_ webDialogView: FBWebDialogView, didCompleteWithResults results: [String: Any]) {
    complete(with: results)
  }

  public func webDialogView(_ webDialogView: FBWebDialogView, didFailWithError error: Error) {
    fail(with: error)
  }

  public func webDialogViewDidCancel(_ webDialogView: FBWebDialogView) {
    cancel()
  }

  public func webDialogViewDidFinishLoad(_ webDialogView: FBWebDialogView) {
    if shouldDeferVisibility {
      DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
        if self.dialogView != nil {
          self.showWebView()
        }
      }
    }
  }
}

extension _WebDialog: DependentAsType {
  struct TypeDependencies {
    var errorFactory: ErrorCreating
    var windowFinder: _WindowFinding
  }

  static var configuredDependencies: TypeDependencies?

  static var defaultDependencies: TypeDependencies? = TypeDependencies(
    errorFactory: _ErrorFactory(),
    windowFinder: InternalUtility.shared
  )
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation

/**
 Internal Type exposed to facilitate transition to Swift.
 API Subject to change or removal without warning. Do not use.

 @warning INTERNAL - DO NOT USE
 */

@objcMembers
@objc(FBSDKWebViewFactory)
public final class _WebViewFactory: NSObject, _WebViewProviding {
  public func createWebView(frame: CGRect) -> WebView {
    WKWebView(frame: frame)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

@testable import FBSDKCoreKit

import TestTools
import XCTest

final class AccessTokenTests: XCTestCase {

  // swiftlint:disable implicitly_unwrapped_optional
  var tokenCache: TestTokenCache!
  var connection: TestGraphRequestConnection!
  var graphRequestConnectionFactory: TestGraphRequestConnectionFactory!
  var graphRequestPiggybackManager: TestGraphRequestPiggybackManager!
  // swiftlint:enable implicitly_unwrapped_optional

  override func setUp() {
    super.setUp()

    AccessToken.resetClassDependencies()

    tokenCache = TestTokenCache()
    connection = TestGraphRequestConnection()
    graphRequestConnectionFactory = TestGraphRequestConnectionFactory(stubbedConnection: connection)
    graphRequestPiggybackManager = TestGraphRequestPiggybackManager()

    AccessToken.configure(
      tokenCache: tokenCache,
      graphRequestConnectionFactory: graphRequestConnectionFactory,
      graphRequestPiggybackManager: graphRequestPiggybackManager,
      errorFactory: TestErrorFactory()
    )
  }

  override func tearDown() {
    super.tearDown()

    AccessToken.current = nil
    AccessToken.resetClassDependencies()
  }

  func testDefaultClassDependencies() {
    AccessToken.resetClassDependencies()

    XCTAssertNil(
      AccessToken.tokenCache,
      "Should not have a token cache by default"
    )
    XCTAssertNil(
      AccessToken.graphRequestConnectionFactory,
      "Should not have a graph request connection factory by default"
    )
    XCTAssertNil(
      AccessToken.graphRequestPiggybackManager,
      "Should not have a graph request piggyback manager by default"
    )
  }

  func testSettingTokenCache() {
    let cache = TestTokenCache(accessToken: nil, authenticationToken: nil)
    AccessToken.tokenCache = cache
    XCTAssertTrue(AccessToken.tokenCache === cache, "Access token cache should be settable")
  }

  func testRetrievingCurrentToken() {
    AccessToken.current = SampleAccessTokens.validToken

    XCTAssertTrue(
      tokenCache.accessToken === SampleAccessTokens.validToken,
      "Setting the global access token should invoke the cache"
    )
  }

  func testRefreshCurrentAccessTokenWithNilToken() {
    AccessToken.current = nil
    AccessToken.refreshCurrentAccessToken(completion: nil)
    XCTAssertEqual(connection.startCallCount, 0, "Should not start connection if no current access token available")
  }

  func testRefreshingNilTokenError() throws {
    AccessToken.current = nil

    var connection: GraphRequestConnecting?
    var data: Any?
    var error: Error?
    AccessToken.refreshCurrentAccessToken { potentialConnection, potentialData, potentialError in
      connection = potentialConnection
      data = potentialData
      error = potentialError
    }
    XCTAssertNil(
      connection,
      "Shouldn't create a connection is there is no token to refresh"
    )
    XCTAssertNil(
      data,
      "Should not call back with data if there is no token to refresh"
    )

    let sdkError = try XCTUnwrap(
      error as? TestSDKError,
      "Should error when attempting to refresh a nil access token"
    )
    XCTAssertEqual(
      sdkError.code,
      CoreError.errorAccessTokenRequired.rawValue,
      "Should return an error with an access token required code"
    )
    XCTAssertEqual(
      sdkError.message,
      "No current access token to refresh",
      "Should return an error with an appropriate message"
    )
  }

  func testRefreshCurrentAccessTokenWithNonNilToken() {
    AccessToken.current = SampleAccessTokens.validToken
    AccessToken.refreshCurrentAccessToken(completion: nil)
    XCTAssertEqual(connection.startCallCount, 1, "Should start one connection for refreshing")

    XCTAssertTrue(
      graphRequestPiggybackManager.addRefreshPiggybackWasCalled,
      "Refreshing an access token should add a refresh piggyback"
    )
  }

  func testIsDataAccessExpired() {
    var token = SampleAccessTokens.create(dataAccessExpirationDate: .distantPast)
    XCTAssertTrue(
      token.isDataAccessExpired,
      "A token should have a convenience method for determining if data access is expired"
    )
    token = SampleAccessTokens.create(dataAccessExpirationDate: .distantFuture)
    XCTAssertFalse(
      token.isDataAccessExpired,
      "A token should have a convenience method for determining if data access is unexpired"
    )
  }

  func testSecureCoding() {
    XCTAssertTrue(
      AccessToken.supportsSecureCoding,
      "Access tokens should support secure coding"
    )
  }

  func testEncodingAndDecoding() throws {
    // Encode and Decode
    let token = SampleAccessTokens.validToken
    let decodedToken = try XCTUnwrap(CodabilityTesting.encodeAndDecode(token))

    // Test Objects
    XCTAssertEqual(decodedToken, token, .isCodable)
    XCTAssertNotIdentical(decodedToken, token, .isCodable)

    // Test Properties
    XCTAssertEqual(decodedToken.tokenString, token.tokenString, .isCodable)
    XCTAssertEqual(decodedToken.appID, token.appID, .isCodable)
    XCTAssertEqual(decodedToken.appID, token.appID, .isCodable)
    XCTAssertEqual(decodedToken.declinedPermissions, token.declinedPermissions, .isCodable)
    XCTAssertEqual(decodedToken.expiredPermissions, token.expiredPermissions, .isCodable)
    XCTAssertEqual(decodedToken.permissions, token.permissions, .isCodable)
    XCTAssertEqual(decodedToken.tokenString, token.tokenString, .isCodable)
    XCTAssertEqual(decodedToken.userID, token.userID, .isCodable)
    XCTAssertEqual(decodedToken.expirationDate, token.expirationDate, .isCodable)
    XCTAssertEqual(decodedToken.refreshDate, token.refreshDate, .isCodable)
    XCTAssertEqual(decodedToken.dataAccessExpirationDate, token.dataAccessExpirationDate, .isCodable)
  }

  func testEquatability() {
    let token1 = SampleAccessTokens.create(withRefreshDate: .distantPast)
    let token2 = SampleAccessTokens.create(withRefreshDate: .distantFuture)
    XCTAssertNotEqual(
      token1,
      token2,
      "Tokens with different values should not be considered equal"
    )
    let token3 = SampleAccessTokens.create(withRefreshDate: .distantPast)
    let token4 = SampleAccessTokens.create(withRefreshDate: .distantPast)
    XCTAssertEqual(
      token3,
      token4,
      "Tokens with the same values should be considered equal"
    )
  }

  func testHashability() {
    let token = SampleAccessTokens.create(withRefreshDate: .distantPast)
    let token2 = SampleAccessTokens.create(withRefreshDate: .distantPast)
    XCTAssertEqual(
      token.hash,
      token2.hash,
      "Token hash values should be predictable and based on the token's properties"
    )
    let token3 = SampleAccessTokens.create(withRefreshDate: .distantFuture)
    XCTAssertNotEqual(
      token.hash,
      token3.hash,
      "Token hash values should be predictable and based on the token's properties"
    )
  }

  func testGrantedPermissions() {
    let token = SampleAccessTokens.create(withPermissions: [name])
    XCTAssertTrue(token.hasGranted(Permission(stringLiteral: name)))
  }
}

// swiftformat:disable extensionaccesscontrol

// MARK: - Assumptions

fileprivate extension String {
  static let isCodable = "AccessToken should be encodable and decodable"
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

@testable import FBSDKCoreKit

import TestTools
import XCTest

final class ApplicationDelegateTests: XCTestCase {

  // swiftlint:disable implicitly_unwrapped_optional
  var notificationCenter: TestNotificationCenter!
  var featureChecker: TestFeatureManager!
  var appEvents: TestAppEvents!
  var userDataStore: UserDefaultsSpy!
  var observer: TestApplicationDelegateObserver!
  var settings: TestSettings!
  var aemManager: TestAEMManager!
  var backgroundEventLogger: TestBackgroundEventLogger!
  var serverConfigurationProvider: TestServerConfigurationProvider!
  let bitmaskKey = "com.facebook.sdk.kits.bitmask"
  var paymentObserver: TestPaymentObserver!
  var profile: Profile!
  var components: CoreKitComponents!
  var configurator: TestCoreKitConfigurator!
  var delegate: ApplicationDelegate! // swiftlint:disable:this weak_delegate
  // swiftlint:enable implicitly_unwrapped_optional

  override func setUp() {
    super.setUp()

    resetTestDependencies()

    notificationCenter = TestNotificationCenter()
    featureChecker = TestFeatureManager()
    appEvents = TestAppEvents()
    userDataStore = UserDefaultsSpy()
    observer = TestApplicationDelegateObserver()
    settings = TestSettings()
    aemManager = TestAEMManager()

    TestBackgroundEventLogger.setDependencies(
      .init(
        infoDictionaryProvider: TestBundle(),
        eventLogger: TestAppEvents()
      )
    )
    backgroundEventLogger = TestBackgroundEventLogger()
    serverConfigurationProvider = TestServerConfigurationProvider()
    paymentObserver = TestPaymentObserver()
    profile = Profile(
      userID: name,
      firstName: nil,
      middleName: nil,
      lastName: nil,
      name: nil,
      linkURL: nil,
      refreshDate: nil
    )
    components = TestCoreKitComponents.makeComponents(
      appEvents: appEvents,
      backgroundEventLogger: backgroundEventLogger,
      defaultDataStore: userDataStore,
      featureChecker: featureChecker,
      notificationCenter: notificationCenter,
      paymentObserver: paymentObserver,
      serverConfigurationProvider: serverConfigurationProvider,
      settings: settings,
      aemManager: aemManager
    )
    configurator = TestCoreKitConfigurator(components: components)

    makeDelegate()
    delegate.resetApplicationObserverCache()
  }

  override func tearDown() {
    notificationCenter = nil
    featureChecker = nil
    appEvents = nil
    userDataStore = nil
    observer = nil
    settings = nil
    backgroundEventLogger = nil
    serverConfigurationProvider = nil
    paymentObserver = nil
    profile = nil
    components = nil
    delegate = nil

    resetTestDependencies()

    super.tearDown()
  }

  func makeDelegate(usesTestConfigurator: Bool = true) {
    let configurator: CoreKitConfiguring = usesTestConfigurator
      ? configurator
      : CoreKitConfigurator(components: components)

    delegate = ApplicationDelegate(
      components: components,
      configurator: configurator
    )
  }

  func resetTestDependencies() {
    ApplicationDelegate.shared.resetHasInitializeBeenCalled()
    TestAccessTokenWallet.reset()
    TestAuthenticationTokenWallet.reset()
    TestGateKeeperManager.reset()
    TestProfileProvider.reset()
  }

  func testDefaultComponentsAndConfiguration() throws {
    delegate = ApplicationDelegate()

    XCTAssertIdentical(
      delegate.components as AnyObject,
      CoreKitComponents.default as AnyObject,
      "An application delegate should be created with the default components by default"
    )

    let configurator = try XCTUnwrap(
      delegate.configurator as? CoreKitConfigurator,
      "An application delegate should be created with a concrete configurator by default"
    )
    XCTAssertIdentical(
      configurator.components as AnyObject,
      CoreKitComponents.default as AnyObject,
      "The configurator should be created with the default components by default"
    )

    XCTAssertEqual(
      delegate.applicationObservers.count,
      0,
      "The delegate should have an empty hash table of application observers"
    )
  }

  func testComponentsAndConfiguration() {
    let components = TestCoreKitComponents.makeComponents()
    let configurator = TestCoreKitConfigurator(components: components)
    delegate = ApplicationDelegate(components: components, configurator: configurator)

    XCTAssertIdentical(
      delegate.components as AnyObject,
      components as AnyObject,
      "An application delegate should be created with the provided components"
    )
    XCTAssertIdentical(
      delegate.configurator as AnyObject,
      configurator,
      "An application delegate should be created with the provided configurator"
    )
    XCTAssertEqual(
      delegate.applicationObservers.count,
      0,
      "The delegate should have an empty hash table of application observers"
    )
  }

  // MARK: - Initializing SDK

  func testInitializingSdkAddsBridgeApiObserver() {
    delegate.initializeSDK()

    XCTAssertTrue(
      delegate.applicationObservers.contains(_BridgeAPI.shared),
      "Should add the shared bridge api instance to the application observers"
    )
  }

  func testInitializingSdkPerformsSettingsLogging() {
    delegate.initializeSDK()
    XCTAssertEqual(
      settings.logWarningsCallCount,
      1,
      "Should have settings log warnings upon initialization"
    )
    XCTAssertEqual(
      settings.logIfSDKSettingsChangedCallCount,
      1,
      "Should have settings log if there were changes upon initialization"
    )
    XCTAssertEqual(
      settings.recordInstallCallCount,
      1,
      "Should have settings record installations upon initialization"
    )
  }

  func testInitializingSdkPerformsBackgroundEventLogging() {
    delegate.initializeSDK()
    XCTAssertEqual(
      backgroundEventLogger.logBackgroundRefresStatusCallCount,
      1,
      "Should have background event logger log background refresh status upon initialization"
    )
  }

  func testInitializingSdkChecksInstrumentFeature() {
    delegate.initializeSDK()
    XCTAssert(
      featureChecker.capturedFeaturesContains(.instrument),
      "Should check if the instrument feature is enabled on initialization"
    )
  }

  func testInitializingSDKPerformsDownstreamConfigurations() {
    delegate.initializeSDK()
    XCTAssertTrue(
      configurator.performConfigurationCalled,
      "Initializing the SDK should ask the configurator to perform downstream configuration"
    )
  }

  func testInitializingAutoSetupWithFeatureEnabled() {
    featureChecker.enable(feature: .aemAutoSetup)
    delegate.initializeSDK()
    XCTAssert(
      aemManager.enabled,
      "Initializing the SDK should have AEM Auto Setup feature enabled with feature enabled"
    )
  }

  func testInitializingAutoSetupWithoutFeatureEnabled() {
    featureChecker.disableFeature(.aemAutoSetup)
    delegate.initializeSDK()
    XCTAssert(
      !aemManager.enabled,
      "Initializing the SDK should not have AEM Auto Setup feature enabled without feature enabled"
    )
  }

  func testDidFinishLaunchingLaunchedApp() {
    delegate.isAppLaunched = true

    XCTAssertFalse(
      delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil),
      "Should return false if the application is already launched"
    )
  }

  func testDidFinishLaunchingSetsCurrentAccessTokenWithCache() {
    let expected = SampleAccessTokens.validToken
    let cache = TestTokenCache(
      accessToken: expected,
      authenticationToken: nil
    )
    TestAccessTokenWallet.tokenCache = cache

    delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)

    XCTAssertEqual(
      TestAccessTokenWallet.current,
      expected,
      "Should set the current access token to the cached access token when it exists"
    )
  }

  func testDidFinishLaunchingSetsCurrentAccessTokenWithoutCache() {
    TestAccessTokenWallet.current = SampleAccessTokens.validToken
    TestAccessTokenWallet.tokenCache = TestTokenCache(
      accessToken: nil,
      authenticationToken: nil
    )

    delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)

    XCTAssertNil(
      TestAccessTokenWallet.current,
      "Should set the current access token to nil access token when there isn't a cached token"
    )
  }

  func testDidFinishLaunchingSetsCurrentAuthenticationTokenWithCache() {
    let expected = SampleAuthenticationToken.validToken
    TestAuthenticationTokenWallet.tokenCache = TestTokenCache(
      accessToken: nil,
      authenticationToken: expected
    )
    delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)

    XCTAssertEqual(
      TestAuthenticationTokenWallet.current,
      expected,
      "Should set the current authentication token to the cached access token when it exists"
    )
  }

  func testDidFinishLaunchingSetsCurrentAuthenticationTokenWithoutCache() {
    TestAuthenticationTokenWallet.tokenCache = TestTokenCache(
      accessToken: nil,
      authenticationToken: nil
    )

    delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)

    XCTAssertNil(
      TestAuthenticationTokenWallet.current,
      "Should set the current authentication token to nil access token when there isn't a cached token"
    )
  }

  func testDidFinishLaunchingWithAutoLogEnabled() {
    settings.isAutoLogAppEventsEnabled = true
    userDataStore.set(0, forKey: bitmaskKey)

    delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)

    XCTAssertEqual(
      appEvents.capturedEventName,
      .initializeSDK,
      "Should log initialization when auto log app events is enabled"
    )
  }

  func testDidFinishLaunchingWithAutoLogDisabled() {
    settings.isAutoLogAppEventsEnabled = false
    userDataStore.set(1, forKey: bitmaskKey)

    delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)

    XCTAssertNil(
      appEvents.capturedEventName,
      "Should not log initialization when auto log app events are disabled"
    )
  }

  func testDidFinishLaunchingWithObservers() {
    delegate.isAppLaunched = false

    let observer1 = TestApplicationDelegateObserver()
    let observer2 = TestApplicationDelegateObserver()

    delegate.addObserver(observer1)
    delegate.addObserver(observer2)

    let notifiedObservers = delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)

    XCTAssertEqual(
      observer1.didFinishLaunchingCallCount,
      1,
      "Should invoke did finish launching on all observers"
    )
    XCTAssertEqual(
      observer2.didFinishLaunchingCallCount,
      1,
      "Should invoke did finish launching on all observers"
    )
    XCTAssertTrue(notifiedObservers, "Should indicate if observers were notified")
  }

  func testDidFinishLaunchingWithoutObservers() {
    let notifiedObservers = delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)

    XCTAssertFalse(notifiedObservers, "Should indicate if no observers were notified")
  }

  func testAppEventsEnabled() {
    settings.isAutoLogAppEventsEnabled = true

    let notification = Notification(
      name: UIApplication.didBecomeActiveNotification,
      object: self,
      userInfo: nil
    )
    delegate.applicationDidBecomeActive(notification)

    XCTAssertTrue(
      appEvents.wasActivateAppCalled,
      "Should have app events activate the app when autolog app events is enabled"
    )
    XCTAssertEqual(
      appEvents.capturedApplicationState,
      .active,
      "Should set the application state to active when the notification is received"
    )
  }

  func testAppEventsDisabled() {
    settings.isAutoLogAppEventsEnabled = false

    let notification = Notification(
      name: UIApplication.didBecomeActiveNotification,
      object: self,
      userInfo: nil
    )
    delegate.applicationDidBecomeActive(notification)

    XCTAssertFalse(
      appEvents.wasActivateAppCalled,
      "Should not have app events activate the app when autolog app events is enabled"
    )
    XCTAssertEqual(
      appEvents.capturedApplicationState,
      .active,
      "Should set the application state to active when the notification is received"
    )
  }

  func testSettingApplicationState() {
    delegate.applicationState = .background
    XCTAssertEqual(
      appEvents.capturedApplicationState,
      .background,
      "The value of applicationState after calling setApplicationState should be UIApplicationStateBackground"
    )
  }

  func testInitializingSdkTriggersApplicationLifecycleNotificationsForAppEvents() {
    delegate.initializeSDK(launchOptions: [:])

    XCTAssertTrue(
      appEvents.wasStartObservingApplicationLifecycleNotificationsCalled,
      "Should have app events start observing application lifecycle notifications upon initialization"
    )
  }

  func testInitializingSDKLogsAppEvent() {
    settings.isAutoLogAppEventsEnabled = true
    userDataStore.setValue(0, forKey: bitmaskKey)

    delegate.logSDKInitialize()

    XCTAssertEqual(
      appEvents.capturedEventName,
      .initializeSDK
    )
    XCTAssertFalse(appEvents.capturedIsImplicitlyLogged)
  }

  func testInitializingSdkObservesSystemNotifications() {
    delegate.initializeSDK(launchOptions: [:])

    XCTAssertTrue(
      notificationCenter.capturedAddObserverInvocations.contains(
        TestNotificationCenter.ObserverEvidence(
          observer: delegate as Any,
          name: UIApplication.didEnterBackgroundNotification,
          selector: #selector(ApplicationDelegate.applicationDidEnterBackground(_:)),
          object: nil
        )
      ),
      "Should start observing application backgrounding upon initialization"
    )
    XCTAssertTrue(
      notificationCenter.capturedAddObserverInvocations.contains(
        TestNotificationCenter.ObserverEvidence(
          observer: delegate as Any,
          name: UIApplication.didBecomeActiveNotification,
          selector: #selector(ApplicationDelegate.applicationDidBecomeActive(_:)),
          object: nil
        )
      ),
      "Should start observing application foregrounding upon initialization"
    )
    XCTAssertTrue(
      notificationCenter.capturedAddObserverInvocations.contains(
        TestNotificationCenter.ObserverEvidence(
          observer: delegate as Any,
          name: UIApplication.willResignActiveNotification,
          selector: #selector(ApplicationDelegate.applicationWillResignActive(_:)),
          object: nil
        )
      ),
      "Should start observing application resignation upon initializtion"
    )
  }

  func testInitializingSdkSetsSessionInformation() {
    delegate.initializeSDK(
      launchOptions: [
        UIApplication.LaunchOptionsKey.sourceApplication: name,
        .url: SampleURLs.valid,
      ]
    )

    XCTAssertEqual(
      appEvents.capturedSetSourceApplication,
      name,
      "Should set the source application based on the launch options"
    )
    XCTAssertEqual(
      appEvents.capturedSetSourceApplicationURL,
      SampleURLs.valid,
      "Should set the source application url based on the launch options"
    )
  }

  func testInitializingSdkRegistersForSessionUpdates() {
    delegate.initializeSDK(launchOptions: [:])

    XCTAssertTrue(
      appEvents.wasRegisterAutoResetSourceApplicationCalled,
      "Should have the analytics session register to auto reset the source application"
    )
  }

  // MARK: - DidFinishLaunching

  func testDidFinishLaunchingLoadsServerConfiguration() {
    delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)

    XCTAssertTrue(
      serverConfigurationProvider.loadServerConfigurationWasCalled,
      "Should load a server configuration on finishing launching the application"
    )
  }

  func testDidFinishLaunchingSetsProfileWithCache() {
    TestProfileProvider.stubbedCachedProfile = profile

    delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)

    XCTAssertEqual(
      TestProfileProvider.current,
      profile,
      "Should set the current profile to the value fetched from the cache"
    )
  }

  func testDidFinishLaunchingSetsProfileWithoutCache() {
    XCTAssertNil(
      TestProfileProvider.stubbedCachedProfile,
      "Setup should nil out the cached profile"
    )

    delegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)

    XCTAssertNil(
      TestProfileProvider.current,
      "Should set the current profile to nil when the cache is empty"
    )
  }

  // MARK: - URL Opening

  func testOpeningURLChecksAEMFeatureAvailability() {
    delegate.application(
      UIApplication.shared,
      open: SampleURLs.validApp,
      options: [:]
    )
    XCTAssertTrue(
      featureChecker.capturedFeaturesContains(.AEM),
      "Opening a deep link should check if the AEM feature is enabled"
    )
  }

  func testOpeningUniversalLinkChecksAEMFeatureAvailabilityWithRawStringValue() {
    // See https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app
    let userActivity = NSUserActivity(activityType: "NSUserActivityTypeBrowsingWeb")
    userActivity.webpageURL = SampleURLs.validUniversalLink
    delegate.application(
      UIApplication.shared,
      continue: userActivity
    )
    XCTAssertTrue(
      featureChecker.capturedFeaturesContains(.AEM),
      "Opening a deep link should check if the AEM feature is enabled"
    )
  }

  func testOpeningUniversalLinkChecksAEMFeatureAvailability() {
    // See https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app
    let userActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb)
    userActivity.webpageURL = SampleURLs.validUniversalLink
    delegate.application(
      UIApplication.shared,
      continue: userActivity
    )
    XCTAssertTrue(
      featureChecker.capturedFeaturesContains(.AEM),
      "Opening a deep link should check if the AEM feature is enabled"
    )
  }

  func testOpeningUniversalLinkNonBrowsingWebDoesNotCheckAEMAvailability() {
    let userActivity = NSUserActivity(activityType: "Example")
    userActivity.webpageURL = SampleURLs.validUniversalLink
    delegate.application(
      UIApplication.shared,
      continue: userActivity
    )
    XCTAssertFalse(
      featureChecker.capturedFeaturesContains(.AEM),
      "Opening a deep link should check if the AEM feature is enabled"
    )
  }

  // MARK: - Application Observers

  func testDefaultsObservers() {
    XCTAssertEqual(
      delegate.applicationObservers.count,
      0,
      "Should have no observers by default"
    )
  }

  func testAddingNewObserver() {
    delegate.addObserver(observer)

    XCTAssertEqual(
      delegate.applicationObservers.count,
      1,
      "Should be able to add a single observer"
    )
  }

  func testAddingDuplicateObservers() {
    delegate.addObserver(observer)
    delegate.addObserver(observer)

    XCTAssertEqual(
      delegate.applicationObservers.count,
      1,
      "Should only add one instance of a given observer"
    )
  }

  func testRemovingObserver() {
    delegate.addObserver(observer)
    delegate.removeObserver(observer)

    XCTAssertEqual(
      delegate.applicationObservers.count,
      0,
      "Should be able to remove observers that are present in the stored list"
    )
  }

  func testRemovingMissingObserver() {
    delegate.removeObserver(observer)

    XCTAssertEqual(
      delegate.applicationObservers.count,
      0,
      "Should not be able to remove absent observers"
    )
  }

  func testAppNotifyObserversWhenAppWillResignActive() {
    delegate.addObserver(observer)

    let notification = Notification(
      name: UIApplication.willResignActiveNotification,
      object: UIApplication.shared,
      userInfo: nil
    )
    delegate.applicationWillResignActive(notification)

    XCTAssertTrue(
      observer.wasWillResignActiveCalled,
      "Should inform observers when the application will resign active status"
    )
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

@testable import FBSDKCoreKit

import TestTools

final class AppLinkNavigationTests: XCTestCase {

  struct AppLinkUrlPayload: Codable {
    let userAgent: String
    let version: String
    let extras: [String: String]
    let targetUrl: URL?

    enum CodingKeys: String, CodingKey {
      case userAgent = "user_agent"
      case version
      case extras
      case targetUrl = "target_url"
    }
  }

  // swiftlint:disable implicitly_unwrapped_optional
  var target: AppLinkTarget!
  var emptyAppLink: AppLink!
  var eventPoster: TestMeasurementEvent!
  var resolver: TestAppLinkResolver!
  var settings: TestSettings!
  fileprivate var urlOpener: URLOpener!
  var navigation: AppLinkNavigation!
  // swiftlint:enable implicitly_unwrapped_optional

  override func setUp() {
    super.setUp()

    target = AppLinkTarget(url: .usingHost1, appStoreId: "123", appName: "ExampleApp")
    emptyAppLink = AppLink(sourceURL: nil, targets: [], webURL: nil)
    eventPoster = TestMeasurementEvent()
    resolver = TestAppLinkResolver()
    settings = TestSettings()
    settings.sdkVersion = "16.1.2"
    urlOpener = URLOpener(canOpenURL: true)
    AppLinkNavigation.defaultResolver = resolver

    AppLinkNavigation.setDependencies(
      .init(
        settings: settings,
        urlOpener: urlOpener,
        appLinkEventPoster: eventPoster,
        appLinkResolver: resolver
      )
    )

    navigation = AppLinkNavigation(
      appLink: emptyAppLink,
      extras: [:],
      appLinkData: [:]
    )
  }

  override func tearDown() {
    target = nil
    emptyAppLink = nil
    eventPoster = nil
    resolver = nil
    settings = nil
    urlOpener = nil
    navigation = nil

    AppLinkNavigation.resetDependencies()

    super.tearDown()
  }

  func testDefaultResolver() {
    AppLinkNavigation.resetDependencies()
    XCTAssertTrue(
      AppLinkNavigation.defaultResolver === WebViewAppLinkResolver.shared,
      "Should use the shared webview app link resolver by default"
    )
  }

  func testSettingDefaultResolver() {
    let resolver = AppLinkResolver()
    AppLinkNavigation.defaultResolver = resolver

    XCTAssertTrue(
      AppLinkNavigation.defaultResolver === resolver,
      "Should be able to set the default app link resolver"
    )
    XCTAssertTrue(
      AppLinkNavigation.appLinkResolver === resolver,
      "Should set the underlying resolver when setting the default"
    )
  }

  func testCreatingWithEmptyAppLink() {
    XCTAssertNotNil(
      navigation,
      "Should be able to create an app link without verifying anything about it at all"
    )
  }

  func testCallbackAppLinkData() {
    XCTAssertEqual(
      AppLinkNavigation.callbackAppLinkData(forApp: "foo", url: "bar"),
      ["referer_app_link": ["app_name": "foo", "url": "bar"]],
      "Should produce the expected app link callback data"
    )
  }

  // MARK: - Dependencies Configuration

  func testDefaultTypeDependencies() throws {
    AppLinkNavigation.resetDependencies()
    XCTAssertThrowsError(try AppLinkNavigation.getDependencies(), .defaultDependencies)
  }

  func testCustomTypeDependencies() throws {
    let dependencies = try AppLinkNavigation.getDependencies()

    XCTAssertIdentical(
      dependencies.settings as AnyObject,
      settings,
      .customDependency(for: "settings sharing")
    )

    XCTAssertIdentical(
      dependencies.urlOpener as AnyObject,
      urlOpener,
      .customDependency(for: "opening urls")
    )

    XCTAssertTrue(
      dependencies.appLinkEventPoster is TestMeasurementEvent,
      .customDependency(for: "event posting")
    )

    XCTAssertIdentical(
      dependencies.appLinkResolver as AnyObject,
      resolver,
      .customDependency(for: "resolving app links")
    )
  }

  // MARK: - Link Creation

  func testAppLinkWithTargetUrl() {
    do {
      let url = try XCTUnwrap(navigation.appLinkURL(targetURL: .usingHost1))
      let payload = decodedPayload(url: url)

      XCTAssertEqual(payload?.userAgent, "FBSDK \(FBSDK_VERSION_STRING)")
      XCTAssertEqual(payload?.version, "1.0")
      XCTAssertEqual(payload?.extras, [:])
      XCTAssertNil(payload?.targetUrl)
    } catch {
      XCTAssertNil(
        error,
        "Should not populate an error when creating an app link with a valid target url"
      )
    }
  }

  func testAppLinkWithTargetUrlWithValidStartingAppLink() {
    let appLink = AppLink(sourceURL: .usingHost1, targets: [target], webURL: .usingHost1)
    navigation = AppLinkNavigation(
      appLink: appLink, extras: [:], appLinkData: [:]
    )
    do {
      let url = try XCTUnwrap(navigation.appLinkURL(targetURL: .usingHost1))
      let payload = decodedPayload(url: url)

      XCTAssertEqual(payload?.userAgent, "FBSDK \(FBSDK_VERSION_STRING)")
      XCTAssertEqual(payload?.version, "1.0")
      XCTAssertEqual(payload?.extras, [:])
      XCTAssertEqual(payload?.targetUrl, .usingHost1)
    } catch {
      XCTAssertNil(
        error,
        "Should not populate an error when creating an app link with a valid target url"
      )
    }
  }

  func testAppLinkWithTargetUrlWithInvalidStartingAppLinkData() {
    navigation = AppLinkNavigation(
      appLink: emptyAppLink, extras: [:], appLinkData: ["foo": Any.self]
    )

    XCTAssertThrowsError(
      try navigation.appLinkURL(targetURL: .usingHost1),
      "An error is thrown when a valid url is passed but bad app link data is provided"
    )
  }

  func testAppLinkWithTargetUrlWithValidStartingAppLinkData() {
    let appLinkData = ["user_agent": "foo", "version": "bar"]
    navigation = AppLinkNavigation(
      appLink: emptyAppLink, extras: ["some": "extra"], appLinkData: appLinkData
    )

    do {
      let url = try XCTUnwrap(navigation.appLinkURL(targetURL: .usingHost1))
      let payload = decodedPayload(url: url)

      XCTAssertEqual(payload?.userAgent, "foo")
      XCTAssertEqual(payload?.version, "bar")
      XCTAssertEqual(payload?.extras, ["some": "extra"])
      XCTAssertNil(payload?.targetUrl)
    } catch {
      XCTAssertNil(
        error,
        "Should not populate an error when creating an app link with a valid target url"
      )
    }
  }

  // MARK: - Posting Navigation Events

  func testPostingNavigationEventWithTypeApp() {
    navigation.postNavigateEventNotification(
      targetURL: nil,
      error: nil,
      navigationType: .app
    )
    XCTAssertEqual(
      eventPoster.capturedEventName,
      AppLinkNavigateOutEventName,
      "Should post a notification with the expected event name"
    )
    XCTAssertEqual(
      eventPoster.capturedArgs,
      ["type": "app", "success": "1"],
      "Post an event with type 'app' should be considered a success"
    )
  }

  func testPostingNavigationEventWithTypeBrowser() {
    navigation.postNavigateEventNotification(
      targetURL: nil,
      error: nil,
      navigationType: .browser
    )
    XCTAssertEqual(
      eventPoster.capturedEventName,
      AppLinkNavigateOutEventName,
      "Should post a notification with the expected event name"
    )
    XCTAssertEqual(
      eventPoster.capturedArgs,
      ["type": "web", "success": "1"],
      "Post an event with type 'browser' should be considered a success"
    )
  }

  func testPostingNavigationEventWithTypeFailure() {
    navigation.postNavigateEventNotification(
      targetURL: nil,
      error: nil,
      navigationType: .failure
    )
    XCTAssertEqual(
      eventPoster.capturedEventName,
      AppLinkNavigateOutEventName,
      "Should post a notification with the expected event name"
    )
    XCTAssertEqual(
      eventPoster.capturedArgs,
      ["type": "fail", "success": "0"],
      "Post an event with type 'failure' should be considered a failure"
    )
  }

  func testPostingNavigationEventWithAppLink() {
    let appLink = AppLink(sourceURL: .usingHost1, targets: [target], webURL: .usingHost1)
    navigation = AppLinkNavigation(
      appLink: appLink, extras: [:], appLinkData: [:]
    )

    navigation.postNavigateEventNotification(
      targetURL: nil,
      error: nil,
      navigationType: .app
    )
    XCTAssertEqual(
      eventPoster.capturedEventName,
      AppLinkNavigateOutEventName,
      "Should post a notification with the expected event name"
    )
    XCTAssertEqual(
      eventPoster.capturedArgs["sourceHost"],
      URL.usingHost1.host,
      "A navigation event notification should include information about the app link"
    )
    XCTAssertEqual(
      eventPoster.capturedArgs["sourceScheme"],
      URL.usingHost1.scheme,
      "A navigation event notification should include information about the app link"
    )
    XCTAssertEqual(
      eventPoster.capturedArgs["sourceURL"],
      URL.usingHost1.absoluteString,
      "A navigation event notification should include information about the app link"
    )
  }

  func testPostingNavigationEventWithError() {
    navigation.postNavigateEventNotification(
      targetURL: nil,
      error: SampleError(),
      navigationType: .app
    )
    XCTAssertEqual(
      eventPoster.capturedEventName,
      AppLinkNavigateOutEventName,
      "Should post a notification with the expected event name"
    )
    XCTAssertEqual(
      eventPoster.capturedArgs["error"],
      "The operation couldnâ€™t be completed. (TestTools.SampleError error 1.)",
      "A navigation event notification should include information about any errors"
    )
    XCTAssertEqual(
      eventPoster.capturedArgs["success"],
      "1",
      "A navigation event notification should be considered successful even if there's an error"
    )
  }

  func testPostingNavigationEventWithBackToReferrer() {
    let appLink = AppLink(sourceURL: nil, targets: [], webURL: nil, isBackToReferrer: true)
    navigation = AppLinkNavigation(
      appLink: appLink, extras: [:], appLinkData: [:]
    )

    navigation.postNavigateEventNotification(
      targetURL: nil,
      error: nil,
      navigationType: .app
    )

    XCTAssertEqual(
      eventPoster.capturedEventName,
      AppLinkNavigateBackToReferrerEventName,
      "A navigation event notification should be indicate if the app link points back to the referrer"
    )
  }

  // MARK: - Navigation Type

  func testNavigationTypeWithoutTarget() {
    XCTAssertEqual(
      navigation.navigationType(for: []),
      .failure,
      "The navigation type for an empty list of targets should be a failure"
    )
  }

  func testNavigationTypeWithInvalidTargetWithoutWebUrl() {
    let target = AppLinkTarget(url: URL(string: "invalid url"), appStoreId: nil, appName: name)
    let appLink = AppLink(sourceURL: nil, targets: [target], webURL: nil)
    navigation = AppLinkNavigation(appLink: appLink, extras: [:], appLinkData: [:])

    XCTAssertEqual(
      navigation.navigationType(for: [target]),
      .failure,
      "The navigation type when there is an invalid target and no web url should be 'failure'"
    )
  }

  func testNavigationTypeWithValidTargetWithoutWebUrl() {
    let target = AppLinkTarget(url: .usingHost1, appStoreId: nil, appName: name)
    let appLink = AppLink(sourceURL: nil, targets: [target], webURL: nil)
    navigation = AppLinkNavigation(appLink: appLink, extras: [:], appLinkData: [:])

    XCTAssertEqual(
      navigation.navigationType(for: [target]),
      .app,
      "The navigation type when there is a valid target and no web url should be 'app'"
    )
  }

  func testNavigationTypeWithValidTargetWithWebUrl() {
    let target = AppLinkTarget(url: .usingHost1, appStoreId: nil, appName: name)
    let appLink = AppLink(sourceURL: nil, targets: [target], webURL: .usingHost1)
    navigation = AppLinkNavigation(appLink: appLink, extras: [:], appLinkData: [:])

    XCTAssertEqual(
      navigation.navigationType(for: [target]),
      .app,
      "The navigation type when there is a valid target and a web url should be 'app'"
    )
  }

  func testNavigationTypeWithInvalidTargetWithWebUrl() {
    let target = AppLinkTarget(url: URL(string: "invalid url"), appStoreId: nil, appName: name)
    let appLink = AppLink(sourceURL: nil, targets: [target], webURL: .usingHost1)
    navigation = AppLinkNavigation(appLink: appLink, extras: [:], appLinkData: [:])

    XCTAssertEqual(
      navigation.navigationType(for: [target]),
      .browser,
      "The navigation type when there is an invalid target and a web url should be 'browser'"
    )
  }

  // MARK: - Navigating

  func testSuccessfullyNavigatingWithTargetWithoutWebUrl() {
    urlOpener.stubOpenSuccess(host: .host1, succeeds: true)
    let target = AppLinkTarget(url: .usingHost1, appStoreId: nil, appName: name)
    let appLink = AppLink(sourceURL: nil, targets: [target], webURL: nil)

    navigation = AppLinkNavigation(appLink: appLink, extras: [:], appLinkData: [:])

    do {
      let result = try navigation.navigate()
      XCTAssertEqual(result, .app, "Should return the correct navigation type")
      XCTAssertNotNil(
        urlOpener.capturedOpenURL,
        "Should create an open a url for a valid target"
      )
      XCTAssertEqual(
        urlOpener.capturedOpenURL?.absoluteString,
        eventPoster.capturedArgs["outputURL"],
        "Should post a notification with the url that was opened"
      )
    } catch {
      XCTAssertNil(error)
    }
  }

  func testUnsuccessfullyNavigatingWithTargetWithWebUrl() {
    urlOpener.stubOpenSuccess(host: .host1, succeeds: false)
    urlOpener.stubOpenSuccess(host: .host2, succeeds: true)
    let target = AppLinkTarget(url: .usingHost1, appStoreId: nil, appName: name)
    let appLink = AppLink(sourceURL: nil, targets: [target], webURL: .usingHost2.appendingPathExtension(name))

    navigation = AppLinkNavigation(appLink: appLink, extras: [:], appLinkData: [:])

    do {
      let result = try navigation.navigate()
      XCTAssertEqual(result, .browser, "Should return the correct navigation type")
      XCTAssertNotNil(
        urlOpener.capturedOpenURL,
        "Should create an open a url for a valid target"
      )
      XCTAssertEqual(
        urlOpener.capturedOpenURL?.absoluteString,
        eventPoster.capturedArgs["outputURL"],
        "Should post a notification with the url that was opened"
      )
    } catch {
      XCTAssertNil(error)
    }
  }

  func testUnsuccessfullyNavigatingWithoutTargetAndNoWebURL() {
    let appLink = AppLink(sourceURL: nil, targets: [], webURL: nil)
    navigation = AppLinkNavigation(appLink: appLink, extras: [:], appLinkData: [:])

    do {
      let result = try navigation.navigate()
      XCTAssertEqual(
        result,
        .failure,
        "A correct navigation type is returned when there are not targets, no web url"
      )
    } catch {
      XCTAssertNil(error, "An error is not thrown when there are not targets and no web url")
    }
  }

  func testUnsuccessfullyNavigatingWithoutTargetAndWebURL() {
    let appLink = AppLink(sourceURL: nil, targets: [], webURL: .usingHost1)
    let appLinkData = ["bad link data": Date()]
    navigation = AppLinkNavigation(appLink: appLink, extras: [:], appLinkData: appLinkData)

    XCTAssertThrowsError(
      try navigation.navigate(),
      "An error is thrown when there are not targets, there is a web url and bad link data is provided"
    )
  }

  func testUnsuccessfullyNavigatingWithTargetAndBadLinkData() {
    let target = AppLinkTarget(url: .usingHost1, appStoreId: nil, appName: name)
    let appLink = AppLink(sourceURL: nil, targets: [target], webURL: nil)
    let appLinkData = ["bad link data": Date()]
    navigation = AppLinkNavigation(appLink: appLink, extras: [:], appLinkData: appLinkData)

    XCTAssertThrowsError(
      try navigation.navigate(),
      "An error is thrown when there are targets, no web url and bad link data is provided"
    )
  }

  func testNavigatingToUrlWithoutAppLink() {
    let expectation = self.expectation(description: name)
    AppLinkNavigation.navigate(to: .usingHost1) { _, _ in
      expectation.fulfill()
    }

    // The captured completion itself is dispatched asynchronously to the main thread
    // so we can delay a tick here to make sure it's complete
    DispatchQueue.main.async {
      self.resolver.capturedCompletion?(nil, nil)
    }

    waitForExpectations(timeout: 1, handler: nil)
  }

  func testNavigatingToUrlWithAppLink() {
    let expectation = self.expectation(description: name)
    var callbackNavigationType: AppLinkNavigationType?
    var callbackError: Error?

    AppLinkNavigation.navigate(to: .usingHost1) { potentialNavigationType, potentialError in
      callbackNavigationType = potentialNavigationType
      callbackError = potentialError
      expectation.fulfill()
    }

    let appLink = AppLink(sourceURL: .usingHost1, targets: [], webURL: nil)

    // The captured completion itself is dispatched asynchronously to the main thread
    // so we can delay a tick here to make sure it's complete
    DispatchQueue.main.async {
      self.resolver.capturedCompletion?(appLink, nil)
    }
    waitForExpectations(timeout: 1, handler: nil)

    XCTAssertEqual(callbackNavigationType, .failure)
    XCTAssertNil(callbackError)
  }

  func testNavigatingToUrlWithError() {
    let expectation = self.expectation(description: name)
    var callbackNavigationType: AppLinkNavigationType?
    var callbackError: Error?

    AppLinkNavigation.navigate(to: .usingHost1) { potentialNavigationType, potentialError in
      callbackNavigationType = potentialNavigationType
      callbackError = potentialError
      expectation.fulfill()
    }

    let appLink = AppLink(sourceURL: .usingHost1, targets: [], webURL: nil)

    // The captured completion itself is dispatched asynchronously to the main thread
    // so we can delay a tick here to make sure it's complete
    DispatchQueue.main.async {
      self.resolver.capturedCompletion?(
        appLink,
        NSError(domain: "foo", code: 0, userInfo: nil)
      )
    }
    waitForExpectations(timeout: 1, handler: nil)

    XCTAssertEqual(callbackNavigationType, .failure)
    XCTAssertNotNil(callbackError)
  }

  // MARK: - Resolving

  func testResolvingAppLinkWithMissingDestination() {
    var didInvokeCompletion = false
    AppLinkNavigation.resolveAppLink(.usingHost1) { _, _ in
      didInvokeCompletion = true
    }
    resolver.capturedCompletion?(nil, nil)

    XCTAssertEqual(
      resolver.capturedURL,
      .usingHost1,
      "Should resolve using the provided url"
    )
    XCTAssertTrue(didInvokeCompletion)
  }

  // MARK: - Helpers

  func decodedPayload(
    url: URL,
    file: StaticString = #file,
    line: UInt = #line
  ) -> AppLinkUrlPayload? {
    guard
      let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
      let queryItems = components.queryItems,
      let data = queryItems.first?.value?.data(using: .utf8),
      let payload = try? JSONDecoder().decode(AppLinkUrlPayload.self, from: data)
    else {
      XCTFail("Could not decode the payload from the query item", file: file, line: line)
      return nil
    }

    return payload
  }
}

// MARK: - Assumptions

// swiftformat:disable extensionaccesscontrol
fileprivate extension String {
  static let defaultDependencies = "AppLinkNavigation has no type dependencies by default"

  static func customDependency(for type: String) -> String {
    "AppLinkNavigation uses a custom \(type) type dependency when provided"
  }
}

// MARK: - Test Values

fileprivate extension URL {
  // swiftlint:disable force_unwrapping
  static let usingHost1 = URL(string: "https://\(String.host1)/")!
  static let usingHost2 = URL(string: "https://\(String.host2)/")!
  // swiftlint:enable force_unwrapping
}

fileprivate extension String {
  static let host1 = "host1.com"
  static let host2 = "host2.com"
}

// MARK: - Custom Test Doubles

extension AppLinkNavigationTests {

  fileprivate final class URLOpener: _InternalURLOpener {
    var capturedOpenURL: URL?
    var openSuccessStubsByHost = [String: Bool]()
    var canOpenURL: Bool

    init(canOpenURL: Bool = false) {
      self.canOpenURL = canOpenURL
    }

    fileprivate func stubOpenSuccess(host: String, succeeds: Bool) {
      openSuccessStubsByHost[host] = succeeds
    }

    fileprivate func canOpen(_ url: URL) -> Bool { canOpenURL }

    fileprivate func open(_ url: URL) -> Bool {
      capturedOpenURL = url

      guard
        let host = url.host,
        let didOpen = openSuccessStubsByHost[host]
      else {
        fatalError("URL must have a host and an opening success stub: \(url.absoluteString)")
      }

      return didOpen
    }

    fileprivate func open(
      _ url: URL,
      options: [UIApplication.OpenExternalURLOptionsKey: Any],
      completionHandler completion: ((Bool) -> Void)?
    ) {}
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

@testable import FBSDKCoreKit

import TestTools
import XCTest

final class AppLinkURLTests: XCTestCase {

  override class func setUp() {
    super.setUp()

    AppLinkURL.reset()
  }

  override func tearDown() {
    AppLinkURL.reset()

    super.tearDown()
  }

  func testDefaultDependencies() {
    XCTAssertNil(
      AppLinkURL.settings,
      "Should not have settings by default"
    )
    XCTAssertNil(
      AppLinkURL.appLinkFactory,
      "Should not have an app link factory by default"
    )
    XCTAssertNil(
      AppLinkURL.appLinkTargetFactory,
      "Should not have an app link target factory by default"
    )
    XCTAssertNil(
      AppLinkURL.appLinkEventPoster,
      "Should not have an app link event poster"
    )
  }

  func testConfiguringDependencies() {
    let settings = TestSettings()
    let appLinkFactory = TestAppLinkFactory()
    let appLinkTargetFactory = TestAppLinkTargetFactory()
    let appLinkEventPoster = TestMeasurementEvent()

    AppLinkURL.configure(
      settings: settings,
      appLinkFactory: appLinkFactory,
      appLinkTargetFactory: appLinkTargetFactory,
      appLinkEventPoster: appLinkEventPoster
    )

    XCTAssertTrue(
      AppLinkURL.settings === settings,
      "Should be able to configure dependencies"
    )
    XCTAssertTrue(
      AppLinkURL.appLinkFactory === appLinkFactory,
      "Should be able to configure dependencies"
    )
    XCTAssertTrue(
      AppLinkURL.appLinkTargetFactory === appLinkTargetFactory,
      "Should be able to configure dependencies"
    )
    XCTAssertTrue(
      AppLinkURL.appLinkEventPoster === appLinkEventPoster,
      "Should be able to configure dependencies"
    )
  }
}