/*
 * 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
@testable import FBSDKLoginKit

import FBSDKCoreKit_Basics
import TestTools
import XCTest

final class LoginURLCompleterTests: XCTestCase {

  enum Keys {
    static let accessToken = "access_token"
    static let dataAccessExpiration = "data_access_expiration_time"
    static let deniedScopes = "denied_scopes"
    static let expires = "expires"
    static let expiresAt = "expires_at"
    static let expiresIn = "expires_in"
    static let grantedScopes = "granted_scopes"
    static let graphDomain = "graph_domain"
    static let idToken = "id_token"
    static let nonce = "nonce"
    static let userID = "user_id"
    static let code = "code"
    static let error = "error"
  }

  enum Values {
    static let appID = "1234567"
    static let idToken = "abc123"
    static let nonce = "some_nonce"
    static let codeVerifier = "some_code_verifier"
    static let redirectURL = "https://www.example.com"
    static let accessToken = "some_token"
  }

  // swiftlint:disable implicitly_unwrapped_optional
  var profileFactory: ProfileFactory!
  var authenticationTokenFactory: TestAuthenticationTokenFactory!
  var graphRequestFactory: TestGraphRequestFactory!
  var internalUtility: TestInternalUtility!
  var errorFactory: _ErrorFactory!
  var urlCompleter: LoginURLCompleter!
  // swiftlint:enable implicitly_unwrapped_optional

  override func setUp() {
    super.setUp()

    profileFactory = ProfileFactory()
    authenticationTokenFactory = TestAuthenticationTokenFactory()
    graphRequestFactory = TestGraphRequestFactory()
    internalUtility = TestInternalUtility()
    errorFactory = _ErrorFactory()
    internalUtility.stubbedAppURL = URL(string: Values.redirectURL)

    LoginURLCompleter.setDependencies(
      .init(
        profileFactory: profileFactory,
        authenticationTokenCreator: authenticationTokenFactory,
        graphRequestFactory: graphRequestFactory,
        internalUtility: internalUtility,
        errorFactory: errorFactory
      )
    )

    urlCompleter = LoginURLCompleter(urlParameters: [:], appID: "")
  }

  override func tearDown() {
    LoginURLCompleter.resetDependencies()
    urlCompleter = nil
    profileFactory = nil
    authenticationTokenFactory = nil
    graphRequestFactory = nil
    errorFactory = nil
    internalUtility = nil

    super.tearDown()
  }

  func testDefaultTypeDependencies() throws {
    LoginURLCompleter.resetDependencies()

    let dependencies = try LoginURLCompleter.getDependencies()

    XCTAssertTrue(
      dependencies.profileFactory is ProfileFactory,
      .defaultDependency("ProfileFactory", for: "profile factory")
    )

    XCTAssertTrue(
      dependencies.authenticationTokenCreator is AuthenticationTokenFactory,
      .defaultDependency("_AuthenticationTokenFactory", for: "authentication token creator")
    )

    XCTAssertTrue(
      dependencies.graphRequestFactory is GraphRequestFactory,
      .defaultDependency("GraphRequestFactory", for: "graph request factory")
    )

    XCTAssertTrue(
      dependencies.internalUtility is InternalUtility,
      .defaultDependency("InternalUtility", for: "internal utility")
    )
  }

  func testCustomDependencies() throws {

    let dependencies = try LoginURLCompleter.getDependencies()

    XCTAssertIdentical(
      dependencies.profileFactory as AnyObject,
      profileFactory,
      .customDependency(for: "profile factory")
    )

    XCTAssertIdentical(
      dependencies.authenticationTokenCreator as AnyObject,
      authenticationTokenFactory,
      .customDependency(for: "authentication token creator")
    )

    XCTAssertIdentical(
      dependencies.graphRequestFactory,
      graphRequestFactory,
      .customDependency(for: "graph request factory")
    )

    XCTAssertIdentical(
      dependencies.internalUtility,
      internalUtility,
      .customDependency(for: "internal utility")
    )
  }

  // MARK: Creation

  func testInitWithAccessTokenWithIDToken() {
    let parameters = SampleRawLoginCompletionParameters.withAccessTokenWithIDToken

    urlCompleter = createLoginCompleter(parameters: parameters, appID: Values.appID)

    verifyParameters(actual: urlCompleter.parameters, expected: parameters)
  }

  func testInitWithAccessToken() {
    let parameters = SampleRawLoginCompletionParameters.withAccessToken
    urlCompleter = createLoginCompleter(parameters: parameters, appID: Values.appID)

    verifyParameters(actual: urlCompleter.parameters, expected: parameters)
  }

  func testInitWithNonce() {
    let parameters = SampleRawLoginCompletionParameters.withNonce
    urlCompleter = createLoginCompleter(parameters: parameters, appID: Values.appID)

    verifyParameters(actual: urlCompleter.parameters, expected: parameters)
  }

  func testInitWithCode() {
    let parameters = SampleRawLoginCompletionParameters.withCode
    urlCompleter = createLoginCompleter(parameters: parameters, appID: Values.appID)

    verifyParameters(actual: urlCompleter.parameters, expected: parameters)
  }

  func testInitWithIDToken() {
    let parameters = SampleRawLoginCompletionParameters.withIDToken
    urlCompleter = createLoginCompleter(parameters: parameters, appID: Values.appID)

    verifyParameters(actual: urlCompleter.parameters, expected: parameters)
  }

  func testInitWithStringExpirations() {
    let parameters = SampleRawLoginCompletionParameters.withStringExpirations
    urlCompleter = createLoginCompleter(parameters: parameters, appID: Values.appID)

    verifyParameters(actual: urlCompleter.parameters, expected: parameters)
  }

  func testInitWithoutAccessTokenWithoutIDTokenWithoutCode() {
    let parameters = SampleRawLoginCompletionParameters.withoutAccessTokenWithoutIDTokenWithoutCode
    urlCompleter = createLoginCompleter(parameters: parameters, appID: Values.appID)

    verifyEmptyParameters(urlCompleter.parameters)
  }

  func testInitWithEmptyStrings() {
    let parameters = SampleRawLoginCompletionParameters.withEmptyStrings
    urlCompleter = createLoginCompleter(parameters: parameters, appID: Values.appID)

    verifyEmptyParameters(urlCompleter.parameters)
  }

  func testInitWithEmptyParameters() {
    urlCompleter = createLoginCompleter(parameters: [:], appID: Values.appID)

    verifyEmptyParameters(urlCompleter.parameters)
  }

  func testInitWithError() {
    let parameters = SampleRawLoginCompletionParameters.withError
    urlCompleter = createLoginCompleter(
      parameters: parameters,
      appID: Values.appID
    )

    XCTAssertNotNil(urlCompleter.parameters.error)
  }

  func testInitWithFuzzyParameters() {
    (0 ..< 100).forEach { _ in
      let parameters = SampleRawLoginCompletionParameters.defaultParameters
      if let fuzzyParameters = Fuzzer.randomize(json: parameters) as? [String: Any] {
        _ = createLoginCompleter(parameters: fuzzyParameters, appID: Values.appID)
      }
    }
  }

  // MARK: Completion

  func testCompleteWithNonceGraphRequestCreation() throws {
    urlCompleter = createLoginCompleter(
      parameters: SampleRawLoginCompletionParameters.withNonce,
      appID: Values.appID
    )
    let handler: LoginCompletionParametersBlock = { _ in }

    urlCompleter.completeLogin(handler: handler)

    XCTAssertNil(urlCompleter.parameters.error)
    XCTAssertNil(authenticationTokenFactory.capturedTokenString)
    let capturedRequest = try XCTUnwrap(graphRequestFactory.capturedRequests.first)
    XCTAssertEqual(
      capturedRequest.graphPath,
      "oauth/access_token",
      "Should create a graph request with the expected graph path"
    )
    XCTAssertEqual(
      capturedRequest.parameters["grant_type"] as? String,
      "fb_exchange_nonce",
      "Should create a graph request with the expected grant type parameter"
    )
    XCTAssertEqual(
      capturedRequest.parameters["fb_exchange_nonce"] as? String,
      urlCompleter.parameters.nonceString,
      "Should create a graph request with the expected nonce parameter"
    )
    XCTAssertEqual(
      capturedRequest.parameters["client_id"] as? String,
      Values.appID,
      "Should create a graph request with the expected app id parameter"
    )
    XCTAssertEqual(
      capturedRequest.parameters["fields"] as? String,
      "",
      "Should create a graph request with the expected fields parameter"
    )
    XCTAssertEqual(
      capturedRequest.flags,
      [.doNotInvalidateTokenOnError, .disableErrorRecovery],
      "The graph request should not invalidate the token on error or disable error recovery"
    )
  }

  func testNonceExchangeCompletionWithError() {
    urlCompleter = createLoginCompleter(
      parameters: SampleRawLoginCompletionParameters.withNonce,
      appID: Values.appID
    )

    var completionWasInvoked = false
    var capturedParameters: _LoginCompletionParameters?
    urlCompleter.completeLogin { parameters in
      capturedParameters = parameters
      completionWasInvoked = true
    }

    graphRequestFactory.capturedRequests.first?.capturedCompletionHandler?(nil, nil, SampleError())

    XCTAssertEqual(
      urlCompleter.parameters,
      capturedParameters,
      "Should call the completion with the provided parameters"
    )
    XCTAssertTrue(
      urlCompleter.parameters.error is SampleError,
      "Should pass through the error from the graph request"
    )

    XCTAssertTrue(completionWasInvoked)
  }

  func testNonceExchangeCompletionWithAccessTokenString() {
    urlCompleter = createLoginCompleter(
      parameters: SampleRawLoginCompletionParameters.withNonce,
      appID: Values.appID
    )
    let stubbedResult = [Keys.accessToken: name]

    var completionWasInvoked = false
    var capturedParameters: _LoginCompletionParameters?
    urlCompleter.completeLogin { parameters in
      capturedParameters = parameters
      completionWasInvoked = true
    }

    graphRequestFactory.capturedRequests.first?.capturedCompletionHandler?(nil, stubbedResult, nil)

    XCTAssertEqual(
      urlCompleter.parameters,
      capturedParameters,
      "Should call the completion with the provided parameters"
    )
    XCTAssertEqual(
      urlCompleter.parameters.accessTokenString,
      name,
      "Should set the access token string from the graph request's result"
    )
    XCTAssertTrue(completionWasInvoked)
  }

  func testNonceExchangeCompletionWithAccessTokenStringAndAuthenticationTokenString() {
    urlCompleter = createLoginCompleter(
      parameters: SampleRawLoginCompletionParameters.withNonce,
      appID: Values.appID
    )
    let nonce = Values.nonce
    let stubbedResult = [
      Keys.accessToken: name,
      Keys.idToken: Values.idToken,
    ]

    urlCompleter.completeLogin(
      nonce: nonce,
      codeVerifier: Values.codeVerifier
    ) { _ in }

    graphRequestFactory.capturedRequests.first?.capturedCompletionHandler?(nil, stubbedResult, nil)

    XCTAssertEqual(
      urlCompleter.parameters.accessTokenString,
      name,
      "Should set the access token string from the graph request's result"
    )
    XCTAssertEqual(
      urlCompleter.parameters.authenticationTokenString,
      Values.idToken,
      "Should set the authentication token string from the graph request's result"
    )
    XCTAssertEqual(
      authenticationTokenFactory.capturedTokenString,
      Values.idToken,
      "Should call AuthenticationTokenFactory with the expected token string"
    )
    XCTAssertEqual(
      authenticationTokenFactory.capturedNonce,
      nonce,
      "Should call AuthenticationTokenFactory with the expected nonce"
    )
  }

  func testNonceExchangeWithRandomResults() {
    urlCompleter = createLoginCompleter(
      parameters: SampleRawLoginCompletionParameters.withNonce,
      appID: Values.appID
    )

    let stubbedResult: [String: Any] = [
      Keys.accessToken: name,
      "expires_in": "10000",
      "data_access_expiration_time": 1,
    ]

    var completionWasInvoked = false
    urlCompleter.completeLogin { _ in
      // Basically just making sure that nothing crashes here when we feed it garbage results
      completionWasInvoked = true
    }

    (0 ..< 100).forEach { _ in
      let mangledResult = stubbedResult
      let parameters = Fuzzer.randomize(json: mangledResult)

      graphRequestFactory.capturedRequests.first?.capturedCompletionHandler?(nil, parameters, nil)

      XCTAssertTrue(completionWasInvoked)
      completionWasInvoked = false
    }
  }

  func testCompleteWithCodeGraphRequestCreation() throws {
    urlCompleter = createLoginCompleter(
      parameters: SampleRawLoginCompletionParameters.withCode,
      appID: Values.appID
    )
    let handler: LoginCompletionParametersBlock = { _ in }

    urlCompleter.completeLogin(
      nonce: Values.nonce,
      codeVerifier: Values.codeVerifier,
      handler: handler
    )

    XCTAssertNil(urlCompleter.parameters.error)
    XCTAssertNil(authenticationTokenFactory.capturedTokenString)
    let capturedRequest = try XCTUnwrap(graphRequestFactory.capturedRequests.first)
    XCTAssertEqual(
      capturedRequest.graphPath,
      "oauth/access_token",
      "Should create a graph request with the expected graph path"
    )
    XCTAssertEqual(
      capturedRequest.parameters["client_id"] as? String,
      Values.appID,
      "Should create a graph request with the expected app ID"
    )
    XCTAssertEqual(
      capturedRequest.parameters["redirect_uri"] as? String,
      Values.redirectURL,
      "Should create a graph request with the expected redirect URL"
    )
    XCTAssertEqual(
      capturedRequest.parameters["code_verifier"] as? String,
      Values.codeVerifier,
      "Should create a graph request with the expected code verifier parameter"
    )
    XCTAssertEqual(
      capturedRequest.parameters[Keys.code] as? String,
      SampleRawLoginCompletionParameters.withCode[Keys.code] as? String,
      "Should create a graph request with the expected code parameter"
    )
    XCTAssertEqual(
      capturedRequest.flags,
      [.doNotInvalidateTokenOnError, .disableErrorRecovery],
      "The graph request should not invalidate the token on error or disable error recovery"
    )
  }

  func testCodeExchangeCompletionWithGraphError() {
    urlCompleter = createLoginCompleter(
      parameters: SampleRawLoginCompletionParameters.withCode,
      appID: Values.appID
    )

    var completionWasInvoked = false
    var capturedParameters: _LoginCompletionParameters?
    let handler: LoginCompletionParametersBlock = { parameters in
      capturedParameters = parameters
      completionWasInvoked = true
    }

    urlCompleter.completeLogin(
      nonce: Values.nonce,
      codeVerifier: Values.codeVerifier,
      handler: handler
    )

    graphRequestFactory.capturedRequests.first?.capturedCompletionHandler?(nil, nil, SampleError())

    XCTAssertEqual(
      urlCompleter.parameters,
      capturedParameters,
      "Should call the completion with the provided parameters"
    )
    XCTAssertTrue(
      urlCompleter.parameters.error is SampleError,
      "Should pass through the error from the graph request"
    )

    XCTAssertTrue(completionWasInvoked)
  }

  func testCodeExchangeCompletionWithError() {
    urlCompleter = createLoginCompleter(
      parameters: SampleRawLoginCompletionParameters.withCode,
      appID: Values.appID
    )
    let stubbedResult = [Keys.error: name]

    var completionWasInvoked = false
    var capturedParameters: _LoginCompletionParameters?
    let handler: LoginCompletionParametersBlock = { parameters in
      capturedParameters = parameters
      completionWasInvoked = true
    }

    urlCompleter.completeLogin(
      nonce: Values.nonce,
      codeVerifier: Values.codeVerifier,
      handler: handler
    )

    graphRequestFactory.capturedRequests.first?.capturedCompletionHandler?(nil, stubbedResult, nil)

    XCTAssertEqual(
      urlCompleter.parameters,
      capturedParameters,
      "Should call the completion with the provided parameters"
    )
    XCTAssertNotNil(
      urlCompleter.parameters.error,
      "Should set error from the graph request's result"
    )
    XCTAssertTrue(completionWasInvoked)
  }

  func testCodeExchangeCompletionWithAccessTokenString() {
    urlCompleter = createLoginCompleter(
      parameters: SampleRawLoginCompletionParameters.withCode,
      appID: Values.appID
    )
    let stubbedResult = [Keys.accessToken: name]

    var completionWasInvoked = false
    var capturedParameters: _LoginCompletionParameters?
    let handler: LoginCompletionParametersBlock = { parameters in
      capturedParameters = parameters
      completionWasInvoked = true
    }

    urlCompleter.completeLogin(
      nonce: Values.nonce,
      codeVerifier: Values.codeVerifier,
      handler: handler
    )

    graphRequestFactory.capturedRequests.first?.capturedCompletionHandler?(nil, stubbedResult, nil)

    XCTAssertEqual(
      urlCompleter.parameters,
      capturedParameters,
      "Should call the completion with the provided parameters"
    )
    XCTAssertEqual(
      urlCompleter.parameters.accessTokenString,
      name,
      "Should set the access token string from the graph request's result"
    )
    XCTAssertTrue(completionWasInvoked)
  }

  func testCodeExchangeCompletionWithAccessTokenStringAndAuthenticationTokenString() {
    urlCompleter = createLoginCompleter(
      parameters: SampleRawLoginCompletionParameters.withCode,
      appID: Values.appID
    )
    let stubbedResult = [
      Keys.accessToken: Values.accessToken,
      Keys.idToken: Values.idToken,
    ]

    urlCompleter.completeLogin(
      nonce: Values.nonce,
      codeVerifier: Values.codeVerifier
    ) { _ in }

    graphRequestFactory.capturedRequests.first?.capturedCompletionHandler?(nil, stubbedResult, nil)

    XCTAssertEqual(
      urlCompleter.parameters.accessTokenString,
      Values.accessToken,
      "Should set the access token string from the graph request's result"
    )
    XCTAssertEqual(
      urlCompleter.parameters.authenticationTokenString,
      Values.idToken,
      "Should set the authentication token string from the graph request's result"
    )
    XCTAssertEqual(
      authenticationTokenFactory.capturedTokenString,
      Values.idToken,
      "Should call AuthenticationTokenFactory with the expected token string"
    )
    XCTAssertEqual(
      authenticationTokenFactory.capturedNonce,
      Values.nonce,
      "Should call AuthenticationTokenFactory with the expected nonce"
    )
  }

  func testCodeExchangeWithRandomResults() {
    urlCompleter = createLoginCompleter(
      parameters: SampleRawLoginCompletionParameters.withCode,
      appID: Values.appID
    )

    let stubbedResult: [String: Any] = [
      Keys.accessToken: Values.accessToken,
      "expires_in": "10000",
      "data_access_expiration_time": 1,
    ]

    var completionWasInvoked = false
    urlCompleter.completeLogin(
      nonce: Values.nonce,
      codeVerifier: Values.codeVerifier
    ) { _ in
      // Basically just making sure that nothing crashes here when we feed it garbage results
      completionWasInvoked = true
    }

    (0 ..< 100).forEach { _ in
      let mangledResult = stubbedResult
      let parameters = Fuzzer.randomize(json: mangledResult)

      graphRequestFactory.capturedRequests.first?.capturedCompletionHandler?(nil, parameters, nil)

      XCTAssertTrue(completionWasInvoked)
      completionWasInvoked = false
    }
  }

  func testCompleteWithAuthenticationTokenWithoutNonce() {
    urlCompleter = createLoginCompleter(
      parameters: SampleRawLoginCompletionParameters.withIDToken,
      appID: Values.appID
    )

    urlCompleter.completeLogin { _ in }

    XCTAssertNotNil(urlCompleter.parameters.error)
    XCTAssertEqual(graphRequestFactory.capturedRequests.count, 0)
    XCTAssertNil(authenticationTokenFactory.capturedTokenString)
  }

  func testCompleteWithAuthenticationTokenWithNonce() {
    urlCompleter = createLoginCompleter(
      parameters: SampleRawLoginCompletionParameters.withIDToken,
      appID: Values.appID
    )

    let nonce = Values.nonce

    urlCompleter.completeLogin(
      nonce: nonce,
      codeVerifier: Values.codeVerifier
    ) { _ in }

    XCTAssertNil(urlCompleter.parameters.error)
    XCTAssertEqual(graphRequestFactory.capturedRequests.count, 0)
    XCTAssertEqual(
      authenticationTokenFactory.capturedTokenString,
      SampleRawLoginCompletionParameters.withIDToken[Keys.idToken] as? String,
      "Should call AuthenticationTokenFactory with the expected token string"
    )
    XCTAssertEqual(
      authenticationTokenFactory.capturedNonce,
      nonce,
      "Should call AuthenticationTokenFactory with the expected nonce"
    )
  }

  func testAuthenticationTokenCreationCompleteWithEmptyResult() {
    urlCompleter = createLoginCompleter(
      parameters: SampleRawLoginCompletionParameters.withIDToken,
      appID: Values.appID
    )

    var completionWasInvoked = false
    var capturedParameters: _LoginCompletionParameters?
    urlCompleter.completeLogin { parameters in
      capturedParameters = parameters
      completionWasInvoked = true
    }

    authenticationTokenFactory.capturedCompletion?(nil)

    XCTAssertNotNil(capturedParameters?.error)
    XCTAssertNil(capturedParameters?.authenticationToken)
    XCTAssert(
      completionWasInvoked,
      "Handler should be invoked"
    )
  }

  func testAuthenticationTokenCreationCompleteWithToken() throws {
    urlCompleter = createLoginCompleter(
      parameters: SampleRawLoginCompletionParameters.withIDToken,
      appID: Values.appID
    )
    let nonce = Values.nonce

    var completionWasInvoked = false
    var capturedParameters: _LoginCompletionParameters?
    urlCompleter.completeLogin(
      nonce: nonce,
      codeVerifier: Values.codeVerifier
    ) { parameters in
      capturedParameters = parameters
      completionWasInvoked = true
    }

    let tokenString = try XCTUnwrap(SampleRawLoginCompletionParameters.withIDToken[Keys.idToken] as? String)
    let token = AuthenticationToken(
      tokenString: tokenString,
      nonce: nonce
    )

    authenticationTokenFactory.capturedCompletion?(token)

    XCTAssertNil(urlCompleter.parameters.error)
    let capturedToken = try XCTUnwrap(capturedParameters?.authenticationToken)
    XCTAssertEqual(
      capturedToken.tokenString,
      SampleRawLoginCompletionParameters.withIDToken[Keys.idToken] as? String
    )
    XCTAssertEqual(capturedToken.nonce, nonce)
    XCTAssert(
      completionWasInvoked,
      "Handler should be invoked"
    )
  }

  func testCompleteWithAccessToken() throws {
    urlCompleter = createLoginCompleter(
      parameters: SampleRawLoginCompletionParameters.withAccessToken,
      appID: Values.appID
    )

    var completionWasInvoked = false
    var capturedParameters: _LoginCompletionParameters?
    urlCompleter.completeLogin(
      nonce: Values.nonce,
      codeVerifier: Values.codeVerifier
    ) { parameters in
      capturedParameters = parameters
      completionWasInvoked = true
    }

    let parameters = try XCTUnwrap(capturedParameters)
    verifyParameters(
      actual: parameters,
      expected: SampleRawLoginCompletionParameters.withAccessToken
    )

    XCTAssertTrue(completionWasInvoked, "Handler should be invoked")
    XCTAssertNil(urlCompleter.parameters.error)
    XCTAssertEqual(graphRequestFactory.capturedRequests.count, 0)
    XCTAssertNil(authenticationTokenFactory.capturedTokenString)
  }

  func testCompleteWithEmptyParameters() {
    urlCompleter = createLoginCompleter(parameters: [:], appID: Values.appID)

    var completionWasInvoked = false
    urlCompleter.completeLogin(
      nonce: Values.nonce,
      codeVerifier: Values.codeVerifier
    ) { _ in
      completionWasInvoked = true
    }

    XCTAssert(completionWasInvoked, "Handler should be invoked")
    XCTAssertNil(urlCompleter.parameters.error)
    XCTAssertEqual(graphRequestFactory.capturedRequests.count, 0)
    XCTAssertNil(authenticationTokenFactory.capturedTokenString)
  }

  func testErrorFromURLParametersWithEmptyParameters() throws {
    let parameters = [String: Any]()
    let error = urlCompleter.error(from: parameters)

    XCTAssertNil(error, .noErrorForEmptyParameters)
  }

  func testErrorFromURLParametersWithParameters() throws {
    let parameters = ["error_message": "foo"]
    let returnedError = try XCTUnwrap(
      urlCompleter.error(from: parameters),
      .errorForParameters
    )
    let error = try XCTUnwrap(returnedError as NSError, .errorForParameters)

    XCTAssertEqual(error.code, 8, .errorCodeMatches)
    XCTAssertEqual(error.domain, "com.facebook.sdk.core", .errorDomainMatches)
    XCTAssertFalse(error.userInfo.isEmpty, .hasNoValuesForUserInfo)
  }

  // MARK: Profile

  func testCreateProfileWithClaims() throws {
    let factory = TestProfileFactory(stubbedProfile: SampleUserProfiles.createValid())
    let parameters = SampleRawLoginCompletionParameters.withAccessTokenWithIDToken

    urlCompleter = createLoginCompleter(parameters: parameters, appID: Values.appID)
    LoginURLCompleter.setDependencies(
      .init(
        profileFactory: factory,
        authenticationTokenCreator: authenticationTokenFactory,
        graphRequestFactory: graphRequestFactory,
        internalUtility: internalUtility,
        errorFactory: errorFactory
      )
    )

    let claim = try XCTUnwrap(
      AuthenticationTokenClaims(
        jti: "some_jti",
        iss: "some_iss",
        aud: "some_aud",
        nonce: Values.nonce,
        exp: 1234,
        iat: 1234,
        sub: "some_sub",
        name: "some_name",
        givenName: "first",
        middleName: "middle",
        familyName: "last",
        email: "example@example.com",
        picture: "www.facebook.com",
        userFriends: ["123", "456"],
        userBirthday: "01/01/1990",
        userAgeRange: ["min": 21],
        userHometown: ["id": "112724962075996", "name": "Martinez, California"],
        userLocation: ["id": "110843418940484", "name": "Seattle, Washington"],
        userGender: "male",
        userLink: "facebook.com"
      )
    )
    _ = urlCompleter.profile(with: claim)

    XCTAssertEqual(
      factory.capturedUserID,
      claim.sub,
      "Should request a profile with the claims sub as the user identifier"
    )
    XCTAssertEqual(
      factory.capturedName,
      claim.name,
      "Should request a profile using the name from the claims"
    )
    XCTAssertEqual(
      factory.capturedFirstName,
      claim.givenName,
      "Should request a profile using the first name from the claims"
    )
    XCTAssertEqual(
      factory.capturedMiddleName,
      claim.middleName,
      "Should request a profile using the middle name from the claims"
    )
    XCTAssertEqual(
      factory.capturedLastName,
      claim.familyName,
      "Should request a profile using the last name from the claims"
    )
    XCTAssertEqual(
      factory.capturedImageURL?.absoluteString,
      claim.picture,
      "Should request an image URL from the claims"
    )
    XCTAssertEqual(
      factory.capturedEmail,
      claim.email,
      "Should request a profile using the email from the claims"
    )
    XCTAssertEqual(
      factory.capturedFriendIDs,
      claim.userFriends,
      "Should request a profile using the friend identifiers from the claims"
    )
    // @lint-ignore FBOBJCDISCOURAGEDFUNCTION
    let formatter = DateFormatter()
    formatter.dateFormat = "MM/dd/yyyy"
    let capturedBirthday = try XCTUnwrap(factory.capturedBirthday)
    XCTAssertEqual(
      formatter.string(from: capturedBirthday),
      claim.userBirthday,
      "Should request a profile using the user birthday from the claims"
    )
    let rawAgeRange = try XCTUnwrap(claim.userAgeRange)
    XCTAssertEqual(
      factory.capturedAgeRange,
      UserAgeRange(from: rawAgeRange),
      "Should request a profile using the user age range from the claims"
    )
    let rawHometownLocation = try XCTUnwrap(claim.userHometown)
    XCTAssertEqual(
      factory.capturedHometown,
      Location(from: rawHometownLocation),
      "Should request a profile using the user hometown from the claims"
    )
    let rawUserLocation = try XCTUnwrap(claim.userLocation)
    XCTAssertEqual(
      factory.capturedLocation,
      Location(from: rawUserLocation),
      "Should request a profile using the user location from the claims"
    )
    XCTAssertEqual(
      factory.capturedGender,
      claim.userGender,
      "Should request a profile using the gender from the claims"
    )
    let rawUserLink = try XCTUnwrap(claim.userLink)
    XCTAssertEqual(
      factory.capturedLinkURL,
      URL(string: rawUserLink),
      "Should request a profile using the link from the claims"
    )
    XCTAssertTrue(
      factory.capturedIsLimited,
      "Should request a profile with limited information"
    )
  }

  // MARK: - Helpers

  func createLoginCompleter(parameters: [String: Any], appID: String) -> LoginURLCompleter {
    LoginURLCompleter(
      urlParameters: parameters,
      appID: appID
    )
  }

  func verifyParameters(
    actual: _LoginCompletionParameters,
    expected: [String: Any],
    _ file: StaticString = #file,
    _ line: UInt = #line
  ) {
    XCTAssertEqual(
      actual.accessTokenString,
      expected[Keys.accessToken] as? String,
      file: file,
      line: line
    )
    XCTAssertEqual(
      actual.authenticationTokenString,
      expected[Keys.idToken] as? String,
      file: file,
      line: line
    )
    XCTAssertEqual(
      actual.appID,
      Values.appID,
      file: file,
      line: line
    )
    XCTAssertEqual(
      actual.challenge,
      SampleRawLoginCompletionParameters.fakeChallenge,
      file: file,
      line: line
    )
    if let rawGrantedPermissions = expected[Keys.grantedScopes] as? String {
      let grantedPermissions = Set(
        rawGrantedPermissions
          .split(separator: ",")
          .compactMap { FBPermission(string: String($0)) }
      )

      XCTAssertEqual(
        actual.permissions,
        grantedPermissions,
        file: file,
        line: line
      )
    }
    if let rawDeclinedPermissions = expected[Keys.deniedScopes] as? String {
      let declinedPermissions = Set(
        rawDeclinedPermissions
          .split(separator: ",")
          .compactMap { FBPermission(string: String($0)) }
      )

      XCTAssertEqual(
        actual.declinedPermissions,
        declinedPermissions,
        file: file,
        line: line
      )
    }
    XCTAssertEqual(
      actual.userID,
      expected[Keys.userID] as? String,
      file: file,
      line: line
    )
    XCTAssertEqual(
      actual.graphDomain,
      expected[Keys.graphDomain] as? String,
      file: file,
      line: line
    )

    if let expectedExpires = expected[Keys.expires] as? Double,
       let expires = actual.expirationDate?.timeIntervalSince1970 {
      XCTAssertEqual(
        expires,
        expectedExpires,
        accuracy: 100,
        file: file,
        line: line
      )
    }
    if let expectedExpiresAt = expected[Keys.expiresAt] as? Double,
       let expiresAt = actual.expirationDate?.timeIntervalSince1970 {
      XCTAssertEqual(
        expiresAt,
        expectedExpiresAt,
        accuracy: 100,
        file: file,
        line: line
      )
    }
    if let expectedExpiresIn = expected[Keys.expiresIn] as? Double,
       let expiresIn = actual.expirationDate?.timeIntervalSinceNow {
      XCTAssertEqual(
        expiresIn,
        expectedExpiresIn,
        accuracy: 100,
        file: file,
        line: line
      )
    }
    if let expectedDataAccessExpiration = expected[Keys.dataAccessExpiration] as? TimeInterval,
       let dataExpiration = actual.dataAccessExpirationDate?.timeIntervalSince1970 {
      XCTAssertEqual(
        dataExpiration,
        expectedDataAccessExpiration,
        accuracy: 100,
        file: file,
        line: line
      )
    }

    XCTAssertEqual(
      actual.nonceString,
      expected[Keys.nonce] as? String,
      file: file,
      line: line
    )
    XCTAssertNil(actual.error, file: file, line: line)
  }

  func verifyEmptyParameters(
    _ parameters: _LoginCompletionParameters,
    _ file: StaticString = #file,
    _ line: UInt = #line
  ) {
    XCTAssertNil(parameters.accessTokenString, file: file, line: line)
    XCTAssertNil(parameters.authenticationTokenString, file: file, line: line)
    XCTAssertNil(parameters.appID, file: file, line: line)
    XCTAssertNil(parameters.challenge, file: file, line: line)
    XCTAssertNil(parameters.permissions, file: file, line: line)
    XCTAssertNil(parameters.declinedPermissions, file: file, line: line)
    XCTAssertNil(parameters.userID, file: file, line: line)
    XCTAssertNil(parameters.graphDomain, file: file, line: line)
    XCTAssertNil(parameters.expirationDate, file: file, line: line)
    XCTAssertNil(parameters.dataAccessExpirationDate, file: file, line: line)
    XCTAssertNil(parameters.nonceString, file: file, line: line)
    XCTAssertNil(parameters.error, file: file, line: line)
  }
}

// swiftformat:disable extensionaccesscontrol

// MARK: - Assumptions

fileprivate extension String {
  static func defaultDependency(_ dependency: String, for type: String) -> String {
    "The LoginURLCompleter type uses \(dependency) as its \(type) dependency by default"
  }

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

  static let noErrorForEmptyParameters = "No error is returned if empty parameters are passed"
  static let errorForParameters = "An error is returned if some parameters are passed"
  static let errorCodeMatches = "The returned error code matches the error code passed"
  static let errorDomainMatches = "The returned error domain matches the expected domain"
  static let hasNoValuesForUserInfo = "User info does not contain any values"
}


/*
 * 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 FBSDKLoginKit

import FBSDKCoreKit
import FBSDKCoreKit_Basics
import TestTools
import XCTest

final class LoginUtilityTests: XCTestCase {

  enum Keys {
    static let expiresIn = "expires_in"
    static let accessToken = "access_token"
    static let grantedScopes = "granted_scopes"
    static let userID = "user_id"
    static let state = "state"
    static let deniedScopes = "denied_scopes"
    static let signedRequest = "signed_request"
    static let idToken = "id_token"
    static let errorReason = "error_reason"
    static let error = "error"
    static let errorCode = "error_code"
    static let errorDescription = "error_description"
  }

  enum Values {
    static let accessToken = "sometoken"
    static let challenge = #"{"challenge":"a%20%3Dbcdef"}"#
    static let error = "access_denied"
    static let errorCode = "200"
    static let errorReason = "user_denied"
    static let expiration = "5183949"
    static let signedRequest = "ggarbage.eyJhbGdvcml0aG0iOiJITUFDSEEyNTYiLCJjb2RlIjoid2h5bm90IiwiaXNzdWVkX2F0IjoxNDIyNTAyMDkyLCJ1c2VyX2lkIjoiMTIzIn0" // swiftlint:disable:this line_length
    static let urlEncodedChallenge = "%7B%22challenge%22%3A%22a%2520%253Dbcdef%22%7D"
    static let idToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFiY2QxMjM0In0=.eyJ1c2VyX2xvY2F0aW9uIjp7ImlkIjoiMTEwODQzNDE4OTQwNDg0IiwibmFtZSI6IlNlYXR0bGUsIFdhc2hpbmd0b24ifSwidXNlcl9saW5rIjoiaHR0cHM6XC9cL3d3dy5mYWNlYm9vay5jb20iLCJzdWIiOiIxMjM0IiwiaWF0IjoxNjM3NjkxOTUwLCJqdGkiOiJhIGp0aSBpcyBqdXN0IGFueSBzdHJpbmciLCJwaWN0dXJlIjoiaHR0cHM6XC9cL3d3dy5mYWNlYm9vay5jb21cL3NvbWVfcGljdHVyZSIsInVzZXJfYWdlX3JhbmdlIjp7Im1pbiI6MjF9LCJ1c2VyX2ZyaWVuZHMiOlsiMTIzIiwiNDU2Il0sImlzcyI6Imh0dHBzOlwvXC9mYWNlYm9vay5jb21cL2RpYWxvZ1wvb2F1dGgiLCJtaWRkbGVfbmFtZSI6Ik1pZGRsZSIsImF1ZCI6IjczOTE2Mjg0MzkiLCJmYW1pbHlfbmFtZSI6IlVzZXIiLCJuYW1lIjoiVGVzdCBVc2VyIiwidXNlcl9iaXJ0aGRheSI6IjAxXC8wMVwvMTk5MCIsIm5vbmNlIjoiZmVkY2IgPWEiLCJleHAiOjE2Mzc4NjQ4MTAsImdpdmVuX25hbWUiOiJUZXN0IiwidXNlcl9nZW5kZXIiOiJtYWxlIiwiZW1haWwiOiJlbWFpbEBlbWFpbC5jb20iLCJ1c2VyX2hvbWV0b3duIjp7ImlkIjoiMTEyNzI0OTYyMDc1OTk2IiwibmFtZSI6Ik1hcnR" // swiftlint:disable:this line_length
    static let invalidToken = "invalid_token"
    static let userID = "123"
    static let publicProfile = "public_profile"
  }

  func testQueryParamsFromLoginURLWithGrantedAndDeniedScopes() throws {
    let url = try XCTUnwrap(
      URL(string: "fb7391628439://authorize/#granted_scopes=public_profile&denied_scopes=email%2Cuser_friends&signed_request=ggarbage.eyJhbGdvcml0aG0iOiJITUFDSEEyNTYiLCJjb2RlIjoid2h5bm90IiwiaXNzdWVkX2F0IjoxNDIyNTAyMDkyLCJ1c2VyX2lkIjoiMTIzIn0&access_token=sometoken&expires_in=5183949&state=%7B%22challenge%22%3A%22a%2520%253Dbcdef%22%7D")
    )
    let parameters = try XCTUnwrap(LoginUtility.getQueryParameters(from: url) as? [String: String])

    XCTAssertEqual(parameters[Keys.expiresIn], Values.expiration)
    XCTAssertEqual(parameters[Keys.accessToken], Values.accessToken)
    XCTAssertEqual(parameters[Keys.grantedScopes], Values.publicProfile)
    XCTAssertEqual(parameters[Keys.userID], Values.userID)
    XCTAssertEqual(parameters[Keys.state], Values.challenge)
    XCTAssertEqual(parameters[Keys.deniedScopes], "email,user_friends")
    XCTAssertEqual(parameters[Keys.signedRequest], Values.signedRequest)

    XCTAssertNil(parameters[Keys.idToken])
    XCTAssertNil(parameters[Keys.errorReason])
    XCTAssertNil(parameters[Keys.error])
    XCTAssertNil(parameters[Keys.errorCode])
    XCTAssertNil(parameters[Keys.errorDescription])
  }

  func testQueryParamsFromLoginURLWithError() throws {
    let url = try XCTUnwrap(
      URL(string: "fb7391628439://authorize/?error=access_denied&error_code=200&error_description=Permissions+error&error_reason=user_denied#_=_")
    )
    let parameters = try XCTUnwrap(LoginUtility.getQueryParameters(from: url) as? [String: String])

    XCTAssertNil(parameters[Keys.expiresIn])
    XCTAssertNil(parameters[Keys.accessToken])
    XCTAssertNil(parameters[Keys.grantedScopes])
    XCTAssertNil(parameters[Keys.userID])
    XCTAssertNil(parameters[Keys.state])
    XCTAssertNil(parameters[Keys.deniedScopes])
    XCTAssertNil(parameters[Keys.signedRequest])
    XCTAssertNil(parameters[Keys.idToken])

    XCTAssertEqual(parameters[Keys.errorReason], Values.errorReason)
    XCTAssertEqual(parameters[Keys.error], Values.error)
    XCTAssertEqual(parameters[Keys.errorCode], Values.errorCode)
    XCTAssertEqual(parameters[Keys.errorDescription], "Permissions error")
  }

  func testQueryParamsFromLoginURLWithGrantedScopesWithoutDeniedScopes() throws {
    let url = try XCTUnwrap(
      URL(string: "fb7391628439://authorize/#granted_scopes=public_profile&denied_scopes=&signed_request=ggarbage.eyJhbGdvcml0aG0iOiJITUFDSEEyNTYiLCJjb2RlIjoid2h5bm90IiwiaXNzdWVkX2F0IjoxNDIyNTAyMDkyLCJ1c2VyX2lkIjoiMTIzIn0&access_token=sometoken&expires_in=5183949&state=%7B%22challenge%22%3A%22a%2520%253Dbcdef%22%7D")
    )
    let parameters = try XCTUnwrap(LoginUtility.getQueryParameters(from: url) as? [String: String])

    XCTAssertEqual(parameters[Keys.expiresIn], Values.expiration)
    XCTAssertEqual(parameters[Keys.accessToken], Values.accessToken)
    XCTAssertEqual(parameters[Keys.grantedScopes], Values.publicProfile)
    XCTAssertEqual(parameters[Keys.userID], Values.userID)
    XCTAssertEqual(parameters[Keys.state], Values.challenge)
    XCTAssertEqual(parameters[Keys.deniedScopes], "")
    XCTAssertEqual(parameters[Keys.signedRequest], Values.signedRequest)

    XCTAssertNil(parameters[Keys.idToken])
    XCTAssertNil(parameters[Keys.errorReason])
    XCTAssertNil(parameters[Keys.error])
    XCTAssertNil(parameters[Keys.errorCode])
    XCTAssertNil(parameters[Keys.errorDescription])
  }

  func testQueryParamsFromLoginURLWithGrantedScopesWithIDToken() throws {
    let url = try XCTUnwrap(
      URL(string: "fb7391628439://authorize/#granted_scopes=public_profile,email,user_friends,user_birthday,user_age_range,user_hometown,user_location,user_gender,user_link&id_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFiY2QxMjM0In0=.eyJ1c2VyX2xvY2F0aW9uIjp7ImlkIjoiMTEwODQzNDE4OTQwNDg0IiwibmFtZSI6IlNlYXR0bGUsIFdhc2hpbmd0b24ifSwidXNlcl9saW5rIjoiaHR0cHM6XC9cL3d3dy5mYWNlYm9vay5jb20iLCJzdWIiOiIxMjM0IiwiaWF0IjoxNjM3NjkxOTUwLCJqdGkiOiJhIGp0aSBpcyBqdXN0IGFueSBzdHJpbmciLCJwaWN0dXJlIjoiaHR0cHM6XC9cL3d3dy5mYWNlYm9vay5jb21cL3NvbWVfcGljdHVyZSIsInVzZXJfYWdlX3JhbmdlIjp7Im1pbiI6MjF9LCJ1c2VyX2ZyaWVuZHMiOlsiMTIzIiwiNDU2Il0sImlzcyI6Imh0dHBzOlwvXC9mYWNlYm9vay5jb21cL2RpYWxvZ1wvb2F1dGgiLCJtaWRkbGVfbmFtZSI6Ik1pZGRsZSIsImF1ZCI6IjczOTE2Mjg0MzkiLCJmYW1pbHlfbmFtZSI6IlVzZXIiLCJuYW1lIjoiVGVzdCBVc2VyIiwidXNlcl9iaXJ0aGRheSI6IjAxXC8wMVwvMTk5MCIsIm5vbmNlIjoiZmVkY2IgPWEiLCJleHAiOjE2Mzc4NjQ4MTAsImdpdmVuX25hbWUiOiJUZXN0IiwidXNlcl9nZW5kZXIiOiJtYWxlIiwiZW1haWwiOiJlbWFpbEBlbWFpbC5jb20iLCJ1c2VyX2hvbWV0b3duIjp7ImlkIjoiMTEyNzI0OTYyMDc1OTk2IiwibmFtZSI6Ik1hcnR") // swiftlint:disable:this line_length
    )
    let parameters = try XCTUnwrap(LoginUtility.getQueryParameters(from: url) as? [String: String])

    XCTAssertEqual(
      parameters[Keys.grantedScopes],
      "public_profile,email,user_friends,user_birthday,user_age_range,user_hometown,user_location,user_gender,user_link"
    )
    XCTAssertEqual(parameters[Keys.idToken], Values.idToken)

    XCTAssertNil(parameters[Keys.expiresIn])
    XCTAssertNil(parameters[Keys.accessToken])
    XCTAssertNil(parameters[Keys.userID])
    XCTAssertNil(parameters[Keys.state])
    XCTAssertNil(parameters[Keys.deniedScopes])
    XCTAssertNil(parameters[Keys.signedRequest])
    XCTAssertNil(parameters[Keys.errorReason])
    XCTAssertNil(parameters[Keys.error])
    XCTAssertNil(parameters[Keys.errorCode])
    XCTAssertNil(parameters[Keys.errorDescription])
  }

  func testQueryParamsFromLoginURLWithGrantedScopesWithIDTokenWithAccessToken() throws {
    let url = try XCTUnwrap(
      URL(string: "fb7391628439://authorize/#granted_scopes=public_profile%2Cemail%2Cuser_friends&signed_request=ggarbage.eyJhbGdvcml0aG0iOiJITUFDSEEyNTYiLCJjb2RlIjoid2h5bm90IiwiaXNzdWVkX2F0IjoxNDIyNTAyMDkyLCJ1c2VyX2lkIjoiMTIzIn0&access_token=sometoken&expires_in=5183949&id_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFiY2QxMjM0In0=.eyJ1c2VyX2xvY2F0aW9uIjp7ImlkIjoiMTEwODQzNDE4OTQwNDg0IiwibmFtZSI6IlNlYXR0bGUsIFdhc2hpbmd0b24ifSwidXNlcl9saW5rIjoiaHR0cHM6XC9cL3d3dy5mYWNlYm9vay5jb20iLCJzdWIiOiIxMjM0IiwiaWF0IjoxNjM3NjkxOTUwLCJqdGkiOiJhIGp0aSBpcyBqdXN0IGFueSBzdHJpbmciLCJwaWN0dXJlIjoiaHR0cHM6XC9cL3d3dy5mYWNlYm9vay5jb21cL3NvbWVfcGljdHVyZSIsInVzZXJfYWdlX3JhbmdlIjp7Im1pbiI6MjF9LCJ1c2VyX2ZyaWVuZHMiOlsiMTIzIiwiNDU2Il0sImlzcyI6Imh0dHBzOlwvXC9mYWNlYm9vay5jb21cL2RpYWxvZ1wvb2F1dGgiLCJtaWRkbGVfbmFtZSI6Ik1pZGRsZSIsImF1ZCI6IjczOTE2Mjg0MzkiLCJmYW1pbHlfbmFtZSI6IlVzZXIiLCJuYW1lIjoiVGVzdCBVc2VyIiwidXNlcl9iaXJ0aGRheSI6IjAxXC8wMVwvMTk5MCIsIm5vbmNlIjoiZmVkY2IgPWEiLCJleHAiOjE2Mzc4NjQ4MTAsImdpdmVuX25hbWUiOiJUZXN0IiwidXNlcl9nZW5kZXIiOiJtYWxlIiwiZW1haWwiOiJlbWFpbEBlbWFpbC5jb20iLCJ1c2VyX2hvbWV0b3duIjp7ImlkIjoiMTEyNzI0OTYyMDc1OTk2IiwibmFtZSI6Ik1hcnR") // swiftlint:disable:this line_length
    )
    let parameters = try XCTUnwrap(LoginUtility.getQueryParameters(from: url) as? [String: String])

    XCTAssertEqual(parameters[Keys.idToken], Values.idToken)
    XCTAssertEqual(parameters[Keys.expiresIn], Values.expiration)
    XCTAssertEqual(parameters[Keys.accessToken], Values.accessToken)
    XCTAssertEqual(parameters[Keys.grantedScopes], "public_profile,email,user_friends")
    XCTAssertEqual(parameters[Keys.userID], Values.userID)
    XCTAssertEqual(parameters[Keys.signedRequest], Values.signedRequest)

    XCTAssertNil(parameters[Keys.state])
    XCTAssertNil(parameters[Keys.deniedScopes])
    XCTAssertNil(parameters[Keys.errorReason])
    XCTAssertNil(parameters[Keys.error])
    XCTAssertNil(parameters[Keys.errorCode])
    XCTAssertNil(parameters[Keys.errorDescription])
  }

  func testQueryParamsFromLoginURLWithInvalidToken() throws {
    let url = try XCTUnwrap(
      URL(string: "fb7391628439://authorize/#id_token=invalid_token&state=%7B%22challenge%22%3A%22a%2520%253Dbcdef%22%7D")
    )
    let parameters = try XCTUnwrap(LoginUtility.getQueryParameters(from: url) as? [String: String])

    XCTAssertEqual(parameters[Keys.state], Values.challenge)
    XCTAssertEqual(parameters[Keys.idToken], Values.invalidToken)

    XCTAssertNil(parameters[Keys.expiresIn])
    XCTAssertNil(parameters[Keys.accessToken])
    XCTAssertNil(parameters[Keys.grantedScopes])
    XCTAssertNil(parameters[Keys.userID])
    XCTAssertNil(parameters[Keys.deniedScopes])
    XCTAssertNil(parameters[Keys.signedRequest])
    XCTAssertNil(parameters[Keys.errorReason])
    XCTAssertNil(parameters[Keys.error])
    XCTAssertNil(parameters[Keys.errorCode])
    XCTAssertNil(parameters[Keys.errorDescription])
  }

  func testQueryParamsFromLoginURLWithDeniedScopesWithoutGrantedScopes() throws {
    let url = try XCTUnwrap(
      URL(string: "fb7391628439://authorize/#denied_scopes=user_friends,user_likes&signed_request=ggarbage.eyJhbGdvcml0aG0iOiJITUFDSEEyNTYiLCJjb2RlIjoid2h5bm90IiwiaXNzdWVkX2F0IjoxNDIyNTAyMDkyLCJ1c2VyX2lkIjoiMTIzIn0&access_token=sometoken&expires_in=5183949&state=%7B%22challenge%22%3A%22a%2520%253Dbcdef%22%7D")
    )
    let parameters = try XCTUnwrap(LoginUtility.getQueryParameters(from: url) as? [String: String])

    XCTAssertEqual(parameters[Keys.state], Values.challenge)

    XCTAssertEqual(parameters[Keys.expiresIn], Values.expiration)
    XCTAssertEqual(parameters[Keys.accessToken], Values.accessToken)
    XCTAssertEqual(parameters[Keys.signedRequest], Values.signedRequest)
    XCTAssertEqual(parameters[Keys.userID], Values.userID)
    XCTAssertEqual(parameters[Keys.deniedScopes], "user_friends,user_likes")

    XCTAssertNil(parameters[Keys.grantedScopes])
    XCTAssertNil(parameters[Keys.idToken])
    XCTAssertNil(parameters[Keys.errorReason])
    XCTAssertNil(parameters[Keys.error])
    XCTAssertNil(parameters[Keys.errorCode])
    XCTAssertNil(parameters[Keys.errorDescription])
  }

  func testQueryParamsFromLoginURLWithoutFacebookDomainWithoutAuthorizeHost() throws {
    let url = try XCTUnwrap(
      URL(string: "test://test?granted_scopes=public_profile&access_token=sometoken&expires_in=5183949")
    )
    XCTAssertNil(LoginUtility.getQueryParameters(from: url))
  }

  func testQueryParamsFromLoginURLWithoutFacebookDomainWithAuthorizeHost() throws {
    let url = try XCTUnwrap(
      URL(string: "test://authorize?granted_scopes=public_profile&access_token=sometoken&expires_in=5183949")
    )
    let parameters = try XCTUnwrap(LoginUtility.getQueryParameters(from: url) as? [String: String])

    XCTAssertNil(parameters[Keys.state])

    XCTAssertEqual(parameters[Keys.expiresIn], Values.expiration)
    XCTAssertEqual(parameters[Keys.accessToken], Values.accessToken)
    XCTAssertEqual(parameters[Keys.grantedScopes], Values.publicProfile)

    XCTAssertNil(parameters[Keys.signedRequest])
    XCTAssertNil(parameters[Keys.userID])
    XCTAssertNil(parameters[Keys.deniedScopes])
    XCTAssertNil(parameters[Keys.idToken])
    XCTAssertNil(parameters[Keys.errorReason])
    XCTAssertNil(parameters[Keys.error])
    XCTAssertNil(parameters[Keys.errorCode])
    XCTAssertNil(parameters[Keys.errorDescription])
  }

  func testStringForAudienceOnlyMe() {
    let audienceString = LoginUtility.stringForAudience(.onlyMe)
    XCTAssertEqual(audienceString, "only_me", .providesStringForAudience)
  }

  func testStringForAudienceFriends() {
    let audienceString = LoginUtility.stringForAudience(.friends)
    XCTAssertEqual(audienceString, "friends", .providesStringForAudience)
  }

  func testStringForAudienceEveryone() {
    let audienceString = LoginUtility.stringForAudience(.everyone)
    XCTAssertEqual(audienceString, "everyone", .providesStringForAudience)
  }
}

// swiftformat:disable extensionaccesscontrol

// MARK: - Assumptions

fileprivate extension String {
  static let providesStringForAudience = "Utility provides strings for default audience values"
}


/*
 * 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 FBSDKLoginKit

import XCTest

final class NonceTests: XCTestCase {

  func testInvalidNonces() {
    [
      "",
      "foo bar",
    ].forEach { nonce in
      XCTAssertFalse(
        NonceValidator.isValid(nonce: nonce),
        "Should not consider: \(nonce) to be a valid nonce"
      )
    }
  }

  func testValidNonces() {
    [
      "123",
      "foo",
      "asdfasdfasdfasdfasdfasdfasdf",
    ].forEach { nonce in
      XCTAssertTrue(
        NonceValidator.isValid(nonce: nonce),
        "Should consider: \(nonce) to be a valid nonce"
      )
    }
  }
}


/*
 * 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 FBSDKLoginKit

import TestTools
import XCTest

final class ProfileFactoryTests: XCTestCase {

  // swiftlint:disable implicitly_unwrapped_optional
  var sampleProfile: Profile!
  var factory: ProfileFactory!
  // swiftlint:enable implicitly_unwrapped_optional

  override func setUp() {
    super.setUp()

    sampleProfile = SampleUserProfiles.createValid(isLimited: true)
    factory = ProfileFactory()
  }

  override func tearDown() {
    sampleProfile = nil
    factory = nil

    super.tearDown()
  }

  func testCreatingProfile() {
    let profile = factory.createProfile(
      userID: sampleProfile.userID,
      firstName: sampleProfile.firstName,
      middleName: sampleProfile.middleName,
      lastName: sampleProfile.lastName,
      name: sampleProfile.name,
      linkURL: sampleProfile.linkURL,
      refreshDate: sampleProfile.refreshDate,
      imageURL: sampleProfile.imageURL,
      email: sampleProfile.email,
      friendIDs: sampleProfile.friendIDs,
      birthday: sampleProfile.birthday,
      ageRange: sampleProfile.ageRange,
      hometown: sampleProfile.hometown,
      location: sampleProfile.location,
      gender: sampleProfile.gender,
      isLimited: true
    )

    XCTAssertEqual(profile.userID, sampleProfile.userID)
    XCTAssertEqual(profile.firstName, sampleProfile.firstName)
    XCTAssertEqual(profile.middleName, sampleProfile.middleName)
    XCTAssertEqual(profile.lastName, sampleProfile.lastName)
    XCTAssertEqual(profile.name, sampleProfile.name)
    XCTAssertEqual(profile.linkURL, sampleProfile.linkURL)
    XCTAssertEqual(profile.refreshDate, sampleProfile.refreshDate)
    XCTAssertEqual(profile.imageURL, sampleProfile.imageURL)
    XCTAssertEqual(profile.email, sampleProfile.email)
    XCTAssertEqual(profile.friendIDs, sampleProfile.friendIDs)
    XCTAssertEqual(profile.birthday, sampleProfile.birthday)
    XCTAssertEqual(profile.ageRange, sampleProfile.ageRange)
    XCTAssertEqual(profile.hometown, sampleProfile.hometown)
    XCTAssertEqual(profile.location, sampleProfile.location)
    XCTAssertEqual(profile.gender, sampleProfile.gender)
  }
}


/*
 * 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
import Foundation

@objcMembers
final class TestAppEvents: AppEvents {
  var capturedParameters: [AppEvents.ParameterName: Any]?

  override func logInternalEvent(
    _ eventName: AppEvents.Name,
    parameters: [AppEvents.ParameterName: Any]?,
    isImplicitlyLogged: Bool
  ) {
    capturedParameters = parameters
  }
}


/*
 * 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
import Foundation

@objcMembers
final class TestLoggerFactory: NSObject, _LoggerCreating {

  func createLogger(withLoggingBehavior loggingBehavior: LoggingBehavior) -> Logging {
    TestLogger(loggingBehavior: .developerErrors)
  }
}


/*
 * 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 FBSDKLoginKit
import Foundation

@objcMembers
final class TestLoginCompleter: NSObject, LoginCompleting {

  var capturedCompletionHandler: LoginCompletionParametersBlock?
  var capturedNonce: String?
  var capturedCodeVerifier: String?

  func completeLogin(handler: @escaping LoginCompletionParametersBlock) {
    capturedCompletionHandler = handler
  }

  func completeLogin(
    nonce: String?,
    codeVerifier: String?,
    handler: @escaping LoginCompletionParametersBlock
  ) {
    capturedCompletionHandler = handler
    capturedNonce = nonce
    capturedCodeVerifier = codeVerifier
  }
}


/*
 * 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 FBSDKLoginKit

final class TestLoginCompleterFactory: LoginCompleterFactoryProtocol {

  let stubbedLoginCompleter: TestLoginCompleter
  var capturedURLParameters = [String: Any]()
  var capturedAppID: String?

  init(stubbedLoginCompleter: TestLoginCompleter = TestLoginCompleter()) {
    self.stubbedLoginCompleter = stubbedLoginCompleter
  }

  func createLoginCompleter(
    urlParameters parameters: [String: Any],
    appID: String
  ) -> LoginCompleting {
    capturedURLParameters = parameters
    capturedAppID = appID

    return stubbedLoginCompleter
  }
}


/*
 * 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 FBSDKLoginKit

import XCTest

final class TooltipViewTests: XCTestCase {
  // swiftlint:disable implicitly_unwrapped_optional
  var tooltipView: FBTooltipView!
  // swiftlint:enable implicitly_unwrapped_optional

  override func setUp() {
    super.setUp()
    tooltipView = FBTooltipView()
  }

  override func tearDown() {
    tooltipView = nil
    super.tearDown()
  }

  func testTooltipCreation() {
    let tagline = "foo"
    let message = "bar"
    tooltipView = FBTooltipView(tagline: tagline, message: message, colorStyle: .friendlyBlue)

    XCTAssertEqual(tooltipView.message, message, .createsWithParameters)
    XCTAssertEqual(tooltipView.tagline, tagline, .createsWithParameters)
    XCTAssertEqual(tooltipView.colorStyle, .friendlyBlue, .createsWithParameters)
  }

  // MARK: - Tooltip presentation

  func testPresentFromView() {
    let window = UIWindow()
    let rootVC = UIViewController()
    window.rootViewController = rootVC
    window.makeKeyAndVisible()
    tooltipView.present(from: rootVC.view)

    if !rootVC.view.subviews.contains(where: { $0 is FBTooltipView }) {
      XCTFail(.showsTooltip)
    }
  }

  func testPresentInViewWithArrowPositionDirection() {
    let window = UIWindow()
    let rootVC = UIViewController()
    window.rootViewController = rootVC
    window.makeKeyAndVisible()
    tooltipView.present(in: rootVC.view, arrowPosition: .zero, direction: .down)

    if !rootVC.view.subviews.contains(where: { $0 is FBTooltipView }) {
      XCTFail(.showsTooltip)
    }
  }

  func testAnimateFadeIn() {
    tooltipView.isHidden = true
    tooltipView.animateFadeIn()
    XCTAssertFalse(tooltipView.isHidden, .showsTooltip)
  }

  // MARK: - Tooltip Style

  func testSetMessage() throws {
    let tagline = "myTagline"
    let oldMessage = "myOldMessage"

    tooltipView = FBTooltipView(tagline: tagline, message: oldMessage, colorStyle: .friendlyBlue)

    XCTAssertEqual(tooltipView.message, oldMessage, .createsWithParameters)
    XCTAssertEqual(tooltipView.tagline, tagline, .createsWithParameters)

    let newMessage = "myNewMessage"
    tooltipView.message = newMessage

    XCTAssertEqual(tooltipView.textLabel.text, "\(tagline) \(newMessage)", .canChangeMessage)
  }

  func testSetTagline() throws {
    let oldTagline = "myOldTagline"
    let message = "myMessage"

    tooltipView = FBTooltipView(tagline: oldTagline, message: message, colorStyle: .friendlyBlue)

    XCTAssertEqual(tooltipView.message, message, .createsWithParameters)
    XCTAssertEqual(tooltipView.tagline, oldTagline, .createsWithParameters)

    let newTagline = "myNewTagline"
    tooltipView.tagline = newTagline

    XCTAssertEqual(tooltipView.textLabel.text, "\(newTagline) \(message)", .canChangeTagline)
  }

  func testSetColorStyle() throws {
    let tagline = "foo"
    let message = "bar"
    tooltipView = FBTooltipView(tagline: tagline, message: message, colorStyle: .friendlyBlue)
    tooltipView.colorStyle = .neutralGray
    XCTAssertEqual(tooltipView.colorStyle, .neutralGray, .canChangeColorStyle)

    XCTAssertEqual(tooltipView.textLabel.textColor, .white, .setsLabelColor)
  }
}

// swiftformat:disable extensionaccesscontrol

// MARK: - Assumptions

fileprivate extension String {
  static let showsTooltip = "Can properly show the tooltip"
  static let createsWithParameters = "Creates a tooltip instance with the provided parameters"
  static let canChangeMessage = "Can properly change tooltip message with a new one"
  static let canChangeTagline = "Can properly change tooltip tagline with a new one"
  static let canChangeColorStyle = "Can properly change tooltip color style with a new one"
  static let setsLabelColor = "Sets tooltip text label color"
}


/*
 * 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

enum SampleRawLoginCompletionParameters {

  static let secondsInDay = 60 * 60 * 24
  static let daysUntilExpiration = 60
  static let daysUntilDataExpiration = 90
  static let dataExpirationDate = Date().timeIntervalSince1970.advanced(by: Double(secondsInDay * daysUntilDataExpiration)) // swiftlint:disable:this line_length
  static let expirationDate = Date().timeIntervalSince1970.advanced(by: Double(secondsInDay * daysUntilExpiration))
  static let fakeChallenge = "some_challenge"
  static let defaultDomain = "facebook"
  static let defaultState = "{\"challenge\":\"some_challenge\"}"

  static let defaultParameters: [String: Any] = [
    "access_token": "some_access_token",
    "id_token": "some_id_token",
    "nonce": "some_nonce",
    "granted_scopes": "public_profile,openid",
    "denied_scopes": "email",
    "signed_request": "some_signed_request",
    "user_id": "123",
    "expires": expirationDate,
    "expires_at": expirationDate,
    "expires_in": secondsInDay * 60,
    "data_access_expiration_time": dataExpirationDate,
    "state": "{\"challenge\":\"\(fakeChallenge)\"}",
    "graph_domain": "facebook",
    "error": "some_error",
    "error_message": "some_error_message",
    "code": "some_code",
  ]

  static var withAccessToken: [String: Any] {
    createParameters(withKeys: [
      "access_token",
      "granted_scopes",
      "denied_scopes",
      "signed_request",
      "user_id",
      "expires",
      "expires_at",
      "expires_in",
      "data_access_expiration_time",
      "state",
      "graph_domain",
    ])
  }

  static var withAccessTokenWithIDToken: [String: Any] {
    createParameters(withKeys: [
      "access_token",
      "id_token",
      "granted_scopes",
      "denied_scopes",
      "signed_request",
      "user_id",
      "expires",
      "expires_at",
      "expires_in",
      "data_access_expiration_time",
      "state",
      "graph_domain",
    ])
  }

  static var withNonce: [String: Any] {
    createParameters(withKeys: [
      "nonce",
      "user_id",
      "state",
      "graph_domain",
    ])
  }

  static var withCode: [String: Any] {
    createParameters(withKeys: [
      "code",
      "user_id",
      "state",
      "graph_domain",
    ])
  }

  static var withIDToken: [String: Any] {
    createParameters(withKeys: [
      "id_token",
      "granted_scopes",
      "denied_scopes",
      "user_id",
      "state",
      "graph_domain",
    ])
  }

  static var withoutAccessTokenWithoutIDTokenWithoutCode: [String: Any] {
    createParameters(withKeys: [
      "granted_scopes",
      "denied_scopes",
      "signed_request",
      "user_id",
      "expires",
      "expires_at",
      "expires_in",
      "data_access_expiration_time",
      "state",
      "graph_domain",
    ])
  }

  static var withEmptyStrings = [
    "access_token": "",
    "id_token": "",
    "nonce": "",
    "code": "",
  ]

  static var withStringExpirations = [
    "access_token": "some_access_token",
    "user_id": "123",
    "expires_in": String(secondsInDay * 60),
    "data_access_expiration_time": String(dataExpirationDate),
    "state": defaultState,
    "graph_domain": defaultDomain,
  ]

  static var withError: [String: Any] {
    createParameters(withKeys: [
      "user_id",
      "state",
      "graph_domain",
      "error",
      "error_message",
    ])
  }

  static func createParameters(withKeys keys: [String]) -> [String: Any] {
    var parameters: [String: Any] = [:]
    keys.forEach { key in
      parameters[key] = defaultParameters[key]
    }
    return parameters
  }
}


/*
 * 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 FBSDKLoginKit

final class TestAuthenticationTokenFactory: AuthenticationTokenCreating {
  var capturedTokenString: String?
  var capturedNonce: String?
  var capturedCompletion: AuthenticationTokenBlock?

  func createToken(
    tokenString: String,
    nonce: String,
    graphDomain: String,
    completion: @escaping AuthenticationTokenBlock
  ) {
    capturedTokenString = tokenString
    capturedNonce = nonce
    capturedCompletion = completion
  }
}


/*
 * 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 TestDeviceLoginManagerDelegate: NSObject, DeviceLoginManagerDelegate {
  var capturedLoginManager: DeviceLoginManager?
  var capturedCodeInfo: DeviceLoginCodeInfo?
  var capturedResult: DeviceLoginManagerResult?
  var capturedError: Error?

  func deviceLoginManager(
    _ loginManager: DeviceLoginManager,
    startedWith codeInfo: DeviceLoginCodeInfo
  ) {
    capturedLoginManager = loginManager
    capturedCodeInfo = codeInfo
  }

  func deviceLoginManager(
    _ loginManager: DeviceLoginManager,
    completedWith result: DeviceLoginManagerResult?,
    error: Error?
  ) {
    capturedLoginManager = loginManager
    capturedResult = result
    capturedError = error
  }
}


/*
 * 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 FBSDKLoginKit

final class TestDevicePoller: DevicePolling {
  var capturedInterval: UInt = 0

  func schedule(interval: UInt, block: @escaping () -> Void) {
    capturedInterval = interval
    block()
  }
}


/*
 * 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
import XCTest

@objcMembers
final class TestLogger: NSObject, Logging {
  var contents = ""

  var loggingBehavior: LoggingBehavior

  required init(loggingBehavior: LoggingBehavior) {
    self.loggingBehavior = loggingBehavior
  }

  static func singleShotLogEntry(_ loggingBehavior: LoggingBehavior, logEntry: String) {}

  func logEntry(_ logEntry: String) {}
}


/*
 * 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 TestLoginButtonDelegate: NSObject, LoginButtonDelegate {
  var didLoggedOut = false
  var willLogin = false
  var capturedResult: LoginManagerLoginResult?
  var capturedError: Error?

  var shouldLogin = true

  func loginButton(_ loginButton: FBLoginButton, didCompleteWith result: LoginManagerLoginResult?, error: Error?) {
    capturedResult = result
    capturedError = error
  }

  func loginButtonDidLogOut(_ loginButton: FBLoginButton) {
    didLoggedOut = true
  }

  func loginButtonWillLogin(_ loginButton: FBLoginButton) -> Bool {
    willLogin = true
    return shouldLogin
  }
}


/*
 * 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 FBSDKLoginKit

final class TestLoginEventLogger: LoginEventLogging {
  var capturedParameters: [AppEvents.ParameterName: Any]?
  var capturedEventName: AppEvents.Name?
  var flushBehavior = AppEvents.FlushBehavior.auto

  func logInternalEvent(
    _ eventName: AppEvents.Name,
    parameters: [AppEvents.ParameterName: Any]?,
    isImplicitlyLogged: Bool
  ) {
    capturedEventName = eventName
    capturedParameters = parameters
  }

  func flush() {}
}


/*
 * 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 FBSDKLoginKit

final class TestLoginProvider: LoginProviding {

  var defaultAudience: DefaultAudience = .friends
  var capturedCompletion: LoginResultBlock?
  var capturedLegacyCompletion: LoginManagerLoginResultBlock?
  var capturedConfiguration: LoginConfiguration?
  var capturedPermissions: [String]?
  var didLogout = false

  func logIn(
    viewController: UIViewController?,
    configuration: LoginConfiguration?,
    completion: @escaping LoginResultBlock
  ) {
    capturedConfiguration = configuration
    capturedCompletion = completion
  }

  func logIn(
    permissions: [String],
    from viewController: UIViewController?,
    handler: LoginManagerLoginResultBlock?
  ) {
    capturedPermissions = permissions
    capturedLegacyCompletion = handler
  }

  func logOut() {
    didLogout = true
  }
}


/*
 * 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 FBSDKLoginKit

import FBSDKCoreKit

final class TestProfileFactory: ProfileCreating {
  var capturedUserID: UserIdentifier?
  var capturedFirstName: String?
  var capturedMiddleName: String?
  var capturedLastName: String?
  var capturedName: String?
  var capturedLinkURL: URL?
  var capturedRefreshDate: Date?
  var capturedImageURL: URL?
  var capturedEmail: String?
  var capturedFriendIDs: [String]?
  var capturedBirthday: Date?
  var capturedAgeRange: UserAgeRange?
  var capturedHometown: Location?
  var capturedLocation: Location?
  var capturedGender: String?
  var capturedIsLimited = false
  private var stubbedProfile: Profile

  init(stubbedProfile: Profile) {
    self.stubbedProfile = stubbedProfile
  }

  func createProfile( // swiftlint:disable:this function_parameter_count
    userID: UserIdentifier,
    firstName: String?,
    middleName: String?,
    lastName: String?,
    name: String?,
    linkURL: URL?,
    refreshDate: Date?,
    imageURL: URL?,
    email: String?,
    friendIDs: [String]?,
    birthday: Date?,
    ageRange: UserAgeRange?,
    hometown: Location?,
    location: Location?,
    gender: String?,
    isLimited: Bool
  ) -> Profile {
    capturedUserID = userID
    capturedFirstName = firstName
    capturedMiddleName = middleName
    capturedLastName = lastName
    capturedName = name
    capturedLinkURL = linkURL
    capturedRefreshDate = refreshDate
    capturedImageURL = imageURL
    capturedEmail = email
    capturedFriendIDs = friendIDs
    capturedBirthday = birthday
    capturedAgeRange = ageRange
    capturedHometown = hometown
    capturedLocation = location
    capturedGender = gender
    capturedIsLimited = isLimited

    return stubbedProfile
  }
}


/*
 * 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 FBSDKLoginKit

final class TestServerConfigurationProvider: ServerConfigurationProviding {
  var capturedCompletion: LoginTooltipBlock?

  func loadServerConfiguration(completion: LoginTooltipBlock?) {
    capturedCompletion = completion
  }
}


/*
 * 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 FBSDKLoginKit

final class TestUserInterfaceElementProvider: UserInterfaceElementProviding {
  var stubbedTopMostViewController: UIViewController?
  var capturedView: UIView?

  func topMostViewController() -> UIViewController? {
    stubbedTopMostViewController
  }

  func viewController(for view: UIView) -> UIViewController? {
    capturedView = view
    return stubbedTopMostViewController
  }
}


/*
 * 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 FBSDKLoginKit

final class TestUserInterfaceStringProvider: UserInterfaceStringProviding {
  var stubbedBundleForStrings: Bundle?

  var bundleForStrings: Bundle {
    stubbedBundleForStrings ?? .main
  }
}


/*
 * 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

enum ShareBridgeAPI {
  enum MethodName {
    static let camera = "camera"
    static let share = "share"
  }

  enum CompletionGesture {
    static let key = "completionGesture"
    static let cancelValue = "cancel"
  }

  enum PostIDKey {
    static let results = "postId"
    static let webParameters = "post_id"
  }
}


/*
 * 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

final class ShareBridgeAPIRequestFactory: BridgeAPIRequestCreating {

  func bridgeAPIRequest(
    with protocolType: FBSDKBridgeAPIProtocolType,
    scheme: String,
    methodName: String?,
    parameters: [String: Any]?,
    userInfo: [String: Any]?
  ) -> BridgeAPIRequestProtocol? {
    _BridgeAPIRequest(
      protocolType: protocolType,
      scheme: URLScheme(rawValue: scheme),
      methodName: methodName,
      parameters: parameters,
      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.
 */

import Foundation
import UIKit

@objc(FBSDKShareInternalURLOpening)
protocol ShareInternalURLOpening {
  func canOpenURL(_ url: URL) -> Bool
}

extension UIApplication: ShareInternalURLOpening {}


/*
 * 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 UIKit

enum ShareUIApplication {
  static let shared: ShareInternalURLOpening = {
    #if DEBUG
    TestShareInternalURLOpener()
    #else
    UIApplication.shared
    #endif
  }()
}

#if DEBUG
private final class TestShareInternalURLOpener: ShareInternalURLOpening {
  func canOpenURL(_ url: URL) -> Bool { false }
}
#endif


/*
 * 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.
 */

@dynamicMemberLookup
protocol DependentAsType {
  associatedtype TypeDependencies

  static var configuredDependencies: TypeDependencies? { get set }
  static var defaultDependencies: TypeDependencies? { get }

  static func setDependencies(_ dependencies: TypeDependencies)

  #if DEBUG
  static func resetDependencies()
  #endif
}

extension DependentAsType {
  static func setDependencies(_ dependencies: TypeDependencies) {
    configuredDependencies = dependencies
  }

  #if DEBUG
  static func resetDependencies() {
    configuredDependencies = nil
  }
  #endif

  static func getDependencies() throws -> TypeDependencies {
    guard let dependencies = configuredDependencies ?? defaultDependencies else {
      throw MissingDependenciesError(for: Self.self)
    }

    return dependencies
  }

  static subscript<Dependency>(dynamicMember keyPath: KeyPath<TypeDependencies, Dependency>) -> Dependency? {
    try? getDependencies()[keyPath: keyPath]
  }
}


/*
 * 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.
 */

struct MissingDependenciesError<Dependent>: Error, CustomStringConvertible {
  private let dependentType: Dependent.Type

  init(for dependentType: Dependent.Type) {
    self.dependentType = dependentType
  }

  var description: String {
    "The dependencies for the type '\(dependentType)' or an instance of it have not been set"
  }
}


/*
 * 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
import Foundation

/// A model for app invite
@objcMembers
@objc(FBSDKAppInviteContent)
public final class AppInviteContent: NSObject {

  /// Specifies the privacy of a group.
  @objc(FBSDKAppInviteDestination)
  public enum Destination: Int {
    /// Deliver to Facebook
    case facebook

    // Deliver to Messenger
    case messenger
  }

  /**
   A URL to a preview image that will be displayed with the app invite

   This is optional.  If you don't include it a fallback image will be used.
   */
  public var appInvitePreviewImageURL: URL?

  /// An app link target that will be used as a target when the user accept the invite.
  public var appLinkURL: URL

  /**
   Promotional code to be displayed while sending and receiving the invite.

   This is optional. This can be between 0 and 10 characters long and can contain
   alphanumeric characters only. To set a promo code, you need to set promo text.
   */
  public var promotionCode: String?

  /**
   Promotional text to be displayed while sending and receiving the invite.

   This is optional. This can be between 0 and 80 characters long and can contain
   alphanumeric and spaces only.
   */
  public var promotionText: String?

  /// Destination for the app invite.  The default value is `.facebook`.
  public var destination = Destination.facebook

  @objc(initWithAppLinkURL:)
  public init(appLinkURL: URL) {
    self.appLinkURL = appLinkURL
  }
}

// MARK: - Type Dependencies

extension AppInviteContent: DependentAsType {
  struct TypeDependencies {
    var validator: ShareValidating.Type
    var errorFactory: ErrorCreating
  }

  static var configuredDependencies: TypeDependencies?

  static var defaultDependencies: TypeDependencies? = TypeDependencies(
    validator: _ShareUtility.self,
    errorFactory: _ErrorFactory()
  )
}

// MARK: - Validation

extension AppInviteContent: SharingValidatable {

  public func validate(options bridgeOptions: ShareBridgeOptions) throws {
    let validator = try Self.getDependencies().validator
    try validator.validateNetworkURL(appLinkURL, named: "appLinkURL")

    if let url = appInvitePreviewImageURL {
      try validator.validateNetworkURL(url, named: "appInvitePreviewImageURL")
    }

    try validatePromoCode()
  }

  private func validatePromoCode() throws {
    let errorFactory = try Self.getDependencies().errorFactory

    let textIsEmpty = promotionText?.isEmpty ?? true
    let codeIsEmpty = promotionCode?.isEmpty ?? true
    guard !textIsEmpty || !codeIsEmpty else { return }

    guard let text = promotionText,
          (1 ... 80).contains(text.count)
    else {
      throw errorFactory.invalidArgumentError(
        name: "promotionText",
        value: promotionText,
        message: "Invalid value for promotionText; promotionText has to be between 1 and 80 characters long.",
        underlyingError: nil
      )
    }

    guard (promotionCode?.count ?? 0) <= 10 else {
      throw errorFactory.invalidArgumentError(
        name: "promotionCode",
        value: promotionCode,
        message: """
          Invalid value for promotionCode; promotionCode has to be between 0 and 10 characters long \
          and is required when promoCode is set.
          """,
        underlyingError: nil
      )
    }

    let allowedCharacters = CharacterSet.alphanumerics.union(.whitespaces)

    guard CharacterSet(charactersIn: text).isSubset(of: allowedCharacters) else {
      throw errorFactory.invalidArgumentError(
        name: "promotionText",
        value: text,
        message: "Invalid value for promotionText; promotionText can contain only alphanumeric characters and spaces.",
        underlyingError: nil
      )
    }

    guard let code = promotionCode else { return }

    if !CharacterSet(charactersIn: code).isSubset(of: allowedCharacters) {
      throw errorFactory.invalidArgumentError(
        name: "promotionCode",
        value: code,
        message: "Invalid value for promotionCode; promotionCode can contain only alphanumeric characters and spaces.",
        underlyingError: nil
      )
    }
  }
}


/*
 * 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.
 */

/**
 * A container of arguments for a camera effect.
 * An argument is a `String` or `[String]` identified by a `String` key.
 */
@objcMembers
@objc(FBSDKCameraEffectArguments)
public final class CameraEffectArguments: NSObject {

  private(set) var arguments = [String: Any]()

  /**
   Sets a string argument in the container.
   @param string The argument
   @param key The key for the argument
   */
  @objc(setString:forKey:)
  public func set(_ string: String?, forKey key: String) {
    arguments[key] = string
  }

  /**
   Gets a string argument from the container.
   @param key The key for the argument
   @return The string value or nil
   */
  public func string(forKey key: String) -> String? {
    arguments[key] as? String
  }

  /**
   Sets a string array argument in the container.
   @param array The array argument
   @param key The key for the argument
   */
  @objc(setArray:forKey:)
  public func set(_ array: [String]?, forKey key: String) {
    arguments[key] = array
  }

  /**
   Gets an array argument from the container.
   @param key The key for the argument
   @return The array argument
   */
  public func array(forKey key: String) -> [String]? {
    arguments[key] as? [String]
  }
}


/*
 * 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 UIKit

/**
 * A container of textures for a camera effect.
 * A texture for a camera effect is an UIImages identified by a NSString key.
 */
@objcMembers
@objc(FBSDKCameraEffectTextures)
public final class CameraEffectTextures: NSObject {

  private(set) var textures = [String: UIImage]()

  /**
   Sets the image for a texture key.
   @param image The `UIImage` for the texture
   @param key The key for the texture
   */
  @objc(setImage:forKey:)
  public func set(_ image: UIImage?, forKey key: String) {
    textures[key] = image
  }

  /**
   Gets the image for a texture key.
   @param key The key for the texture
   @return The texture `UIImage` or nil
   */
  @objc(imageForKey:)
  public func image(forKey key: String) -> UIImage? {
    textures[key]
  }
}


/*
 * 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

struct ContentFlags {
  var containsMedia: Bool
  var containsPhotos: Bool
  var containsVideos: Bool

  init(
    containsMedia: Bool = false,
    containsPhotos: Bool = false,
    containsVideos: Bool = false
  ) {
    self.containsMedia = containsMedia
    self.containsPhotos = containsPhotos
    self.containsVideos = containsVideos
  }

  var containsAllTypes: Bool {
    containsMedia && containsPhotos && containsVideos
  }

  static func |= (lhs: inout Self, rhs: Self) {
    lhs = ContentFlags(
      containsMedia: lhs.containsMedia || rhs.containsMedia,
      containsPhotos: lhs.containsPhotos || rhs.containsPhotos,
      containsVideos: lhs.containsVideos || rhs.containsVideos
    )
  }
}


/*
 * 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

/// Represents a single hashtag that can be used with the share dialog.
@objcMembers
@objc(FBSDKHashtag)
public final class Hashtag: NSObject {
  /**
   The hashtag string.
   You are responsible for making sure that `stringRepresentation` is a valid hashtag (a single '#' followed by one or more
   word characters). Invalid hashtags are ignored when sharing content. You can check validity with the`valid` property.
   @return The hashtag string
   */
  public var stringRepresentation: String

  @objc(initWithString:)
  public init(_ string: String) {
    stringRepresentation = string
  }

  public override var description: String {
    guard isValid else {
      return "Invalid hashtag '\(stringRepresentation)'"
    }

    return stringRepresentation
  }

  /**
   Tests if a hashtag is valid.
   A valid hashtag matches the regular expression "#\w+": A single '#' followed by one or more word characters.
   @return true if the hashtag is valid, false otherwise.
   */
  public var isValid: Bool {
    stringRepresentation.range(of: "^#\\w+$", options: .regularExpression) != nil
  }

  // MARK: - Equality

  public override var hash: Int {
    stringRepresentation.hash
  }

  public override func isEqual(_ object: Any?) -> Bool {
    guard let other = object as? Hashtag else { return false }

    return stringRepresentation == other.stringRepresentation
  }
}


/*
 * 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
import FBSDKCoreKit_Basics
import Foundation
import UIKit

/// A model for content to share with a Facebook camera effect.
@objcMembers
@objc(FBSDKShareCameraEffectContent)
public final class ShareCameraEffectContent: NSObject {

  /// ID of the camera effect to use.
  public var effectID = ""

  /// Arguments for the effect.
  public var effectArguments = CameraEffectArguments()

  /// Textures for the effect.
  public var effectTextures = CameraEffectTextures()

  /**
   URL for the content being shared.

   This URL will be checked for all link meta tags for linking in platform specific ways.  See documentation
   for App Links (https://developers.facebook.com/docs/applinks/)
   */
  public var contentURL: URL?

  /// Hashtag for the content being shared.
  public var hashtag: Hashtag?

  /**
   List of IDs for taggable people to tag with this content.

   See documentation for Taggable Friends
   (https://developers.facebook.com/docs/graph-api/reference/user/taggable_friends)
   */
  public var peopleIDs = [String]()

  /// The ID for a place to tag with this content.
  public var placeID: String?

  /// A value to be added to the referrer URL when a person follows a link from this shared content on feed.
  public var ref: String?

  /// For shares into Messenger, this pageID will be used to map the app to page and attach attribution to the share.
  public var pageID: String?

  /// A unique identifier for a share involving this content, useful for tracking purposes.
  public private(set) var shareUUID: String? = UUID().uuidString
}

// MARK: - Type Dependencies

extension ShareCameraEffectContent: DependentAsType {
  struct TypeDependencies {
    var internalUtility: InternalUtilityProtocol
    var errorFactory: ErrorCreating
  }

  static var configuredDependencies: TypeDependencies?

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

// MARK: - Sharing

extension ShareCameraEffectContent: SharingContent {

  /**
   Adds content to an existing dictionary as key/value pairs and returns the
   updated dictionary
   @param existingParameters An immutable dictionary of existing values
   @param bridgeOptions The options for bridging
   @return A new dictionary with the modified contents
   */
  @objc(addParameters:bridgeOptions:)
  public func addParameters(
    _ existingParameters: [String: Any],
    options bridgeOptions: ShareBridgeOptions
  ) -> [String: Any] {
    var updatedParameters = existingParameters
    updatedParameters["effect_id"] = effectID

    if let jsonString = try? BasicUtility.jsonString(
      for: effectArguments.arguments,
      invalidObjectHandler: nil
    ) {
      updatedParameters["effect_arguments"] = jsonString
    }

    // Convert the entire textures dictionary into one data instance, because
    // the existing API protocol only allows one value to be put into the pasteboard.

    // Convert UIImages to NSData, because UIImage is not archivable.
    let textureImageData = effectTextures.textures.compactMapValues(UIImage.pngData)

    if let serializedTextureImages = try? NSKeyedArchiver.archivedData(
      withRootObject: textureImageData,
      requiringSecureCoding: true
    ) {
      updatedParameters["effect_textures"] = serializedTextureImages
    }

    return updatedParameters
  }

  @objc(validateWithOptions:error:)
  public func validate(options bridgeOptions: ShareBridgeOptions) throws {
    guard !effectID.isEmpty else { return }

    if !CharacterSet(charactersIn: effectID).isSubset(of: .decimalDigits) {
      let errorFactory = try Self.getDependencies().errorFactory
      throw errorFactory.invalidArgumentError(
        name: "effectID",
        value: effectID,
        message: "Invalid value for effectID, effectID can contain only numerical characters.",
        underlyingError: nil
      )
    }
  }
}


/*
 * 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

/// A model for status and link content to be shared.
@objcMembers
@objc(FBSDKShareLinkContent)
public final class ShareLinkContent: NSObject {

  /**
   Some quote text of the link.

   If specified, the quote text will render with custom styling on top of the link.
   */
  public var quote: String?

  /**
   URL for the content being shared.

   This URL will be checked for all link meta tags for linking in platform specific ways.  See documentation
   for App Links (https://developers.facebook.com/docs/applinks/)
   */
  public var contentURL: URL?

  /// Hashtag for the content being shared.
  public var hashtag: Hashtag?

  /**
   List of IDs for taggable people to tag with this content.

   See documentation for Taggable Friends
   (https://developers.facebook.com/docs/graph-api/reference/user/taggable_friends)
   */
  public var peopleIDs = [String]()

  /// The ID for a place to tag with this content.
  public var placeID: String?

  /// A value to be added to the referrer URL when a person follows a link from this shared content on feed.
  public var ref: String?

  /// For shares into Messenger, this pageID will be used to map the app to page and attach attribution to the share.
  public var pageID: String?

  /// A unique identifier for a share involving this content, useful for tracking purposes.
  public let shareUUID: String? = UUID().uuidString
}

extension ShareLinkContent: SharingContent {
  /**
   Adds content to an existing dictionary as key/value pairs and returns the
   updated dictionary
   @param existingParameters An immutable dictionary of existing values
   @param bridgeOptions The options for bridging
   @return A new dictionary with the modified contents
   */
  @objc(addParameters:bridgeOptions:)
  public func addParameters(
    _ existingParameters: [String: Any],
    options bridgeOptions: ShareBridgeOptions
  ) -> [String: Any] {
    var updatedParameters = existingParameters

    if let url = contentURL {
      updatedParameters["link"] = url
      // Pass link parameter as "messenger_link" due to versioning requirements for message dialog flow.
      // We will only use the new share flow we developed if messenger_link is present, not link.
      updatedParameters["messenger_link"] = url
    }
    if let quote = quote {
      updatedParameters["quote"] = quote
    }

    return updatedParameters
  }
}

extension ShareLinkContent: SharingValidatable {
  /// Asks the receiver to validate that its content or media values are valid.
  @objc(validateWithOptions:error:)
  public func validate(options bridgeOptions: ShareBridgeOptions) throws {
    guard let url = contentURL else { return }

    try _ShareUtility.validateNetworkURL(url, named: "contentURL")
  }
}


/*
 * 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.
 */

/// A protocol for media content (photo or video) to be shared.
@objc(FBSDKShareMedia)
public protocol ShareMedia {}


/*
 * 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
import Foundation

/// A model for media content (photo or video) to be shared.
@objcMembers
@objc(FBSDKShareMediaContent)
public final class ShareMediaContent: NSObject {

  /// Media to be shared: an array of `SharePhoto` or `ShareVideo`
  public var media = [ShareMedia]()

  /**
   URL for the content being shared.

   This URL will be checked for all link meta tags for linking in platform specific ways.  See documentation
   for App Links (https://developers.facebook.com/docs/applinks/)
   */
  public var contentURL: URL?

  /// Hashtag for the content being shared.
  public var hashtag: Hashtag?

  /**
   List of IDs for taggable people to tag with this content.

   See documentation for Taggable Friends
   (https://developers.facebook.com/docs/graph-api/reference/user/taggable_friends)
   */
  public var peopleIDs = [String]()

  /// The ID for a place to tag with this content.
  public var placeID: String?

  /// A value to be added to the referrer URL when a person follows a link from this shared content on feed.
  public var ref: String?

  /// For shares into Messenger, this pageID will be used to map the app to page and attach attribution to the share.
  public var pageID: String?

  /// A unique identifier for a share involving this content, useful for tracking purposes.
  public let shareUUID: String? = UUID().uuidString
}

extension ShareMediaContent: SharingContent {
  /**
   Adds content to an existing dictionary as key/value pairs and returns the
   updated dictionary
   @param existingParameters An immutable dictionary of existing values
   @param bridgeOptions The options for bridging
   @return A new dictionary with the modified contents
   */
  @objc(addParameters:bridgeOptions:)
  public func addParameters(
    _ existingParameters: [String: Any],
    options bridgeOptions: ShareBridgeOptions
  ) -> [String: Any] {
    // ShareMediaContent is currently available via the Share extension only
    // (thus no parameterization is implemented at this time)
    existingParameters
  }
}

extension ShareMediaContent: SharingValidatable {
  /// Asks the receiver to validate that its content or media values are valid.
  @objc(validateWithOptions:error:)
  public func validate(options bridgeOptions: ShareBridgeOptions) throws {
    try _ShareUtility.validateArray(media, minCount: 1, maxCount: 20, named: "photos")

    var hasVideo = false

    try media.forEach { media in
      if let photo = media as? SharePhoto {
        do {
          try photo.validate(options: bridgeOptions)
        } catch {
          throw _ErrorFactory().invalidArgumentError(
            domain: ShareErrorDomain,
            name: "media",
            value: photo,
            message: "photos must have UIImages",
            underlyingError: nil
          )
        }
      } else if let video = media as? ShareVideo {
        guard !hasVideo else {
          throw _ErrorFactory().invalidArgumentError(
            domain: ShareErrorDomain,
            name: "media",
            value: media,
            message: "Only 1 video is allowed",
            underlyingError: nil
          )
        }

        hasVideo = true

        try _ShareUtility.validateRequiredValue(video, named: "video")
        try video.validate(options: bridgeOptions)
      } else {
        throw _ErrorFactory().invalidArgumentError(
          domain: ShareErrorDomain,
          name: "media",
          value: media,
          message: "Only FBSDKSharePhoto and FBSDKShareVideo are allowed in `media` property",
          underlyingError: nil
        )
      }
    }
  }
}