/*
 * 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 that allows the user to join a tournament.
public final class JoinTournamentDialog: GamingWebDialog<JoinTournamentDialogSuccess> {

  private let viewName = "join_tournament"
  public var tournamentID: String?
  public var payload: String?

  enum Keys {
    static let tournamentID = "tournament_id"
    static let payload = "payload"
  }

  public init() {
    super.init(name: viewName)
  }

  /**
   Shows a dialog prompting the user to join a specific, active tournament.
   - Parameters:
    - tournamentID: ID for the tournament to be joined.
    - payload: Optional payload for the tournament.
   */
  public func showSpecific(
    tournamentID: String,
    payload: String?,
    completion: @escaping (Result<JoinTournamentDialogSuccess, Error>) -> Void
  ) {
    parameters = [Keys.tournamentID: tournamentID]
    if let payload = payload {
      parameters[Keys.payload] = payload
    }
    show(completion: completion)
  }

  /**
   Shows a dialog listing joinable tournaments suggested by Facebook.
   - Parameters:
    - payload: Payload for the tournament.
   */
  public func showSuggested(
    payload: String?,
    completion: @escaping (Result<JoinTournamentDialogSuccess, Error>) -> Void
  ) {
    if let payload = payload {
      parameters[Keys.payload] = payload
    }
    show(completion: completion)
  }
}


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

import Foundation

/**
 The result of a successful `JoinTournamentDialog`.
 */
public struct JoinTournamentDialogSuccess: GamingWebDialogSuccess {
  /// ID of the joined tournament.
  public let tournamentID: String
  /// Optional payload for the tournament.
  public let payload: String?

  public init(_ dict: [String: Any]) throws {
    guard let tournamentID = dict[JoinTournamentDialog.Keys.tournamentID] as? String,
          !tournamentID.isEmpty else { throw GamingServicesDialogError.cancelled }

    self.tournamentID = tournamentID
    payload = dict[JoinTournamentDialog.Keys.payload] as? String
  }
}


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

import Foundation

import FBSDKCoreKit

public enum ShareTournamentDialogError: Error {
  case invalidAccessToken
  case invalidAuthToken
  case invalidTournamentID
  case unableToCreateDialogUrl
  case unknownBridgeError
  case errorMessage(String)
  case bridgeError(Error)
}

public final class ShareTournamentDialog: NSObject, URLOpening {

  let gamingGraphDomain = "gaming"
  var bridgeURLOpener: BridgeAPIRequestOpening = _BridgeAPI.shared
  weak var delegate: ShareTournamentDialogDelegate?
  var currentConfiguration: TournamentConfig?
  var tournamentToUpdate: Tournament?

  init(
    delegate: ShareTournamentDialogDelegate,
    urlOpener: BridgeAPIRequestOpening
  ) {
    self.delegate = delegate
    bridgeURLOpener = urlOpener
  }

  /**
   Creates a share a dialog that can be used to share a score using a new tournament configuration
   or an existing tournament

   - Parameter delegate: The delegate for the dialog to be invoked in case of error, cancellation or completion
   */
  public convenience init(
    delegate: ShareTournamentDialogDelegate
  ) {
    self.init(delegate: delegate, urlOpener: _BridgeAPI.shared)
  }

  /**
   Attempts to show the share dialog to share an existing tournament
   - Parameter score: A score to share in the tournament could be a numeric score or time interval
      dependent on the given tournament score type
   - Parameter tournamentID: The ID of the  tournament to share and update with the given score
   - throws  Will throw if an error occurs when attempting to show the dialog
   */
  public func show(score: Int, tournamentID: String) throws {
    guard !tournamentID.isEmpty else {
      throw ShareTournamentDialogError.invalidTournamentID
    }
    try show(score: score, tournament: Tournament(identifier: tournamentID))
  }

  /**
   Attempts to show the share dialog to share an existing tournament
   - Parameter score: A score to share in the tournament could be a numeric score or time interval
      dependent on the given tournament score type
   - Parameter tournament: The tournament to share and update with the given score
   - throws  Will throw if an error occurs when attempting to show the dialog
   */
  public func show(score: Int, tournament: Tournament) throws {
    tournamentToUpdate = tournament
    guard let accessToken = AccessToken.current else {
      throw ShareTournamentDialogError.invalidAccessToken
    }
    guard let authToken = AuthenticationToken.current, authToken.graphDomain == gamingGraphDomain else {
      throw ShareTournamentDialogError.invalidAuthToken
    }
    guard
      let url = ShareTournamentDialogURLBuilder
        .update(tournament)
        .url(withPathAppID: accessToken.appID, score: score)
    else {
      throw ShareTournamentDialogError.unableToCreateDialogUrl
    }

    bridgeURLOpener.open(url, sender: self) { [weak self] success, error in
      guard let strongSelf = self else {
        return
      }
      if let error = error {
        strongSelf.delegate?.didFail(withError: ShareTournamentDialogError.bridgeError(error), dialog: strongSelf)
        return
      }
      if !success {
        strongSelf.delegate?.didFail(withError: ShareTournamentDialogError.unknownBridgeError, dialog: strongSelf)
      }
    }
  }

  /**
   Attempts to show the share dialog to share a newly created tournnament

   - Parameter initialScore: An initial score to share, could be a numeric score or time interval
      dependent on the tournament configuration
   - Parameter config: The tournament configuration used to create a new tournament
   - throws  Will throw if an error occurs when attempting to show the dialog
   */
  public func show(initialScore: Int, config configuration: TournamentConfig) throws {
    currentConfiguration = configuration
    guard let accessToken = AccessToken.current else {
      throw ShareTournamentDialogError.invalidAccessToken
    }
    guard let authToken = AuthenticationToken.current, authToken.graphDomain == gamingGraphDomain else {
      throw ShareTournamentDialogError.invalidAuthToken
    }
    guard
      let url = ShareTournamentDialogURLBuilder
        .create(configuration)
        .url(withPathAppID: accessToken.appID, score: initialScore)
    else {
      throw ShareTournamentDialogError.unableToCreateDialogUrl
    }

    bridgeURLOpener.open(url, sender: self) { [weak self] success, error in
      guard let strongSelf = self else {
        return
      }
      if let error = error {
        strongSelf.delegate?.didFail(withError: ShareTournamentDialogError.bridgeError(error), dialog: strongSelf)
        return
      }
      if !success {
        strongSelf.delegate?.didFail(withError: ShareTournamentDialogError.unknownBridgeError, dialog: strongSelf)
      }
    }
  }

  func isTournamentURL(url: URL) -> Bool {
    if let scheme = url.scheme,
       let host = url.host,
       let appID = Settings.shared.appID {
      return scheme.hasPrefix("fb\(appID)") && host.elementsEqual("instant_tournament")
    }
    return false
  }

  func parseTournamentURL(url: URL) -> Bool {
    let components = URLComponents(url: url, resolvingAgainstBaseURL: true)
    guard let queryItems = components?.queryItems else {
      delegate?.didCancel(dialog: self)
      return false
    }
    let tournamentIDQueryItem = queryItems.filter { item in
      item.name.elementsEqual("tournament_id")
    }
    let errorMessageQueryItem = queryItems.filter { item in
      item.name.elementsEqual("error_message")
    }
    if errorMessageQueryItem.count == 1, let errorMessage = errorMessageQueryItem.first?.value {
      delegate?.didFail(withError: ShareTournamentDialogError.errorMessage(errorMessage), dialog: self)
    }
    guard
      tournamentIDQueryItem.count == 1,
      let tournamentID = tournamentIDQueryItem.first?.value
    else {
      return false
    }
    if let currentConfiguration = currentConfiguration {
      let createdTournament = Tournament(identifier: tournamentID, configuration: currentConfiguration)
      delegate?.didComplete(dialog: self, tournament: createdTournament)
    }
    if let tournamentToUpdate = tournamentToUpdate, tournamentToUpdate.identifier == tournamentID {
      delegate?.didComplete(dialog: self, tournament: tournamentToUpdate)
    }
    return false
  }

  // MARK: URLOpening

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

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

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

  public func applicationDidBecomeActive(_ application: UIApplication) {}
}


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

public protocol ShareTournamentDialogDelegate: AnyObject {
  func didComplete(dialog: ShareTournamentDialog, tournament: Tournament)
  func didFail(withError error: Error, dialog: ShareTournamentDialog)
  func didCancel(dialog: ShareTournamentDialog)
}


/*
 * 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(FBSDKShowable)
public protocol Showable {
  func show() -> Bool
}

extension ChooseContextDialog: Showable {}
extension CreateContextDialog: Showable {}


/*
 * 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 switchAsync cross play request.
@objcMembers
@objc(FBSDKSwitchContextContent)
public final class SwitchContextContent: NSObject, ValidatableProtocol {

  /**
   The context token of the existing context for which this request is being made.
   @return The context token of the existing context
   */
  var contextTokenID: String

  @objc(initDialogContentWithContextID:)
  public init(contextID: String) {
    contextTokenID = contextID
  }

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

  // MARK: - SharingValidation

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


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

import Foundation

import FBSDKCoreKit

/// A dialog to switch the current gaming context through a web view
@objcMembers
@objc(FBSDKSwitchContextDialog)
public final class SwitchContextDialog: ContextWebDialog, Showable {

  enum Keys {
    static let contextID = "context_id"
  }

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

  let windowFinder: _WindowFinding
  var frame: CGRect {
    createWebDialogFrame(
      withWidth: DialogFrame.width,
      height: DialogFrame.height,
      windowFinder: windowFinder
    )
  }

  /**
   Creates a switch context web dialog with content and a delegate.

   - Parameter content: The content for the switch context dialog
   - Parameter windowFinder: The application window finder that provides the window to display the dialog
   - Parameter delegate: The receiver's delegate used to let the receiver know a context switch was successful or failed
   */
  public init(
    content: SwitchContextContent,
    windowFinder: _WindowFinding,
    delegate: ContextDialogDelegate
  ) {
    self.windowFinder = windowFinder
    super.init(delegate: delegate, dialogContent: content)
  }

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

    guard let content = dialogContent as? SwitchContextContent else {
      delegate?.contextDialog(
        self,
        didFailWithError: GamingServicesDialogError.invalidContentType
      )
      return false
    }

    currentWebDialog = _WebDialog(
      name: "context",
      parameters: [Keys.contextID: content.contextTokenID],
      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 FBSDKCoreKit

struct SwitchContextDialogFactory: SwitchContextDialogMaking {

  let tokenProvider: _AccessTokenProviding.Type

  init(tokenProvider: _AccessTokenProviding.Type) {
    self.tokenProvider = tokenProvider
  }

  func makeSwitchContextDialog(
    content: SwitchContextContent,
    windowFinder: _WindowFinding,
    delegate: ContextDialogDelegate
  ) throws -> Showable? {
    guard tokenProvider.current != nil else {
      throw ContextDialogPresenterError.invalidAccessToken
    }

    return SwitchContextDialog(
      content: content,
      windowFinder: windowFinder,
      delegate: delegate
    )
  }
}


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

public enum TournamentDecodingError: Error {
  case invalidExpirationDate
  case invalidScoreType
}

public struct Tournament: Codable {

  /// The unique ID that is associated with this tournament.
  public internal(set) var identifier: String

  /**
   Timestamp when the tournament ends.
   If the expiration is in the past, then the tournament is already finished and has expired.
   */
  public internal(set) var endTime: Date?

  /// Title of the tournament provided upon the creation of the tournament.
  public internal(set) var title: String?

  /// Payload of the tournament provided upon the creation of the tournament.
  public var payload: String?

  init(
    identifier: String,
    configuration: TournamentConfig
  ) {
    self.init(identifier: identifier, title: configuration.title, payload: configuration.payload)
    if let expirationTimeStamp = configuration.endTime {
      endTime = Date(timeIntervalSince1970: expirationTimeStamp)
    }
  }

  init(
    identifier: String,
    endTime: Date? = nil,
    title: String? = nil,
    payload: String? = nil
  ) {
    self.identifier = identifier
    self.endTime = endTime
    self.title = title
    self.payload = payload
  }

  public init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)

    identifier = try container.decode(String.self, forKey: .identifier)
    let dateStamp = try container.decode(String.self, forKey: .endTime)

    if let expirationDate = DateFormatter.format(ISODateString: dateStamp) {
      endTime = expirationDate
    } else {
      throw TournamentDecodingError.invalidExpirationDate
    }

    title = try container.decodeIfPresent(String.self, forKey: .title)
    payload = try container.decodeIfPresent(String.self, forKey: .payload)
  }

  enum CodingKeys: String, CodingKey {
    case identifier = "id"
    case endTime = "tournament_end_time"
    case title = "tournament_title"
    case payload = "tournament_payload"
  }
}


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

public struct TournamentConfig {

  /// Title of the tournament
  public var title: String?

  /**
   Timestamp when the tournament ends.
   If the expiration is in the past, then the tournament is already finished and has expired.
   */
  public var endTime: TimeInterval?

  /// The type of score the tournament accepts and ranks. See enum `ScoreType`
  public var scoreType: TournamentScoreType?

  /// The sort order of the scores for the tournament
  public var sortOrder: TournamentSortOrder?

  /// Payload of the tournament
  public var payload: String?

  /// The image associated with the tournament
  public var image: UIImage?

  /// Creates a new tournament configuration with the given parameters. Passing the created tournament to the `ShareTournamentDialog.show(score:tournamentConfig:)` will share and create the tournament server side.
  ///
  /// - Parameter title: Text title for the tournament
  /// - Parameter endTime: A date representing the end time for the tournament. if not specified, the default is a week after creation date.
  /// - Parameter scoreType: A score tyoe for the formatting of the scores in the tournament leaderboard. See enum `TournamentScoreType`, if not specified, the default is `TournamentScoreType.numeric`.
  /// - Parameter sortOrder: A sort order for the ordering of which score is best in the tournament. See enum `TournamentSortOrder`, if not specified, the default is `TournamentSortOrder.descending`.
  /// - Parameter image: An image that will be associated with the tournament and included in any posts.
  /// - Parameter payload: Optional data to attach to the update. All game sessions launched from the update will be able to access this blob. Must be less than or equal to 1000 characters when stringified.
  ///
  public init(
    title: String? = nil,
    endTime: Date? = nil,
    scoreType: TournamentScoreType? = nil,
    sortOrder: TournamentSortOrder? = nil,
    image: UIImage? = nil,
    payload: String? = nil
  ) {
    self.title = title
    self.endTime = endTime?.timeIntervalSince1970
    self.scoreType = scoreType
    self.sortOrder = sortOrder
    self.image = image
    self.payload = payload
  }
}


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

public enum TournamentFetchError: Error {
  case server(Error)
  case decoding
  case invalidAuthToken
  case invalidAccessToken
}

public final class TournamentFetcher {

  let graphRequestFactory: GraphRequestFactoryProtocol
  let gamingGraphDomain = "gaming"

  /// Creates the TournamentFetcher
  public convenience init() {
    self.init(graphRequestFactory: GraphRequestFactory())
  }

  init(graphRequestFactory: GraphRequestFactoryProtocol) {
    self.graphRequestFactory = graphRequestFactory
  }

  /**
      Attempts to fetch all the tournaments where the current logged in user is a participant ;

   - Parameter completionHandler: The caller's completion handler to invoke once the graph request is complete
   */
  public func fetchTournaments(completionHandler: @escaping (Result<[Tournament], TournamentFetchError>) -> Void) {
    guard let authToken = AuthenticationToken.current, authToken.graphDomain == gamingGraphDomain else {
      return completionHandler(.failure(TournamentFetchError.invalidAuthToken))
    }

    guard let accessToken = AccessToken.current else {
      return completionHandler(.failure(TournamentFetchError.invalidAccessToken))
    }

    let request = graphRequestFactory.createGraphRequest(
      withGraphPath: "\(accessToken.userID)/tournaments",
      parameters: [:]
    )

    request.start { _, result, error in
      if let error = error {
        completionHandler(.failure(.server(error)))
        return
      }
      guard
        let result = result as? [String: Any],
        let data = try? JSONSerialization.data(withJSONObject: result, options: []),
        let graphAPIResponse = try? JSONDecoder().decode(GraphAPIResponse<[Tournament]>.self, from: data)
      else {
        completionHandler(.failure(.decoding))
        return
      }
      completionHandler(.success(graphAPIResponse.data))
    }
  }
}


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

import FBSDKCoreKit

public enum TournamentUpdaterError: Error {
  case server(Error)
  case decoding
  case invalidAuthToken
  case invalidAccessToken
  case invalidTournamentID
}

/// A graph request wrapper to update a tournament
public final class TournamentUpdater {

  enum GraphRequest {
    static let gamingGraphDomain = "gaming"
    static let scoreParameterKey = "score"

    static func path(identifier: String) -> String {
      "\(identifier)/update_score"
    }
  }

  let graphRequestFactory: GraphRequestFactoryProtocol

  /// Creates the TournamentUpdater
  public convenience init() {
    self.init(graphRequestFactory: GraphRequestFactory())
  }

  init(graphRequestFactory: GraphRequestFactoryProtocol) {
    self.graphRequestFactory = graphRequestFactory
  }

  /**
   Updates the given tournament with the given score

   - Parameter tournamentID: The ID of the tournament you want to update
   - Parameter score: The new score to update within the tournament
   - Parameter completionHandler: The caller's completion handler to invoke once the graph request is complete
   */

  public func update(
    tournamentID: String,
    score: Int,
    completionHandler: @escaping (Result<Bool, TournamentUpdaterError>) -> Void
  ) {
    guard !tournamentID.isEmpty else {
      return completionHandler(.failure(TournamentUpdaterError.invalidTournamentID))
    }
    update(tournament: Tournament(identifier: tournamentID), score: score, completionHandler: completionHandler)
  }

  /**
   Updates the given tournament with the given score

   - Parameter tournament: The tournament you want to update
   - Parameter score: The new score to update within the tournament
   - Parameter completionHandler: The caller's completion handler to invoke once the graph request is complete
   */

  public func update(
    tournament: Tournament,
    score: Int,
    completionHandler: @escaping (Result<Bool, TournamentUpdaterError>) -> Void
  ) {
    guard let authToken = AuthenticationToken.current, authToken.graphDomain == GraphRequest.gamingGraphDomain else {
      return completionHandler(.failure(TournamentUpdaterError.invalidAuthToken))
    }

    let parameters = [GraphRequest.scoreParameterKey: score]
    let request = graphRequestFactory.createGraphRequest(
      withGraphPath: GraphRequest.path(identifier: tournament.identifier),
      parameters: parameters as [String: Any],
      httpMethod: .post
    )

    request.start { _, result, error in
      if let error = error {
        completionHandler(.failure(.server(error)))
        return
      }
      guard
        let result = result as? [String: Bool],
        let data = try? JSONSerialization.data(withJSONObject: result, options: []),
        let serverResults = try? JSONDecoder().decode(ServerResult.self, from: data),
        serverResults.success
      else {
        completionHandler(.failure(.decoding))
        return
      }
      completionHandler(.success(serverResults.success))
    }
  }
}


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

struct ChooseContextDialogFactory: ChooseContextDialogMaking {
  func makeChooseContextDialog(
    content: ChooseContextContent,
    delegate: ContextDialogDelegate
  ) -> Showable {
    ChooseContextDialog(content: content, delegate: delegate)
  }
}


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

protocol ChooseContextDialogMaking {
  func makeChooseContextDialog(content: ChooseContextContent, delegate: ContextDialogDelegate) -> Showable
}


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

struct CreateContextDialogFactory: CreateContextDialogMaking {
  private var tokenProvider: _AccessTokenProviding.Type

  init(tokenProvider: _AccessTokenProviding.Type) {
    self.tokenProvider = tokenProvider
  }

  func makeCreateContextDialog(
    content: CreateContextContent,
    windowFinder: _WindowFinding,
    delegate: ContextDialogDelegate
  ) throws -> Showable? {
    guard tokenProvider.current != nil else {
      throw ContextDialogPresenterError.invalidAccessToken
    }

    return CreateContextDialog(
      content: content,
      windowFinder: windowFinder,
      delegate: delegate
    )
  }
}


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

protocol CreateContextDialogMaking {
  func makeCreateContextDialog(
    content: CreateContextContent,
    windowFinder: _WindowFinding,
    delegate: ContextDialogDelegate
  ) throws -> Showable?
}


/*
 * 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 create a CustomUpdateContent object
public enum CustomUpdateContentError: Error {
  case notInGameContext
  case invalidMessage
  case invalidMedia
  case invalidImage
}


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

import UIKit

public final class CustomUpdateContentImage {

  var message: String
  var image: UIImage?
  var ctaText: String?
  var payload: String?
  var messageLocalization: [String: String]
  var ctaLocalization: [String: String]

  /**
   Init method for a custom update content with an image

   - Parameters:
    - message:  The message to be display in the update
    - image: The image to display in the update
    - cta: The text to display in the action button for the update
    - payload: The payload string that will be passed backed when the receiver interacts with the update
    - messageLocalization: A dictionary of any Localization that should be applied to the message
    - ctaLocalization: A dictionary of any Localization that should be applied to the CTA
   */
  public init(
    message: String,
    image: UIImage,
    cta: String? = nil,
    payload: String? = nil,
    messageLocalization: [String: String] = [:],
    ctaLocalization: [String: String] = [:]
  ) {
    self.message = message
    self.image = image
    ctaText = cta
    self.payload = payload
    self.messageLocalization = messageLocalization
    self.ctaLocalization = ctaLocalization
  }
}


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

import UIKit

public final class CustomUpdateContentMedia {

  var message: String
  var media: URLMedia?
  var ctaText: String?
  var payload: String?
  var messageLocalization: [String: String]
  var ctaLocalization: [String: String]

  /**
   initializer method for a custom update content with media

   - Parameters:
    - message:  The message to display in the update
    - media: The media to display with the message
    - cta: The text to display in the action button for the update
    - payload: This is the payload string that will be passed backed when the receiver interacts with the sent message
    - messageLocalization: A dictionary of any Localization that should be applied to the message
    - ctaLocalization: A dictionary of any Localization that should be applied to the CTA
   */
  public init(
    message: String,
    media: URLMedia,
    cta: String? = nil,
    payload: String? = nil,
    messageLocalization: [String: String] = [:],
    ctaLocalization: [String: String] = [:]
  ) {
    self.message = message
    ctaText = cta
    self.media = media
    self.payload = payload
    self.messageLocalization = messageLocalization
    self.ctaLocalization = ctaLocalization
  }
}


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

final class CustomUpdateGraphAPIContentRemote: Codable {

  var contextTokenID: String
  var text: CustomUpdateLocalizedText
  var media: CustomUpdateMedia?
  var cta: CustomUpdateLocalizedText?
  var image: Data?
  var data: String?

  init(customUpdateContentMedia: CustomUpdateContentMedia) throws {
    guard let currentContextID = GamingContext.current?.identifier else {
      throw CustomUpdateContentError.notInGameContext
    }

    guard let message = CustomUpdateLocalizedText(
      defaultString: customUpdateContentMedia.message,
      localizations: customUpdateContentMedia.messageLocalization
    ) else {
      throw CustomUpdateContentError.invalidMessage
    }

    guard
      let media = customUpdateContentMedia.media,
      let customUpdateMedia = CustomUpdateMedia(media: media)
    else {
      throw CustomUpdateContentError.invalidMedia
    }

    contextTokenID = currentContextID
    text = message
    cta = CustomUpdateLocalizedText(
      defaultString: customUpdateContentMedia.ctaText ?? "",
      localizations: customUpdateContentMedia.ctaLocalization
    )
    data = customUpdateContentMedia.payload
    self.media = customUpdateMedia
  }

  init(customUpdateContentImage: CustomUpdateContentImage) throws {
    guard let currentContextID = GamingContext.current?.identifier else {
      throw CustomUpdateContentError.notInGameContext
    }

    guard let message = CustomUpdateLocalizedText(
      defaultString: customUpdateContentImage.message,
      localizations: customUpdateContentImage.messageLocalization
    ) else {
      throw CustomUpdateContentError.invalidMessage
    }

    guard
      let imageData = customUpdateContentImage.image?.pngData()
    else {
      throw CustomUpdateContentError.invalidImage
    }

    contextTokenID = currentContextID
    text = message
    cta = CustomUpdateLocalizedText(
      defaultString: customUpdateContentImage.ctaText ?? "",
      localizations: customUpdateContentImage.ctaLocalization
    )
    data = customUpdateContentImage.payload
    image = imageData
  }

  enum CodingKeys: String, CodingKey {
    case contextTokenID = "context_token_id"
    case text
    case cta
    case image
    case media
    case data
  }
}


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

import FBSDKCoreKit
import Foundation

struct ServerResult: Codable {
  var success: Bool
}

/// Errors representing a failure to send a custom update graph request
public enum CustomUpdateGraphRequestError: Error {
  case server(Error)
  case invalidAccessToken
  case contentParsing
  case decoding
}

public final class CustomUpdateGraphRequest {

  public let graphRequestFactory: GraphRequestFactoryProtocol
  let graphPath = "me/custom_update"
  let gamingGraphDomain = "gaming"
  let base64EncodedImageHeader = "data:image/png;base64,"

  public init() {
    graphRequestFactory = GraphRequestFactory()
  }

  public init(graphRequestFactory: GraphRequestFactoryProtocol = GraphRequestFactory()) {
    self.graphRequestFactory = graphRequestFactory
  }

  /**
    This will attempt to send a custom update graph request with the given content

   - Parameters:
    - content: A media custom update request content
    - completionHandler:  A completion handler to be invoked with the graph request success result or error
   */
  public func request(
    content: CustomUpdateContentMedia,
    completionHandler: @escaping (Result<Bool, CustomUpdateGraphRequestError>) -> Void
  ) throws {
    guard let authToken = AuthenticationToken.current, authToken.graphDomain == gamingGraphDomain else {
      throw CustomUpdateGraphRequestError.invalidAccessToken
    }

    let remoteContent = try CustomUpdateGraphAPIContentRemote(customUpdateContentMedia: content)
    let parameters = try parameterDictionary(content: remoteContent)
    let request = graphRequestFactory.createGraphRequest(
      withGraphPath: graphPath,
      parameters: parameters,
      httpMethod: HTTPMethod.post
    )

    try start(request: request, completion: completionHandler)
  }

  /**
    This will attempt to send a custom update graph request with the given content

   - Parameters:
    - content: An image custom update request content
    - completionHandler:  A completion handler to be invoked if  with the graph request success result or error
   */
  public func request(
    content: CustomUpdateContentImage,
    completionHandler: @escaping (Result<Bool, CustomUpdateGraphRequestError>) -> Void
  ) throws {
    guard let authToken = AuthenticationToken.current, authToken.graphDomain == gamingGraphDomain else {
      throw CustomUpdateGraphRequestError.invalidAccessToken
    }

    let remoteContent = try CustomUpdateGraphAPIContentRemote(customUpdateContentImage: content)
    let parameters = try parameterDictionary(content: remoteContent)
    let request = graphRequestFactory.createGraphRequest(
      withGraphPath: graphPath,
      parameters: parameters,
      httpMethod: HTTPMethod.post
    )

    try start(request: request, completion: completionHandler)
  }

  private func start(
    request: GraphRequestProtocol,
    completion: @escaping (Result<Bool, CustomUpdateGraphRequestError>) -> Void
  )
  throws {
    request.start { _, result, error in
      if let error = error {
        return completion(.failure(.server(error)))
      }
      guard
        let result = result as? [String: Bool],
        let data = try? JSONSerialization.data(withJSONObject: result, options: []),
        let serverResult = try? JSONDecoder().decode(ServerResult.self, from: data)
      else {
        return completion(.failure(.decoding))
      }
      completion(.success(serverResult.success))
    }
  }

  private func parameterDictionary(content: CustomUpdateGraphAPIContentRemote) throws -> [String: Any] {
    let encodedContent = try JSONEncoder().encode(content)
    guard var parameters = try JSONSerialization.jsonObject(
      with: encodedContent,
      options: .allowFragments
    ) as? [String: Any] else {
      throw _ErrorFactory().invalidArgumentError(
        name: "CustomUpdateContent",
        value: content,
        message: "Custom Update Content is invalid please check parameters.",
        underlyingError: nil
      )
    }

    parameters["text"] = try NSString(
      data: JSONEncoder().encode(content.text),
      encoding: String.Encoding.utf8.rawValue
    )

    if let cta = content.cta {
      parameters["cta"] = try NSString(
        data: JSONEncoder().encode(cta),
        encoding: String.Encoding.utf8.rawValue
      )
    }

    if let media = content.media {
      parameters["media"] = try NSString(
        data: JSONEncoder().encode(media),
        encoding: String.Encoding.utf8.rawValue
      )
    }

    if let imageData = content.image {
      parameters["image"] = base64EncodedImageHeader + imageData.base64EncodedString()
    }

    return parameters
  }
}


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

import Foundation

final class CustomUpdateLocalizedText: NSObject, Codable {
  var defaultString: String
  var localizations: [String: String]

  init?(defaultString: String, localizations: [String: String]) {
    if defaultString.isEmpty {
      return nil
    }

    self.defaultString = defaultString
    self.localizations = localizations
  }

  enum CodingKeys: String, CodingKey {
    case defaultString = "default"
    case localizations
  }
}


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

import Foundation

struct CustomUpdateMedia: Codable, Equatable {

  var gif: FacebookGIF?
  var video: FacebookVideo?

  init?(media: URLMedia) {
    if let video = media as? FacebookVideo {
      self.video = video
      return
    }

    guard let gif = media as? FacebookGIF,
          video == nil
    else {
      return nil
    }
    self.gif = gif
  }
}


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

import Foundation

enum DateFormatter {
  private static var isoFormatter = ISO8601DateFormatter()

  static func format(ISODateString date: String) -> Date? {
    isoFormatter.date(from: date)
  }
}


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

@dynamicMemberLookup
protocol DependentAsObject: AnyObject {
  associatedtype ObjectDependencies

  var configuredDependencies: ObjectDependencies? { get set }
  var defaultDependencies: ObjectDependencies? { get }

  func setDependencies(_ dependencies: ObjectDependencies)

  #if DEBUG
  func resetDependencies()
  #endif
}

extension DependentAsObject {
  func setDependencies(_ dependencies: ObjectDependencies) {
    configuredDependencies = dependencies
  }

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

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

    return dependencies
  }

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


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

@dynamicMemberLookup
protocol DependentAsType {
  associatedtype TypeDependencies

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

  static func setDependencies(_ dependencies: TypeDependencies)

  #if DEBUG
  static func resetDependencies()
  #endif
}

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

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

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

    return dependencies
  }

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


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

import Foundation

public struct FacebookGIF: Codable, Equatable, URLMedia {
  var url: URL

  /**
   Initializer for a gif url media

    - Parameters:
     - url: The url that represents the gif on the facebook platform
    */
  public init(withUrl url: URL) {
    self.url = 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

public struct FacebookVideo: Codable, Equatable, URLMedia {
  var url: URL

  /**
   Initializer for a video url media

   - Parameters:
   - url: The url that represents the video on the facebook platform
   */
  public init(withUrl url: URL) {
    self.url = 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

extension FileHandle: FileHandling {
  func fb_seekToEndOfFile() -> UInt64 {
    seekToEndOfFile()
  }

  func fb_seek(toFileOffset offset: UInt64) {
    seek(toFileOffset: offset)
  }

  func fb_readData(ofLength length: Int) -> Data {
    readData(ofLength: length)
  }
}


/*
 * 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 FileHandleCreating {
  func fileHandleForReading(from url: URL) throws -> FileHandling
}


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

final class FileHandleFactory: NSObject, FileHandleCreating {
  func fileHandleForReading(from url: URL) throws -> FileHandling {
    try FileHandle(forReadingFrom: 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

protocol FileHandling {
  func fb_seekToEndOfFile() -> UInt64
  func fb_seek(toFileOffset offset: UInt64)
  func fb_readData(ofLength length: Int) -> Data
}


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

import Foundation

protocol GameRequestURLProviding {
  static func createDeepLinkURL(queryDictionary: [String: Any]) -> URL?
  static func filtersName(for filters: GameRequestFilter) -> String?
  static func actionTypeName(for actionType: GameRequestActionType) -> String?
}

extension GameRequestURLProvider: GameRequestURLProviding {}


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

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

extension _Logger: GamingLogging {}


/*
 * 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 GamingServiceController: NSObject {

  private let serviceType: GamingServiceType
  private var completionHandler: GamingServiceResultCompletion?
  private let pendingResult: [String: Any]
  private let urlOpener: URLOpener
  private let settings: SettingsProtocol

  convenience init(
    serviceType: GamingServiceType,
    pendingResult: [String: Any],
    completionHandler completion: @escaping GamingServiceResultCompletion
  ) {
    self.init(
      serviceType: serviceType,
      pendingResult: pendingResult,
      urlOpener: _BridgeAPI.shared,
      settings: Settings.shared,
      completionHandler: completion
    )
  }

  init(
    serviceType: GamingServiceType,
    pendingResult: [String: Any],
    urlOpener: URLOpener,
    settings: SettingsProtocol,
    completionHandler completion: @escaping GamingServiceResultCompletion
  ) {
    self.serviceType = serviceType
    completionHandler = completion
    self.pendingResult = pendingResult
    self.urlOpener = urlOpener
    self.settings = settings
  }

  func handleBridgeAPIError(_ error: Error?) {
    completionHandler?(
      false,
      nil,
      _ErrorFactory().error(
        code: CoreError.errorBridgeAPIInterruption.rawValue,
        userInfo: nil,
        message: "\(error != nil ? "Error" : "An unknown error") occured while interacting with Gaming Services",
        underlyingError: error
      )
    )

    completionHandler = nil
  }

  func isValidCallbackURL(_ url: URL, forService service: String) -> Bool {
    // verify the URL is intended as a callback for the SDK's friend finder
    guard let appID = settings.appID,
          let scheme = url.scheme else { return false }

    return scheme.hasPrefix("fb\(appID)") && url.host == service
  }
}

extension GamingServiceController: GamingServiceControllerProtocol {
  public func call(withArgument argument: String?) {
    guard
      let argument = argument,
      let url = URL(string: "https://fb.gg/me/\(serviceType.urlPath)/\(argument)")
    else {
      return
    }

    urlOpener.open(url, sender: self) { [weak self] success, error in
      guard !success else { return }

      self?.handleBridgeAPIError(error)
    }
  }
}

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

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

    if let completionHandler = completionHandler, isGamingUrl {
      completionHandler(true, pendingResult, nil)
      self.completionHandler = nil
    }

    return isGamingUrl
  }

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

  public func applicationDidBecomeActive(_ application: UIApplication) {
    completionHandler?(true, pendingResult, nil)
    completionHandler = nil
  }

  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 Foundation

protocol GamingServiceControllerCreating {
  func create(
    serviceType: GamingServiceType,
    pendingResult: [String: Any]?,
    completion: @escaping GamingServiceResultCompletion
  ) -> GamingServiceControllerProtocol
}


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

final class GamingServiceControllerFactory: GamingServiceControllerCreating {
  func create(
    serviceType: GamingServiceType,
    pendingResult: [String: Any]?,
    completion: @escaping GamingServiceResultCompletion
  ) -> GamingServiceControllerProtocol {
    GamingServiceController(
      serviceType: serviceType,
      pendingResult: pendingResult ?? [:],
      completionHandler: completion
    )
  }
}


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

import Foundation

protocol GamingServiceControllerProtocol {
  func call(withArgument argument: 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 Foundation

enum GamingServiceType {
  case friendFinder
  case mediaAsset
  case community

  var urlPath: String {
    switch self {
    case .friendFinder:
      return "friendfinder"
    case .mediaAsset:
      return "media_asset"
    case .community:
      return "community"
    }
  }
}


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

protocol GamingShareValidating {
  static func validateRequiredValue(_ value: Any, named name: String) throws
}

extension _ShareUtility: GamingShareValidating {}


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

protocol GamingUtility {
  static func getGraphDomainFromToken() -> String?
}

extension Utility: GamingUtility {}


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

// swiftlint:disable:all prefer_final_classes

import FBSDKCoreKit
import Foundation

/**
 General-purpose web dialog for presenting `fb.gg/dialog/{view}`
 - warning: INTERNAL - DO NOT USE. This class is public so that other public types may extend it.
 */
public class GamingWebDialog<Success: GamingWebDialogSuccess>: WebDialogDelegate {
  private enum Keys {
    static var path: String { "/gaming/dialog/" }
    static var errorCode: String { "error_code" }
    static var errorMessage: String { "error_message" }
  }

  public var completion: ((Result<Success, Error>) -> Void)?
  var dialog: _WebDialog?
  var parameters = [String: String]()
  var frame = CGRect.zero
  let name: String

  init(name: String) {
    self.name = name
  }

  func show(completion: @escaping (Result<Success, Error>) -> Void) {
    self.completion = completion
    dialog = _WebDialog(
      name: name,
      parameters: parameters,
      webViewFrame: frame,
      path: Keys.path + name
    )
    dialog?.delegate = self
    InternalUtility.shared.registerTransientObject(self)
    dialog?.show()
  }

  public func webDialog(_ webDialog: _WebDialog, didCompleteWithResults results: [String: Any]) {
    guard webDialog == dialog else {
      if let dialog = dialog {
        let error = _ErrorFactory().unknownError(message: "The dialog failed to retrieve results.")
        self.webDialog(dialog, didFailWithError: error)
      }
      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
      )
      completion?(.failure(error))
      completion = nil
      return
    }

    do {
      let success = try Success(results)
      completion?(.success(success))
    } catch {
      completion?(.failure(error))
    }
    completion = nil
  }

  public func webDialog(_ webDialog: _WebDialog, didFailWithError error: Error) {
    guard webDialog == dialog else {
      return
    }
    completion?(.failure(error))
    InternalUtility.shared.unregisterTransientObject(self)
  }

  public func webDialogDidCancel(_ webDialog: _WebDialog) {
    guard webDialog == dialog else {
      return
    }
    do {
      let success = try Success([:])
      completion?(.success(success))
    } catch {
      completion?(.failure(error))
    }
    InternalUtility.shared.unregisterTransientObject(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 Foundation

/**
 The result of a completed `GamingWebDialog`
 - warning: INTERNAL - DO NOT USE
 */
public protocol GamingWebDialogSuccess {
  init(_ dict: [String: Any]) 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.
 */

struct GraphAPIResponse<T: Decodable>: Decodable {
  var data: T
}


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

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

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

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


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

import Foundation

import FBSDKCoreKit

internal enum ShareTournamentDialogURLBuilder {

  enum Constants {
    static let host = "fb.gg"
    static let path = "/me/instant_tournament/"
  }

  enum QueryKeys {
    static let score = "score"
    static let tournamentID = "tournament_id"
    static let title = "tournament_title"
    static let scoreFormat = "score_format"
    static let sortOrder = "sort_order"
    static let endTime = "end_time"
    static let payload = "tournament_payload"
  }

  case create(TournamentConfig)
  case update(Tournament)

  func queryItems(for configuration: TournamentConfig, score: Int) -> [URLQueryItem] {
    var tournamentDictionary = [String: String]()
    tournamentDictionary[QueryKeys.score] = "\(score)"

    if let title = configuration.title {
      tournamentDictionary[QueryKeys.title] = title
    }
    if let endTime = configuration.endTime {
      tournamentDictionary[QueryKeys.endTime] = "\(Int(endTime))"
    }
    if let scoreType = configuration.scoreType?.rawValue {
      tournamentDictionary[QueryKeys.scoreFormat] = scoreType
    }
    if let sortOrder = configuration.sortOrder?.rawValue {
      tournamentDictionary[QueryKeys.sortOrder] = sortOrder
    }
    if let payload = configuration.payload {
      tournamentDictionary[QueryKeys.payload] = payload
    }
    return tournamentDictionary.map { queryName, value in
      URLQueryItem(name: queryName, value: value)
    }
  }

  func url(withPathAppID appID: String, score: Int) -> URL? {
    var components = URLComponents()
    components.scheme = URLScheme.https.rawValue
    components.host = Constants.host
    components.path = "\(Constants.path)\(appID)"

    if case let .update(tournament) = self {
      components.queryItems = [
        URLQueryItem(name: QueryKeys.tournamentID, value: tournament.identifier),
        URLQueryItem(name: QueryKeys.score, value: "\(score)"),
        URLQueryItem(name: QueryKeys.payload, value: tournament.payload),
      ]
    }
    if case let .create(configuration) = self {
      components.queryItems = queryItems(for: configuration, score: score)
    }

    return components.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 FBSDKCoreKit

protocol SwitchContextDialogMaking {
  func makeSwitchContextDialog(
    content: SwitchContextContent,
    windowFinder: _WindowFinding,
    delegate: ContextDialogDelegate
  ) throws -> Showable?
}


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

// Internal protocol to enable us to verify that the underlying pure Swift type is
// exercised correctly by the wrapper class
protocol SwitchContextDialogProtocol: WebDialogDelegate, DialogProtocol {

  var currentWebDialog: _WebDialog? { get set }

  func createWebDialogFrame(
    withWidth: CGFloat,
    height: CGFloat,
    windowFinder: _WindowFinding
  ) -> CGRect
}

extension SwitchContextDialog: SwitchContextDialogProtocol {}


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

/// The types of score a tournament can handle
public enum TournamentScoreType: String {
  case numeric = "NUMERIC"
  case time = "TIME"
}


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

/// The `TournamentSortOrder` is how the tournament score is ranked in the tournament.
public enum TournamentSortOrder: String {
  case higherIsBetter = "HIGHER_IS_BETTER"
  case lowerIsBetter = "LOWER_IS_BETTER"
}


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

public protocol URLMedia: Codable {
  init(withUrl: 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 FBSDKCoreKit
import Foundation

final class VideoUploader: VideoUploading {
  private enum Keys {
    static let videoUploaderDefaultGraphNode = "me"
    static let videoUploaderEdge = "videos"

    static let resultCompletionGesture = "completionGesture"
    static let resultCompletionGestureValuePost = "post"

    static let videoEndOffset = "end_offset"
    static let videoFileChunk = "video_file_chunk"
    static let videoID = "video_id"
    static let videoSize = "file_size"
    static let videoStartOffset = "start_offset"
    static let videoUploadPhase = "upload_phase"
    static let videoUploadPhaseFinish = "finish"
    static let videoUploadPhaseStart = "start"
    static let videoUploadPhaseTransfer = "transfer"
    static let videoUploadSessionID = "upload_session_id"
    static let videoUploadSuccess = "success"
  }

  private let videoName: String
  private let videoSize: UInt
  let parameters: [String: Any]
  private let graphRequestFactory: GraphRequestFactoryProtocol

  private var videoID: Int?
  var uploadSessionID: Int?
  private var graphPath: String {
    "\(graphNode)/\(Keys.videoUploaderEdge)"
  }

  /// The graph node to which video should be uploaded
  var graphNode = Keys.videoUploaderDefaultGraphNode

  /// Receiver's delegate
  weak var delegate: VideoUploaderDelegate?

  /**
   Initialize VideoUploader
   @param videoName The file name of the video to be uploaded
   @param videoSize The size of the video to be uploaded
   @param parameters Optional parameters for video uploads. See Graph API documentation for the full list of parameters https://developers.facebook.com/docs/graph-api/reference/video
   @param delegate Receiver's delegate
   */
  convenience init(
    videoName: String,
    videoSize: UInt,
    parameters: [String: Any],
    delegate: VideoUploaderDelegate
  ) {
    self.init(
      videoName: videoName,
      videoSize: videoSize,
      parameters: parameters,
      delegate: delegate,
      graphRequestFactory: GraphRequestFactory()
    )
  }

  init(
    videoName: String,
    videoSize: UInt,
    parameters: [String: Any],
    delegate: VideoUploaderDelegate,
    graphRequestFactory: GraphRequestFactoryProtocol
  ) {
    self.parameters = parameters
    self.delegate = delegate
    self.videoName = videoName
    self.videoSize = videoSize
    self.graphRequestFactory = graphRequestFactory
  }

  /// Start the upload process
  func start() {
    guard videoSize != 0 else {
      let uploadError = errorWithMessage("Invalid video size: \(videoSize)")
      delegate?.videoUploader(self, didFailWithError: uploadError)
      return
    }

    let parameters = [
      Keys.videoUploadPhase: Keys.videoUploadPhaseStart,
      Keys.videoSize: String(format: "%tu", videoSize),
    ]
    let request = graphRequestFactory.createGraphRequest(
      withGraphPath: graphPath,
      parameters: parameters,
      httpMethod: .post
    )
    request.start { [weak self] _, result, error in
      guard let self = self else { return }

      if let error = error {
        self.delegate?.videoUploader(self, didFailWithError: error)
        return
      }

      guard
        let result = result as? [String: Any],
        let uploadSessionID = self.extractInt(result[Keys.videoUploadSessionID]),
        let videoID = self.extractInt(result[Keys.videoID])
      else {
        let uploadError = self.errorWithMessage("Failed to get valid upload_session_id or video_id.")
        self.delegate?.videoUploader(self, didFailWithError: uploadError)
        return
      }

      guard let offsetDictionary = self.extractOffsets(fromResultDictionary: result) else {
        return
      }

      self.uploadSessionID = uploadSessionID
      self.videoID = videoID
      self.startTransferRequest(withOffsetDictionary: offsetDictionary)
    }
  }

  func startTransferRequest(withOffsetDictionary offsetDictionary: [String: Any]) {
    guard
      let startOffsetInt = extractInt(offsetDictionary[Keys.videoStartOffset]),
      let endOffsetInt = extractInt(offsetDictionary[Keys.videoEndOffset]),
      let startOffset = UInt(exactly: startOffsetInt),
      let endOffset = UInt(exactly: endOffsetInt)
    else {
      return
    }

    guard startOffset != endOffset else {
      postFinishRequest()
      return
    }

    DispatchQueue.global(qos: .userInitiated).async { [weak self] in
      guard let self = self else { return }

      guard endOffset >= startOffset else { return }

      let chunkSize = endOffset - startOffset

      guard
        let data = self.delegate?.videoChunkData(for: self, startOffset: startOffset, endOffset: endOffset),
        data.count == chunkSize
      else {
        self.failVideoUploadForChunkSizeOffset(startOffset, endOffset: endOffset)
        return
      }

      self.startTransferRequest(withNewOffset: offsetDictionary, data: data)
    }
  }

  func postFinishRequest() {
    var parameters: [String: Any] = [
      Keys.videoUploadPhase: Keys.videoUploadPhaseFinish,
    ]
    if let uploadSessionID = uploadSessionID {
      parameters[Keys.videoUploadSessionID] = uploadSessionID
    }
    parameters.merge(self.parameters) { _, new in new }

    let request = graphRequestFactory.createGraphRequest(
      withGraphPath: graphPath,
      parameters: parameters,
      httpMethod: .post
    )
    request.start { [weak self] _, result, error in
      guard let self = self else { return }

      if let error = error {
        self.delegate?.videoUploader(self, didFailWithError: error)
        return
      }

      guard
        let result = result as? [String: Any],
        let resultUploadSuccess = result[Keys.videoUploadSuccess]
      else {
        let uploadError = self.errorWithMessage("Failed to finish uploading.")
        self.delegate?.videoUploader(self, didFailWithError: uploadError)
        return
      }

      var shareResult: [String: Any] = [
        Keys.videoUploadSuccess: resultUploadSuccess,
        Keys.resultCompletionGesture: Keys.resultCompletionGestureValuePost,
      ]

      if let videoID = self.videoID {
        shareResult[Keys.videoID] = videoID
      }

      self.delegate?.videoUploader(self, didCompleteWithResults: shareResult)
    }
  }

  func extractOffsets(fromResultDictionary result: Any) -> [String: Any]? {
    guard
      let result = result as? [String: Any],
      let startOffsetString = result[Keys.videoStartOffset] as? String,
      let endOffsetString = result[Keys.videoEndOffset] as? String
    else {
      return nil
    }

    guard
      let startNum = Int(startOffsetString),
      let endNum = Int(endOffsetString)
    else {
      let uploadError = errorWithMessage("Fail to get valid start_offset or end_offset.")
      delegate?.videoUploader(self, didFailWithError: uploadError)
      return nil
    }

    guard startNum <= endNum else {
      let uploadError = errorWithMessage("Invalid offset: start_offset is greater than end_offset.")
      delegate?.videoUploader(self, didFailWithError: uploadError)
      return nil
    }

    let shareResults: [String: Any] = [
      Keys.videoStartOffset: startNum,
      Keys.videoEndOffset: endNum,
    ]

    return shareResults
  }

  func startTransferRequest(withNewOffset offsetDictionary: [String: Any], data: Data) {
    let dataAttachment = GraphRequestDataAttachment(
      data: data,
      filename: videoName,
      contentType: ""
    )

    var parameters: [String: Any] = [
      Keys.videoUploadPhase: Keys.videoUploadPhaseTransfer,
      Keys.videoFileChunk: dataAttachment,
    ]

    if let startOffset = offsetDictionary[Keys.videoStartOffset] {
      parameters[Keys.videoStartOffset] = startOffset
    }

    if let uploadSessionID = uploadSessionID {
      parameters[Keys.videoUploadSessionID] = uploadSessionID
    }

    let request = graphRequestFactory.createGraphRequest(
      withGraphPath: graphPath,
      parameters: parameters,
      httpMethod: .post
    )
    request.start { _, _, innerError in
      if let innerError = innerError {
        self.delegate?.videoUploader(self, didFailWithError: innerError)
        return
      }

      guard let innerOffsetDictionary = self.extractOffsets(fromResultDictionary: offsetDictionary) else {
        return
      }

      self.startTransferRequest(withOffsetDictionary: innerOffsetDictionary)
    }
  }

  private func failVideoUploadForChunkSizeOffset(_ startOffset: UInt, endOffset: UInt) {
    let message = "Fail to get video chunk with start offset: \(startOffset), end offset: \(endOffset)."
    let uploadError = errorWithMessage(message)
    delegate?.videoUploader(self, didFailWithError: uploadError)
  }

  private func errorWithMessage(_ message: String) -> Error {
    let errorFactory = _ErrorFactory()
    return errorFactory.error(
      domain: "com.facebook.sdk.gaming.videoupload",
      code: 0,
      userInfo: nil,
      message: message,
      underlyingError: nil
    )
  }

  private func extractInt(_ num: Any?) -> Int? {
    if let int = num as? Int {
      return int
    } else if let string = num as? String {
      return Int(string)
    } else {
      return 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

protocol VideoUploaderCreating {
  func create(
    videoName: String,
    videoSize: UInt,
    parameters: [String: Any],
    delegate: VideoUploaderDelegate
  ) -> VideoUploading
}


/*
 * 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 `VideoUploader`.
 The delegate passes video chunk to `VideoUploader` object in `Data` format and is notified with the results of the uploader.
 */
protocol VideoUploaderDelegate: AnyObject {
  /**
   Get chunk of the video to be uploaded in 'NSData' format
   @param videoUploader The `FBSDKVideoUploader` object which is performing the upload process
   @param startOffset The start offset of video chunk to be uploaded
   @param endOffset The end offset of video chunk being to be uploaded
   */
  func videoChunkData(for videoUploader: VideoUploader, startOffset: UInt, endOffset: UInt) -> Data?

  /**
   Notify the delegate that upload process success.
   @param videoUploader The `FBSDKVideoUploader` object which is performing the upload process
   @param results The result from successful upload
   */
  func videoUploader(_ videoUploader: VideoUploader, didCompleteWithResults results: [String: Any])

  /**
   Notify the delegate that upload process fails.
   @param videoUploader The `FBSDKVideoUploader` object which is performing the upload process
   @param error The error object from unsuccessful upload
   */
  func videoUploader(_ videoUploader: VideoUploader, didFailWithError error: Error)
}


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

import Foundation

final class VideoUploaderFactory: VideoUploaderCreating {
  func create(
    videoName: String,
    videoSize: UInt,
    parameters: [String: Any],
    delegate: VideoUploaderDelegate
  ) -> VideoUploading {
    VideoUploader(
      videoName: videoName,
      videoSize: videoSize,
      parameters: parameters,
      delegate: delegate
    )
  }
}


/*
 * 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 VideoUploading {
  var delegate: VideoUploaderDelegate? { get set }

  func start()
}


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

import FBSDKCoreKit
import TestTools
import XCTest

final class AccessTokenProviderTests: XCTestCase {

  final class TestTokenCache: NSObject, TokenCaching {
    var accessToken: AccessToken?
    var authenticationToken: AuthenticationToken?
  }

  lazy var token = SampleAccessTokens.create(withPermissions: [name])
  let cache = TestTokenCache()

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

    AccessToken.current = nil
    AccessToken.tokenCache = nil
  }

  override func tearDown() {
    AccessToken.current = nil
    AccessToken.tokenCache = nil

    super.tearDown()
  }

  func testCurrentToken() {
    AccessToken.current = token

    XCTAssertTrue(
      AccessTokenProvider.current === token,
      "The current access token should match that on the AccessToken singleton"
    )
  }

  func testGettingTokenCache() {
    AccessToken.tokenCache = cache

    XCTAssertTrue(
      AccessTokenProvider.tokenCache === cache,
      "The current token cache should match that on the AccessToken singleton"
    )
  }

  func testSettingTokenCache() {
    AccessTokenProvider.tokenCache = cache

    XCTAssertTrue(
      AccessToken.tokenCache === cache,
      "Should set the provided token cache on the underlying access token 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.
 */

@testable import FBSDKGamingServicesKit

import FBSDKCoreKit
import TestTools
import XCTest

final class ChooseContextContentTests: XCTestCase {

  let content = ChooseContextContent()

  func testCreatingWithoutSettingParameters() {
    XCTAssertEqual(
      content.filter,
      .none,
      "The default filter value should be .none"
    )
    XCTAssertEqual(
      content.minParticipants,
      0,
      "The default min participants value should be 0"
    )
    XCTAssertEqual(
      content.maxParticipants,
      0,
      "The default max participants value should be 0"
    )
  }

  func testValidatingWithInvalidMinAndMaxParticipants() {
    content.minParticipants = 2
    content.maxParticipants = 1

    do {
      try content.validate()
      XCTFail("Content with a min participants greater than max participant is invalid")
    } catch let error as NSError {
      XCTAssertEqual(error.domain, ErrorDomain)
      XCTAssertEqual(
        error.userInfo[ErrorDeveloperMessageKey] as? String,
        "The minimum size cannot be greater than the maximum size"
      )
    }
  }

  func testNameForFilterType() {
    XCTAssertEqual("NO_FILTER", ChooseContextContent.filtersName(forFilters: .none))
    XCTAssertEqual("NEW_PLAYERS_ONLY", ChooseContextContent.filtersName(forFilters: .newPlayersOnly))
    XCTAssertEqual("NEW_CONTEXT_ONLY", ChooseContextContent.filtersName(forFilters: .newContextOnly))
    XCTAssertEqual("INCLUDE_EXISTING_CHALLENGES", ChooseContextContent.filtersName(forFilters: .existingChallenges))
  }
}


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

final class ChooseContextDialogFactoryTests: XCTestCase {

  var content: ChooseContextContent {
    let content = ChooseContextContent()
    content.filter = .newPlayersOnly
    content.maxParticipants = 100
    content.minParticipants = 1000

    return content
  }

  let delegate = TestContextDialogDelegate()

  func testCreatingDialog() throws {
    let dialog = try XCTUnwrap(
      ChooseContextDialogFactory().makeChooseContextDialog(
        content: content,
        delegate: delegate
      ) as? ChooseContextDialog,
      "Should create a context dialog of the expected concrete type"
    )

    XCTAssertEqual(
      dialog.dialogContent as? ChooseContextContent,
      content,
      "Should create the dialog with the expected content"
    )
    XCTAssertTrue(
      dialog.delegate === delegate,
      "Should create the dialog with the expected delegate"
    )
  }
}

// swiftformat:disable extensionaccesscontrol

public extension ChooseContextContent {
  // swiftlint:disable:next override_in_extension
  override func isEqual(_ object: Any?) -> Bool {
    guard let other = object as? ChooseContextContent else {
      return false
    }

    return filter == other.filter &&
      maxParticipants == other.maxParticipants &&
      minParticipants == other.minParticipants
  }
}


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

import FBSDKCoreKit
import TestTools
import XCTest

final class ChooseContextDialogTests: XCTestCase, ContextDialogDelegate {

  var dialogDidCompleteSuccessfully = false
  var dialogDidCancel = false
  var dialogError: NSError?
  let defaultAppID = "abc123"
  let msiteParamsQueryString = #"{"filter": "NO_FILTER", "min_size": 0, "max_size": 0, "app_id": "abc123"}"#

  override func setUp() {
    super.setUp()

    GamingContext.current = nil
    dialogDidCompleteSuccessfully = false
    dialogDidCancel = false
    dialogError = nil
    ApplicationDelegate.shared.application(
      UIApplication.shared,
      didFinishLaunchingWithOptions: [:]
    )
    Settings.shared.appID = defaultAppID
  }

  override func tearDown() {
    super.tearDown()

    GamingContext.current = nil
  }

  func testDialogCompletingWithValidContextID() throws {
    let validCallbackURL = URL(string: "fbabc123://gaming/contextchoose/?context_id=123456789&context_size=3")

    let dialog = try XCTUnwrap(SampleContextDialogs.chooseContextDialogWithoutContentValues(delegate: self))
    _ = dialog.show()
    _ = dialog.application(
      UIApplication.shared,
      open: validCallbackURL,
      sourceApplication: "",
      annotation: nil
    )

    XCTAssertNotNil(dialog)
    XCTAssertTrue(dialogDidCompleteSuccessfully)
    XCTAssertFalse(dialogDidCancel)
    // TODO: Fix tests to have completion handler begin called
    if let nsError = dialogError?.userInfo[NSUnderlyingErrorKey] as? NSError,
       let errorMessage = nsError.userInfo[ErrorLocalizedDescriptionKey] as? String,
       errorMessage != "Cannot login due to urlOpener being nil" {
      XCTAssertNil(dialogError)
    }

    XCTAssertEqual(GamingContext.current?.size, 3)
    XCTAssertEqual(GamingContext.current?.identifier, "123456789")
  }

  func testCompletingWithEmptyContextID() throws {
    GamingContext.current = GamingContext(identifier: name, size: 2)

    let url = URL(string: "fbabc123://gaming/contextchoose/?context_id=")

    let dialog = try XCTUnwrap(SampleContextDialogs.chooseContextDialogWithoutContentValues(delegate: self))
    _ = dialog.show()
    _ = dialog.application(
      UIApplication.shared,
      open: url,
      sourceApplication: "",
      annotation: nil
    )

    XCTAssertTrue(
      dialogDidCancel,
      "Should cancel if a context cannot be created from the URL"
    )
    XCTAssertEqual(
      GamingContext.current?.identifier,
      name,
      "The current gaming context should still hold the old context"
    )
    XCTAssertEqual(
      GamingContext.current?.size,
      2,
      "The current gaming context should still be the same size"
    )
  }

  func testDialogCancels() throws {
    let dialog = try XCTUnwrap(SampleContextDialogs.chooseContextDialogWithoutContentValues(delegate: self))
    _ = dialog.show()
    dialog.applicationDidBecomeActive(UIApplication.shared)

    XCTAssertNotNil(dialog)
    XCTAssertFalse(dialogDidCompleteSuccessfully)
    XCTAssertTrue(dialogDidCancel)
    // TODO: Fix tests to have completion handler begin called
    if let nsError = dialogError?.userInfo[NSUnderlyingErrorKey] as? NSError,
       let errorMessage = nsError.userInfo[ErrorLocalizedDescriptionKey] as? String,
       errorMessage != "Cannot login due to urlOpener being nil" {
      XCTAssertNil(dialogError)
    }
  }

  func testShowDialogWithoutSettingAppID() throws {
    let appIDErrorMessage = "App ID is not set in settings"
    let content = ChooseContextContent()
    let dialog = ChooseContextDialog(content: content, delegate: self)
    Settings.shared.appID = nil
    _ = dialog.show()

    let dialogError = try XCTUnwrap(dialogError)
    XCTAssertNotNil(dialog)
    XCTAssertEqual(CoreError.errorUnknown.rawValue, dialogError.code)
    XCTAssertEqual(appIDErrorMessage, dialogError.userInfo[ErrorDeveloperMessageKey] as? String)
  }

  func testShowDialogWithInvalidSizeContent() throws {
    let appIDErrorMessage = "The minimum size cannot be greater than the maximum size"
    let contentErrorName = "minParticipants"
    let dialog = try XCTUnwrap(SampleContextDialogs.showChooseContextDialogWithInvalidSizes(delegate: self))
    _ = dialog.show()

    let dialogError = try XCTUnwrap(dialogError)
    XCTAssertNotNil(dialog)
    XCTAssertNotNil(dialogError)
    XCTAssertEqual(CoreError.errorInvalidArgument.rawValue, dialogError.code)
    XCTAssertEqual(appIDErrorMessage, dialogError.userInfo[ErrorDeveloperMessageKey] as? String)
    XCTAssertEqual(contentErrorName, dialogError.userInfo[ErrorArgumentNameKey] as? String)
  }

  func testShowDialogWithNullValuesInContent() throws {
    let dialog = try XCTUnwrap(SampleContextDialogs.chooseContextDialogWithoutContentValues(delegate: self))
    _ = dialog.show()

    XCTAssertNotNil(dialog)
    XCTAssertFalse(dialogDidCompleteSuccessfully)
    XCTAssertFalse(dialogDidCancel)
    // TODO: Fix tests to have completion handler begin called
    if let nsError = dialogError?.userInfo[NSUnderlyingErrorKey] as? NSError,
       let errorMessage = nsError.userInfo[ErrorLocalizedDescriptionKey] as? String,
       errorMessage != "Cannot login due to urlOpener being nil" {
      XCTAssertNil(dialogError)
    }
  }

  func testCanOpenURLWithIncorrectHostAndPath() throws {
    let dialog = try XCTUnwrap(SampleContextDialogs.chooseContextDialogWithoutContentValues(delegate: self))
    let incorrectHostURL = try XCTUnwrap(URL(string: "fbabc123://invalid/contextchoose/?context_id=123456789&context_size=3"))
    XCTAssertFalse(dialog.canOpen(incorrectHostURL, for: nil, sourceApplication: "", annotation: nil))

    let incorrectPathURL = try XCTUnwrap(URL(string: "fbabc123://gaming/invalid/?context_id=123456789&context_size=3"))
    XCTAssertFalse(dialog.canOpen(incorrectPathURL, for: nil, sourceApplication: "", annotation: nil))
  }

  // MARK: - Delegate Methods

  func contextDialogDidComplete(_ contextDialog: ContextWebDialog) {
    dialogDidCompleteSuccessfully = true
  }

  func contextDialog(_ contextDialog: ContextWebDialog, didFailWithError error: Error) {
    dialogError = error as NSError
  }

  func contextDialogDidCancel(_ contextDialog: ContextWebDialog) {
    dialogDidCancel = true
  }
}


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

@testable import FBSDKGamingServicesKit
import XCTest

final class ChooseContextDialogURLFactoryTests: XCTestCase {

  // swiftlint:disable:next implicitly_unwrapped_optional
  var chooseContent: ChooseContextContent!
  let appID = "12345"

  override func setUp() {
    super.setUp()

    chooseContent = ChooseContextContent()
  }

  override func tearDown() {
    chooseContent = nil

    super.tearDown()
  }

  func testContextChooseDialogURL() throws {
    let URLFactory = ChooseContextDialogURLFactory(appID: appID, content: chooseContent)
    let chooseDialogURL = try URLFactory.generateDialogDeeplinkURL()
    let chooseDialogURLComponents = try XCTUnwrap(
      URLComponents(
        url: chooseDialogURL,
        resolvingAgainstBaseURL: false
      )
    )
    var queryParameters = [String: String]()
    chooseDialogURLComponents.queryItems?.forEach { queryParameters[$0.name] = $0.value }

    XCTAssertEqual(
      chooseDialogURLComponents.host,
      "fb.gg",
      "The host for the created url should be fb.gg"
    )
    XCTAssertEqual(
      chooseDialogURLComponents.path,
      "/dialog/choosecontext/\(appID)",
      "The path for the created url should be /me/instant_tournament/{app_id}"
    )
    XCTAssertEqual(queryParameters["filter"], "NO_FILTER")
    XCTAssertEqual(queryParameters["min_size"], "0")
    XCTAssertEqual(queryParameters["max_size"], "0")
  }
}


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

@testable import FBSDKGamingServicesKit

import FBSDKCoreKit
import TestTools
import XCTest

final class ContextDialogPresenterTests: XCTestCase {

  // swiftlint:disable implicitly_unwrapped_optional
  var createContextContent: CreateContextContent!
  var switchContextContent: SwitchContextContent!
  var chooseContextContent: ChooseContextContent!
  var delegate: TestContextDialogDelegate!
  var createContextDialogFactory: TestCreateContextDialogFactory!
  var switchContextDialogFactory: TestSwitchContextDialogFactory!
  var chooseContextDialogFactory: TestChooseContextDialogFactory!
  var presenter: ContextDialogPresenter!
  // swiftlint:enable implicitly_unwrapped_optional

  override func setUp() {
    super.setUp()

    AccessToken.current = nil

    createContextContent = CreateContextContent(playerID: "playerID")
    switchContextContent = SwitchContextContent(contextID: "contextID")
    chooseContextContent = ChooseContextContent()
    delegate = TestContextDialogDelegate()
    createContextDialogFactory = TestCreateContextDialogFactory()
    switchContextDialogFactory = TestSwitchContextDialogFactory()
    chooseContextDialogFactory = TestChooseContextDialogFactory()
    presenter = ContextDialogPresenter(
      createContextDialogFactory: createContextDialogFactory,
      switchContextDialogFactory: switchContextDialogFactory,
      chooseContextDialogFactory: chooseContextDialogFactory
    )
  }

  override func tearDown() {
    createContextContent = nil
    switchContextContent = nil
    chooseContextContent = nil
    delegate = nil
    createContextDialogFactory = nil
    switchContextDialogFactory = nil
    chooseContextDialogFactory = nil
    presenter = nil

    AccessToken.current = nil

    super.tearDown()
  }

  private func setSampleAccessToken() {
    AccessToken.current = SampleAccessTokens.validToken
  }

  func testDefaults() {
    presenter = ContextDialogPresenter()

    XCTAssertTrue(
      presenter.createContextDialogFactory is CreateContextDialogFactory,
      "Should have a create context dialog factory of the expected concrete type"
    )
    XCTAssertTrue(
      presenter.chooseContextDialogFactory is ChooseContextDialogFactory,
      "Should have a choose context dialog factory of the expected concrete type"
    )
    XCTAssertTrue(
      presenter.switchContextDialogFactory is SwitchContextDialogFactory,
      "Should have a switch context dialog factory of the expected concrete type"
    )
  }

  func testCreatingWithCustomDependencies() {
    XCTAssertTrue(
      presenter.createContextDialogFactory is TestCreateContextDialogFactory,
      "Should be able to make a presenter with a custom create context dialog factory"
    )
    XCTAssertTrue(
      presenter.chooseContextDialogFactory is TestChooseContextDialogFactory,
      "Should be able to make a presenter with a custom choose context dialog factory"
    )
    XCTAssertTrue(
      presenter.switchContextDialogFactory is TestSwitchContextDialogFactory,
      "Should be able to make a presenter with a custom switch context dialog factory"
    )
  }

  func testShowingCreateContextDialog() throws {
    _ = try XCTUnwrap(
      presenter.makeAndShowCreateContextDialog(
        content: createContextContent,
        delegate: delegate
      ),
      "Should not throw an error if the dialog is created successfully"
    )

    XCTAssertTrue(
      createContextDialogFactory.wasMakeCreateContextDialogCalled,
      "Should use the factory to make a create context dialog"
    )
    XCTAssertTrue(
      createContextDialogFactory.dialog.wasShowCalled,
      "Should call show on the dialog"
    )
  }

  func testShowingCreateContextDialogWithFailedDialogCreation() throws {
    createContextDialogFactory.shouldCreateDialog = false

    do {
      try presenter.makeAndShowCreateContextDialog(
        content: createContextContent,
        delegate: delegate
      )
      XCTFail("Should not create a dialog with a missing access token")
    } catch {
      let error = try XCTUnwrap(
        error as? ContextDialogPresenterError,
        "Unexpected error: \(error)"
      )
      XCTAssertEqual(error, .showCreateContext)
    }

    XCTAssertFalse(
      createContextDialogFactory.dialog.wasShowCalled,
      "Should not call show on the dialog"
    )
  }

  func testShowingSwitchContextDialog() throws {
    _ = try XCTUnwrap(
      presenter.makeAndShowSwitchContextDialog(
        content: switchContextContent,
        delegate: delegate
      ),
      "Should not create a dialog with a missing access token"
    )

    XCTAssertTrue(
      switchContextDialogFactory.wasMakeSwitchContextDialogCalled,
      "Should use the factory to make a switch context dialog"
    )
    XCTAssertTrue(
      switchContextDialogFactory.dialog.wasShowCalled,
      "Should call show on the dialog"
    )
  }

  func testShowingSwitchContextDialogWithFailedDialogCreation() throws {
    switchContextDialogFactory.shouldCreateDialog = false

    do {
      try
        presenter.makeAndShowSwitchContextDialog(
          content: switchContextContent,
          delegate: delegate
        )
      XCTFail("Should not create a dialog with a missing access token")
    } catch {
      let error = try XCTUnwrap(
        error as? ContextDialogPresenterError,
        "Unexpected error: \(error)"
      )
      XCTAssertEqual(error, .showSwitchContext)
    }

    XCTAssertFalse(
      switchContextDialogFactory.dialog.wasShowCalled,
      "Should not call show on the dialog"
    )
  }

  func testShowingChooseContextDialog() throws {
    presenter.makeAndShowChooseContextDialog(
      content: chooseContextContent,
      delegate: delegate
    )

    XCTAssertTrue(
      chooseContextDialogFactory.wasMakeChooseContextDialogCalled,
      "Should use the factory to make a choose context dialog"
    )
    XCTAssertTrue(
      chooseContextDialogFactory.dialog.wasShowCalled,
      "Should call show on the dialog"
    )
  }
}


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

import FBSDKCoreKit
import TestTools
import XCTest

final class ContextWebDialogTests: XCTestCase, ContextDialogDelegate {

  let contextIDKey = "context_id"
  let contextID = "123"
  let errorCodeKey = "error_code"
  let cancelErrorCode = 4201
  let errorCode = 404
  let errorMessageKey = "error_message"
  let errorMessage = "Dialog Error"

  lazy var contextWebDialog = ContextWebDialog(delegate: self, dialogContent: nil)
  let testWindow = UIWindow(frame: CGRect(x: 0, y: 0, width: 300, height: 600))
  var dialogCompleted = false
  var dialogCanceled = false
  var dialogError: Error?

  func testShow() throws {
    let dialog = try XCTUnwrap(contextWebDialog)

    XCTAssertFalse(
      dialog.show(),
      "Since ContextWebDialog is just a superclass, its show method should always return false"
    )
    XCTAssertFalse(dialogCompleted)
    XCTAssertFalse(dialogCanceled)
  }

  func testDialogCompletesAndUpdatesContext() throws {
    let dialog = try XCTUnwrap(contextWebDialog)
    let webDelegate = dialog as WebDialogDelegate
    let webDialog = _WebDialog(name: "Test")
    webDialog.delegate = webDelegate
    dialog.currentWebDialog = webDialog

    dialog.webDialog(webDialog, didCompleteWithResults: [contextIDKey: contextID])

    XCTAssertTrue(dialogCompleted)
    XCTAssertFalse(dialogCanceled)
    XCTAssertNil(dialogError)
    XCTAssertEqual(GamingContext.current?.identifier, contextID)
  }

  func testDialogCancelsThroughErrorResults() throws {
    let dialog = try XCTUnwrap(contextWebDialog)
    let webDelegate = dialog as WebDialogDelegate
    let webDialog = _WebDialog(name: "Test")
    webDialog.delegate = webDelegate
    dialog.currentWebDialog = webDialog

    dialog.webDialog(webDialog, didCompleteWithResults: [errorCodeKey: cancelErrorCode, errorMessageKey: errorMessage])
    let error = try XCTUnwrap(dialogError) as NSError

    XCTAssertFalse(dialogCompleted)
    XCTAssertFalse(dialogCanceled)
    XCTAssertNotNil(error)
    XCTAssertEqual(error.code, cancelErrorCode)
    XCTAssertEqual(error.userInfo.values.first as? String, errorMessage)
  }

  func testDialogReturnsError() throws {
    let dialog = try XCTUnwrap(contextWebDialog)
    let webDelegate = dialog as WebDialogDelegate
    let webDialog = _WebDialog(name: "Test")
    webDialog.delegate = webDelegate
    dialog.currentWebDialog = webDialog

    dialog.webDialog(webDialog, didCompleteWithResults: [errorCodeKey: errorCode, errorMessageKey: errorMessage])
    let error: NSError = try XCTUnwrap(dialogError) as NSError

    XCTAssertFalse(dialogCompleted)
    XCTAssertFalse(dialogCanceled)
    XCTAssertNotNil(error)
    XCTAssertEqual(error.code, errorCode)
    XCTAssertEqual(error.userInfo.values.first as? String, errorMessage)
  }

  func testDialogCancelsWhenWebDialogReturnsEmptyResults() throws {
    let dialog = try XCTUnwrap(contextWebDialog)
    let delegate = dialog as WebDialogDelegate
    let webDialog = _WebDialog(name: "Test")
    webDialog.delegate = delegate
    dialog.currentWebDialog = webDialog

    dialog.webDialog(webDialog, didCompleteWithResults: [:])

    XCTAssertFalse(dialogCompleted)
    XCTAssertTrue(dialogCanceled)
    XCTAssertNil(dialogError)
  }

  func testCreateWebDialogFrameWithValuesLessThanWindowFrame() throws {
    let dialog = try XCTUnwrap(contextWebDialog)
    let finder = TestWindowFinder(window: testWindow)
    let frame = dialog.createWebDialogFrame(withWidth: 200, height: 200, windowFinder: finder)

    XCTAssertEqual(frame, CGRect(x: 50, y: 200, width: 200, height: 200))
  }

  func testCreateWebDialogFrameWithValuesGreaterThanWindowFrame() throws {
    let dialog = try XCTUnwrap(contextWebDialog)
    let finder = TestWindowFinder(window: testWindow)
    let frame = dialog.createWebDialogFrame(withWidth: 301, height: 601, windowFinder: finder)
    let windowOrigin = testWindow.frame.origin
    let windowSize = testWindow.frame.size

    let expectedFrame = CGRect(x: windowOrigin.x, y: windowOrigin.y, width: windowSize.width, height: windowSize.height)
    XCTAssertEqual(frame, expectedFrame)
  }

  func contextDialogDidComplete(_ contextDialog: ContextWebDialog) {
    dialogCompleted = true
  }

  func contextDialog(_ contextDialog: ContextWebDialog, didFailWithError error: Error) {
    dialogError = error
  }

  func contextDialogDidCancel(_ contextDialog: ContextWebDialog) {
    dialogCanceled = true
  }
}


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

@testable import FBSDKGamingServicesKit
import TestTools
import XCTest

final class CreateContextDialogFactoryTests: XCTestCase {

  let content = CreateContextContent(playerID: "123")
  let windowFinder = TestWindowFinder()
  let delegate = TestContextDialogDelegate()
  let tokenProvider = TestAccessTokenProvider()

  override func setUp() {
    super.setUp()

    TestAccessTokenProvider.reset()
  }

  override func tearDown() {
    TestAccessTokenProvider.reset()

    super.tearDown()
  }

  func testCreatingDialogWithAccessToken() throws {
    TestAccessTokenProvider.stubbedAccessToken = SampleAccessTokens.validToken
    let factory = CreateContextDialogFactory(tokenProvider: TestAccessTokenProvider.self)

    let dialog = try XCTUnwrap(
      factory.makeCreateContextDialog(
        content: content,
        windowFinder: windowFinder,
        delegate: delegate
      ) as? CreateContextDialog,
      "Should create a context dialog of the expected concrete type"
    )

    XCTAssertEqual(
      dialog.dialogContent as? CreateContextContent,
      content,
      "Should create the dialog with the expected content"
    )
    XCTAssertTrue(
      dialog.delegate === delegate,
      "Should create the dialog with the expected delegate"
    )
  }

  func testCreatingDialogWithMissingAccessToken() throws {
    let factory = CreateContextDialogFactory(tokenProvider: TestAccessTokenProvider.self)
    var capturedError: ContextDialogPresenterError?
    var createDialog: CreateContextDialog?

    do {
      createDialog = try factory.makeCreateContextDialog(
        content: content,
        windowFinder: windowFinder,
        delegate: delegate
      ) as? CreateContextDialog
    } catch {
      capturedError = error as? ContextDialogPresenterError
    }
    XCTAssertEqual(
      capturedError,
      ContextDialogPresenterError.invalidAccessToken,
      "Should throw ContextDialogPresenterError.invalidAccessToken error"
    )
    XCTAssertNil(
      createDialog,
      "Should not create a context dialog with a missing access token"
    )
  }
}