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

final class GraphRequestConnectionTests: XCTestCase, GraphRequestConnectionDelegate {

  let appID = "appid"
  var didInvokeDelegateRequestConnectionDidSendBodyData = false

  // swiftlint:disable implicitly_unwrapped_optional force_unwrapping line_length
  let sampleUrl = URL(string: "https://example.com")!
  let missingTokenData = "{\"error\": {\"message\": \"Token is broken\",\"code\": 190,\"error_subcode\": 463}}".data(using: .utf8)!
  let clientToken = "client_token"
  var session: TestURLSessionProxy!
  var secondSession: TestURLSessionProxy!
  var sessionFactory: TestURLSessionProxyFactory!
  var errorConfiguration: TestErrorConfiguration!
  var errorConfigurationProvider: TestErrorConfigurationProvider!
  var errorRecoveryConfiguration: _ErrorRecoveryConfiguration!
  var settings: TestSettings!
  var graphRequestConnectionFactory: TestGraphRequestConnectionFactory!
  var eventLogger: TestEventLogger!
  var processInfo: TestProcessInfo!
  var macCatalystDeterminator: TestMacCatalystDeterminator!
  var logger: TestLogger!
  var connection: GraphRequestConnection!
  var errorFactory: TestErrorFactory!
  var metadata: GraphRequestMetadata!
  var piggybackManager: TestGraphRequestPiggybackManager!
  // swiftlint:enable implicitly_unwrapped_optional force_unwrapping line_length

  func createSampleMetadata() -> GraphRequestMetadata {
    GraphRequestMetadata(
      request: makeSampleRequest(),
      completionHandler: nil,
      batchParameters: nil
    )
  }

  var requestConnectionStartingCallback: ((GraphRequestConnecting) -> Void)?
  var requestConnectionCallback: ((GraphRequestConnecting, Error?) -> Void)?

  override func setUp() {
    super.setUp()

    metadata = createSampleMetadata()
    TestAccessTokenWallet.reset()
    TestAuthenticationTokenWallet.reset()
    GraphRequestConnection.setCanMakeRequests()

    session = TestURLSessionProxy()
    secondSession = TestURLSessionProxy()
    sessionFactory = TestURLSessionProxyFactory.create(withSessions: [session, secondSession])
    errorRecoveryConfiguration = makeNonTransientErrorRecoveryConfiguration()
    errorConfiguration = TestErrorConfiguration()
    errorConfiguration.stubbedRecoveryConfiguration = errorRecoveryConfiguration
    errorConfigurationProvider = TestErrorConfigurationProvider(configuration: errorConfiguration)
    settings = TestSettings()
    settings.appID = appID
    settings.clientToken = clientToken
    graphRequestConnectionFactory = TestGraphRequestConnectionFactory()
    eventLogger = TestEventLogger()
    processInfo = TestProcessInfo()
    macCatalystDeterminator = TestMacCatalystDeterminator()
    logger = TestLogger(loggingBehavior: .developerErrors)
    errorFactory = TestErrorFactory()
    piggybackManager = TestGraphRequestPiggybackManager()
    GraphRequestConnection.configure(
      urlSessionProxyFactory: sessionFactory,
      errorConfigurationProvider: errorConfigurationProvider,
      piggybackManager: piggybackManager,
      settings: settings,
      graphRequestConnectionFactory: graphRequestConnectionFactory,
      eventLogger: eventLogger,
      operatingSystemVersionComparer: processInfo,
      macCatalystDeterminator: macCatalystDeterminator,
      accessTokenProvider: TestAccessTokenWallet.self,
      errorFactory: errorFactory,
      authenticationTokenProvider: TestAuthenticationTokenWallet.self
    )
    connection = GraphRequestConnection()
    graphRequestConnectionFactory.stubbedConnection = connection
  }

  override func tearDown() {
    session = nil
    secondSession = nil
    sessionFactory = nil
    errorConfiguration = nil
    errorConfigurationProvider = nil
    errorRecoveryConfiguration = nil
    settings = nil
    graphRequestConnectionFactory = nil
    eventLogger = nil
    processInfo = nil
    macCatalystDeterminator = nil
    logger = nil
    connection = nil
    errorFactory = nil
    metadata = nil
    piggybackManager = nil

    GraphRequestConnection.resetClassDependencies()
    GraphRequestConnection.resetDefaultConnectionTimeout()
    GraphRequestConnection.resetCanMakeRequests()
    TestLogger.reset()
    TestAccessTokenWallet.reset()
    TestAuthenticationTokenWallet.reset()

    super.tearDown()
  }

  // MARK: - GraphRequestConnectionDelegate

  func requestConnection(_ connection: GraphRequestConnecting, didFailWithError error: Error) {
    if let completion = requestConnectionCallback {
      completion(connection, error)
      requestConnectionCallback = nil
    }
  }

  func requestConnectionDidFinishLoading(_ connection: GraphRequestConnecting) {
    if let completion = requestConnectionCallback {
      completion(connection, nil)
      requestConnectionCallback = nil
    }
  }

  func requestConnectionWillBeginLoading(_ connection: GraphRequestConnecting) {
    if let completion = requestConnectionStartingCallback {
      completion(connection)
      requestConnectionStartingCallback = nil
    }
  }

  func requestConnection(
    _ connection: GraphRequestConnecting,
    didSendBodyData bytesWritten: Int,
    totalBytesWritten: Int,
    totalBytesExpectedToWrite: Int
  ) {
    didInvokeDelegateRequestConnectionDidSendBodyData = true
  }

  // MARK: - Dependencies

  func testDefaultDependencies() {
    GraphRequestConnection.resetClassDependencies()

    XCTAssertNil(
      GraphRequestConnection.sessionProxyFactory,
      "A graph request connection should not have a session provider by default"
    )
    XCTAssertNil(
      GraphRequestConnection.errorConfigurationProvider,
      "A graph request connection should not have a error configuration provider by default"
    )
    XCTAssertNil(
      GraphRequestConnection.piggybackManager,
      "A graph request connection should not have a piggyback manager by default"
    )
    XCTAssertNil(
      GraphRequestConnection.settings,
      "A graph request connection should not have a settings type by default"
    )
    XCTAssertNil(
      GraphRequestConnection.graphRequestConnectionFactory,
      "A graph request connection should not have a connection factory by default"
    )
    XCTAssertNil(
      GraphRequestConnection.eventLogger,
      "A graph request connection should not have an events logger by default"
    )
    XCTAssertNil(
      GraphRequestConnection.operatingSystemVersionComparer,
      "A graph request connection should not have an operating system version comparer by default"
    )
    XCTAssertNil(
      GraphRequestConnection.macCatalystDeterminator,
      "A graph request connection should not have a Mac Catalyst determinator by default"
    )
    XCTAssertNil(
      GraphRequestConnection.accessTokenProvider,
      "A graph request connection should not an access token provider by default"
    )
    XCTAssertNil(
      GraphRequestConnection.errorFactory,
      "A graph request connection should not have an error factory by default"
    )
    XCTAssertNil(
      GraphRequestConnection.authenticationTokenProvider,
      "A graph request connection should not have an authentication token provider by default"
    )
  }

  func testCreatingWithCustomDependencies() {
    XCTAssertTrue(
      GraphRequestConnection.sessionProxyFactory === sessionFactory,
      "A graph request connection should persist the session provider it was created with"
    )
    XCTAssertTrue(
      connection.session === session,
      "A graph request connection should derive sessions from the session provider"
    )
    XCTAssertTrue(
      GraphRequestConnection.errorConfigurationProvider === errorConfigurationProvider,
      "A graph request connection should persist the error configuration provider it was created with"
    )
    XCTAssertTrue(
      GraphRequestConnection.piggybackManager === piggybackManager,
      "A graph request connection should persist the piggyback manager it was created with"
    )
    XCTAssertTrue(
      GraphRequestConnection.settings === settings,
      "A graph request connection should persist the settings it was created with"
    )
    XCTAssertTrue(
      GraphRequestConnection.graphRequestConnectionFactory === graphRequestConnectionFactory,
      "A graph request connection should persist the connection factory it was created with"
    )
    XCTAssertTrue(
      GraphRequestConnection.eventLogger === eventLogger,
      "A graph request connection should persist the events logger it was created with"
    )
    XCTAssertTrue(
      GraphRequestConnection.operatingSystemVersionComparer === processInfo,
      "A graph request connection should persist the operating system comparer it was created with"
    )
    XCTAssertTrue(
      GraphRequestConnection.macCatalystDeterminator === macCatalystDeterminator,
      "A graph request connection should persist the Mac Catalyst determinator it was created with"
    )
    XCTAssertTrue(
      GraphRequestConnection.accessTokenProvider === TestAccessTokenWallet.self,
      "A graph request connection should persist the access token provider it was created with"
    )
    XCTAssertTrue(
      GraphRequestConnection.errorFactory === errorFactory,
      "A graph request connection should persist the error factory it was created with"
    )
    XCTAssertTrue(
      GraphRequestConnection.authenticationTokenProvider === TestAuthenticationTokenWallet.self,
      "A graph request connection should persist the authentication token provider it was created with"
    )
  }

  // MARK: - Properties

  func testDefaultConnectionTimeout() {
    XCTAssertEqual(
      GraphRequestConnection.defaultConnectionTimeout,
      60,
      "Should have a default connection timeout of 60 seconds"
    )
  }

  func testOverridingDefaultConnectionTimeoutWithInvalidTimeout() {
    GraphRequestConnection.defaultConnectionTimeout = -1
    XCTAssertEqual(
      GraphRequestConnection.defaultConnectionTimeout,
      60,
      "Should not be able to override the default connection timeout with an invalid timeout"
    )
  }

  func testOverridingDefaultConnectionTimeoutWithValidTimeout() {
    GraphRequestConnection.defaultConnectionTimeout = 100
    XCTAssertEqual(
      GraphRequestConnection.defaultConnectionTimeout,
      100,
      "Should be able to override the default connection timeout"
    )
  }

  func testDefaultOverriddenVersionPart() {
    XCTAssertNil(
      connection.overriddenVersionPart,
      "There should not be an overridden version part by default"
    )
  }

  func testOverridingVersionPartWithInvalidVersions() {
    ["", "abc", "-5", "1.1.1.1.1", "v1.1.1.1"]
      .forEach { string in
        connection.overrideGraphAPIVersion(string)
        XCTAssertEqual(
          connection.overriddenVersionPart,
          string,
          "Should not be able to override the graph api version with \(string) but you can"
        )
      }
  }

  func testOverridingVersionPartWithValidVersions() {
    ["1", "1.1", "1.1.1", "v1", "v1.1", "v1.1.1"]
      .forEach { string in
        connection.overrideGraphAPIVersion(string)
        XCTAssertEqual(
          connection.overriddenVersionPart,
          string,
          "Should be able to override the graph api version with a valid version string"
        )
      }
  }

  func testOverridingVersionCopies() {
    var version = "v1.0"
    connection.overrideGraphAPIVersion(version)
    version = "foo"

    XCTAssertNotEqual(
      version,
      connection.overriddenVersionPart,
      "Should copy the version so that changes to the original string do not affect the stored value"
    )
  }

  func testDefaultCanMakeRequests() {
    GraphRequestConnection.resetCanMakeRequests()
    XCTAssertFalse(
      GraphRequestConnection.canMakeRequests(),
      "Should not be able to make requests by default"
    )
  }

  func testDelegateQueue() {
    XCTAssertNil(connection.delegateQueue, "Should not have a delegate queue by default")
  }

  func testSettingDelegateQueue() {
    let queue = OperationQueue()
    connection.delegateQueue = queue
    XCTAssertEqual(
      connection.delegateQueue,
      queue,
      "Should be able to set the delegate queue"
    )
    XCTAssertEqual(
      session.delegateQueue,
      queue,
      "Should set the session's delegate queue when setting the connnection's delegate queue"
    )
  }

  // MARK: - Adding Requests

  func testAddingRequestWithoutBatchEntryName() throws {
    connection.add(makeRequestForMeWithEmptyFields()) { _, _, _ in }
    let metadata = try XCTUnwrap(connection.requests.firstObject as? GraphRequestMetadata)

    XCTAssertTrue(
      metadata.batchParameters.isEmpty,
      "Adding a request without a batch entry name should not store batch parameters"
    )
  }

  func testAddingRequestWithEmptyBatchEntryName() throws {
    connection.add(
      makeRequestForMeWithEmptyFields(),
      name: ""
    ) { _, _, _ in }
    let metadata = try XCTUnwrap(connection.requests.firstObject as? GraphRequestMetadata)

    XCTAssertTrue(
      metadata.batchParameters.isEmpty,
      "Should not store batch parameters for a request with an empty batch entry name"
    )
  }

  func testAddingRequestWithValidBatchEntryName() throws {
    connection.add(
      makeRequestForMeWithEmptyFields(),
      name: "foo"
    ) { _, _, _ in }
    let expectedParameters = ["name": "foo"]
    let metadata = try XCTUnwrap(connection.requests.firstObject as? GraphRequestMetadata)

    XCTAssertEqual(
      metadata.batchParameters as? [String: String],
      expectedParameters,
      "Should create and store batch parameters for a request with a non-empty batch entry name"
    )
  }

  func testAddingRequestWithBatchParameters() {
    [
      GraphRequestConnectionState.started,
      .cancelled,
      .completed,
      .serialized,
    ]
      .forEach { state in
        connection.state = state
        assertRaisesException(
          message: "Should raise an exception on request addition when state has raw value: \(state.rawValue)"
        ) {
          self.connection.add(
            self.makeRequestForMeWithEmptyFields(),
            parameters: [:]
          ) { _, _, _ in }
        }
      }
    connection.state = .created

    assertDoesNotRaiseException(
      message: "Should not throw an error on request addition when state is 'created'"
    ) {
      self.connection.add(
        self.makeRequestForMeWithEmptyFields(),
        parameters: [:]
      ) { _, _, _ in }
    }
  }

  func testAddingRequestToBatchWithBatchParameters() throws {
    let batchParameters = [
      name: "Foo",
      "Bar": "Baz",
    ]
    let metadata = GraphRequestMetadata(
      request: makeSampleRequest(),
      completionHandler: nil,
      batchParameters: batchParameters
    )
    let batch = NSMutableArray()
    connection.addRequest(
      metadata,
      toBatch: batch,
      attachments: NSMutableDictionary(),
      batchToken: nil
    )

    let first = batch.firstObject as? [String: String]
    XCTAssertEqual(
      first?[name],
      "Foo",
      "Should add the batch parameters to the from the request to the batch"
    )
    XCTAssertEqual(
      first?["Bar"],
      "Baz",
      "Should add the batch parameters to the from the request to the batch"
    )
  }

  func testAddingRequestToBatchSetsMethod() {
    let postRequest = TestGraphRequest(
      graphPath: "me",
      httpMethod: .post
    )
    let metadata = GraphRequestMetadata(
      request: postRequest,
      completionHandler: nil,
      batchParameters: [:]
    )
    let batch = NSMutableArray()
    connection.addRequest(
      metadata,
      toBatch: batch,
      attachments: NSMutableDictionary(),
      batchToken: nil
    )
    let parameters = batch.firstObject as? [String: HTTPMethod]
    XCTAssertEqual(
      parameters?["method"],
      .post,
      "Should include the http method from the graph request in the batch"
    )
  }

  func testAddingRequestToBatchWithToken() throws {
    let token = name
    let expectedItem = URLQueryItem(name: "access_token", value: token)
    let metadata = GraphRequestMetadata(
      request: makeSampleRequest(),
      completionHandler: nil,
      batchParameters: [:]
    )
    let batch = NSMutableArray()
    connection.addRequest(
      metadata,
      toBatch: batch,
      attachments: NSMutableDictionary(),
      batchToken: token
    )
    let parameters = try XCTUnwrap(batch.firstObject as? [String: String])
    let urlString = try XCTUnwrap(parameters["relative_url"])
    let queryItems = try XCTUnwrap(
      URLComponents(string: urlString)?.queryItems
    )
    XCTAssertTrue(
      queryItems.contains(expectedItem),
      "Should include the batch token in the url for the batch request"
    )
  }

  func testAddingRequestToBatchWithAttachments() throws {
    let data = try XCTUnwrap("foo".data(using: .utf8))
    let data2 = try XCTUnwrap("bar".data(using: .utf8))

    let request = makeSampleRequest(parameters: [name: data])
    let request2 = makeSampleRequest(parameters: [name: data2])
    let metadata1 = makeMetadata(from: request)
    let metadata2 = makeMetadata(from: request2)

    let batch = NSMutableArray()
    let attachments = NSMutableDictionary()
    connection.addRequest(
      metadata1,
      toBatch: batch,
      attachments: attachments,
      batchToken: nil
    )
    connection.addRequest(
      metadata2,
      toBatch: batch,
      attachments: attachments,
      batchToken: nil
    )
    batch.enumerateObjects { object, index, _ in
      let expectedFileName = "file\(index)"
      let parameters = object as? [String: String]
      XCTAssertEqual(
        parameters?["attached_files"],
        expectedFileName,
        "Should store retrieval keys for the attachments taken from the graph requests"
      )
    }
    let expectedAttachments = [
      "file0": data,
      "file1": data2,
    ]
    XCTAssertEqual(
      expectedAttachments,
      attachments as? [String: Data],
      "Should add attachments from the graph requests"
    )
  }

  // MARK: - Attachments

  func testAppendingNonFormStringAttachment() {
    let body = TestGraphRequestBody()
    connection.appendAttachments(
      [name: "foo"],
      to: body,
      addFormData: false,
      logger: logger
    )
    XCTAssertNil(
      body.capturedKey,
      "Should not append strings if the attachment type is not form data"
    )
    XCTAssertNil(
      body.capturedFormValue,
      "Should not append strings if the attachment type is not form data"
    )
  }

  func testAppendingFormStringAttachment() {
    let body = TestGraphRequestBody()
    connection.appendAttachments(
      [name: "foo"],
      to: body,
      addFormData: true,
      logger: logger
    )
    XCTAssertEqual(
      body.capturedKey,
      name,
      "Should append strings when the attachment type is form data"
    )
    XCTAssertEqual(body.capturedFormValue, "foo", "Should pass through whether or not to use form data")
  }

  func testAppendingImageData() {
    let image = UIImage()
    let body = TestGraphRequestBody()
    connection.appendAttachments(
      [name: image],
      to: body,
      addFormData: false,
      logger: logger
    )
    XCTAssertEqual(
      body.capturedImage,
      image,
      "Should always append images"
    )

    body.capturedImage = nil
    connection.appendAttachments(
      [name: image],
      to: body,
      addFormData: true,
      logger: logger
    )
    XCTAssertIdentical(
      body.capturedImage,
      image,
      "Should always append images"
    )
  }

  func testAppendingData() {
    let data = name.data(using: .utf8)! // swiftlint:disable:this force_unwrapping
    let body = TestGraphRequestBody()
    connection.appendAttachments(
      [name: data],
      to: body,
      addFormData: false,
      logger: logger
    )
    XCTAssertEqual(
      body.capturedData,
      data,
      "Should always append data"
    )

    body.capturedData = nil
    connection.appendAttachments(
      [name: data],
      to: body,
      addFormData: true,
      logger: logger
    )
    XCTAssertEqual(
      body.capturedData,
      data,
      "Should always append data"
    )
  }

  func testAppendingDataAttachments() {
    let data = name.data(using: .utf8)! // swiftlint:disable:this force_unwrapping
    let attachment = GraphRequestDataAttachment(
      data: data,
      filename: "fooFile",
      contentType: "application/json"
    )
    let body = TestGraphRequestBody()
    connection.appendAttachments(
      [name: attachment],
      to: body,
      addFormData: false,
      logger: logger
    )
    XCTAssertEqual(
      body.capturedAttachment,
      attachment,
      "Should always append data attachments"
    )

    body.capturedAttachment = nil
    connection.appendAttachments(
      [name: attachment],
      to: body,
      addFormData: true,
      logger: logger
    )
    XCTAssertEqual(
      body.capturedAttachment,
      attachment,
      "Should always append data attachments"
    )
  }

  func testAppendingUnknownAttachmentTypeWithLogger() {
    let body = TestGraphRequestBody()
    let logger = makeLogger()
    connection.appendAttachments(
      [name: UIColor.gray],
      to: body,
      addFormData: false,
      logger: logger
    )
    XCTAssertEqual(
      TestLogger.capturedLoggingBehavior,
      .developerErrors,
      "Should log an error when an unsupported type is attached"
    )
    XCTAssertEqual(
      TestLogger.capturedLogEntry,
      "Unsupported FBSDKGraphRequest attachment:UIExtendedGrayColorSpace 0.5 1, skipping.",
      "Should log an error when an unsupported type is attached"
    )
  }

  // MARK: - Cancelling

  func testCancellingConnection() {
    var expectedInvalidationCallCount = 0

    [
      GraphRequestConnectionState.created,
      .started,
      .cancelled,
      .completed,
      .serialized,
    ]
      .forEach { state in
        connection.state = state
        expectedInvalidationCallCount += 1

        connection.cancel()

        XCTAssertEqual(
          connection.state,
          .cancelled,
          "Cancelling a connection should set the state to the expected value"
        )
        XCTAssertEqual(
          session.invalidateAndCancelCallCount,
          expectedInvalidationCallCount,
          "Cancelling a connetion should invalidate and cancel the session"
        )
      }
  }

  // MARK: - Starting

  func testStartingConnectionWithUninitializedSDK() {
    GraphRequestConnection.resetCanMakeRequests()
    connection.logger = makeLogger()

    let expectedMessage = "FBSDKGraphRequestConnection cannot be started before Facebook SDK initialized."
    var capturedError: Error?
    connection.add(makeSampleRequest()) { _, _, error in
      capturedError = error
    }
    connection.start()

    let testError = capturedError as? TestSDKError
    XCTAssertEqual(
      testError?.type,
      .unknown,
      "Starting a graph request before the SDK is initialized should return an unknown-type error"
    )
    XCTAssertEqual(
      testError?.message,
      expectedMessage,
      "Starting a graph request before the SDK is initialized should return an error with the appropriate mesage"
    )

    XCTAssertEqual(
      connection.state,
      .cancelled,
      "Starting a graph request before the SDK is initialized should update the connection state"
    )

    XCTAssertEqual(
      TestLogger.capturedLogEntry,
      expectedMessage,
      "Starting a graph request before the SDK is initialized should log a warning"
    )
    XCTAssertEqual(
      TestLogger.capturedLoggingBehavior,
      .developerErrors,
      "Starting a graph request before the SDK is initialized should log a warning"
    )
  }

  func testStartingWithInvalidStates() {
    connection.logger = makeLogger()

    [
      GraphRequestConnectionState.started,
      .cancelled,
      .completed,
    ]
      .forEach { state in
        connection.state = .created
        connection.add(makeSampleRequest()) { _, _, _ in
          XCTFail("Should not be called")
        }
        connection.state = state
        connection.start()
        XCTAssertEqual(
          connection.state,
          state,
          "Should not change the connection state when starting in an invalid state"
        )
        XCTAssertEqual(
          TestLogger.capturedLogEntry,
          "FBSDKGraphRequestConnection cannot be started again.",
          "Starting a connection in an invalid state"
        )
        XCTAssertEqual(
          TestLogger.capturedLoggingBehavior,
          .developerErrors,
          "Starting a connection in an invalid state"
        )
        XCTAssertNil(session.capturedRequest, "Should not start a request for a connection in an invalid state")
      }
  }

  func testStartingWithValidStates() {
    [
      GraphRequestConnectionState.created,
      .serialized,
    ]
      .forEach { state in
        connection.state = .created
        connection.add(makeSampleRequest()) { _, _, _ in
          XCTFail("Should not be called")
        }
        connection.state = state
        connection.start()
        XCTAssertEqual(
          connection.state,
          .started,
          "Should change the connection state to 'started' when starting in an valid state"
        )
        XCTAssertNotNil(session.capturedRequest, "Should start a request for a connection in an valid state")
      }
  }

  func testStartingWithDelegateQueue() {
    connection.delegate = self
    let queue = TestOperationQueue()
    connection.delegateQueue = queue
    connection.add(makeSampleRequest()) { _, _, _ in
      XCTFail("Should not be called")
    }
    connection.start()
    XCTAssertTrue(
      queue.addOperationWithBlockWasCalled,
      "Starting a connection should add the request to the delegate queue when one exists"
    )
  }

  func testStartingInvokesPiggybackManager() {
    connection.add(makeSampleRequest()) { _, _, _ in }
    connection.start()

    XCTAssertTrue(
      connection === piggybackManager.capturedConnection,
      "Starting a request should invoke the piggyback manager"
    )
  }

  // MARK: - Errors From Results

  func testErrorFromResultWithNonDictionaryInput() {
    let inputs: [Any] = ["foo", 123, true, NSNull(), Data(), [Any]()]

    for input in inputs {
      XCTAssertNil(
        connection.error(fromResult: input, request: makeSampleRequest()),
        "Should not create an error from \(input)"
      )
    }
  }

  func testErrorFromResultWithMissingBodyInInput() {
    XCTAssertNil(
      connection.error(fromResult: [], request: makeSampleRequest()),
      "Should not create an error from an empty dictionary"
    )
  }

  func testErrorFromResultWithMissingErrorInInputBody() {
    let result: [String: Any] = [
      "body": [],
    ]

    XCTAssertNil(
      connection.error(fromResult: result, request: makeSampleRequest()),
      "Should not create an error from a dictionary with a missing error key"
    )
  }

  func testErrorFromResultWithFuzzyInput() {
    (1 ... 100).forEach { _ in
      connection.error(
        fromResult: Fuzzer.randomize(json: makeSampleErrorDictionary()),
        request: makeSampleRequest()
      )
    }
  }

  func testErrorFromResultDependsOnErrorConfiguration() {
    connection.error(
      fromResult: NSDictionary(dictionary: makeSampleErrorDictionary()),
      request: makeSampleRequest()
    )
    let capturedRequest = errorConfiguration.capturedGraphRequest

    XCTAssertNotNil(capturedRequest?.graphPath, "Should capture the graph request from the result")
    XCTAssertEqual(
      errorConfiguration.capturedRecoveryConfigurationCode,
      "1",
      "Should capture the error code from the result"
    )
    XCTAssertEqual(
      errorConfiguration.capturedRecoveryConfigurationSubcode,
      "2",
      "Should capture the error subcode from the result"
    )
  }

  func testErrorFromResult() {
    let error = connection.error(
      fromResult: makeSampleErrorDictionary(),
      request: makeSampleRequest()
    ) as NSError?
    XCTAssertEqual(
      error?.userInfo[NSLocalizedRecoverySuggestionErrorKey] as? String,
      errorRecoveryConfiguration.localizedRecoveryDescription,
      "Should derive the recovery description from the recovery configuration"
    )
    XCTAssertEqual(
      error?.userInfo[NSLocalizedRecoveryOptionsErrorKey] as? [String],
      errorRecoveryConfiguration.localizedRecoveryOptionDescriptions,
      "Should derive the recovery options from the recovery configuration"
    )
    XCTAssertNil(
      error?.userInfo[NSRecoveryAttempterErrorKey],
      "A non transient error should not provide a recovery attempter"
    )
  }

  func testErrorFromResultMessagePriority() {
    var response: [String: Any] = [
      "body": [
        "error": ["error_msg": "error_msg"],
      ],
    ]
    var error = connection.error(
      fromResult: response,
      request: makeSampleRequest()
    ) as? TestSDKError
    XCTAssertEqual(
      error?.message,
      "error_msg",
      "Should use the 'error_msg' if it's the only message available"
    )
    response = [
      "body": [
        "error": [
          "error_msg": "error_msg",
          "error_reason": "error_reason",
        ],
      ],
    ]
    error = connection.error(
      fromResult: response,
      request: makeSampleRequest()
    ) as? TestSDKError
    XCTAssertEqual(
      error?.message,
      "error_reason",
      "Should prefer the 'error_reason' to the 'error_msg'"
    )

    response = [
      "body": [
        "error": [
          "error_msg": "error_msg",
          "error_reason": "error_reason",
          "message": "message",
        ],
      ],
    ]
    error = connection.error(
      fromResult: response,
      request: makeSampleRequest()
    ) as? TestSDKError
    XCTAssertEqual(
      error?.message,
      "message",
      "Should prefer the 'message' key to other error message keys"
    )
  }

  // MARK: - Client Token

  func testClientToken() throws {
    let expectation = XCTestExpectation(description: name)

    errorConfigurationProvider.configuration = nil
    var capturedError: Error?
    connection.add(makeRequestForMeWithEmptyFields()) { _, _, error in
      capturedError = error
      expectation.fulfill()
    }
    connection.start()

    let data = "{\"error\": {\"message\": \"Token is broken\",\"code\": 190,\"error_subcode\": 463, \"type\":\"OAuthException\"}}".data(using: .utf8)! // swiftlint:disable:this force_unwrapping line_length
    let response = HTTPURLResponse(
      url: sampleUrl,
      statusCode: 400,
      httpVersion: nil,
      headerFields: nil
    )

    session.capturedCompletion?(data, response, nil)

    wait(for: [expectation], timeout: 1)

    let error = try XCTUnwrap(capturedError as NSError?)
    // make sure there is no recovery info for client token failures.
    XCTAssertNil(error.localizedRecoverySuggestion)
  }

  func testClientTokenSkipped() throws {
    let expectation = expectation(description: name)
    var capturedError: Error?
    errorConfigurationProvider.configuration = nil
    connection.add(makeRequestForMeWithEmptyFields()) { _, _, error in
      capturedError = error
      expectation.fulfill()
    }
    connection.start()

    let response = HTTPURLResponse(url: sampleUrl, statusCode: 400, httpVersion: nil, headerFields: nil)

    session.capturedCompletion?(missingTokenData, response, nil)
    wait(for: [expectation], timeout: 1)

    let error = try XCTUnwrap(capturedError as NSError?)
    // make sure there is no recovery info for client token failures.
    XCTAssertNil(error.localizedRecoverySuggestion)
  }

  func testConnectionDelegate() {
    let expectation = expectation(description: name)

    var actualCallbacksCount = 0
    connection.add(makeRequestForMeWithEmptyFields()) { _, _, _ in
      XCTAssertEqual(1, actualCallbacksCount, "this should have been the second callback")
      actualCallbacksCount += 1
    }
    connection.add(makeRequestForMeWithEmptyFields()) { _, _, _ in
      XCTAssertEqual(2, actualCallbacksCount, "this should have been the third callback")
      actualCallbacksCount += 1
    }
    requestConnectionStartingCallback = { _ in
      XCTAssertEqual(actualCallbacksCount, 0, "this should have been the first callback")
      actualCallbacksCount += 1
    }
    requestConnectionCallback = { _, error in
      XCTAssertNil(error, "unexpected error: \(String(describing: error))")
      XCTAssertEqual(actualCallbacksCount, 3, "this should have been the fourth callback")
      actualCallbacksCount += 1
      expectation.fulfill()
    }
    connection.delegate = self
    connection.start()

    let meResponse = "{ \"Any\":\"userid\"}".replacingOccurrences(of: "\"", with: "\\\"")
    let responseString = "[{\"code\":200,\"body\": \"\(meResponse)\" }, {\"code\":200,\"body\": \"\(meResponse)\" } ]"
    let data = responseString.data(using: .utf8)
    let response = HTTPURLResponse(url: sampleUrl, statusCode: 200, httpVersion: nil, headerFields: nil)

    session.capturedCompletion?(data, response, nil)

    wait(for: [expectation], timeout: 1)
  }

  func testNonErrorEmptyDictionaryOrNullResponse() {
    let expectation = expectation(description: name)

    var actualCallbacksCount = 0
    connection.add(makeRequestForMeWithEmptyFields()) { _, _, _ in
      XCTAssertEqual(actualCallbacksCount, 1, "this should have been the second callback")
      actualCallbacksCount += 1
    }
    connection.add(makeRequestForMeWithEmptyFields()) { _, _, _ in
      XCTAssertEqual(actualCallbacksCount, 2, "this should have been the third callback")
      actualCallbacksCount += 1
    }
    requestConnectionStartingCallback = { _ in
      XCTAssertEqual(actualCallbacksCount, 0, "this should have been the first callback")
      actualCallbacksCount += 1
    }
    requestConnectionCallback = { _, error in
      XCTAssertNil(error, "unexpected error: \(String(describing: error))")
      XCTAssertEqual(actualCallbacksCount, 3, "this should have been the fourth callback")
      actualCallbacksCount += 1
      expectation.fulfill()
    }
    connection.delegate = self
    connection.start()

    let responseString = "[{\"code\":200,\"body\": null }, {\"code\":200,\"body\": {} } ]"
    let data = responseString.data(using: .utf8)
    let response = HTTPURLResponse(url: sampleUrl, statusCode: 200, httpVersion: nil, headerFields: nil)

    session.capturedCompletion?(data, response, nil)

    wait(for: [expectation], timeout: 1)
  }

  func testConnectionDelegateWithNetworkError() {
    let expectation = expectation(description: name)

    connection.add(makeRequestForMeWithEmptyFields()) { _, _, _ in }
    requestConnectionCallback = { _, error in
      XCTAssertNotNil(error, "didFinishLoading shouldn't have been called")
      expectation.fulfill()
    }
    connection.delegate = self
    connection.start()

    session.capturedCompletion?(nil, nil, NSError(domain: ".domain", code: -1009, userInfo: nil))

    wait(for: [expectation], timeout: 1)
  }

  func testUnsettingAccessToken() {
    let expectation = expectation(description: name)
    let accessToken = AccessToken(
      tokenString: "token",
      permissions: ["public_profile"],
      declinedPermissions: [],
      expiredPermissions: [],
      appID: "appid",
      userID: "userid",
      expirationDate: nil,
      refreshDate: nil,
      dataAccessExpirationDate: nil
    )
    TestAccessTokenWallet.current = accessToken

    connection.add(makeRequest(tokenString: accessToken.tokenString)) { _, result, error in
      XCTAssertNil(result)
      let testError = error as? TestSDKError
      XCTAssertEqual("Token is broken", testError?.message)
      XCTAssertNil(
        TestAccessTokenWallet.current,
        "Should clear the current stored access token"
      )
      expectation.fulfill()
    }
    connection.start()

    let response = HTTPURLResponse(url: sampleUrl, statusCode: 400, httpVersion: nil, headerFields: nil)

    session.capturedCompletion?(missingTokenData, response, nil)

    wait(for: [expectation], timeout: 1)
  }

  func testUnsettingAccessTokenSkipped() {
    let expectation = expectation(description: name)
    TestAccessTokenWallet.current = AccessToken(
      tokenString: "token",
      permissions: ["public_profile"],
      declinedPermissions: [],
      expiredPermissions: [],
      appID: "appid",
      userID: "userid",
      expirationDate: nil,
      refreshDate: nil,
      dataAccessExpirationDate: nil
    )

    let request = TestGraphRequest(
      graphPath: "me",
      parameters: ["fields": ""],
      tokenString: "notCurrentToken"
    )
    connection.add(request) { _, result, error in
      XCTAssertNil(result)
      let testError = error as? TestSDKError
      XCTAssertEqual("Token is broken", testError?.message)
      XCTAssertNotNil(TestAccessTokenWallet.current)
      expectation.fulfill()
    }
    connection.start()

    let response = HTTPURLResponse(url: sampleUrl, statusCode: 400, httpVersion: nil, headerFields: nil)

    session.capturedCompletion?(missingTokenData, response, nil)

    wait(for: [expectation], timeout: 1)
  }

  func testUnsettingAccessTokenFlag() {
    let expectation = expectation(description: name)
    TestAccessTokenWallet.current = AccessToken(
      tokenString: "token",
      permissions: ["public_profile"],
      declinedPermissions: [],
      expiredPermissions: [],
      appID: "appid",
      userID: "userid",
      expirationDate: nil,
      refreshDate: nil,
      dataAccessExpirationDate: nil
    )

    let request = TestGraphRequest(
      graphPath: "me",
      parameters: ["fields": ""],
      flags: [.doNotInvalidateTokenOnError]
    )
    connection.add(request) { _, result, error in
      XCTAssertNil(result)
      let testError = error as? TestSDKError
      XCTAssertEqual("Token is broken", testError?.message)
      XCTAssertNotNil(TestAccessTokenWallet.current)
      expectation.fulfill()
    }
    connection.start()

    let response = HTTPURLResponse(url: sampleUrl, statusCode: 400, httpVersion: nil, headerFields: nil)

    session.capturedCompletion?(missingTokenData, response, nil)

    wait(for: [expectation], timeout: 1)
  }

  func testRequestWithUserAgentSuffix() throws {
    settings.userAgentSuffix = "UnitTest.1.0.0"

    connection.add(makeRequestForMeWithEmptyFields()) { _, _, _ in }
    connection.start()

    let userAgent = try XCTUnwrap(session.capturedRequest?.value(forHTTPHeaderField: "User-Agent"))
    XCTAssertTrue(userAgent.hasSuffix("/UnitTest.1.0.0"), "unexpected user agent \(userAgent)")
  }

  func testRequestWithoutUserAgentSuffix() throws {
    settings.userAgentSuffix = nil

    connection.add(makeRequestForMeWithEmptyFields()) { _, _, _ in }
    connection.start()

    let userAgent = try XCTUnwrap(session.capturedRequest?.value(forHTTPHeaderField: "User-Agent"))
    XCTAssertEqual(
      userAgent,
      "FBiOSSDK.\(FBSDK_VERSION_STRING)",
      "unexpected user agent \(userAgent)"
    )
  }

  func testRequestWithMacCatalystUserAgent() throws {
    macCatalystDeterminator.stubbedIsMacCatalystApp = true
    settings.userAgentSuffix = nil

    connection.add(makeRequestForMeWithEmptyFields()) { _, _, _ in }
    connection.start()

    let userAgent = try XCTUnwrap(session.capturedRequest?.value(forHTTPHeaderField: "User-Agent"))
    XCTAssertTrue(userAgent.hasSuffix("/macOS"), "unexpected user agent \(userAgent)")
  }

  func testNonDictionaryInError() {
    let expectation = expectation(description: name)

    connection.add(makeRequestForMeWithEmptyFields()) { _, _, _ in
      // should not crash when receiving something other than a dictionary within the response.
      expectation.fulfill()
    }
    connection.start()

    let data = "{\"error\": \"a-non-dictionary\"}".data(using: .utf8)
    let response = HTTPURLResponse(url: sampleUrl, statusCode: 200, httpVersion: nil, headerFields: nil)

    session.capturedCompletion?(data, response, nil)

    wait(for: [expectation], timeout: 1)
  }

  func testRequestWithBatchConstructionWithSingleGetRequest() throws {
    let singleRequest = TestGraphRequest(graphPath: "me", parameters: ["fields": "with_suffix"])
    connection.add(singleRequest) { _, _, _ in }
    let requests = try XCTUnwrap(connection.requests as? [GraphRequestMetadata])
    let request = connection.request(withBatch: requests, timeout: 0)

    let url = try XCTUnwrap(request.url)
    let urlComponents = try XCTUnwrap(URLComponents(url: url, resolvingAgainstBaseURL: true))
    let requestBody = try XCTUnwrap(request.httpBody)

    XCTAssertEqual(urlComponents.host, "graph.facebook.com")
    XCTAssertTrue(urlComponents.path.contains("me"))
    XCTAssertEqual(request.httpMethod, "GET")
    XCTAssertEqual(requestBody.count, 0)
    XCTAssertEqual(request.value(forHTTPHeaderField: "Content-Type"), "application/json")
  }

  func testRequestWithBatchConstructionWithSinglePostRequest() throws {
    // T108100329: Abstract internal utility from GraphRequestConnection
    AuthenticationToken.current = nil

    let parameters: [String: Any] = [
      "first_key": "first_value",
    ]
    let singleRequest = TestGraphRequest(graphPath: "activities", parameters: parameters, httpMethod: .post)
    connection.add(singleRequest) { _, _, _ in }
    let requests = try XCTUnwrap(connection.requests as? [GraphRequestMetadata])
    let request = connection.request(withBatch: requests, timeout: 0)

    let url = try XCTUnwrap(request.url)
    let urlComponents = try XCTUnwrap(URLComponents(url: url, resolvingAgainstBaseURL: true))
    let requestBody = try XCTUnwrap(request.httpBody)

    XCTAssertEqual(urlComponents.host, "graph.facebook.com")
    XCTAssertTrue(urlComponents.path.contains("activities"))
    XCTAssertEqual(request.httpMethod, "POST")
    XCTAssertGreaterThan(requestBody.count, 0)
    XCTAssertEqual(request.value(forHTTPHeaderField: "Content-Encoding"), "gzip")
    XCTAssertEqual(request.value(forHTTPHeaderField: "Content-Type"), "application/json")
  }

  // MARK: - accessTokenWithRequest

  func testAccessTokenWithRequest() {
    // T108100329: Abstract internal utility from GraphRequestConnection
    AuthenticationToken.current = nil

    let expectedToken = "fake_token"
    let request = TestGraphRequest(
      graphPath: "me",
      parameters: ["fields": ""],
      tokenString: expectedToken,
      httpMethod: .get,
      flags: []
    )
    let token = connection.accessToken(with: request)
    XCTAssertEqual(token, expectedToken)
  }

  func testAccessTokenWithRequestWithoutFacebookClientToken() throws {
    settings.clientToken = nil
    connection.logger = makeLogger()

    assertRaisesException(message: "An exception should be raised if a client token is unavailable") {
      self.connection.accessToken(with: self.makeRequestForMeWithEmptyFieldsNoTokenString())
    }

    XCTAssertEqual(
      TestLogger.capturedLoggingBehavior,
      .developerErrors,
      "Should log a developer error when a request is started with no client token set"
    )

    let message = try XCTUnwrap(TestLogger.capturedLogEntry)
    XCTAssertTrue(
      message.starts(with: "Starting with v13 of the SDK, a client token must be embedded in your client code"),
      "Should log the expected error message when a request is started with no client token set"
    )

    TestLogger.reset()

    assertRaisesException(message: "An exception should be raised if a client token is unavailable") {
      self.connection.accessToken(with: self.makeRequestForMeWithEmptyFieldsNoTokenString())
    }

    XCTAssertEqual(
      TestLogger.capturedLoggingBehavior,
      .developerErrors,
      "Should log consistently for requests started with no client token set"
    )
  }

  func testAccessTokenWithRequestWithFacebookClientToken() {
    connection.logger = makeLogger()
    let token = connection.accessToken(with: makeRequestForMeWithEmptyFieldsNoTokenString())

    let expectedToken = "\(appID)|\(clientToken)"
    XCTAssertEqual(token, expectedToken)

    XCTAssertNil(
      TestLogger.capturedLoggingBehavior,
      "Should not log a developer error when a request is started with a client token set"
    )
  }

  func testAccessTokenWithRequestWithGamingClientToken() {
    settings.clientToken = clientToken
    let authToken = AuthenticationToken(
      tokenString: "token_string",
      nonce: "nonce",
      graphDomain: "gaming"
    )
    TestAuthenticationTokenWallet.current = authToken
    let token = connection.accessToken(with: makeRequestForMeWithEmptyFieldsNoTokenString())

    let expectedToken = "GG|\(appID)|\(clientToken)"
    XCTAssertEqual(token, expectedToken)
  }

  // MARK: - Error recovery.

  func testRetryWithTransientError() throws {
    let expectation = expectation(description: name)

    settings.isGraphErrorRecoveryEnabled = true

    errorRecoveryConfiguration = makeTransientErrorRecoveryConfiguration()
    errorConfiguration.stubbedRecoveryConfiguration = errorRecoveryConfiguration
    errorConfigurationProvider.configuration = errorConfiguration
    let retryConnection = GraphRequestConnection()
    graphRequestConnectionFactory.stubbedConnection = retryConnection

    var completionCallCount = 0
    var capturedError: Error?
    connection.add(makeRequestForMeWithEmptyFields()) { _, _, error in
      completionCallCount += 1
      XCTAssertEqual(completionCallCount, 1, "The completion should only be called once")
      capturedError = error
      expectation.fulfill()
    }

    connection.start()

    let data = "{\"error\": {\"message\": \"Server is busy\",\"code\": 1,\"error_subcode\": 463}}".data(using: .utf8)
    let response = HTTPURLResponse(url: sampleUrl, statusCode: 400, httpVersion: nil, headerFields: nil)

    // The first captured completion will be invoked and cause the retry
    session.capturedCompletion?(data, response, nil)

    // It's necessary to dispatch async to avoid the completion from being invoked before it is captured
    DispatchQueue.main.async {
      let secondData = "{\"error\": {\"message\": \"Server is busy\",\"code\": 2,\"error_subcode\": 463}}".data(using: .utf8) // swiftlint:disable:this line_length
      self.secondSession.capturedCompletion?(secondData, response, nil)
    }

    wait(for: [expectation], timeout: 1)

    let error = try XCTUnwrap(capturedError as NSError?)
    XCTAssertEqual(
      2,
      error.userInfo[GraphRequestErrorGraphErrorCodeKey] as? Int,
      "The completion should be called with the expected error code"
    )
  }

  func testRetryDisabled() throws {
    settings.isGraphErrorRecoveryEnabled = false

    let expectation = expectation(description: name)

    var completionCallCount = 0
    var capturedError: Error?
    connection.add(makeRequestForMeWithEmptyFields()) { _, _, error in
      capturedError = error
      completionCallCount += 1
      XCTAssertEqual(completionCallCount, 1, "The completion should only be called once")
      expectation.fulfill()
    }

    connection.start()

    let data = "{\"error\": {\"message\": \"Server is busy\",\"code\": 1,\"error_subcode\": 463}}".data(using: .utf8)
    let response = HTTPURLResponse(url: sampleUrl, statusCode: 400, httpVersion: nil, headerFields: nil)

    // The first captured completion will be invoked and cause the retry
    session.capturedCompletion?(data, response, nil)

    wait(for: [expectation], timeout: 1)

    let error = try XCTUnwrap(capturedError as NSError?)
    XCTAssertEqual(
      1,
      error.userInfo[GraphRequestErrorGraphErrorCodeKey] as? Int,
      "The completion should be called with the expected error code"
    )
  }

  // MARK: - Response Parsing

  func testParsingJsonResponseWithInvalidData() {
    var value = 0xb70f
    let data = Data(bytes: &value, count: 2)
    var error: NSError?
    connection.parseJSONResponse(data, error: &error, statusCode: 0)

    XCTAssertEqual(
      eventLogger.capturedEventName?.rawValue,
      "fb_response_invalid_utf8",
      "Should log the correct event name"
    )
    XCTAssertTrue(
      eventLogger.capturedIsImplicitlyLogged,
      "Should implicitly log an event indicating a json parsing failure"
    )
  }

  func testProcessingResultBodyWithDebugDictionary() {
    connection.logger = makeLogger()
    let entries = [
      "message1 Link: link1",
      "message2 Link: link2",
    ]
    connection.processResultBody(debugResponse, error: nil, metadata: metadata, canNotifyDelegate: false)
    XCTAssertEqual(
      TestLogger.capturedLogEntries,
      entries,
      "Should log entries from the debug dictionary"
    )
  }

  func testProcessingResultBodyWithRandomizedDebugDictionary() {
    (1 ..< 100).forEach { _ in
      if let body = Fuzzer.randomize(json: debugResponse) as? [String: Any] {
        connection.processResultBody(body, error: nil, metadata: metadata, canNotifyDelegate: false)
      }
    }
  }

  func testLogRequestWithInactiveLogger() {
    let request = NSMutableURLRequest(url: sampleUrl)
    let logger = makeLogger()
    let bodyLogger = makeLogger()
    let attachmentLogger = makeLogger()
    connection.logger = logger
    connection.logRequest(request, bodyLength: 1024, bodyLogger: bodyLogger, attachmentLogger: attachmentLogger)

    XCTAssertEqual(logger.capturedAppendedKeys, [])
    XCTAssertEqual(logger.capturedAppendedValues, [])
  }

  func testLogRequestWithActiveLogger() {
    let request = NSMutableURLRequest(url: sampleUrl)
    request.addValue("user agent", forHTTPHeaderField: "User-Agent")
    request.addValue("content type", forHTTPHeaderField: "Content-Type")
    let logger = makeLogger()
    let bodyLogger = makeLogger()
    let attachmentLogger = makeLogger()

    // Start with some previously 'logged' contents
    bodyLogger.capturedContents = "bodyContents"
    attachmentLogger.capturedContents = "attachmentLoggerContents"
    logger.stubbedIsActive = true
    connection.logger = logger

    connection.logRequest(request, bodyLength: 1024, bodyLogger: bodyLogger, attachmentLogger: attachmentLogger)

    let expectedKeys = [
      "URL",
      "Method",
      "UserAgent",
      "MIME",
      "Body Size",
      "Body (w/o attachments)",
      "Attachments",
    ]

    let expectedValues = [
      "https://example.com",
      "GET",
      "user agent",
      "content type",
      "1 kB",
      "bodyContents",
      "attachmentLoggerContents",
    ]

    XCTAssertEqual(
      logger.capturedAppendedKeys,
      expectedKeys,
      "Should append the expected key value pairs to log"
    )
    XCTAssertEqual(
      logger.capturedAppendedValues,
      expectedValues,
      "Should append the expected key value pairs to log"
    )
  }

  func testInvokesDelegate() {
    connection.delegate = self
    connection.urlSession(
      URLSession.shared,
      task: URLSessionDataTask(),
      didSendBodyData: 0,
      totalBytesSent: 0,
      totalBytesExpectedToSend: 0
    )

    XCTAssertTrue(
      didInvokeDelegateRequestConnectionDidSendBodyData,
      "The url session data delegate should pass through to the graph request connection delegate"
    )
  }

  // MARK: - Helpers

  func makeLogger() -> TestLogger {
    TestLogger(loggingBehavior: .developerErrors)
  }

  func makeSampleRequest() -> TestGraphRequest {
    makeRequestForMeWithEmptyFields()
  }

  func makeSampleRequest(parameters: [String: Any]) -> TestGraphRequest {
    TestGraphRequest(graphPath: "me", parameters: parameters)
  }

  func makeRequestForMeWithEmptyFields() -> TestGraphRequest {
    TestGraphRequest(graphPath: "me", parameters: ["fields": ""])
  }

  func makeRequestForMeWithEmptyFieldsNoTokenString() -> TestGraphRequest {
    TestGraphRequest(
      graphPath: "me",
      parameters: ["fields": ""],
      flags: []
    )
  }

  func makeRequest(tokenString: String) -> TestGraphRequest {
    TestGraphRequest(
      graphPath: "me",
      parameters: ["fields": ""],
      tokenString: tokenString
    )
  }

  func makeMetadata(from request: GraphRequestProtocol) -> GraphRequestMetadata {
    GraphRequestMetadata(
      request: request,
      completionHandler: nil,
      batchParameters: [:]
    )
  }

  func makeSampleErrorDictionary() -> [String: Any] {
    [
      "code": 200,
      "body": [
        "error": [
          "is_transient": 1,
          "code": 1,
          "error_subcode": 2,
          "error_msg": "error_msg",
          "error_reason": "error_reason",
          "message": "message",
          "error_user_title": "error_user_title",
          "error_user_msg": "error_user_msg",
        ],
      ],
    ]
  }

  func makeTransientErrorRecoveryConfiguration() -> _ErrorRecoveryConfiguration {
    _ErrorRecoveryConfiguration(
      recoveryDescription: "Recovery Description",
      optionDescriptions: ["Option1", "Option2"],
      category: .transient,
      recoveryActionName: "Recovery Action"
    )
  }

  func makeNonTransientErrorRecoveryConfiguration() -> _ErrorRecoveryConfiguration {
    _ErrorRecoveryConfiguration(
      recoveryDescription: "Recovery Description",
      optionDescriptions: ["Option1", "Option2"],
      category: .other,
      recoveryActionName: "Recovery Action"
    )
  }

  var debugResponse: [String: Any] {
    [
      "__debug__": [
        "messages": [
          [
            "message": "message1",
            "type": "type1",
            "link": "link1",
          ],
          [
            "message": "message2",
            "type": "warning",
            "link": "link2",
          ],
        ],
      ],
    ]
  }
}


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

  let path = "me"
  let parameters = ["fields": ""]
  let version = "v16.0"
  let prefix = "graph."
  let settings = TestSettings()
  var factory = TestGraphRequestConnectionFactory()

  override func setUp() {
    super.setUp()

    AuthenticationToken.current = nil
    GraphRequest.resetClassDependencies()
    AccessToken.resetCurrentAccessTokenCache()
    TestAccessTokenWallet.reset()

    GraphRequest.configure(
      settings: settings,
      currentAccessTokenStringProvider: TestAccessTokenWallet.self,
      graphRequestConnectionFactory: factory
    )
  }

  override func tearDown() {
    GraphRequest.resetClassDependencies()
    TestAccessTokenWallet.reset()

    super.tearDown()
  }

  // MARK: - Tests

  func testDefaultDependencies() {
    GraphRequest.resetClassDependencies()
    let request = GraphRequest(graphPath: path)

    XCTAssertNil(
      GraphRequest.settings,
      "Should not have default settings"
    )
    XCTAssertNil(
      request.tokenString,
      "Should not have a token string when no token string provider has been provided"
    )
    XCTAssertNil(
      request.graphRequestConnectionFactory,
      "Should not have a default connection factory"
    )
  }

  func testConfiguringWithDependencies() {
    TestAccessTokenWallet.current = SampleAccessTokens.validToken
    let request = GraphRequest(graphPath: path)

    XCTAssertTrue(
      GraphRequest.settings === settings,
      "GraphRequest should store the settings dependency it was configured with"
    )
    XCTAssertEqual(
      request.tokenString,
      TestAccessTokenWallet.tokenString,
      "Should use the token string provider for the token string"
    )
    XCTAssertTrue(
      request.graphRequestConnectionFactory === factory,
      "New instances should use the factory provider configured on the type"
    )
  }

  func testCreatingWithCustomURLSessionProxyFactory() {
    factory = TestGraphRequestConnectionFactory(stubbedConnection: GraphRequestConnection())
    let request = GraphRequest(
      graphPath: path,
      parameters: nil,
      tokenString: nil,
      httpMethod: nil,
      flags: [],
      graphRequestConnectionFactory: factory
    )

    XCTAssertTrue(
      request.graphRequestConnectionFactory === factory,
      "A graph request should persist the session factory it was created with"
    )
  }

  func testDefaultGETParameters() {
    verifyRequest(
      GraphRequest(graphPath: path),
      expectedGraphPath: path,
      expectedParameters: parameters,
      expectedTokenString: nil,
      expectedVersion: version,
      expectedMethod: .get
    )
  }

  func testStartRequestUsesRequestProvidedByFactory() {
    let connection = TestGraphRequestConnection()
    factory = TestGraphRequestConnectionFactory(stubbedConnection: connection)
    let request = GraphRequest(
      graphPath: path,
      parameters: nil,
      tokenString: nil,
      httpMethod: nil,
      flags: [],
      graphRequestConnectionFactory: factory
    )

    request.start { _, _, _ in }
    connection.capturedCompletion?(nil, nil, nil)

    XCTAssertEqual(
      connection.startCallCount,
      1,
      "The graph request should use the provided connection"
    )
  }

  func testGraphRequestGETWithEmptyParameters() {
    [
      GraphRequest(graphPath: path, parameters: [:]),
      GraphRequest(graphPath: path, parameters: [:], flags: []),
      GraphRequest(graphPath: path, parameters: [:], tokenString: nil, version: version, httpMethod: .get),
    ]
      .forEach {
        verifyRequest(
          $0,
          expectedGraphPath: path,
          expectedParameters: [:],
          expectedTokenString: nil,
          expectedVersion: version,
          expectedMethod: .get
        )
      }
  }

  func testGraphRequestGETWithNonEmptyParameters() {
    [
      GraphRequest(graphPath: path, parameters: parameters),
      GraphRequest(graphPath: path, parameters: parameters, flags: []),
      GraphRequest(graphPath: path, parameters: parameters, tokenString: nil, version: version, httpMethod: .get),
    ]
      .forEach {
        verifyRequest(
          $0,
          expectedGraphPath: path,
          expectedParameters: parameters,
          expectedTokenString: nil,
          expectedVersion: version,
          expectedMethod: .get
        )
      }
  }

  func testDefaultPOSTParameters() {
    let request = GraphRequest(graphPath: path, httpMethod: .post)
    verifyRequest(
      request,
      expectedGraphPath: path,
      expectedParameters: [:],
      expectedTokenString: nil,
      expectedVersion: version,
      expectedMethod: .post
    )
  }

  func testGraphRequestPOSTWithEmptyParameters() {
    [
      GraphRequest(graphPath: path, parameters: [:], httpMethod: .post),
      GraphRequest(graphPath: path, parameters: [:], tokenString: nil, version: version, httpMethod: .post),
    ]
      .forEach {
        verifyRequest(
          $0,
          expectedGraphPath: path,
          expectedParameters: [:],
          expectedTokenString: nil,
          expectedVersion: version,
          expectedMethod: .post
        )
      }
  }

  func testGraphRequestPOSTWithNonEmptyParameters() {
    [
      GraphRequest(graphPath: path, parameters: parameters, httpMethod: .post),
      GraphRequest(graphPath: path, parameters: parameters, tokenString: nil, version: version, httpMethod: .post),
    ]
      .forEach {
        verifyRequest(
          $0,
          expectedGraphPath: path,
          expectedParameters: parameters,
          expectedTokenString: nil,
          expectedVersion: version,
          expectedMethod: .post
        )
      }
  }

  func testSerializeURL() throws {
    let baseURL = try InternalUtility.shared.facebookURL(
      withHostPrefix: prefix,
      path: path,
      queryParameters: [:],
      defaultVersion: version
    )
    let url = GraphRequest.serializeURL(
      baseURL.absoluteString,
      params: parameters,
      httpMethod: HTTPMethod.post.rawValue,
      forBatch: true
    )

    let expectedURL = "https://graph.facebook.com/\(version)/me?fields="
    XCTAssertEqual(url, expectedURL)

    let encodedURL = Utility.encode(urlString: expectedURL)

    XCTAssertEqual(encodedURL, "https%3A%2F%2Fgraph.facebook.com%2F\(version)%2Fme%3Ffields%3D")
    XCTAssertEqual(Utility.decode(urlString: encodedURL), expectedURL)
  }

  func testAttachments() {
    XCTAssertTrue(GraphRequest.isAttachment(UIImage()))
    XCTAssertTrue(GraphRequest.isAttachment(Data()))
    XCTAssertTrue(
      GraphRequest.isAttachment(
        GraphRequestDataAttachment(
          data: Data(),
          filename: "fakefile",
          contentType: "foo"
        )
      )
    )
    XCTAssertFalse(GraphRequest.isAttachment("string"))
    XCTAssertFalse(GraphRequest.isAttachment(Date()))
  }

  func testDebuggingHelpers() {
    let request = GraphRequest(graphPath: path, parameters: parameters, httpMethod: .post)
    let descriptionPart = "graphPath: me, HTTPMethod: POST, parameters: {\n    fields = \"\""
    XCTAssertTrue(
      request.description.contains(descriptionPart),
      "Requests should have useful information in their description"
    )
  }

  func testDebuggingMetadata() {
    let request = GraphRequest(graphPath: path)
    let metadata = GraphRequestMetadata(request: request, completionHandler: nil, batchParameters: [:])
    XCTAssertTrue(
      metadata.description.contains("request: "),
      "Request metadata should include information about the request"
    )
  }

  func testSetSettingsWithCertainVersion() {
    let testVersion = "v123"
    settings.graphAPIVersion = testVersion
    let request = GraphRequest(graphPath: path)
    XCTAssertEqual(request.version, testVersion)
  }

  func testSetSettingsWithGraphErrorRecoveryEnabled() {
    settings.isGraphErrorRecoveryEnabled = true
    let request = GraphRequest(graphPath: path)
    XCTAssertFalse(request.isGraphErrorRecoveryDisabled)
  }

  func testSetSettingsWithDebugParamValue() throws {
    let debugParameter = "TestValue"
    settings.graphAPIDebugParameterValue = debugParameter
    let baseURL = try InternalUtility.shared.facebookURL(
      withHostPrefix: prefix,
      path: path,
      queryParameters: [:],
      defaultVersion: version
    )
    let url = GraphRequest.serializeURL(
      baseURL.absoluteString,
      params: [:],
      httpMethod: HTTPMethod.post.rawValue,
      forBatch: true
    )
    let expectedURL = String(
      format: "https://graph.facebook.com/%@/me?debug=%@",
      version,
      debugParameter
    )

    XCTAssertEqual(url, expectedURL)
  }

  // MARK: - Custom test assertions

  // swiftlint:disable:next function_parameter_count
  func verifyRequest(
    _ request: GraphRequest,
    expectedGraphPath: String,
    expectedParameters: [String: String],
    expectedTokenString: String?,
    expectedVersion: String,
    expectedMethod: HTTPMethod,
    file: StaticString = #filePath,
    line: UInt = #line
  ) {
    XCTAssertEqual(request.graphPath, expectedGraphPath, file: file, line: line)
    XCTAssertEqual(request.parameters as? [String: String], expectedParameters, file: file, line: line)
    XCTAssertEqual(request.tokenString, expectedTokenString, file: file, line: line)
    XCTAssertEqual(request.version, expectedVersion, file: file, line: line)
    XCTAssertEqual(request.httpMethod, expectedMethod, file: file, line: line)
  }
}


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

  let expectedCacheMemory = 1024 * 1024 * 8
  let expectedCacheCapacity = 1024 * 1024 * 100
  let defaultTTL = 60.0
  let provider = TestSessionProvider()
  lazy var downloader = ImageDownloader(sessionProvider: provider)
  lazy var url = SampleURLs.valid(path: name)
  lazy var request = URLRequest(url: url)
  let (image, imageData): (UIImage, Data) = {
    UIGraphicsBeginImageContextWithOptions(CGSize(width: 36, height: 36), false, 1)
    let image = UIGraphicsGetImageFromCurrentImageContext()! // swiftlint:disable:this force_unwrapping
    UIGraphicsEndImageContext()
    let imageData = image.pngData()! // swiftlint:disable:this force_unwrapping
    return (image, imageData)
  }()

  override func setUp() {
    super.setUp()

    downloader.urlCache.removeAllCachedResponses()
  }

  func testDefaultSessionProvider() {
    XCTAssertEqual(
      ObjectIdentifier(ImageDownloader.sharedInstance.sessionProvider),
      ObjectIdentifier(URLSession.shared),
      "Should use the shared system session by default"
    )
  }

  func testCreatingWithSession() {
    XCTAssertEqual(
      ObjectIdentifier(downloader.sessionProvider),
      ObjectIdentifier(provider),
      "Should be able to create with a session provider"
    )
  }

  func testFetchingImageStartsTask() {
    let dataTask = TestSessionDataTask()
    provider.stubbedDataTask = dataTask
    downloader.downloadImage(
      with: url,
      ttl: defaultTTL
    ) { _ in }

    XCTAssertEqual(
      provider.dataTaskCallCount,
      1,
      "Should request a data task when fetching an image"
    )
    XCTAssertEqual(
      dataTask.resumeCallCount,
      1,
      "Should start the data task for fetching an image"
    )
  }

  // Data | Response | Error
  // nil  | nil      | nil
  func testCompletingTaskWithMissingDataResponseAndError() {
    completeFetchingImage(
      data: nil,
      response: nil,
      error: nil,
      message: "Should not complete with an image when the session task completes with no inputs"
    )
  }

  // Data | Response | Error
  // nil  | nil      | yes
  func testCompletingTaskWithError() {
    completeFetchingImage(
      data: nil,
      response: nil,
      error: SampleError(),
      message: "Should not complete with an image when the session task completes with an error"
    )
  }

  // Data | Response       | Error
  // nil  | yes (non-http) | nil
  func testCompletingTaskWithNonHTTPResponse() {
    completeFetchingImage(
      data: nil,
      response: SampleHTTPURLResponses.valid,
      error: nil,
      message: "Should not complete with an image when there is non-http response"
    )
  }

  // Data | Response   | Error
  // nil  | yes (http) | nil
  func testCompletingTaskWithInvalidStatusCode() {
    completeFetchingImage(
      data: nil,
      response: SampleHTTPURLResponses.invalidStatusCode,
      error: nil,
      message: "Should not complete with an image when there is an invalid url response status code"
    )
  }

  // Data | Response | Error
  // nil  | yes      | yes
  func testCompletingTaskWithResponseAndError() {
    completeFetchingImage(
      data: nil,
      response: SampleHTTPURLResponses.valid,
      error: SampleError(),
      message: "Should not complete with an image when there is an error"
    )
  }

  // Data           | Response | Error
  // yes (invalid)  | nil      | nil
  func testCompletingTaskWithInvalidData() {
    completeFetchingImage(
      data: SampleGraphResponses.empty.data,
      response: nil,
      error: nil,
      message: "Should not complete with an image when there is invalid data"
    )
  }

  // Data        | Response | Error
  // yes (valid) | nil      | yes
  func testCompletingTaskWithValidDataAndError() {
    completeFetchingImage(
      data: imageData,
      response: nil,
      error: SampleError(),
      message: "Should not complete with an image when there is an error"
    )
  }

  // Data        | Response      | Error
  // yes (valid) | yes (invalid) | nil
  func testCompletingTaskWithValidDataAndInvalidResponse() {
    completeFetchingImage(
      data: imageData,
      response: SampleHTTPURLResponses.invalidStatusCode,
      error: nil,
      message: "Should not complete with an image when there is an invalid response code"
    )
  }

  // Data          | Response    | Error
  // yes (invalid) | yes (valid) | nil
  func testCompletingTaskWithInvalidDataAndValidResponse() {
    completeFetchingImage(
      data: SampleGraphResponses.empty.data,
      response: SampleHTTPURLResponses.validStatusCode,
      error: nil,
      message: "Should not complete with an image when there is invalid image data"
    )
  }

  // Data        | Response    | Error
  // yes (valid) | yes (valid) | nil
  func testCompletingTaskWithValidDataAndValidResponse() throws {
    try XCTSkipIf(
      true,
      "This test is being skipped due to changes in Xcode 14.1 that produce new and different PNG data"
    )

    completeFetchingImage(
      data: imageData,
      response: SampleHTTPURLResponses.validStatusCode,
      error: nil,
      expectedImage: image,
      message: "Should not complete with an image when there is invalid image data"
    )
  }

  // MARK: - Caching

  func testDefaultCache() {
    XCTAssertEqual(
      downloader.urlCache.memoryCapacity,
      expectedCacheMemory,
      "Should use a well known value for the memory capacity of the image cache"
    )
    XCTAssertEqual(
      downloader.urlCache.diskCapacity,
      expectedCacheCapacity,
      "Should use a well known value for the disk capacity of the image cache"
    )
  }

  func testUncachedImagesAreFetched() {
    downloader.downloadImage(with: url, ttl: defaultTTL, completion: nil)

    XCTAssertEqual(
      provider.dataTaskCallCount,
      1,
      "Should attempt to fetch an uncached image"
    )
  }

  func testUnexpiredCachedImagesAreNotFetched() {
    seedURLCache()
    downloader.downloadImage(with: url, ttl: defaultTTL, completion: nil)

    XCTAssertEqual(
      provider.dataTaskCallCount,
      0,
      "Should not attempt to fetch a cached image"
    )
  }

  func testExpiredCachedImagedAreFetched() {
    seedURLCache(date: .distantPast)
    downloader.downloadImage(with: url, ttl: defaultTTL, completion: nil)

    XCTAssertEqual(
      provider.dataTaskCallCount,
      1,
      "Should attempt to fetch a cached image if it is expired"
    )
  }

  func testFetchingCachedImage() throws {
    try XCTSkipIf(
      true,
      "This test is being skipped due to changes in Xcode 14.1 that produce new and different PNG data"
    )

    seedURLCache()

    var completionInvoked = false
    downloader.downloadImage(with: url, ttl: defaultTTL) { potentialImage in
      guard let image = potentialImage else {
        return XCTFail("Should call the fetch completion with an image created from cached image data")
      }
      XCTAssertEqual(
        image.pngData(),
        self.imageData,
        "Should call the fetch completion with an image created from cached image data"
      )
      completionInvoked = true
    }

    provider.capturedCompletion?(nil, nil, nil)
    XCTAssertTrue(completionInvoked)
  }

  func testFetchingWithSuccessSavesToCache() {
    let response = SampleHTTPURLResponses.validStatusCode
    downloader.downloadImage(with: url, ttl: defaultTTL) { _ in }

    provider.capturedCompletion?(imageData, response, nil)

    let expectedCachedResponse = CachedURLResponse(response: response, data: imageData)
    guard let cachedResponse = downloader.urlCache.cachedResponse(for: request) else {
      return XCTFail("Should have a cached response")
    }

    XCTAssertEqual(
      cachedResponse.data,
      expectedCachedResponse.data,
      "Should cache a successful fetch"
    )
  }

  func testClearingCache() {
    seedURLCache()
    downloader.removeAll()

    XCTAssertNil(downloader.urlCache.cachedResponse(for: request))
  }

  // MARK: - Helpers

  func seedURLCache(date: Date = Date()) {
    downloader.urlCache.storeCachedResponse(
      CachedURLResponse(
        response: SampleHTTPURLResponses.validStatusCode,
        data: imageData,
        userInfo: ["timestamp": date],
        storagePolicy: URLCache.StoragePolicy.allowed
      ),
      for: request
    )
    XCTAssertNotNil(downloader.urlCache.cachedResponse(for: request))
  }

  func completeFetchingImage(
    data: Data?,
    response: URLResponse?,
    error: Error?,
    expectedImage: UIImage? = nil,
    message: String,
    file: StaticString = #file,
    line: UInt = #line
  ) {
    var completionInvoked = false
    var image: UIImage?
    downloader.downloadImage(
      with: SampleURLs.valid,
      ttl: 0
    ) { potentialImage in
      image = potentialImage
      completionInvoked = true
    }

    provider.capturedCompletion?(data, response, error)

    XCTAssertEqual(
      image?.pngData(),
      expectedImage?.pngData(),
      message,
      file: file,
      line: line
    )
    XCTAssertTrue(
      completionInvoked,
      file: file,
      line: line
    )
  }
}


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

// MARK: Creation

final class LocationTests: XCTestCase {

  func testCreate() throws {
    let dict = ["id": "110843418940484", "name": "Seattle, Washington"]
    let location = try XCTUnwrap(Location(from: dict), "Should be able to create Location")
    XCTAssertEqual(location.id, dict["id"])
    XCTAssertEqual(location.name, dict["name"])
  }

  func testCreateWithIDOnly() {
    let dict = ["id": "110843418940484"]
    let location = Location(from: dict)

    XCTAssertNil(
      location,
      "Should not be able to create Location with no name specified"
    )
  }

  func testCreateWithNameOnly() {
    let dict = ["name": "Seattle, Washington"]
    let location = Location(from: dict)

    XCTAssertNil(
      location,
      "Should not be able to create Location with no id specified"
    )
  }

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

  func testEncodingAndDecoding() throws {
    let dict = ["id": "110843418940484", "name": "Seattle, Washington"]
    let location = try XCTUnwrap(Location(from: dict))

    let decodedObject = try CodabilityTesting.encodeAndDecode(location)

    // Test Objects
    XCTAssertNotIdentical(location, decodedObject, .isCodable)
    XCTAssertEqual(location, decodedObject, .isCodable)

    // Test Properties
    XCTAssertEqual(location.id, decodedObject.id, .isCodable)
    XCTAssertEqual(location.name, decodedObject.name, .isCodable)
  }
}

// swiftformat:disable extensionaccesscontrol

// MARK: - Assumptions

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


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

import FBSDKCoreKit
import XCTest

final class LoggingBehaviorTests: XCTestCase {
  func testBehaviors() {
    XCTAssertEqual(
      LoggingBehavior.accessTokens.rawValue,
      "include_access_tokens",
      .rawValue
    )
    XCTAssertEqual(
      LoggingBehavior.performanceCharacteristics.rawValue,
      "perf_characteristics",
      .rawValue
    )
    XCTAssertEqual(
      LoggingBehavior.appEvents.rawValue,
      "app_events",
      .rawValue
    )
    XCTAssertEqual(
      LoggingBehavior.informational.rawValue,
      "informational",
      .rawValue
    )
    XCTAssertEqual(
      LoggingBehavior.cacheErrors.rawValue,
      "cache_errors",
      .rawValue
    )
    XCTAssertEqual(
      LoggingBehavior.uiControlErrors.rawValue,
      "ui_control_errors",
      .rawValue
    )
    XCTAssertEqual(
      LoggingBehavior.graphAPIDebugWarning.rawValue,
      "graph_api_debug_warning",
      .rawValue
    )
    XCTAssertEqual(
      LoggingBehavior.graphAPIDebugInfo.rawValue,
      "graph_api_debug_info",
      .rawValue
    )
    XCTAssertEqual(
      LoggingBehavior.networkRequests.rawValue,
      "network_requests",
      .rawValue
    )
    XCTAssertEqual(
      LoggingBehavior.developerErrors.rawValue,
      "developer_errors",
      .rawValue
    )
  }
}

// MARK: - Assumptions

// swiftformat:disable:next extensionAccessControl
fileprivate extension String {
  static let rawValue = "A logging behavior has a well-known raw 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 XCTest

final class MathUtilityTests: XCTestCase {
  func testCeilForSize() {
    let fixtures: [(size: CGSize, expectedSize: CGSize)] = [
      (CGSize.zero, .zero),
      (CGSize(width: 10.1, height: 10.1), CGSize(width: 11, height: 11)),
      (CGSize(width: 10.5, height: 10.5), CGSize(width: 11, height: 11)),
      (CGSize(width: 10.7, height: 10.7), CGSize(width: 11, height: 11)),
    ]

    fixtures.forEach { fixture in
      XCTAssertEqual(
        FBSDKMath.ceil(for: fixture.size),
        fixture.expectedSize,
        "Should provide the correct ceiling size for \(fixture.size)"
      )
    }
  }

  func testFloorForSize() {
    let fixtures: [(size: CGSize, expectedSize: CGSize)] = [
      (CGSize.zero, .zero),
      (CGSize(width: 10.1, height: 10.1), CGSize(width: 10, height: 10)),
      (CGSize(width: 10.5, height: 10.5), CGSize(width: 10, height: 10)),
      (CGSize(width: 10.7, height: 10.7), CGSize(width: 10, height: 10)),
    ]

    fixtures.forEach { fixture in
      XCTAssertEqual(
        FBSDKMath.floor(for: fixture.size),
        fixture.expectedSize,
        "Should provide the correct floor size for \(fixture.size)"
      )
    }
  }

  func testHashWithInteger() {
    XCTAssertEqual(
      14624536017465086961,
      FBSDKMath.hash(with: 5),
      "Hashing an integer should return a predictable value"
    )
  }

  func testHashWithIntegerArray() {
    var array: [UInt] = [1, 2, 3]
    let hash = FBSDKMath.hash(withIntegerArray: &array, count: 10)
    let hash2 = FBSDKMath.hash(withIntegerArray: &array, count: 10)

    XCTAssertEqual(
      hash,
      hash2,
      "Hashing functions should return predicable values"
    )
  }

  func testHashingIntegerArrayWithEmptyCount() {
    var array: [UInt] = [1, 2, 3]
    XCTAssertEqual(FBSDKMath.hash(withIntegerArray: &array, count: 0), 0)
  }
}


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

  // MARK: - Test Assumptions

  private enum Assumptions {
    static let isNonNetworkErrorByCode = """
      An error without a network error code and no underlying network error is \
      not a network error
      """

    static let isNonNetworkErrorByUnderlyingError = """
      An error without a network error code is only a network error if it \
      contains an underlying network error
      """

    static func isNetworkErrorByCode(_ code: URLError.Code) -> String {
      "An error with a network error code \(code) is a network error"
    }

    static let isNetworkErrorByUnderlyingNetworkError = """
      An error with an underlying error that is a network error is itself a \
      network error
      """
  }

  // MARK: - Error Codes

  static let nonNetworkErrorCode = URLError.Code.cancelled
  static let networkErrorCodes: [URLError.Code] = [
    .timedOut,
    .cannotFindHost,
    .cannotConnectToHost,
    .networkConnectionLost,
    .dnsLookupFailed,
    .notConnectedToInternet,
    .internationalRoamingOff,
    .callIsActive,
    .dataNotAllowed,
  ]

  // MARK: - Test Fixture

  let checker: NetworkErrorChecking = NetworkErrorChecker()

  private func makeError(
    networkCode: URLError.Code,
    underlyingError: Error? = nil
  ) -> NSError {
    var userInfo: [String: Any]?
    if let underlyingError = underlyingError {
      userInfo = [NSUnderlyingErrorKey: underlyingError]
    }

    return NSError(
      domain: "",
      code: networkCode.rawValue,
      userInfo: userInfo
    )
  }

  // MARK: - Tests

  func testNonNetworkErrorWithoutUnderlyingError() {
    let error = makeError(networkCode: Self.nonNetworkErrorCode)
    XCTAssertFalse(
      checker.isNetworkError(error),
      Assumptions.isNonNetworkErrorByCode
    )
  }

  func testNonNetworkErrorWithUnderlyingError() {
    let underlyingError = makeError(networkCode: Self.nonNetworkErrorCode)
    let error = makeError(
      networkCode: Self.nonNetworkErrorCode,
      underlyingError: underlyingError
    )

    XCTAssertFalse(
      checker.isNetworkError(error),
      Assumptions.isNonNetworkErrorByUnderlyingError
    )
  }

  func testNetworkErrorWithoutUnderlyingError() {
    Self.networkErrorCodes
      .forEach { code in
        let error = makeError(networkCode: code)
        XCTAssertTrue(
          checker.isNetworkError(error),
          Assumptions.isNetworkErrorByCode(code)
        )
      }
  }

  func testNetworkErrorWithUnderlyingError() throws {
    let code = try XCTUnwrap(Self.networkErrorCodes.randomElement())
    let underlyingError = makeError(networkCode: code)
    let error = makeError(
      networkCode: Self.nonNetworkErrorCode,
      underlyingError: underlyingError
    )

    XCTAssertTrue(
      checker.isNetworkError(error),
      Assumptions.isNetworkErrorByUnderlyingNetworkError
    )
  }
}


/*
 * 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 PermissionTests: XCTestCase {
  struct PermissionMapping {
    let permission: Permission
    let name: String
  }

  let mappings = [
    PermissionMapping(permission: .publicProfile, name: "public_profile"),
    PermissionMapping(permission: .userFriends, name: "user_friends"),
    PermissionMapping(permission: .email, name: "email"),
    PermissionMapping(permission: .userAboutMe, name: "user_about_me"),
    PermissionMapping(permission: .userActionsBooks, name: "user_actions.books"),
    PermissionMapping(permission: .userActionsFitness, name: "user_action.fitness"),
    PermissionMapping(permission: .userActionsMusic, name: "user_actions.music"),
    PermissionMapping(permission: .userActionsNews, name: "user_actions.news"),
    PermissionMapping(permission: .userActionsVideo, name: "user_actions.video"),
    PermissionMapping(permission: .userBirthday, name: "user_birthday"),
    PermissionMapping(permission: .userEducationHistory, name: "user_education_history"),
    PermissionMapping(permission: .userEvents, name: "user_events"),
    PermissionMapping(permission: .userGamesActivity, name: "user_games_activity"),
    PermissionMapping(permission: .userGender, name: "user_gender"),
    PermissionMapping(permission: .userHometown, name: "user_hometown"),
    PermissionMapping(permission: .userLikes, name: "user_likes"),
    PermissionMapping(permission: .userLocation, name: "user_location"),
    PermissionMapping(permission: .userManagedGroups, name: "user_managed_groups"),
    PermissionMapping(permission: .userPhotos, name: "user_photos"),
    PermissionMapping(permission: .userPosts, name: "user_posts"),
    PermissionMapping(permission: .userRelationships, name: "user_relationships"),
    PermissionMapping(permission: .userRelationshipDetails, name: "user_relationship_details"),
    PermissionMapping(permission: .userReligionPolitics, name: "user_religion_politics"),
    PermissionMapping(permission: .userTaggedPlaces, name: "user_tagged_places"),
    PermissionMapping(permission: .userVideos, name: "user_videos"),
    PermissionMapping(permission: .userWebsite, name: "user_website"),
    PermissionMapping(permission: .userWorkHistory, name: "user_work_history"),
    PermissionMapping(permission: .readCustomFriendlists, name: "read_custom_friendlists"),
    PermissionMapping(permission: .readInsights, name: "read_insights"),
    PermissionMapping(permission: .readAudienceNetworkInsights, name: "read_audience_network_insights"),
    PermissionMapping(permission: .readPageMailboxes, name: "read_page_mailboxes"),
    PermissionMapping(permission: .pagesShowList, name: "pages_show_list"),
    PermissionMapping(permission: .pagesManageCta, name: "pages_manage_cta"),
    PermissionMapping(permission: .pagesManageInstantArticles, name: "pages_manage_instant_articles"),
    PermissionMapping(permission: .adsRead, name: "ads_read"),
    PermissionMapping(permission: .userLink, name: "user_link"),
    PermissionMapping(permission: .userAgeRange, name: "user_age_range"),
    PermissionMapping(permission: .custom("test_permission"), name: "test_permission"),
  ]

  func testCases() {
    for mapping in mappings {
      switch mapping.permission {
      // If the compiler tells that this switch must be exhaustive, then a new
      // permission has been added.  You must add a new test mapping above
      // if you are adding a new case here.
      case
        .publicProfile,
        .userFriends,
        .email,
        .userAboutMe,
        .userActionsBooks,
        .userActionsFitness,
        .userActionsMusic,
        .userActionsNews,
        .userActionsVideo,
        .userBirthday,
        .userEducationHistory,
        .userEvents,
        .userGamesActivity,
        .userGender,
        .userHometown,
        .userLikes,
        .userLocation,
        .userManagedGroups,
        .userPhotos,
        .userPosts,
        .userRelationships,
        .userRelationshipDetails,
        .userReligionPolitics,
        .userTaggedPlaces,
        .userVideos,
        .userWebsite,
        .userWorkHistory,
        .readCustomFriendlists,
        .readInsights,
        .readAudienceNetworkInsights,
        .readPageMailboxes,
        .pagesShowList,
        .pagesManageCta,
        .pagesManageInstantArticles,
        .adsRead,
        .userLink,
        .userAgeRange,
        .custom:
        break
      @unknown default:
        break
      }
    }
  }

  func testCreatingFromStringLiteral() {
    for mapping in mappings {
      XCTAssertEqual(
        Permission(stringLiteral: mapping.name),
        mapping.permission,
        "Should be able to create the \(mapping.permission) permission with the string literal \"\(mapping.name)\""
      )
    }

    XCTAssertEqual(
      Permission(stringLiteral: ""),
      .custom(""),
      "Should be able to create the custom permission with an empty string"
    )
  }

  func testNames() {
    for mapping in mappings {
      XCTAssertEqual(
        mapping.permission.name,
        mapping.name,
        "The name of the \(mapping.permission) permission should be \"\(mapping.name)\""
      )
    }

    XCTAssertEqual(
      Permission.custom("").name,
      "",
      "The name of the custom permission created with an empty string should be an empty string"
    )
  }
}


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

@testable import FBSDKCoreKit

import XCTest

final class ProfilePictureViewTests: XCTestCase {
  // swiftlint:disable implicitly_unwrapped_optional
  var profilePictureView: FBProfilePictureView!
  var testProfile: Profile! = SampleUserProfiles.createValid()
  // swiftlint:enable implicitly_unwrapped_optional

  override func setUp() {
    super.setUp()

    AccessToken.setCurrent(nil, shouldDispatchNotif: false)
    Profile.current = nil
    testProfile = SampleUserProfiles.createValid()
    profilePictureView = FBProfilePictureView(frame: .zero)
  }

  override func tearDown() {
    profilePictureView = nil
    testProfile = nil

    super.tearDown()
  }

  // MARK: - Initialization

  func testInitializationWithoutProfileWithFrame() {
    let profilePictureFrame = CGRect(x: 0, y: 0, width: 100, height: 100)
    profilePictureView = FBProfilePictureView(frame: profilePictureFrame)

    XCTAssertEqual(
      profilePictureView.backgroundColor,
      .white,
      .setsBackgroundColor
    )
    XCTAssertEqual(
      profilePictureView.contentMode,
      .scaleAspectFit,
      .setsContentMode
    )
    XCTAssertEqual(
      profilePictureView.frame,
      profilePictureFrame,
      .setsCustomFrame
    )
    XCTAssertEqual(
      profilePictureView.profileID,
      "me",
      .setsProfileID
    )
    XCTAssertTrue(profilePictureView.subviews.first is UIImageView, .addsImageView)
    XCTAssertFalse(profilePictureView.isUserInteractionEnabled, .disablesUserInteraction)
  }

  func testInitializationWithProfileWithoutFrame() {
    profilePictureView = FBProfilePictureView(profile: testProfile)
    initialConfigurationSetupWithProfile()
    XCTAssertEqual(
      profilePictureView.frame,
      .zero,
      .setsDefaultFrame
    )
    XCTAssertEqual(
      profilePictureView.profileID,
      "123",
      .setsCustomProfile
    )
  }

  func testInitializationWithProfileWithFrame() {
    let profilePictureFrame = CGRect(x: 0, y: 0, width: 100, height: 100)
    profilePictureView = FBProfilePictureView(frame: profilePictureFrame, profile: testProfile)
    initialConfigurationSetupWithProfile()
    XCTAssertEqual(
      profilePictureView.frame,
      profilePictureFrame,
      .setsCustomFrame
    )
    XCTAssertEqual(
      profilePictureView.profileID,
      "123",
      .setsCustomProfile
    )
  }

  // MARK: - Image Update

  func testPlaceholderImage() throws {
    let profilePictureFrame = CGRect(x: 0, y: 0, width: 100, height: 100)
    profilePictureView = FBProfilePictureView(frame: profilePictureFrame)
    profilePictureView.setPlaceholderImage()

    let expecation = expectation(description: "placeholder image expectation")
    expecation.isInverted = true
    waitForExpectations(timeout: 2)

    XCTAssertNotNil(profilePictureView.imageView.image, .setsPlaceholderImage)
  }

  func testUpdateImageWithDataAndSameState() throws {
    try XCTSkipIf(
      true,
      "This test is being skipped due to constant failure in CI (but not locally) -- see T132421535"
    )

    let state = ProfilePictureViewState(
      profileID: "123",
      size: .zero,
      scale: 0,
      pictureMode: .normal,
      imageShouldFit: false
    )
    profilePictureView.lastState = state
    profilePictureView.imageView.image = nil

    let updateImagePredicate = NSPredicate { _, _ in
      self.profilePictureView.imageView.image != nil
    }

    _ = expectation(for: updateImagePredicate, evaluatedWith: nil)
    DispatchQueue.global(qos: .userInitiated).async {
      self.profilePictureView.updateImage(data: Data.redIconImage, state: state)
    }

    waitForExpectations(timeout: 4)
    XCTAssertNotNil(profilePictureView.imageView.image, .updatesImageIfSameState)
  }

  func testUpdateImageWithDataAndDifferentState() throws {
    profilePictureView = FBProfilePictureView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))

    let oldState = ProfilePictureViewState(
      profileID: "123",
      size: .zero,
      scale: 0,
      pictureMode: .normal,
      imageShouldFit: false
    )
    profilePictureView.lastState = oldState

    let newState = ProfilePictureViewState(
      profileID: "test12345",
      size: .zero,
      scale: 0,
      pictureMode: .normal,
      imageShouldFit: true
    )
    profilePictureView.updateImage(data: Data.redIconImage, state: newState)

    XCTAssertNil(
      profilePictureView.imageView.image,
      .doesNotUpdateImageIfDifferentState
    )
  }

  func testSetNeedsImageUpdateWithNoViewBounds() {
    XCTAssertNil(profilePictureView.imageView.image, .doesNotAddImage)
  }

  func testSetNeedsImageUpdateWithNoProfileImage() throws {
    try XCTSkipIf(
      true,
      "This test is being skipped due to constant failure in CI (but not locally) -- see T132421535"
    )

    let profilePictureFrame = CGRect(x: 0, y: 0, width: 100, height: 100)
    profilePictureView = FBProfilePictureView(frame: profilePictureFrame)

    let updateImagePredicate = NSPredicate { _, _ in
      self.profilePictureView.imageView.image != nil
    }

    _ = expectation(for: updateImagePredicate, evaluatedWith: nil)
    DispatchQueue.global(qos: .userInitiated).async {
      self.profilePictureView.setNeedsImageUpdate()
    }

    waitForExpectations(timeout: 2)
    XCTAssertNotNil(profilePictureView.imageView.image, .setsPlaceholderImage)
  }

  // MARK: - Notifications

  func testAccessTokenDidChangeNotificationWithUserInfoKeyAndMeProfileID() {
    let notification = Notification(
      name: .AccessTokenDidChange,
      object: nil,
      userInfo: [AccessTokenDidChangeUserIDKey: true]
    )
    Profile.current = testProfile
    profilePictureView.updateImageWithProfile()
    XCTAssertNotNil(profilePictureView.lastState, .hasLastState)
    profilePictureView.accessTokenDidChange(notification)
    XCTAssertNil(profilePictureView.lastState, .hasResetLastStateOnAccessTokenChange)
  }

  func testAccessTokenDidChangeNotificationWithoutUserInfoKeyAndMeProfileID() {
    let notification = Notification(
      name: .AccessTokenDidChange,
      object: nil,
      userInfo: nil
    )
    let state = ProfilePictureViewState(
      profileID: "123",
      size: .zero,
      scale: 0,
      pictureMode: .normal,
      imageShouldFit: false
    )
    profilePictureView.lastState = state
    profilePictureView.accessTokenDidChange(notification)
    XCTAssertNotNil(profilePictureView.lastState, .hasLastState)
  }

  func testAccessTokenDidChangeNotificationWithUserInfoKeyAndWithoutMeProfileID() {
    let notification = Notification(
      name: .AccessTokenDidChange,
      object: nil,
      userInfo: [AccessTokenDidChangeUserIDKey: true]
    )

    let state = ProfilePictureViewState(
      profileID: "123",
      size: .zero,
      scale: 0,
      pictureMode: .normal,
      imageShouldFit: false
    )
    profilePictureView.lastState = state

    profilePictureView.profileID = "123"
    profilePictureView.accessTokenDidChange(notification)
    XCTAssertNotNil(profilePictureView.lastState, .hasLastState)
  }

  func testAccessTokenDidChangeNotificationWithFalseUserInfoKeyAndWithoutMeProfileID() {
    let notification = Notification(
      name: .AccessTokenDidChange,
      object: nil,
      userInfo: [AccessTokenDidChangeUserIDKey: false]
    )

    let state = ProfilePictureViewState(
      profileID: "123",
      size: .zero,
      scale: 0,
      pictureMode: .normal,
      imageShouldFit: false
    )
    profilePictureView.lastState = state

    profilePictureView.profileID = "123"
    profilePictureView.accessTokenDidChange(notification)
    XCTAssertNotNil(profilePictureView.lastState, .hasLastState)
  }

  func testAccessTokenDidChangeNotificationWithoutUserInfoKeyAndWithMeProfileID() {
    let notification = Notification(
      name: .AccessTokenDidChange,
      object: nil,
      userInfo: [AccessTokenDidChangeUserIDKey: false]
    )

    let state = ProfilePictureViewState(
      profileID: "123",
      size: .zero,
      scale: 0,
      pictureMode: .normal,
      imageShouldFit: false
    )
    profilePictureView.lastState = state

    profilePictureView.accessTokenDidChange(notification)
    XCTAssertNil(profilePictureView.lastState, .hasResetLastStateOnAccessTokenChange)
  }

  func testProfileDidChangeNotificationWithMeProfileIDAndMeStateProfileID() {
    let notification = Notification(
      name: .ProfileDidChange,
      object: nil,
      userInfo: nil
    )
    Profile.current = testProfile
    profilePictureView.profileDidChange(notification)
    XCTAssertNotNil(profilePictureView.lastState, .hasLastState)
  }

  func testProfileDidChangeNotificationWithoutMeProfileID() {
    let notification = Notification(
      name: .ProfileDidChange,
      object: nil,
      userInfo: nil
    )
    let state = ProfilePictureViewState(
      profileID: "123",
      size: .zero,
      scale: 0,
      pictureMode: .normal,
      imageShouldFit: false
    )
    profilePictureView.lastState = state
    profilePictureView.profileID = "123"
    profilePictureView.profileDidChange(notification)
    XCTAssertNotNil(profilePictureView.lastState, .hasLastState)
  }

  // MARK: - Helpers

  func initialConfigurationSetupWithProfile() {
    XCTAssertNotEqual(
      profilePictureView.backgroundColor,
      .white,
      .setsBackgroundColor
    )
    XCTAssertNotEqual(
      profilePictureView.contentMode,
      .scaleAspectFit,
      .setsContentMode
    )
    XCTAssertTrue(
      profilePictureView.subviews.isEmpty,
      .doesNotAddImageView
    )
    XCTAssertTrue(profilePictureView.isUserInteractionEnabled, .enablesUserInteraction)
  }
}

// swiftformat:disable extensionaccesscontrol

// MARK: - Assumptions

fileprivate extension String {
  static let setsCustomProfile = "We are able to set a custom profile during initialization"
  static let hasProfileImage = "A profile picture view has a profile image"
  static let setsBackgroundColor = "Profile picture view background color is set to a color during initialization"
  static let setsContentMode = "Profile picture view content mode is set to aspect fit during initialization"
  static let setsCustomFrame = "Profile picture view can be passed a custom frame during initialization"
  static let setsProfileID = "Profile picture view sets a default profile ID during initialization"
  static let addsImageView = "Profile picture view adds an image view to its view hierarchy during initialization"
  static let disablesUserInteraction = """
      Profile picture view user interaction is disabled when initiating without a profile
    """
  static let setsDefaultFrame = """
      If no custom frame is passed during initialization default profile picture view frame should be zero
    """
  static let setsPlaceholderImage = "Profile picture view can set a place holder image when a there is no user image"
  static let hasLastState = "Profile picture view has a last state available"
  static let updatesImageIfSameState = "Profile picture view image is updated when state has not changed"
  static let doesNotUpdateImageIfDifferentState = """
      Profile picture view image image should not be updated when having different state than the last one
    """
  static let hasResetLastStateOnAccessTokenChange = """
       State should reset when we receive an access token notification, we have a profile id equals "me", \
       and we have a user id value.
    """
  static let doesNotAddImageView = """
       No image view is added to the profile picture view hierarchy when initiating with a profile
    """
  static let enablesUserInteraction = """
       Profile picture view user interaction is enabled when initiating with a profile
    """
  static let doesNotAddImage = """
       No image is added to the profile picture view when no image view is available or \
       when profile picture view bounds are zero
    """
}

// MARK: - Test Values

fileprivate extension UIImage {
  static let redIcon = UIImage(
    named: "redSilhouette.png",
    in: Bundle(for: ProfilePictureViewTests.self),
    compatibleWith: nil
  )
}

fileprivate extension Data {
  static let redIconImage = UIImage.redIcon!.pngData()! // swiftlint:disable:this force_unwrapping
}