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

import FBSDKCoreKit_Basics
import XCTest

final class DataTests: XCTestCase {
  func testReadingContentsOfFile() throws {
    let path = try XCTUnwrap(String.sampleFilePath)
    let actual = try NSData.fb_data(withContentsOfFile: path)
    let expected = try XCTUnwrap(NSData(contentsOfFile: path)) as Data

    XCTAssertEqual(actual, expected, .readContentsOfFile)
  }
}

// swiftformat:disable extensionaccesscontrol

// MARK: - Assumptions

fileprivate extension String {
  static let readContentsOfFile = "Data can be read from the contents of a file through an internal abstraction"
}

// MARK: - Test Values

fileprivate extension String {
  static let sampleFilePath = Bundle(for: DataTests.self).path(forResource: "Sample", ofType: "txt")
}


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

import FBSDKCoreKit_Basics
import TestTools
import XCTest

final class FBSDKURLSessionTests: XCTestCase {
  final class TestURLSessionDataDelegate: NSObject, URLSessionDataDelegate {}

  let delegate = TestURLSessionDataDelegate()
  // swiftlint:disable:next implicitly_unwrapped_optional
  var session: FBSDKURLSession!

  override func setUp() {
    super.setUp()

    session = FBSDKURLSession(
      delegate: delegate,
      delegateQueue: OperationQueue()
    )
  }

  override func tearDown() {
    session = nil

    super.tearDown()
  }

  func testDefaultSession() {
    XCTAssertNil(session.session, "Should not have an underlying session by default")
  }

  func testExecutingURLRequestWithoutSession() {
    session.execute(SampleURLRequest.valid) { _, _, _ in }

    XCTAssertNotNil(session.session, "Should create a session when needed")
    XCTAssertIdentical(
      session.delegate,
      delegate,
      "Should create the session using the provided delegate"
    )
  }

  func testExecutingURLRequestWithSession() {
    session.execute(SampleURLRequest.valid) { _, _, _ in }

    let createdSession = session.session

    session.execute(SampleURLRequest.valid) { _, _, _ in }

    XCTAssertIdentical(
      createdSession,
      session.session,
      "Should not create a new session when one exists"
    )
  }

  func testInvalidatingSession() {
    session.execute(SampleURLRequest.valid) { _, _, _ in }
    XCTAssertNotNil(session.session)

    session.invalidateAndCancel()

    XCTAssertNil(
      session.session,
      "Invalidating should clear the underlying session"
    )
  }
}


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

import FBSDKCoreKit_Basics
import XCTest

@available(iOS 13, *)
final class FileManagerTests: XCTestCase {
  override func setUp() async throws {
    try await super.setUp()

    try FileManager.default.createDirectory(at: testDirectory, withIntermediateDirectories: true)
  }

  override func tearDown() async throws {
    try FileManager.default.removeItem(at: testDirectory)

    try await super.tearDown()
  }

  func testCreatingDirectory() throws {
    try FileManager.default.fb_createDirectory(
      atPath: sampleDirectory.path,
      withIntermediateDirectories: true,
      attributes: nil
    )

    var isDirectory: ObjCBool = false
    XCTAssertTrue(
      FileManager.default.fileExists(atPath: sampleDirectory.path, isDirectory: &isDirectory),
      .createDirectory
    )
    XCTAssertTrue(isDirectory.boolValue, .createDirectory)
  }

  func testCheckingFileExistence() throws {
    try Data.sample.write(to: sampleFile)

    XCTAssertTrue(
      FileManager.default.fb_fileExists(atPath: sampleFile.path),
      .checkFileExistence
    )
  }

  func testRemovingItem() throws {
    try Data.sample.write(to: sampleFile)

    try FileManager.default.fb_removeItem(atPath: sampleFile.path)

    XCTAssertFalse(
      FileManager.default.fileExists(atPath: sampleFile.path),
      .removeItem
    )
  }

  func testReadingDirectoryContents() throws {
    try Data.sample.write(to: sampleFile)
    let expected = try FileManager.default.contentsOfDirectory(atPath: testDirectory.path)
    let actual = try FileManager.default.fb_contentsOfDirectory(atPath: testDirectory.path)

    XCTAssertEqual(actual, expected, .readDirectoryContents)
  }

  // MARK: - Helpers

  private let testDirectoryName = UUID().uuidString

  private var testDirectory: URL {
    FileManager.default
      .temporaryDirectory
      .appendingPathComponent(testDirectoryName, isDirectory: true)
  }

  private var sampleFile: URL {
    testDirectory.appendingPathComponent(.sampleFilename, isDirectory: false)
  }

  private var sampleDirectory: URL {
    testDirectory.appendingPathComponent(.sampleDirectoryName, isDirectory: true)
  }
}

// swiftformat:disable extensionaccesscontrol

// MARK: - Assumptions

fileprivate extension String {
  static let createDirectory = "A directory can be created through an internal abstraction"
  static let checkFileExistence = "A file's existence can be checked through an internal abstraction"
  static let removeItem = "An item can be removed through an internal abstraction"
  static let readDirectoryContents = "A directory's contents can be read through an internal abstraction"
}

// MARK: - Test Values

fileprivate extension String {
  static let sampleFilename = "Sample.txt"
  static let sampleDirectoryName = "sample1/sample2"
}

fileprivate extension Data {
  static let sample = "sample text".data(using: .utf8)! // swiftlint:disable:this force_unwrapping
}


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

import XCTest

final class LibAnalyzerTests: XCTestCase {

  func testGetMethodsTableFromPrefixesAndFrameworks() {
    let prefixes = ["FBSDK", "_FBSDK"]
    let frameworks = ["FBSDKCoreKit", "FBSDKLoginKit", "FBSDKShareKit"]
    let result = LibAnalyzer.getMethodsTable(prefixes, frameworks: frameworks)
    XCTAssertFalse(result.isEmpty, "Should find at least one method declared in the provided frameworks")
  }

  func testSymbolicateCallstack() {
    let callstack = ["0x0000000109cbd02e", "0x0000000100cbd02e", "0x0000000111cbd04e"]
    let methodMapping = [
      "0x0109cbd02e": "-[FBSDKWebViewAppLinkResolver appLinkFromALData:destination:]+3110632",
      "0x0110cbd02e": "-[NSNib _instantiateWithOwner:options:topLevelObjects:] + 136",
      "0x0111cbd02e": "-[NSStoryboard instantiateControllerWithIdentifier:] + 236",
    ]
    var result = LibAnalyzer.symbolicateCallstack(callstack, methodMapping: methodMapping)

    XCTAssertNotNil(result, "Should return a value if both paramaters were passed")
    XCTAssertEqual(
      result,
      ["-[FBSDKWebViewAppLinkResolver appLinkFromALData:destination:]+3110632+0", "(2 DEV METHODS)"]
    )

    result = LibAnalyzer.symbolicateCallstack([], methodMapping: [:])
    XCTAssertNil(result, "method should return nil if either one or both parameters are empty")
  }
}


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

import FBSDKCoreKit_Basics
import Foundation
import XCTest

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

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

  override func setUp() {
    super.setUp()

    notificationCenter = NotificationCenter.default
    notificationObject = NSObject()
  }

  override func tearDown() {
    removeObserver()

    notificationCenter = nil
    notificationObject = nil
    observer = nil

    super.tearDown()
  }

  func testAddingObserver() throws {
    notificationCenter.fb_addObserver(
      self,
      selector: #selector(handleNotification(_:)),
      name: .sample,
      object: notificationObject
    )

    postNotification()
    try validateNotification(message: .addObserver)
  }

  func testAddingObserverWithTrailingClosure() throws {
    notificationCenter.fb_addObserver(forName: .sample, object: notificationObject, queue: nil) { notification in
      self.observedNotification = notification
    }

    postNotification()
    try validateNotification(message: .addObserver)
  }

  func testRemovingObserver() {
    addObserver()

    notificationCenter.fb_removeObserver(observer!) // swiftlint:disable:this force_unwrapping

    postNotification()
    XCTAssertNil(observedNotification, .removeObserver)
  }

  // MARK: - Helpers

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

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

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

    notificationCenter.removeObserver(observer)
  }

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

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

// swiftformat:disable extensionaccesscontrol

// MARK: - Assumptions

fileprivate extension String {
  static let addObserver = "A notification center can add a notification observer through an internal abstraction"
  static let removeObserver = "A notification center can remove a notification observer through an internal abstraction"
}

// MARK: - Test Values

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


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

import XCTest

final class URLSessionDataTaskTests: XCTestCase {

  // swiftlint:disable:next implicitly_unwrapped_optional
  var task: URLSessionDataTask!

  override func setUp() {
    super.setUp()
    task = URLSession.shared.dataTask(with: .sample)
  }

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

  func testState() {
    XCTAssertEqual(task.fb_state, .suspended, .hasState)
  }

  func testResume() {
    task.fb_resume()
    XCTAssertEqual(task.state, .running, .resume)
  }

  func testCancel() {
    task.fb_cancel()
    XCTAssertEqual(task.state, .canceling, .cancel)
  }
}

// swiftformat:disable extensionaccesscontrol

// MARK: - Assumptions

fileprivate extension String {
  static let hasState = "A data task has a state property through an internal abstraction"
  static let resume = "A data task can be resumed through an internal abstraction"
  static let cancel = "A data task can be cancelled through an internal abstraction"
}

// MARK: - Test Values

fileprivate extension URL {
  // swiftlint:disable:next force_unwrapping
  static let sample = URL(string: "https://facebook.com")!
}


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

import TestTools
import XCTest

final class UrlSessionTaskTests: XCTestCase {

  let dataTask = TestSessionDataTask()
  let provider = TestSessionProvider()
  lazy var task = UrlSessionTask(
    request: SampleURLRequest.valid,
    fromSession: provider,
    completionHandler: nil
  )! // swiftlint:disable:this force_unwrapping

  override func setUp() {
    super.setUp()

    provider.stubbedDataTask = dataTask
  }

  func testStarting() {
    task.start()

    XCTAssertEqual(
      dataTask.resumeCallCount,
      1,
      "Starting a session task should resume the underlying data task"
    )
  }

  func testStopping() {
    task.cancel()

    XCTAssertEqual(
      dataTask.cancelCallCount,
      1,
      "Cancelling a session task should cancel the underlying data task"
    )
  }

  func testState() {
    dataTask.stubbedState = .running
    XCTAssertEqual(
      task.state,
      .running,
      "Should return the state of the underlying data task"
    )
  }
}


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

import FBSDKCoreKit_Basics
import XCTest

final class URLSessionTests: XCTestCase {

  // swiftlint:disable:next implicitly_unwrapped_optional
  var session: URLSession!

  override func setUp() {
    super.setUp()
    session = URLSession.shared
  }

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

  func testCreatingDataTask() throws {
    let task = try XCTUnwrap(session.fb_dataTask(with: .sample) { _, _, _ in } as? URLSessionTask)
    XCTAssertEqual(task.state, .suspended, .createsDataTasks)
  }
}

// swiftformat:disable extensionaccesscontrol

// MARK: - Assumptions

fileprivate extension String {
  static let createsDataTasks = "A URL session creates a data task through an internal abstraction"
}

// MARK: - Test Values

fileprivate extension URLRequest {
  // swiftlint:disable:next force_unwrapping
  static let sample = URLRequest(url: URL(string: "https://facebook.com")!)
}


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

import FBSDKCoreKit_Basics
import XCTest

final class UserDefaultsTests: XCTestCase {

  // swiftlint:disable:next implicitly_unwrapped_optional
  var userDefaults: UserDefaults!

  override func setUp() {
    super.setUp()
    userDefaults = UserDefaults()
  }

  override func tearDown() {
    userDefaults.removeObject(forKey: .integerKey)
    userDefaults.removeObject(forKey: .stringKey)
    userDefaults.removeObject(forKey: .dataKey)
    userDefaults.removeObject(forKey: .objectKey)
    userDefaults = nil

    super.tearDown()
  }

  func testGettingInteger() {
    userDefaults.set(14, forKey: .integerKey)
    XCTAssertEqual(userDefaults.fb_integer(forKey: .integerKey), 14, .getInteger)
  }

  func testSettingInteger() {
    userDefaults.fb_setInteger(14, forKey: .integerKey)
    XCTAssertEqual(userDefaults.integer(forKey: .integerKey), 14, .setInteger)
  }

  func testGettingObject() {
    let object = NSString(string: "some value")
    userDefaults.set(object, forKey: .objectKey)

    XCTAssertIdentical(userDefaults.fb_object(forKey: .objectKey) as AnyObject, object, .getObject)
  }

  func testSettingObject() {
    let object = NSString(string: "some value")
    userDefaults.fb_setObject(object, forKey: .objectKey)

    XCTAssertIdentical(userDefaults.object(forKey: .objectKey) as AnyObject, object, .setObject)
  }

  func testGettingString() {
    userDefaults.set("some value", forKey: .stringKey)
    XCTAssertEqual(userDefaults.fb_string(forKey: .stringKey), "some value", .getString)
  }

  func testGettingData() {
    let data = Data(repeating: 14, count: 14)
    userDefaults.set(data, forKey: .dataKey)

    XCTAssertEqual(userDefaults.fb_data(forKey: .dataKey), data, .getData)
  }

  func testRemovingObject() {
    let object = NSString(string: "some value")
    userDefaults.set(object, forKey: .objectKey)

    userDefaults.fb_removeObject(forKey: .objectKey)
    XCTAssertNil(userDefaults.object(forKey: .objectKey), .removeObject)
  }
}

// swiftformat:disable extensionaccesscontrol

// MARK: - Assumptions

fileprivate extension String {
  static let getInteger = "User defaults can get an integer through an internal abstraction"
  static let setInteger = "User defaults can set an integer through an internal abstraction"
  static let getObject = "User defaults can get an object through an internal abstraction"
  static let setObject = "User defaults can set an object through an internal abstraction"
  static let getString = "User defaults can get a string through an internal abstraction"
  static let getData = "User defaults can get data through an internal abstraction"
  static let removeObject = "User defaults can remove an object through an internal abstraction"
}

// MARK: - Test Values

fileprivate extension String {
  static let integerKey = "integer"
  static let stringKey = "string"
  static let dataKey = "data"
  static let objectKey = "object"
}


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

import FBSDKCoreKit

final class AccessTokenProvider: _AccessTokenProviding {
  static var tokenCache: TokenCaching? {
    get {
      AccessToken.tokenCache
    }
    set {
      AccessToken.tokenCache = newValue
    }
  }

  static var current: AccessToken? {
    get {
      AccessToken.current
    }
    set {} // swiftlint:disable:this unused_setter_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.
 */

import FBSDKCoreKit
import Foundation

@objc(FBSDKChooseContextFilter)
public enum ChooseContextFilter: Int, CaseIterable {
  case none
  case existingChallenges
  case newPlayersOnly
  case newContextOnly

  public var name: String {
    switch self {
    case .none:
      return "NO_FILTER"
    case .existingChallenges:
      return "INCLUDE_EXISTING_CHALLENGES"
    case .newPlayersOnly:
      return "NEW_PLAYERS_ONLY"
    case .newContextOnly:
      return "NEW_CONTEXT_ONLY"
    }
  }
}

/// A model for an instant games choose context app switch dialog
@objcMembers
@objc(FBSDKChooseContextContent)
public final class ChooseContextContent: NSObject, ValidatableProtocol {

  /// This sets the filter which determines which context will show when the user is app switched to the choose context dialog.
  public var filter = ChooseContextFilter.none

  /// This sets the maximum number of participants that the suggested context(s) shown in the dialog should have.
  public var maxParticipants = 0

  /// This sets the minimum number of participants that the suggested context(s) shown in the dialog should have.
  public var minParticipants = 0

  public static func filtersName(forFilters filter: ChooseContextFilter) -> String {
    switch filter {
    case .newContextOnly:
      return "NEW_CONTEXT_ONLY"
    case .existingChallenges:
      return "INCLUDE_EXISTING_CHALLENGES"
    case .newPlayersOnly:
      return "NEW_PLAYERS_ONLY"
    case .none:
      return "NO_FILTER"
    }
  }

  // MARK: - ValidatableProtocol

  public func validate() throws {
    if minParticipants == 0, maxParticipants == 0 {
      return
    }

    let minimumGreaterThanMaximum = minParticipants > maxParticipants
    if minimumGreaterThanMaximum, maxParticipants != 0 {
      let message = "The minimum size cannot be greater than the maximum size"
      throw _ErrorFactory().requiredArgumentError(
        name: "minParticipants",
        message: message,
        underlyingError: nil
      )
    }
  }

  // MARK: - NSObject

  public override func isEqual(_ object: Any?) -> Bool {
    let contentObject = (object as? ChooseContextContent)
    return filter.rawValue == contentObject?.filter.rawValue
      && minParticipants == contentObject?.minParticipants
      && maxParticipants == contentObject?.maxParticipants
  }
}


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

import FBSDKCoreKit
import Foundation

/// A dialog for the choose context through app switch
@objcMembers
@objc(FBSDKChooseContextDialog)
public final class ChooseContextDialog: ContextWebDialog, URLOpening {

  private enum Constants {
    static let contextKey = "context_id"
    static let contextSize = "context_size"
    static let errorMessage = "error_message"
  }

  private var internalUtility: InternalUtilityProtocol
  private var urlFactory: DialogDeeplinkURLCreating

  /**
   Convenience method to build up a choose context app switch with content and a delegate.
   @param content The content for the choose context dialog
   @param delegate The receiver's delegate.
   */
  public convenience init(
    content: ChooseContextContent,
    delegate: ContextDialogDelegate
  ) {
    self.init(content, delegate: delegate, internalUtility: InternalUtility.shared)
  }

  init(
    _ content: ChooseContextContent,
    delegate: ContextDialogDelegate,
    internalUtility: InternalUtilityProtocol
  ) {
    self.internalUtility = internalUtility
    urlFactory = ChooseContextDialogURLFactory(appID: Settings.shared.appID ?? "", content: content)
    super.init(delegate: delegate, dialogContent: content)
  }

  public override func show() -> Bool {
    var dialogURL: URL
    do {
      try validate()
      dialogURL = try urlFactory.generateDialogDeeplinkURL()
    } catch {
      handleDialogError(error as NSError)
      return false
    }

    _BridgeAPI.shared.open(
      dialogURL,
      sender: self
    ) { [weak self] success, bridgeError in
      guard let self = self else {
        return
      }

      if !success, bridgeError != nil {
        let sdkError = _ErrorFactory().error(
          code: CoreError.errorBridgeAPIInterruption.rawValue,
          userInfo: nil,
          message: "Error occurred while interacting with Gaming Services, Failed to open bridge.",
          underlyingError: bridgeError
        )
        self.handleDialogError(sdkError)
      }
    }
    return true
  }

  public override func validate() throws {
    guard Settings.shared.appID != nil else {
      throw _ErrorFactory().error(
        code: CoreError.errorUnknown.rawValue,
        userInfo: nil,
        message: "App ID is not set in settings",
        underlyingError: nil
      )
    }
    try dialogContent?.validate()
  }

  private func handleDialogError(_ dialogError: Error) {
    delegate?.contextDialog(self, didFailWithError: dialogError)
  }

  func gamingContextFromURL(_ url: URL) throws -> GamingContext? {
    let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)
    guard let queryItems = urlComponents?.queryItems, !queryItems.isEmpty else {
      return nil
    }
    return try extractGamingContext(from: queryItems)
  }

  private func extractGamingContext(from queryItems: [URLQueryItem]) throws -> GamingContext? {
    var contextID: String?
    var contextSize = 0

    for queryItem in queryItems {
      if queryItem.name == Constants.contextKey, let identifier = queryItem.value {
        contextID = identifier
      }
      if queryItem.name == Constants.contextSize, let size = Int(queryItem.value ?? "") {
        contextSize = size
      }
      if queryItem.name == Constants.errorMessage, let errorMessage = queryItem.value {
        throw _ErrorFactory().unknownError(message: errorMessage, userInfo: nil)
      }
    }

    guard let identifier = contextID, !identifier.isEmpty else {
      return nil
    }
    return GamingContext(identifier: identifier, size: contextSize)
  }

  // MARK: - URLOpening

  public func application(
    _ application: UIApplication?,
    open url: URL?,
    sourceApplication: String?,
    annotation: Any?
  ) -> Bool {
    guard
      let url = url,
      canOpen(
        url,
        for: application,
        sourceApplication: sourceApplication,
        annotation: annotation
      )
    else {
      return false
    }

    var gameContext: GamingContext?
    do {
      gameContext = try gamingContextFromURL(url)
    } catch {
      handleDialogError(error)
      return false
    }

    if gameContext != nil {
      delegate?.contextDialogDidComplete(self)
    } else {
      delegate?.contextDialogDidCancel(self)
    }
    return true
  }

  public func canOpen(
    _ url: URL,
    for application: UIApplication?,
    sourceApplication: String?,
    annotation: Any?
  ) -> Bool {
    guard let appID = Settings.shared.appID else {
      return false
    }
    let isCorrectScheme = url.scheme?.hasPrefix("fb\(appID)") ?? false
    let isCorrectHost = url.host?.elementsEqual("gaming") ?? false
    let isCorrectPath = url.path.elementsEqual("/contextchoose")
    return isCorrectScheme && isCorrectHost && isCorrectPath
  }

  public func applicationDidBecomeActive(_ application: UIApplication) {
    delegate?.contextDialogDidCancel(self)
  }

  public func isAuthenticationURL(_ url: URL) -> Bool {
    false
  }
}


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

import FBSDKCoreKit
import Foundation

struct ChooseContextDialogURLFactory: DialogDeeplinkURLCreating {

  private enum URLValues {
    static let host = "fb.gg"
    static let path = "/dialog/choosecontext/"
  }

  private enum QueryKeys {
    static let filter = "filter"
    static let minSize = "min_size"
    static let maxSize = "max_size"
  }

  var content: ChooseContextContent
  var appID: String

  init(appID: String, content: ChooseContextContent) {
    self.appID = appID
    self.content = content
  }

  func generateDialogDeeplinkURL() throws -> URL {
    var components = URLComponents()
    components.scheme = URLScheme.https.rawValue
    components.host = URLValues.host
    components.path = "\(URLValues.path)\(appID)"

    components.queryItems = [
      URLQueryItem(name: QueryKeys.filter, value: ChooseContextContent.filtersName(forFilters: content.filter)),
      URLQueryItem(name: QueryKeys.minSize, value: String(content.minParticipants)),
      URLQueryItem(name: QueryKeys.maxSize, value: String(content.maxParticipants)),
    ]
    guard let url = components.url else {
      throw GamingServicesDialogError.deeplinkURLCreation
    }
    return url
  }
}


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

import Foundation

/**
 A delegate for context dialogs to communicate with the dialog handler.
 The delegate is notified with the results of the cross play request as long as the application has permissions to
 receive the information.  For example, if the person is not signed into the containing app, the shower may not be able
 to distinguish between completion of a cross play request and cancellation.
 */
@objc(FBSDKContextDialogDelegate)
public protocol ContextDialogDelegate {

  /**
   Sent to the delegate when the context dialog completes without error.
   @param contextDialog The FBSDKContextDialog that completed.
   */
  func contextDialogDidComplete(_ contextDialog: ContextWebDialog)

  /**
   Sent to the delegate when the context dialog encounters an error.
   @param contextDialog The FBSDKContextDialog that completed.
   @param error The error.
   */
  func contextDialog(_ contextDialog: ContextWebDialog, didFailWithError error: Error)

  /**
   Sent to the delegate when the cross play request dialog is cancelled.
   @param contextDialog The FBSDKContextDialog that completed.
   */
  func contextDialogDidCancel(_ contextDialog: ContextWebDialog)
}


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

/**
 A dialog presenter responsible for creating and showing all the dialogs that create, switch,
 choose and otherwise manipulate the gaming context.
 */
@objcMembers
@objc(FBSDKContextDialogPresenter)
public final class ContextDialogPresenter: NSObject {

  private(set) var createContextDialogFactory: CreateContextDialogMaking
  private(set) var switchContextDialogFactory: SwitchContextDialogMaking
  private(set) var chooseContextDialogFactory: ChooseContextDialogMaking

  public override convenience init() {
    self.init(
      createContextDialogFactory: CreateContextDialogFactory(tokenProvider: AccessTokenProvider.self),
      switchContextDialogFactory: SwitchContextDialogFactory(tokenProvider: AccessTokenProvider.self),
      chooseContextDialogFactory: ChooseContextDialogFactory()
    )
  }

  init(
    createContextDialogFactory: CreateContextDialogMaking,
    switchContextDialogFactory: SwitchContextDialogMaking,
    chooseContextDialogFactory: ChooseContextDialogMaking
  ) {
    self.createContextDialogFactory = createContextDialogFactory
    self.switchContextDialogFactory = switchContextDialogFactory
    self.chooseContextDialogFactory = chooseContextDialogFactory

    super.init()
  }

  /**
   Convenience method to build up and show an instant games create context dialog with content and delegate.

   - Parameters:
      - content: The content for the create context dialog
      - delegate: The receiver's delegate.
   */
  public func makeAndShowCreateContextDialog(
    content: CreateContextContent,
    delegate: ContextDialogDelegate
  ) throws {
    guard let dialog = try createContextDialogFactory.makeCreateContextDialog(
      content: content,
      windowFinder: InternalUtility.shared,
      delegate: delegate
    )
    else {
      throw ContextDialogPresenterError.showCreateContext
    }

    _ = dialog.show()
  }

  /**
   Convenience method to build up and show an instant games switch context dialog with the giving content and delegate.

   - Parameters:
      - content: The content for the switch context dialog
      - delegate: The receiver's delegate.
   */
  public func makeAndShowSwitchContextDialog(
    content: SwitchContextContent,
    delegate: ContextDialogDelegate
  ) throws {
    guard let dialog = try switchContextDialogFactory.makeSwitchContextDialog(
      content: content,
      windowFinder: InternalUtility.shared,
      delegate: delegate
    )
    else {
      throw ContextDialogPresenterError.showSwitchContext
    }

    _ = dialog.show()
  }

  /**
   Convenience method to build up and show an instant games choose context dialog with content and a delegate.

   - Parameters:
      - content: The content for the choose context dialog
      - delegate: The receiver's delegate.
   */
  public func makeAndShowChooseContextDialog(
    content: ChooseContextContent,
    delegate: ContextDialogDelegate
  ) {
    _ = chooseContextDialogFactory.makeChooseContextDialog(
      content: content,
      delegate: delegate
    )
    .show()
  }
}


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

import Foundation

/// Errors representing a failure to present a context dialog
public enum ContextDialogPresenterError: Error {
  case showCreateContext
  case showSwitchContext
  case showChooseContext
  case invalidAccessToken
}


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

import Foundation

import FBSDKCoreKit

/// A super class type for the context dialogs classes that show an in-app webview to display content.
@objcMembers
@objc(FBSDKContextWebDialog) // swiftlint:disable:next prefer_final_classes
public class ContextWebDialog: NSObject, WebDialogDelegate, DialogProtocol {

  private enum Keys {
    static var contextID = "context_id"
    static var contextSize = "context_size"
    static var errorCode = "error_code"
    static var errorMessage = "error_message"
  }

  public var delegate: ContextDialogDelegate?
  public var dialogContent: ValidatableProtocol?
  public var currentWebDialog: _WebDialog?

  init(delegate: ContextDialogDelegate?, dialogContent: ValidatableProtocol?) {
    super.init()
    self.delegate = delegate
    self.dialogContent = dialogContent
  }

  public func show() -> Bool {
    false
  }

  public func validate() throws {}

  // MARK: - WebDialogDelegate

  public func webDialog(_ webDialog: _WebDialog, didCompleteWithResults results: [String: Any]) {
    if currentWebDialog != webDialog {
      return
    }
    handleCompletionWithDialogResults(results)
    InternalUtility.shared.unregisterTransientObject(self)
  }

  public func webDialog(_ webDialog: _WebDialog, didFailWithError error: Error) {
    guard let delegate = delegate, currentWebDialog == webDialog else {
      return
    }
    delegate.contextDialog(self, didFailWithError: error)
    InternalUtility.shared.unregisterTransientObject(self)
  }

  public func webDialogDidCancel(_ webDialog: _WebDialog) {
    guard let delegate = delegate, currentWebDialog == webDialog else {
      return
    }
    delegate.contextDialogDidCancel(self)
    InternalUtility.shared.unregisterTransientObject(self)
  }

  /// Depending on the content size within the browser, this method allows for the resizing of web dialog
  public func createWebDialogFrame(
    withWidth width: CGFloat,
    height: CGFloat,
    windowFinder: _WindowFinding
  ) -> CGRect {
    guard let window = windowFinder.findWindow() else {
      return .zero
    }
    let windowFrame = window.frame
    let xPoint = windowFrame.width < width ? 0 : windowFrame.midX - (width / 2)
    let yPoint = windowFrame.height < height ? 0 : windowFrame.midY - (height / 2)
    let dialogWidth = windowFrame.width < width ? windowFrame.width : width
    let dialogHeight = windowFrame.height < height ? windowFrame.height : height

    return CGRect(x: xPoint, y: yPoint, width: dialogWidth, height: dialogHeight)
  }

  private func handleCompletionWithDialogResults(_ results: [String: Any]) {
    guard let delegate = delegate else {
      return
    }

    if
      let errorCode = results[Keys.errorCode] as? Int,
      let errorMessage = results[Keys.errorMessage] as? String {
      let errorFactory = _ErrorFactory()
      let error = errorFactory.error(
        code: errorCode,
        userInfo: nil,
        message: errorMessage,
        underlyingError: nil
      )
      return delegate.contextDialog(self, didFailWithError: error)
    }

    if let identifier = results[Keys.contextID] as? String {
      let sizeString = results[Keys.contextSize] as? String ?? ""
      let size = Int(sizeString) ?? 0

      GamingContext.current = GamingContext(identifier: identifier, size: size)
      return delegate.contextDialogDidComplete(self)
    }

    delegate.contextDialogDidCancel(self)
  }
}


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

import FBSDKCoreKit
import Foundation

/// A model for an instant games createAsync cross play request.
@objcMembers
@objc(FBSDKCreateContextContent)
public final class CreateContextContent: NSObject, ValidatableProtocol {

  /**
   The ID of the player that is being challenged.
   @return The ID for the player being challenged
   */
  public var playerID: String

  /**
   Builds a content object that will be use to display a create context dialog
   @param playerID The player ID of the user being challenged which will be used  to create a game context
   */
  @objc(initDialogContentWithPlayerID:)
  public init(playerID: String) {
    self.playerID = playerID
  }

  public override func isEqual(_ object: Any?) -> Bool {
    let contentObject = (object as? CreateContextContent)
    return playerID == contentObject?.playerID
  }

  // MARK: - SharingValidation

  public func validate() throws {
    let hasPlayerID = !playerID.isEmpty
    guard hasPlayerID else {
      throw _ErrorFactory().requiredArgumentError(
        name: "playerID",
        message: "The playerID is required.",
        underlyingError: nil
      )
    }
  }
}


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

import FBSDKCoreKit
import Foundation

/// A dialog to create a context through a web view
@objcMembers
@objc(FBSDKCreateContextDialog)
public final class CreateContextDialog: ContextWebDialog {

  private enum Keys {
    static let methodName = "context"
  }

  private enum DialogFrame {
    static let width: CGFloat = 300.0
    static let height: CGFloat = 185.0
  }

  let windowFinder: _WindowFinding

  /**
   Builds a context creation web dialog with content and a delegate.
   @param content The content for the create context dialog
   @param windowFinder The application window finder that provides the window to display the dialog
   @param delegate The receiver's delegate used to let the receiver know a context was created or failure
   */
  public init(
    content: CreateContextContent,
    windowFinder: _WindowFinding,
    delegate: ContextDialogDelegate
  ) {
    self.windowFinder = windowFinder
    super.init(delegate: delegate, dialogContent: content)
  }

  @discardableResult public override func show() -> Bool {
    do {
      try validate()
    } catch {
      delegate?.contextDialog(self, didFailWithError: error)
      return false
    }

    guard let dialogContent = dialogContent as? CreateContextContent else {
      return false
    }

    var parameters: [String: Any] = [:]
    if !dialogContent.playerID.isEmpty {
      parameters["player_id"] = dialogContent.playerID
    }

    let frame = createWebDialogFrame(
      withWidth: DialogFrame.width,
      height: DialogFrame.height,
      windowFinder: windowFinder
    )

    currentWebDialog = _WebDialog(
      name: Keys.methodName,
      parameters: parameters as? [String: String],
      webViewFrame: frame
    )
    currentWebDialog?.delegate = self
    currentWebDialog?.show()

    InternalUtility.shared.registerTransientObject(self)
    return true
  }

  public override func validate() throws {
    guard let content = dialogContent else {
      throw GamingServicesDialogError.missingContent
    }
    try content.validate()
  }
}


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

import Foundation

protocol DialogDeeplinkURLCreating {
  func generateDialogDeeplinkURL() throws -> URL
}


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

import Foundation

/// The protocol sdk dialogs must conform to and implement all the following methods.
@objc(FBSDKDialog)
public protocol DialogProtocol {
  /// The receiver's delegate or nil if it doesn't have a delegate.
  weak var delegate: ContextDialogDelegate? { get set }

  /// The content object used to create the specific dialog
  var dialogContent: ValidatableProtocol? { get set }

  /**
   Begins to show the specfic dialog
   @return true if the receiver was able to show the dialog, otherwise false.
   */
  func show() -> Bool

  /// Validates the content for the dialog
  func validate() throws
}

/// A protocol that a content object must conform to be used in a Gaming Services dialog
@objc(FBSDKValidatable)
public protocol ValidatableProtocol {

  func validate() throws
}


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

import FBSDKCoreKit
import Foundation

@objc(FBSDKFriendFinderDialog)
public final class FriendFinderDialog: NSObject {

  let factory: GamingServiceControllerCreating

  // Transitional singleton introduced as a way to change the usage semantics
  // from a type-based interface to an instance-based interface.
  static let shared = FriendFinderDialog()

  public override convenience init() {
    self.init(gamingServiceControllerFactory: GamingServiceControllerFactory())
  }

  init(gamingServiceControllerFactory: GamingServiceControllerCreating) {
    factory = gamingServiceControllerFactory
  }

  @objc(launchFriendFinderDialogWithCompletionHandler:)
  public static func launch(completionHandler: @escaping GamingServiceCompletionHandler) {
    shared.launch(completionHandler: completionHandler)
  }

  func launch(completionHandler: @escaping GamingServiceCompletionHandler) {
    guard let appID = Settings.shared.appID ?? AccessToken.current?.appID else {
      let error = _ErrorFactory().error(
        code: CoreError.errorAccessTokenRequired.rawValue,
        userInfo: nil,
        message: "A valid access token is required to launch the Friend Finder",
        underlyingError: nil
      )
      completionHandler(false, error)
      return
    }

    let controller = factory.create(serviceType: .friendFinder, pendingResult: nil) { success, _, error in
      completionHandler(success, error)
    }

    controller.call(withArgument: appID)
  }
}


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

import Foundation

/// Additional context about the nature of the game request.
@objc(FBSDKGameRequestActionType)
public enum GameRequestActionType: UInt {
  /// No action type
  case none

  /// Send action type: The user is sending an object to the friends.
  case send

  /// Ask For action type: The user is asking for an object from friends.
  case askFor

  /// Turn action type: It is the turn of the friends to play against the user in a match.
  case turn

  /// Invite action type: The user is inviting a friend.
  case invite
}


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

/// A model for a game request.
@objcMembers
@objc(FBSDKGameRequestContent)
public final class GameRequestContent: NSObject, SharingValidatable, NSSecureCoding {

  /**
   Used when defining additional context about the nature of the request.

   The parameter 'objectID' is required if the action type is either
   '.send' or '.askFor'.

   - SeeAlso: objectID
   */
  public var actionType = GameRequestActionType.none

  /**
   Additional freeform data you may pass for tracking. This will be stored as part of
   the request objects created. The maximum length is 255 characters.
   */
  public var data: String?

  /**
   This controls the set of friends someone sees if a multi-friend selector is shown.
   It is `.none` by default, meaning that all friends can be shown.
   If specify as `.appUsers`, only friends who use the app will be shown.
   On the other hands, use `.appNonUsers` to filter only friends who do not use the app.

   The parameter name is preserved to be consistent with the counter part on desktop.
   */
  public var filters = GameRequestFilter.none

  /**
   A plain-text message to be sent as part of the request. This text will surface in the App Center view
   of the request, but not on the notification jewel. Required parameter.
   */
  public var message = ""

  /**
   The Open Graph object ID of the object being sent.

   - SeeAlso: actionType
   */
  public var objectID = ""

  /**
   An array of user IDs, usernames or invite tokens (NSString) of people to send request.

   These may or may not be a friend of the sender. If this is specified by the app,
   the sender will not have a choice of recipients. If not, the sender will see a multi-friend selector

   This is equivalent to the "to" parameter when using the web game request dialog.
   */
  public var recipients = [String]()

  /**
   An array of user IDs that will be included in the dialog as the first suggested friends.
   Cannot be used together with filters.

   This is equivalent to the `suggestions` parameter when using the web game request dialog.
   */
  public var recipientSuggestions = [String]()

  /// The title for the dialog.
  public var title = ""

  /// The call to action for the dialog.
  public var cta = ""

  @objc(validateWithOptions:error:)
  public func validate(options: ShareBridgeOptions = []) throws {
    try _ShareUtility.validateRequiredValue(message, named: "message")

    let errorFactory = _ErrorFactory()
    let mustHaveObjectID = (actionType == .send) || (actionType == .askFor)
    let hasObjectID = !objectID.isEmpty

    guard mustHaveObjectID == hasObjectID else {
      throw errorFactory.requiredArgumentError(
        domain: ShareErrorDomain,
        name: "objectID",
        message: "The objectID is required when the actionType is either .send or .askfor",
        underlyingError: nil
      )
    }

    let hasRecipients = !recipients.isEmpty
    let hasFilters = (filters != .none)
    let hasSuggestions = !recipientSuggestions.isEmpty

    guard !(hasRecipients && hasFilters) else {
      throw errorFactory.invalidArgumentError(
        domain: ShareErrorDomain,
        name: "recipients",
        value: recipients,
        message: "Cannot specify recipients and filters at the same time.",
        underlyingError: nil
      )
    }

    guard !(hasRecipients && hasSuggestions) else {
      throw errorFactory.invalidArgumentError(
        domain: ShareErrorDomain,
        name: "recipients",
        value: recipients,
        message: "Cannot specify recipients and suggestions at the same time.",
        underlyingError: nil
      )
    }

    guard !(hasFilters && hasSuggestions) else {
      throw errorFactory.invalidArgumentError(
        domain: ShareErrorDomain,
        name: "recipientSuggestions",
        value: recipientSuggestions,
        message: "Cannot specify filters and suggestions at the same time.",
        underlyingError: nil
      )
    }

    guard (data?.count ?? 0) <= 255 else {
      throw errorFactory.invalidArgumentError(
        domain: ShareErrorDomain,
        name: "data",
        value: data,
        message: "The data cannot be longer than 255 characters",
        underlyingError: nil
      )
    }

    try _ShareUtility.validateArgument(
      actionType,
      named: "actionType",
      in: [.none, .send, .askFor, .turn, .invite]
    )

    try _ShareUtility.validateArgument(
      filters,
      named: "filters",
      in: [.none, .appUsers, .appNonUsers, .everybody]
    )
  }

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

    return (self === other) || isEqual(to: other)
  }

  /**
   Compares the receiver to another game request content.
   @param content The other content
   @return `true` if the receiver's values are equal to the other content's values; otherwise `false`
   */
  @objc(isEqualToGameRequestContent:)
  public func isEqual(to content: GameRequestContent) -> Bool {
    actionType == content.actionType
      && filters == content.filters
      && data == content.data
      && message == content.message
      && objectID == content.objectID
      && recipientSuggestions == content.recipientSuggestions
      && title == content.title
      && recipients == content.recipients
  }

  private enum CodingKeys: String, CodingKey {
    case to
    case message
    case actionType
    case objectID
    case filters
    case suggestions
    case data
    case title
  }

  public class var supportsSecureCoding: Bool { true }

  public convenience init(coder decoder: NSCoder) {
    self.init()

    actionType = GameRequestActionType(
      rawValue: UInt(decoder.decodeInteger(forKey: CodingKeys.actionType.rawValue))
    ) ?? .none
    data = decoder.decodeObject(of: NSString.self, forKey: CodingKeys.data.rawValue) as String?
    filters = GameRequestFilter(rawValue: UInt(decoder.decodeInteger(forKey: CodingKeys.filters.rawValue))) ?? .none
    message = (decoder.decodeObject(of: NSString.self, forKey: CodingKeys.message.rawValue) as String?) ?? ""
    objectID = (decoder.decodeObject(of: NSString.self, forKey: CodingKeys.objectID.rawValue) as String?) ?? ""
    recipientSuggestions = (decoder.decodeObject(
      of: [NSArray.self, NSString.self],
      forKey: CodingKeys.suggestions.rawValue
    ) as? [String]) ?? []
    title = (decoder.decodeObject(of: NSString.self, forKey: CodingKeys.title.rawValue) as String?) ?? ""
    recipients = (decoder.decodeObject(
      of: [NSArray.self, NSString.self],
      forKey: CodingKeys.to.rawValue
    ) as? [String]) ?? []
  }

  public func encode(with encoder: NSCoder) {
    encoder.encode(Int(actionType.rawValue), forKey: CodingKeys.actionType.rawValue)
    encoder.encode(data, forKey: CodingKeys.data.rawValue)
    encoder.encode(Int(filters.rawValue), forKey: CodingKeys.filters.rawValue)
    encoder.encode(message, forKey: CodingKeys.message.rawValue)
    encoder.encode(objectID, forKey: CodingKeys.objectID.rawValue)
    encoder.encode(recipientSuggestions, forKey: CodingKeys.suggestions.rawValue)
    encoder.encode(title, forKey: CodingKeys.title.rawValue)
    encoder.encode(recipients, forKey: CodingKeys.to.rawValue)
  }
}


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

import FBSDKCoreKit
import FBSDKCoreKit_Basics
import FBSDKShareKit
import Foundation

/// A dialog for sending game requests.
@objcMembers
@objc(FBSDKGameRequestDialog)
public final class GameRequestDialog: NSObject {

  /// The receiver's delegate or nil if it doesn't have a delegate.
  public weak var delegate: GameRequestDialogDelegate?

  /// The content for game request.
  public var content: GameRequestContent

  /// Specifies whether frictionless requests are enabled.
  public var isFrictionlessRequestsEnabled = false

  /**
   A Boolean value that indicates whether the receiver can initiate a game request.

   May return `false` if the appropriate Facebook app is not installed and is required or an access token is
   required but not available.  This method does not validate the content on the receiver, so this can be checked before
   building up the content.

   See `validate()`

   @return `true` if the receiver can share, otherwise `false`.
   */
  public var canShow: Bool { true }

  private var dialogIsFrictionless = false
  private var isAwaitingResult = false
  private lazy var webDialog: _WebDialog = {
    let webDialog = _WebDialog(name: GameRequestDialog.appRequestMethodName)
    webDialog.delegate = self
    return webDialog
  }()

  private static let recipientCache = GameRequestFrictionlessRecipientCache()
  private static let appRequestMethodName = "apprequests"
  private static let gameRequestURLHost = "game_requests"

  var configuredDependencies: ObjectDependencies?

  var defaultDependencies: ObjectDependencies? = .init(
    bridgeAPIRequestOpener: _BridgeAPI.shared,
    errorFactory: _ErrorFactory(),
    gameRequestURLProvider: GameRequestURLProvider.self,
    internalUtility: InternalUtility.shared,
    logger: _Logger.self,
    settings: Settings.shared,
    shareValidator: _ShareUtility.self,
    utility: Utility.self
  )

  public init(content: GameRequestContent, delegate: GameRequestDialogDelegate?) {
    self.content = content
    self.delegate = delegate
  }

  /**
   Convenience method to build up a game request with content and a delegate.
   @param content The content for the game request.
   @param delegate The receiver's delegate.
   */

  @objc(dialogWithContent:delegate:)
  public static func dialog(
    content: GameRequestContent,
    delegate: GameRequestDialogDelegate?
  ) -> GameRequestDialog {
    GameRequestDialog(content: content, delegate: delegate)
  }

  /**
   Convenience method to build up and show a game request with content and a delegate.
   @param content The content for the game request.
   @param delegate The receiver's delegate.
   */
  @objc(showWithContent:delegate:)
  @discardableResult
  public static func show(
    content: GameRequestContent,
    delegate: GameRequestDialogDelegate?
  ) -> GameRequestDialog {
    show(dialog: GameRequestDialog(content: content, delegate: delegate))
  }

  /// Testable implementation of the public static `show` method. Allows us to configure dependencies on the
  /// dialog that is acted upon.
  static func show(dialog: GameRequestDialog) -> GameRequestDialog {
    guard let dependencies = try? dialog.getDependencies() else { return dialog }

    if dependencies.utility.getGraphDomainFromToken() == "gaming",
       dependencies.internalUtility.isFacebookAppInstalled {
      dialog.launch()
    } else {
      dialog.show()
    }

    return dialog
  }

  private func launch() {
    do {
      try validate()
    } catch {
      return handleDialogError(error)
    }

    let contentDictionary = convertGameRequestContentToDictionaryV2()

    guard
      let dependencies = try? getDependencies(),
      let url = dependencies.gameRequestURLProvider.createDeepLinkURL(queryDictionary: contentDictionary)
    else { return }

    isAwaitingResult = true
    dependencies.bridgeAPIRequestOpener.open(url, sender: self) { [weak self] success, potentialError in
      guard success else { return }

      if let error = potentialError {
        self?.handleBridgeAPIFailure(error)
      }
    }
  }

  private func facebookAppReturnedURL(_ url: URL) {
    cleanUp()
    guard let results = parsePayload(from: url) else { return }

    delegate?.gameRequestDialog(self, didCompleteWithResults: results)
  }

  private func handleDialogError(_ error: Error) {
    delegate?.gameRequestDialog(self, didFailWithError: error)
    cleanUp()
  }

  private func handleBridgeAPIFailure(_ error: Error) {
    // swiftformat:disable:next redundantSelf
    guard let errorFactory = self.errorFactory else { return }

    let bridgeAPIError = errorFactory.error(
      code: CoreError.errorBridgeAPIInterruption.rawValue,
      userInfo: nil,
      message: "Error occured while interacting with Gaming Services, Failed to open bridge.",
      underlyingError: error
    )
    handleDialogError(bridgeAPIError)
  }

  private func isValidCallbackURL(_ url: URL) -> Bool {
    guard let scheme = url.scheme,
          let host = url.host
    else { return false }

    // swiftformat:disable:next redundantSelf
    let appID = self.settings?.appID ?? ""
    let schemePrefixMatches = scheme.hasPrefix("fb\(appID)")
    let hostMatches = (host == Self.gameRequestURLHost)
    return schemePrefixMatches && hostMatches
  }

  private enum PayloadKeys {
    static let requestID = "request_id"
    static let recipients = "recipients"
  }

  private func parsePayload(from url: URL) -> [String: Any]? {
    // If the URL contains no query items, then the user self closed the dialog within fbios.
    guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
          let queryItems = components.queryItems,
          queryItems.contains(where: { $0.name == PayloadKeys.requestID })
    else {
      didCancel()
      return nil
    }

    var payload = [String: Any]()
    if let requestIDItem = queryItems.last(where: { $0.name == PayloadKeys.requestID }),
       let value = requestIDItem.value {
      payload[requestIDItem.name] = value
    }
    if let recipientsItem = queryItems.last(where: { $0.name == PayloadKeys.recipients }),
       let recipients = recipientsItem.value {
      payload[recipientsItem.name] = recipients.components(separatedBy: ",")
    }

    return payload
  }

  /**
   Begins the game request from the receiver.
   @return `true` if the receiver was able to show the dialog, otherwise `false`.
   */
  @discardableResult
  public func show() -> Bool {
    guard let dependencies = try? getDependencies() else { return false }

    do {
      try validate()
    } catch {
      delegate?.gameRequestDialog(self, didFailWithError: error)
      return false
    }

    var parameters = convertGameRequestContentToDictionaryV1()

    // check if we are sending to a specific set of recipients.  if we are and they are all frictionless recipients, we
    // can perform this action without displaying the web dialog
    webDialog.shouldDeferVisibility = false

    if isFrictionlessRequestsEnabled {
      // specify these parameters to get the frictionless recipients from the dialog when it is presented
      parameters["frictionless"] = true
      parameters["get_frictionless_recipients"] = true

      dialogIsFrictionless = true
      if Self.recipientCache.recipientsAreFrictionless(content.recipients) {
        webDialog.shouldDeferVisibility = true
      }
    }

    launchViaBridgeAPI(parameters: parameters)

    dependencies.internalUtility.registerTransientObject(self)
    return true
  }

  /// Validates the content on the receiver.
  @objc(validateWithError:)
  public func validate() throws {
    let validator = try getDependencies().shareValidator
    try validator.validateRequiredValue(content, named: "content")
    try content.validate(options: [])
  }

  private func convertGameRequestContentToDictionaryV1() -> [String: Any] {
    // swiftformat:disable:next redundantSelf
    guard let urlProvider = self.gameRequestURLProvider else { return [:] }

    var parameters: [String: Any] = [
      "to": content.recipients.joined(separator: ","),
      "message": content.message,
      "object_id": content.objectID,
      "title": content.title,
      "suggestions": content.recipientSuggestions.joined(separator: ","),
    ]

    if let actionTypeName = urlProvider.actionTypeName(for: content.actionType) {
      parameters["action_type"] = actionTypeName
    }
    if let data = content.data {
      parameters["data"] = data
    }
    if let filtersName = urlProvider.filtersName(for: content.filters) {
      parameters["filters"] = filtersName
    }

    return parameters
  }

  private func convertGameRequestContentToDictionaryV2() -> [String: Any] {
    // swiftformat:disable:next redundantSelf
    guard let urlProvider = self.gameRequestURLProvider else { return [:] }

    var parameters: [String: Any] = [
      "to": content.recipientSuggestions.joined(separator: ","),
      "message": content.message,
      "object_id": content.objectID,
      "title": content.title,
      "cta": content.cta,
    ]

    if let actionTypeName = urlProvider.actionTypeName(for: content.actionType) {
      parameters["action_type"] = actionTypeName
    }
    if let data = content.data {
      parameters["data"] = data
    }
    if let filtersName = urlProvider.filtersName(for: content.filters) {
      parameters["options"] = filtersName
    }

    return parameters
  }

  private func launchViaBridgeAPI(parameters: [String: Any]) {
    guard let dependencies = try? getDependencies() else { return }

    guard let topMostViewController = dependencies.internalUtility.topMostViewController() else {
      dependencies.logger.singleShotLogEntry(
        .developerErrors,
        logEntry: "There are no valid ViewController to present FBSDKWebDialog"
      )
      return handleCompletion()
    }

    let potentialRequest = _BridgeAPIRequest(
      protocolType: .web,
      scheme: .https,
      methodName: Self.appRequestMethodName,
      parameters: parameters,
      userInfo: nil
    )

    guard let request = potentialRequest else { return }

    dependencies.internalUtility.registerTransientObject(self)

    dependencies.bridgeAPIRequestOpener.open(
      request,
      useSafariViewController: false,
      from: topMostViewController
    ) { [weak self] response in
      self?.handleBridgeAPIResponse(response)
    }
  }

  private func handleBridgeAPIResponse(_ response: BridgeAPIResponse) {
    if response.isCancelled {
      didCancel()
    } else if let error = response.error {
      didFail(error: error)
    } else {
      didComplete(results: response.responseParameters)
    }
  }

  private func didComplete(results potentialResults: [String: Any]?) {
    guard let dependencies = try? getDependencies() else { return }

    guard var results = potentialResults else {
      let error = dependencies.errorFactory.error(
        domain: ShareErrorDomain,
        code: ShareError.unknown.rawValue,
        userInfo: nil,
        message: nil,
        underlyingError: nil
      )
      return handleCompletion(error: error)
    }

    if dialogIsFrictionless {
      Self.recipientCache.update(results: results)
    }

    cleanUp()

    let unsignedErrorCode = (results["error_code"] as? UInt) ?? 0
    let errorCode = Int(exactly: unsignedErrorCode) ?? 0

    let error = dependencies.errorFactory.error(
      code: errorCode,
      userInfo: nil,
      message: results["error_message"] as? String,
      underlyingError: nil
    )

    if errorCode != 0 {
      // reformat "to[x]" keys into an array.
      var index = 0
      var recipients = [Any]()
      while true {
        if let result = results["to[\(index)]"] {
          recipients.append(result)
        } else {
          break
        }

        index += 1
      }

      if !recipients.isEmpty {
        results["to"] = recipients
      }
    }

    handleCompletion(dialogResults: results, error: error)
    dependencies.internalUtility.unregisterTransientObject(self)
  }

  private func didFail(error: Error) {
    cleanUp()
    handleCompletion(error: error)
    // swiftformat:disable:next redundantSelf
    self.internalUtility?.unregisterTransientObject(self)
  }

  private func didCancel() {
    cleanUp()
    delegate?.gameRequestDialogDidCancel(self)
    // swiftformat:disable:next redundantSelf
    self.internalUtility?.unregisterTransientObject(self)
  }

  private func cleanUp() {
    dialogIsFrictionless = false
    isAwaitingResult = false
  }

  private func handleCompletion(dialogResults: [String: Any]? = nil, error: Error? = nil) {
    let nsError = error as NSError?
    let errorCode = nsError?.code ?? 0

    if errorCode == 0,
       let results = dialogResults {
      delegate?.gameRequestDialog(self, didCompleteWithResults: results)
    } else if errorCode == 4201 {
      delegate?.gameRequestDialogDidCancel(self)
    } else if let error = error {
      delegate?.gameRequestDialog(self, didFailWithError: error)
    }
  }
}

extension GameRequestDialog: WebDialogDelegate {
  public func webDialog(_ webDialog: _WebDialog, didCompleteWithResults results: [String: Any]) {
    guard self.webDialog === webDialog else { return }

    didComplete(results: results)
  }

  public func webDialog(_ webDialog: _WebDialog, didFailWithError error: Error) {
    guard self.webDialog === webDialog else { return }

    didFail(error: error)
  }

  public func webDialogDidCancel(_ webDialog: _WebDialog) {
    guard self.webDialog === webDialog else { return }

    didCancel()
  }
}

extension GameRequestDialog: URLOpening {
  public func application(
    _ application: UIApplication?,
    open potentialURL: URL?,
    sourceApplication: String?,
    annotation: Any?
  ) -> Bool {
    guard let url = potentialURL else { return false }

    let isGamingUrl = canOpen(
      url,
      for: application,
      sourceApplication: sourceApplication,
      annotation: annotation
    )

    if isGamingUrl,
       isAwaitingResult {
      facebookAppReturnedURL(url)
    }

    return isGamingUrl
  }

  public func canOpen(
    _ url: URL,
    for application: UIApplication?,
    sourceApplication: String?,
    annotation: Any?
  ) -> Bool {
    isValidCallbackURL(url)
  }

  public func applicationDidBecomeActive(_ application: UIApplication) {
    if isAwaitingResult {
      didCancel()
    }
  }

  public func isAuthenticationURL(_ url: URL) -> Bool {
    false
  }
}

extension GameRequestDialog: DependentAsObject {
  struct ObjectDependencies {
    var bridgeAPIRequestOpener: BridgeAPIRequestOpening
    var errorFactory: ErrorCreating
    var gameRequestURLProvider: GameRequestURLProviding.Type
    var internalUtility: InternalUtilityProtocol
    var logger: GamingLogging.Type
    var settings: SettingsProtocol
    var shareValidator: GamingShareValidating.Type
    var utility: GamingUtility.Type
  }
}


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

import Foundation

/**
 A delegate for `GameRequestDialog`.

 The delegate is notified with the results of the game request as long as the application has permissions to
 receive the information.  For example, if the person is not signed into the containing app, the shower may not be able
 to distinguish between completion of a game request and cancellation.
 */
@objc(FBSDKGameRequestDialogDelegate)
public protocol GameRequestDialogDelegate: AnyObject {

  /**
   Sent to the delegate when the game request completes without error.
   @param gameRequestDialog The `GameRequestDialog` that completed.
   @param results The results from the dialog.  This may be nil or empty.
   */
  @objc(gameRequestDialog:didCompleteWithResults:)
  func gameRequestDialog(
    _ gameRequestDialog: GameRequestDialog,
    didCompleteWithResults results: [String: Any]
  )

  /**
   Sent to the delegate when the game request encounters an error.
   @param gameRequestDialog The `GameRequestDialog` that completed.
   @param error The error.
   */
  @objc(gameRequestDialog:didFailWithError:)
  func gameRequestDialog(
    _ gameRequestDialog: GameRequestDialog,
    didFailWithError error: Error
  )

  /**
   Sent to the delegate when the game request dialog is cancelled.
   @param gameRequestDialog The `GameRequestDialog` that completed.
   */
  func gameRequestDialogDidCancel(_ gameRequestDialog: GameRequestDialog)
}


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

import Foundation

/// Filter for who can be displayed in the multi-friend selector.
@objc(FBSDKGameRequestFilter)
public enum GameRequestFilter: UInt {
  /// No filter, all friends can be displayed.
  case none

  /// Friends using the app can be displayed.
  case appUsers

  /// Friends not using the app can be displayed.
  case appNonUsers

  /// All friends can be displayed if FB app is installed.
  case everybody
}


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

import FBSDKCoreKit
import FBSDKCoreKit_Basics
import Foundation

final class GameRequestFrictionlessRecipientCache {
  private(set) var recipientIDs = Set<String>()

  var configuredDependencies: ObjectDependencies?

  var defaultDependencies: ObjectDependencies? = .init(
    graphRequestFactory: GraphRequestFactory(),
    notificationCenter: NotificationCenter.default,
    accessTokenWallet: AccessToken.self
  )

  // Custom implementation of dependency setting to perform additional work
  // that would normally go in the initializer
  func setDependencies(_ dependencies: ObjectDependencies) {
    configuredDependencies = dependencies

    _ = dependencies.notificationCenter.fb_addObserver(
      forName: .AccessTokenDidChange,
      object: nil,
      queue: nil
    ) { [weak self] notification in
      self?.accessTokenDidChange(notification: notification)
    }

    updateCache()
  }

  func recipientsAreFrictionless(_ potentialRecipients: Any?) -> Bool {
    guard let typeErasedRecipients = potentialRecipients else { return false }

    let recipients: [String]
    if let recipientsArray = typeErasedRecipients as? [String] {
      recipients = recipientsArray
    } else if let recipientsString = typeErasedRecipients as? String {
      recipients = recipientsString.components(separatedBy: ",")
    } else {
      return false
    }

    return Set(recipients).isSubset(of: recipientIDs)
  }

  func update(results: [String: Any]) {
    if let value = results["updated_frictionless"],
       TypeUtility.boolValue(value) {
      updateCache()
    }
  }

  private func accessTokenDidChange(notification: Notification) {
    let didChangeUserID = (notification.userInfo?[AccessTokenDidChangeUserIDKey] as? Bool) ?? false
    guard didChangeUserID else { return }

    recipientIDs = []
    updateCache()
  }

  private func updateCache() {
    guard let dependencies = try? getDependencies(),
          dependencies.accessTokenWallet.current != nil
    else {
      recipientIDs = []
      return
    }

    let request = dependencies.graphRequestFactory.createGraphRequest(
      withGraphPath: "me/apprequestformerrecipients",
      parameters: ["fields": ""],
      flags: [.doNotInvalidateTokenOnError, .disableErrorRecovery]
    )
    request.start { _, result, error in
      guard error == nil else { return }

      let values = result as? [String: Any]
      let items = values?["data"] as? [String: Any]
      let recipients = items?["recipient_id"] as? [String]
      self.recipientIDs = Set(recipients ?? [])
    }
  }
}

extension GameRequestFrictionlessRecipientCache: DependentAsObject {
  struct ObjectDependencies {
    var graphRequestFactory: GraphRequestFactoryProtocol
    var notificationCenter: NotificationDelivering
    var accessTokenWallet: _AccessTokenProviding.Type
  }
}


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

import FBSDKCoreKit
import Foundation

@objcMembers
@objc(FBSDKGameRequestURLProvider)
public final class GameRequestURLProvider: NSObject {
  private enum URLValues {
    static let host = "fb.gg"
    static let path = "/game_requestui/"
  }

  @objc(createDeepLinkURLWithQueryDictionary:)
  public class func createDeepLinkURL(queryDictionary: [String: Any]) -> URL? {
    guard let dependencies = try? getDependencies() else { return nil }

    var components = URLComponents()
    components.scheme = URLScheme.https.rawValue
    components.host = URLValues.host
    components.path = "\(URLValues.path)\(dependencies.accessTokenWallet.current?.appID ?? "")"
    components.queryItems = Self.getQueryArray(from: queryDictionary)
    return components.url
  }

  @objc(filtersNameForFilters:)
  public class func filtersName(for filters: GameRequestFilter) -> String? {
    guard let dependencies = try? getDependencies() else { return nil }

    switch filters {
    case .appUsers:
      return "app_users"
    case .appNonUsers:
      return "app_non_users"
    case .everybody:
      let graphDomain = dependencies.authenticationTokenWallet.current?.graphDomain
      if graphDomain == "gaming",
         dependencies.appAvailabilityChecker.isFacebookAppInstalled {
        return "everybody"
      } else {
        return nil
      }
    default:
      return nil
    }
  }

  @objc(actionTypeNameForActionType:)
  public class func actionTypeName(for actionType: GameRequestActionType) -> String? {
    switch actionType {
    case .send: return "send"
    case .askFor: return "askfor"
    case .turn: return "turn"
    case .invite: return "invite"
    default: return nil
    }
  }

  private class func getQueryArray(from gameRequestDictionary: [String: Any]) -> [URLQueryItem] {
    gameRequestDictionary.compactMap { key, value in
      guard let value = value as? String else { return nil }

      return URLQueryItem(name: key, value: value)
    }
  }
}

extension GameRequestURLProvider: DependentAsType {
  struct TypeDependencies {
    var accessTokenWallet: _AccessTokenProviding.Type
    var authenticationTokenWallet: _AuthenticationTokenProviding.Type
    var appAvailabilityChecker: AppAvailabilityChecker
  }

  static var configuredDependencies: TypeDependencies?

  static var defaultDependencies: TypeDependencies? = TypeDependencies(
    accessTokenWallet: AccessToken.self,
    authenticationTokenWallet: AuthenticationToken.self,
    appAvailabilityChecker: InternalUtility.shared
  )
}


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

import Foundation

@objcMembers
@objc(FBSDKGamingContext)
public final class GamingContext: NSObject {

  /// A shared object that holds data about the current user's game instance which could be solo game or multiplayer game with other users.
  @objc(currentContext) public static var current: GamingContext?

  /// A unique identifier for the current game context. This represents a specific game instance that the user is playing in.
  public let identifier: String

  /// The number of players in the current user's game instance
  public let size: Int

  public init?(identifier: String, size: Int) {
    guard !identifier.isEmpty else { return nil }

    self.identifier = identifier
    self.size = max(0, size)

    super.init()
    Self.current = self
  }
}


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

import FBSDKCoreKit
import Foundation

@objcMembers
@objc(FBSDKGamingGroupIntegration)
public final class GamingGroupIntegration: NSObject {

  let settings: SettingsProtocol
  let serviceControllerFactory: GamingServiceControllerCreating

  override convenience init() {
    self.init(
      settings: Settings.shared,
      serviceControllerFactory: GamingServiceControllerFactory()
    )
  }

  init(settings: SettingsProtocol, serviceControllerFactory: GamingServiceControllerCreating) {
    self.settings = settings
    self.serviceControllerFactory = serviceControllerFactory
  }

  public static func openGroupPage(completionHandler: @escaping GamingServiceCompletionHandler) {
    let integration = GamingGroupIntegration()
    integration.openGroupPage(completionHandler: completionHandler)
  }

  func openGroupPage(completionHandler: @escaping GamingServiceCompletionHandler) {
    let controller = serviceControllerFactory.create(serviceType: .community, pendingResult: nil) { success, _, error in
      completionHandler(success, error)
    }

    controller.call(withArgument: settings.appID)
  }
}


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

import FBSDKCoreKit
import Foundation

@objcMembers
@objc(FBSDKGamingImageUploader)
public final class GamingImageUploader: NSObject {

  private var progressHandler: GamingServiceProgressHandler?

  let factory: GamingServiceControllerCreating
  let graphRequestConnectionFactory: GraphRequestConnectionFactoryProtocol

  // Transitional singleton introduced as a way to change the usage semantics
  // from a type-based interface to an instance-based interface.
  static let shared = GamingImageUploader()

  override convenience init() {
    self.init(
      gamingServiceControllerFactory: GamingServiceControllerFactory(),
      graphRequestConnectionFactory: GraphRequestConnectionFactory()
    )
  }

  convenience init(progressHandler: GamingServiceProgressHandler?) {
    self.init(
      gamingServiceControllerFactory: GamingServiceControllerFactory(),
      graphRequestConnectionFactory: GraphRequestConnectionFactory()
    )
    self.progressHandler = progressHandler
  }

  init(
    gamingServiceControllerFactory: GamingServiceControllerCreating,
    graphRequestConnectionFactory: GraphRequestConnectionFactoryProtocol
  ) {
    factory = gamingServiceControllerFactory
    self.graphRequestConnectionFactory = graphRequestConnectionFactory
  }

  /**
   Runs an upload to a users Gaming Media Library with the given configuration

   @param configuration model object contain the content that will be uploaded
   @param completion a callback that is fired dependent on the configuration.
   Fired when the upload completes or when the users returns to the caller app
   after the media dialog is shown.
   */
  @objc(uploadImageWithConfiguration:andResultCompletion:)
  public static func uploadImage(
    with configuration: GamingImageUploaderConfiguration,
    andResultCompletion completion: @escaping GamingServiceResultCompletion
  ) {
    shared.uploadImage(
      with: configuration,
      andResultCompletion: completion
    )
  }

  func uploadImage(
    with configuration: GamingImageUploaderConfiguration,
    andResultCompletion completion: @escaping GamingServiceResultCompletion
  ) {
    uploadImage(
      with: configuration,
      completion: completion,
      andProgressHandler: nil
    )
  }

  /**
   Runs an upload to a users Gaming Media Library with the given configuration

   @param configuration model object contain the content that will be uploaded
   @param completion a callback that is fired dependent on the configuration.
   Fired when the upload completes or when the users returns to the caller app
   after the media dialog is shown.
   @param progressHandler an optional callback that is fired multiple times as
   bytes are transferred to Facebook.
   */
  @objc(uploadImageWithConfiguration:completion:andProgressHandler:)
  public static func uploadImage(
    with configuration: GamingImageUploaderConfiguration,
    completion: @escaping GamingServiceResultCompletion,
    andProgressHandler progressHandler: GamingServiceProgressHandler?
  ) {
    shared.uploadImage(
      with: configuration,
      completion: completion,
      andProgressHandler: progressHandler
    )
  }

  func uploadImage(
    with configuration: GamingImageUploaderConfiguration,
    completion completionHandler: @escaping GamingServiceResultCompletion,
    andProgressHandler progressHandler: GamingServiceProgressHandler?
  ) {
    let errorFactory = _ErrorFactory()

    if AccessToken.current == nil {
      completionHandler(
        false,
        nil,
        errorFactory.error(
          code: CoreError.errorAccessTokenRequired.rawValue,
          userInfo: nil,
          message: "A valid access token is required to upload Images",
          underlyingError: nil
        )
      )

      return
    }

    guard let imageData = configuration.image.pngData() else {
      completionHandler(
        false,
        nil,
        errorFactory.error(
          code: CoreError.errorInvalidArgument.rawValue,
          userInfo: nil,
          message: "Attempting to upload a nil image",
          underlyingError: nil
        )
      )

      return
    }

    let connection = graphRequestConnectionFactory.createGraphRequestConnection()

    let uploader = GamingImageUploader(progressHandler: progressHandler)

    connection.delegate = uploader
    InternalUtility.shared.registerTransientObject(connection.delegate as Any)

    connection.add(
      GraphRequest(
        graphPath: "me/photos",
        parameters: [
          "caption": configuration.caption ?? "",
          "picture": imageData,
        ],
        httpMethod: .post
      )
    ) { [weak self] graphConnection, result, error in
      guard let strongSelf = self else { return }

      if let graphConnectionDelegate = graphConnection?.delegate {
        InternalUtility.shared.unregisterTransientObject(graphConnectionDelegate)
      }

      if error != nil || result == nil {
        completionHandler(
          false,
          nil,
          errorFactory.error(
            code: CoreError.errorGraphRequestGraphAPI.rawValue,
            userInfo: nil,
            message: "Image upload failed",
            underlyingError: error
          )
        )
        return
      }

      let result = result as? [String: Any]

      if !configuration.shouldLaunchMediaDialog {
        completionHandler(true, result, nil)
        return
      }

      let controller = strongSelf.factory.create(
        serviceType: .mediaAsset,
        pendingResult: result,
        completion: completionHandler
      )

      controller.call(withArgument: result?["id"] as? String)
    }

    connection.start()
  }
}

// MARK: - GraphRequestConnectionDelegate

extension GamingImageUploader: GraphRequestConnectionDelegate {

  public func requestConnection(
    _ connection: GraphRequestConnecting,
    didSendBodyData bytesWritten: Int,
    totalBytesWritten: Int,
    totalBytesExpectedToWrite: Int
  ) {
    guard let progressHandler = progressHandler else { return }

    progressHandler(
      Int64(bytesWritten),
      Int64(totalBytesWritten),
      Int64(totalBytesExpectedToWrite)
    )
  }
}


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

import Foundation
import UIKit

@objcMembers
@objc(FBSDKGamingImageUploaderConfiguration)
public final class GamingImageUploaderConfiguration: NSObject {
  public private(set) var image: UIImage
  public private(set) var caption: String?
  public private(set) var shouldLaunchMediaDialog: Bool

  /**
   A model for Gaming image upload content to be shared.

   @param image the image that will be shared.
   @param caption and optional caption that will appear along side the image on Facebook.
   @param shouldLaunchMediaDialog whether or not to open the media dialog on Facebook when the upload completes.
   */

  public init(
    image: UIImage,
    caption: String?,
    shouldLaunchMediaDialog: Bool
  ) {
    self.image = image
    self.caption = caption
    self.shouldLaunchMediaDialog = shouldLaunchMediaDialog
  }
}


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

import FBSDKCoreKit
import Foundation

@objcMembers
@objc(FBSDKGamingPayload)
public final class GamingPayload: NSObject {

  public var URL: AppLinkURL
  var gameEntryData: [String: String] = [:]

  public var payload: String

  var gameRequestID: String {
    gameEntryData[GamingPayloadObserver.Keys.gamingPayloadGameRequestID.rawValue] ?? ""
  }

  public init(URL: AppLinkURL) {
    self.URL = URL
    let requestID = URL.appLinkExtras?[GamingPayloadObserver.Keys.gamingPayloadGameRequestID.rawValue] as? String ?? ""
    payload = URL.appLinkExtras?[GamingPayloadObserver.Keys.gamingPayload.rawValue] as? String ?? ""

    gameEntryData[GamingPayloadObserver.Keys.gamingPayload.rawValue] = payload
    gameEntryData[GamingPayloadObserver.Keys.gamingPayloadGameRequestID.rawValue] = requestID
  }
}


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

import Foundation

@objc(FBSDKGamingPayloadDelegate)
public protocol GamingPayloadDelegate: NSObjectProtocol {
  /**
   Delegate method will be triggered when a `GamingPayloadObserver` parses a url with a payload and game request ID
   @param payload The payload recieved in the url
   @param gameRequestID The game request ID recieved in the url
   */
  @objc optional func parsedGameRequestURLContaining(_ payload: GamingPayload, gameRequestID: String)

  /**
   Delegate method will be triggered when a `GamingPayloadObserver` parses a gaming context url with a payload and game context token ID. The current gaming context will be update with the context ID.
   @param payload The payload recieved in the url
   */
  @objc optional func parsedGamingContextURLContaining(_ payload: GamingPayload)

  /**
   Delegate method will be triggered when a `GamingPayloadObserver` parses a url with a payload and tournament ID
   @param payload The payload associated with the tournament
   @param tournamentID The tournament ID the player wants to enter
   */
  @objc optional func parsedTournamentURLContaining(_ payload: GamingPayload, tournamentID: String)
}


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

import FBSDKCoreKit
import Foundation

@objcMembers
@objc(FBSDKGamingPayloadObserver)
public final class GamingPayloadObserver: NSObject {
  public weak var delegate: GamingPayloadDelegate? {
    willSet {
      if let sharedInstance = GamingPayloadObserver.shared {
        if newValue == nil {
          ApplicationDelegate.shared.removeObserver(sharedInstance)
          GamingPayloadObserver.shared = nil
        }

        if delegate == nil { // i.e. oldValue
          ApplicationDelegate.shared.addObserver(sharedInstance)
        }
      }
    }
  }

  private static var shared: GamingPayloadObserver? = GamingPayloadObserver()

  enum Keys: String, CaseIterable {
    case gamingPayload = "payload"
    case gamingPayloadGameRequestID = "game_request_id"
    case gamingPayloadContextTokenID = "context_token_id"
    case gamingPayloadTournamentID = "tournament_id"
  }

  private override init() {}

  public convenience init(delegate: GamingPayloadDelegate?) {
    self.init()
    self.delegate = delegate
    ApplicationDelegate.shared.addObserver(self)
  }

  private func parseURLForPayloadEntryData(appLinkUrl: AppLinkURL) -> [String: String]? {
    let expectedParameters = Keys.allCases.map { $0.rawValue }
    let urlParameters = appLinkUrl.appLinkExtras
    let recievedParameters = urlParameters?.filter { expectedParameters.contains($0.key) }
    let containsPayload = recievedParameters?[Keys.gamingPayload.rawValue] != nil

    if containsPayload, recievedParameters?.keys.count ?? 0 > 1 {
      return recievedParameters as? [String: String]
    }
    return [:]
  }

  private func handleDeeplinkURLIntoApp(appLinkUrl: AppLinkURL) -> Bool {
    guard
      let delegate = delegate,
      let gameEntryData = parseURLForPayloadEntryData(appLinkUrl: appLinkUrl),
      !gameEntryData.keys.isEmpty
    else {
      return false
    }
    let payload = GamingPayload(URL: appLinkUrl)

    if
      let gameRequestID = gameEntryData[Keys.gamingPayloadGameRequestID.rawValue],
      delegate.responds(to: #selector(GamingPayloadDelegate.parsedGameRequestURLContaining(_:gameRequestID:))) {
      delegate.parsedGameRequestURLContaining?(payload, gameRequestID: gameRequestID)
      return true
    }

    if
      let gameContextTokenID = gameEntryData[Keys.gamingPayloadContextTokenID.rawValue],
      delegate.responds(to: #selector(GamingPayloadDelegate.parsedGamingContextURLContaining(_:))) {
      GamingContext.current = GamingContext(identifier: gameContextTokenID, size: 0)
      delegate.parsedGamingContextURLContaining?(payload)
      return true
    }

    if
      let tournamentID = gameEntryData[Keys.gamingPayloadTournamentID.rawValue],
      delegate.responds(to: #selector(GamingPayloadDelegate.parsedTournamentURLContaining(_:tournamentID:)))
    {
      delegate.parsedTournamentURLContaining?(payload, tournamentID: tournamentID)
      return true
    }
    return false
  }
}

extension GamingPayloadObserver: FBSDKApplicationObserving {
  public func application(
    _ application: UIApplication,
    open url: URL,
    sourceApplication: String?,
    annotation: Any?
  ) -> Bool {
    let sdkURL = AppLinkURL(url: url)
    return handleDeeplinkURLIntoApp(appLinkUrl: sdkURL)
  }
}


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

import Foundation

/**
 Main completion handling of any Gaming Service (Friend Finder, Image/Video Upload).

 @param success whether the call to the service was considered a success.
 @param error the error that occured during the service call, if any.
 */
public typealias GamingServiceCompletionHandler = (_ success: Bool, _ error: Error?) -> Void
public typealias FBSDKGamingServiceCompletionHandler = GamingServiceCompletionHandler

/**
 Main completion handling of any Gaming Service (Friend Finder, Image/Video Upload).

 @param success whether the call to the service was considered a success.
 @param result the result that was returned by the service, if any.
 @param error the error that occured during the service call, if any.
 */
public typealias GamingServiceResultCompletion = (_ success: Bool, _ result: [String: Any]?, _ error: Error?) -> Void
public typealias FBSDKGamingServiceResultCompletion = GamingServiceResultCompletion
/**
 Main completion handling of any Gaming Service (Friend Finder, Image/Video Upload).

 @param bytesSent the number of bytes sent since the last invocation
 @param totalBytesSent the total number of bytes sent
 @param totalBytesExpectedToSend the number of bytes that remain to be sent
 */
public typealias GamingServiceProgressHandler = (
  _ bytesSent: Int64,
  _ totalBytesSent: Int64,
  _ totalBytesExpectedToSend: Int64
) -> Void
public typealias FBSDKGamingServiceProgressHandler = GamingServiceProgressHandler


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

import Foundation

/// Errors to describe what went wrong with a GamingServicesDialog
public enum GamingServicesDialogError: Error {
  /// Indicates an invalid content type was used with a given dialog
  case invalidContentType

  /// Indicates a failure based on missing dialog content
  case missingContent

  /// Indicates error occured creating dialog deeplink
  case deeplinkURLCreation

  /// Indicates that the dialog was cancelled
  case cancelled
}


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

import FBSDKCoreKit
import Foundation

@objcMembers
@objc(FBSDKGamingVideoUploader)
public final class GamingVideoUploader: NSObject {

  private var totalBytesSent: UInt = 0
  private var totalBytesExpectedToSend: UInt = 0

  var fileHandle: FileHandling?
  let fileHandleFactory: FileHandleCreating
  let videoUploaderFactory: VideoUploaderCreating

  var completionHandler: GamingServiceResultCompletion?
  var progressHandler: GamingServiceProgressHandler?

  // Transitional singleton introduced as a way to change the usage semantics
  // from a type-based interface to an instance-based interface.
  // The goal is to move from:
  // ClassWithoutUnderlyingInstance -> ClassRelyingOnUnderlyingInstance -> Instance
  static let shared = GamingVideoUploader()

  init(
    fileHandleFactory: FileHandleCreating = FileHandleFactory(),
    videoUploaderFactory: VideoUploaderCreating = VideoUploaderFactory()
  ) {
    self.fileHandleFactory = fileHandleFactory
    self.videoUploaderFactory = videoUploaderFactory
  }

  private convenience init(
    fileHandle: FileHandling,
    totalBytesExpectedToSend: UInt,
    completionHandler: GamingServiceResultCompletion?,
    progressHandler: GamingServiceProgressHandler?
  ) {
    self.init()
    self.fileHandle = fileHandle
    self.totalBytesExpectedToSend = totalBytesExpectedToSend
    self.completionHandler = completionHandler
    self.progressHandler = progressHandler
  }

  /**
   Runs an upload to a users Gaming Media Library with the given configuration

   @param configuration model object contain the content that will be uploaded
   @param completion a callback that is fired when the upload completes.
   */
  @objc(uploadVideoWithConfiguration:andResultCompletion:)
  public static func uploadVideo(
    configuration: GamingVideoUploaderConfiguration,
    completion: @escaping GamingServiceResultCompletion
  ) {
    shared.uploadVideo(
      configuration: configuration,
      completion: completion
    )
  }

  func uploadVideo(
    configuration: GamingVideoUploaderConfiguration,
    completion: @escaping GamingServiceResultCompletion
  ) {
    uploadVideo(
      configuration: configuration,
      completion: completion,
      progressHandler: nil
    )
  }

  /**
   Runs an upload to a users Gaming Media Library with the given configuration

   @param configuration model object contain the content that will be uploaded
   @param completion a callback that is fired when the upload completes.
   @param progressHandler an optional callback that is fired multiple times as bytes are transferred to Facebook.
   */
  @objc(uploadVideoWithConfiguration:completion:andProgressHandler:)
  public static func uploadVideo(
    configuration: GamingVideoUploaderConfiguration,
    completion: @escaping GamingServiceResultCompletion,
    progressHandler: GamingServiceProgressHandler?
  ) {
    shared.uploadVideo(
      configuration: configuration,
      completion: completion,
      progressHandler: progressHandler
    )
  }

  func uploadVideo(
    configuration: GamingVideoUploaderConfiguration,
    completion: @escaping GamingServiceResultCompletion,
    progressHandler: GamingServiceProgressHandler?
  ) {
    let errorFactory = _ErrorFactory()

    guard AccessToken.current != nil else {
      completion(
        false,
        nil,
        errorFactory.error(
          code: CoreError.errorAccessTokenRequired.rawValue,
          userInfo: nil,
          message: "A valid access token is required to upload Images",
          underlyingError: nil
        )
      )

      return
    }

    guard
      let fileHandle = try? fileHandleFactory.fileHandleForReading(from: configuration.videoURL),
      fileHandle.fb_seekToEndOfFile() != 0
    else {
      completion(
        false,
        nil,
        errorFactory.error(
          code: CoreError.errorInvalidArgument.rawValue,
          userInfo: nil,
          message: "Attempting to upload an empty video file",
          underlyingError: nil
        )
      )

      return
    }

    let fileSize = UInt(fileHandle.fb_seekToEndOfFile())

    let uploader = GamingVideoUploader(
      fileHandle: fileHandle,
      totalBytesExpectedToSend: fileSize,
      completionHandler: completion,
      progressHandler: progressHandler
    )

    InternalUtility.shared.registerTransientObject(uploader)

    let videoUploader = videoUploaderFactory.create(
      videoName: configuration.videoURL.lastPathComponent,
      videoSize: fileSize,
      parameters: [:],
      delegate: uploader
    )

    videoUploader.start()
  }

  private func safelyComplete(
    success: Bool,
    error: Error?,
    result: [String: Any]?
  ) {
    var finalError = error

    if !success,
       error == nil {
      finalError = _ErrorFactory().error(
        code: CoreError.errorUnknown.rawValue,
        userInfo: nil,
        message: "Video upload was unsuccessful, but no error was thrown.",
        underlyingError: nil
      )
    }

    completionHandler?(success, result, finalError)

    InternalUtility.shared.unregisterTransientObject(self)
  }

  private func safelyHandleProgress(totalBytesSent: UInt) {
    guard let progressHandler = progressHandler else { return }

    let bytesSent = totalBytesSent - self.totalBytesSent
    self.totalBytesSent = totalBytesSent

    progressHandler(
      Int64(bytesSent),
      Int64(self.totalBytesSent),
      Int64(totalBytesExpectedToSend)
    )
  }
}

// MARK: - VideoUploaderDelegate

extension GamingVideoUploader: VideoUploaderDelegate {

  func videoChunkData(
    for videoUploader: VideoUploader,
    startOffset: UInt,
    endOffset: UInt
  ) -> Data? {
    let chunkSize = endOffset - startOffset
    guard let fileHandle = fileHandle else { return nil }

    fileHandle.fb_seek(toFileOffset: UInt64(startOffset))
    let videoChunkData = fileHandle.fb_readData(ofLength: Int(chunkSize))
    guard videoChunkData.count == chunkSize else {
      return nil
    }

    safelyHandleProgress(totalBytesSent: startOffset)

    return videoChunkData
  }

  func videoUploader(
    _ videoUploader: VideoUploader,
    didCompleteWithResults results: [String: Any]
  ) {
    safelyHandleProgress(totalBytesSent: totalBytesExpectedToSend)

    var serverSuccess = false
    let success = results["success"]

    if let stringSuccess = success as? NSString {
      serverSuccess = stringSuccess.boolValue
    } else if let numberSuccess = success as? NSNumber {
      serverSuccess = numberSuccess.boolValue
    }

    safelyComplete(
      success: serverSuccess,
      error: nil,
      result: ["video_id": results["video_id"] ?? ""]
    )
  }

  func videoUploader(
    _ videoUploader: VideoUploader,
    didFailWithError error: Error
  ) {
    safelyComplete(
      success: false,
      error: error,
      result: nil
    )
  }
}


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

import Foundation

@objcMembers
@objc(FBSDKGamingVideoUploaderConfiguration)
public final class GamingVideoUploaderConfiguration: NSObject {
  public private(set) var videoURL: URL
  public private(set) var caption: String?

  /**
   A model for Gaming video upload content to be shared.
   @param videoURL a url to the videos location on local disk.
   @param caption and optional caption that will appear along side the video on Facebook.
   */

  public init(videoURL: URL, caption: String?) {
    self.videoURL = videoURL
    self.caption = caption
  }
}