/*
 * 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 AccessTokenExpirerTests: XCTestCase {

  let center = TestNotificationCenter()
  lazy var expirer = _AccessTokenExpirer(notificationCenter: center)

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

    AccessToken.current = nil
  }

  override func setUp() {
    super.setUp()

    expirer = _AccessTokenExpirer(notificationCenter: center)
  }

  override func tearDown() {
    super.tearDown()

    AccessToken.current = nil
  }

  func testCreating() {
    XCTAssertTrue(
      center.capturedAddObserverInvocations.contains(
        TestNotificationCenter.ObserverEvidence(
          observer: expirer as Any,
          name: .AccessTokenDidChange,
          selector: #selector(expirer.checkAccessTokenExpirationDate),
          object: nil
        )
      ),
      "Should check the access token expiration date when the shared token changes"
    )
    XCTAssertTrue(
      center.capturedAddObserverInvocations.contains(
        TestNotificationCenter.ObserverEvidence(
          observer: expirer as Any,
          name: .FBSDKApplicationDidBecomeActive,
          selector: #selector(expirer.checkAccessTokenExpirationDate),
          object: nil
        )
      ),
      "Should check the access token when the application becomes active"
    )
  }

  func testTimerFiring() throws {
    AccessToken.current = SampleAccessTokens.validToken

    expirer.timerDidFire()

    let userInfo = try XCTUnwrap(center.capturedPostUserInfos.first)

    XCTAssertEqual(
      center.capturedPostNames.first,
      .AccessTokenDidChange,
      "Should post about the updated access token when it changes"
    )
    XCTAssertEqual(
      userInfo[AccessTokenChangeNewKey] as? AccessToken,
      SampleAccessTokens.validToken,
      "Should include the new access token in the user info"
    )
    XCTAssertEqual(
      userInfo[AccessTokenChangeOldKey] as? AccessToken,
      SampleAccessTokens.validToken,
      "It will include the current token under the 'old' token key. This is probably wrong"
    )
    let didExpire = try XCTUnwrap(userInfo[AccessTokenDidExpireKey] as? Bool)

    XCTAssertTrue(
      didExpire,
      "The user info should include the information that the access token was expired"
    )
  }
}


/*
 * 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 AppEventsDeviceInfoTests: XCTestCase {

  // swiftlint:disable implicitly_unwrapped_optional
  var deviceInfo: _AppEventsDeviceInfo!
  var settings: TestSettings!
  // swiftlint:enable implicitly_unwrapped_optional

  override func setUp() {
    super.setUp()

    settings = TestSettings()
    deviceInfo = _AppEventsDeviceInfo()
    deviceInfo.configure(settings: settings)
  }

  override func tearDown() {
    settings = nil
    deviceInfo = nil

    super.tearDown()
  }

  func testDefaultDependencies() {
    deviceInfo = _AppEventsDeviceInfo()
    XCTAssertNil(
      deviceInfo.settings,
      "Should not have settings by default"
    )
  }

  func testConfiguringWithDependencies() {
    XCTAssertTrue(
      deviceInfo.settings === settings,
      "Should use the provided settings"
    )
  }
}


/*
 * 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 AppLinkFactoryTests: XCTestCase {

  let sourceURL = SampleURLs.valid(path: "source")
  let webURL = SampleURLs.valid(path: "webURL")
  let target = TestAppLinkTarget(url: nil, appStoreId: nil, appName: "foo")
  let isBackToReferrer = Bool.random()

  func testCreatingAppLink() {
    let factory = AppLinkFactory()
    guard let appLink = factory.createAppLink(
      sourceURL: sourceURL,
      targets: [target],
      webURL: webURL,
      isBackToReferrer: isBackToReferrer
    ) as? AppLink
    else {
      return XCTFail("Should create the app links of the expected concrete type")
    }

    XCTAssertEqual(
      appLink.sourceURL,
      sourceURL,
      "Should use the provided source URL to create the app link"
    )
    XCTAssertEqual(
      appLink.webURL,
      webURL,
      "Should use the provided web URL to create the app link"
    )
    XCTAssertTrue(
      appLink.targets[0] as? TestAppLinkTarget === target,
      "Should use the provided targets to create the app link"
    )
    XCTAssertEqual(
      appLink.isBackToReferrer,
      isBackToReferrer,
      "Should use the provided is back to referrer flag to create the app link"
    )
  }
}


/*
 * 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 AppLinkTargetFactoryTests: XCTestCase {

  let url = SampleURLs.valid
  let appStoreId = "123"

  func testCreatingAppLinkTarget() throws {
    let factory = AppLinkTargetFactory()
    let target = try XCTUnwrap(
      factory.createAppLinkTarget(
        url: url,
        appStoreId: appStoreId,
        appName: name
      ) as? AppLinkTarget,
      "Should create an app link target of the expected concrete type"
    )

    XCTAssertEqual(
      target.url,
      url,
      "Should use the provided url to create the app link target"
    )
    XCTAssertEqual(
      target.appStoreId,
      appStoreId,
      "Should use the provided app store identifier to create the app link target"
    )
    XCTAssertEqual(
      target.appName,
      name,
      "Should use the provided name to create the app link target"
    )
  }
}


/*
 * 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 AuthenticationStatusUtilityTests: XCTestCase {

  // swiftlint:disable implicitly_unwrapped_optional
  let url = URL(string: "m.facebook.com/platform/oidc/status/")! // swiftlint:disable:this force_unwrapping
  var sessionDataTask: TestSessionDataTask!
  var sessionDataTaskProvider: TestSessionProvider!
  // swiftlint:enable implicitly_unwrapped_optional

  override func setUp() {
    super.setUp()

    _AuthenticationStatusUtility.resetClassDependencies()

    TestAccessTokenWallet.stubbedCurrentAccessToken = SampleAccessTokens.validToken
    TestAuthenticationTokenWallet.current = SampleAuthenticationToken.validToken
    TestProfileProvider.current = SampleUserProfiles.createValid()

    sessionDataTask = TestSessionDataTask()
    sessionDataTaskProvider = TestSessionProvider()
    sessionDataTaskProvider.stubbedDataTask = sessionDataTask

    _AuthenticationStatusUtility.configure(
      profileSetter: TestProfileProvider.self,
      sessionDataTaskProvider: sessionDataTaskProvider,
      accessTokenWallet: TestAccessTokenWallet.self,
      authenticationTokenWallet: TestAuthenticationTokenWallet.self
    )
  }

  override func tearDown() {
    _AuthenticationStatusUtility.resetClassDependencies()
    TestAccessTokenWallet.reset()
    TestAuthenticationTokenWallet.reset()
    TestProfileProvider.reset()
    sessionDataTask = nil
    sessionDataTaskProvider = nil

    super.tearDown()
  }

  func testDefaultClassDependencies() {
    _AuthenticationStatusUtility.resetClassDependencies()

    XCTAssertNil(
      _AuthenticationStatusUtility.profileSetter,
      "Should not have a profile setter by default"
    )
    XCTAssertNil(
      _AuthenticationStatusUtility.sessionDataTaskProvider,
      "Should not have a session data task provider by default"
    )
    XCTAssertNil(
      _AuthenticationStatusUtility.accessTokenWallet,
      "Should not have an access token default"
    )
    XCTAssertNil(
      _AuthenticationStatusUtility.authenticationTokenWallet,
      "Should not have an authentication token by default"
    )
  }

  func testConfiguringWithCustomClassDependencies() {
    XCTAssertTrue(
      _AuthenticationStatusUtility.profileSetter === TestProfileProvider.self,
      "Should be able to set a custom profile setter"
    )
    XCTAssertTrue(
      _AuthenticationStatusUtility.sessionDataTaskProvider === sessionDataTaskProvider,
      "Should be able to set a custom session data task provider"
    )
    XCTAssertTrue(
      _AuthenticationStatusUtility.accessTokenWallet === TestAccessTokenWallet.self,
      "Should be able to set a custom access token"
    )
    XCTAssertTrue(
      _AuthenticationStatusUtility.authenticationTokenWallet === TestAuthenticationTokenWallet.self,
      "Should be able to set a custom authentication token"
    )
  }

  func testCheckAuthenticationStatusWithNoToken() {
    TestAuthenticationTokenWallet.current = nil
    _AuthenticationStatusUtility.checkAuthenticationStatus()

    XCTAssertNil(
      sessionDataTaskProvider.capturedRequest,
      "Should not create a request if there is no authentication token"
    )

    XCTAssertNotNil(
      TestAccessTokenWallet.current,
      "Should not reset the current access token on failure to check the status of an authentication token"
    )
    XCTAssertNotNil(
      TestProfileProvider.current,
      "Should not reset the current profile on failure to check the status of an authentication token"
    )
  }

  func testRequestURL() {
    let url = _AuthenticationStatusUtility._requestURL()

    XCTAssertEqual(url.host, "m.facebook.com")
    XCTAssertEqual(url.path, "/platform/oidc/status")

    let params = InternalUtility.shared.parameters(fromFBURL: url)
    XCTAssertNotNil(
      params["id_token"],
      "Incorrect ID token parameter in request url"
    )
  }

  func testHandleNotAuthorizedResponse() {
    let header = ["fb-s": "not_authorized"]
    let response = HTTPURLResponse(
      url: url,
      statusCode: 200,
      httpVersion: nil,
      headerFields: header
    )! // swiftlint:disable:this force_unwrapping

    _AuthenticationStatusUtility._handle(response)

    XCTAssertNil(
      TestAuthenticationTokenWallet.current,
      "Authentication token should be cleared when not authorized"
    )
    XCTAssertNil(
      TestAccessTokenWallet.current,
      "Access token should be cleared when not authorized"
    )
    XCTAssertNil(
      TestProfileProvider.current,
      "Profile should be cleared when not authorized"
    )
  }

  func testHandleConnectedResponse() {
    let header = ["fb-s": "connected"]
    let response = HTTPURLResponse(
      url: url,
      statusCode: 200,
      httpVersion: nil,
      headerFields: header
    )! // swiftlint:disable:this force_unwrapping

    _AuthenticationStatusUtility._handle(response)

    XCTAssertNotNil(
      TestAuthenticationTokenWallet.current,
      "Authentication token should not be cleared when connected"
    )
    XCTAssertNotNil(
      TestAccessTokenWallet.current,
      "Access token should not be cleared when connected"
    )
    XCTAssertNotNil(
      TestProfileProvider.current,
      "Profile should not be cleared when connected"
    )
  }

  func testHandleNoStatusResponse() {
    let response = HTTPURLResponse(
      url: url,
      statusCode: 200,
      httpVersion: nil,
      headerFields: [:]
    )! // swiftlint:disable:this force_unwrapping

    _AuthenticationStatusUtility._handle(response)

    XCTAssertNotNil(
      TestAuthenticationTokenWallet.current,
      "Authentication token should not be cleared when connected"
    )
    XCTAssertNotNil(
      TestAccessTokenWallet.current,
      "Access token should not be cleared when connected"
    )
    XCTAssertNotNil(
      TestProfileProvider.current,
      "Profile should not be cleared when connected"
    )
  }

  func testHandleResponseWithFuzzyData() {
    for _ in 0 ..< 100 {
      let header = [
        "fb-s": Fuzzer.random.description,
        "some_header_key": Fuzzer.random.description,
      ]

      let response = HTTPURLResponse(
        url: url,
        statusCode: 200,
        httpVersion: nil,
        headerFields: header as? [String: String]
      )! // swiftlint:disable:this force_unwrapping

      _AuthenticationStatusUtility._handle(response)
    }
  }
}


/*
 * 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 BackgroundEventLoggerTests: XCTestCase {

  // swiftlint:disable implicitly_unwrapped_optional
  var bundleWithIdentifier: TestBundle!
  var bundleWithoutIdentifier: TestBundle!
  var logger: TestEventLogger!
  var backgroundEventLogger: BackgroundEventLogger!
  // swiftlint:enable implicitly_unwrapped_optional

  override func setUp() {
    super.setUp()
    bundleWithIdentifier = TestBundle(infoDictionary: ["BGTaskSchedulerPermittedIdentifiers": ["123"]])
    bundleWithoutIdentifier = TestBundle()
    logger = TestEventLogger()

    BackgroundEventLogger.setDependencies(
      .init(
        infoDictionaryProvider: bundleWithIdentifier,
        eventLogger: logger
      )
    )
    backgroundEventLogger = BackgroundEventLogger()
  }

  override func tearDown() {
    bundleWithIdentifier = nil
    bundleWithoutIdentifier = nil
    logger = nil
    backgroundEventLogger = nil
    BackgroundEventLogger.resetDependencies()
    super.tearDown()
  }

  func testDefaultTypeDependencies() throws {
    BackgroundEventLogger.resetDependencies()
    let dependencies = try BackgroundEventLogger.getDependencies()

    XCTAssertIdentical(
      dependencies.infoDictionaryProvider as AnyObject,
      Bundle.main,
      .defaultDependency("the main bundle", for: "providing dictionaries")
    )

    XCTAssertIdentical(
      dependencies.eventLogger as AnyObject,
      AppEvents.shared,
      .defaultDependency("the shared AppEvents", for: "event logging")
    )
  }

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

    XCTAssertIdentical(
      dependencies.infoDictionaryProvider as AnyObject,
      bundleWithIdentifier,
      .customDependency(for: "providing dictionaries")
    )

    XCTAssertIdentical(
      dependencies.eventLogger as AnyObject,
      logger,
      .customDependency(for: "event logging")
    )
  }

  func testLogBackgroundStatusWithBackgroundRefreshStatusAvailable() {
    backgroundEventLogger.logBackgroundRefreshStatus(.available)

    XCTAssertEqual(
      logger.capturedEventName,
      .backgroundStatusAvailable,
      "AppEvents instance should log fb_sdk_background_status_available if background refresh status is available"
    )
  }

  func testLogBackgroundStatusWithBackgroundRefreshStatusDenied() {
    backgroundEventLogger.logBackgroundRefreshStatus(.denied)

    XCTAssertEqual(
      logger.capturedEventName,
      .backgroundStatusDenied,
      "AppEvents instance should log fb_sdk_background_status_denied if background refresh status is available"
    )
  }

  func testLogBackgroundStatusWithBackgroundRefreshStatusRestricted() {
    backgroundEventLogger.logBackgroundRefreshStatus(.restricted)

    XCTAssertEqual(
      logger.capturedEventName,
      .backgroundStatusRestricted,
      "AppEvents instance should log fb_sdk_background_status_restricted if background refresh status is available"
    )
  }

  func testIsNewBackgroundRefreshWithIdentifiers() {
    XCTAssertTrue(
      backgroundEventLogger.isNewBackgroundRefresh,
      "Should expect background refresh API is the new one if the identifier exists"
    )
  }

  func testIsNewBackgroundRefreshWithoutIdentifiers() {
    BackgroundEventLogger.resetDependencies()
    BackgroundEventLogger.setDependencies(
      .init(
        infoDictionaryProvider: bundleWithoutIdentifier,
        eventLogger: logger
      )
    )
    backgroundEventLogger = BackgroundEventLogger()

    XCTAssertFalse(
      backgroundEventLogger.isNewBackgroundRefresh,
      "Should expect background refresh API is the new one if the identifier exists"
    )
  }
}

// swiftformat:disable extensionaccesscontrol

// MARK: - Assumptions

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

  static func customDependency(for type: String) -> String {
    "The BackgroundEventLogger 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 Foundation
import TestTools
import XCTest

final class BasicUtilityTests: XCTestCase {

  func testInvalidObjectToJSONString() throws {
    let invalidObject = TestErrorFactory()

    XCTAssertThrowsError(
      _ = try BasicUtility.jsonString(for: invalidObject) { object, _ in object },
      "An error should be thrown"
    ) { error in
      let nsError = error as NSError
      XCTAssertEqual(nsError.code, 2, "An invalid argument error code should be used")
      XCTAssertEqual(
        nsError.userInfo["com.facebook.sdk:FBSDKErrorArgumentNameKey"] as? String,
        "object",
        "The error name should refer to the object parameter"
      )
      XCTAssertIdentical(
        nsError.userInfo["com.facebook.sdk:FBSDKErrorArgumentValueKey"] as AnyObject,
        invalidObject,
        "The error value should contain the object argument"
      )
      XCTAssertEqual(
        nsError.userInfo["com.facebook.sdk:FBSDKErrorDeveloperMessageKey"] as? String,
        "Invalid object for JSON serialization.",
        "The error message should indicate that the object argument cannot be serialized to JSON"
      )
    }
  }

  func testJSONString() throws {
    let urlString = "https://www.facebook.com"
    let url = URL(string: urlString)
    let dictionary = ["url": url]

    let jsonString = try BasicUtility.jsonString(for: dictionary, invalidObjectHandler: nil)
    XCTAssertEqual(jsonString, #"{"url":"https:\/\/www.facebook.com"}"#)
    let decoded = try XCTUnwrap(try BasicUtility.object(forJSONString: jsonString) as? [String: Any])

    XCTAssertEqual(Array(decoded.keys), ["url"])
    XCTAssertEqual(decoded["url"] as? String, urlString)
  }

  func testConvertRequestValue() throws {
    let result1 = BasicUtility.convertRequestValue(1)
    XCTAssertEqual(result1 as? String, "1")

    let value2 = try XCTUnwrap(URL(string: "https://test"))
    let result2 = BasicUtility.convertRequestValue(value2)
    XCTAssertEqual(result2 as? String, "https://test")

    let value3 = [String]()
    let result3 = BasicUtility.convertRequestValue(value3)
    XCTAssertTrue(result3 is [String])
  }

  func testQueryString() throws {
    let urlString = "http://example.com/path/to/page.html?key1&key2=value2&key3=value+3%20%3D%20foo#fragment=go"
    let urlQueryString = try XCTUnwrap(URL(string: urlString)?.query)
    let dictionary = BasicUtility.dictionary(withQueryString: urlQueryString)
    let expectedDictionary = [
      "key1": "",
      "key2": "value2",
      "key3": "value 3 = foo",
    ]
    XCTAssertEqual(dictionary, expectedDictionary)
    let queryString = try BasicUtility.queryString(with: dictionary, invalidObjectHandler: nil)
    let expectedQueryString = "key1=&key2=value2&key3=value%203%20%3D%20foo"
    XCTAssertEqual(queryString, expectedQueryString)

    // test repetition now that the query string has been cleaned and normalized
    let dictionary2 = BasicUtility.dictionary(withQueryString: queryString)
    XCTAssertEqual(dictionary2, expectedDictionary)
    let queryString2 = try BasicUtility.queryString(with: dictionary2, invalidObjectHandler: nil)
    XCTAssertEqual(queryString2, expectedQueryString)
  }

  func testURLEncode() {
    let value = "test this \"string\u{2019}s\" encoded value"
    let encoded = BasicUtility.urlEncode(value)
    XCTAssertEqual(encoded, "test%20this%20%22string%E2%80%99s%22%20encoded%20value")
    let decoded = BasicUtility.urlDecode(encoded)
    XCTAssertEqual(decoded, value)
  }

  func testURLEncodeSpecialCharacters() {
    let value = ":!*();@/&?#[]+$,='%\"\u{2019}"
    let encoded = BasicUtility.urlEncode(value)
    XCTAssertEqual(encoded, "%3A%21%2A%28%29%3B%40%2F%26%3F%23%5B%5D%2B%24%2C%3D%27%25%22%E2%80%99")
    let decoded = BasicUtility.urlDecode(encoded)
    XCTAssertEqual(decoded, value)
  }
}


/*
 * 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 CrashHandlerTests: XCTestCase {

  let testFileManager = TestFileManager(tempDirectoryURL: URL(fileURLWithPath: "1"))
  let testBundle = TestBundle()
  lazy var crashHandler = CrashHandler(
    fileManager: testFileManager,
    bundle: testBundle,
    dataExtractor: TestFileDataExtractor.self
  )

  func testCreatingWithCustomFileManager() {
    XCTAssertTrue(
      crashHandler.fileManager is TestFileManager,
      "Should be able to create with custom file managing"
    )
  }

  func testCreatingWithCustomBundle() {
    XCTAssertTrue(
      crashHandler.bundle is TestBundle,
      "Should be able to create with custom bundle"
    )
  }

  func testCreatingWithCustomDataExtractor() {
    XCTAssertTrue(
      crashHandler.dataExtractor is TestFileDataExtractor.Type,
      "Should be able to create with custom data extractor"
    )
  }

  func testGetFBSDKVersion() {
    XCTAssertEqual(
      CrashHandler.getFBSDKVersion(),
      Settings.shared.sdkVersion
    )
  }

  func testGetCrashLogFileNames() {
    let files = [
      "crash_log_1576471375.json",
      "crash_lib_data_05DEDC8AFC724E09A5E68190C492B92B.json",
      "DATA_DETECTION_ADDRESS_1.weights",
      "SUGGEST_EVENT_3.weights",
      "SUGGEST_EVENT_3.rules",
      "crash.text",
    ]

    let result = crashHandler._getCrashLogFileNames(files)

    XCTAssertTrue(result.contains("crash_log_1576471375.json"))
    XCTAssertFalse(result.contains("crash_lib_data_05DEDC8AFC724E09A5E68190C492B92B.json"))
    XCTAssertFalse(result.contains("DATA_DETECTION_ADDRESS_1.weights"))
    XCTAssertFalse(result.contains("SUGGEST_EVENT_3.weights"))
    XCTAssertFalse(result.contains("SUGGEST_EVENT_3.rules"))
    XCTAssertFalse(result.contains("crash.text"))
  }

  func testGettingFileNamesFromEmptyList() {
    XCTAssertTrue(
      crashHandler._getCrashLogFileNames([]).isEmpty,
      "Should not get file names from an empty list of names"
    )
  }

  func testGetPathToCrashFile() {
    let timeStamp = "test_timestamp"
    let crashLogFileName = "crash_log_\(timeStamp).json"
    let pathToCrashFile = crashHandler._getPath(toCrashFile: timeStamp)

    XCTAssertTrue(pathToCrashFile.hasSuffix(crashLogFileName))
  }

  func testCallStackContainsPrefix() {
    let prefixList = ["FBSDK", "_FBSDK"]

    let callStack1 = [
      "(2 DEV METHODS)",
      "-[FBSDKWebViewAppLinkResolver appLinkFromALData:destination:]+2110632",
      "-[FBSDKWebViewAppLinkResolver appLinkFromALData:destination:]+10540",
      "(14 DEV METHODS)",
    ]

    XCTAssertTrue(
      crashHandler._callstack(callStack1, containsPrefix: prefixList),
      """
      A callstack should be considered to contain a prefix if the first
      item in the stack begins with any of the provided prefixes
      """
    )

    let callStack2 = [
      "(2 DEV METHODS)",
      "-[FBAdPersistentCacheImpl storeAssetInMemory:forKey:expiration:]+14455428",
      "(12 DEV METHODS)",
    ]

    XCTAssertFalse(
      crashHandler._callstack(callStack2, containsPrefix: prefixList),
      """
      A callstack should be considered to contain a prefix if the first
      item in the stack begins with any of the provided prefixes
      """
    )
  }

  func testLoadCrashLogs() {
    let fileName = "dance_with_animals.txt"
    crashHandler._loadCrashLog(fileName)
    guard
      let path = TestFileDataExtractor.capturedFileNames.first,
      path.contains("dance_with_animals.txt")
    else {
      XCTFail("Loading a crash log should check the provided path for crashlog data")
      return
    }
  }

  func testSaveCrashLogs() {
    crashHandler._saveCrashLog(processedCrashLogs()[0])
    XCTAssertEqual(testBundle.capturedKeys, ["CFBundleShortVersionString", "CFBundleVersion"])
  }

  func testFilterCrashLogs() {
    let filteredCrashLogs = crashHandler._filterCrashLogs(
      ["FBSDK", "_FBSDK"],
      processedCrashLogs: processedCrashLogs()
    )
    XCTAssertEqual(1, filteredCrashLogs.count)
  }

  func processedCrashLogs() -> [[String: Any]] {
    let crashLog1 = [
      "app_version": "4.16(4)",
      "callstack": [
        "(2 DEV METHODS)",
        "-[FBSDKWebViewAppLinkResolver appLinkFromALData:destination:]+2110632",
        "-[FBSDKWebViewAppLinkResolver appLinkFromALData:destination:]+10540",
        "(14 DEV METHODS)",
      ],
      "reason": "InvalidOperationException",
      "timestamp": "1585764970",
      "device_model": "iPhone7,2",
      "device_os_version": "12.4.1",
    ] as [String: Any]

    let crashLog2 = [
      "app_version": "1.173.0(2)",
      "callstack": [
        "(3 DEV METHODS)",
        "-[SettingsItemViewController imageWithImage:destination:]+2110632",
        "(6 DEV METHODS)",
      ],
      "reason": "NSInvalidArgumentException",
      "timestamp": "1585764970",
      "device_model": "iPad4,1",
      "device_os_version": "12.4.5",
    ] as [String: Any]

    return [crashLog1, crashLog2]
  }
}


/*
 * 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 ErrorConfigurationTests: XCTestCase {

  let graphRequest = TestGraphRequest()
  let rawErrorCodeConfiguration = [
    [
      "name": "other",
      "items": [["code": 190, "subcodes": [459]]],
    ],
    [
      "name": "login",
      "items": [["code": 1, "subcodes": [12312]]],
      "recovery_message": "somemessage",
      "recovery_options": ["Yes", "No thanks"],
    ],
  ]

  func testErrorConfigurationDefaults() {
    let configuration = _ErrorConfiguration(dictionary: nil)

    XCTAssertEqual(
      .transient,
      configuration.recoveryConfiguration(
        forCode: "1",
        subcode: nil,
        request: graphRequest
      )?.errorCategory
    )
    XCTAssertEqual(
      .transient,
      configuration.recoveryConfiguration(
        forCode: "1",
        subcode: "12312",
        request: graphRequest
      )?.errorCategory
    )
    XCTAssertEqual(
      .transient,
      configuration.recoveryConfiguration(
        forCode: "2",
        subcode: "*",
        request: graphRequest
      )?.errorCategory
    )
    XCTAssertNil(
      configuration.recoveryConfiguration(
        forCode: nil,
        subcode: nil,
        request: graphRequest
      )
    )
    XCTAssertEqual(
      .recoverable,
      configuration.recoveryConfiguration(
        forCode: "190",
        subcode: "459",
        request: graphRequest
      )?.errorCategory
    )
    XCTAssertEqual(
      .recoverable,
      configuration.recoveryConfiguration(
        forCode: "190",
        subcode: "300",
        request: graphRequest
      )?.errorCategory
    )
    XCTAssertEqual(
      .recoverable,
      configuration.recoveryConfiguration(
        forCode: "190",
        subcode: "458",
        request: graphRequest
      )?.errorCategory
    )
    XCTAssertEqual(
      .recoverable,
      configuration.recoveryConfiguration(
        forCode: "102",
        subcode: "*",
        request: graphRequest
      )?.errorCategory
    )
    XCTAssertNil(
      configuration.recoveryConfiguration(
        forCode: "104",
        subcode: nil,
        request: graphRequest
      )
    )
  }

  func testErrorConfigurationAdditonalArray() throws {
    let intermediaryConfiguration = _ErrorConfiguration(dictionary: nil)
    intermediaryConfiguration.update(with: rawErrorCodeConfiguration)
    let data = try NSKeyedArchiver.archivedData(
      withRootObject: intermediaryConfiguration,
      requiringSecureCoding: true
    )

    let configuration = try NSKeyedUnarchiver.unarchivedObject(
      ofClass: _ErrorConfiguration.self, from: data
    )! // swiftlint:disable:this force_unwrapping
    XCTAssertEqual(
      .transient,
      configuration.recoveryConfiguration(
        forCode: "1",
        subcode: nil,
        request: graphRequest
      )?.errorCategory
    )
    XCTAssertEqual(
      .recoverable,
      configuration.recoveryConfiguration(
        forCode: "1",
        subcode: "12312",
        request: graphRequest
      )?.errorCategory
    )
    XCTAssertEqual(
      .transient,
      configuration.recoveryConfiguration(
        forCode: "2",
        subcode: "*",
        request: graphRequest
      )?.errorCategory
    )
    XCTAssertNil(
      configuration.recoveryConfiguration(
        forCode: nil,
        subcode: nil,
        request: graphRequest
      )
    )
    XCTAssertEqual(
      .other,
      configuration.recoveryConfiguration(
        forCode: "190",
        subcode: "459",
        request: graphRequest
      )?.errorCategory
    )
    XCTAssertEqual(
      GraphRequestError.recoverable,
      configuration.recoveryConfiguration(
        forCode: "190",
        subcode: "300",
        request: graphRequest
      )?.errorCategory
    )
    XCTAssertEqual(
      .recoverable,
      configuration.recoveryConfiguration(
        forCode: "102",
        subcode: "*",
        request: graphRequest
      )?.errorCategory
    )
  }

  func testParsingRandomName() {
    for _ in 0 ..< 100 {
      let array = [
        [
          "name": Fuzzer.random,
          "items": [["code": 190, "subcodes": [459]]],
        ],
        [
          "name": "login",
          "items": [["code": 1, "subcodes": [12312]]],
          "recovery_message": "somemessage",
          "recovery_options": ["Yes", "No thanks"],
        ],
      ]
      let configuration = _ErrorConfiguration(dictionary: nil)
      configuration.update(with: array)
    }
  }

  func testParsingRandomSubcodes() {
    for _ in 0 ..< 100 {
      let array = [
        [
          "name": "other",
          "items": [["code": 190, "subcodes": [Fuzzer.random]]],
        ],
        [
          "name": "login",
          "items": [["code": 1, "subcodes": [Fuzzer.random]]],
          "recovery_message": "somemessage",
          "recovery_options": ["Yes", "No thanks"],
        ],
      ]
      let configuration = _ErrorConfiguration(dictionary: nil)
      configuration.update(with: array)
    }
  }

  func testParsingRandomCodes() {
    for _ in 0 ..< 100 {
      let array = [
        [
          "name": "other",
          "items": [["code": Fuzzer.random, "subcodes": [459]]],
        ],
        [
          "name": "login",
          "items": [["code": Fuzzer.random, "subcodes": [12312]]],
          "recovery_message": "somemessage",
          "recovery_options": ["Yes", "No thanks"],
        ],
      ]
      let configuration = _ErrorConfiguration(dictionary: nil)
      configuration.update(with: array)
    }
  }

  func testParsingRandomItemDictionaries() {
    for _ in 0 ..< 100 {
      let array = [
        [
          "name": "other",
          "items": Fuzzer.random,
        ],
        [
          "name": "login",
          "items": Fuzzer.random,
          "recovery_message": "somemessage",
          "recovery_options": ["Yes", "No thanks"],
        ],
      ]
      let configuration = _ErrorConfiguration(dictionary: nil)
      configuration.update(with: array)
    }
  }

  func testParsingRandomRecoveryOptionsArray() {
    for _ in 0 ..< 100 {
      let array = [
        [
          "name": "other",
          "items": [["code": 190, "subcodes": [459]]],
        ],
        [
          "name": "login",
          "items": [["code": 1, "subcodes": [12312]]],
          "recovery_message": "somemessage",
          "recovery_options": [Fuzzer.random, Fuzzer.random],
        ],
      ]
      let configuration = _ErrorConfiguration(dictionary: nil)
      configuration.update(with: array)
    }
  }

  func testParsingRandomRecoveryOptions() {
    for _ in 0 ..< 100 {
      let array = [
        [
          "name": "other",
          "items": [["code": 190, "subcodes": [459]]],
        ],
        [
          "name": "login",
          "items": [["code": 1, "subcodes": [12312]]],
          "recovery_message": "somemessage",
          "recovery_options": Fuzzer.random,
        ],
      ]
      let configuration = _ErrorConfiguration(dictionary: nil)
      configuration.update(with: array)
    }
  }

  func testParsingRecoveryMessageWithoutOptions() {
    for _ in 0 ..< 100 {
      let array = [
        [
          "name": "other",
          "items": [["code": 190, "subcodes": [459]]],
        ],
        [
          "name": "login",
          "items": [["code": 1, "subcodes": [12312]]],
          "recovery_message": "somemessage",
          "recovery_options": ["Yes", "No thanks"],
        ],
      ]
      let configuration = _ErrorConfiguration(dictionary: nil)
      configuration.update(with: array)
    }
  }

  func testParsingRandomEntries() {
    for _ in 0 ..< 100 {
      // swiftlint:disable:next force_cast
      let array = Fuzzer.randomize(json: rawErrorCodeConfiguration) as! [[String: Any]]
      let configuration = _ErrorConfiguration(dictionary: nil)
      configuration.update(with: array)
    }
  }
}


/*
 * 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 FeatureManagerTests: XCTestCase {

  // swiftlint:disable implicitly_unwrapped_optional
  var manager: _FeatureManager!
  var settings: TestSettings!
  var store: UserDefaultsSpy!
  let userDefaultsPrefix = "com.facebook.sdk:FBSDKFeatureManager.FBSDKFeature"
  let gatekeeperKeyPrefix = "FBSDKFeature"
  // swiftlint:enable implicitly_unwrapped_optional

  override func setUp() {
    super.setUp()

    TestGateKeeperManager.reset()
    settings = TestSettings()
    store = UserDefaultsSpy()

    manager = _FeatureManager()
    _FeatureManager.setDependencies(
      .init(
        gateKeeperManager: TestGateKeeperManager.self,
        settings: settings,
        store: store
      )
    )
  }

  override func tearDown() {
    super.tearDown()

    _FeatureManager.resetDependencies()
    TestGateKeeperManager.reset()
    settings = nil
    store = nil
    manager = nil
  }

  func testDefaultTypeDependencies() throws {
    _FeatureManager.resetDependencies()
    let dependencies = try _FeatureManager.getDependencies()

    XCTAssertIdentical(
      dependencies.gateKeeperManager as AnyObject,
      _GateKeeperManager.self,
      .defaultDependency("the gatekeeper manager", for: "gatekeeper managing")
    )

    XCTAssertIdentical(
      dependencies.settings as AnyObject,
      Settings.shared,
      .defaultDependency("the shared settings", for: "settings sharing")
    )

    XCTAssertIdentical(
      dependencies.store as AnyObject,
      UserDefaults.standard,
      .defaultDependency("the shared user defaults", for: "data persisting")
    )
  }

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

    XCTAssertIdentical(
      dependencies.gateKeeperManager as AnyObject,
      TestGateKeeperManager.self,
      .customDependency(for: "gatekeeper managing")
    )

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

    XCTAssertIdentical(
      dependencies.store as AnyObject,
      store,
      .customDependency(for: "data persisting")
    )
  }

  func testCheckingCoreFeature() {
    var capturedEnabled = false
    manager.check(.core) { enabled in
      capturedEnabled = enabled
    }

    TestGateKeeperManager.capturedLoadGateKeepersCompletion?(nil)

    XCTAssertTrue(
      capturedEnabled,
      "CoreKit shiould always be considered enabled"
    )
    XCTAssertTrue(
      TestGateKeeperManager.loadGateKeepersWasCalled,
      "Checking if core is enabled should load the gatekeepers"
    )
    XCTAssertTrue(
      TestGateKeeperManager.capturedBoolForGateKeeperKeys.isEmpty,
      "Checking if core is enabled should not check for the loaded gatekeepers"
    )
  }

  func testCheckingTopLevelFeatures() {
    let testData: [(feature: SDKFeature, name: String)] = [
      (.appEvents, "AppEvents"),
      (.instrument, "Instrument"),
      (.login, "LoginKit"),
      (.share, "ShareKit"),
      (.gamingServices, "GamingServicesKit"),
    ]
    testData.forEach { data in
      manager.check(data.feature) { _ in }

      TestGateKeeperManager.capturedLoadGateKeepersCompletion?(nil)

      XCTAssertTrue(
        TestGateKeeperManager.loadGateKeepersWasCalled,
        "Checking if \(data.name) is enabled should load the gatekeepers"
      )
      XCTAssertEqual(
        TestGateKeeperManager.capturedBoolForGateKeeperKeys.first,
        gatekeeperKeyPrefix + data.name,
        """
        Checking if top-level feature: \(data.name) is enabled
        should check for the feature name: \(data.name) in the loaded gatekeepers
        """
      )
      TestGateKeeperManager.reset()
    }
  }

  func testCheckingNonTopLevelFeaturesWithParentFeaturesEnabled() {
    let testData: [(feature: SDKFeature, parentFeatureName: String, name: String)] = [
      (.codelessEvents, "AppEvents", "CodelessEvents"),
      (.restrictiveDataFiltering, "AppEvents", "RestrictiveDataFiltering"),
      (.AAM, "AppEvents", "AAM"),
      (.privacyProtection, "AppEvents", "PrivacyProtection"),
      (.suggestedEvents, "AppEvents", "SuggestedEvents"),
      (.intelligentIntegrity, "AppEvents", "IntelligentIntegrity"),
      (.modelRequest, "AppEvents", "ModelRequest"),
      (.eventDeactivation, "AppEvents", "EventDeactivation"),
      (.skAdNetwork, "AppEvents", "SKAdNetwork"),
      (.skAdNetworkConversionValue, "AppEvents", "SKAdNetworkConversionValue"),
      (.ateLogging, "AppEvents", "ATELogging"),
      (.crashReport, "Instrument", "CrashReport"),
      (.crashShield, "Instrument", "CrashShield"),
      (.errorReport, "Instrument", "ErrorReport"),
    ]
    testData.forEach { data in
      manager.check(data.feature) { _ in }

      TestGateKeeperManager.capturedLoadGateKeepersCompletion?(nil)

      XCTAssertTrue(
        TestGateKeeperManager.loadGateKeepersWasCalled,
        "Checking if \(data.name) is enabled should load the gatekeepers"
      )
      let key = gatekeeperKeyPrefix + data.parentFeatureName
      XCTAssertTrue(
        TestGateKeeperManager.capturedBoolForGateKeeperKeys.contains(key),
        """
        Checking if top-level feature: \(data.parentFeatureName) is enabled
        should check for the feature name: \(data.name) in the loaded gatekeepers
        """
      )
      TestGateKeeperManager.reset()
    }
  }

  func testChecksIfFeaturesAreDisabledBeforeCheckingGateKeeper() {
    let testData: [(feature: SDKFeature, name: String)] = [
      (.appEvents, "AppEvents"),
      (.instrument, "Instrument"),
      (.login, "LoginKit"),
      (.share, "ShareKit"),
      (.gamingServices, "GamingServicesKit"),
      (.codelessEvents, "CodelessEvents"),
      (.restrictiveDataFiltering, "RestrictiveDataFiltering"),
      (.AAM, "AAM"),
      (.privacyProtection, "PrivacyProtection"),
      (.suggestedEvents, "SuggestedEvents"),
      (.intelligentIntegrity, "IntelligentIntegrity"),
      (.modelRequest, "ModelRequest"),
      (.eventDeactivation, "EventDeactivation"),
      (.skAdNetwork, "SKAdNetwork"),
      (.skAdNetworkConversionValue, "SKAdNetworkConversionValue"),
      (.ateLogging, "ATELogging"),
      (.crashReport, "CrashReport"),
      (.crashShield, "CrashShield"),
      (.errorReport, "ErrorReport"),
    ]
    testData.forEach { data in
      var capturedKey: String?
      store.stringForKeyCallback = { key in
        capturedKey = key
        // The existence of the sdk version for a feature key in user defaults
        // is interpreted to mean that the feature is disabled for that version
        return self.settings.sdkVersion
      }
      manager.check(data.feature) { _ in }

      TestGateKeeperManager.capturedLoadGateKeepersCompletion?(nil)

      XCTAssertEqual(
        capturedKey,
        userDefaultsPrefix + data.name,
        "Should check if the feature has been disabled as indicated by its existence in user defaults"
      )
      XCTAssertFalse(
        TestGateKeeperManager.loadGateKeepersWasCalled,
        "Should not check the gatekeeper for \(data.name) if the feature is disabled"
      )
      TestGateKeeperManager.reset()
    }
  }

  func testDisablingFeatures() {
    let testData: [SDKFeature] = [
      .none,
      .AAM,
      .codelessEvents,
      .restrictiveDataFiltering,
      .errorReport,
      .privacyProtection,
      .suggestedEvents,
      .intelligentIntegrity,
      .eventDeactivation,
      .skAdNetworkConversionValue,
    ]

    testData.forEach { featureName in
      manager.disableFeature(featureName)
      let featureString = _FeatureManager.shared.featureName(for: featureName)
      XCTAssertEqual(
        store.capturedSetObjectKey,
        userDefaultsPrefix + featureString
      )
    }
  }
}

// swiftformat:disable extensionaccesscontrol

// MARK: - Assumptions

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

  static func customDependency(for type: String) -> String {
    "The _FeatureManager 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 InternalUtilityTests: XCTestCase {

  let validParameters = ["foo": "bar"]
  let validPath = "example"
  let facebookUrlSchemeMissingMessage = "fbapi is missing from your Info.plist under LSApplicationQueriesSchemes and is required." // swiftlint:disable:this line_length
  let messengerUrlSchemeMissingMessage = "fb-messenger-share-api is missing from your Info.plist under LSApplicationQueriesSchemes and is required." // swiftlint:disable:this line_length
  let dialogURL = URL(
    string: "https://m.facebook.com/\(FBSDK_DEFAULT_GRAPH_API_VERSION)/dialog"
  )! // swiftlint:disable:this force_unwrapping
  let nonDialogURL = URL(
    string: "https://m.facebook.com/\(FBSDK_DEFAULT_GRAPH_API_VERSION)/foo"
  )! // swiftlint:disable:this force_unwrapping

  // swiftlint:disable implicitly_unwrapped_optional
  var internalUtility: InternalUtility!
  var bundle: TestBundle!
  var loggerFactory: TestLoggerFactory!
  var logger: TestLogger!
  var settings: TestSettings!
  var errorFactory: TestErrorFactory!
  // swiftlint:enable implicitly_unwrapped_optional force_unwrapping

  override func setUp() {
    super.setUp()

    InternalUtility.reset()

    bundle = TestBundle()
    loggerFactory = TestLoggerFactory()
    logger = TestLogger(loggingBehavior: .developerErrors)
    loggerFactory.logger = logger
    settings = TestSettings()
    errorFactory = TestErrorFactory()

    internalUtility = InternalUtility()
    internalUtility.deleteFacebookCookies()
    configureInternalUtility()
  }

  override func tearDown() {
    InternalUtility.reset()

    bundle = nil
    loggerFactory = nil
    logger = nil
    settings = nil
    errorFactory = nil
    internalUtility = nil

    super.tearDown()
  }

  func configureInternalUtility() {
    internalUtility.configure(
      infoDictionaryProvider: bundle,
      loggerFactory: loggerFactory,
      settings: settings,
      errorFactory: errorFactory
    )
  }

  func testDefaultDependencies() {
    internalUtility = InternalUtility()

    XCTAssertNil(
      internalUtility.infoDictionaryProvider,
      "Should not have an info dictionary provider by default"
    )
    XCTAssertNil(
      internalUtility.loggerFactory,
      "Should not have a logger factory by default"
    )
    XCTAssertNil(
      internalUtility.settings,
      "Should not have settings by default"
    )
    XCTAssertNil(
      internalUtility.errorFactory,
      "Should not have an error factory by default"
    )
  }

  func testConfiguringWithDependencies() {
    XCTAssertTrue(
      internalUtility.infoDictionaryProvider === bundle,
      "Should be able to provide an info dictionary provider"
    )
    XCTAssertTrue(
      internalUtility.loggerFactory === loggerFactory,
      "The shared instance should use the provided logger factory"
    )
    XCTAssertTrue(
      internalUtility.settings === settings,
      "The shared instance should use the provided settings"
    )
    XCTAssertIdentical(
      internalUtility.errorFactory,
      errorFactory,
      "The shared instance should use the provided error factory"
    )
  }

  func testFacebookURL() throws {
    settings.facebookDomainPart = ""
    var urlString = ""

    urlString = try internalUtility.facebookURL(
      hostPrefix: "",
      path: "",
      queryParameters: [:]
    ).absoluteString

    XCTAssertEqual(urlString, "https://facebook.com/\(FBSDK_DEFAULT_GRAPH_API_VERSION)")

    urlString = try internalUtility.facebookURL(
      hostPrefix: "m.",
      path: "",
      queryParameters: [:]
    ).absoluteString
    XCTAssertEqual(urlString, "https://m.facebook.com/\(FBSDK_DEFAULT_GRAPH_API_VERSION)")

    urlString = try internalUtility.facebookURL(
      hostPrefix: "m",
      path: "",
      queryParameters: [:]
    ).absoluteString
    XCTAssertEqual(urlString, "https://m.facebook.com/\(FBSDK_DEFAULT_GRAPH_API_VERSION)")

    urlString = try internalUtility.facebookURL(
      hostPrefix: "m",
      path: "/dialog/share",
      queryParameters: [:]
    ).absoluteString
    XCTAssertEqual(urlString, "https://m.facebook.com/\(FBSDK_DEFAULT_GRAPH_API_VERSION)/dialog/share")

    urlString = try internalUtility.facebookURL(
      hostPrefix: "m",
      path: "dialog/share",
      queryParameters: [:]
    ).absoluteString
    XCTAssertEqual(urlString, "https://m.facebook.com/\(FBSDK_DEFAULT_GRAPH_API_VERSION)/dialog/share")

    urlString = try internalUtility.facebookURL(
      hostPrefix: "m",
      path: "dialog/share",
      queryParameters: ["key": "value"]
    ).absoluteString
    XCTAssertEqual(
      urlString,
      "https://m.facebook.com/\(FBSDK_DEFAULT_GRAPH_API_VERSION)/dialog/share?key=value"
    )

    urlString = try internalUtility.facebookURL(
      hostPrefix: "m",
      path: "/v1.0/dialog/share",
      queryParameters: [:]
    ).absoluteString
    XCTAssertEqual(urlString, "https://m.facebook.com/v1.0/dialog/share")

    urlString = try internalUtility.facebookURL(
      withHostPrefix: "m",
      path: "/dialog/share",
      queryParameters: [:],
      defaultVersion: "v2.0"
    ).absoluteString
    XCTAssertEqual(urlString, "https://m.facebook.com/v2.0/dialog/share")

    urlString = try internalUtility.facebookURL(
      withHostPrefix: "m",
      path: "/v1.0/dialog/share",
      queryParameters: [:],
      defaultVersion: "v2.0"
    ).absoluteString
    XCTAssertEqual(urlString, "https://m.facebook.com/v1.0/dialog/share")

    urlString = try internalUtility.facebookURL(
      hostPrefix: "m",
      path: "/v987654321.2/dialog/share",
      queryParameters: [:]
    ).absoluteString
    XCTAssertEqual(urlString, "https://m.facebook.com/v987654321.2/dialog/share")

    urlString = try internalUtility.facebookURL(
      withHostPrefix: "m",
      path: "/v.1/dialog/share",
      queryParameters: [:],
      defaultVersion: "v2.0"
    ).absoluteString
    XCTAssertEqual(urlString, "https://m.facebook.com/v2.0/v.1/dialog/share")

    urlString = try internalUtility.facebookURL(
      withHostPrefix: "m",
      path: "/v1/dialog/share",
      queryParameters: [:],
      defaultVersion: "v2.0"
    ).absoluteString
    XCTAssertEqual(urlString, "https://m.facebook.com/v2.0/v1/dialog/share")

    settings.graphAPIVersion = "v3.3"
    urlString = try internalUtility.facebookURL(
      withHostPrefix: "m",
      path: "/v1/dialog/share",
      queryParameters: [:],
      defaultVersion: ""
    ).absoluteString
    XCTAssertEqual(urlString, "https://m.facebook.com/v3.3/v1/dialog/share")

    settings.graphAPIVersion = FBSDK_DEFAULT_GRAPH_API_VERSION
    urlString = try internalUtility.facebookURL(
      withHostPrefix: "m",
      path: "/dialog/share",
      queryParameters: [:],
      defaultVersion: ""
    ).absoluteString
    XCTAssertEqual(urlString, "https://m.facebook.com/\(FBSDK_DEFAULT_GRAPH_API_VERSION)/dialog/share")
  }

  func testFacebookGamingURL() throws {
    settings.facebookDomainPart = ""
    let authToken = AuthenticationToken(
      tokenString: "token_string",
      nonce: "nonce",
      graphDomain: "gaming"
    )
    AuthenticationToken.current = authToken

    var urlString = try internalUtility.facebookURL(
      hostPrefix: "graph",
      path: "",
      queryParameters: [:]
    ).absoluteString
    XCTAssertEqual(urlString, "https://graph.fb.gg/\(FBSDK_DEFAULT_GRAPH_API_VERSION)")

    urlString = try internalUtility.facebookURL(
      hostPrefix: "graph-video",
      path: "",
      queryParameters: [:]
    ).absoluteString
    XCTAssertEqual(urlString, "https://graph-video.fb.gg/\(FBSDK_DEFAULT_GRAPH_API_VERSION)")
  }

  // MARK: - Extracting Permissions

  func testParsingPermissionsWithFuzzyValues() {
    // A lack of a runtime crash is considered a success here.
    (1 ... 100).forEach { _ in
      internalUtility.extractPermissions(
        fromResponse: SampleRawRemotePermissionList.randomValues,
        grantedPermissions: [],
        declinedPermissions: [],
        expiredPermissions: []
      )
    }
  }

  func testExtractingPermissionsFromResponseWithInvalidTopLevelKey() {
    let grantedPermissions = NSMutableSet()
    let declinedPermissions = NSMutableSet()
    let expiredPermissions = NSMutableSet()

    internalUtility.extractPermissions(
      fromResponse: SampleRawRemotePermissionList.missingTopLevelKey,
      grantedPermissions: grantedPermissions,
      declinedPermissions: declinedPermissions,
      expiredPermissions: expiredPermissions
    )
    XCTAssertEqual(grantedPermissions.count, 0, "Should not add granted permissions if top level key is missing")
    XCTAssertEqual(declinedPermissions.count, 0, "Should not add declined permissions if top level key is missing")
    XCTAssertEqual(expiredPermissions.count, 0, "Should not add expired permissions if top level key is missing")
  }

  func testExtractingPermissionsFromResponseWithMissingPermissions() {
    let grantedPermissions = NSMutableSet()
    let declinedPermissions = NSMutableSet()
    let expiredPermissions = NSMutableSet()

    internalUtility.extractPermissions(
      fromResponse: SampleRawRemotePermissionList.missingPermissions,
      grantedPermissions: grantedPermissions,
      declinedPermissions: declinedPermissions,
      expiredPermissions: expiredPermissions
    )
    XCTAssertEqual(grantedPermissions.count, 0, "Should not add missing granted permissions")
    XCTAssertEqual(declinedPermissions.count, 0, "Should not add missing declined permissions")
    XCTAssertEqual(expiredPermissions.count, 0, "Should not add missing expired permissions")
  }

  func testExtractingPermissionsFromResponseWithMissingStatus() {
    let grantedPermissions = NSMutableSet()
    let declinedPermissions = NSMutableSet()
    let expiredPermissions = NSMutableSet()

    internalUtility.extractPermissions(
      fromResponse: SampleRawRemotePermissionList.missingStatus,
      grantedPermissions: grantedPermissions,
      declinedPermissions: declinedPermissions,
      expiredPermissions: expiredPermissions
    )
    XCTAssertEqual(grantedPermissions.count, 0, "Should not add a permission with a missing status")
    XCTAssertEqual(declinedPermissions.count, 0, "Should not add a permission with a missing status")
    XCTAssertEqual(expiredPermissions.count, 0, "Should not add a permission with a missing status")
  }

  func testExtractingPermissionsFromResponseWithValidPermissions() {
    let grantedPermissions = NSMutableSet()
    let declinedPermissions = NSMutableSet()
    let expiredPermissions = NSMutableSet()

    internalUtility.extractPermissions(
      fromResponse: SampleRawRemotePermissionList.validAllStatuses,
      grantedPermissions: grantedPermissions,
      declinedPermissions: declinedPermissions,
      expiredPermissions: expiredPermissions
    )
    XCTAssertEqual(grantedPermissions.count, 1, "Should add granted permissions when available")
    XCTAssertTrue(grantedPermissions.contains("email"), "Should add the correct permission to granted permissions")

    XCTAssertEqual(declinedPermissions.count, 1, "Should add declined permissions when available")
    XCTAssertTrue(declinedPermissions.contains("birthday"), "Should add the correct permission to declined permissions")

    XCTAssertEqual(expiredPermissions.count, 1, "Should add expired permissions when available")
    XCTAssertTrue(expiredPermissions.contains("first_name"), "Should add the correct permission to expired permissions")
  }

  // MARK: - Can open URL scheme

  func testCanOpenUrlSchemeWithMissingScheme() {
    XCTAssertFalse(
      internalUtility._canOpenURLScheme(nil),
      "Should not be able to open a missing scheme"
    )

    XCTAssertNil(logger.capturedContents, "A developer error should not be logged for a nil scheme")
  }

  func testCanOpenUrlSchemeWithInvalidSchemes() {
    [
      "http: ",
      "",
      "   ",
      "#@%(*&#$(^#@!$",
      "////foo",
      "foo: ",
      "foo: /",
    ]
      .forEach { invalidScheme in
        internalUtility._canOpenURLScheme(invalidScheme)

        verifyTestLoggerInvoked(
          loggingBehavior: .developerErrors,
          logEntry: "Invalid URL scheme provided: \(invalidScheme)"
        )
      }
  }

  func testCanOpenUrlSchemeWithValidSchemes() {
    [
      "foo",
      "FOO",
      "foo+bar",
      "foo-bar",
      "foo.bar",
    ]
      .forEach { validScheme in
        internalUtility._canOpenURLScheme(validScheme)
        XCTAssertNil(logger.capturedContents, "A developer error should not be logged for valid schemes")
      }
  }

  // MARK: - App URL Scheme

  func testAppURLSchemeWithMissingAppIdMissingSuffix() {
    settings.appID = nil
    settings.appURLSchemeSuffix = nil
    // This is not desired behavior but accurately reflects what is currently written.
    XCTAssertEqual(
      internalUtility.appURLScheme,
      "fb",
      "Should return an app url scheme derived from the app id and app url scheme suffix"
    )
  }

  func testAppURLSchemeWithMissingAppIdInvalidSuffix() {
    settings.appID = nil
    settings.appURLSchemeSuffix = "   "
    // This is not desired behavior but accurately reflects what is currently written.
    XCTAssertEqual(
      internalUtility.appURLScheme,
      "fb   ",
      "Should return an app url scheme derived from the app id and app url scheme suffix"
    )
  }

  func testAppURLSchemeWithMissingAppIdValidSuffix() {
    settings.appID = nil
    settings.appURLSchemeSuffix = "foo"
    // This is not desired behavior but accurately reflects what is currently written.
    XCTAssertEqual(
      internalUtility.appURLScheme,
      "fbfoo",
      "Should return an app url scheme derived from the app id and app url scheme suffix"
    )
  }

  func testAppURLSchemeWithInvalidAppIdMissingSuffix() {
    let appID = " "
    settings.appID = appID
    settings.appURLSchemeSuffix = nil
    let expected = "fb\(appID)"

    // This is not desired behavior but accurately reflects what is currently written.
    XCTAssertEqual(
      internalUtility.appURLScheme,
      expected,
      "Should return an app url scheme derived app id and app url scheme suffix defined in settings"
    )
  }

  func testAppURLSchemeWithInvalidAppIdInvalidSuffix() {
    let appID = " "
    let suffix = " "
    settings.appID = appID
    settings.appURLSchemeSuffix = suffix
    let expected = "fb\(appID)\(suffix)"

    // This is not desired behavior but accurately reflects what is currently written.
    XCTAssertEqual(
      internalUtility.appURLScheme,
      expected,
      "Should return an app url scheme derived app id and app url scheme suffix defined in settings"
    )
  }

  func testAppURLSchemeWithInvalidAppIdValidSuffix() {
    let appID = " "
    let suffix = "foo"
    settings.appID = appID
    settings.appURLSchemeSuffix = suffix
    let expected = "fb\(appID)\(suffix)"

    // This is not desired behavior but accurately reflects what is currently written.
    XCTAssertEqual(
      internalUtility.appURLScheme,
      expected,
      "Should return an app url scheme derived app id and app url scheme suffix defined in settings"
    )
  }

  func testAppURLSchemeWithValidAppIdMissingSuffix() {
    let appID = "appid"
    settings.appID = appID
    settings.appURLSchemeSuffix = nil
    let expected = "fb\(appID)"

    XCTAssertEqual(
      internalUtility.appURLScheme,
      expected,
      "Should return an app url scheme derived app id and app url scheme suffix defined in settings"
    )
  }

  func testAppURLSchemeWithValidAppIdInvalidSuffix() {
    let appID = "appid"
    let suffix = "   "
    settings.appID = appID
    settings.appURLSchemeSuffix = suffix
    let expected = "fb\(appID)\(suffix)"

    // This is not desired behavior but accurately reflects what is currently written.
    XCTAssertEqual(
      internalUtility.appURLScheme,
      expected,
      "Should return an app url scheme derived app id and app url scheme suffix defined in settings"
    )
  }

  func testAppURLSchemeWithValidAppIdValidSuffix() {
    let appID = "appid"
    let suffix = "foo"
    settings.appID = appID
    settings.appURLSchemeSuffix = suffix
    let expected = "fb\(appID)\(suffix)"

    XCTAssertEqual(
      internalUtility.appURLScheme,
      expected,
      "Should return an app url scheme derived app id and app url scheme suffix defined in settings"
    )
  }

  // MARK: - App URL with host

  func testAppUrlWithEmptyHost() throws {
    settings.appID = "appid"
    settings.appURLSchemeSuffix = "foo"

    let url = try internalUtility.appURL(
      withHost: "",
      path: validPath,
      queryParameters: validParameters
    )

    XCTAssertNil(url.host, "Should not set an empty host.")
  }

  func testAppUrlWithValidHost() throws {
    settings.appID = "appid"
    settings.appURLSchemeSuffix = "foo"

    let url = try internalUtility.appURL(
      withHost: "facebook",
      path: validPath,
      queryParameters: validParameters
    )

    XCTAssertEqual(url.host, "facebook", "Should set the expected host.")
  }

  // MARK: - Check registered can open url scheme

  func testCheckRegisteredCanOpenURLScheme() {
    let scheme = "foo"

    internalUtility.checkRegisteredCanOpenURLScheme(scheme)

    verifyTestLoggerInvoked(
      loggingBehavior: .developerErrors,
      logEntry: "\(scheme) is missing from your Info.plist under LSApplicationQueriesSchemes and is required."
    )
  }

  func testCheckRegisteredCanOpenURLSchemeMultipleTimes() {
    let scheme = "foo"

    XCTAssertEqual(logger.logEntryCallCount, 0, "There should not be developer errors logged initially")

    internalUtility.checkRegisteredCanOpenURLScheme(scheme)

    XCTAssertEqual(logger.logEntryCallCount, 1, "One developer error should be logged")
    verifyTestLoggerInvoked(
      loggingBehavior: .developerErrors,
      logEntry: "\(scheme) is missing from your Info.plist under LSApplicationQueriesSchemes and is required."
    )

    internalUtility.checkRegisteredCanOpenURLScheme(scheme)
    XCTAssertEqual(logger.logEntryCallCount, 1, "Additional errors should not be logged for the same error")
  }

  // MARK: - Dictionary from FBURL

  func testWithAuthorizeHostNoParameters() throws {
    let url = try XCTUnwrap(URL(string: "foo://authorize"))
    let parameters = internalUtility.parameters(fromFBURL: url)

    XCTAssertTrue(
      parameters.isEmpty,
      "Should not extract parameters from a url if there are none"
    )
  }

  func testWithAuthorizeHostNoFragment() throws {
    let url = try XCTUnwrap(URL(string: "foo://authorize?foo=bar"))
    let parameters = internalUtility.parameters(fromFBURL: url) as? [String: String]

    XCTAssertEqual(
      parameters,
      ["foo": "bar"],
      "Should extract parameters from a url"
    )
  }

  func testWithAuthorizeHostAndFragment() throws {
    let url = try XCTUnwrap(URL(string: "foo://authorize?foo=bar#param1=value1&param2=value2"))
    let parameters = internalUtility.parameters(fromFBURL: url) as? [String: String]
    let expectedParameters = [
      "foo": "bar",
      "param1": "value1",
      "param2": "value2",
    ]

    XCTAssertEqual(
      parameters,
      expectedParameters,
      "Extracted parameters from an auth url should include fragment parameters"
    )
  }

  func testWithoutAuthorizeHostNoParameters() throws {
    let url = try XCTUnwrap(URL(string: "foo://example"))
    let parameters = internalUtility.parameters(fromFBURL: url)

    XCTAssertTrue(
      parameters.isEmpty,
      "Should not extract parameters from a url if there are none"
    )
  }

  func testWithoutAuthorizeHostNoFragment() throws {
    let url = try XCTUnwrap(URL(string: "foo://example?foo=bar"))
    let parameters = internalUtility.parameters(fromFBURL: url) as? [String: String]

    XCTAssertEqual(
      parameters,
      ["foo": "bar"],
      "Should extract parameters from a url"
    )
  }

  func testWithoutAuthorizeHostWithFragment() throws {
    let url = try XCTUnwrap(URL(string: "foo://example?foo=bar#param1=value1&param2=value2"))
    let parameters = internalUtility.parameters(fromFBURL: url) as? [String: String]

    XCTAssertEqual(
      parameters,
      ["foo": "bar"],
      "Extracted parameters from a non auth url should not include fragment parameters"
    )
  }

  // MARK: - Cookies

  func testDeletingDialogCookies() {
    let cookie1 = makeCookie(url: dialogURL)
    let cookie2 = makeCookie(url: dialogURL, name: "cookie 2")

    HTTPCookieStorage.shared.setCookie(cookie1)
    HTTPCookieStorage.shared.setCookie(cookie2)

    let cookies = HTTPCookieStorage.shared.cookies(for: dialogURL)
    let expectedCookies = [cookie1, cookie2]

    XCTAssertEqual(cookies, expectedCookies, "Sanity check that there are cookies to delete")

    internalUtility.deleteFacebookCookies()
    XCTAssertEqual(
      HTTPCookieStorage.shared.cookies(for: dialogURL),
      [],
      "All cookies for the facebook dialog url should be deleted"
    )
  }

  func testDeletingNonDialogCookies() {
    let cookie = makeCookie(url: nonDialogURL)
    HTTPCookieStorage.shared.setCookie(cookie)

    let cookies = HTTPCookieStorage.shared.cookies(for: nonDialogURL)
    XCTAssertEqual(cookies, [cookie], "Sanity check that there are cookies to delete")

    internalUtility.deleteFacebookCookies()
    XCTAssertEqual(
      HTTPCookieStorage.shared.cookies(for: nonDialogURL),
      [cookie],
      "Should only delete cookies for the dialog url"
    )
  }

  func testDeletingMixOfCookies() {
    let dialogCookie = makeCookie(url: dialogURL)
    let nonDialogCookie = makeCookie(url: nonDialogURL)

    HTTPCookieStorage.shared.setCookie(dialogCookie)
    HTTPCookieStorage.shared.setCookie(nonDialogCookie)

    internalUtility.deleteFacebookCookies()

    XCTAssertEqual(
      HTTPCookieStorage.shared.cookies(for: dialogURL),
      [],
      "Should delete cookies for the dialog url"
    )
    XCTAssertEqual(
      HTTPCookieStorage.shared.cookies(for: nonDialogURL),
      [nonDialogCookie],
      "Should only delete cookies for the dialog url"
    )
  }

  // MARK: - App Installation

  func testIsRegisteredCanOpenURLSchemeWithMissingScheme() {
    let querySchemes = [String]()
    bundle = TestBundle(infoDictionary: ["LSApplicationQueriesSchemes": querySchemes])
    configureInternalUtility()

    XCTAssertFalse(
      internalUtility.isRegisteredCanOpenURLScheme(name),
      "Should not be consider a scheme to be registered if it's missing from the application query schemes"
    )
  }

  func testIsRegisteredCanOpenURLSchemeWithScheme() {
    let querySchemes = [name]
    bundle = TestBundle(infoDictionary: ["LSApplicationQueriesSchemes": querySchemes])
    configureInternalUtility()

    XCTAssertTrue(
      internalUtility.isRegisteredCanOpenURLScheme(name),
      "Should consider a scheme to be registered if it exists in the application query schemes"
    )
  }

  func testIsRegisteredCanOpenURLSchemeCache() {
    let querySchemes = [name]
    bundle = TestBundle(infoDictionary: ["LSApplicationQueriesSchemes": querySchemes])
    configureInternalUtility()

    XCTAssertTrue(internalUtility.isRegisteredCanOpenURLScheme(name), "Sanity check")

    bundle.fb_infoDictionary = [:]

    XCTAssertTrue(
      internalUtility.isRegisteredCanOpenURLScheme(name),
      "Should return the cached value of the main bundle and not the updated values"
    )
  }

  func testFacebookAppInstalledMissingQuerySchemes() {
    bundle = TestBundle(infoDictionary: [:])
    configureInternalUtility()

    XCTAssertFalse(internalUtility.isFacebookAppInstalled)

    verifyTestLoggerInvoked(loggingBehavior: .developerErrors, logEntry: facebookUrlSchemeMissingMessage)
  }

  func testFacebookAppInstalledEmptyQuerySchemes() {
    let querySchemes = [String]()
    bundle = TestBundle(infoDictionary: ["LSApplicationQueriesSchemes": querySchemes])
    configureInternalUtility()

    XCTAssertFalse(internalUtility.isFacebookAppInstalled)

    verifyTestLoggerInvoked(loggingBehavior: .developerErrors, logEntry: facebookUrlSchemeMissingMessage)
  }

  func testFacebookAppInstalledMissingQueryScheme() {
    let querySchemes = ["Foo"]
    bundle = TestBundle(infoDictionary: ["LSApplicationQueriesSchemes": querySchemes])
    configureInternalUtility()

    XCTAssertFalse(internalUtility.isFacebookAppInstalled)

    verifyTestLoggerInvoked(loggingBehavior: .developerErrors, logEntry: facebookUrlSchemeMissingMessage)
  }

  func testFacebookAppInstalledValidQueryScheme() {
    let querySchemes = ["fbauth2"]
    bundle = TestBundle(infoDictionary: ["LSApplicationQueriesSchemes": querySchemes])
    configureInternalUtility()

    XCTAssertFalse(internalUtility.isFacebookAppInstalled)

    XCTAssertNil(TestLogger.capturedLoggingBehavior)
  }

  func testFacebookAppInstalledCache() {
    bundle = TestBundle(infoDictionary: [:])
    configureInternalUtility()

    XCTAssertEqual(logger.logEntryCallCount, 0, "There should not be developer errors logged initially")

    XCTAssertFalse(internalUtility.isFacebookAppInstalled)

    XCTAssertEqual(logger.logEntryCallCount, 1, "One developer error should be logged")
    verifyTestLoggerInvoked(loggingBehavior: .developerErrors, logEntry: facebookUrlSchemeMissingMessage)

    // Calling it again should not result in an additional call to the singleShotLogEntry method
    XCTAssertFalse(internalUtility.isFacebookAppInstalled)
    XCTAssertEqual(logger.logEntryCallCount, 1, "Additional errors should not be logged for the same error")
  }

  func testMessengerAppInstalledMissingQuerySchemes() {
    bundle = TestBundle(infoDictionary: [:])
    configureInternalUtility()

    XCTAssertFalse(internalUtility.isMessengerAppInstalled)

    verifyTestLoggerInvoked(
      loggingBehavior: .developerErrors,
      logEntry: messengerUrlSchemeMissingMessage
    )
  }

  func testMessengerAppInstalledEmptyQuerySchemes() {
    let querySchemes = [String]()
    bundle = TestBundle(infoDictionary: ["LSApplicationQueriesSchemes": querySchemes])
    configureInternalUtility()

    XCTAssertFalse(internalUtility.isMessengerAppInstalled)

    verifyTestLoggerInvoked(
      loggingBehavior: .developerErrors,
      logEntry: messengerUrlSchemeMissingMessage
    )
  }

  func testMessengerAppInstalledMissingQueryScheme() {
    let querySchemes = ["Foo"]
    bundle = TestBundle(infoDictionary: ["LSApplicationQueriesSchemes": querySchemes])
    configureInternalUtility()

    XCTAssertFalse(internalUtility.isMessengerAppInstalled)

    verifyTestLoggerInvoked(
      loggingBehavior: .developerErrors,
      logEntry: messengerUrlSchemeMissingMessage
    )
  }

  func testMessengerAppInstalledValidQueryScheme() {
    let querySchemes = ["fb-messenger-share-api"]
    bundle = TestBundle(infoDictionary: ["LSApplicationQueriesSchemes": querySchemes])
    configureInternalUtility()

    XCTAssertFalse(internalUtility.isMessengerAppInstalled)

    XCTAssertNil(TestLogger.capturedLoggingBehavior)
  }

  func testMessengerAppInstalledCache() {
    bundle = TestBundle(infoDictionary: [:])
    configureInternalUtility()

    XCTAssertTrue(
      TestLogger.capturedLogEntries.isEmpty, "There should not be developer errors logged initially"
    )

    XCTAssertFalse(internalUtility.isMessengerAppInstalled)

    XCTAssertEqual(logger.logEntryCallCount, 1, "One developer error should be logged")
    verifyTestLoggerInvoked(
      loggingBehavior: .developerErrors,
      logEntry: messengerUrlSchemeMissingMessage
    )

    // Calling it again should not result in an additional call to the singleShotLogEntry method
    XCTAssertFalse(internalUtility.isMessengerAppInstalled)
    XCTAssertEqual(logger.logEntryCallCount, 1, "Additional errors should not be logged for the same error")
  }

  // MARK: - Random Utility Methods

  func testIsBrowserURLWithNonBrowserURL() {
    [
      URL(string: "file://foo")!, // swiftlint:disable:this force_unwrapping
      URL(string: "example://bar")!, // swiftlint:disable:this force_unwrapping
    ]
      .forEach { url in
        XCTAssertFalse(
          internalUtility.isBrowserURL(url),
          "\(url.absoluteString) should not be considered a browser url"
        )
      }
  }

  func testIsBrowserURLWithBrowserURL() {
    [
      URL(string: "HTTPS://example.com"),
      URL(string: "HTTP://example.com"),
      URL(string: "https://example.com"),
      URL(string: "http://example.com"),
    ]
      .compactMap { $0 }
      .forEach { url in

        XCTAssertTrue(
          internalUtility.isBrowserURL(url),
          "\(url.absoluteString) should be considered a browser url"
        )
      }
  }

  func testIsFacebookBundleIdentifierWithInvalidIdentifiers() {
    [
      "",
      "foo",
      "com.foo.bar",
      "com.facebook",
    ]
      .forEach { identifier in
        XCTAssertFalse(
          internalUtility.isFacebookBundleIdentifier(identifier),
          "\(identifier) should not be considered a facebook bundle indentifier"
        )
      }
  }

  func testIsFacebookBundleIdentifierWithValidIdentifiers() {
    [
      "com.facebook.",
      "com.facebook.foo",
      ".com.facebook.",
      ".com.facebook.foo",
    ]
      .forEach { identifier in
        XCTAssertTrue(
          internalUtility.isFacebookBundleIdentifier(identifier),
          "\(identifier) should be considered a facebook bundle indentifier"
        )
      }
  }

  func testNonSafariBundleIdentifiers() {
    [
      "",
      " ",
      "com.foo",
    ]
      .forEach { identifier in
        XCTAssertFalse(
          internalUtility.isSafariBundleIdentifier(identifier),
          "\(identifier) should not be considered a safari bundle identifier"
        )
      }
  }

  func testSafariBundleIdentifiers() {
    [
      "com.apple.mobilesafari",
      "com.apple.SafariViewService",
    ]
      .forEach { identifier in
        XCTAssertTrue(
          internalUtility.isSafariBundleIdentifier(identifier),
          "\(identifier) should be considered a safari bundle identifier"
        )
      }
  }

  func testValidatingAppIDWhenUninitialized() {
    internalUtility = InternalUtility()
    settings.appID = "abc" // not actually used

    assertRaisesException(message: "Should raise an exception") {
      self.internalUtility.validateAppID()
    }
  }

  func testValidatingAppID() {
    settings.appID = nil

    assertRaisesException(message: "Should raise an exception") {
      self.internalUtility.validateAppID()
    }
  }

  func testValidateClientAccessTokenWhenUninitialized() {
    internalUtility = InternalUtility()
    settings.appID = "abc" // not actually used
    settings.clientToken = "123" // not actually used

    assertRaisesException(message: "Should raise an exception") {
      self.internalUtility.validateRequiredClientAccessToken()
    }
  }

  func testValidateClientAccessTokenWithoutClientTokenWithoutAppID() {
    settings.appID = nil
    settings.clientToken = nil

    assertRaisesException(message: "Should raise an exception") {
      self.internalUtility.validateRequiredClientAccessToken()
    }
  }

  func testValidateClientAccessTokenWithClientTokenWithoutAppID() {
    settings.appID = nil
    settings.clientToken = "client123"

    XCTAssertEqual(
      internalUtility.validateRequiredClientAccessToken(),
      "(null)|client123",
      "A valid client-access token should include the app identifier and the client token"
    )
  }

  func testValidateClientAccessTokenWithClientTokenWithAppID() {
    settings.appID = "appid"
    settings.clientToken = "client123"

    XCTAssertEqual(
      internalUtility.validateRequiredClientAccessToken(),
      "appid|client123",
      "A valid client-access token should include the app identifier and the client token"
    )
  }

  func testValidateClientAccessTokenWithoutClientTokenWithAppID() {
    settings.appID = "appid"
    settings.clientToken = nil

    assertRaisesException(message: "Should raise an exception") {
      self.internalUtility.validateRequiredClientAccessToken()
    }
  }

  func testIsRegisteredUrlSchemeWithRegisteredScheme() {
    bundle = makeBundle(registeredUrlSchemes: ["com.foo.bar"])
    configureInternalUtility()

    XCTAssertTrue(
      internalUtility.isRegisteredURLScheme("com.foo.bar"),
      "Schemes in the bundle should be considered registered"
    )
  }

  func testIsRegisteredUrlSchemeWithoutRegisteredScheme() {
    bundle = makeBundle(registeredUrlSchemes: ["com.foo.bar"])
    configureInternalUtility()

    XCTAssertFalse(
      internalUtility.isRegisteredURLScheme("com.facebook"),
      "Schemes absent from the bundle should not be considered registered"
    )
  }

  func testIsRegisteredUrlSchemeCaching() {
    bundle = TestBundle()
    configureInternalUtility()

    internalUtility.isRegisteredURLScheme("com.facebook")

    XCTAssertTrue(
      bundle.didAccessInfoDictionary,
      "Should query the bundle for URL types"
    )
    bundle.reset()

    internalUtility.isRegisteredURLScheme("com.facebook")

    XCTAssertFalse(
      bundle.didAccessInfoDictionary,
      "Should not query the bundle more than once"
    )
  }

  func testValidatingUrlSchemesWithoutAppID() {
    settings.appID = nil

    assertRaisesException(
      message: "Cannot validate url schemes without an app identifier"
    ) {
      self.internalUtility.validateURLSchemes()
    }
  }

  func testValidatingUrlSchemesWhenNotConfigured() {
    assertRaisesException(
      message: "Cannot validate url schemes before configuring"
    ) {
      self.internalUtility.validateURLSchemes()
    }
  }

  func testValidatingUrlSchemesWithAppIdMatchingBundleEntry() {
    settings.appID = "appid"
    settings.appURLSchemeSuffix = nil
    bundle = makeBundle(registeredUrlSchemes: ["fbappid"])
    configureInternalUtility()

    assertDoesNotRaiseException(
      message: "The registered app url scheme must match the app id and url scheme suffix prepended with 'fb'"
    ) {
      self.internalUtility.validateURLSchemes()
    }
  }

  func testValidatingUrlSchemesWithNonAppIdMatchingBundleEntry() {
    settings.appID = "appid"
    settings.appURLSchemeSuffix = nil
    bundle = makeBundle(registeredUrlSchemes: ["fb123"])
    configureInternalUtility()

    assertRaisesException(
      message: "The registered app url scheme must match the app id and url scheme suffix prepended with 'fb'"
    ) {
      self.internalUtility.validateURLSchemes()
    }
  }

  // We can't loop through these because of how stubbing works.
  func testValidatingFacebookUrlSchemes_api() {
    bundle = makeBundle(registeredUrlSchemes: ["fbapi"])
    configureInternalUtility()

    assertRaisesException(
      message: "Should throw an error if fbapi is present in the bundle url schemes"
    ) {
      self.internalUtility.validateFacebookReservedURLSchemes()
    }
  }

  // We can't loop through these because of how stubbing works.
  func testValidatingFacebookUrlSchemes_messenger() {
    bundle = makeBundle(registeredUrlSchemes: ["fb-messenger-share-api"])
    configureInternalUtility()

    assertRaisesException(
      message: "Should throw an error if fb-messenger-share-api is present in the bundle url schemes"
    ) {
      self.internalUtility.validateFacebookReservedURLSchemes()
    }
  }

  func testExtendDictionaryWithDefaultDataProcessingOptions() {
    let parameters = NSMutableDictionary()
    internalUtility.extendDictionary(withDataProcessingOptions: parameters)

    XCTAssertEqual(
      parameters,
      [:],
      "Parameters should not change with default data processing options"
    )
  }

  func testExtendDictionaryWithDataProcessingOptions() {
    settings.persistableDataProcessingOptions = [
      "data_processing_options": ["LDU"],
      "data_processing_options_country": 10,
      "data_processing_options_state": 100,
    ]

    let parameters = NSMutableDictionary()
    internalUtility.extendDictionary(withDataProcessingOptions: parameters)

    XCTAssertEqual(
      parameters["data_processing_options"] as? String,
      "[\"LDU\"]",
      "Parameters should be extended with expected data processing options"
    )
    XCTAssertEqual(
      parameters["data_processing_options_country"] as? Int,
      10,
      "Parameters should be extended with expected data processing options"
    )
    XCTAssertEqual(
      parameters["data_processing_options_state"] as? Int,
      100,
      "Parameters should be extended with expected data processing options"
    )
  }

  func testIsPublishPermission() {
    [
      "publish",
      "publishSomething",
      "manage",
      "manageSomething",
      "ads_management",
      "create_event",
      "rsvp_event",
    ]
      .forEach { permission in
        XCTAssertTrue(internalUtility.isPublishPermission(permission))
      }

    [
      "",
      "email",
      "_publish",
    ]
      .forEach { permission in
        XCTAssertFalse(internalUtility.isPublishPermission(permission))
      }
  }

  func testIsUnityWithMissingSuffix() {
    settings.userAgentSuffix = nil
    XCTAssertFalse(
      internalUtility.isUnity,
      "User agent should determine whether an app is Unity"
    )
  }

  func testIsUnityWithNonUnitySuffix() {
    settings.userAgentSuffix = "Foo"
    XCTAssertFalse(
      internalUtility.isUnity,
      "User agent should determine whether an app is Unity"
    )
  }

  func testIsUnityWithUnitySuffix() {
    settings.userAgentSuffix = "__Unity__"
    XCTAssertTrue(
      internalUtility.isUnity,
      "User agent should determine whether an app is Unity"
    )
  }

  func testHexadecimalStringFromData() throws {
    XCTAssertNil(
      internalUtility.hexadecimalString(from: Data())
    )

    let data = try XCTUnwrap("foo".data(using: .utf8))
    let expected = "666f6f"

    XCTAssertEqual(
      internalUtility.hexadecimalString(from: data),
      expected
    )
  }

  func testObjectIsEqualToObject() {
    var obj1: Any? = "foo"
    let obj2: Any = "foo"
    let obj3: Any = "bar"

    XCTAssertTrue(internalUtility.object(obj1 as Any, isEqualTo: obj1 as Any))
    XCTAssertTrue(internalUtility.object(obj1 as Any, isEqualTo: obj2))
    XCTAssertFalse(internalUtility.object(obj1 as Any, isEqualTo: obj3))

    obj1 = nil
    XCTAssertFalse(internalUtility.object(obj1 as Any, isEqualTo: obj2))
    XCTAssertFalse(internalUtility.object(obj2, isEqualTo: obj1 as Any))
  }

  // MARK: - Helpers

  func verifyTestLoggerInvoked(
    loggingBehavior: LoggingBehavior,
    logEntry: String,
    file: StaticString = #file,
    line: UInt = #line
  ) {
    XCTAssertEqual(
      loggerFactory.capturedLoggingBehavior,
      loggingBehavior,
      file: file,
      line: line
    )
    XCTAssertEqual(
      logger.capturedContents,
      logEntry,
      file: file,
      line: line
    )
  }

  func makeCookie(url: URL, name: String = "MyCookie") -> HTTPCookie {
    HTTPCookie(
      properties: [
        .originURL: url,
        .path: url.path,
        .name: name,
        .value: "Is good",
      ]
    )! // swiftlint:disable:this force_unwrapping
  }

  func makeBundle(registeredUrlSchemes: [String]) -> TestBundle {
    TestBundle(
      infoDictionary: [
        "CFBundleURLTypes": [
          ["CFBundleURLSchemes": registeredUrlSchemes],
        ],
      ]
    )
  }
}


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

final class NotificationCenterTests: XCTestCase {
  // swiftlint:disable implicitly_unwrapped_optional
  var notificationCenter: NotificationCenter!
  var notificationObject: AnyObject!
  var observer: Any!
  // swiftlint:enable implicitly_unwrapped_optional

  let userInfo = ["key": "value"]
  var observedNotification: Notification?

  override func setUp() {
    super.setUp()

    notificationCenter = NotificationCenter.default
    notificationObject = NSObject()
  }

  override func tearDown() {
    removeObserver()

    notificationCenter = nil
    notificationObject = nil
    observer = nil

    super.tearDown()
  }

  func testPostingNotification() throws {
    addObserver()

    notificationCenter.fb_post(
      name: .sample,
      object: notificationObject,
      userInfo: userInfo
    )

    try validateNotification(message: .postNotification)
  }

  // MARK: - Helpers

  @objc
  private func handleNotification(_ notification: Notification) {
    observedNotification = notification
  }

  private func addObserver() {
    observer = notificationCenter.addObserver(
      forName: .sample,
      object: notificationObject,
      queue: nil
    ) { [self] notification in
      observedNotification = notification
    }
  }

  private func removeObserver() {
    guard let observer = observer else { return }

    notificationCenter.removeObserver(observer)
  }

  private func postNotification() {
    notificationCenter.post(
      name: .sample,
      object: notificationObject,
      userInfo: userInfo
    )
  }

  private func validateNotification(message: String, file: StaticString = #file, line: UInt = #line) throws {
    let notification = try XCTUnwrap(observedNotification, message, file: file, line: line)
    XCTAssertEqual(notification.name, .sample, message, file: file, line: line)
    XCTAssertIdentical(notification.object as AnyObject, notificationObject, message, file: file, line: line)
    XCTAssertEqual(notification.userInfo as? [String: String], userInfo, message, file: file, line: line)
  }
}

// swiftformat:disable extensionaccesscontrol

// MARK: - Assumptions

fileprivate extension String {
  static let postNotification = "A notification center can publish a notification through an internal abstraction"
}

// MARK: - Test Values

fileprivate extension Notification.Name {
  static let sample = Self("sample-notification")
}


/*
 * 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 ProcessInfoTests: XCTestCase {
  // swiftlint:disable:next implicitly_unwrapped_optional
  var processInfo: ProcessInfo!
  lazy var currentOperatingSystemVersion = processInfo.operatingSystemVersion
  lazy var laterOperatingSystemVersion = OperatingSystemVersion(
    majorVersion: currentOperatingSystemVersion.majorVersion,
    minorVersion: currentOperatingSystemVersion.minorVersion,
    patchVersion: currentOperatingSystemVersion.patchVersion + 1
  )

  override func setUp() {
    super.setUp()
    processInfo = ProcessInfo()
  }

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

  func testOperatingSystemAtLeastVersion() {
    XCTAssertEqual(
      processInfo.isOperatingSystemAtLeast(currentOperatingSystemVersion),
      processInfo.fb_isOperatingSystemAtLeast(currentOperatingSystemVersion),
      .isOperatingSystemAtLeastVersion
    )
    XCTAssertEqual(
      processInfo.isOperatingSystemAtLeast(laterOperatingSystemVersion),
      processInfo.fb_isOperatingSystemAtLeast(laterOperatingSystemVersion),
      .isOperatingSystemAtLeastVersion
    )
  }

  func testIsMacCatalystApp() {
    if #available(iOS 13, *) {
      XCTAssertEqual(
        processInfo.fb_isMacCatalystApp,
        processInfo.isMacCatalystApp,
        .isMacCatalystApp
      )
    } else {
      XCTAssertFalse(processInfo.fb_isMacCatalystApp, .isMacCatalystApp)
    }
  }
}

// swiftformat:disable extensionaccesscontrol

// MARK: - Assumptions

fileprivate extension String {
  static let isOperatingSystemAtLeastVersion = """
    A process info instance can compare operating system versions through an internal abstraction
    """
  static let isMacCatalystApp = """
    A process info instance indicates whether an app is a Mac Catalyst app through an internal abstraction
    """
}


/*
 * 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 ProfileCodingKeyTests: XCTestCase {
  func testRawValues() {
    XCTAssertEqual(Profile.CodingKeys.userID.rawValue, "userID")
    XCTAssertEqual(Profile.CodingKeys.firstName.rawValue, "firstName")
    XCTAssertEqual(Profile.CodingKeys.middleName.rawValue, "middleName")
    XCTAssertEqual(Profile.CodingKeys.lastName.rawValue, "lastName")
    XCTAssertEqual(Profile.CodingKeys.name.rawValue, "name")
    XCTAssertEqual(Profile.CodingKeys.linkURL.rawValue, "linkURL")
    XCTAssertEqual(Profile.CodingKeys.refreshDate.rawValue, "refreshDate")
    XCTAssertEqual(Profile.CodingKeys.imageURL.rawValue, "imageURL")
    XCTAssertEqual(Profile.CodingKeys.email.rawValue, "email")
    XCTAssertEqual(Profile.CodingKeys.friendIDs.rawValue, "friendIDs")
    XCTAssertEqual(Profile.CodingKeys.isLimited.rawValue, "isLimited")
    XCTAssertEqual(Profile.CodingKeys.birthday.rawValue, "birthday")
    XCTAssertEqual(Profile.CodingKeys.ageRange.rawValue, "ageRange")
    XCTAssertEqual(Profile.CodingKeys.hometown.rawValue, "hometown")
    XCTAssertEqual(Profile.CodingKeys.location.rawValue, "location")
    XCTAssertEqual(Profile.CodingKeys.gender.rawValue, "gender")
  }
}


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

@testable import FBSDKCoreKit

import XCTest

final class TokenCacheTests: XCTestCase {

  // swiftlint:disable implicitly_unwrapped_optional
  var cache: TokenCache!
  var settings: TestSettings!
  var keychainStore: TestKeychainStore!
  var store: TestDataStore!
  // swiftlint:enable implicitly_unwrapped_optional

  override func setUp() {
    super.setUp()

    settings = TestSettings()
    keychainStore = TestKeychainStore()
    store = TestDataStore()
    cache = TokenCache()
    cache.setDependencies(
      .init(
        settings: settings,
        keychainStore: keychainStore,
        dataStore: store
      )
    )
  }

  override func tearDown() {
    settings = nil
    keychainStore = nil
    store = nil
    cache = nil

    super.tearDown()
  }

  func testDefaultTypeDependencies() {
    XCTAssertNil(
      try? TokenCache().getDependencies(),
      "TokenCache does not have dependencies by default"
    )
  }

  func testSettingsDependencies() {
    XCTAssertTrue(
      cache.settings === settings,
      "Can set a settings for the cache"
    )
    XCTAssertTrue(
      cache.keychainStore === keychainStore,
      "Can set a keychain store for the cache"
    )
    XCTAssertTrue(
      cache.dataStore === store,
      "Can set a store for the cache"
    )
  }
}


/*
 * 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 AppEventParameterNameTests: XCTestCase {
  func testGeneralRawValues() {
    // Public
    XCTAssertEqual(AppEvents.ParameterName.currency.rawValue, "fb_currency")
    XCTAssertEqual(AppEvents.ParameterName.registrationMethod.rawValue, "fb_registration_method")
    XCTAssertEqual(AppEvents.ParameterName.contentType.rawValue, "fb_content_type")
    XCTAssertEqual(AppEvents.ParameterName.content.rawValue, "fb_content")
    XCTAssertEqual(AppEvents.ParameterName.contentID.rawValue, "fb_content_id")
    XCTAssertEqual(AppEvents.ParameterName.searchString.rawValue, "fb_search_string")
    XCTAssertEqual(AppEvents.ParameterName.success.rawValue, "fb_success")
    XCTAssertEqual(AppEvents.ParameterName.maxRatingValue.rawValue, "fb_max_rating_value")
    XCTAssertEqual(AppEvents.ParameterName.paymentInfoAvailable.rawValue, "fb_payment_info_available")
    XCTAssertEqual(AppEvents.ParameterName.numItems.rawValue, "fb_num_items")
    XCTAssertEqual(AppEvents.ParameterName.level.rawValue, "fb_level")
    XCTAssertEqual(AppEvents.ParameterName.description.rawValue, "fb_description")
    XCTAssertEqual(AppEvents.ParameterName.adType.rawValue, "ad_type")
    XCTAssertEqual(AppEvents.ParameterName.orderID.rawValue, "fb_order_id")
    XCTAssertEqual(AppEvents.ParameterName.eventName.rawValue, "_eventName")
    XCTAssertEqual(AppEvents.ParameterName.logTime.rawValue, "_logTime")

    // Internal
    XCTAssertEqual(AppEvents.ParameterName.implicitlyLogged.rawValue, "_implicitlyLogged")
    XCTAssertEqual(AppEvents.ParameterName.inBackground.rawValue, "_inBackground")
  }

  func testPushNotificationRawValues() {
    // Internal
    XCTAssertEqual(AppEvents.ParameterName.pushCampaign.rawValue, "fb_push_campaign")
    XCTAssertEqual(AppEvents.ParameterName.pushAction.rawValue, "fb_push_action")
  }

  func testECommerceRawValues() {
    // Internal
    XCTAssertEqual(AppEvents.ParameterName.implicitlyLoggedPurchase.rawValue, "_implicitlyLogged")
    XCTAssertEqual(AppEvents.ParameterName.inAppPurchaseType.rawValue, "fb_iap_product_type")
    XCTAssertEqual(AppEvents.ParameterName.productTitle.rawValue, "fb_content_title")
    XCTAssertEqual(AppEvents.ParameterName.transactionID.rawValue, "fb_transaction_id")
    XCTAssertEqual(AppEvents.ParameterName.transactionDate.rawValue, "fb_transaction_date")
    XCTAssertEqual(AppEvents.ParameterName.subscriptionPeriod.rawValue, "fb_iap_subs_period")
    XCTAssertEqual(AppEvents.ParameterName.isStartTrial.rawValue, "fb_iap_is_start_trial")
    XCTAssertEqual(AppEvents.ParameterName.hasFreeTrial.rawValue, "fb_iap_has_free_trial")
    XCTAssertEqual(AppEvents.ParameterName.trialPeriod.rawValue, "fb_iap_trial_period")
    XCTAssertEqual(AppEvents.ParameterName.trialPrice.rawValue, "fb_iap_trial_price")
  }

  func testTimeSpentRawValues() {
    // Internal
    XCTAssertEqual(AppEvents.ParameterName.sessionInterruptions.rawValue, "fb_mobile_app_interruptions")
    XCTAssertEqual(AppEvents.ParameterName.timeBetweenSessions.rawValue, "fb_mobile_time_between_sessions")
    XCTAssertEqual(AppEvents.ParameterName.sessionID.rawValue, "_session_id")
    XCTAssertEqual(AppEvents.ParameterName.launchSource.rawValue, "fb_mobile_launch_source")
  }
}


/*
 * 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 AppEventsATEPublisherTests: XCTestCase {

  // swiftlint:disable implicitly_unwrapped_optional
  var factory: TestGraphRequestFactory!
  var settings: TestSettings!
  var store: UserDefaultsSpy!
  var deviceInformationProvider: TestDeviceInformationProvider!
  var publisher: AppEventsATEPublisher!
  // swiftlint:enable implicitly_unwrapped_optional

  let twelveHoursAgoInSeconds: TimeInterval = -12 * 60 * 60
  let fortyEightHoursAgoInSeconds: TimeInterval = -48 * 60 * 60
  lazy var key = "com.facebook.sdk:lastATEPing\(name)"

  override func setUp() {
    super.setUp()

    factory = TestGraphRequestFactory()
    settings = TestSettings()
    store = UserDefaultsSpy()
    deviceInformationProvider = TestDeviceInformationProvider()
    publisher = AppEventsATEPublisher(
      appIdentifier: name,
      graphRequestFactory: factory,
      settings: settings,
      store: store,
      deviceInformationProvider: deviceInformationProvider
    )! // swiftlint:disable:this force_unwrapping
  }

  override func tearDown() {
    factory = nil
    settings = nil
    store = nil
    deviceInformationProvider = nil
    publisher = nil

    super.tearDown()
  }

  func testCreatingWithEmptyAppIdentifier() {
    XCTAssertNil(
      AppEventsATEPublisher(
        appIdentifier: "",
        graphRequestFactory: factory,
        settings: settings,
        store: store,
        deviceInformationProvider: deviceInformationProvider
      ),
      "Should not create an ATE publisher with an empty app identifier"
    )
  }

  func testCreatingWithValidAppIdentifier() throws {
    XCTAssertEqual(
      publisher.appIdentifier,
      name,
      "Should be able to create a publisher with a non-empty string for the app identifier"
    )
  }

  func testPublishingATEUsesDeviceInformation() throws {
    settings.advertisingTrackingStatus = .allowed

    publisher.publishATE()

    XCTAssertTrue(
      deviceInformationProvider.encodedDeviceInfoWasCalled,
      "Should use device information when publishing ATE"
    )
  }

  func testPublishingATEWithoutLastPublishDate() throws {
    settings.advertisingTrackingStatus = .allowed

    publisher.publishATE()
    let request = try XCTUnwrap(factory.capturedRequests.first)
    XCTAssertEqual(
      request.startCallCount,
      1,
      "Should start the request to publish the ATE"
    )
    XCTAssertEqual(
      store.capturedObjectRetrievalKey,
      "com.facebook.sdk:lastATEPing\(publisher.appIdentifier)",
      "Should use the store to access the last published date"
    )
    XCTAssertFalse(
      publisher.isProcessing,
      "After processing, isProcessing should equal to NO"
    )
  }

  func testPublishingWithNonExpiredLastPublishDate() throws {
    store.set(
      Date(timeIntervalSinceNow: twelveHoursAgoInSeconds),
      forKey: key
    )
    settings.advertisingTrackingStatus = .allowed

    publisher.publishATE()

    XCTAssertEqual(factory.capturedRequests.count, 0)
    XCTAssertEqual(
      store.capturedObjectRetrievalKey,
      key,
      "Should use the store to access the last published date"
    )
    XCTAssertFalse(
      publisher.isProcessing,
      "After processing, isProcessing should equal to NO"
    )
  }

  func testPublishingWithExpiredLastPublishDate() throws {
    store.set(
      Date(timeIntervalSinceNow: fortyEightHoursAgoInSeconds),
      forKey: key
    )
    settings.advertisingTrackingStatus = .allowed

    publisher.publishATE()

    let request = try XCTUnwrap(factory.capturedRequests.first)

    XCTAssertEqual(request.startCallCount, 1)
    XCTAssertEqual(
      store.capturedObjectRetrievalKey,
      key,
      "Should use the store to access the last published date"
    )

    XCTAssertFalse(
      publisher.isProcessing,
      "After processing, isProcessing should equal to NO"
    )
  }
}