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

@testable import FBSDKCoreKit

import TestTools
import XCTest

final class ProfileTests: XCTestCase {
  // swiftlint:disable implicitly_unwrapped_optional
  var dataStore: UserDefaultsSpy!
  var notificationCenter: TestNotificationCenter!
  var settings: TestSettings!
  var urlHoster: TestURLHoster!
  var graphRequestFactory: TestGraphRequestFactory!
  var profile: Profile!
  var testGraphRequest: TestGraphRequest!
  var result: [String: Any]!
  var imageURL: URL!
  // swiftlint:enable implicitly_unwrapped_optional

  let stubbedURL = URL(string: "testProfile.com")! // swiftlint:disable:this force_unwrapping
  let accessTokenKey = "access_token"
  let pictureModeKey = "type"
  let widthKey = "width"
  let heightKey = "height"
  let sdkVersion = "100"
  let validClientToken = "Foo"

  static let validSquareSize = CGSize(width: 100, height: 100)
  static let validNonSquareSize = CGSize(width: 10, height: 20)

  override func setUp() {
    super.setUp()

    dataStore = UserDefaultsSpy()
    notificationCenter = TestNotificationCenter()
    settings = TestSettings()
    urlHoster = TestURLHoster(url: stubbedURL)
    graphRequestFactory = TestGraphRequestFactory()
    profile = SampleUserProfiles.createValid()
    testGraphRequest = TestGraphRequest()
    graphRequestFactory.stubbedGraphRequest = testGraphRequest

    result = [
      "id": profile.userID,
      "first_name": profile.firstName as Any,
      "middle_name": profile.middleName as Any,
      "last_name": profile.lastName as Any,
      "name": profile.name as Any,
      "link": profile.linkURL as Any,
      "email": profile.email as Any,
    ]

    Settings.shared.graphAPIVersion = sdkVersion
    TestAccessTokenWallet.reset()
    Profile.current = nil
    Profile.setDependencies(
      .init(
        accessTokenProvider: TestAccessTokenWallet.self,
        dataStore: dataStore,
        graphRequestFactory: graphRequestFactory,
        notificationCenter: notificationCenter,
        settings: settings,
        urlHoster: urlHoster
      )
    )
  }

  override func tearDown() {
    super.tearDown()

    testGraphRequest = nil
    dataStore = nil
    notificationCenter = nil
    settings = nil
    urlHoster = nil
    graphRequestFactory = nil

    Profile.resetDependencies()
    TestAccessTokenWallet.reset()
    Profile.current = nil

    profile = nil
  }

  private func makeImageURL(
    mode: Profile.PictureMode = .normal,
    size: CGSize = ProfileTests.validSquareSize
  ) throws {
    imageURL = try XCTUnwrap(
      profile.imageURL(
        forMode: mode,
        size: size
      )
    )
  }

  // MARK: - Type Dependencies

  func testDefaultDependencies() {
    Profile.resetDependencies()
    XCTAssertThrowsError(try Profile.getDependencies(), .defaultDependencies)
  }

  func testCustomDependencies() throws {
    let dependencies = try Profile.getDependencies()

    XCTAssertIdentical(
      dependencies.accessTokenProvider,
      TestAccessTokenWallet.self,
      .customDependency(for: "access token provider")
    )
    XCTAssertIdentical(
      dependencies.dataStore,
      dataStore,
      .customDependency(for: "data store")
    )
    XCTAssertIdentical(
      dependencies.graphRequestFactory,
      graphRequestFactory,
      .customDependency(for: "graph request factory")
    )
    XCTAssertIdentical(
      dependencies.notificationCenter,
      notificationCenter,
      .customDependency(for: "notification center")
    )
    XCTAssertIdentical(
      dependencies.settings,
      settings,
      .customDependency(for: "settings")
    )
    XCTAssertIdentical(
      dependencies.urlHoster,
      urlHoster,
      .customDependency(for: "url hoster")
    )
  }

  // MARK: - Creating Image URL

  func testCreatingImageURL() throws {
    try makeImageURL()
    XCTAssertEqual(urlHoster.capturedHostPrefix, "graph")
    XCTAssertEqual(urlHoster.capturedPath, "\(profile.userID)/picture")
    XCTAssertNotNil(urlHoster.capturedQueryParameters)
    XCTAssertEqual(imageURL, urlHoster.stubbedURL)
  }

  func testCreatingImageURLWithNoAccessTokenNoClientToken() throws {
    try makeImageURL()
    let queryItems = try XCTUnwrap(urlHoster.capturedQueryParameters)
    XCTAssertEqual(
      queryItems[pictureModeKey],
      "normal",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[widthKey],
      "100",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[heightKey],
      "100",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
  }

  func testCreatingImageURLWithClientTokenNoAccessToken() throws {
    settings.clientToken = validClientToken
    try makeImageURL()

    let queryItems = try XCTUnwrap(urlHoster.capturedQueryParameters)
    XCTAssertEqual(
      queryItems[pictureModeKey],
      "normal",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[widthKey],
      "100",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[heightKey],
      "100",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[accessTokenKey],
      "Foo",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
  }

  func testCreatingImageURLWithAccessTokenNoClientToken() throws {
    TestAccessTokenWallet.current = SampleAccessTokens.validToken
    try makeImageURL()

    let queryItems = try XCTUnwrap(urlHoster.capturedQueryParameters)
    XCTAssertEqual(
      queryItems[pictureModeKey],
      "normal",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[widthKey],
      "100",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[heightKey],
      "100",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[accessTokenKey],
      SampleAccessTokens.validToken.tokenString,
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
  }

  func testCreatingImageURLWithAccessTokenAndClientToken() throws {
    TestAccessTokenWallet.current = SampleAccessTokens.validToken
    settings.clientToken = validClientToken
    try makeImageURL()

    let queryItems = try XCTUnwrap(urlHoster.capturedQueryParameters)
    XCTAssertEqual(
      queryItems[pictureModeKey],
      "normal",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[widthKey],
      "100",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[heightKey],
      "100",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[accessTokenKey],
      SampleAccessTokens.validToken.tokenString,
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
  }

  func testCreatingEnumWithSmallMode() throws {
    try makeImageURL(mode: .small, size: Self.validNonSquareSize)

    let queryItems = try XCTUnwrap(urlHoster.capturedQueryParameters)
    XCTAssertEqual(
      queryItems[pictureModeKey],
      "small",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[widthKey],
      "10",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[heightKey],
      "20",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
  }

  func testCreatingEnumWithAlbumMode() throws {
    try makeImageURL(mode: .album, size: Self.validNonSquareSize)

    let queryItems = try XCTUnwrap(urlHoster.capturedQueryParameters)
    XCTAssertEqual(
      queryItems[pictureModeKey],
      "album",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[widthKey],
      "10",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[heightKey],
      "20",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
  }

  func testCreatingEnumWithLargeMode() throws {
    try makeImageURL(mode: .large, size: Self.validNonSquareSize)

    let queryItems = try XCTUnwrap(urlHoster.capturedQueryParameters)
    XCTAssertEqual(
      queryItems[pictureModeKey],
      "large",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[widthKey],
      "10",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[heightKey],
      "20",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
  }

  func testCreatingEnumWithSquareMode() throws {
    try makeImageURL(mode: .square, size: Self.validNonSquareSize)

    let queryItems = try XCTUnwrap(urlHoster.capturedQueryParameters)
    XCTAssertEqual(
      queryItems[pictureModeKey],
      "square",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[widthKey],
      "10",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[heightKey],
      "20",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
  }

  // MARK: - Size Validations

  func testCreatingImageURLWithNoSize() throws {
    try makeImageURL(mode: .square, size: .zero)

    let queryItems = try XCTUnwrap(urlHoster.capturedQueryParameters)
    XCTAssertEqual(
      queryItems[pictureModeKey],
      "square",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[widthKey],
      "0",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[heightKey],
      "0",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
  }

  func testCreatingSquareImageURLWithNonSquareSize() throws {
    try makeImageURL(mode: .square, size: Self.validNonSquareSize)

    let queryItems = try XCTUnwrap(urlHoster.capturedQueryParameters)
    XCTAssertEqual(
      queryItems[pictureModeKey],
      "square",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[widthKey],
      "10",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[heightKey],
      "20",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
  }

  func testCreatingSquareImageURLWithNegativeSize() throws {
    try makeImageURL(mode: .square, size: CGSize(width: -10, height: -10))

    let queryItems = try XCTUnwrap(urlHoster.capturedQueryParameters)
    XCTAssertEqual(
      queryItems[pictureModeKey],
      "square",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[widthKey],
      "-10",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
    XCTAssertEqual(
      queryItems[heightKey],
      "-10",
      "Should add the expected query items to a url when creating a url for fetching a profile image"
    )
  }

  // MARK: - Profile Loading

  func testGraphPathForProfileLoadWithLinkPermission() {
    verifyGraphParameters(
      permissions: ["user_link"],
      expectedFields: "id,first_name,middle_name,last_name,name,link"
    )
  }

  func testGraphPathForProfileLoadWithNoPermission() {
    verifyGraphParameters(
      permissions: [],
      expectedFields: "id,first_name,middle_name,last_name,name"
    )
  }

  func testGraphPathForProfileLoadWithEmailPermission() {
    verifyGraphParameters(
      permissions: ["email"],
      expectedFields: "id,first_name,middle_name,last_name,name,email"
    )
  }

  func testGraphPathForProfileLoadWithFriendsPermission() {
    verifyGraphParameters(
      permissions: ["user_friends"],
      expectedFields: "id,first_name,middle_name,last_name,name,friends"
    )
  }

  func testGraphPathForProfileLoadWithBirthdayPermission() {
    verifyGraphParameters(
      permissions: ["user_birthday"],
      expectedFields: "id,first_name,middle_name,last_name,name,birthday"
    )
  }

  func testGraphPathForProfileLoadWithAgeRangePermission() {
    verifyGraphParameters(
      permissions: ["user_age_range"],
      expectedFields: "id,first_name,middle_name,last_name,name,age_range"
    )
  }

  func testGraphPathForProfileLoadWithHometownPermission() {
    verifyGraphParameters(
      permissions: ["user_hometown"],
      expectedFields: "id,first_name,middle_name,last_name,name,hometown"
    )
  }

  func testGraphPathForProfileLoadWithLocationPermission() {
    verifyGraphParameters(
      permissions: ["user_location"],
      expectedFields: "id,first_name,middle_name,last_name,name,location"
    )
  }

  func testGraphPathForProfileLoadWithGenderPermission() {
    verifyGraphParameters(
      permissions: ["user_gender"],
      expectedFields: "id,first_name,middle_name,last_name,name,gender"
    )
  }

  func testLoadingProfile() throws {
    let formatter = DateFormatter()
    formatter.dateFormat = "MM/dd/yyyy"
    graphRequestFactory.stubbedGraphRequest = testGraphRequest
    var capturedProfile: Profile?
    var capturedError: Error?

    Profile.loadProfile(accessToken: SampleAccessTokens.validToken) {
      capturedProfile = $0
      capturedError = $1
    }

    let completion = try XCTUnwrap(testGraphRequest.capturedCompletionHandler)
    completion(nil, sampleGraphResult(), nil)
    XCTAssertNil(capturedError)
    let profile = try XCTUnwrap(capturedProfile, "capturedProfile should not be nil")
    let expectedProfile = SampleUserProfiles.createValid()
    XCTAssertEqual(profile.firstName, expectedProfile.firstName)
    XCTAssertEqual(profile.middleName, expectedProfile.middleName)
    XCTAssertEqual(profile.lastName, expectedProfile.lastName)
    XCTAssertEqual(profile.name, expectedProfile.name)
    XCTAssertEqual(profile.userID, expectedProfile.userID)
    XCTAssertEqual(profile.linkURL, expectedProfile.linkURL)
    XCTAssertEqual(profile.email, expectedProfile.email)
    XCTAssertEqual(profile.friendIDs, expectedProfile.friendIDs)
    XCTAssertEqual(
      formatter.string(from: profile.birthday ?? Date()),
      "01/01/1990"
    )
    XCTAssertEqual(profile.ageRange, expectedProfile.ageRange)
    XCTAssertEqual(profile.hometown, expectedProfile.hometown)
    XCTAssertEqual(profile.gender, expectedProfile.gender)
    XCTAssertEqual(profile.location, expectedProfile.location)
  }

  func testLoadingProfileWithInvalidLink() throws {
    result["link"] = "   "

    var capturedProfile: Profile?
    var capturedError: Error?

    Profile.loadProfile(accessToken: SampleAccessTokens.validToken) {
      capturedProfile = $0
      capturedError = $1
    }

    let completion = try XCTUnwrap(testGraphRequest.capturedCompletionHandler)
    completion(nil, result, nil)
    XCTAssertNil(capturedError)
    let profile = try XCTUnwrap(capturedProfile, "capturedProfile should not be nil")
    XCTAssertNil(profile.linkURL)
  }

  func testProfileNilWithNilAccessToken() {
    var capturedProfile: Profile?
    var capturedError: Error?

    Profile.loadProfile(accessToken: nil) {
      capturedProfile = $0
      capturedError = $1
    }
    XCTAssertEqual(
      testGraphRequest.startCallCount,
      0,
      "Should not fetch a profile if there is no access token"
    )
    XCTAssertNil(capturedProfile)
    XCTAssertNil(capturedError)
  }

  func testLoadingProfileWithNoFriends() throws {
    result["friends"] = ["data": []]
    var capturedProfile: Profile?
    var capturedError: Error?

    Profile.loadProfile(accessToken: SampleAccessTokens.validToken) {
      capturedProfile = $0
      capturedError = $1
    }

    let completion = try XCTUnwrap(testGraphRequest.capturedCompletionHandler)
    completion(nil, result, nil)
    XCTAssertNil(capturedError)
    let profile = try XCTUnwrap(capturedProfile, "capturedProfile should not be nil")
    XCTAssertNil(profile.friendIDs)
  }

  func testLoadingProfileWithInvalidFriends() throws {
    result["friends"] = "   "
    var capturedProfile: Profile?
    var capturedError: Error?

    Profile.loadProfile(accessToken: SampleAccessTokens.validToken) {
      capturedProfile = $0
      capturedError = $1
    }

    let completion = try XCTUnwrap(testGraphRequest.capturedCompletionHandler)
    completion(nil, result, nil)
    XCTAssertNil(capturedError)
    let profile = try XCTUnwrap(capturedProfile, "capturedProfile should not be nil")
    XCTAssertNil(profile.friendIDs)
  }

  func testLoadingProfileWithInvalidBirthday() throws {
    result["birthday"] = 123
    var capturedProfile: Profile?
    var capturedError: Error?

    Profile.loadProfile(accessToken: SampleAccessTokens.validToken) {
      capturedProfile = $0
      capturedError = $1
    }

    let completion = try XCTUnwrap(testGraphRequest.capturedCompletionHandler)
    completion(nil, result, nil)
    XCTAssertNil(capturedError)
    let profile = try XCTUnwrap(capturedProfile, "capturedProfile should not be nil")
    XCTAssertNil(profile.birthday)
  }

  func testLoadingProfileWithInvalidAgeRange() throws {
    result["ageRange"] = "ageRange"
    var capturedProfile: Profile?
    var capturedError: Error?

    Profile.loadProfile(accessToken: SampleAccessTokens.validToken) {
      capturedProfile = $0
      capturedError = $1
    }

    let completion = try XCTUnwrap(testGraphRequest.capturedCompletionHandler)
    completion(nil, result, nil)
    XCTAssertNil(capturedError)
    let profile = try XCTUnwrap(capturedProfile, "capturedProfile should not be nil")
    XCTAssertNil(profile.ageRange)
  }

  func testLoadingProfileWithInvalidHometown() throws {
    result["hometown"] = "hometown"
    var capturedProfile: Profile?
    var capturedError: Error?

    Profile.loadProfile(accessToken: SampleAccessTokens.validToken) {
      capturedProfile = $0
      capturedError = $1
    }

    let completion = try XCTUnwrap(testGraphRequest.capturedCompletionHandler)
    completion(nil, result, nil)
    XCTAssertNil(capturedError)
    let profile = try XCTUnwrap(capturedProfile, "capturedProfile should not be nil")
    XCTAssertNil(profile.hometown)
  }

  func testLoadingProfileWithInvalidLocation() throws {
    result["location"] = "location"
    var capturedProfile: Profile?
    var capturedError: Error?

    Profile.loadProfile(accessToken: SampleAccessTokens.validToken) {
      capturedProfile = $0
      capturedError = $1
    }

    let completion = try XCTUnwrap(testGraphRequest.capturedCompletionHandler)
    completion(nil, result, nil)
    XCTAssertNil(capturedError)
    let profile = try XCTUnwrap(capturedProfile, "capturedProfile should not be nil")
    XCTAssertNil(profile.location)
  }

  func testLoadingProfileWithInvalidGender() throws {
    result["gender"] = [:]
    var capturedProfile: Profile?
    var capturedError: Error?

    Profile.loadProfile(accessToken: SampleAccessTokens.validToken) {
      capturedProfile = $0
      capturedError = $1
    }

    let completion = try XCTUnwrap(testGraphRequest.capturedCompletionHandler)
    completion(nil, result, nil)
    XCTAssertNil(capturedError)
    let profile = try XCTUnwrap(capturedProfile, "capturedProfile should not be nil")
    XCTAssertNil(profile.gender)
  }

  func testProfileNotRefreshedIfNotStale() throws {
    Profile.current = profile
    var capturedProfile: Profile?
    var capturedError: Error?

    Profile.loadProfile(accessToken: nil) {
      capturedProfile = $0
      capturedError = $1
    }
    XCTAssertEqual(
      testGraphRequest.startCallCount,
      0,
      "Should not fetch a profile if it is not stale"
    )
    XCTAssertNil(capturedError)
    let profile = try XCTUnwrap(capturedProfile, "capturedProfile should not be nil")
    let expectedProfile = SampleUserProfiles.createValid()
    XCTAssertEqual(profile.firstName, expectedProfile.firstName)
    XCTAssertEqual(profile.middleName, expectedProfile.middleName)
    XCTAssertEqual(profile.lastName, expectedProfile.lastName)
    XCTAssertEqual(profile.name, expectedProfile.name)
    XCTAssertEqual(profile.userID, expectedProfile.userID)
    XCTAssertEqual(profile.linkURL, expectedProfile.linkURL)
    XCTAssertEqual(profile.email, expectedProfile.email)
  }

  func testLoadingProfileWithLimitedProfileWithoutToken() throws {
    let expected = SampleUserProfiles.validLimited
    Profile.current = expected
    let request = TestGraphRequest()
    var capturedProfile: Profile?
    var capturedError: Error?

    Profile.loadProfile(accessToken: nil) {
      capturedProfile = $0
      capturedError = $1
    }

    XCTAssertEqual(
      request.startCallCount,
      0,
      "Should not fetch a profile if there is no access token"
    )
    XCTAssertNil(capturedError)
    let profile = try XCTUnwrap(capturedProfile, "capturedProfile should not be nil")
    XCTAssertEqual(
      profile,
      expected,
      "Should return the current profile synchronously"
    )
  }

  func testLoadingProfileWithLimitedProfileWithToken() {
    Profile.current = SampleUserProfiles.validLimited
    Profile.loadProfile(accessToken: SampleAccessTokens.validToken) { _, _ in
      XCTFail("Should not invoke the completion")
    }
    XCTAssertEqual(
      testGraphRequest.startCallCount,
      1,
      "Should fetch a profile if it is limited and there is an access token"
    )
  }

  func testLoadingProfileWithExpiredNonLimitedProfileWithToken() {
    let expected = SampleUserProfiles.createValid(isExpired: true)
    Profile.current = expected
    Profile.loadProfile(accessToken: SampleAccessTokens.validToken) { _, _ in
      XCTFail("Should not invoke the completion")
    }
    XCTAssertEqual(
      testGraphRequest.startCallCount,
      1,
      "Should fetch a profile if it is expired and there is an access token"
    )
  }

  func testLoadingProfileWithCurrentlyLoadingProfile() {
    let expected = SampleUserProfiles.createValid(isExpired: true)
    Profile.current = expected
    let connection = TestGraphRequestConnection()
    testGraphRequest.stubbedConnection = connection
    Profile.loadProfile(accessToken: SampleAccessTokens.validToken) { _, _ in
      XCTFail("Should not invoke the completion")
    }
    Profile.loadProfile(accessToken: SampleAccessTokens.validToken) { _, _ in
      XCTFail("Should not invoke the completion")
    }
    XCTAssertEqual(
      connection.cancelCallCount,
      1,
      "Should cancel an existing connection if a new connection is started before it completes"
    )
  }

  func testProfileParseBlockReturnsNilIfResultIsEmpty() throws {
    let result: [String: String] = [:]
    Profile.loadProfile(accessToken: SampleAccessTokens.validToken, completion: nil)

    let completion = try XCTUnwrap(testGraphRequest.capturedCompletionHandler)
    completion(nil, result, nil)
    XCTAssertNil(Profile.current)
  }

  func testProfileParseBlockReturnsNilIfResultHasNoID() throws {
    let result = [
      "first_name": "firstname",
      "middle_name": "middlename",
      "last_name": "lastname",
      "name": "name",
    ]
    Profile.loadProfile(accessToken: SampleAccessTokens.validToken, completion: nil)
    let completion = try XCTUnwrap(testGraphRequest.capturedCompletionHandler)
    completion(nil, result, nil)
    XCTAssertNil(Profile.current)
  }

  func testProfileParseBlockReturnsNilIfResultHasEmptyID() throws {
    result["id"] = ""
    Profile.loadProfile(accessToken: SampleAccessTokens.validToken, completion: nil)
    let completion = try XCTUnwrap(testGraphRequest.capturedCompletionHandler)
    completion(nil, result, nil)
    XCTAssertNil(Profile.current)
  }

  func testLoadProfileWithRandomData() throws {
    for _ in 0 ..< 100 {
      let randomizedResult = Fuzzer.randomize(json: sampleGraphResult())
      var completed = false
      Profile.loadProfile(accessToken: SampleAccessTokens.validToken) { _, _ in
        completed = true
      }
      let completion = try XCTUnwrap(testGraphRequest.capturedCompletionHandler)
      completion(nil, randomizedResult, nil)
      XCTAssert(completed, "Completion handler should be invoked synchronously")
    }
  }

  // MARK: Update Notifications

  func testClearingMissingProfile() {
    Profile.current = nil
    XCTAssertTrue(
      notificationCenter.capturedPostNames.isEmpty,
      "Clearing an empty current profile should not post a notification"
    )
  }

  func testClearingProfile() {
    Profile.current = profile
    notificationCenter.capturedPostNames = []

    Profile.current = nil

    XCTAssertFalse(
      notificationCenter.capturedPostNames.isEmpty,
      "Clearing the current profile should post a notification"
    )
  }

  func testSettingProfile() {
    Profile.current = profile

    XCTAssertFalse(
      notificationCenter.capturedPostNames.isEmpty,
      "Setting the current profile to `nil` should post a notification"
    )
  }

  func testSettingSameProfile() {
    Profile.current = profile
    notificationCenter.capturedPostNames = []

    Profile.current = profile

    XCTAssertTrue(
      notificationCenter.capturedPostNames.isEmpty,
      "Setting the current profile to the same profile should not post a notification"
    )
  }

  func testUpdatingProfile() {
    Profile.current = profile
    notificationCenter.capturedPostNames = []

    let newProfile = SampleUserProfiles.createValid(
      userID: "different",
      name: "different",
      imageURL: nil,
      isExpired: true,
      isLimited: true
    )
    Profile.current = newProfile

    XCTAssertFalse(
      notificationCenter.capturedPostNames.isEmpty,
      "Replacing the current profile should post a notification"
    )
  }

  // MARK: Storage

  func testEncodingAndDecoding() throws {
    let profile = SampleUserProfiles.validLimited
    let decodedObject = try CodabilityTesting.encodeAndDecode(profile)
    // Test Properties

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

  func testDefaultDataStore() {
    Profile.resetDependencies()
    XCTAssertNil(
      Profile.dataStore,
      "Should not have a default data store"
    )
  }

  func testConfiguringWithDataStore() {
    XCTAssertTrue(
      Profile.dataStore === dataStore,
      "Should be able to set a persistent data store"
    )
  }

  func testConfiguringWithNotificationCenter() {
    XCTAssertTrue(
      Profile.notificationCenter === notificationCenter,
      "Should be able to set a Notification Posting"
    )
  }

  func testDefaultAccessTokenProvider() {
    Profile.resetDependencies()
    XCTAssertNil(
      Profile.accessTokenProvider,
      "Should not have a default access token provider"
    )
  }

  func testConfiguringWithTokenProvider() {
    XCTAssertTrue(
      Profile.accessTokenProvider is TestAccessTokenWallet.Type,
      "Should be able to set a token wallet"
    )
  }

  func testDefaultSettings() {
    Profile.resetDependencies()
    XCTAssertNil(
      Profile.settings,
      "Should not have default settings"
    )
  }

  func testConfiguringWithSettings() {
    XCTAssertTrue(
      Profile.settings === settings,
      "Should be able to set settings"
    )
  }

  func testHashability() {
    let profile = SampleUserProfiles.createValid()
    let profile2 = SampleUserProfiles.createValid(userID: name)

    XCTAssertEqual(
      profile.hash,
      profile.hash,
      "Hashed profiles should be consistent"
    )
    XCTAssertNotEqual(
      profile.hash,
      profile2.hash,
      "Hashed profiles should be unique"
    )
  }

  func testFetchingCachedProfile() {
    _ = Profile.fetchCachedProfile()

    XCTAssertEqual(
      dataStore.capturedObjectRetrievalKey,
      "com.facebook.sdk.FBSDKProfile.currentProfile",
      "Fetching a cached profile should query the data store with the expected retrieval key"
    )
  }

  func sampleGraphResult() -> [String: Any] {
    [
      "id": profile.userID,
      "first_name": profile.firstName as Any,
      "middle_name": profile.middleName as Any,
      "last_name": profile.lastName as Any,
      "name": profile.name as Any,
      "link": profile.linkURL as Any,
      "email": profile.email as Any,
      "friends": [
        "data": [
          [
            "name": "user1",
            "id": profile.friendIDs?[0],
          ],
          [
            "name": "user2",
            "id": profile.friendIDs?[1],
          ],
        ],
      ],
      "birthday": "01/01/1990",
      "age_range": [
        "min": profile.ageRange?.min,
      ],
      "hometown": [
        "id": profile.hometown?.id,
        "name": profile.hometown?.name,
      ],
      "location": [
        "id": profile.location?.id,
        "name": profile.location?.name,
      ],
      "gender": profile.gender as Any,
    ]
  }

  func verifyGraphParameters(
    permissions: [String],
    expectedFields: String,
    file: StaticString = #file,
    line: UInt = #line
  ) {
    let token = SampleAccessTokens.create(withPermissions: permissions)
    let parameters = Profile.makeGraphRequestParameters(token: token)
    XCTAssertEqual(parameters["fields"], expectedFields, file: file, line: line)
  }
}

// swiftformat:disable extensionaccesscontrol

// MARK: - Assumptions

fileprivate extension String {
  static let isCodable = "Profile should be encodable and decodable"
  static let defaultDependencies = "The Profile type does not have any default dependencies"

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


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

@testable import FBSDKCoreKit

import TestTools
import XCTest

final class SettingsTests: XCTestCase {

  // swiftlint:disable implicitly_unwrapped_optional
  var logger: TestEventLogger!
  var appEventsConfigurationProvider: TestAppEventsConfigurationProvider!
  var userDefaultsSpy: UserDefaultsSpy!
  var settings: Settings!
  var bundle: InfoDictionaryProviding!
  // swiftlint:enable implicitly_unwrapped_optional

  var testBundle: TestBundle {
    bundle as! TestBundle // swiftlint:disable:this force_cast
  }

  var userAgentSuffix = ""

  static let emptyString = ""
  static let whiteSpaceToken = "  "

  override func setUp() {
    super.setUp()

    logger = TestEventLogger()
    appEventsConfigurationProvider = TestAppEventsConfigurationProvider()
    userDefaultsSpy = UserDefaultsSpy()
    bundle = TestBundle()
    settings = Settings()
    configureSettings()
  }

  override func tearDown() {
    logger = nil
    appEventsConfigurationProvider = nil
    userDefaultsSpy = nil
    bundle = nil
    settings = nil

    super.tearDown()
  }

  private func configureSettings() {
    settings.setDependencies(
      .init(
        appEventsConfigurationProvider: appEventsConfigurationProvider,
        dataStore: userDefaultsSpy,
        eventLogger: logger,
        infoDictionaryProvider: bundle
      )
    )
  }

  func testDefaultDependencies() {
    settings.resetDependencies()
    XCTAssertThrowsError(
      _ = try settings.getDependencies(),
      "Settings instances should not have any default dependencies"
    )
  }

  func testCustomDependencies() throws {
    let dependencies = try settings.getDependencies()

    XCTAssertIdentical(
      dependencies.dataStore,
      userDefaultsSpy,
      "Should be able to configure a data store dependency"
    )
    XCTAssertIdentical(
      dependencies.appEventsConfigurationProvider,
      appEventsConfigurationProvider,
      "Should be able to configure an app events configuration provider dependency"
    )
    XCTAssertIdentical(
      dependencies.infoDictionaryProvider,
      bundle,
      "Should be able to configure an info dictionary provider dependency"
    )
    XCTAssertIdentical(
      dependencies.eventLogger,
      logger,
      "Should be able to configure an event logger dependency"
    )
  }

  // MARK: Advertiser Tracking Status

  func testFacebookAdvertiserTrackingStatusDefaultValue() {
    let configuration = TestAppEventsConfiguration(defaultATEStatus: .disallowed)
    appEventsConfigurationProvider.stubbedConfiguration = configuration

    XCTAssertEqual(
      settings.advertisingTrackingStatus,
      configuration.defaultATEStatus,
      """
      Advertiser tracking status should use the cached app events configuration
      when there is no persisted overridden value
      """
    )
    XCTAssertEqual(
      userDefaultsSpy.capturedObjectRetrievalKey,
      "com.facebook.sdk:FBSDKSettingsAdvertisingTrackingStatus",
      """
      Should attempt to retrieve the tracking status from the data userDefaultsSpy before
      checking the cached configuration
      """
    )
    XCTAssertTrue(
      appEventsConfigurationProvider.didRetrieveCachedConfiguration,
      "Should attempt to retrieve the tracking status from a cached configuration"
    )
  }

  func testGettingExplicitlySetFacebookAdvertiserTrackingStatus() {
    settings.advertisingTrackingStatus = .disallowed
    XCTAssertEqual(
      settings.advertisingTrackingStatus,
      .disallowed,
      "Should return the explicitly set tracking status"
    )
    XCTAssertNil(
      userDefaultsSpy.capturedObjectRetrievalKey,
      "Should not attempt to retrieve the tracking status from the data userDefaultsSpy"
    )
    XCTAssertFalse(
      appEventsConfigurationProvider.didRetrieveCachedConfiguration,
      "Should not attempt to retrieve the tracking status from a cached configuration"
    )
  }

  func testGettingPersistedFacebookAdvertiserTrackingStatus() {
    let key = "com.facebook.sdk:FBSDKSettingsAdvertisingTrackingStatus"
    userDefaultsSpy.set(
      NSNumber(value: AdvertisingTrackingStatus.allowed.rawValue),
      forKey: key
    )
    XCTAssertEqual(
      settings.advertisingTrackingStatus,
      .allowed,
      "Should return the tracking status from the data userDefaultsSpy"
    )
    XCTAssertEqual(
      userDefaultsSpy.capturedObjectRetrievalKey,
      key,
      "Should retrieve the tracking status from the data userDefaultsSpy"
    )
    XCTAssertFalse(
      appEventsConfigurationProvider.didRetrieveCachedConfiguration,
      "Should not attempt to retrieve the tracking status from a cached configuration"
    )
  }

  func testGettingCachedFacebookAdvertiserTrackingStatus() {
    let key = "com.facebook.sdk:FBSDKSettingsAdvertisingTrackingStatus"
    userDefaultsSpy.set(
      NSNumber(value: AdvertisingTrackingStatus.allowed.rawValue),
      forKey: key
    )
    XCTAssertEqual(
      settings.advertisingTrackingStatus,
      .allowed,
      "Should return the tracking status from the data userDefaultsSpy"
    )
    XCTAssertEqual(
      userDefaultsSpy.capturedObjectRetrievalKey,
      key,
      "Should retrieve the tracking status from the data userDefaultsSpy"
    )
    XCTAssertFalse(
      appEventsConfigurationProvider.didRetrieveCachedConfiguration,
      "Should not attempt to retrieve the tracking status from a cached configuration"
    )
  }

  func testSettingFacebookAdvertiserTrackingStatusToEnabled() {
    settings.isAdvertiserTrackingEnabled = true
    XCTAssertEqual(
      userDefaultsSpy.capturedSetObjectKey,
      "com.facebook.sdk:FBSDKSettingsSetAdvertiserTrackingEnabledTimestamp",
      "Should persist the time when the tracking status is set to enabled"
    )
  }

  func testSettingFacebookAdvertiserTrackingStatusToDisallowed() {
    settings.isAdvertiserTrackingEnabled = false
    XCTAssertEqual(
      userDefaultsSpy.capturedSetObjectKey,
      "com.facebook.sdk:FBSDKSettingsSetAdvertiserTrackingEnabledTimestamp",
      "Should persist the time when the tracking status is set to disallowed"
    )
  }

  func testSettingFacebookAdvertiserTrackingStatusToEnabledProperty() {
    settings.isAdvertiserTrackingEnabled = true

    XCTAssertTrue(
      settings.isAdvertiserTrackingEnabled,
      "Setting advertiser tracking status should be allowed"
    )
    XCTAssertEqual(
      userDefaultsSpy.capturedSetObjectKey,
      "com.facebook.sdk:FBSDKSettingsSetAdvertiserTrackingEnabledTimestamp",
      "Should persist the time when the tracking status is set to enabled"
    )
  }

  func testSettingFacebookAdvertiserTrackingStatusToDisallowedProperty() {
    settings.isAdvertiserTrackingEnabled = false

    XCTAssertFalse(
      settings.isAdvertiserTrackingEnabled,
      "Setting advertiser tracking status should be disallowed"
    )
    XCTAssertEqual(
      userDefaultsSpy.capturedSetObjectKey,
      "com.facebook.sdk:FBSDKSettingsSetAdvertiserTrackingEnabledTimestamp",
      "Should persist the time when the tracking status is set to disallowed"
    )
  }

  func testSettingFacebookAdvertiserTrackingStatusToUnspecified() {
    settings.advertisingTrackingStatus = .unspecified

    XCTAssertNil(
      userDefaultsSpy.object(forKey: "com.facebook.sdk:FBSDKSettingsSetAdvertiserTrackingEnabledTimestamp"),
      "Should not capture the time the status is set to unspecified"
    )
  }

  // MARK: - Logging behaviors

  func testLoggingBehaviors() {
    let behaviors = Set([LoggingBehavior.accessTokens, .appEvents])
    settings.loggingBehaviors = behaviors

    XCTAssertEqual(
      settings.loggingBehaviors,
      behaviors,
      "Should be able to set and retrieve logging behaviors"
    )
  }

  func testEnableLoggingBehaviors() {
    let appEvents = LoggingBehavior(rawValue: "app_events")
    let networkRequests = LoggingBehavior(rawValue: "network_requests")
    let mockLoggingBehaviors = Set([appEvents, networkRequests])
    let informationBehaviort = LoggingBehavior(rawValue: "informational")

    settings.loggingBehaviors = mockLoggingBehaviors

    XCTAssertEqual(mockLoggingBehaviors, settings.loggingBehaviors)

    // test enable logging behavior
    settings.enableLoggingBehavior(LoggingBehavior(rawValue: "informational"))
    XCTAssertTrue(settings.loggingBehaviors.contains(informationBehaviort))
    settings.disableLoggingBehavior(informationBehaviort)
    XCTAssertFalse(settings.loggingBehaviors.contains(informationBehaviort))
  }

  // MARK: - Graph Error Recovery Enabled

  func testDefaultGraphErrorRecoveryEnabled() {
    XCTAssertTrue(
      settings.isGraphErrorRecoveryEnabled,
      "isGraphErrorRecoveryEnabled should be enabled by default"
    )
  }

  func testDefaultGraphAPIVersion() {
    XCTAssertEqual(
      settings.graphAPIVersion,
      FBSDK_DEFAULT_GRAPH_API_VERSION,
      "Settings should provide a default graph api version"
    )
  }

  // MARK: - Logging Behaviors

  func testSettingsBehaviorsFromMissingPlistEntry() {
    let expected: Set = [LoggingBehavior.developerErrors]
    XCTAssertEqual(
      settings.loggingBehaviors,
      expected,
      "Logging behaviors should default to developer errors when there is no plist entry"
    )
  }

  func testSettingBehaviorsFromPlistWithInvalidEntry() {
    bundle = TestBundle(infoDictionary: ["FacebookLoggingBehavior": ["Foo"]])
    configureSettings()

    XCTAssertEqual(
      settings.loggingBehaviors.first,
      LoggingBehavior(rawValue: "Foo"),
      "Logging behaviors should default to developer errors when"
        + "settings are created with a plist that only has invalid entries but it does not"
    )
  }

  func testSettingBehaviorsFromPlistWithValidEntry() {
    bundle = Bundle(for: Settings.self)
    configureSettings()

    let expected = Set([LoggingBehavior.developerErrors])
    XCTAssertEqual(settings.loggingBehaviors, expected)
  }

  func testLoggingBehaviorsInternalStorage() throws {
    settings.loggingBehaviors = Set([LoggingBehavior.informational])
    XCTAssertNotNil(settings.loggingBehaviors, "sanity check")
    XCTAssertNil(
      userDefaultsSpy.capturedObjectRetrievalKey,
      "Should not attempt to access the cache to retrieve objects that have a current value"
    )
    XCTAssertNil(
      testBundle.capturedKeys.last,
      "Should not attempt to access the plist to retrieve objects that have a current value"
    )
  }

  // MARK: - Domain Prefix

  func testSettingDomainPrefixFromMissingPlistEntry() {
    XCTAssertNil(
      settings.facebookDomainPart,
      "There should be no default value for a facebook domain prefix"
    )
  }

  func testSettingDomainPrefixFromEmptyPlistEntry() {
    bundle = TestBundle(infoDictionary: ["FacebookDomainPart": Self.emptyString])
    configureSettings()

    XCTAssertEqual(
      settings.facebookDomainPart,
      Self.emptyString,
      "Should not use an empty string as a facebook domain prefix but it does"
    )
  }

  func testSettingFacebookDomainPrefixFromPlist() {
    bundle = TestBundle(infoDictionary: ["FacebookDomainPart": "beta"])
    configureSettings()

    XCTAssertEqual(
      settings.facebookDomainPart,
      "beta",
      "A developer should be able to set any string as the facebook domain prefix to use in building urls"
    )
  }

  func testSettingDomainPrefixWithPlistEntry() {
    let domainPrefix = "abc123"
    bundle = TestBundle(infoDictionary: ["FacebookDomainPart": domainPrefix])
    configureSettings()
    settings.facebookDomainPart = "foo"

    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookDomainPart"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertEqual(
      settings.facebookDomainPart,
      "foo",
      "Settings should return the explicitly set domain prefix over one gleaned from a plist entry"
    )
  }

  func testSettingDomainPrefixWithoutPlistEntry() {
    settings.facebookDomainPart = "foo"
    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookDomainPart"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertEqual(
      settings.facebookDomainPart,
      "foo",
      "Settings should return the explicitly set domain prefix"
    )
  }

  func testSettingEmptyDomainPrefix() {
    settings.facebookDomainPart = Self.emptyString
    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookDomainPart"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertEqual(
      settings.facebookDomainPart,
      Self.emptyString,
      "Settings should return the explicitly set domain prefix"
    )
  }

  func testSettingWhitespaceOnlyDomainPrefix() {
    settings.facebookDomainPart = Self.whiteSpaceToken
    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookDomainPart"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertEqual(
      settings.facebookDomainPart,
      Self.whiteSpaceToken,
      "Settings should return the explicitly set domain prefix"
    )
  }

  func testDomainPartInternalStorage() {
    settings.facebookDomainPart = "foo"
    resetLoggingSideEffects()

    XCTAssertNotNil(settings.facebookDomainPart, "sanity check")

    XCTAssertNil(
      userDefaultsSpy.capturedObjectRetrievalKey,
      "Should not attempt to access the cache to retrieve objects that have a current value"
    )
    XCTAssertNil(
      testBundle.capturedKeys.last,
      "Should not attempt to access the plist to retrieve objects that have a current value"
    )
  }

  // MARK: - Client Token

  func testClientTokenFromPlist() {
    let clientToken = "abc123"
    bundle = TestBundle(infoDictionary: ["FacebookClientToken": clientToken])
    configureSettings()

    XCTAssertEqual(
      settings.clientToken,
      clientToken,
      "A developer should be able to set any string as the client token"
    )
  }

  func testClientTokenFromMissingPlistEntry() {
    XCTAssertNil(
      settings.clientToken,
      "A client token should not have a default value if it is not available in the plist"
    )
  }

  func testSettingClientTokenFromEmptyPlistEntry() {
    bundle = TestBundle(infoDictionary: ["FacebookClientToken": Self.emptyString])
    configureSettings()

    XCTAssertEqual(
      settings.clientToken,
      Self.emptyString,
      "Should not use an empty string as a facebook client token but it will"
    )
  }

  func testSettingClientTokenWithPlistEntry() {
    let clientToken = "abc123"
    bundle = TestBundle(infoDictionary: ["FacebookClientToken": clientToken])
    configureSettings()

    settings.clientToken = "foo"

    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookClientToken"],
      "Should not persist the value of a non-cachable property when setting it"
    )

    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookClientToken"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertEqual(
      settings.clientToken,
      "foo",
      "Settings should return the explicitly set client token over one gleaned from a plist entry"
    )
  }

  func testSettingClientTokenWithoutPlistEntry() {
    settings.clientToken = "foo"

    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookClientToken"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertEqual(
      settings.clientToken,
      "foo",
      "Settings should return the explicitly set client token"
    )
  }

  func testSettingEmptyClientToken() {
    settings.clientToken = Self.emptyString

    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookClientToken"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertEqual(
      settings.clientToken,
      Self.emptyString,
      "Should not userDefaultsSpy an invalid token but it will"
    )
  }

  func testSettingWhitespaceOnlyClientToken() {
    settings.clientToken = Self.whiteSpaceToken

    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookClientToken"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertEqual(
      settings.clientToken,
      Self.whiteSpaceToken,
      "Should not userDefaultsSpy a whitespace only client token but it will"
    )
  }

  func testClientTokenInternalStorage() {
    settings.clientToken = "foo"

    resetLoggingSideEffects()

    XCTAssertNotNil(settings.clientToken, "sanity check")
    XCTAssertNil(
      userDefaultsSpy.capturedObjectRetrievalKey,
      "Should not attempt to access the cache to retrieve objects that have a current value"
    )
    XCTAssertNil(
      testBundle.capturedKeys.last,
      "Should not attempt to access the plist to retrieve objects that have a current value"
    )
  }

  // MARK: - App Identifier

  func testAppIdentifierFromPlist() {
    let appIdentifier = "abc1234"
    bundle = TestBundle(infoDictionary: ["FacebookAppID": appIdentifier])
    configureSettings()

    XCTAssertEqual(
      settings.appID,
      appIdentifier,
      "A developer should be able to set any string as the app identifier"
    )
  }

  func testAppIdentifierFromMissingPlistEntry() {
    XCTAssertNil(
      settings.appID,
      "An app identifier should not have a default value if it is not available in the plist"
    )
  }

  func testSettingAppIdentifierFromEmptyPlistEntry() {
    bundle = TestBundle(infoDictionary: ["FacebookAppID": Self.emptyString])
    configureSettings()

    XCTAssertEqual(
      settings.appID,
      Self.emptyString,
      "Should not use an empty string as an app identifier but it will"
    )
  }

  func testSettingAppIdentifierWithPlistEntry() {
    let appIdentifier = "abc123"
    bundle = TestBundle(infoDictionary: ["FacebookClientToken": appIdentifier])
    configureSettings()

    settings.appID = "foo"

    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookAppID"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertEqual(
      settings.appID,
      "foo",
      "Settings should return the explicitly set app identifier over one gleaned from a plist entry"
    )
  }

  func testSettingAppIdentifierWithoutPlistEntry() {
    settings.appID = "foo"

    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookAppID"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertEqual(
      settings.appID,
      "foo",
      "Settings should return the explicitly set app identifier"
    )
  }

  func testSettingEmptyAppIdentifier() {
    settings.appID = Self.emptyString

    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookAppID"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertEqual(
      settings.appID,
      Self.emptyString,
      "Should not userDefaultsSpy an empty app identifier but it will"
    )
  }

  func testSettingWhitespaceOnlyAppIdentifier() {
    settings.appID = Self.whiteSpaceToken

    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookAppID"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertEqual(
      settings.appID,
      Self.whiteSpaceToken,
      "Should not userDefaultsSpy a whitespace only app identifier but it will"
    )
  }

  func testAppIdentifierInternalStorage() {
    settings.appID = "foo"

    resetLoggingSideEffects()

    XCTAssertNotNil(settings.appID, "sanity check")
    XCTAssertNil(
      userDefaultsSpy.capturedObjectRetrievalKey,
      "Should not attempt to access the cache to retrieve objects that have a current value"
    )
    XCTAssertNil(
      testBundle.capturedKeys.last,
      "Should not attempt to access the plist to retrieve objects that have a current value"
    )
  }

  // MARK: - Display Name

  func testDisplayNameFromPlist() {
    let displayName = "abc123"
    bundle = TestBundle(infoDictionary: ["FacebookDisplayName": displayName])
    configureSettings()

    XCTAssertEqual(
      settings.displayName,
      displayName,
      "A developer should be able to set any string as the display name"
    )
  }

  func testDisplayNameFromMissingPlistEntry() {
    XCTAssertNil(
      settings.displayName,
      "A display name should not have a default value if it is not available in the plist"
    )
  }

  func testSettingDisplayNameFromEmptyPlistEntry() {
    bundle = TestBundle(infoDictionary: ["FacebookDisplayName": Self.emptyString])
    configureSettings()

    XCTAssertEqual(
      settings.displayName,
      Self.emptyString,
      "Should not use an empty string as a display name but it will"
    )
  }

  func testSettingDisplayNameWithPlistEntry() {
    let displayName = "abc123"
    bundle = TestBundle(infoDictionary: ["FacebookDisplayName": displayName])
    configureSettings()

    settings.displayName = "foo"

    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookDisplayName"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertEqual(
      settings.displayName,
      "foo",
      "Settings should return the explicitly set display name over one gleaned from a plist entry"
    )
  }

  func testSettingDisplayNameWithoutPlistEntry() {
    settings.displayName = "foo"

    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookDisplayName"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertEqual(
      settings.displayName,
      "foo",
      "Settings should return the explicitly set display name"
    )
  }

  func testSettingEmptyDisplayName() {
    settings.displayName = Self.emptyString

    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookDisplayName"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertEqual(
      settings.displayName,
      Self.emptyString,
      "Should not userDefaultsSpy an empty display name but it will"
    )
  }

  func testSettingWhitespaceOnlyDisplayName() {
    settings.displayName = Self.whiteSpaceToken

    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookDisplayName"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertEqual(
      settings.displayName,
      Self.whiteSpaceToken,
      "Should not userDefaultsSpy a whitespace only display name but it will"
    )
  }

  func testDisplayNameInternalStorage() {
    settings.displayName = "foo"

    resetLoggingSideEffects()

    XCTAssertNotNil(settings.displayName, "sanity check")
    XCTAssertNil(
      userDefaultsSpy.capturedObjectRetrievalKey,
      "Should not attempt to access the cache to retrieve objects that have a current value"
    )
    XCTAssertNil(
      testBundle.capturedKeys.last,
      "Should not attempt to access the plist to retrieve objects that have a current value"
    )
  }

  // MARK: - JPEG Compression Quality

  func testJPEGCompressionQualityFromPlist() {
    let jpegCompressionQuality = 0.1
    bundle = TestBundle(infoDictionary: ["FacebookJpegCompressionQuality": jpegCompressionQuality])
    configureSettings()

    XCTAssertEqual(
      Double(settings.jpegCompressionQuality),
      jpegCompressionQuality,
      accuracy: 0.01,
      "A developer should be able to set a jpeg compression quality via the plist"
    )
  }

  func testJPEGCompressionQualityFromMissingPlistEntry() {
    XCTAssertEqual(
      settings.jpegCompressionQuality,
      0.9,
      accuracy: 0.01,
      "There should be a known default value for jpeg compression quality"
    )
  }

  func testSettingJPEGCompressionQualityFromInvalidPlistEntry() {
    bundle = TestBundle(infoDictionary: ["FacebookJpegCompressionQuality": -2.0])
    configureSettings()

    XCTAssertNotEqual(
      settings.jpegCompressionQuality,
      -0.2,
      "Should not use a negative value as a jpeg compression quality"
    )
  }

  func testSettingJPEGCompressionQualityWithPlistEntry() {
    bundle = TestBundle(infoDictionary: ["FacebookJpegCompressionQuality": 0.2])
    configureSettings()
    settings.jpegCompressionQuality = 0.3

    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookJpegCompressionQuality"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertEqual(
      settings.jpegCompressionQuality,
      0.3,
      accuracy: 0.01,
      "Settings should return the explicitly set jpeg compression quality over one gleaned from a plist entry"
    )
  }

  func testSettingJPEGCompressionQualityWithoutPlistEntry() {
    settings.jpegCompressionQuality = 1.0

    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookJpegCompressionQuality"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertEqual(
      settings.jpegCompressionQuality,
      1.0,
      "Settings should return the explicitly set jpeg compression quality"
    )
  }

  func testSettingJPEGCompressionQualityTooLow() {
    settings.jpegCompressionQuality = -0.1

    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookJpegCompressionQuality"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertNotEqual(
      settings.jpegCompressionQuality,
      -0.1,
      "Should not userDefaultsSpy a negative jpeg compression quality"
    )
  }

  func testSettingJPEGCompressionQualityTooHigh() {
    settings.jpegCompressionQuality = 1.1

    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookJpegCompressionQuality"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertNotEqual(
      settings.jpegCompressionQuality,
      1.1,
      "Should not userDefaultsSpy a jpeg compression quality that is larger than 1.0"
    )
  }

  func testJPEGCompressionQualityInternalStorage() {
    settings.jpegCompressionQuality = 1

    resetLoggingSideEffects()

    XCTAssertEqual(settings.jpegCompressionQuality, 1, "Sanity check")
    XCTAssertNil(
      userDefaultsSpy.capturedObjectRetrievalKey,
      "Should not attempt to access the cache to retrieve objects that have a current value"
    )
    XCTAssertNil(
      testBundle.capturedKeys.last,
      "Should not attempt to access the plist to retrieve objects that have a current value"
    )
  }

  // MARK: - URL Scheme Suffix

  func testURLSchemeSuffixFromPlist() {
    let urlSchemeSuffix = "abc123"
    bundle = TestBundle(infoDictionary: ["FacebookUrlSchemeSuffix": urlSchemeSuffix])
    configureSettings()

    XCTAssertEqual(
      settings.appURLSchemeSuffix,
      urlSchemeSuffix,
      "A developer should be able to set any string as the url scheme suffix"
    )
  }

  func testURLSchemeSuffixFromMissingPlistEntry() {
    XCTAssertNil(
      settings.appURLSchemeSuffix,
      "A url scheme suffix should not have a default value if it is not available in the plist"
    )
  }

  func testSettingURLSchemeSuffixFromEmptyPlistEntry() {
    bundle = TestBundle(infoDictionary: ["FacebookUrlSchemeSuffix": Self.emptyString])
    configureSettings()

    XCTAssertEqual(
      settings.appURLSchemeSuffix,
      Self.emptyString,
      "Should not use an empty string as a url scheme suffix but it will"
    )
  }

  func testSettingURLSchemeSuffixWithPlistEntry() {
    let urlSchemeSuffix = "abc123"
    bundle = TestBundle(infoDictionary: ["FacebookUrlSchemeSuffix": urlSchemeSuffix])
    configureSettings()
    settings.appURLSchemeSuffix = "foo"

    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookUrlSchemeSuffix"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertEqual(
      settings.appURLSchemeSuffix,
      "foo",
      "Settings should return the explicitly set url scheme suffix over one gleaned from a plist entry"
    )
  }

  func testSettingURLSchemeSuffixWithoutPlistEntry() {
    settings.appURLSchemeSuffix = "foo"

    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookUrlSchemeSuffix"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertEqual(
      settings.appURLSchemeSuffix,
      "foo",
      "Settings should return the explicitly set url scheme suffix"
    )
  }

  func testSettingEmptyURLSchemeSuffix() {
    settings.appURLSchemeSuffix = Self.emptyString

    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookUrlSchemeSuffix"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertEqual(
      settings.appURLSchemeSuffix,
      Self.emptyString,
      "Should not userDefaultsSpy an empty url scheme suffix but it will"
    )
  }

  func testSettingWhitespaceOnlyURLSchemeSuffix() {
    settings.appURLSchemeSuffix = Self.whiteSpaceToken

    XCTAssertNil(
      userDefaultsSpy.capturedValues["FacebookUrlSchemeSuffix"],
      "Should not persist the value of a non-cachable property when setting it"
    )
    XCTAssertEqual(
      settings.appURLSchemeSuffix,
      Self.whiteSpaceToken,
      "Should not userDefaultsSpy a whitespace only url scheme suffix but it will"
    )
  }

  func testURLSchemeSuffixInternalStorage() {
    settings.appURLSchemeSuffix = "foo"

    resetLoggingSideEffects()

    XCTAssertNotNil(settings.appURLSchemeSuffix, "sanity check")
    XCTAssertNil(
      userDefaultsSpy.capturedObjectRetrievalKey,
      "Should not attempt to access the cache to retrieve objects that have a current value"
    )
    XCTAssertNil(
      testBundle.capturedKeys.last,
      "Should not attempt to access the plist to retrieve objects that have a current value"
    )
  }

  // MARK: - Auto Log App Events Enabled

  func testAutoLogAppEventsEnabledFromPlist() {
    bundle = TestBundle(infoDictionary: ["FacebookAutoLogAppEventsEnabled": false])
    configureSettings()

    XCTAssertFalse(
      settings.isAutoLogAppEventsEnabled,
      "A developer should be able to set the value of auto log app events from the plist"
    )
  }

  func testAutoLogAppEventsEnabledDefaultValue() {
    XCTAssertTrue(
      settings.isAutoLogAppEventsEnabled,
      "Auto logging of app events should default to true when there is no plist value given"
    )
  }

  func testAutoLogAppEventsEnabledInvalidPlistEntry() {
    bundle = TestBundle(infoDictionary: ["FacebookAutoLogAppEventsEnabled": Self.emptyString])
    configureSettings()

    XCTAssertTrue(
      settings.isAutoLogAppEventsEnabled,
      "Auto logging of app events should default to true when there is an invalid plist value given"
    )
  }

  func testSettingAutoLogAppEventsEnabled() {
    settings.isAutoLogAppEventsEnabled = false

    XCTAssertNotNil(
      userDefaultsSpy.capturedValues["FacebookAutoLogAppEventsEnabled"],
      "Should persist the value of a cachable property when setting it"
    )
    XCTAssertFalse(
      settings.isAutoLogAppEventsEnabled,
      "Should use the explicitly set property"
    )
  }

  func testOverridingCachedAutoLogAppEventsEnabled() {
    XCTAssertTrue(settings.isAutoLogAppEventsEnabled)

    bundle = TestBundle(infoDictionary: ["FacebookAutoLogAppEventsEnabled": false])
    configureSettings()

    XCTAssertTrue(
      settings.isAutoLogAppEventsEnabled,
      "Should favor cached properties over those set in the plist"
    )
  }

  func testAutoLogAppEventsEnabledInternalStorage() {
    settings.isAutoLogAppEventsEnabled = true

    resetLoggingSideEffects()

    XCTAssertTrue(settings.isAutoLogAppEventsEnabled, "sanity check")
    XCTAssertNil(
      userDefaultsSpy.capturedObjectRetrievalKey,
      "Should not attempt to access the cache to retrieve objects that have a current value"
    )
    XCTAssertNil(
      testBundle.capturedKeys.last,
      "Should not attempt to access the plist to retrieve objects that have a current value"
    )
  }

  // MARK: - Advertiser Identifier Collection Enabled

  func testFacebookAdvertiserIDCollectionEnabled() {
    bundle = TestBundle(infoDictionary: ["FacebookAdvertiserIDCollectionEnabled": false])
    configureSettings()

    XCTAssertFalse(
      settings.isAdvertiserIDCollectionEnabled,
      "A developer should be able to set whether advertiser ID collection is enabled from the plist"
    )
  }

  func testFacebookAdvertiserIDCollectionEnabledDefaultValue() {
    XCTAssertTrue(
      settings.isAdvertiserIDCollectionEnabled,
      "Auto collection of advertiser Any should default to true when there is no plist value given"
    )
  }

  func testFacebookAdvertiserIDCollectionEnabledInvalidPlistEntry() {
    bundle = TestBundle(infoDictionary: ["FacebookAdvertiserIDCollectionEnabled": Self.emptyString])
    configureSettings()

    XCTAssertTrue(
      settings.isAdvertiserIDCollectionEnabled,
      "Auto collection of advertiser Any should default to true when"
        + "there is an invalid plist value given"
    )
  }

  func testSettingFacebookAdvertiserIDCollectionEnabled() {
    settings.isAdvertiserIDCollectionEnabled = false

    XCTAssertNotNil(
      userDefaultsSpy.capturedValues["FacebookAdvertiserIDCollectionEnabled"],
      "Should persist the value of a cachable property when setting it"
    )
    XCTAssertFalse(
      settings.isAdvertiserIDCollectionEnabled,
      "Should use the explicitly set property"
    )
  }

  func testOverridingCachedFacebookAdvertiserIDCollectionEnabled() {
    settings.isAdvertiserIDCollectionEnabled = true
    XCTAssertTrue(settings.isAdvertiserIDCollectionEnabled)

    bundle = TestBundle(infoDictionary: ["FacebookAdvertiserIDCollectionEnabled": false])
    configureSettings()

    XCTAssertTrue(
      settings.isAdvertiserIDCollectionEnabled,
      "Should favor cached properties over those set in the plist"
    )
  }

  func testAdvertiserIDCollectionEnabledInternalStorage() {
    settings.isAdvertiserIDCollectionEnabled = true

    resetLoggingSideEffects()

    XCTAssertTrue(settings.isAdvertiserIDCollectionEnabled, "sanity check")
    XCTAssertNil(
      userDefaultsSpy.capturedObjectRetrievalKey,
      "Should not attempt to access the cache to retrieve objects that have a current value"
    )
    XCTAssertNil(
      testBundle.capturedKeys.last,
      "Should not attempt to access the plist to retrieve objects that have a current value"
    )
  }

  // MARK: - SKAdNetwork Report Enabled

  func testFacebookSKAdNetworkReportEnabledFromPlist() {
    bundle = TestBundle(infoDictionary: ["FacebookSKAdNetworkReportEnabled": false])
    configureSettings()

    XCTAssertFalse(
      settings.isSKAdNetworkReportEnabled,
      "A developer should be able to set the value of SKAdNetwork Report from the plist"
    )
  }

  func testFacebookSKAdNetworkReportEnabledDefaultValue() {
    XCTAssertTrue(
      settings.isSKAdNetworkReportEnabled,
      "SKAdNetwork Report should default to true when there is no plist value given"
    )
  }

  func testFacebookSKAdNetworkReportEnabledInvalidPlistEntry() {
    bundle = TestBundle(infoDictionary: ["FacebookSKAdNetworkReportEnabled": Self.emptyString])
    configureSettings()

    XCTAssertTrue(
      settings.isSKAdNetworkReportEnabled,
      "SKAdNetwork Report should default to true when there is an invalid plist value given"
    )
  }

  func testSettingFacebookSKAdNetworkReportEnabled() {
    settings.isSKAdNetworkReportEnabled = false

    XCTAssertNotNil(
      userDefaultsSpy.capturedValues["FacebookSKAdNetworkReportEnabled"],
      "Should persist the value of a cachable property when setting it"
    )
    XCTAssertFalse(
      settings.isSKAdNetworkReportEnabled,
      "Should use the explicitly set property"
    )
  }

  func testOverridingCachedFacebookSKAdNetworkReportEnabled() {
    XCTAssertTrue(settings.isSKAdNetworkReportEnabled)

    bundle = TestBundle(infoDictionary: ["FacebookSKAdNetworkReportEnabled": false])
    configureSettings()

    XCTAssertTrue(
      settings.isSKAdNetworkReportEnabled,
      "Should favor cached properties over those set in the plist"
    )
  }

  func testFacebookSKAdNetworkReportEnabledInternalStorage() {
    settings.isSKAdNetworkReportEnabled = true

    resetLoggingSideEffects()

    XCTAssertTrue(settings.isSKAdNetworkReportEnabled, "sanity check")
    XCTAssertNil(
      userDefaultsSpy.capturedObjectRetrievalKey,
      "Should not attempt to access the cache to retrieve objects that have a current value"
    )
    XCTAssertNil(
      testBundle.capturedKeys.last,
      "Should not attempt to access the plist to retrieve objects that have a current value"
    )
  }

  // MARK: - Codeless Debug Log Enabled

  func testFacebookCodelessDebugLogEnabled() {
    bundle = TestBundle(infoDictionary: ["FacebookCodelessDebugLogEnabled": false])
    configureSettings()

    XCTAssertFalse(
      settings.isCodelessDebugLogEnabled,
      "A developer should be able to set whether codeless debug logging is enabled from the plist"
    )
  }

  func testFacebookCodelessDebugLogEnabledDefaultValue() {
    bundle = TestBundle(infoDictionary: [:])
    configureSettings()

    XCTAssertFalse(
      settings.isCodelessDebugLogEnabled,
      "Codeless debug logging enabled should default to false when there is no plist value given"
    )
  }

  func testFacebookCodelessDebugLogEnabledInvalidPlistEntry() {
    bundle = TestBundle(infoDictionary: ["FacebookCodelessDebugLogEnabled": Self.emptyString])
    configureSettings()

    XCTAssertFalse(
      settings.isCodelessDebugLogEnabled,
      "Codeless debug logging enabled should default to true when there is an invalid plist value given but it does not"
    )
  }

  func testSettingFacebookCodelessDebugLogEnabled() {
    settings.isCodelessDebugLogEnabled = false

    XCTAssertNotNil(
      userDefaultsSpy.capturedValues["FacebookCodelessDebugLogEnabled"],
      "Should persist the value of a cachable property when setting it"
    )
    XCTAssertFalse(
      settings.isCodelessDebugLogEnabled,
      "Should use the explicitly set property"
    )
  }

  func testOverridingCachedFacebookCodelessDebugLogEnabled() {
    settings.isCodelessDebugLogEnabled = true
    XCTAssertTrue(settings.isCodelessDebugLogEnabled)

    bundle = TestBundle(infoDictionary: ["FacebookCodelessDebugLogEnabled": false])
    configureSettings()

    XCTAssertTrue(
      settings.isCodelessDebugLogEnabled,
      "Should favor cached properties over those set in the plist"
    )
  }

  func testCachedFacebookCodelessDebugLogEnabledInternalStorage() {
    settings.isCodelessDebugLogEnabled = true

    resetLoggingSideEffects()

    XCTAssertTrue(settings.isCodelessDebugLogEnabled, "sanity check")
    XCTAssertNil(
      userDefaultsSpy.capturedObjectRetrievalKey,
      "Should not attempt to access the cache to retrieve objects that have a current value"
    )
    XCTAssertNil(
      testBundle.capturedKeys.last,
      "Should not attempt to access the plist to retrieve objects that have a current value"
    )
  }

  // MARK: - Caching Properties

  func testInitialAccessForCachablePropertyWithNonEmptyCache() {
    // Using false because it is not the default value for `isAutoInitializationEnabled`
    userDefaultsSpy.capturedValues = ["FacebookAutoLogAppEventsEnabled": false]

    XCTAssertFalse(
      settings.isAutoLogAppEventsEnabled,
      "Should retrieve an initial value for a cachable property when there is a non-empty cache"
    )

    XCTAssertEqual(
      userDefaultsSpy.capturedObjectRetrievalKey,
      "FacebookAutoLogAppEventsEnabled",
      "Should attempt to access the cache to retrieve the initial value for a cachable property"
    )
    XCTAssertFalse(
      testBundle.capturedKeys.contains("FacebookAutoLogAppEventsEnabled"),
      "Should not attempt to access the plist for cachable properties that have a value in the cache"
    )
  }

  func testInitialAccessForCachablePropertyWithEmptyCacheNonEmptyPlist() {
    // Using false because it is not the default value for `isAutoInitializationEnabled`
    bundle = TestBundle(infoDictionary: ["FacebookAutoLogAppEventsEnabled": false])
    configureSettings()

    XCTAssertFalse(
      settings.isAutoLogAppEventsEnabled,
      "Should retrieve an initial value from the property list"
    )

    XCTAssertEqual(
      userDefaultsSpy.capturedObjectRetrievalKey,
      "FacebookAutoLogAppEventsEnabled",
      "Should attempt to access the cache to retrieve the initial value for a cachable property"
    )
    XCTAssertEqual(
      testBundle.capturedKeys.last,
      "FacebookAutoLogAppEventsEnabled",
      "Should attempt to access the plist for cachable properties that have no value in the cache"
    )
  }

  func testInitialAccessForCachablePropertyWithEmptyCacheEmptyPlistAndDefaultValue() {
    XCTAssertTrue(
      settings.isAutoLogAppEventsEnabled,
      "Should use the default value for a property when there are no values in the cache or plist"
    )

    XCTAssertEqual(
      userDefaultsSpy.capturedObjectRetrievalKey,
      "FacebookAutoLogAppEventsEnabled",
      "Should attempt to access the cache to retrieve the initial value for a cachable property"
    )
    XCTAssertEqual(
      testBundle.capturedKeys.last,
      "FacebookAutoLogAppEventsEnabled",
      "Should attempt to access the plist for cachable properties that have no value in the cache"
    )
  }

  func testInitialAccessForNonCachablePropertyWithEmptyPlist() {
    XCTAssertNil(
      settings.clientToken,
      "A non-cachable property with no default value and no plist entry should not have a value"
    )

    XCTAssertNil(
      userDefaultsSpy.capturedObjectRetrievalKey,
      "Should not attempt to access the cache for a non-cachable property"
    )
    XCTAssertEqual(
      testBundle.capturedKeys.last,
      "FacebookClientToken",
      "Should attempt to access the plist for non-cachable properties"
    )
  }

  func testInitialAccessForNonCachablePropertyWithNonEmptyPlist() {
    bundle = TestBundle(infoDictionary: ["FacebookClientToken": "abc123"])
    configureSettings()

    XCTAssertEqual(
      settings.clientToken,
      "abc123",
      "Should retrieve the initial value from the property list"
    )

    XCTAssertNil(
      userDefaultsSpy.capturedObjectRetrievalKey,
      "Should not attempt to access the cache for a non-cachable property"
    )
    XCTAssertEqual(
      testBundle.capturedKeys.last,
      "FacebookClientToken",
      "Should attempt to access the plist for non-cachable properties"
    )
  }

  // MARK: - Graph Error Recovery Enabled

  func testSetGraphErrorRecoveryEnabled() {
    settings.isGraphErrorRecoveryEnabled = true
    XCTAssertTrue(settings.isGraphErrorRecoveryEnabled)

    settings.isGraphErrorRecoveryEnabled = false
    XCTAssertFalse(settings.isGraphErrorRecoveryEnabled)
  }

  // MARK: - Limit Event and Data Usage

  func testSetLimitEventAndDataUsageDefault() {
    XCTAssertFalse(
      settings.isEventDataUsageLimited,
      "Should limit event data usage by default"
    )
  }

  func testSetUseCachedValuesForExpensiveMetadata() {
    settings.shouldUseCachedValuesForExpensiveMetadata = true

    XCTAssertTrue(
      userDefaultsSpy.capturedValues["com.facebook.sdk:FBSDKSettingsUseCachedValuesForExpensiveMetadata"] as? Int != 0,
      "Should userDefaultsSpy whether or not to limit event and data usage in the user defaults"
    )
    XCTAssertTrue(
      settings.shouldUseCachedValuesForExpensiveMetadata,
      "should use cached values for expensive metadata"
    )
  }

  func testSetUseTokenOptimizations() {
    settings.shouldUseTokenOptimizations = false

    XCTAssertTrue(
      userDefaultsSpy.capturedValues["com.facebook.sdk.FBSDKSettingsUseTokenOptimizations"] as? Int == 0,
      "Should userDefaultsSpy whether or not to use token optimizations"
    )
    XCTAssertFalse(
      settings.shouldUseTokenOptimizations,
      "Should use token optimizations"
    )
  }

  func testSetLimitEventAndDataUsageWithEmptyCache() {
    settings.isEventDataUsageLimited = true

    XCTAssertTrue(
      userDefaultsSpy.capturedValues["com.facebook.sdk:FBSDKSettingsUseCachedValuesForExpensiveMetadata"] as? Int != 0,
      "Should userDefaultsSpy whether or not to limit event and data usage in the user defaults"
    )
    XCTAssertTrue(
      settings.isEventDataUsageLimited,
      "Should be able to set whether event data usage is limited"
    )
  }

  func testSetLimitEventAndDataUsageWithNonEmptyCache() {
    settings.isEventDataUsageLimited = true
    XCTAssertTrue(settings.isEventDataUsageLimited, "sanity check")

    settings.isEventDataUsageLimited = false
    XCTAssertFalse(
      settings.isEventDataUsageLimited,
      "Should be able to override the existing value of should limit event data usage"
    )

    XCTAssertTrue(
      userDefaultsSpy.capturedValues["com.facebook.sdk:FBSDKSettingsLimitEventAndDataUsage"] as? Int == 0,
      "Should userDefaultsSpy the overridden preference for limiting event data usage in the user defaults"
    )
  }

  // MARK: - Data Processing Options

  func testDataProcessingOptionDefaults() {
    settings.setDataProcessingOptions([])
    let dataProcessingOptions = settings.persistableDataProcessingOptions

    XCTAssertEqual(
      dataProcessingOptions?[DataProcessingOptionKey.country.rawValue] as! Int32, // swiftlint:disable:this force_cast
      0,
      "Country should default to zero when not provided"
    )
    XCTAssertEqual(
      dataProcessingOptions?[DataProcessingOptionKey.state.rawValue] as! Int32, // swiftlint:disable:this force_cast
      0,
      "State should default to zero when not provided"
    )
  }

  func testSettingEmptyDataProcessingOptions() {
    settings.setDataProcessingOptions([])

    XCTAssertNotNil(
      settings.persistableDataProcessingOptions,
      "Should not be able to set data processing options to an empty list of options but you can"
    )
  }

  func testSettingInvalidDataProcessOptions() throws {
    settings.setDataProcessingOptions(["Foo", "Bar"])

    XCTAssertNotNil(
      settings.persistableDataProcessingOptions,
      "Should not be able to set data processing options to invalid list of options but you can"
    )

    let persistedData = try NSKeyedArchiver.archivedData(
      withRootObject: settings.persistableDataProcessingOptions as Any,
      requiringSecureCoding: true
    )

    let capturedValues = userDefaultsSpy.capturedValues

    let capturedData = try XCTUnwrap(capturedValues["com.facebook.sdk:FBSDKSettingsDataProcessingOptions"] as? Data)

    let dict1 = try? NSKeyedUnarchiver.unarchivedObject(
      ofClasses: [NSDictionary.self, NSString.self, NSNumber.self, NSDate.self, NSSet.self, NSArray.self],
      from: capturedData
    )

    let dict2 = try? NSKeyedUnarchiver.unarchivedObject(
      ofClasses: [NSDictionary.self, NSString.self, NSNumber.self, NSDate.self, NSSet.self, NSArray.self],
      from: persistedData
    )

    let array1 = (dict1 as? [String: Any])?["data_processing_options"] as? [String]
    let array2 = (dict2 as? [String: Any])?["data_processing_options"] as? [String]
    XCTAssertEqual(array1, ["Foo", "Bar"])
    XCTAssertEqual(array2, ["Foo", "Bar"])
  }

  func testSettingDataProcessingOptionsWithCountryAndState() {
    let countryCode: Int32 = -1000000000
    let stateCode: Int32 = 100000000

    settings.setDataProcessingOptions([], country: Int32(countryCode), state: Int32(stateCode))
    let dataProcessingOptions = settings.persistableDataProcessingOptions
    XCTAssertEqual(
      (settings.persistableDataProcessingOptions?[DataProcessingOptionKey.options.rawValue] as? [Any])?.isEmpty,
      true,
      "Should use the provided array of processing options"
    )
    XCTAssertEqual(
      dataProcessingOptions?[DataProcessingOptionKey.country.rawValue] as? Int32,
      countryCode,
      "Should use the provided country code"
    )
    XCTAssertEqual(
      dataProcessingOptions?[DataProcessingOptionKey.state.rawValue] as? Int32,
      stateCode,
      "Should use the provided state code"
    )
  }

  func testDataProcessingOptionsWithEmptyCache() {
    XCTAssertNil(
      settings.persistableDataProcessingOptions,
      "Should not be able to get data processing options if there is none cached"
    )
    XCTAssertEqual(
      userDefaultsSpy.capturedObjectRetrievalKey,
      "com.facebook.sdk:FBSDKSettingsDataProcessingOptions",
      "Should attempt to access the cache to retrieve the initial value for a cachable property"
    )
    XCTAssertNil(
      testBundle.capturedKeys.last,
      "Should not attempt to access the plist for data processing options"
    )
  }

  func testDataProcessingOptionsWithNonEmptyCache() throws {
    let data = try NSKeyedArchiver.archivedData(withRootObject: [:], requiringSecureCoding: false)
    userDefaultsSpy.fb_setObject(data, forKey: Settings.PersistenceKey.dataProcessingOptions.rawValue)

    XCTAssertNotNil(
      settings.persistableDataProcessingOptions,
      "Should be able to retrieve data processing options from the cache"
    )
    XCTAssertEqual(
      userDefaultsSpy.capturedObjectRetrievalKey,
      "com.facebook.sdk:FBSDKSettingsDataProcessingOptions",
      "Should attempt to access the cache to retrieve the initial value for a cachable property"
    )
    XCTAssertNil(
      testBundle.capturedKeys.last,
      "Should not attempt to access the plist for data processing options"
    )
  }

  func testDataProcessingOptionsInternalStorage() {
    settings.setDataProcessingOptions([])

    XCTAssertNotNil(
      settings.persistableDataProcessingOptions,
      "sanity check"
    )
    XCTAssertNil(
      userDefaultsSpy.capturedObjectRetrievalKey,
      "Should not attempt to access the cache to retrieve objects that have a current value"
    )
    XCTAssertNil(
      testBundle.capturedKeys.last,
      "Should not attempt to access the plist to retrieve objects that have a current value"
    )
  }

  func testRecordInstall() {
    XCTAssertNil(
      userDefaultsSpy.capturedValues["com.facebook.sdk:FBSDKSettingsInstallTimestamp"],
      "Should not persist the value of before setting it"
    )
    settings.recordInstall()
    XCTAssertNotNil(
      userDefaultsSpy.capturedValues["com.facebook.sdk:FBSDKSettingsInstallTimestamp"],
      "Should persist the value after setting it"
    )
    let date = userDefaultsSpy.capturedValues["com.facebook.sdk:FBSDKSettingsInstallTimestamp"]
    settings.recordInstall()
    XCTAssertEqual(
      date as? Date,
      userDefaultsSpy.capturedValues["com.facebook.sdk:FBSDKSettingsInstallTimestamp"] as? Date,
      "Should not change the cached install timesstamp"
    )
  }

  func testRecordSetAdvertiserTrackingEnabled() {
    settings.recordSetAdvertiserTrackingEnabled()
    XCTAssertNotNil(
      userDefaultsSpy.capturedValues["com.facebook.sdk:FBSDKSettingsSetAdvertiserTrackingEnabledTimestamp"],
      "Should persist the value after setting it"
    )
    let date = userDefaultsSpy.capturedValues["com.facebook.sdk:FBSDKSettingsSetAdvertiserTrackingEnabledTimestamp"]
    settings.recordSetAdvertiserTrackingEnabled()
    XCTAssertNotEqual(
      date as? Date,
      userDefaultsSpy.capturedValues["com.facebook.sdk:FBSDKSettingsSetAdvertiserTrackingEnabledTimestamp"] as? Date,
      "Should update set advertiser tracking enabled timesstamp"
    )
  }

  func testIsATETimeSufficientlyDelayed() {
    settings.recordInstall()
    settings.recordSetAdvertiserTrackingEnabled()
    XCTAssertFalse(settings.isATETimeSufficientlyDelayed)
    settings.recordSetAdvertiserTrackingEnabled()
    let today = Date()
    let calendar = Calendar(identifier: .gregorian)
    var addComponents = DateComponents()
    addComponents.month = -1
    let expiredDate = calendar.date(byAdding: addComponents, to: today, wrappingComponents: false)
    userDefaultsSpy.setValue(expiredDate, forKey: "com.facebook.sdk:FBSDKSettingsInstallTimestamp")
    XCTAssertTrue(settings.isATETimeSufficientlyDelayed)
  }

  // MARK: - test for internal functions

  // MARK: - User Agent Suffix

  func testSettingEmptyUserAgentSuffix() {
    userAgentSuffix = Self.emptyString

    XCTAssertEqual(
      userAgentSuffix,
      Self.emptyString,
      "Should not userDefaultsSpy an empty user agent suffix but it will"
    )
  }

  func testSettingWhitespaceOnlyUserAgentSuffix() {
    userAgentSuffix = Self.whiteSpaceToken

    XCTAssertEqual(
      userAgentSuffix,
      Self.whiteSpaceToken,
      "Should not userDefaultsSpy a whitespace only user agent suffix but it will"
    )
  }

  func testSetGraphAPIVersion() {
    let mockGraphAPIVersion = "mockGraphAPIVersion"
    settings.graphAPIVersion = mockGraphAPIVersion
    XCTAssertEqual(mockGraphAPIVersion, settings.graphAPIVersion)
  }

  func testIsDataProcessingRestricted() {
    settings.setDataProcessingOptions(["LDU"])
    XCTAssertTrue(settings.isDataProcessingRestricted)
    settings.setDataProcessingOptions([])
    XCTAssertFalse(settings.isDataProcessingRestricted)
    settings.setDataProcessingOptions(["ldu"])
    XCTAssertTrue(settings.isDataProcessingRestricted)
    settings.setDataProcessingOptions(nil)
    XCTAssertFalse(settings.isDataProcessingRestricted)
  }

  func resetLoggingSideEffects() {
    bundle = TestBundle()
    userDefaultsSpy = UserDefaultsSpy()
  }
}


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

@testable import FBSDKCoreKit

import XCTest

final class ShareDialogConfigurationTests: XCTestCase {

  // swiftlint:disable implicitly_unwrapped_optional
  var serverConfiguration: TestServerConfiguration!
  var provider: TestServerConfigurationProvider!
  var configuration: ShareDialogConfiguration!
  // swiftlint:enable implicitly_unwrapped_optional

  override func setUp() {
    super.setUp()

    serverConfiguration = TestServerConfiguration()
    provider = TestServerConfigurationProvider(configuration: serverConfiguration)
    configuration = ShareDialogConfiguration()

    configuration.setDependencies(
      .init(serverConfigurationProvider: provider)
    )
  }

  override func tearDown() {
    configuration.resetDependencies()

    serverConfiguration = nil
    provider = nil
    configuration = nil

    super.tearDown()
  }

  func testDefaultDependencies() throws {
    configuration.resetDependencies()
    let dependencies = try configuration.getDependencies()

    XCTAssertTrue(
      dependencies.serverConfigurationProvider is _ServerConfigurationManager,
      """
      The ShareDialogConfiguration type uses _ServerConfigurationManager as
      its ServerConfigurationProvider dependency by default
      """
    )
  }

  func testCreatingWithCustomDependencies() {
    XCTAssertEqual(
      configuration.serverConfigurationProvider as? TestServerConfigurationProvider,
      provider,
      "Should be able to configure with a custom server configuration provider"
    )
  }

  func testDefaultShareMode() {
    serverConfiguration.stubbedDefaultShareMode = name

    XCTAssertEqual(
      configuration.defaultShareMode,
      name,
      "Should delegate retrieving the default share mode to the underlying server configuration"
    )
  }

  func testShouldUseNativeDialog() {
    _ = configuration.shouldUseNativeDialog(forDialogName: name)
    XCTAssertTrue(
      provider.didRetrieveCachedServerConfiguration,
      "Checking if a native dialog should be used should retrieve a server configuration to query"
    )
    XCTAssertEqual(
      serverConfiguration.capturedUseNativeDialogName,
      name,
      "Should delegate to the underlying server configuration"
    )
  }

  func testShouldUseSafariController() {
    _ = configuration.shouldUseSafariViewController(forDialogName: name)
    XCTAssertTrue(
      provider.didRetrieveCachedServerConfiguration,
      "Checking if a native dialog should be used should retrieve a server configuration to query"
    )
    XCTAssertEqual(
      serverConfiguration.capturedUseSafariControllerName,
      name,
      "Should delegate to the underlying server configuration"
    )
  }
}


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

@testable import FBSDKCoreKit

import XCTest

final class URLSchemeTests: XCTestCase {
  func testSchemeRawValues() {
    XCTAssertEqual(URLScheme.facebookAPI.rawValue, "fbapi")
    XCTAssertEqual(URLScheme.messengerApp.rawValue, "fb-messenger-share-api")
    XCTAssertEqual(URLScheme.https.rawValue, "https")
    XCTAssertEqual(URLScheme.http.rawValue, "http")
    XCTAssertEqual(URLScheme.web.rawValue, "web")
  }
}


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

@testable import FBSDKCoreKit

import XCTest

final class UserAgeRangeTests: XCTestCase {

  func testCreateWithMinOnly() {
    let dict: [String: NSNumber] = ["min": 1]
    let ageRange = UserAgeRange(from: dict)

    XCTAssertNotNil(ageRange, "Should be able to create UserAgeRange with min value specified")
    XCTAssertEqual(ageRange?.min, dict["min"])
    XCTAssertNil(ageRange?.max)
  }

  func testCreateWithMaxOnly() {
    let dict: [String: NSNumber] = ["max": 1]
    let ageRange = UserAgeRange(from: dict)

    XCTAssertNotNil(ageRange, "Should be able to create UserAgeRange with max value specified")
    XCTAssertNil(ageRange?.min)
    XCTAssertEqual(ageRange?.max, dict["max"])
  }

  func testCreateWithMinSmallerThanMax() {
    let dict: [String: NSNumber] = ["min": 1, "max": 2]
    let ageRange = UserAgeRange(from: dict)

    XCTAssertNotNil(
      ageRange,
      "Should be able to create UserAgeRange with min value smaller than max"
    )
    XCTAssertEqual(ageRange?.min, dict["min"])
    XCTAssertEqual(ageRange?.max, dict["max"])
  }

  func testCreateFromEmptyDictionary() {
    XCTAssertNil(
      UserAgeRange(from: [:]),
      "Should not be able to create UserAgeRange from empty dictionary"
    )
  }

  func testCreateWithNegativeMin() {
    let dict: [String: NSNumber] = ["min": -1]
    XCTAssertNil(
      UserAgeRange(from: dict),
      "Should not be able to create UserAgeRange with negative min value"
    )
  }

  func testCreateWithNegativeMax() {
    let dict = ["max": -1 as NSNumber]
    XCTAssertNil(
      UserAgeRange(from: dict),
      "Should not be able to create UserAgeRange with negative max value"
    )
  }

  func testCreateWithMinLargerThanMax() {
    let dict: [String: NSNumber] = ["min": 2, "max": 1]
    XCTAssertNil(
      UserAgeRange(from: dict),
      "Should not be able to create UserAgeRange with min larger than max"
    )
  }

  func testCreateWithMinEqualToMax() {
    let dict: [String: NSNumber] = ["min": 1, "max": 1]
    XCTAssertNil(
      UserAgeRange(from: dict),
      "Should not be able to create UserAgeRange with min value equal to max"
    )
  }

  func testEncodingAndDecoding() throws {
    let dict: [String: NSNumber] = ["min": 1, "max": 2]
    let ageRange = try XCTUnwrap(
      UserAgeRange(from: dict),
      "Should be able to create UserAgeRange with min value specified"
    )
    let decodedObject = try CodabilityTesting.encodeAndDecode(ageRange)
    XCTAssertEqual(ageRange.min, decodedObject.min, .isCodable)
    XCTAssertEqual(ageRange.max, decodedObject.max, .isCodable)
  }
}

// swiftformat:disable extensionaccesscontrol

// MARK: - Assumptions

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


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

@testable import FBSDKCoreKit

import XCTest

final class UserDataStoreTests: XCTestCase {

  let store = _UserDataStore()
  let email = "apptest@fb.com"

  override func setUp() {
    super.setUp()

    store.clearUserData()
  }

  override func tearDown() {
    super.tearDown()

    store.clearUserData()
  }

  func testSettingUserDataByType() throws {
    let hashedEmail = try XCTUnwrap(
      BasicUtility.sha256Hash(NSString(utf8String: email))
    )

    store.setUserData(email, forType: .email)
    let retrieved = try XCTUnwrap(
      store.getUserData(),
      "Should be able to retrieve stored user data"
    )

    XCTAssertTrue(
      retrieved.contains("em"),
      "Should store the data under the expected key"
    )
    XCTAssertTrue(
      retrieved.contains(hashedEmail),
      "Should hash the data before storing it"
    )
  }

  func testClearingUserDataByType() throws {
    store.setUserData(email, forType: .email)
    store.setUserData(name, forType: .firstName)
    store.clearUserData(forType: .email)

    let retrieved = try XCTUnwrap(
      store.getUserData(),
      "Should be able to retrieve stored user data"
    )

    // User data is stored as a string representation of a dictionary.
    // example: `{"key": "hashed value"}`
    XCTAssertFalse(
      retrieved.contains("em"),
      "Should clear the provided type of user data"
    )
    XCTAssertTrue(
      retrieved.contains("fn"),
      "Should not clear unspecified user data"
    )
  }
}


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

@testable import FBSDKCoreKit

import XCTest

final class UtilityTests: XCTestCase {
  func testSHA256Hash() {
    let hashed = Utility.sha256Hash("facebook" as NSObject)

    XCTAssertEqual(hashed, "3d59f7548e1af2151b64135003ce63c0a484c26b9b8b166a7b1c1805ec34b00a")
  }

  func testURLDecodeShouldNotModifyUnencodedURLString() {
    let unencoded = "https://www.facebook.com/index.html?a=b&c=d"

    XCTAssertEqual(unencoded, Utility.decode(urlString: unencoded))
  }

  func testURLEncode() {
    let unencoded = "https://www.facebook.com/index.html?a=b&c=d"
    let encoded = "https%3A%2F%2Fwww.facebook.com%2Findex.html%3Fa%3Db%26c%3Dd"

    XCTAssertEqual(encoded, Utility.encode(urlString: unencoded))
    XCTAssertEqual(unencoded, Utility.decode(urlString: encoded))
  }

  func testURLEncodeWithJSON() {
    let url = "https://m.facebook.com/v3.2/dialog/oauth?auth_type=rerequest&client_id=123456789&default_audience=friends&display=touch&e2e={\"init\":123456.1234567890}&fbapp_pres=0&redirect_uri=fb111111111111111://authorize/&response_type=token,signed_request&return_scopes=true&scope=&sdk=ios&sdk_version=4.39.0&state={\"challenge\":\"aBcDeFghiJKlmnOpQRS%tU\",\"0_auth_logger_id\":\"01234ABC-12AB-34DE-1234-ABCDEFG12345\",\"com.facebook.some_identifier\":true,\"3_method\":\"sfvc_auth\"}" // swiftlint:disable:this line_length
    let encoded = "https%3A%2F%2Fm.facebook.com%2Fv3.2%2Fdialog%2Foauth%3Fauth_type%3Drerequest%26client_id%3D123456789%26default_audience%3Dfriends%26display%3Dtouch%26e2e%3D%7B%22init%22%3A123456.1234567890%7D%26fbapp_pres%3D0%26redirect_uri%3Dfb111111111111111%3A%2F%2Fauthorize%2F%26response_type%3Dtoken%2Csigned_request%26return_scopes%3Dtrue%26scope%3D%26sdk%3Dios%26sdk_version%3D4.39.0%26state%3D%7B%22challenge%22%3A%22aBcDeFghiJKlmnOpQRS%25tU%22%2C%220_auth_logger_id%22%3A%2201234ABC-12AB-34DE-1234-ABCDEFG12345%22%2C%22com.facebook.some_identifier%22%3Atrue%2C%223_method%22%3A%22sfvc_auth%22%7D" // swiftlint:disable:this line_length
    XCTAssertEqual(encoded, Utility.encode(urlString: url))
    XCTAssertEqual(url, Utility.decode(urlString: encoded))
  }

  @available(iOS, deprecated: 9.0)
  func testNewEncodeWorksLikeLegacy() {
    for num in 0 ..< 256 {
      let str = String(Character(UnicodeScalar(num)!)) // swiftlint:disable:this force_unwrapping
      if str == "{" || str == "}" {
        continue // Curly braces were not included in legacy URL encode
      }
      XCTAssertEqual(Utility.encode(urlString: str), legacyURLEncode(str))
    }
  }

  @available(iOS, deprecated: 9.0) // Needed to disable warning on CFURLCreateStringByAddingPercentEscapes
  func legacyURLEncode(_ value: String) -> String? {
    CFURLCreateStringByAddingPercentEscapes(
      nil,
      value as CFString,
      nil,
      ":!*();@/&?+$,='" as CFString,
      CFStringBuiltInEncodings.UTF8.rawValue
    ) 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.
 */

@testable import FBSDKCoreKit

import XCTest

final class ValueClampingTests: XCTestCase {
  func testIntegerClamping() {
    XCTAssertEqual((-1).fb_clamped(to: 0 ... 2), 0, .clampedValue)
    XCTAssertEqual(1.fb_clamped(to: 0 ... 2), 1, .clampedValue)
    XCTAssertEqual(3.fb_clamped(to: 0 ... 2), 2, .clampedValue)
  }

  func testDoubleClamping() {
    XCTAssertEqual((-1.0).fb_clamped(to: 0.0 ... 2.0), 0.0, .clampedValue)
    XCTAssertEqual(1.0.fb_clamped(to: 0.0 ... 2.0), 1.0, .clampedValue)
    XCTAssertEqual(3.0.fb_clamped(to: 0.0 ... 2.0), 2.0, .clampedValue)
  }
}

// MARK: - Assumptions

// swiftformat:disable:next extensionAccessControl
fileprivate extension String {
  static let clampedValue = """
    Clamping a comparable value to a range leaves an in-range value unaltered and moves an out-of-range value to the \
    nearest value in the range.
    """
}