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

@testable import FBSDKCoreKit
import SafariServices

import TestTools
import XCTest

@available(iOS 13.0, *)
final class BridgeAPITests: XCTestCase {

  let sampleSource = "com.example"
  let sampleAnnotation = "foo"

  let sampleURL = SampleURLs.valid
  let validBridgeResponseURL = URL(string: "http://bridge")! // swiftlint:disable:this force_unwrapping

  // swiftlint:disable implicitly_unwrapped_optional
  var logger: TestLogger!
  var urlOpener: TestInternalURLOpener!
  var responseFactory: TestBridgeAPIResponseFactory!
  var frameworkLoader: TestDylibResolver!
  var appURLSchemeProvider: TestInternalUtility!
  var errorFactory: TestErrorFactory!
  var api: _BridgeAPI!
  // swiftlint:enable implicitly_unwrapped_optional

  override func setUp() {
    super.setUp()

    FBSDKLoginManager.resetTestEvidence()

    logger = TestLogger(loggingBehavior: .developerErrors)
    urlOpener = TestInternalURLOpener()
    responseFactory = TestBridgeAPIResponseFactory()
    appURLSchemeProvider = TestInternalUtility()
    errorFactory = TestErrorFactory()

    configureSDK()

    api = _BridgeAPI(
      logger: logger,
      urlOpener: urlOpener,
      bridgeAPIResponseFactory: responseFactory,
      appURLSchemeProvider: appURLSchemeProvider,
      errorFactory: errorFactory
    )

    TestBackgroundEventLogger.setDependencies(
      .init(
        infoDictionaryProvider: TestBundle(),
        eventLogger: TestAppEvents()
      )
    )
  }

  override func tearDown() {
    logger = nil
    urlOpener = nil
    responseFactory = nil
    frameworkLoader = nil
    appURLSchemeProvider = nil
    errorFactory = nil
    api = nil

    FBSDKLoginManager.resetTestEvidence()

    super.tearDown()
  }

  func configureSDK() {
    let backgroundEventLogger = TestBackgroundEventLogger()
    let serverConfigurationProvider = TestServerConfigurationProvider(
      configuration: ServerConfigurationFixtures.defaultConfiguration
    )
    let components = TestCoreKitComponents.makeComponents(
      backgroundEventLogger: backgroundEventLogger,
      serverConfigurationProvider: serverConfigurationProvider
    )
    let delegate = ApplicationDelegate(
      components: components,
      configurator: TestCoreKitConfigurator(components: components)
    )
    delegate.initializeSDK()
  }

  // MARK: - Dependencies

  func testDefaultDependencies() throws {
    XCTAssertIdentical(
      _BridgeAPI.shared.urlOpener as AnyObject,
      CoreUIApplication.shared,
      "Should use the expected concrete url opener by default"
    )
    XCTAssertTrue(
      _BridgeAPI.shared.bridgeAPIResponseFactory is _BridgeAPIResponseFactory,
      "Should use and instance of the expected concrete response factory type by default"
    )
    XCTAssertTrue(
      _BridgeAPI.shared.appURLSchemeProvider is InternalUtility,
      "Should use the expected internal utility type by default"
    )

    let reporter = try _ErrorFactory.getDependencies().reporter
    XCTAssertTrue(
      reporter === ErrorReporter.shared,
      "Should use the shared error reporter by default"
    )
  }

  func testCreatingWithDependencies() {
    XCTAssertEqual(
      api.logger as? TestLogger,
      logger,
      "Should be able to create a bridge api with a specific logger"
    )
    XCTAssertEqual(
      api.urlOpener as? TestInternalURLOpener,
      urlOpener,
      "Should be able to create a bridge api with a specific url opener"
    )
    XCTAssertTrue(
      api.bridgeAPIResponseFactory is TestBridgeAPIResponseFactory,
      "Should be able to create a bridge api with a specific response factory"
    )
    XCTAssertTrue(
      api.errorFactory === errorFactory,
      "Should be able to create a bridge API instance with an error factory"
    )
  }

  // MARK: - Lifecycle Methods

  // MARK: Will Resign Active

  func testWillResignActiveWithoutAuthSessionWithoutAuthSessionState() {
    api.applicationWillResignActive(UIApplication.shared)

    XCTAssertEqual(
      api.authenticationSessionState,
      .none,
      "Should not modify the auth session state if there is no auth session"
    )
  }

  func testWillResignActiveWithAuthSessionWithoutAuthSessionState() {
    api.authenticationSession = AuthenticationSessionSpy.makeDefaultSpy()

    api.applicationWillResignActive(UIApplication.shared)

    XCTAssertEqual(
      api.authenticationSessionState,
      .none,
      "Should not modify the auth session state unless the current state is 'started'"
    )
  }

  func testWillResignActiveWithAuthSessionWithNonStartedAuthSessionState() {
    api.authenticationSession = AuthenticationSessionSpy.makeDefaultSpy()

    [
      AuthenticationSessionState.none,
      .showAlert,
      .showWebBrowser,
      .canceledBySystem,
    ]
      .shuffled()
      .forEach { state in
        api.authenticationSessionState = state
        api.applicationWillResignActive(UIApplication.shared)
        XCTAssertEqual(
          api.authenticationSessionState,
          state,
          "Should not modify the auth session state unless the current state is 'started'"
        )
      }
  }

  func testWillResignActiveWithAuthSessionWithStartedAuthSessionState() {
    api.authenticationSession = AuthenticationSessionSpy.makeDefaultSpy()

    api.authenticationSessionState = .started
    api.applicationWillResignActive(UIApplication.shared)
    XCTAssertEqual(
      api.authenticationSessionState,
      .showAlert,
      "Should change the auth state from 'started' to 'alert' before resigning activity"
    )
  }

  // MARK: Did Become Active

  func testUpdatingShowAlertStateForDidBecomeActiveWithoutAuthSession() {
    [
      AuthenticationSessionState.none,
      .started,
      .showAlert,
      .showWebBrowser,
      .canceledBySystem,
    ]
      .shuffled()
      .forEach { state in
        api.authenticationSessionState = state
        api.applicationDidBecomeActive(UIApplication.shared)
        XCTAssertEqual(
          api.authenticationSessionState,
          state,
          "Should not modify the auth session state if there is no auth session"
        )
      }
  }

  func testUpdatingShowAlertStateForDidBecomeActive() {
    let authSessionSpy = AuthenticationSessionSpy.makeDefaultSpy()
    api.authenticationSession = authSessionSpy
    api.authenticationSessionState = .showAlert

    api.applicationDidBecomeActive(UIApplication.shared)

    XCTAssertEqual(
      api.authenticationSessionState,
      .showWebBrowser,
      "Becoming active when the state is 'showAlert' should set the state to be 'showWebBrowser'"
    )
    XCTAssertEqual(
      authSessionSpy.cancelCallCount,
      0,
      "Becoming active when the state is 'showAlert' should not cancel the session"
    )
    XCTAssertNotNil(
      api.authenticationSession,
      "Becoming active when the state is 'showAlert' should not destroy the session"
    )
  }

  func testUpdatingCancelledBySystemStateForDidBecomeActive() {
    let authSessionSpy = AuthenticationSessionSpy.makeDefaultSpy()
    api.authenticationSession = authSessionSpy
    api.authenticationSessionState = .canceledBySystem

    api.applicationDidBecomeActive(UIApplication.shared)

    XCTAssertEqual(
      api.authenticationSessionState,
      .canceledBySystem,
      "Becoming active when the state is 'canceledBySystem' should not change the state"
    )
    XCTAssertNil(
      api.authenticationSession,
      "Becoming active when the state is 'canceledBySystem' should destroy the session"
    )
    XCTAssertEqual(
      authSessionSpy.cancelCallCount,
      1,
      "Becoming active when the state is 'canceledBySystem' should cancel the session"
    )
  }

  func testCompletingWithCancelledBySystemStateForDidBecomeActive() {
    let authSessionSpy = AuthenticationSessionSpy.makeDefaultSpy()
    api.authenticationSession = authSessionSpy
    api.authenticationSessionState = .canceledBySystem

    var capturedCallbackURL: URL?
    var capturedError: Error?
    api.authenticationSessionCompletionHandler = { callbackURL, error in
      capturedCallbackURL = callbackURL
      capturedError = error
    }

    api.applicationDidBecomeActive(UIApplication.shared)

    XCTAssertNil(
      capturedCallbackURL,
      "A completion triggered by becoming active in a canceled state should not have a callback URL"
    )
    XCTAssertEqual(
      (capturedError as NSError?)?.domain,
      "com.apple.AuthenticationServices.WebAuthenticationSession",
      "A completion triggered by becoming active in a canceled state should include an error"
    )
  }

  // MARK: Did Enter Background

  func testDidEnterBackgroundWithoutAuthSession() {
    api.isActive = true
    api.isExpectingBackground = true

    [
      AuthenticationSessionState.none,
      .started,
      .showAlert,
      .showWebBrowser,
      .canceledBySystem,
    ]
      .shuffled()
      .forEach { state in
        api.authenticationSessionState = state
        api.applicationDidEnterBackground(UIApplication.shared)
        XCTAssertFalse(
          api.isActive,
          "Should mark a bridge api inactive when entering the background"
        )
        XCTAssertFalse(
          api.isExpectingBackground,
          "Should mark a bridge api as not expecting backgrounding when entering the background"
        )
      }
  }

  func testDidEnterBackgroundInShowAlertState() {
    api.authenticationSession = AuthenticationSessionSpy.makeDefaultSpy()
    api.authenticationSessionState = .showAlert

    api.applicationDidEnterBackground(UIApplication.shared)
    XCTAssertEqual(
      api.authenticationSessionState,
      .canceledBySystem,
      "Should cancel the session when entering the background while showing an alert"
    )
  }

  func testDidEnterBackgroundInNonShowAlertState() {
    api.authenticationSession = AuthenticationSessionSpy.makeDefaultSpy()

    [
      AuthenticationSessionState.none,
      .started,
      .showWebBrowser,
      .canceledBySystem,
    ]
      .shuffled()
      .forEach { state in
        api.authenticationSessionState = state
        api.applicationDidEnterBackground(UIApplication.shared)
        XCTAssertEqual(
          api.authenticationSessionState,
          state,
          "Should only modify the auth session state on backgrounding if it is showing an alert"
        )
      }
  }

  // MARK: Did Finish Launching With Options

  func testDidFinishLaunchingWithoutLaunchedURLWithoutSourceApplication() {
    XCTAssertFalse(
      api.application(UIApplication.shared, didFinishLaunchingWithOptions: [:]),
      "Should not consider it a successful launch if there is no launch url or source application"
    )
  }

  func testDidFinishLaunchingWithoutLaunchedUrlWithSourceApplication() {
    let options = [UIApplication.LaunchOptionsKey.sourceApplication: "com.example"]
    XCTAssertFalse(
      api.application(UIApplication.shared, didFinishLaunchingWithOptions: options),
      "Should not consider it a successful launch if there is no launch url"
    )
  }

  func testDidFinishLaunchingWithLaunchedUrlWithoutSourceApplication() {
    let options = [UIApplication.LaunchOptionsKey.url: sampleURL]
    XCTAssertFalse(
      api.application(UIApplication.shared, didFinishLaunchingWithOptions: options),
      "Should not consider it a successful launch if there is no source application"
    )
  }

  func testDidFinishLaunchingWithLaunchedUrlWithSourceApplication() {
    let options: [UIApplication.LaunchOptionsKey: Any] = [
      UIApplication.LaunchOptionsKey.url: sampleURL,
      UIApplication.LaunchOptionsKey.sourceApplication: sampleSource,
      UIApplication.LaunchOptionsKey.annotation: sampleAnnotation,
    ]

    FBSDKLoginManager.stubbedOpenURLSuccess = true

    XCTAssertTrue(
      api.application(UIApplication.shared, didFinishLaunchingWithOptions: options),
      "Should return the success value determined by the login manager's open url method"
    )

    XCTAssertEqual(
      FBSDKLoginManager.capturedOpenURL,
      sampleURL,
      "Should pass the launch url to the login manager"
    )
    XCTAssertEqual(
      FBSDKLoginManager.capturedSourceApplication,
      sampleSource,
      "Should pass the source application to the login manager"
    )
    XCTAssertEqual(
      FBSDKLoginManager.capturedAnnotation,
      sampleAnnotation,
      "Should pass the annotation to the login manager"
    )
  }

  // MARK: - Open URL

  func testOpenURLWithMissingSender() {
    api.open(
      sampleURL,
      sender: nil
    ) { _, _ in }

    XCTAssertTrue(
      api.isExpectingBackground,
      "Should set expecting background to true when opening a URL"
    )
    XCTAssertNil(
      api.pendingURLOpener,
      "Should not set the pending url opener if there is no sender"
    )
  }

  func testOpenURLWithSender() {
    let urlOpener = FBSDKLoginManager()
    api.open(
      sampleURL,
      sender: urlOpener
    ) { _, _ in }

    XCTAssertTrue(api.isExpectingBackground, "Should set expecting background to true when opening a URL")
    XCTAssertTrue(
      api.pendingURLOpener === urlOpener,
      "Should set the pending url opener to the sender"
    )
  }

  func testOpenUrlWhenApplicationOpens() {
    var capturedSuccess = false
    var capturedError: Error?
    api.open(
      sampleURL,
      sender: nil
    ) { success, error in
      capturedSuccess = success
      capturedError = error
    }

    urlOpener.capturedOpenURLCompletion?(true)

    XCTAssertTrue(
      capturedSuccess,
      "Should call the completion handler with the expected value"
    )
    XCTAssertNil(capturedError, "Should not call the completion handler with an error")
  }

  func testOpenUrlWhenApplicationDoesNotOpen() {
    var capturedSuccess = true
    var capturedError: Error?
    api.open(
      sampleURL,
      sender: nil
    ) { success, error in
      capturedSuccess = success
      capturedError = error
    }

    urlOpener.capturedOpenURLCompletion?(false)

    XCTAssertFalse(
      capturedSuccess,
      "Should call the completion handler with the expected value"
    )
    XCTAssertNil(capturedError, "Should not call the completion handler with an error")
  }

  // MARK: - Request completion block

  func testRequestCompletionBlockCalledWithSuccess() {
    let request = TestBridgeAPIRequest(url: sampleURL)
    let responseBlock: BridgeAPIResponseBlock = { _ in
      XCTFail("Should not call the response block when the request completion is called with success")
    }
    api.pendingRequest = request
    api.pendingRequestCompletionBlock = { _ in }

    let completion = api.bridgeAPIRequestCompletionBlock(request: request, completion: responseBlock)

    // With Error
    completion(true, SampleError())
    assertPendingPropertiesNotCleared()

    // Without Error
    completion(true, nil)
    assertPendingPropertiesNotCleared()
  }

  func testRequestCompletionBlockWithNonHttpRequestCalledWithoutSuccessWithError() throws {
    let request = TestBridgeAPIRequest(url: sampleURL, scheme: "file")

    var capturedResponse: BridgeAPIResponse?
    let responseBlock: BridgeAPIResponseBlock = { response in
      capturedResponse = response
    }
    api.pendingRequest = request
    api.pendingRequestCompletionBlock = { _ in }

    let completion = api.bridgeAPIRequestCompletionBlock(request: request, completion: responseBlock)

    // With Error
    completion(false, SampleError())
    assertPendingPropertiesCleared()

    XCTAssertTrue(
      capturedResponse?.request === request,
      "The response should contain the original request"
    )
    let error = try XCTUnwrap(capturedResponse?.error as? TestSDKError)
    XCTAssertEqual(
      error.type,
      .general,
      "The response should contain a general error"
    )
    XCTAssertEqual(
      error.code,
      CoreError.errorAppVersionUnsupported.rawValue,
      "The error should use an app version unsupported error code"
    )
    XCTAssertEqual(
      error.message,
      "the app switch failed because the destination app is out of date",
      "The error should use an appropriate error message"
    )
  }

  func testRequestCompletionBlockWithNonHttpRequestCalledWithoutSuccessWithoutError() throws {
    let request = TestBridgeAPIRequest(url: sampleURL, scheme: "file")

    var capturedResponse: BridgeAPIResponse?
    let responseBlock: BridgeAPIResponseBlock = { response in
      capturedResponse = response
    }
    api.pendingRequest = request
    api.pendingRequestCompletionBlock = { _ in }

    let completion = api.bridgeAPIRequestCompletionBlock(request: request, completion: responseBlock)

    // Without Error
    completion(false, nil)
    assertPendingPropertiesCleared()

    XCTAssertTrue(
      capturedResponse?.request === request,
      "The response should contain the original request"
    )
    let error = try XCTUnwrap(capturedResponse?.error as? TestSDKError)
    XCTAssertEqual(
      error.type,
      .general,
      "The response should contain a general error"
    )
    XCTAssertEqual(
      error.code,
      CoreError.errorAppVersionUnsupported.rawValue,
      "The error should use an app version unsupported error code"
    )
    XCTAssertEqual(
      error.message,
      "the app switch failed because the destination app is out of date",
      "The error should use an appropriate error message"
    )
  }

  func testRequestCompletionBlockWithHttpRequestCalledWithoutSuccessWithError() throws {
    let request = TestBridgeAPIRequest(url: sampleURL, scheme: "https")

    var capturedResponse: BridgeAPIResponse?
    let responseBlock: BridgeAPIResponseBlock = { response in
      capturedResponse = response
    }

    api.pendingRequest = request
    api.pendingRequestCompletionBlock = { _ in }

    let completion = api.bridgeAPIRequestCompletionBlock(request: request, completion: responseBlock)

    // With Error
    completion(false, SampleError())
    assertPendingPropertiesCleared()

    XCTAssertTrue(
      capturedResponse?.request === request,
      "The response should contain the original request"
    )
    let error = try XCTUnwrap(capturedResponse?.error as? TestSDKError)
    XCTAssertEqual(
      error.type,
      .general,
      "The response should contain a general error"
    )
    XCTAssertEqual(
      error.code,
      CoreError.errorBrowserUnavailable.rawValue,
      "The error should use a browser unavailable error code"
    )
    XCTAssertEqual(
      error.message,
      "the app switch failed because the browser is unavailable",
      "The response should use an appropriate error message"
    )
  }

  func testRequestCompletionBlockWithHttpRequestCalledWithoutSuccessWithoutError() throws {
    let request = TestBridgeAPIRequest(url: sampleURL, scheme: "https")

    var capturedResponse: BridgeAPIResponse?
    let responseBlock: BridgeAPIResponseBlock = { response in
      capturedResponse = response
    }

    api.pendingRequest = request
    api.pendingRequestCompletionBlock = { _ in }

    let completion = api.bridgeAPIRequestCompletionBlock(request: request, completion: responseBlock)

    // Without Error
    completion(false, nil)
    assertPendingPropertiesCleared()

    XCTAssertTrue(
      capturedResponse?.request === request,
      "The response should contain the original request"
    )
    let error = try XCTUnwrap(capturedResponse?.error as? TestSDKError)
    XCTAssertEqual(
      error.type,
      .general,
      "The response should contain a general error"
    )
    XCTAssertEqual(
      error.code,
      CoreError.errorBrowserUnavailable.rawValue,
      "The error should use a browser unavailable error code"
    )
    XCTAssertEqual(
      error.message,
      "the app switch failed because the browser is unavailable",
      "The response should use an appropriate error message"
    )
  }

  // MARK: - Safari View Controller Delegate Methods

  func testSafariVcDidFinishWithPendingUrlOpener() throws {
    let urlOpener = FBSDKLoginManager()
    api.pendingURLOpener = urlOpener
    api.safariViewController = TestSafariViewController(url: sampleURL)

    // Setting a pending request so we can assert that it's nilled out upon cancellation
    api.pendingRequest = createSampleTestBridgeAPIRequest()

    // Funny enough there's no check that the safari view controller from the delegate
    // is the same instance stored in the safariViewController property
    let safariViewController = try XCTUnwrap(api.safariViewController)
    api.safariViewControllerDidFinish(safariViewController)

    XCTAssertNil(api.pendingURLOpener, "Should remove the reference to the pending url opener")
    XCTAssertNil(
      api.safariViewController,
      "Should remove the reference to the safari view controller when the delegate method is called"
    )

    XCTAssertNil(api.pendingRequest, "Should cancel the request")
    XCTAssertTrue(
      urlOpener.openURLWasCalled,
      "Should ask the opener to open a url (even though there is not one provided)"
    )
    XCTAssertNil(FBSDKLoginManager.capturedOpenURL, "The url opener should be called with nil arguments")
    XCTAssertNil(FBSDKLoginManager.capturedSourceApplication, "The url opener should be called with nil arguments")
    XCTAssertNil(FBSDKLoginManager.capturedAnnotation, "The url opener should be called with nil arguments")
  }

  func testSafariVcDidFinishWithoutPendingUrlOpener() throws {
    api.safariViewController = TestSafariViewController(url: sampleURL)

    // Setting a pending request so we can assert that it's nilled out upon cancellation
    api.pendingRequest = createSampleTestBridgeAPIRequest()

    // Funny enough there's no check that the safari view controller from the delegate
    // is the same instance stored in the safariViewController property
    let safariViewController = try XCTUnwrap(api.safariViewController)
    api.safariViewControllerDidFinish(safariViewController)

    XCTAssertNil(api.pendingURLOpener, "Should remove the reference to the pending url opener")
    XCTAssertNil(
      api.safariViewController,
      "Should remove the reference to the safari view controller when the delegate method is called"
    )

    XCTAssertNil(api.pendingRequest, "Should cancel the request")
    XCTAssertNil(FBSDKLoginManager.capturedOpenURL, "The url opener should not be called")
    XCTAssertNil(FBSDKLoginManager.capturedSourceApplication, "The url opener should not be called")
    XCTAssertNil(FBSDKLoginManager.capturedAnnotation, "The url opener should not be called")
  }

  // MARK: - ContainerViewController Delegate Methods

  func testViewControllerDidDisappearWithSafariViewController() {
    api.safariViewController = TestSafariViewController(url: sampleURL)
    let container = _ContainerViewController()

    // Setting a pending request so we can assert that it's nilled out upon cancellation
    api.pendingRequest = createSampleTestBridgeAPIRequest()

    api.viewControllerDidDisappear(container, animated: false)

    XCTAssertEqual(
      logger.capturedContents,
      """
      **ERROR**:
      The SFSafariViewController's parent view controller was dismissed.
      This can happen if you are triggering login from a UIAlertController. Instead, make sure your topmost view \
      controller will not be prematurely dismissed.
      """
    )
    XCTAssertNil(api.pendingRequest, "Should cancel the request")
  }

  func testViewControllerDidDisappearWithoutSafariViewController() {
    let container = _ContainerViewController()

    // Setting a pending request so we can assert that it's nilled out upon cancellation
    api.pendingRequest = createSampleTestBridgeAPIRequest()

    api.viewControllerDidDisappear(container, animated: false)

    XCTAssertNotNil(api.pendingRequest, "Should not cancel the request")
    XCTAssertNil(logger.capturedContents, "Expected nothing to be logged")
  }

  // MARK: - Bridge Response URL Handling

  func testHandlingBridgeResponseWithInvalidScheme() {
    stubBridgeApiResponseWithUrlCreation()
    appURLSchemeProvider.appURLScheme = "foo"

    XCTAssertFalse(
      api.handleBridgeAPIResponse(url: sampleURL, sourceApplication: ""),
      "Should not successfully handle bridge api response url with an invalid url scheme"
    )
    assertPendingPropertiesCleared()
  }

  func testHandlingBridgeResponseWithInvalidHost() throws {
    stubBridgeApiResponseWithUrlCreation()
    appURLSchemeProvider.appURLScheme = try XCTUnwrap(sampleURL.scheme)

    XCTAssertFalse(
      api.handleBridgeAPIResponse(url: sampleURL, sourceApplication: ""),
      "Should not successfully handle bridge api response url with an invalid url host"
    )
    assertPendingPropertiesCleared()
  }

  func testHandlingBridgeResponseWithMissingRequest() throws {
    stubBridgeApiResponseWithUrlCreation()
    appURLSchemeProvider.appURLScheme = try XCTUnwrap(validBridgeResponseURL.scheme)

    XCTAssertFalse(
      api.handleBridgeAPIResponse(url: validBridgeResponseURL, sourceApplication: ""),
      "Should not successfully handle bridge api response url with a missing request"
    )
    assertPendingPropertiesCleared()
  }

  func testHandlingBridgeResponseWithMissingCompletionBlock() throws {
    stubBridgeApiResponseWithUrlCreation()
    appURLSchemeProvider.appURLScheme = try XCTUnwrap(validBridgeResponseURL.scheme)
    api.pendingRequest = TestBridgeAPIRequest(url: sampleURL)

    XCTAssertTrue(
      api.handleBridgeAPIResponse(url: validBridgeResponseURL, sourceApplication: ""),
      "Should successfully handle bridge api response url with a missing completion block"
    )
    assertPendingPropertiesCleared()
  }

  func testHandlingBridgeResponseWithBridgeResponse() throws {
    let response = BridgeAPIResponse(
      request: TestBridgeAPIRequest(url: sampleURL),
      responseParameters: [:],
      cancelled: false,
      error: nil
    )
    responseFactory.stubbedResponse = response
    appURLSchemeProvider.appURLScheme = try XCTUnwrap(validBridgeResponseURL.scheme)
    api.pendingRequest = TestBridgeAPIRequest(url: sampleURL)

    var capturedResponse: BridgeAPIResponse?
    api.pendingRequestCompletionBlock = { response in
      capturedResponse = response
    }

    XCTAssertTrue(
      api.handleBridgeAPIResponse(url: validBridgeResponseURL, sourceApplication: ""),
      "Should successfully handle creation of a bridge api response"
    )

    XCTAssertEqual(capturedResponse, response, "Should invoke the completion with the expected bridge api response")
    assertPendingPropertiesCleared()
  }

  func testHandlingBridgeResponseWithBridgeError() throws {
    let response = BridgeAPIResponse(
      request: TestBridgeAPIRequest(url: sampleURL),
      responseParameters: [:],
      cancelled: false,
      error: SampleError()
    )
    responseFactory.stubbedResponse = response
    appURLSchemeProvider.appURLScheme = try XCTUnwrap(validBridgeResponseURL.scheme)
    api.pendingRequest = TestBridgeAPIRequest(url: sampleURL)

    var capturedResponse: BridgeAPIResponse?
    api.pendingRequestCompletionBlock = { response in
      capturedResponse = response
    }

    XCTAssertTrue(
      api.handleBridgeAPIResponse(url: validBridgeResponseURL, sourceApplication: ""),
      "Should retry creation of a bridge api response if the first attempt has an error"
    )
    XCTAssertEqual(capturedResponse, response, "Should invoke the completion with the expected bridge api response")
    assertPendingPropertiesCleared()
  }

  func testHandlingBridgeResponseWithMissingResponseMissingError() throws {
    let response = BridgeAPIResponse(
      request: TestBridgeAPIRequest(url: sampleURL),
      responseParameters: [:],
      cancelled: false,
      error: nil
    )

    responseFactory.stubbedResponse = response
    responseFactory.shouldFailCreation = true
    appURLSchemeProvider.appURLScheme = try XCTUnwrap(validBridgeResponseURL.scheme)
    api.pendingRequest = TestBridgeAPIRequest(url: sampleURL)

    var capturedResponse: BridgeAPIResponse?
    api.pendingRequestCompletionBlock = { response in
      capturedResponse = response
    }

    XCTAssertFalse(
      api.handleBridgeAPIResponse(url: validBridgeResponseURL, sourceApplication: ""),
      "Should return false when a bridge response cannot be created"
    )
    XCTAssertNil(capturedResponse, "Should not invoke pending completion handler")
    assertPendingPropertiesCleared()
  }

  // MARK: - Helpers

  func createSampleTestBridgeAPIRequest() -> TestBridgeAPIRequest {
    TestBridgeAPIRequest(
      url: sampleURL,
      protocolType: FBSDKBridgeAPIProtocolType.web,
      scheme: "1"
    )
  }

  func stubBridgeApiResponseWithUrlCreation() {
    let response = BridgeAPIResponse(
      request: TestBridgeAPIRequest(url: sampleURL),
      responseParameters: [:],
      cancelled: false,
      error: nil
    )
    responseFactory.stubbedResponse = response
  }

  func assertPendingPropertiesCleared(file: StaticString = #file, line: UInt = #line) {
    XCTAssertNil(
      api.pendingRequest,
      "Should clear the pending request",
      file: file,
      line: line
    )
    XCTAssertNil(
      api.pendingRequestCompletionBlock,
      "Should clear the pending request completion block",
      file: file,
      line: line
    )
  }

  func assertPendingPropertiesNotCleared(file: StaticString = #file, line: UInt = #line) {
    XCTAssertNotNil(
      api.pendingRequest,
      "Should not clear the pending request",
      file: file,
      line: line
    )
    XCTAssertNotNil(
      api.pendingRequestCompletionBlock,
      "Should not clear the pending request completion block",
      file: file,
      line: line
    )
  }
}

final class TestSafariViewController: SFSafariViewController {}


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

@testable import FBSDKCoreKit

import Foundation
import TestTools

final class BridgeAPIProtocolNativeV1Tests: XCTestCase {
  let actionID = UUID().uuidString
  let scheme = UUID().uuidString
  let methodName = UUID().uuidString
  let methodVersion = UUID().uuidString
  let sampleURL = URL(string: "https://example.com")! // swiftlint:disable:this force_unwrapping
  // swiftlint:disable implicitly_unwrapped_optional
  var pasteboard: TestPasteboard!
  var errorFactory: TestErrorFactory!
  var protocolNativeV1: _BridgeAPIProtocolNativeV1!
  var bundle: TestBundle!
  var notificationCenter: TestNotificationCenter!
  var internalUtility: InternalUtilityProtocol!

  // swiftlint:enable implicitly_unwrapped_optional

  override func setUp() {
    super.setUp()
    pasteboard = TestPasteboard()
    errorFactory = TestErrorFactory()
    bundle = TestBundle()
    internalUtility = InternalUtility.shared
    notificationCenter = TestNotificationCenter()

    _BridgeAPIProtocolNativeV1.setDependencies(
      .init(
        errorFactory: errorFactory,
        bundle: bundle,
        notificationDeliverer: notificationCenter,
        internalUtility: internalUtility
      )
    )
    protocolNativeV1 = _BridgeAPIProtocolNativeV1(appScheme: scheme)
  }

  override func tearDown() {
    errorFactory = nil
    bundle = nil
    notificationCenter = nil
    internalUtility = nil
    pasteboard = nil
    protocolNativeV1 = nil
    _BridgeAPIProtocolNativeV1.resetDependencies()

    super.tearDown()
  }

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

    XCTAssertTrue(
      dependencies.errorFactory is _ErrorFactory,
      .defaultDependency("an error factoring", for: "error creating")
    )

    XCTAssertIdentical(
      dependencies.bundle as AnyObject,
      Bundle.main,
      .defaultDependency("a dictionary provider", for: "providing dictionaries")
    )

    XCTAssertIdentical(
      dependencies.notificationDeliverer as AnyObject,
      NotificationCenter.default,
      .defaultDependency("a notification deliverer", for: "delivering notifications")
    )

    XCTAssertIdentical(
      dependencies.internalUtility as AnyObject,
      InternalUtility.shared,
      .defaultDependency("internal utility", for: "creating urls")
    )
  }

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

    XCTAssertIdentical(
      dependencies.errorFactory as AnyObject,
      errorFactory,
      .customDependency(for: "error creating")
    )

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

    XCTAssertIdentical(
      dependencies.notificationDeliverer as AnyObject,
      notificationCenter,
      .customDependency(for: "delivering notifications")
    )

    XCTAssertIdentical(
      dependencies.internalUtility as AnyObject,
      internalUtility,
      .customDependency(for: "creating urls")
    )
  }

  func testRequestURL() {
    let parameters = [
      "api_key_1": "value1",
      "api_key_2": "value2",
    ]

    do {
      let requestURL = try protocolNativeV1.requestURL(
        actionID: actionID,
        scheme: scheme,
        methodName: methodName,
        parameters: parameters
      )

      let expectedPrefix = "\(scheme)://dialog/\(methodName)?"
      XCTAssertTrue(requestURL.absoluteString.hasPrefix(expectedPrefix))

      /* Due to the non-deterministic order of Dictionary->JSON serialization,
       we cannot do string comparisons to verify. */
      let queryParameters = Utility.dictionary(withQuery: requestURL.query ?? "")
      let expectedKeys = Set(["bridge_args", "method_args"])
      XCTAssertEqual(Set(queryParameters.keys), expectedKeys)
      do {
        let basicParameters = try BasicUtility.object(forJSONString: queryParameters["method_args", default: ""])
        XCTAssertEqual(basicParameters as? [String: String], parameters)
      } catch {
        XCTFail("Unexpected error thrown: \(error)")
      }
    } catch {
      XCTFail("Unexpected error thrown: \(error)")
    }
  }

  func testNilResponseParameters() {
    var cancelled: ObjCBool = true

    do {
      let response = try protocolNativeV1.responseParameters(
        actionID: actionID,
        queryParameters: [:],
        cancelled: &cancelled
      )
      XCTAssertNil(response)
    } catch {
      XCTAssertNotNil(error)
    }
    XCTAssertFalse(cancelled.boolValue)

    do {
      let response = try protocolNativeV1.responseParameters(
        actionID: actionID,
        queryParameters: [:],
        cancelled: &cancelled
      )
      XCTAssertNil(response)
    } catch {
      XCTAssertNotNil(error)
    }
    XCTAssertFalse(cancelled.boolValue)
  }

  func testEmptyResponseParameters() {
    var cancelled: ObjCBool = true
    var queryParameters: [String: Any] = [
      "bridge_args": [
        "action_id": actionID,
      ],
      "method_results": [:],
    ]
    queryParameters = stubEncodeQueryParameters(queryParameters)

    do {
      let response = try protocolNativeV1.responseParameters(
        actionID: actionID,
        queryParameters: [:],
        cancelled: &cancelled
      )

      XCTAssertEqual(response as? [String: String], [:])
    } catch {
      XCTAssertNotNil(error)
    }
    XCTAssertFalse(cancelled.boolValue)
  }

  func testResponseParameters() {
    var cancelled: ObjCBool = true
    let responseParameters: [String: Any] = [
      "result_key_1": 1,
      "result_key_2": "two",
      "result_key_3": [
        "result_key_4": 4,
        "result_key_5": "five",
      ],
    ]
    var queryParameters: [String: Any] = [
      "bridge_args": [
        "action_id": actionID,
      ],
      "method_results": responseParameters,
    ]
    queryParameters = stubEncodeQueryParameters(queryParameters)

    do {
      let response: [String: Any] = try protocolNativeV1.responseParameters(
        actionID: actionID,
        queryParameters: queryParameters,
        cancelled: &cancelled
      )
      XCTAssertEqual(response as NSObject, responseParameters as NSObject)
    } catch {
      XCTFail("Unexpected error thrown: \(error)")
    }
  }

  func testInvalidActionID() {
    var cancelled: ObjCBool = true
    let responseParameters: [String: Any] = [
      "result_key_1": 1,
      "result_key_2": "two",
      "result_key_3": [
        "result_key_4": 4,
        "result_key_5": "five",
      ],
    ]
    var queryParameters: [String: Any] = [
      "bridge_args": [
        "action_id": UUID().uuidString,
      ],
      "method_results": responseParameters,
    ]
    queryParameters = stubEncodeQueryParameters(queryParameters)
    do {
      let response = try protocolNativeV1.responseParameters(
        actionID: actionID,
        queryParameters: [:],
        cancelled: &cancelled
      )
      XCTAssertNil(response)
    } catch {
      XCTAssertNotNil(error)
    }
    XCTAssertFalse(cancelled.boolValue)
  }

  func testInvalidBridgeArgs() throws {
    var cancelled: ObjCBool = true

    let bridgeArgs = "this is an invalid bridge_args value"
    var queryParameters: [String: Any] = [
      "bridge_args": bridgeArgs,
      "method_results": [
        "result_key_1": 1,
        "result_key_2": "two",
      ],
    ]
    queryParameters = stubEncodeQueryParameters(queryParameters)
    do {
      let response = try protocolNativeV1.responseParameters(
        actionID: actionID,
        queryParameters: queryParameters,
        cancelled: &cancelled
      )
      XCTAssertNil(response)
    } catch {
      let error = try XCTUnwrap(error as? TestSDKError)
      XCTAssertEqual(error.type, .invalidArgument)
      XCTAssertEqual(error.code, TestSDKError.testErrorCode)
      XCTAssertEqual(error.domain, TestSDKError.testErrorDomain)
      XCTAssertEqual(error.name, "bridge_args")
      XCTAssertEqual(error.value as? String, bridgeArgs)
      XCTAssertNotNil(error.message)
      XCTAssertNotNil(error.underlyingError)
    }
    XCTAssertFalse(cancelled.boolValue)
  }

  func testInvalidMethodResults() throws {
    var cancelled: ObjCBool = true

    let methodResults = "this is an invalid method_results value"
    var queryParameters: [String: Any] = [
      "bridge_args": [
        "action_id": actionID,
      ],
      "method_results": methodResults,
    ]
    queryParameters = stubEncodeQueryParameters(queryParameters)
    do {
      let response = try protocolNativeV1.responseParameters(
        actionID: actionID,
        queryParameters: queryParameters,
        cancelled: &cancelled
      )
      XCTAssertNil(response)
    } catch {
      let error = try XCTUnwrap(error as? TestSDKError)
      XCTAssertEqual(error.type, .invalidArgument)
      XCTAssertEqual(error.code, TestSDKError.testErrorCode)
      XCTAssertEqual(error.domain, TestSDKError.testErrorDomain)
      XCTAssertEqual(error.name, "method_results")
      XCTAssertEqual(error.value as? String, methodResults)
      XCTAssertNotNil(error.message)
      XCTAssertNotNil(error.underlyingError)
    }
    XCTAssertFalse(cancelled.boolValue)
  }

  func testResultError() throws {
    var cancelled: ObjCBool = true

    let code = 42
    let domain = "my custom error domain"
    let userInfo: [String: Any] = [
      "key_1": 1,
      "key_2": "two",
    ]
    var queryParameters: [String: Any] = [
      "bridge_args": [
        "action_id": actionID,
        "error": [
          "code": code,
          "domain": domain,
          "user_info": userInfo,
        ],
      ],
      "method_results": [
        "result_key_1": 1,
        "result_key_2": "two",
      ],
    ]
    queryParameters = stubEncodeQueryParameters(queryParameters)
    do {
      let response = try protocolNativeV1.responseParameters(
        actionID: actionID,
        queryParameters: queryParameters,
        cancelled: &cancelled
      )
      XCTAssertNil(response)
    } catch {
      let error = try XCTUnwrap(error as? TestSDKError)
      XCTAssertEqual(error.code, code)
      XCTAssertEqual(error.domain, domain)
      XCTAssertEqual(error.userInfo["key_1"] as? Int, 1)
      XCTAssertEqual(error.userInfo["key_2"] as? String, "two")
    }
    XCTAssertFalse(cancelled.boolValue)
  }

  func testResultCancel() {
    var cancelled: ObjCBool = false

    var queryParameters: [String: Any] = [
      "bridge_args": [
        "action_id": actionID,
      ],
      "method_results": [
        "completionGesture": "cancel",
      ],
    ]
    queryParameters = stubEncodeQueryParameters(queryParameters)
    do {
      let response = try protocolNativeV1.responseParameters(
        actionID: actionID,
        queryParameters: queryParameters,
        cancelled: &cancelled
      )
      XCTAssertNotNil(response)
    } catch {
      XCTFail("Unexpected error thrown: \(error)")
    }
    XCTAssertTrue(cancelled.boolValue)
  }

  func testRequestParametersWithDataJSON() {
    let protocolNative = _BridgeAPIProtocolNativeV1(
      appScheme: scheme,
      pasteboard: nil,
      dataLengthThreshold: UInt.max,
      shouldIncludeAppIcon: false
    )

    let parameters: [String: Any] = [
      "api_key_1": "value1",
      "api_key_2": "value2",
      "data": stubData(),
    ]

    do {
      let requestURL = try protocolNative.requestURL(
        actionID: actionID,
        scheme: scheme,
        methodName: methodName,
        parameters: parameters
      )

      let expectedPrefix = "\(scheme)://dialog/\(methodName)?"
      XCTAssertTrue(requestURL.absoluteString.hasPrefix(expectedPrefix) == true)
      /* Due to the non-deterministic order of Dictionary->JSON serialization,
       we cannot do string comparisons to verify. */
      let queryParameters = Utility.dictionary(withQuery: requestURL.query ?? "")
      let expectedKeys = Set(["bridge_args", "method_args"])
      XCTAssertEqual(Set(queryParameters.keys), expectedKeys)
      var expectedMethodArgs = parameters
      expectedMethodArgs["data"] = stubDataSerialized(parameters["data"] as? Data)

      do {
        let methodArgs = try BasicUtility.object(forJSONString: queryParameters["method_args"] ?? "") as? [String: Any]
        XCTAssertEqual(methodArgs as NSObject?, expectedMethodArgs as NSObject)
        if let parseMethodData = methodArgs?["data"] as? [String: Any],
           let decodedData = parseMethodData["fbAppBridgeType_jsonReadyValue"] {
          let baseDecodedData = Base64.decode(asData: decodedData as? String)
          XCTAssertNotNil(baseDecodedData)
          XCTAssertEqual(baseDecodedData, parameters["data"] as? Data)
        } else {
          XCTFail("Failed to parse method arguments")
        }
      } catch {
        XCTFail("Unexpected error thrown: \(error)")
      }
    } catch {
      XCTFail("Unexpected error thrown: \(error)")
    }
  }

  func testRequestParametersWithImageJSON() {
    let protocolNative = _BridgeAPIProtocolNativeV1(
      appScheme: scheme,
      pasteboard: nil,
      dataLengthThreshold: UInt.max,
      shouldIncludeAppIcon: false
    )

    let parameters: [String: Any] = [
      "api_key_1": "value1",
      "api_key_2": "value2",
      "image": stubImage(),
    ]

    do {
      let requestURL = try protocolNative.requestURL(
        actionID: actionID,
        scheme: scheme,
        methodName: methodName,
        parameters: parameters
      )

      let expectedPrefix = "\(scheme)://dialog/\(methodName)?"
      XCTAssertTrue(requestURL.absoluteString.hasPrefix(expectedPrefix) == true)
      /* Due to the non-deterministic order of Dictionary->JSON serialization,
       we cannot do string comparisons to verify. */
      let queryParameters = Utility.dictionary(withQuery: requestURL.query ?? "")
      let expectedKeys = Set(["bridge_args", "method_args"])
      XCTAssertEqual(Set(queryParameters.keys), expectedKeys)
      var expectedMethodArgs = parameters
      expectedMethodArgs["image"] = stubImageSerialized(parameters["image"] as? UIImage)
      do {
        let methodArgs = try BasicUtility.object(
          forJSONString: queryParameters["method_args"] ?? "") as? [String: Any]
        XCTAssertEqual(methodArgs as NSObject?, expectedMethodArgs as NSObject)
        if let parseMethodData = methodArgs?["image"] as? [String: Any],
           let decodedData = parseMethodData["fbAppBridgeType_jsonReadyValue"] {
          guard let baseDecodedData = Base64.decode(asData: decodedData as? String) else {
            return XCTAssertNil("Failed to decode data from Base64")
          }
          XCTAssertNotNil(UIImage(data: baseDecodedData))
        } else {
          XCTFail("Failed to parse method arguments")
        }
      } catch {
        XCTFail("Unexpected error thrown: \(error)")
      }
    } catch {
      XCTFail("Unexpected error thrown: \(error)")
    }
  }

  func testRequestParametersWithDataPasteboard() {
    let pasteboardName = UUID().uuidString
    pasteboard.name = pasteboardName
    let data = stubData()

    let protocolNative = _BridgeAPIProtocolNativeV1(
      appScheme: scheme,
      pasteboard: pasteboard,
      dataLengthThreshold: 0,
      shouldIncludeAppIcon: false
    )

    let parameters: [String: Any] = [
      "api_key_1": "value1",
      "api_key_2": "value2",
      "data": data,
    ]

    do {
      let requestURL = try protocolNative.requestURL(
        actionID: actionID,
        scheme: scheme,
        methodName: methodName,
        parameters: parameters
      )

      XCTAssertEqual(pasteboard.capturedData, data as Data?)
      XCTAssertEqual(pasteboard.capturedPasteboardType, "com.facebook.Facebook.FBAppBridgeType")

      let expectedPrefix = "\(scheme)://dialog/\(methodName)?"
      XCTAssertTrue(requestURL.absoluteString.hasPrefix(expectedPrefix) == true)
      /* Due to the non-deterministic order of Dictionary->JSON serialization,
       we cannot do string comparisons to verify. */
      let queryParameters = Utility.dictionary(withQuery: requestURL.query ?? "")
      let expectedKeys = Set(["bridge_args", "method_args"])
      XCTAssertEqual(Set(queryParameters.keys), expectedKeys)
      var expectedMethodArgs = parameters
      expectedMethodArgs["data"] = stubDataContainer(withPasteboardName: pasteboardName, tag: "data")
      do {
        let methodArgs = try BasicUtility.object(
          forJSONString: queryParameters["method_args"] ?? "") as? [String: Any]
        XCTAssertEqual(methodArgs as NSObject?, expectedMethodArgs as NSObject)
      } catch {
        XCTFail("Unexpected error thrown: \(error)")
      }
    } catch {
      XCTFail("Unexpected error thrown: \(error)")
    }
  }

  func testRequestParametersWithImagePasteboard() {
    let pasteboardName = UUID().uuidString
    pasteboard.name = pasteboardName
    let image = stubImage()
    let data = stubData(with: image)

    let protocolNative = _BridgeAPIProtocolNativeV1(
      appScheme: scheme,
      pasteboard: pasteboard,
      dataLengthThreshold: 0,
      shouldIncludeAppIcon: false
    )

    let parameters: [String: Any] = [
      "api_key_1": "value1",
      "api_key_2": "value2",
      "image": image,
    ]

    do {
      let requestURL = try protocolNative.requestURL(
        actionID: actionID,
        scheme: scheme,
        methodName: methodName,
        parameters: parameters
      )

      XCTAssertEqual(pasteboard.capturedData, data as Data?)
      XCTAssertEqual(pasteboard.capturedPasteboardType, "com.facebook.Facebook.FBAppBridgeType")

      let expectedPrefix = "\(scheme)://dialog/\(methodName)?"
      XCTAssertTrue(requestURL.absoluteString.hasPrefix(expectedPrefix) == true)
      /* Due to the non-deterministic order of Dictionary->JSON serialization,
       we cannot do string comparisons to verify. */
      let queryParameters = Utility.dictionary(withQuery: requestURL.query ?? "")
      let expectedKeys = Set(["bridge_args", "method_args"])
      XCTAssertEqual(Set(queryParameters.keys), expectedKeys)
      var expectedMethodArgs = parameters
      expectedMethodArgs["image"] = stubDataContainer(withPasteboardName: pasteboardName, tag: "png")
      do {
        let methodArgs = try BasicUtility.object(
          forJSONString: queryParameters["method_args"] ?? "") as? [String: Any]
        XCTAssertEqual(methodArgs as NSObject?, expectedMethodArgs as NSObject)
      } catch {
        XCTFail("Unexpected error thrown: \(error)")
      }
    } catch {
      XCTFail("Unexpected error thrown: \(error)")
    }
  }

  // MARK: - Helpers

  func stubEncodeQueryParameters(_ queryParameters: [String: Any]) -> [String: Any] {
    let encoded = [:] as NSMutableDictionary
    for (key, value) in queryParameters {
      do {
        try BasicUtility.dictionary(
          encoded,
          setJSONStringFor: value,
          forKey: key as NSCopying
        )
      } catch {
        TypeUtility.dictionary(
          encoded,
          setObject: value,
          forKey: key as NSCopying
        )
      }
    }
    return encoded as? [String: Any] ?? [:]
  }

  func stubData() -> NSMutableData {
    if let data = NSMutableData(length: 1024) {
      arc4random_buf(data.mutableBytes, data.count)
      return data
    }
    return NSMutableData()
  }

  func stubDataContainer(withPasteboardName pasteboardName: String?, tag: String?) -> [String: Any]? {
    [
      "isPasteboard": NSNumber(value: true),
      "tag": tag ?? "",
      "fbAppBridgeType_jsonReadyValue": pasteboardName ?? "",
    ]
  }

  func stubDataSerialized(_ data: Data?) -> [String: Any]? {
    stubDataSerialized(data, tag: "data")
  }

  func stubDataSerialized(_ data: Data?, tag: String?) -> [String: Any]? {
    guard let string = data?.base64EncodedString() else { return nil }

    return [
      "isBase64": NSNumber(value: true),
      "tag": tag ?? "",
      "fbAppBridgeType_jsonReadyValue": string,
    ]
  }

  func stubData(with image: UIImage?) -> Data? {
    image?.jpegData(compressionQuality: Settings.shared.jpegCompressionQuality)
  }

  func stubImage() -> UIImage {
    UIGraphicsBeginImageContext(CGSize(width: 10.0, height: 10.0))
    let context = UIGraphicsGetCurrentContext()
    UIColor.red.setFill()
    context?.fill(CGRect(x: 0.0, y: 0.0, width: 5.0, height: 5.0))
    UIColor.green.setFill()
    context?.fill(CGRect(x: 5.0, y: 0.0, width: 5.0, height: 5.0))
    UIColor.blue.setFill()
    context?.fill(CGRect(x: 5.0, y: 5.0, width: 5.0, height: 5.0))
    UIColor.yellow.setFill()
    context?.fill(CGRect(x: 0.0, y: 5.0, width: 5.0, height: 5.0))
    let imageRef = context?.makeImage()
    UIGraphicsEndImageContext()
    var image = UIImage()
    if let imageRef: CGImage = imageRef {
      image = UIImage(cgImage: imageRef)
    }
    return image
  }

  func stubImageSerialized(_ image: UIImage?) -> [String: Any]? {
    let data = stubData(with: image)
    return stubDataSerialized(data, tag: "png")
  }
}

// MARK: - Assumptions

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

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


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

@testable import FBSDKCoreKit

import XCTest

final class BridgeAPIProtocolWebV1Tests: XCTestCase {

  enum Keys {
    static let actionID = "action_id"
    static let bridgeArgs = "bridge_args"
    static let completionGesture = "completionGesture"
    static let didComplete = "didComplete"
    static let display = "display"
    static let errorCode = "error_code"
    static let redirectURI = "redirect_uri"
  }

  enum Values {
    static let actionID = "123"
    static let cancel = "cancel"
    static let cancellationErrorCode = 4201
    static let methodName = "open"
    static let redirectURI = "fb://bridge/open?bridge_args=%7B%22action_id%22%3A%22123%22%7D"
    static let touch = "touch"
  }

  enum QueryParameters {
    static let withoutBridgeArgs: [String: Any] = [:]
    static let withEmptyBridgeArgs: [String: Any] = [Keys.bridgeArgs: ""]
    static let valid = withBridgeArgs(responseActionID: Values.actionID)

    static func jsonString(actionID: String) -> String {
      let data = try! JSONSerialization.data( // swiftlint:disable:this force_try
        withJSONObject: [Keys.actionID: actionID], options: []
      )
      return String(data: data, encoding: .utf8)! // swiftlint:disable:this force_unwrapping
    }

    static func withBridgeArgs(responseActionID: String) -> [String: Any] {
      [Keys.bridgeArgs: jsonString(actionID: responseActionID)]
    }

    static func validWithErrorCode(_ code: Int) -> [String: Any] {
      [
        Keys.errorCode: code,
        Keys.bridgeArgs: jsonString(actionID: Values.actionID),
      ]
    }
  }

  // swiftlint:disable implicitly_unwrapped_optional
  var bridge: BridgeAPIProtocolWebV1!
  var errorFactory: ErrorCreating!
  var internalUtility: TestInternalUtility!
  // swiftlint:enable implicitly_unwrapped_optional

  override func setUp() {
    super.setUp()

    errorFactory = TestErrorFactory()
    internalUtility = TestInternalUtility()
    bridge = BridgeAPIProtocolWebV1(
      errorFactory: errorFactory,
      internalUtility: internalUtility
    )
  }

  override func tearDown() {
    errorFactory = nil
    internalUtility = nil
    bridge = nil

    super.tearDown()
  }

  func testInitialization() {
    XCTAssertTrue(
      bridge.errorFactory === errorFactory,
      "Should be able to create an instance with an error factory"
    )
    XCTAssertTrue(
      bridge.internalUtility === internalUtility,
      "Should be able to create an instance with an error factory"
    )
  }

  func testDefaultDependencies() throws {
    bridge = BridgeAPIProtocolWebV1()

    let reporter = try _ErrorFactory.getDependencies().reporter
    XCTAssertTrue(
      reporter === ErrorReporter.shared,
      "The default factory should use the shared error reporter"
    )
    XCTAssertTrue(
      bridge.internalUtility === InternalUtility.shared,
      "The bridge should use the shared internal utility by default"
    )
  }

  func testCreatingURLWithAllFields() throws {
    // For first use of internal utility
    let redirectURL = SampleURLs.valid.appendingPathComponent("redirect")
    internalUtility.stubbedAppURL = redirectURL

    // For second use of internal utility
    let facebookURL = SampleURLs.valid.appendingPathComponent("facebook")
    internalUtility.stubbedFacebookURL = facebookURL

    let queryParameters = ["a": "b"]

    let url = try bridge.requestURL(
      actionID: Values.actionID,
      scheme: URLScheme.https.rawValue,
      methodName: Values.methodName,
      parameters: queryParameters
    )

    let appURLMessage = "The protocol should use its internal utility to create an app URL"
    XCTAssertEqual(internalUtility.capturedAppURLHost, "bridge", appURLMessage)
    XCTAssertEqual(internalUtility.capturedAppURLPath, Values.methodName, appURLMessage)
    XCTAssertEqual(
      internalUtility.capturedAppURLQueryParameters,
      ["bridge_args": QueryParameters.jsonString(actionID: Values.actionID)],
      appURLMessage
    )

    let facebookURLMessage = "The protocol should use its internal utility to create a Facebook URL"
    var expectedParameters = queryParameters
    expectedParameters["display"] = "touch"
    expectedParameters["redirect_uri"] = redirectURL.absoluteString

    XCTAssertEqual(internalUtility.capturedFacebookURLHostPrefix, "m", facebookURLMessage)
    XCTAssertEqual(
      internalUtility.capturedFacebookURLPath,
      "/dialog/\(Values.methodName)",
      facebookURLMessage
    )
    XCTAssertEqual(
      internalUtility.capturedFacebookURLQueryParameters,
      expectedParameters,
      facebookURLMessage
    )

    XCTAssertEqual(
      url,
      facebookURL,
      "The protocol should return the Facebook URL created by its internal utility"
    )
  }

  func testResponseParametersWithUnknownErrorCode() {
    XCTAssertNil(
      try? bridge.responseParameters(
        actionID: Values.actionID,
        queryParameters: QueryParameters.validWithErrorCode(123),
        cancelled: nil
      ),
      "Should not create response parameters when there is an unknown error code"
    )
  }

  func testResponseParametersWithoutBridgeParameters() {
    XCTAssertNil(
      try? bridge.responseParameters(
        actionID: Values.actionID,
        queryParameters: QueryParameters.withoutBridgeArgs,
        cancelled: nil
      ),
      "Should not create response parameters when there are no bridge arguments"
    )
  }

  func testResponseParametersWithoutActionID() {
    XCTAssertNil(
      try? bridge.responseParameters(
        actionID: Values.actionID,
        queryParameters: QueryParameters.withEmptyBridgeArgs,
        cancelled: nil
      ),
      "Should not create response parameters when there is no action id"
    )
  }

  func testResponseParametersWithMismatchedResponseActionID() {
    XCTAssertNil(
      try? bridge.responseParameters(
        actionID: Values.actionID,
        queryParameters: QueryParameters.withBridgeArgs(responseActionID: "foo"),
        cancelled: nil
      ),
      "Should not create response parameters when the action IDs do not match"
    )
  }

  func testResponseParametersWithMatchingResponseActionID() {
    guard let response = try? bridge.responseParameters(
      actionID: Values.actionID,
      queryParameters: QueryParameters.valid,
      cancelled: nil
    ) else {
      return XCTFail("Should create a valid response")
    }

    XCTAssertEqual(
      response as? [String: Int],
      [Keys.didComplete: 1],
      "Should indicate that the response completed"
    )
  }

  func testResponseParametersWithCancellationErrorCode() {
    guard let response = try? bridge.responseParameters(
      actionID: Values.actionID,
      queryParameters: QueryParameters.validWithErrorCode(Values.cancellationErrorCode),
      cancelled: nil
    ) else {
      return XCTFail("Should create a valid response")
    }

    XCTAssertEqual(
      response as? [String: String],
      [Keys.completionGesture: Values.cancel],
      "Should indicate a cancelation when there's a cancellation error code"
    )
  }
}


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

@testable import FBSDKCoreKit

import TestTools
import XCTest

final class BridgeAPIProtocolWebV2Tests: XCTestCase {

  enum Keys {
    static let actionID = "action_id"
    static let bridgeArgs = "bridge_args"
    static let redirectURL = "redirect_url"
    static let iosBundleID = "ios_bundle_id"
  }

  enum Values {
    static let actionID = "123"
    static let methodName = "open"
    static let scheme = URLScheme.https
    static let bundleIdentifier = "bundle.identifier"
  }

  let validQueryParameters = ["Foo": "Bar"]

  // swiftlint:disable implicitly_unwrapped_optional
  var bridge: BridgeAPIProtocolWebV2!
  var serverConfigurationProvider: _ServerConfigurationProviding!
  var nativeBridge: TestBridgeAPIProtocol!
  var errorFactory: ErrorCreating!
  var internalUtility: TestInternalUtility!
  var bundle: TestBundle!
  // swiftlint:enable implicitly_unwrapped_optional

  override func setUp() {
    super.setUp()

    serverConfigurationProvider = TestServerConfigurationProvider()
    nativeBridge = TestBridgeAPIProtocol()
    errorFactory = TestErrorFactory()
    internalUtility = TestInternalUtility()
    bundle = TestBundle()
    bundle.fb_bundleIdentifier = Values.bundleIdentifier
    bridge = BridgeAPIProtocolWebV2(
      serverConfigurationProvider: serverConfigurationProvider,
      nativeBridge: nativeBridge,
      errorFactory: errorFactory,
      internalUtility: internalUtility,
      infoDictionaryProvider: bundle
    )
  }

  override func tearDown() {
    serverConfigurationProvider = nil
    nativeBridge = nil
    errorFactory = nil
    internalUtility = nil
    bundle = nil
    bridge = nil

    super.tearDown()
  }

  func testInitializing() {
    XCTAssertTrue(
      bridge.serverConfigurationProvider is TestServerConfigurationProvider,
      "Should be able to create with a custom server configuration provider"
    )
    XCTAssertTrue(
      bridge.nativeBridge === nativeBridge,
      "Should be able to create with a custom native bridge"
    )
    XCTAssertTrue(
      bridge.errorFactory === errorFactory,
      "Should be able to create with a custom error factory"
    )
    XCTAssertTrue(
      bridge.internalUtility === internalUtility,
      "Should be able to create with a custom internal utility"
    )
    XCTAssertTrue(
      bridge.infoDictionaryProvider === bundle,
      "Should be able to create with a custom info dictionary provider"
    )
  }

  func testDefaultDependencies() throws {
    bridge = BridgeAPIProtocolWebV2()

    XCTAssertTrue(
      bridge.serverConfigurationProvider is _ServerConfigurationManager,
      "Should use the expected default server configuration provider"
    )
    XCTAssertTrue(
      bridge.nativeBridge is _BridgeAPIProtocolNativeV1,
      "Should use the expected default native bridge"
    )

    let reporter = try _ErrorFactory.getDependencies().reporter
    XCTAssertTrue(
      reporter === ErrorReporter.shared,
      "The error factory should use the shared error reporter"
    )
    XCTAssertTrue(
      bridge.internalUtility === InternalUtility.shared,
      "Should use the expected default internal utility"
    )
    XCTAssertTrue(
      bridge.infoDictionaryProvider === Bundle.main,
      "Should use the expected default info dictionary provider"
    )
  }

  // MARK: - Request URL

  func testRequestURLWithoutServerConfiguration() {
    XCTAssertNil(
      try? bridge.requestURL(
        actionID: "",
        scheme: "",
        methodName: "",
        parameters: [:]
      ),
      "Should not create a url without a server configuration"
    )
  }

  func testRequestURLWithoutDialog() {
    let url = try? bridge.requestURL(
      actionID: "",
      scheme: "",
      methodName: "Foo",
      parameters: [:]
    )

    XCTAssertNil(
      url,
      "Should not create a url if the server configuration does not provide a dialog"
    )
  }

  func testRequestURLWithoutMatchingDialogForMethodName() {
    stubServerConfigurationWithDialog(named: "Bar")
    let url = try? bridge.requestURL(
      actionID: "",
      scheme: "",
      methodName: "Foo",
      parameters: [:]
    )

    XCTAssertNil(
      url,
      "Should not create a url if the server configuration does not provide a dialog matching the method name"
    )
  }

  func testRequestURLWithUnavailableNativeBridgeURL() {
    stubServerConfigurationWithDialog(named: Values.methodName)
    nativeBridge.stubbedRequestURLError = SampleError()
    let url = try? bridge.requestURL(
      actionID: "",
      scheme: "",
      methodName: Values.methodName,
      parameters: [:]
    )

    XCTAssertNil(
      url,
      "Should not create a url if the native bridge cannot provide one"
    )
  }

  func testRequestURL() throws {
    let value = UUID().uuidString
    let queryItem = URLQueryItem(name: "somethingUnique", value: value)
    let urlWithParams = SampleURLs.valid(queryItems: [queryItem])
    nativeBridge.stubbedRequestURL = urlWithParams
    stubServerConfigurationWithDialog(
      named: Values.methodName,
      url: urlWithParams
    )

    internalUtility.stubbedAppURL = urlWithParams
    internalUtility.stubbedFacebookURL = urlWithParams

    _ = try? bridge.requestURL(
      actionID: Values.actionID,
      scheme: URLScheme.https.rawValue,
      methodName: Values.methodName,
      parameters: validQueryParameters
    )

    let queryParameters = try XCTUnwrap(internalUtility.capturedURLQueryParameters)
    XCTAssertEqual(
      queryParameters["somethingUnique"],
      value,
      "The url should contain the query items from the url provided by the native bridge"
    )

    XCTAssertEqual(
      internalUtility.capturedURLHost,
      urlWithParams.host,
      "Should create a url using the host from the dialog configuration"
    )
    XCTAssertEqual(
      internalUtility.capturedURLPath,
      urlWithParams.path,
      "Should create a url using the path from the dialog configuration"
    )

    XCTAssertEqual(
      queryParameters[Keys.iosBundleID],
      bundle.fb_bundleIdentifier,
      "Should add the bundle ID to the query parameters"
    )
    XCTAssertEqual(
      queryParameters[Keys.redirectURL],
      urlWithParams.absoluteString,
      "Should add the redirect url to the query parameters"
    )
  }

  // MARK: - Redirect URL

  func testRedirectURLWithoutActionIdOrMethodName() {
    let appURL = SampleURLs.valid.appendingPathComponent("appURL")
    internalUtility.stubbedAppURL = appURL

    let url = try? bridge._redirectURL(withActionID: nil, methodName: nil)

    let message = "Should create a redirect url without an action identifier and method name using the internal utility"

    XCTAssertEqual(internalUtility.capturedAppURLHost, "bridge", message)
    XCTAssertEqual(internalUtility.capturedAppURLPath, "", message)
    XCTAssertEqual(internalUtility.capturedAppURLQueryParameters, [:], message)
    XCTAssertEqual(url, appURL, message)
  }

  func testRedirectURLWithMethodNameOnly() {
    let appURL = SampleURLs.valid.appendingPathComponent("appURL")
    internalUtility.stubbedAppURL = appURL

    let url = try? bridge._redirectURL(withActionID: nil, methodName: Values.methodName)

    let message = "Should create a redirect url using the method name for the path using the internal utility"

    XCTAssertEqual(internalUtility.capturedAppURLHost, "bridge", message)
    XCTAssertEqual(internalUtility.capturedAppURLPath, Values.methodName, message)
    XCTAssertEqual(internalUtility.capturedAppURLQueryParameters, [:], message)
    XCTAssertEqual(url, appURL, message)
  }

  func testRedirectURLWithActionIdOnly() {
    let appURL = SampleURLs.valid.appendingPathComponent("appURL")
    internalUtility.stubbedAppURL = appURL

    guard
      let url = try? bridge._redirectURL(withActionID: Values.actionID, methodName: nil),
      let bridgeArgsData = try? JSONSerialization.data(withJSONObject: [Keys.actionID: Values.actionID], options: []),
      let bridgeArgsString = String(data: bridgeArgsData, encoding: .utf8)
    else {
      return XCTFail("Should be able to generate test data")
    }

    let message = """
        Should create a redirect url with serialized bridge api arguments that \
        include the action identifier using the internal utility
      """

    XCTAssertEqual(internalUtility.capturedAppURLHost, "bridge", message)
    XCTAssertEqual(internalUtility.capturedAppURLPath, "", message)
    XCTAssertEqual(
      internalUtility.capturedAppURLQueryParameters,
      [Keys.bridgeArgs: bridgeArgsString],
      message
    )
    XCTAssertEqual(url, appURL, message)
  }

  func testRedirectURLWithMethodNameAndActionID() {
    let appURL = SampleURLs.valid.appendingPathComponent("appURL")
    internalUtility.stubbedAppURL = appURL

    guard
      let url = try? bridge._redirectURL(withActionID: Values.actionID, methodName: Values.methodName),
      let bridgeArgsData = try? JSONSerialization.data(withJSONObject: [Keys.actionID: Values.actionID], options: []),
      let bridgeArgsString = String(data: bridgeArgsData, encoding: .utf8)
    else {
      return XCTFail("Should be able to generate test data")
    }

    let message = """
        Should create a redirect url with serialized bridge api arguments that \
        include the action identifier using the internal utility
      """

    XCTAssertEqual(internalUtility.capturedAppURLHost, "bridge", message)
    XCTAssertEqual(internalUtility.capturedAppURLPath, Values.methodName, message)
    XCTAssertEqual(
      internalUtility.capturedAppURLQueryParameters,
      [Keys.bridgeArgs: bridgeArgsString],
      message
    )
    XCTAssertEqual(url, appURL, message)
  }

  // MARK: - Request URL for DialogConfiguration

  func testRequestURLForDialogConfigurationWithoutScheme() throws {
    let facebookURL = SampleURLs.valid.appendingPathComponent("facebook")
    internalUtility.stubbedFacebookURL = facebookURL

    let url = try XCTUnwrap(URL(string: "/"))
    let configuration = _DialogConfiguration(name: UUID().uuidString, url: url, appVersions: [])
    let requestURL = try? bridge._requestURL(for: configuration)

    let message = """
      Should provide a request url for a dialog configuration without a scheme \
      using the internal utility
      """

    XCTAssertEqual(internalUtility.capturedFacebookURLHostPrefix, "m", message)
    XCTAssertEqual(internalUtility.capturedFacebookURLPath, "/", message)
    XCTAssertTrue(internalUtility.capturedFacebookURLQueryParameters?.isEmpty ?? false, message)
    XCTAssertEqual(requestURL, facebookURL, message)
  }

  func testRequestURLForDialogConfigurationWithScheme() {
    let configuration = _DialogConfiguration(
      name: name,
      url: SampleURLs.valid(path: name),
      appVersions: []
    )
    let requestURL = try? bridge._requestURL(for: configuration)

    XCTAssertEqual(
      requestURL?.absoluteString,
      SampleURLs.valid(path: name).absoluteString,
      "Should use the url from the dialog configuration if it has a scheme"
    )
  }

  // MARK: - Response Parameters

  func testResponseParameters() {
    var isCancelled = ObjCBool(false)
    _ = try? bridge.responseParameters(
      actionID: Values.actionID,
      queryParameters: validQueryParameters,
      cancelled: &isCancelled
    )
    XCTAssertEqual(
      nativeBridge.capturedResponseActionID,
      Values.actionID,
      "Should pass through to the native bridge"
    )
    guard
      let parameters = nativeBridge.capturedResponseQueryParameters as? [String: String],
      parameters == validQueryParameters
    else {
      return XCTFail("Should pass through to the native bridge")
    }

    XCTAssertNotNil(
      nativeBridge.capturedResponseCancelledRef,
      "Should pass through to the native bridge"
    )
  }

  // MARK: - Helpers

  func stubServerConfigurationWithDialog(
    named name: String,
    url: URL = SampleURLs.valid
  ) {
    let dialogConfiguration = _DialogConfiguration(
      name: name,
      url: url,
      appVersions: []
    )
    let configuration = ServerConfigurationFixtures.configuration(
      withDictionary: ["dialogConfigurations": [name: dialogConfiguration]]
    )
    serverConfigurationProvider = TestServerConfigurationProvider(configuration: configuration)
    bridge = BridgeAPIProtocolWebV2(
      serverConfigurationProvider: serverConfigurationProvider,
      nativeBridge: nativeBridge,
      errorFactory: errorFactory,
      internalUtility: internalUtility,
      infoDictionaryProvider: bundle
    )
  }

  func queryItems(from url: URL) -> [URLQueryItem] {
    URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? []
  }
}