/*
 * 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 MetadataIndexerTests: XCTestCase {
  var emailField = UITextField()
  var emailView = UITextView()
  var phoneField = UITextField()
  var phoneView = UITextView()
  var pwdField = UITextField()
  var pwdView = UITextView()
  var userDataStore = TestUserDataStore()
  lazy var metadataIndexer = _MetadataIndexer(
    userDataStore: userDataStore,
    swizzler: TestSwizzler.self
  )

  let rules = [
    "r1": [
      "k": "email,e-mail,em,electronicmail",
      "v": "^([A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})$",
    ],
    "r2": [
      "k": "phone,mobile,contact",
      "v": "^([0-9]{5,15})$",
    ],
    "r3": [
      "k": "gender,gen,sex",
      "v": "^(male|boy|man|female|girl|woman)$",
    ],
    "r4": [
      "k": "city",
      "v": "",
    ],
    "r5": [
      "k": "state,province",
      "v": "",
    ],
    "r6": [
      "k": "zip,zcode,pincode,pcode,postalcode,postcode",
      "v": "(^\\d{5}$)|(^\\d{9}$)|(^\\d{5}-\\d{4}$)",
    ],
    "r7": [
      "k": "firstname,first name,fn,fname,givenname,forename",
      "v": "",
    ],
    "r8": [
      "k": "lastname,last name,ln,lname,surname,sname,familyname",
      "v": "",
    ],
  ]

  override func setUp() {
    super.setUp()

    metadataIndexer.constructRules(rules)
    metadataIndexer.initStore()

    // clear store
    for key in rules {
      metadataIndexer.store[key] = []
    }

    emailField.placeholder = NSLocalizedString("Enter your email", comment: "")
    emailField.keyboardType = .emailAddress

    emailView.keyboardType = .emailAddress

    phoneField.placeholder = NSLocalizedString("Enter your phone", comment: "")
    phoneField.keyboardType = .phonePad

    pwdField.placeholder = NSLocalizedString("Enter your password", comment: "")
    pwdField.isSecureTextEntry = true

    pwdView.isSecureTextEntry = true
  }

  override func tearDown() {
    TestSwizzler.reset()

    super.tearDown()
  }

  func testCreatingWithDependencies() {
    XCTAssertEqual(
      metadataIndexer.userDataStore as? TestUserDataStore,
      userDataStore,
      "Should use the provided user data store"
    )
    XCTAssertTrue(
      metadataIndexer.swizzler == TestSwizzler.self,
      "Should use the provided swizzler"
    )
  }

  func testInitStore() {
    XCTAssertEqual(
      userDataStore.getInternalHashedDataForTypeCallCount,
      rules.count,
      "Should request the internal hashed data for each rule"
    )
  }

  func testSetupWithMissingRules() {
    metadataIndexer.setup(withRules: nil)

    XCTAssertNil(
      userDataStore.capturedEnableRules,
      "Should not invoke the user data store when there are missing rules"
    )
  }

  func testSetupWithEmptyRules() {
    metadataIndexer.setup(withRules: [:])

    XCTAssertNil(
      userDataStore.capturedEnableRules,
      "Should not invoke the user data store when there are empty rules"
    )
  }

  func testSetupWithRules() throws {
    metadataIndexer.setup(withRules: rules)

    let keys = Array(rules.keys)

    let capturedEnableRules = try XCTUnwrap(userDataStore.capturedEnableRules)
    XCTAssertEqual(keys.sorted(), capturedEnableRules.sorted())
  }

  func testCheckAndAppendDataForKeyWithUnknownKey() {
    metadataIndexer.checkAndAppendData(name, forKey: "foo")

    XCTAssertNil(
      userDataStore.capturedInternalHashedDataForTypeData,
      "Should not set data for a key that is not in the known rules"
    )
    XCTAssertNil(
      userDataStore.capturedInternalHashedDataForTypeType,
      "Should not set data for a key that is not in the known rules"
    )
  }

  func testCheckAndAppendDataForKeyWithKeyMatchingRule() {
    /* Assumes the indexer is set up with the rule:
     "r1": @{
       "k": "email,e-mail,em,electronicmail",
       "v": "^([A-Z0-9a-z._%+-]+[A-Za-z0-9.-]+\\.[A-Za-z]{2,})$",
     },
     */
    let expected = BasicUtility.sha256Hash(name as NSObject)
    metadataIndexer.checkAndAppendData(name, forKey: "r1")

    XCTAssertEqual(
      userDataStore.capturedInternalHashedDataForTypeData,
      expected,
      "Should hash and set the provided data"
    )
  }

  func testCheckAndAppendDataForKeyWithEmptyData() {
    metadataIndexer.checkAndAppendData("", forKey: "key")

    XCTAssertNil(
      userDataStore.capturedInternalHashedDataForTypeData,
      "Shouldn't set empty hashed data"
    )
  }

  // test for geting secure text entry in UITextField
  func testCheckSecureTextEntryOfTextField() {
    // without secure text
    XCTAssertFalse(
      metadataIndexer.checkSecureTextEntry(emailField),
      "test for UITextField without secure text"
    )

    // with secure text
    XCTAssertTrue(
      metadataIndexer.checkSecureTextEntry(pwdField),
      "test for UITextField with secure text"
    )
  }

  // test for geting secure text entry in UITextView
  func testCheckSecureTextEntryOfTextView() {
    // without secure text
    XCTAssertFalse(
      metadataIndexer.checkSecureTextEntry(emailView),
      "test for UITextView without secure text"
    )

    // with secure text
    XCTAssertTrue(
      metadataIndexer.checkSecureTextEntry(pwdView),
      "test for UITextView with secure text"
    )
  }

  // test for geting keyboard type from UITextField
  func testGetKeyboardTypeOfTextField() {
    XCTAssertEqual(
      emailField.keyboardType,
      metadataIndexer.getKeyboardType(emailField),
      "test for geting keyboard type from UITextField"
    )
  }

  // test for geting keyboard type from UITextView
  func testGetKeyboardTypeOfTextView() {
    XCTAssertEqual(
      emailView.keyboardType,
      metadataIndexer.getKeyboardType(emailView),
      "test for geting keyboard type from UITextView"
    )
  }

  // test for geting metadata with valid email
  func testGetMetadataWithEmail() {
    let text = "test@fb.com"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: "Enter your Email",
      labels: nil,
      secureTextEntry: false,
      inputType: .emailAddress
    )
    let r1Indexer = metadataIndexerStoredValue(forKey: "r1")
    let r2Indexer = metadataIndexerStoredValue(forKey: "r2")

    XCTAssertEqual(
      r1Indexer[0],
      Utility.sha256Hash(text as NSObject),
      "Getting metadata with a valid email should check rules related to emails."
    )
    XCTAssertEqual(
      r2Indexer.count,
      0,
      "Getting metadata with a valid email should not check rules related to phone numbers."
    )
  }

  // test for geting metadata with valid phone number
  func testGetMetadataWithPhoneNumber() {
    let text = "1112223333"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: "Enter your Phone Number",
      labels: nil,
      secureTextEntry: false,
      inputType: .phonePad
    )
    let r1Indexer = metadataIndexerStoredValue(forKey: "r1")
    let r2Indexer = metadataIndexerStoredValue(forKey: "r2")
    XCTAssertEqual(
      r2Indexer[0],
      Utility.sha256Hash(text as NSObject),
      "Getting metadata with a valid phone number should check rules related to phone numbers."
    )
    XCTAssertEqual(
      r1Indexer.count,
      0,
      "Getting metadata with a valid phone number should not check rules related to emails."
    )
  }

  // test for geting metadata with valid phone number or zipcode with labels
  func testGetMetadataWithPhoneNumberWithLabels() {
    let text = "11122"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: "",
      labels: ["phone", "zipcode"],
      secureTextEntry: false,
      inputType: .phonePad
    )
    let r2Indexer = metadataIndexerStoredValue(forKey: "r2")
    let r6Indexer = metadataIndexerStoredValue(forKey: "r6")
    XCTAssertEqual(
      r2Indexer[0],
      Utility.sha256Hash(text as NSObject),
      "Getting metadata with a phone number or zipcode with label should check rules related to phone numbers."
    )
    XCTAssertEqual(
      r6Indexer[0],
      Utility.sha256Hash(text as NSObject),
      "Getting metadata with a phone number or zipcode with label should check rules related to zipcodes."
    )
  }

  // test for geting metadata with secure text
  func testGetMetadataWithSecureText() {
    let text = "dfjald1314"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: "Enter your Pass-word",
      labels: nil,
      secureTextEntry: true,
      inputType: .default
    )
    let r1Indexer = metadataIndexerStoredValue(forKey: "r1")
    let r2Indexer = metadataIndexerStoredValue(forKey: "r2")

    XCTAssertEqual(
      r1Indexer.count,
      0,
      "Getting metadata with a secret text should not check rules related to emails."
    )
    XCTAssertEqual(
      r2Indexer.count,
      0,
      "Getting metadata with a secret text should not check rules related to phone numbers."
    )
  }

  // test for geting metadata with invalid email
  func testGetMetadataWithInvalidEmail() {
    let text = "test"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: "Enter your Email",
      labels: nil,
      secureTextEntry: false,
      inputType: .emailAddress
    )
    let r1Indexer = metadataIndexerStoredValue(forKey: "r1")
    let r2Indexer = metadataIndexerStoredValue(forKey: "r2")

    XCTAssertEqual(
      r1Indexer.count,
      0,
      "Getting metadata with an invalid email should not check rules related to emails."
    )
    XCTAssertEqual(
      r2Indexer.count,
      0,
      "Getting metadata with an invalid email should not check rules related to phone numbers."
    )
  }

  // test for geting metadata with invalid email placeholder
  func testGetMetadataWithInvalidEmailPlaceholder() {
    let text = "test@fb.com"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: "Enter your",
      labels: nil,
      secureTextEntry: false,
      inputType: .emailAddress
    )
    let r1Indexer = metadataIndexerStoredValue(forKey: "r1")
    let r2Indexer = metadataIndexerStoredValue(forKey: "r2")

    XCTAssertEqual(
      r1Indexer.count,
      0,
      "Getting metadata with an invalid email placeholder should not check rules related to emails."
    )
    XCTAssertEqual(
      r2Indexer.count,
      0,
      "Getting metadata with an invalid email placeholder should not check rules related to phone numbers."
    )
  }

  // test for getting metadata with valid phone number containing +-().
  func testGetMetadataWithValidPhoneNumberWithPunctuations() throws {
    let text = "+1(222)-333-444"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: "Enter your Phone Number",
      labels: nil,
      secureTextEntry: false,
      inputType: .phonePad
    )
    let r2Indexer = metadataIndexerStoredValue(forKey: "r2")
    let numberNoPunctuations = try XCTUnwrap(Utility.sha256Hash("1222333444" as NSObject))
    let numberWithPunctuations = try XCTUnwrap(Utility.sha256Hash(text as NSObject))
    XCTAssertTrue(
      r2Indexer.contains(numberNoPunctuations),
      "Getting metadata with a phone number with punctuations should check rules related to phone numbers with pure numbers." // swiftlint:disable:this line_length
    )
    XCTAssertFalse(
      r2Indexer.contains(numberWithPunctuations),
      "Getting metadata with a phone number with punctuations should not check rules related to phone numbers with the original text." // swiftlint:disable:this line_length
    )
  }

  // test for geting metadata with invalid phone number
  func testGetMetadataWithInvalidPhoneNumber() {
    metadataIndexer.getMetadataWithText(
      "1234",
      placeholder: "Enter your Phone Number",
      labels: nil,
      secureTextEntry: false,
      inputType: .phonePad
    )

    metadataIndexer.getMetadataWithText(
      "1234567891011121314",
      placeholder: "Mobile Number",
      labels: nil,
      secureTextEntry: false,
      inputType: .phonePad
    )
    let r2Indexer = metadataIndexerStoredValue(forKey: "r2")
    XCTAssertEqual(
      r2Indexer.count,
      0,
      "Getting metadata with an invalid phone number should not check rules related to phone numbers."
    )
  }

  // test for geting metadata with invalid phone number placeholder
  func testGetMetadataWithInvalidPhoneNumberPlaceholder() {
    let text = "1112223333"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: "Enter your",
      labels: nil,
      secureTextEntry: false,
      inputType: .phonePad
    )
    let r1Indexer = metadataIndexerStoredValue(forKey: "r1")
    let r2Indexer = metadataIndexerStoredValue(forKey: "r2")
    XCTAssertEqual(
      r1Indexer.count,
      0,
      "Getting metadata with an invalid phone number placeholder should not check rules related to emails."
    )
    XCTAssertEqual(
      r2Indexer.count,
      0,
      "Getting metadata with an invalid phone number placeholder should not check rules related to phone numbers."
    )
  }

  // test for geting metadata with text which is neither email nor phone number
  func testGetMetadataWithTextNotEmailAndPhone() {
    let text = "facebook"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: "Enter your Name",
      labels: nil,
      secureTextEntry: false,
      inputType: .alphabet
    )
    let r1Indexer = metadataIndexerStoredValue(forKey: "r1")
    let r2Indexer = metadataIndexerStoredValue(forKey: "r2")
    XCTAssertEqual(
      r1Indexer.count,
      0,
      "Getting metadata with a plain text (not email nor phone number) should not check rules related to emails."
    )
    XCTAssertEqual(
      r2Indexer.count,
      0,
      "Getting metadata with a plain text (not email nor phone number) should not check rules related to phone numbers."
    )
  }

  // test for geting metadata with no text
  func testGetMetadataWithNoText() {
    let text = ""
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: "Enter your Email",
      labels: nil,
      secureTextEntry: false,
      inputType: .emailAddress
    )
    let r1Indexer = metadataIndexerStoredValue(forKey: "r1")
    let r2Indexer = metadataIndexerStoredValue(forKey: "r2")
    XCTAssertEqual(
      r1Indexer.count,
      0,
      "Getting metadata with an empty string should not check rules related to emails."
    )
    XCTAssertEqual(
      r2Indexer.count,
      0,
      "Getting metadata with an empty string should not check rules related to phone numbers."
    )
  }

  // test for geting metadata with too long text
  func testGetMetadataWithTooLongText() {
    let text = "".padding(toLength: 1000, withPad: "a", startingAt: 0) + "@fb.com"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: "Enter your Email",
      labels: nil,
      secureTextEntry: false,
      inputType: .emailAddress
    )
    let r1Indexer = metadataIndexerStoredValue(forKey: "r1")

    let r2Indexer = metadataIndexerStoredValue(forKey: "r2")
    XCTAssertEqual(
      r1Indexer.count,
      0,
      "Getting metadata with a too long text should not check rules related to emails."
    )
    XCTAssertEqual(
      r2Indexer.count,
      0,
      "Getting metadata with a too long text should not check rules related to phone numbers."
    )
  }

  // test for geting metadata with too long placeholder
  func testGetMetadataWithTooLongPlaceholder() {
    let text = "test@fb.com"
    let indicator = "".padding(toLength: 1000, withPad: "enter your email ", startingAt: 0)
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: indicator,
      labels: nil,
      secureTextEntry: false,
      inputType: .emailAddress
    )
    let r1Indexer = metadataIndexerStoredValue(forKey: "r1")
    let r2Indexer = metadataIndexerStoredValue(forKey: "r2")

    XCTAssertEqual(
      r1Indexer.count,
      0,
      "Getting metadata with a too long placeholder should not check rules related to emails."
    )
    XCTAssertEqual(
      r2Indexer.count,
      0,
      "Getting metadata with a too long placeholder should not check rules related to phone numbers."
    )
  }

  // test for getting metadata with gender
  func testGetMetadataWithValidGender() {
    let text = "male"
    let indicator = "gender"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: indicator,
      labels: nil,
      secureTextEntry: false,
      inputType: .default
    )
    let r3Indexer = metadataIndexerStoredValue(forKey: "r3")
    XCTAssertEqual(
      r3Indexer[0],
      Utility.sha256Hash("m" as NSObject),
      "Getting metadata with a valid gender should check rules related genders."
    )
  }

  func testGetMetadataWithInvalidGender() {
    let text = "test"
    let indicator = "gender"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: indicator,
      labels: nil,
      secureTextEntry: false,
      inputType: .default
    )
    let r3Indexer = metadataIndexerStoredValue(forKey: "r3")
    XCTAssertEqual(
      r3Indexer.count,
      0,
      "Getting metadata with an invalid gender should not check rules related to genders."
    )
  }

  func testGetMetadataWithInvalidGenderIndicator() {
    let text = "female"
    let indicator = "test"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: indicator,
      labels: nil,
      secureTextEntry: false,
      inputType: .default
    )
    let r3Indexer = metadataIndexerStoredValue(forKey: "r3")
    XCTAssertEqual(
      r3Indexer.count,
      0,
      "Getting metadata with an invalid gender indicator should not check rules related to genders."
    )
  }

  // test for getting meta with city
  func testGetMetadataWithValidCity() {
    let text = "Menlo Park"
    let indicator = "city"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: indicator,
      labels: nil,
      secureTextEntry: false,
      inputType: .default
    )
    let r4Indexer = metadataIndexerStoredValue(forKey: "r4")
    XCTAssertEqual(
      r4Indexer[0],
      Utility.sha256Hash("menlopark" as NSObject),
      "Getting metadata with a valid city name should check rules related cities."
    )
  }

  func testGetMetadataWithInvalidCity() {
    // Although rule_V for city is "", but should not accept empty text case
    let text = ""
    let indicator = "city"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: indicator,
      labels: nil,
      secureTextEntry: false,
      inputType: .default
    )
    let r4Indexer = metadataIndexerStoredValue(forKey: "r4")

    XCTAssertEqual(
      r4Indexer.count,
      0,
      "Getting metadata with an invalid city name should not check rules related to cities."
    )
  }

  func testGetMetadataWithInvalidCityIndicator() {
    let text = "Menlo Park"
    let indicator = "test"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: indicator,
      labels: nil,
      secureTextEntry: false,
      inputType: .default
    )
    let r4Indexer = metadataIndexerStoredValue(forKey: "r4")

    XCTAssertEqual(
      r4Indexer.count,
      0,
      "Getting metadata with an invalid city indicator should not check rules related to cities."
    )
  }

  // test for getting meta with state
  func testGetMetadataWithValidState() {
    let text = "CA"
    let indicator = "province"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: indicator,
      labels: nil,
      secureTextEntry: false,
      inputType: .default
    )
    let r5Indexer = metadataIndexerStoredValue(forKey: "r5")

    XCTAssertEqual(
      r5Indexer[0],
      Utility.sha256Hash("ca" as NSObject),
      "Getting metadata with a valid state/province name should check rules related to states."
    )
  }

  func testGetMetadataWithInvalidState() {
    // Although rule_V for state is "", but should not accept empty text case
    let text = ""
    let indicator = "state"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: indicator,
      labels: nil,
      secureTextEntry: false,
      inputType: .default
    )
    let r5Indexer = metadataIndexerStoredValue(forKey: "r5")

    XCTAssertEqual(
      r5Indexer.count,
      0,
      "Getting metadata with an invalid state/province name should not check rules related to states."
    )
  }

  func testGetMetadataWithInvalidStateIndicator() {
    let text = "CA"
    let indicator = "test"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: indicator,
      labels: nil,
      secureTextEntry: false,
      inputType: .default
    )
    let r5Indexer = metadataIndexerStoredValue(forKey: "r5")
    XCTAssertEqual(
      r5Indexer.count,
      0,
      "Getting metadata with an invalid state/province indicator should not check rules related to states."
    )
  }

  // test for getting meta with zip
  func testGetMetadataWithValidZip() {
    let text = "94025"
    let indicator = "zcode"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: indicator,
      labels: nil,
      secureTextEntry: false,
      inputType: .default
    )
    let r6Indexer = metadataIndexerStoredValue(forKey: "r6")
    XCTAssertEqual(
      r6Indexer[0],
      Utility.sha256Hash(text as NSObject),
      "Getting metadata with a valid zipcode should check rules related to zipcodes."
    )
  }

  // test for getting metadata with valid zipcode containing "-" (will also be regarded as phone number)
  func testGetMetadataWithValidZipWithPunctuations() throws {
    let text = "94025-1234"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: "",
      labels: ["zcode", "phone"],
      secureTextEntry: false,
      inputType: .phonePad
    )
    let r6Indexer = metadataIndexerStoredValue(forKey: "r6")
    let r2Indexer = metadataIndexerStoredValue(forKey: "r2")
    let validZipWithPunctuations = try XCTUnwrap(Utility.sha256Hash(text as NSObject))
    XCTAssertEqual(
      r6Indexer[0],
      Utility.sha256Hash("94025" as NSObject),
      "Getting metadata with a valid zipcode with punctuations should check rules related to zipcodes."
    )
    XCTAssertEqual(
      r2Indexer[0],
      Utility.sha256Hash("940251234" as NSObject),
      "Getting metadata with a valid zipcode with punctuations should check rules related to phone numbers." // swiftlint:disable:this line_length
    )
    XCTAssertFalse(
      r6Indexer.contains(validZipWithPunctuations),
      "Getting metadata with a valid zipcode with punctuations should not check rules related related to zipcodes with original text." // swiftlint:disable:this line_length
    )
  }

  func testGetMetadataWithInvalidZip() {
    // the rule for zip code should be 5-digit number
    let text = "9402"
    let indicator = "zcode"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: indicator,
      labels: nil,
      secureTextEntry: false,
      inputType: .default
    )
    let r6Indexer = metadataIndexerStoredValue(forKey: "r6")
    XCTAssertEqual(
      r6Indexer.count,
      0,
      "Getting metadata with an invalid zipcode should not check rules related to zipcodes."
    )
  }

  func testGetMetadataWithInvalidZipIndicator() {
    let text = "94025"
    let indicator = "test"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: indicator,
      labels: nil,
      secureTextEntry: false,
      inputType: .default
    )
    let r6Indexer = metadataIndexerStoredValue(forKey: "r6")
    XCTAssertEqual(
      r6Indexer.count,
      0,
      "Getting metadata with an invalid zipcode indicator should not check rules related to zipcodes."
    )
  }

  // test for getting meta with first name
  func testGetMetadataWithValidFn() {
    let text = "David"
    let indicator = "fn"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: indicator,
      labels: nil,
      secureTextEntry: false,
      inputType: .default
    )
    let r7Indexer = metadataIndexerStoredValue(forKey: "r7")
    XCTAssertEqual(
      r7Indexer[0],
      Utility.sha256Hash("david" as NSObject),
      "Getting metadata with a valid first name should check rules related to first names."
    )
  }

  func testGetMetadataWithInvalidFn() {
    // Although rule_V for first name is "", but should not accept empty text case
    let text = ""
    let indicator = "fn"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: indicator,
      labels: nil,
      secureTextEntry: false,
      inputType: .default
    )
    let r7Indexer = metadataIndexerStoredValue(forKey: "r7")
    XCTAssertEqual(
      r7Indexer.count,
      0,
      "Getting metadata with an empty string should check rules related to first names."
    )
  }

  func testGetMetadataWithInvalidFnIndicator() {
    let text = "David"
    let indicator = "test"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: indicator,
      labels: nil,
      secureTextEntry: false,
      inputType: .default
    )
    let r7Indexer = metadataIndexerStoredValue(forKey: "r7")
    XCTAssertEqual(
      r7Indexer.count,
      0,
      "Getting metadata with an invalid first name indicator should check rules related to first names."
    )
  }

  // test for getting meta with last name
  func testGetMetadataWithValidLn() {
    let text = "Taylor"
    let indicator = "ln"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: indicator,
      labels: nil,
      secureTextEntry: false,
      inputType: .default
    )
    let r8Indexer = metadataIndexerStoredValue(forKey: "r8")
    XCTAssertEqual(
      r8Indexer[0],
      Utility.sha256Hash("taylor" as NSObject),
      "Getting metadata with a valid last name should check rules related to last names."
    )
  }

  func testGetMetadataWithInvalidLn() {
    // Although rule_V for last name is "", but should not accept empty text case
    let text = ""
    let indicator = "ln"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: indicator,
      labels: nil,
      secureTextEntry: false,
      inputType: .default
    )
    let r8Indexer = metadataIndexerStoredValue(forKey: "r8")
    XCTAssertEqual(
      r8Indexer.count,
      0,
      "Getting metadata with an invalid last name should check rules related to last names."
    )
  }

  func testGetMetadataWithInvalidLnIndicator() {
    let text = "Taylor"
    let indicator = "test"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: indicator,
      labels: nil,
      secureTextEntry: false,
      inputType: .default
    )
    let r8Indexer = metadataIndexerStoredValue(forKey: "r8")
    XCTAssertEqual(
      r8Indexer.count,
      0,
      "Getting metadata with an invalid last name indicator should check rules related to last names."
    )
  }

  // test for getting meta with first name with labels (will also be regarded as last name, state, city
  func testGetMetadataWithFirstNameWithLabels() throws {
    let text = "Taylor"
    metadataIndexer.getMetadataWithText(
      text,
      placeholder: "",
      labels: ["fn", "ln", "state", "city"],
      secureTextEntry: false,
      inputType: .default
    )
    let r4Indexer = metadataIndexerStoredValue(forKey: "r4")
    let r5Indexer = metadataIndexerStoredValue(forKey: "r5")
    let r7Indexer = metadataIndexerStoredValue(forKey: "r7")
    let r8Indexer = metadataIndexerStoredValue(forKey: "r8")

    let cityLabel = try XCTUnwrap(Utility.sha256Hash("taylor" as NSObject))
    let stateLabel = try XCTUnwrap(Utility.sha256Hash("taylor" as NSObject))
    let fnLabel = try XCTUnwrap(Utility.sha256Hash("taylor" as NSObject))
    let lnLabel = try XCTUnwrap(Utility.sha256Hash("taylor" as NSObject))

    XCTAssertTrue(
      r4Indexer.contains(cityLabel),
      "Getting metadata with a first name with city label should check rules related to cities."
    )
    XCTAssertTrue(
      r5Indexer.contains(stateLabel),
      "Getting metadata with a first name with state label should check rules related to states."
    )
    XCTAssertTrue(
      r7Indexer.contains(fnLabel),
      "Getting metadata with a first name with fn label should check rules related to first names."
    )
    XCTAssertTrue(
      r8Indexer.contains(lnLabel),
      "Getting metadata with a first name with ln label should check rules related to last names."
    )
  }

  func metadataIndexerStoredValue(forKey key: String) -> [String] {
    metadataIndexer.store[key] as! [String] // swiftlint:disable:this force_cast
  }
}


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

@available(iOS 14, *)
final class AEMManagerTests: XCTestCase {

  let swizzler = TestSwizzler.self
  let aemReporter = TestAEMReporter.self
  let eventLogger = TestEventLogger()
  let crashHandler = TestCrashHandler()
  let featureChecker = TestFeatureManager()
  let appEventsUtility = TestAppEventsUtility()

  override func setUp() {
    super.setUp()

    _AEMManager.shared.configure(
      swizzler: swizzler,
      reporter: aemReporter,
      eventLogger: eventLogger,
      crashHandler: crashHandler,
      featureChecker: featureChecker,
      appEventsUtility: appEventsUtility
    )
  }

  func testConfigure() {
    XCTAssertIdentical(
      swizzler as AnyObject,
      _AEMManager.shared.swizzler,
      "Should configure with the expected swizzler"
    )
    XCTAssertIdentical(
      aemReporter as AnyObject,
      _AEMManager.shared.aemReporter,
      "Should configure with the expected AEM reporter"
    )
    XCTAssertIdentical(
      eventLogger as AnyObject,
      _AEMManager.shared.eventLogger,
      "Should configure with the expected event logger"
    )
    XCTAssertIdentical(
      crashHandler as AnyObject,
      _AEMManager.shared.crashHandler,
      "Should configure with the expected crash handler"
    )
    XCTAssertIdentical(
      featureChecker as AnyObject,
      _AEMManager.shared.featureChecker,
      "Should configure with the expected feature checker"
    )
    XCTAssertIdentical(
      appEventsUtility as AnyObject,
      _AEMManager.shared.appEventsUtility,
      "Should configure with the expected app events utility"
    )
  }

  func testLogAutoSetupStatus() {
    _AEMManager.shared.logAutoSetupStatus(true, source: "test_source1")
    XCTAssertEqual(
      eventLogger.capturedEventName?.rawValue,
      "fb_mobile_aem_auto_setup_opt_in",
      "Should log the correct opt in event name"
    )
    XCTAssertEqual(
      eventLogger.capturedParameters as? [AppEvents.ParameterName: String],
      [.init("source"): "test_source1"],
      "Should log the correct opt in event parameter"
    )

    _AEMManager.shared.logAutoSetupStatus(false, source: "test_source2")
    XCTAssertEqual(
      eventLogger.capturedEventName?.rawValue,
      "fb_mobile_aem_auto_setup_opt_out",
      "Should log the correct opt out event name"
    )
    XCTAssertEqual(
      eventLogger.capturedParameters as? [AppEvents.ParameterName: String],
      [.init("source"): "test_source2"],
      "Should log the correct opt out event parameter"
    )
  }
}


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

  enum Values {
    static let datasetID = "id123"
    static let accessKey = "key123"
    static let capiGatewayURL = "www.123.com"
  }

  let factory = TestGraphRequestFactory()
  let settings = TestSettings()

  override func setUp() {
    super.setUp()

    FBSDKAppEventsCAPIManager.shared.configure(factory: factory, settings: settings)
  }

  func testConfigure() {
    XCTAssertIdentical(
      factory as AnyObject,
      FBSDKAppEventsCAPIManager.shared.factory,
      "Should configure with the expected graph request factory"
    )
    XCTAssertEqual(
      settings,
      FBSDKAppEventsCAPIManager.shared.settings as? TestSettings,
      "Should configure with the expected settings"
    )
  }

  func testRecordEventWithoutAppID() {
    settings.appID = nil

    FBSDKAppEventsCAPIManager.shared.enable()
    FBSDKAppEventsCAPIManager.shared.recordEvent([:])
    XCTAssertTrue(
      factory.capturedRequests.isEmpty,
      "Should not send graph request when app id is nil"
    )
  }

  func testEnable() {
    settings.appID = "123"

    FBSDKAppEventsCAPIManager.shared.enable()
    XCTAssertTrue(
      FBSDKAppEventsCAPIManager.shared.isSDKGKEnabled,
      "CAPI should be enabled"
    )
  }

  func testRecordEventWithoutLoadingError() {
    settings.appID = "123"

    FBSDKAppEventsCAPIManager.shared.enable()
    FBSDKAppEventsCAPIManager.shared.recordEvent([:])
    guard let completion = factory.capturedRequests.first?.capturedCompletionHandler else {
      return XCTFail("Should start a request with a completion handler")
    }
    completion(
      nil,
      [
        "data": [
          [
            "is_enabled": false,
            "access_key": Values.accessKey,
            "dataset_id": Values.datasetID,
            "endpoint": Values.capiGatewayURL,
          ],
        ],
      ],
      nil
    )
    XCTAssertEqual(
      FBSDKTransformerGraphRequestFactory.shared.credentials?.accessKey,
      Values.accessKey,
      "Credential's access key should be the same as that in the setting request"
    )
    XCTAssertEqual(
      FBSDKTransformerGraphRequestFactory.shared.credentials?.datasetID,
      Values.datasetID,
      "Credential's access key should be the same as that in the setting request"
    )
    XCTAssertEqual(
      FBSDKTransformerGraphRequestFactory.shared.credentials?.capiGatewayURL,
      Values.capiGatewayURL,
      "Credential's access key should be the same as that in the setting request"
    )
  }

  func testExecuteBlocks() {
    // swiftlint:disable:next discouraged_optional_boolean
    var capturedIsEnabled: Bool?
    FBSDKAppEventsCAPIManager.shared.completionBlocks.append { isEnabled in
      capturedIsEnabled = isEnabled
    }
    FBSDKAppEventsCAPIManager.shared.executeBlocks(isEnabled: true)

    XCTAssertTrue(
      FBSDKAppEventsCAPIManager.shared.completionBlocks.isEmpty,
      "Completion blocks should be cleared after execution"
    )
    XCTAssertTrue(
      capturedIsEnabled == true,
      "Should pass the expected isEnabled value to completion blocks"
    )
  }

  func testIsRefreshTimestampValid() {
    FBSDKAppEventsCAPIManager.shared.configRefreshTimestamp = nil
    XCTAssertFalse(
      FBSDKAppEventsCAPIManager.shared.isRefreshTimestampValid(),
      "Refresh timestamp is not valid if it is nil"
    )

    FBSDKAppEventsCAPIManager.shared.configRefreshTimestamp = Calendar.current.date(
      byAdding: .day,
      value: -2,
      to: Date()
    )
    XCTAssertFalse(
      FBSDKAppEventsCAPIManager.shared.isRefreshTimestampValid(),
      "Refresh timestamp is not valid if it is expired"
    )

    FBSDKAppEventsCAPIManager.shared.configRefreshTimestamp = Calendar.current.date(
      byAdding: .hour,
      value: -1,
      to: Date()
    )
    XCTAssertTrue(
      FBSDKAppEventsCAPIManager.shared.isRefreshTimestampValid(),
      "Refresh timestamp is valid if it is not expired"
    )
  }
}


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

@testable import FBSDKCoreKit
import XCTest

// swiftlint:disable:next type_name
final class FBSDKAppEventsConversionsAPITransformerTests: XCTestCase {

  // MARK: Helper variables

  let customEvent1 = "[{\"_eventName\":\"fb_mobile_add_to_cart\",\"_logTime\":12345}]"
  let customEvent2 =
    // swiftlint:disable:next line_length
    "[{\"_eventName\":\"fb_mobile_add_to_cart\",\"_logTime\":12345}, {\"_eventName\":\"new_event\",\"_logTime\":67890, \"fb_content_type\":\"product\", \"_valueToSum\":21.97, \"fb_currency\":\"GBP\"}]"

  // swiftlint:disable:next identifier_name
  let ud = "{\"fn\":\"1234567890\", \"em\":\"ABCDE\"}"

  let restOfData1 = [
    AppEventsConversionsAPITransformer.DataProcessingParameterName.options.rawValue: 1,
    AppEventsConversionsAPITransformer.DataProcessingParameterName.state.rawValue: 0,
  ] as [String: Any]

  let transformedAppData1 = [
    AppEventUserAndAppDataField.advTE.rawValue: 1,
    AppEventUserAndAppDataField.extinfo.rawValue: ["i2"],
  ] as [String: Any]

  let transformedUserData1 = [
    ConversionsAPIUserAndAppDataField.madid.rawValue: "ABCDE-12345",
    "fn": "1234567890",
    "em": "ABCDE",
  ] as [String: String]

  let transformedCustomEvent1 = [
    ConversionsAPICustomEventField.eventName.rawValue: ConversionsAPIEventName.addToCart.rawValue,
    ConversionsAPICustomEventField.eventTime.rawValue: 12345,
  ] as [String: Any]

  let transformedCustomEvent2 = [
    ConversionsAPICustomEventField.eventName.rawValue: "new_event",
    ConversionsAPICustomEventField.eventTime.rawValue: 67890,
    ConversionsAPISection.customData.rawValue: [
      ConversionsAPICustomEventField.contentType.rawValue: "product",
      ConversionsAPICustomEventField.valueToSum.rawValue: 21.97,
      ConversionsAPICustomEventField.currency.rawValue: "GBP",
    ],
  ] as [String: Any]

  // MARK: Helper functions

  func checkIfCommonFieldsAreEqual(parameters: [String: Any]) throws {
    let userData = try XCTUnwrap(parameters[ConversionsAPISection.userData.rawValue] as? [String: String])
    XCTAssertEqual(userData, transformedUserData1)

    let appData = try XCTUnwrap(parameters[ConversionsAPISection.appData.rawValue] as? [String: Any])

    let ATE = try XCTUnwrap(appData[ConversionsAPIUserAndAppDataField.advTE.rawValue] as? Int)
    let extinfo = try XCTUnwrap(appData[ConversionsAPIUserAndAppDataField.extinfo.rawValue] as? [Any])
    XCTAssertEqual(ATE, 1)
    XCTAssertEqual(extinfo.count, 1)
    XCTAssertEqual(extinfo[0] as? String, "i2")
    let actionSource = try XCTUnwrap(parameters[OtherEventConstants.actionSource.rawValue] as? String)
    XCTAssertEqual(actionSource, OtherEventConstants.app.rawValue)

    let options = try XCTUnwrap(
      parameters[AppEventsConversionsAPITransformer.DataProcessingParameterName.options.rawValue] as? Int)
    let state = try XCTUnwrap(
      parameters[AppEventsConversionsAPITransformer.DataProcessingParameterName.state.rawValue] as? Int)
    XCTAssertEqual(options, 1)
    XCTAssertEqual(state, 0)
  }

  func checkIfMAIEventIsEqual(parameters: [String: Any]) throws {
    let eventName = try XCTUnwrap(parameters[ConversionsAPICustomEventField.eventName.rawValue] as? String)
    let eventTime = try XCTUnwrap(parameters[ConversionsAPICustomEventField.eventTime.rawValue] as? Int)
    XCTAssertEqual(eventName, OtherEventConstants.mobileAppInstall.rawValue)
    XCTAssertEqual(eventTime, 23456)
  }

  func checkIfCustomEvent1IsEqual(parameters: [String: Any]) throws {
    let eventName = try XCTUnwrap(parameters[ConversionsAPICustomEventField.eventName.rawValue] as? String)
    let eventTime = try XCTUnwrap(parameters[ConversionsAPICustomEventField.eventTime.rawValue] as? Int)

    XCTAssertEqual(eventName, "AddToCart")
    XCTAssertEqual(eventTime, 12345)
  }

  func checkIfCustomEvent2IsEqual(parameters: [String: Any]) throws {
    let eventName = try XCTUnwrap(parameters[ConversionsAPICustomEventField.eventName.rawValue] as? String)
    let eventTime = try XCTUnwrap(parameters[ConversionsAPICustomEventField.eventTime.rawValue] as? Int)
    let customData1 = try XCTUnwrap(parameters[ConversionsAPISection.customData.rawValue] as? [String: Any])
    let contentType = try XCTUnwrap(customData1[ConversionsAPICustomEventField.contentType.rawValue] as? String)
    let valueToSum = try XCTUnwrap(customData1[ConversionsAPICustomEventField.valueToSum.rawValue] as? Double)
    let currency = try XCTUnwrap(customData1[ConversionsAPICustomEventField.currency.rawValue] as? String)

    XCTAssertEqual(eventName, "new_event")
    XCTAssertEqual(eventTime, 67890)
    XCTAssertEqual(contentType, "product")
    XCTAssertEqual(valueToSum, 21.97)
    XCTAssertEqual(currency, "GBP")
  }

  // MARK: Tests

  func testTransformEvents() throws {
    let events = "[{\"_eventName\":\"fb_mobile_add_to_cart\",\"_logTime\":12345}]"
    let transformedEvents = AppEventsConversionsAPITransformer.transformEvents(from: events)
    XCTAssertEqual(transformedEvents?.count, 1)
    let firstEvent = try XCTUnwrap(transformedEvents?[0])
    try checkIfCustomEvent1IsEqual(parameters: firstEvent)
  }

  func testTransformMultipleEvents() throws {
    let transformedEvents = AppEventsConversionsAPITransformer.transformEvents(from: customEvent2)

    XCTAssertEqual(transformedEvents?.count, 2)
    let firstEvent = try XCTUnwrap(transformedEvents?[0])
    try checkIfCustomEvent1IsEqual(parameters: firstEvent)

    let secondEvent = try XCTUnwrap(transformedEvents?[1])
    try checkIfCustomEvent2IsEqual(parameters: secondEvent)
  }

  func testTransformAndUpdateAppAndUserData() throws {
    var userData = [String: Any]()
    var appData = [String: Any]()

    AppEventsConversionsAPITransformer.transformAndUpdateAppAndUserData(
      userData: &userData, appData: &appData, field: AppEventUserAndAppDataField.advertiserId, value: "ABCDE-12345"
    )
    AppEventsConversionsAPITransformer.transformAndUpdateAppAndUserData(
      userData: &userData, appData: &appData, field: AppEventUserAndAppDataField.advTE, value: 1
    )
    AppEventsConversionsAPITransformer.transformAndUpdateAppAndUserData(
      userData: &userData, appData: &appData, field: AppEventUserAndAppDataField.extinfo, value: ["i2"]
    )
    AppEventsConversionsAPITransformer.transformAndUpdateAppAndUserData(
      userData: &userData,
      appData: &appData,
      field: AppEventUserAndAppDataField.userData,
      value: "{\"fn\":\"1234567890\", \"em\":\"ABCDE\"}"
    )
    AppEventsConversionsAPITransformer.transformAndUpdateAppAndUserData(
      userData: &userData, appData: &appData, field: AppEventUserAndAppDataField.campaignIds, value: "testcampaignids"
    )

    let ATE = try XCTUnwrap(appData[ConversionsAPIUserAndAppDataField.advTE.rawValue] as? Int)
    let extinfo = try XCTUnwrap(appData[ConversionsAPIUserAndAppDataField.extinfo.rawValue] as? [Any])
    let campaignIds = try XCTUnwrap(appData[ConversionsAPIUserAndAppDataField.campaignIds.rawValue] as? String)
    let madid = try XCTUnwrap(userData[ConversionsAPIUserAndAppDataField.madid.rawValue] as? String)
    // swiftlint:disable:next identifier_name
    let fn = try XCTUnwrap(userData["fn"] as? String)
    // swiftlint:disable:next identifier_name
    let em = try XCTUnwrap(userData["em"] as? String)

    XCTAssertEqual(ATE, 1)
    XCTAssertEqual(extinfo.count, 1)
    XCTAssertEqual(extinfo[0] as? String, "i2")
    XCTAssertEqual(campaignIds, "testcampaignids")
    XCTAssertEqual(madid, "ABCDE-12345")
    XCTAssertEqual(fn, "1234567890")
    XCTAssertEqual(em, "ABCDE")
  }

  func testTransformValueContents() throws {
    let transformedValue = AppEventsConversionsAPITransformer.transformValue(
      field: CustomEventField.contents.rawValue,
      value: "[{\"id\": \"1234\", \"quantity\": 1,},{\"id\":\"5678\", \"quantity\": 2,}]"
    ) as? [[String: Any]]
    XCTAssertEqual(transformedValue?.count, 2)

    var dict = try XCTUnwrap(transformedValue?[0])
    XCTAssertEqual(dict["id"] as? String, "1234")
    XCTAssertEqual(dict["quantity"] as? Int, 1)

    dict = try XCTUnwrap(transformedValue?[1])

    XCTAssertEqual(dict["id"] as? String, "5678")
    XCTAssertEqual(dict["quantity"] as? Int, 2)
  }

  func testTransformValueATE() throws {
    let ateTransformedValue = try XCTUnwrap(
      AppEventsConversionsAPITransformer.transformValue(
        field: AppEventUserAndAppDataField.advTE.rawValue,
        value: "1"
      ) as? Bool
    )

    XCTAssertTrue(ateTransformedValue)
  }

  func testTransformValueExtinfo() throws {
    let transformedValue = AppEventsConversionsAPITransformer.transformValue(
      field: AppEventUserAndAppDataField.extinfo.rawValue,
      value: "[\"i0\", [\"i1\"], [\"i2\"]]"
    ) as? [Any]

    XCTAssertEqual(transformedValue?.count, 3)
    XCTAssertEqual(transformedValue?[0] as? String, "i0")
  }

  func testTransformValueEventTime() throws {
    var eventTime = AppEventsConversionsAPITransformer.transformValue(
      field: CustomEventField.eventTime.rawValue,
      value: 12345
    ) as? Int
    XCTAssertEqual(eventTime, 12345)

    eventTime = AppEventsConversionsAPITransformer.transformValue(
      field: CustomEventField.eventTime.rawValue,
      value: "12345"
    ) as? Int
    XCTAssertEqual(eventTime, 12345)

    eventTime = AppEventsConversionsAPITransformer.transformValue(
      field: CustomEventField.eventTime.rawValue,
      value: "abcd"
    ) as? Int
    XCTAssertNil(eventTime)
  }

  func testCombineCommonFields() throws {
    let combinedFields = AppEventsConversionsAPITransformer.combineCommonFields(
      userData: transformedUserData1,
      appData: transformedAppData1,
      restOfData: restOfData1
    )
    try checkIfCommonFieldsAreEqual(parameters: combinedFields)
  }

  func testCombineAllTransformedData1() throws {
    let transformedEvents = AppEventsConversionsAPITransformer.combineAllTransformedData(
      eventType: AppEventType.mobileAppInstall,
      userData: transformedUserData1,
      appData: transformedAppData1,
      restOfData: restOfData1,
      customEvents: [[String: Any]](),
      eventTime: 23456
    )
    XCTAssertEqual(transformedEvents?.count, 1)

    let firstEvent = try XCTUnwrap(transformedEvents?[0])

    try checkIfCommonFieldsAreEqual(parameters: firstEvent)
    try checkIfMAIEventIsEqual(parameters: firstEvent)
  }

  func testCombineAllTransformedData2() throws {
    let customEvents = [transformedCustomEvent1, transformedCustomEvent2]

    let transformedEvents = AppEventsConversionsAPITransformer.combineAllTransformedData(
      eventType: AppEventType.custom,
      userData: transformedUserData1,
      appData: transformedAppData1,
      restOfData: restOfData1,
      customEvents: customEvents,
      eventTime: nil
    )
    XCTAssertEqual(transformedEvents?.count, 2)

    let firstEvent = try XCTUnwrap(transformedEvents?[0])
    try checkIfCommonFieldsAreEqual(parameters: firstEvent)
    try checkIfCustomEvent1IsEqual(parameters: firstEvent)

    let secondEvent = try XCTUnwrap(transformedEvents?[1])
    try checkIfCommonFieldsAreEqual(parameters: secondEvent)
    try checkIfCustomEvent2IsEqual(parameters: secondEvent)
  }

  func testCombineAllTransformedDataCheckForNulls() {
    let customEvents = [transformedCustomEvent1, transformedCustomEvent2]

    XCTAssertNil(AppEventsConversionsAPITransformer.combineAllTransformedData(
      eventType: AppEventType.other,
      userData: transformedUserData1,
      appData: transformedAppData1,
      restOfData: restOfData1,
      customEvents: customEvents,
      eventTime: nil
    ))
    XCTAssertNil(AppEventsConversionsAPITransformer.combineAllTransformedData(
      eventType: AppEventType.custom,
      userData: transformedUserData1,
      appData: transformedAppData1,
      restOfData: restOfData1,
      customEvents: [[String: Any]](),
      eventTime: nil
    ))
    XCTAssertNil(AppEventsConversionsAPITransformer.combineAllTransformedData(
      eventType: AppEventType.mobileAppInstall,
      userData: transformedUserData1,
      appData: transformedAppData1,
      restOfData: restOfData1,
      customEvents: [[String: Any]](),
      eventTime: nil
    ))
  }

  func testConversionsAPICompatibleEvent1() throws {
    var parameters = [
      "event": "MOBILE_APP_INSTALL",
      "advertiser_id": "ABCDE-12345",
      "advertiser_tracking_enabled": 1,
      "ud": ud,
      "extinfo": ["i2"],
      "install_timestamp": 23456,
    ] as [String: Any]
    parameters.merge(restOfData1) { $1 }

    let transformedEvents = AppEventsConversionsAPITransformer.conversionsAPICompatibleEvent(from: parameters)
    XCTAssertEqual(transformedEvents?.count, 1)
    let firstEvent = try XCTUnwrap(transformedEvents?[0])
    try checkIfCommonFieldsAreEqual(parameters: firstEvent)
    try checkIfMAIEventIsEqual(parameters: firstEvent)
  }

  func testConversionsAPICompatibleEvent2() throws {
    var parameters = [
      "event": "CUSTOM_APP_EVENTS",
      "advertiser_id": "ABCDE-12345",
      "advertiser_tracking_enabled": 1,
      "ud": ud,
      "extinfo": ["i2"],
      "custom_events": customEvent2,
    ] as [String: Any]
    parameters.merge(restOfData1) { $1 }

    let transformedEvents = AppEventsConversionsAPITransformer.conversionsAPICompatibleEvent(from: parameters)
    XCTAssertEqual(transformedEvents?.count, 2)
    let secondEvent = try XCTUnwrap(transformedEvents?[1])

    try checkIfCommonFieldsAreEqual(parameters: secondEvent)
    try checkIfCustomEvent2IsEqual(parameters: secondEvent)
  }
}


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

  enum Keys {
    static let eventName = "event_name"
  }

  enum Values {
    static let datasetID = "id123"
    static let accessKey = "key123"
    static let cloudbridgeURL = "www.123.com"
    static let testEvent = "test"
  }

  override func setUp() {
    super.setUp()
    FBSDKTransformerGraphRequestFactory.shared.transformedEvents = []
  }

  func testConfigure() {
    FBSDKTransformerGraphRequestFactory.shared.configure(
      datasetID: Values.datasetID,
      url: Values.cloudbridgeURL,
      accessKey: Values.accessKey
    )

    XCTAssertEqual(
      FBSDKTransformerGraphRequestFactory.shared.credentials?.datasetID,
      Values.datasetID,
      "Credential's dataset ID is not expected"
    )
    XCTAssertEqual(
      FBSDKTransformerGraphRequestFactory.shared.credentials?.accessKey,
      Values.accessKey,
      "Credential's access key is not expected"
    )
    XCTAssertEqual(
      FBSDKTransformerGraphRequestFactory.shared.credentials?.capiGatewayURL,
      Values.cloudbridgeURL,
      "Credential's cloudbridge url is not expected"
    )
  }

  func testCapiGatewayRequestDictionary() throws {
    FBSDKTransformerGraphRequestFactory.shared.configure(
      datasetID: Values.datasetID,
      url: Values.cloudbridgeURL,
      accessKey: Values.accessKey
    )

    let event = [["_eventName": "fb_mobile_add_to_cart"]] as [[String: String]]

    let dictionary = FBSDKTransformerGraphRequestFactory.shared.capiGatewayRequestDictionary(with: event)

    XCTAssertEqual(dictionary["accessKey"] as? String, "key123")
    XCTAssertEqual(dictionary["data"] as? [[String: String]], event)
  }

  func testErrorHandlingWithServerError() throws {
    let url = try XCTUnwrap(URL(string: "graph.facebook.com"))
    var response = HTTPURLResponse(url: url, statusCode: 400, httpVersion: "HTTP/1.1", headerFields: nil)

    FBSDKTransformerGraphRequestFactory.shared.handleError(
      response: response,
      events: [[Keys.eventName: Values.testEvent]]
    )
    XCTAssertTrue(
      FBSDKTransformerGraphRequestFactory.shared.transformedEvents.isEmpty,
      "Should not re-append the events to the cache queue if the request fails for server error with 400"
    )

    response = HTTPURLResponse(url: url, statusCode: 500, httpVersion: "HTTP/1.1", headerFields: nil)

    FBSDKTransformerGraphRequestFactory.shared.handleError(
      response: response,
      events: [[Keys.eventName: Values.testEvent]]
    )
    XCTAssertTrue(
      FBSDKTransformerGraphRequestFactory.shared.transformedEvents.isEmpty,
      "Should not re-append the events to the cache queue if the request fails for server error with 500"
    )
  }

  func testErrorHandlingWithoutServerError() throws {
    let url = try XCTUnwrap(URL(string: "graph.facebook.com"))
    var response = HTTPURLResponse(url: url, statusCode: 503, httpVersion: "HTTP/1.1", headerFields: nil)

    FBSDKTransformerGraphRequestFactory.shared.transformedEvents = [[Keys.eventName: "purchase"]]
    FBSDKTransformerGraphRequestFactory.shared.handleError(
      response: response,
      events: [[Keys.eventName: Values.testEvent]]
    )
    XCTAssertEqual(
      FBSDKTransformerGraphRequestFactory.shared.transformedEvents.count,
      2,
      "Should re-append the event to the cache queue if the request fails due to 503 error"
    )
    XCTAssertEqual(
      FBSDKTransformerGraphRequestFactory.shared.transformedEvents.first as? [String: String],
      [Keys.eventName: Values.testEvent],
      "The appended event is not expected"
    )

    response = HTTPURLResponse(url: url, statusCode: 429, httpVersion: "HTTP/1.1", headerFields: nil)

    FBSDKTransformerGraphRequestFactory.shared.handleError(
      response: response,
      events: [[Keys.eventName: "addToCart"]]
    )
    XCTAssertEqual(
      FBSDKTransformerGraphRequestFactory.shared.transformedEvents.count,
      3,
      "Should re-append the event to the cache queue if the request fails due to 429 error"
    )

    XCTAssertEqual(
      FBSDKTransformerGraphRequestFactory.shared.transformedEvents.first as? [String: String],
      [Keys.eventName: "addToCart"],
      "The appended event is not expected"
    )
  }

  func testAppendEventsOverLimits() {
    let event = [Keys.eventName: Values.testEvent]
    var events: [[String: Any]] = []
    for _ in 0 ..< 100 {
      events.append(event)
    }

    for _ in 0 ..< 990 {
      FBSDKTransformerGraphRequestFactory.shared.transformedEvents.append(event)
    }

    FBSDKTransformerGraphRequestFactory.shared.appendEvents(events: events)
    XCTAssertEqual(
      FBSDKTransformerGraphRequestFactory.shared.transformedEvents.count,
      1000,
      "Cached event queue should have events no more than 1000"
    )
  }

  func testAppendEventsWithinLimits() {
    let event = [Keys.eventName: Values.testEvent]
    var events: [[String: Any]] = []

    for _ in 0 ..< 100 {
      events.append(event)
    }

    for _ in 0 ..< 10 {
      FBSDKTransformerGraphRequestFactory.shared.transformedEvents.append(event)
    }

    FBSDKTransformerGraphRequestFactory.shared.appendEvents(events: events)
    XCTAssertEqual(
      FBSDKTransformerGraphRequestFactory.shared.transformedEvents.count,
      110,
      "Cached event queue has unexpected number of events"
    )
  }
}


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

  let graphRequestFactory = TestGraphRequestFactory()
  let dataStore = UserDefaultsSpy()
  let connection = TestGraphRequestConnection()
  lazy var graphRequestConnectionFactory = TestGraphRequestConnectionFactory.create(withStubbedConnection: connection)
  let settings = TestSettings()
  let advertiserIDProvider = TestAdvertiserIDProvider()
  let appID = "123"
  let enabledConfiguration = ServerConfigurationFixtures.configuration(withDictionary: ["codelessEventsEnabled": true])
  let serverConfigurationProvider = TestServerConfigurationProvider()
  lazy var codelessSettingStorageKey = "com.facebook.sdk:codelessSetting\(name)"
  var capturedIsEnabled = false
  var capturedError: Error?
  let frame = CGRect(x: 20, y: 20, width: 36, height: 36)
  lazy var view = UIView(frame: frame)
  let autoEventSetupEnabled = "auto_event_setup_enabled"

  enum Keys {
    static let codelessEnabled = "is_app_indexing_enabled"
    static let fields = "fields"
    static let advertiserID = "advertiser_id"
    static let deviceSessionID = "device_session_id"
    static let extInfo = "extinfo"
  }

  override func setUp() {
    super.setUp()

    Self.reset()

    settings.appID = name

    _CodelessIndexer.configure(
      graphRequestFactory: graphRequestFactory,
      serverConfigurationProvider: serverConfigurationProvider,
      dataStore: dataStore,
      graphRequestConnectionFactory: graphRequestConnectionFactory,
      swizzler: TestSwizzler.self,
      settings: settings,
      advertiserIDProvider: advertiserIDProvider
    )
  }

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

    reset()
  }

  class func reset() {
    _CodelessIndexer.reset()
    TestSwizzler.reset()
  }

  // MARK: - Dependencies

  func testDefaultDependencies() {
    _CodelessIndexer.reset()

    XCTAssertNil(
      _CodelessIndexer.graphRequestFactory,
      "Should not have a request provider by default"
    )
    XCTAssertNil(
      _CodelessIndexer.serverConfigurationProvider,
      "Should not have a server configuration provider by default"
    )
    XCTAssertNil(
      _CodelessIndexer.dataStore,
      "Should not have a persistent data store by default"
    )
    XCTAssertNil(
      _CodelessIndexer.graphRequestConnectionFactory,
      "Should not have a connection provider by default"
    )
    XCTAssertNil(
      _CodelessIndexer.swizzler,
      "Should not have a swizzler by default"
    )
    XCTAssertNil(
      _CodelessIndexer.settings,
      "Should not have a settings instance by default"
    )
    XCTAssertNil(
      _CodelessIndexer.advertiserIDProvider,
      "Should not have an advertiser ID provider by default"
    )
  }

  func testConfiguringWithDependencies() {
    XCTAssertIdentical(
      _CodelessIndexer.graphRequestFactory as AnyObject,
      graphRequestFactory,
      "Should be able to configure with a request provider"
    )
    XCTAssertTrue(
      _CodelessIndexer.serverConfigurationProvider is TestServerConfigurationProvider,
      "Should be able to configure with a server configuration provider"
    )
    XCTAssertEqual(
      _CodelessIndexer.dataStore as? UserDefaultsSpy,
      dataStore,
      "Should be able to configure with a persistent data store"
    )
    XCTAssertEqual(
      _CodelessIndexer.graphRequestConnectionFactory as? TestGraphRequestConnectionFactory,
      graphRequestConnectionFactory,
      "Should be able to configure with a connection provider"
    )
    XCTAssertTrue(
      _CodelessIndexer.swizzler is TestSwizzler.Type,
      "Should be able to configure with a swizzler"
    )
    XCTAssertTrue(
      _CodelessIndexer.settings is TestSettings,
      "Should be able to configure with a settings"
    )
    XCTAssertTrue(
      _CodelessIndexer.advertiserIDProvider is TestAdvertiserIDProvider,
      "Should be able to configure with an advertiser ID provider"
    )
  }

  // MARK: - Setup Request

  func testSetupRequestWithoutAdvertiserID() {
    XCTAssertNil(
      _CodelessIndexer.requestToLoadCodelessSetup(appID: appID),
      "Should not create a request to load the codeless setup if there is no advertiser ID"
    )
  }

  func testSetupRequestWithAdvertiserID() {
    advertiserIDProvider.advertiserID = name

    _CodelessIndexer.requestToLoadCodelessSetup(appID: appID)

    let expectedParameters = [
      Keys.fields: autoEventSetupEnabled,
      Keys.advertiserID: name,
    ]
    XCTAssertEqual(
      graphRequestFactory.capturedGraphPath,
      appID,
      "Should create a request using the app identifier as the path"
    )
    XCTAssertEqual(
      graphRequestFactory.capturedParameters as? [String: String],
      expectedParameters,
      "Should create a request with the expected parameters"
    )
    XCTAssertNil(
      graphRequestFactory.capturedTokenString,
      "Should not include a token string in the request"
    )
    XCTAssertNil(
      graphRequestFactory.capturedHTTPMethod,
      "Should not specify an http method when creating the request"
    )
    XCTAssertEqual(
      graphRequestFactory.capturedFlags,
      [.skipClientToken, .disableErrorRecovery],
      "Should create a request with the expected flags"
    )
  }

  // MARK: - Enabling

  func testEnablingSetsGesture() {
    _CodelessIndexer.enable()

    XCTAssertTrue(
      TestSwizzler.evidence.contains(
        SwizzleEvidence(
          selector: #selector(UIApplication.motionBegan(_:with:)),
          class: UIApplication.self
        )
      )
    )
    TestSwizzler.reset()

    _CodelessIndexer.enable()

    XCTAssertTrue(
      TestSwizzler.evidence.isEmpty,
      "Should only swizzle the shake gesture once"
    )
  }

  // MARK: - Loading Setup

  func testLoadingSettingWithoutAppID() {
    settings.appID = nil

    _CodelessIndexer.loadCodelessSetting { _, _ in
      XCTFail("Should not load a codeless setting without an app identifier")
    }
    XCTAssertFalse(
      serverConfigurationProvider.loadServerConfigurationWasCalled,
      "Should not load the server configuration if the app identifier is missing"
    )
  }

  func testLoadingSettingWithCodelessEventsDisabledByConfiguration() {
    _CodelessIndexer.loadCodelessSetting { _, _ in
      XCTFail("Should not load a codeless setting when codeless events are disabled")
    }
    serverConfigurationProvider.capturedCompletionBlock?(
      ServerConfigurationFixtures.defaultConfiguration,
      nil
    )
    XCTAssertNil(
      dataStore.capturedObjectRetrievalKey,
      "Should not attempt to read the cached codeless setting when codeless events are disabled"
    )
  }

  func testLoadingValidCachedSetting() throws {
    dataStore.set(try archivedSetting(), forKey: codelessSettingStorageKey)

    _CodelessIndexer.loadCodelessSetting { isEnabled, potentialError in
      self.capturedIsEnabled = isEnabled
      self.capturedError = potentialError
    }

    serverConfigurationProvider.capturedCompletionBlock?(enabledConfiguration, nil)

    XCTAssertEqual(
      dataStore.capturedObjectRetrievalKey,
      codelessSettingStorageKey,
      "Should read the cached codeless setting"
    )
    XCTAssertNil(
      capturedError,
      "Should not complete with an error when there is a valid cached setting"
    )
    XCTAssertTrue(
      capturedIsEnabled,
      "Should complete with the enabled value from cached setting"
    )
  }

  func testLoadingExpiredCachedSettingWithoutAdvertiserID() throws {
    dataStore.set(try archivedSetting(date: .distantPast), forKey: codelessSettingStorageKey)

    _CodelessIndexer.loadCodelessSetting { _, _ in
      XCTFail("Should not invoke the completion")
    }

    serverConfigurationProvider.capturedCompletionBlock?(enabledConfiguration, nil)

    XCTAssertNil(
      connection.capturedRequest,
      "Should not add a request to fetch the codeless setting if there is no advertiser identifier"
    )
  }

  func testLoadingExpiredCachedSettingWithAdvertiserID() throws {
    advertiserIDProvider.advertiserID = name
    dataStore.set(try archivedSetting(date: .distantPast), forKey: codelessSettingStorageKey)

    _CodelessIndexer.loadCodelessSetting { _, _ in
      XCTFail("Should not invoke the completion")
    }

    serverConfigurationProvider.capturedCompletionBlock?(enabledConfiguration, nil)

    XCTAssertNotNil(
      connection.capturedRequest,
      "Should add a request to fetch the codeless setting"
    )
    XCTAssertEqual(
      connection.startCallCount,
      1,
      "Should start the request to fetch the codeless setting"
    )
  }

  func testCompletingLoadingSettingWithOnlyError() throws {
    advertiserIDProvider.advertiserID = name
    dataStore.set(try archivedSetting(date: .distantPast), forKey: codelessSettingStorageKey)

    _CodelessIndexer.loadCodelessSetting { _, _ in
      XCTFail("Should not invoke the completion if the network call completes with an error")
    }

    serverConfigurationProvider.capturedCompletionBlock?(enabledConfiguration, nil)

    connection.capturedCompletion?(nil, nil, SampleError())
  }

  func testCompletingLoadingSettingWithMissingResults() {
    advertiserIDProvider.advertiserID = name

    _CodelessIndexer.loadCodelessSetting { isEnabled, potentialError in
      self.capturedIsEnabled = isEnabled
      self.capturedError = potentialError
    }

    serverConfigurationProvider.capturedCompletionBlock?(enabledConfiguration, nil)

    connection.capturedCompletion?(nil, nil, nil)

    XCTAssertNil(
      capturedError,
      "Should not complete with an error when there are missing results"
    )
    XCTAssertFalse(
      capturedIsEnabled,
      "Should complete with a default enabled value of false when there are no results"
    )
  }

  func testCompletingLoadingSettingWithInvalidResults() {
    advertiserIDProvider.advertiserID = name

    _CodelessIndexer.loadCodelessSetting { isEnabled, potentialError in
      self.capturedIsEnabled = isEnabled
      self.capturedError = potentialError
    }

    serverConfigurationProvider.capturedCompletionBlock?(enabledConfiguration, nil)

    connection.capturedCompletion?(nil, ["foo": "bar"], nil)

    XCTAssertNil(
      capturedError,
      "Should not complete with an error when the results are invalid"
    )
    XCTAssertFalse(
      capturedIsEnabled,
      "Should complete with a default enabled value of false when the results are invalid"
    )
  }

  func testCompletingLoadingSettingWithValidResults() {
    advertiserIDProvider.advertiserID = name

    _CodelessIndexer.loadCodelessSetting { isEnabled, potentialError in
      self.capturedIsEnabled = isEnabled
      self.capturedError = potentialError
    }

    serverConfigurationProvider.capturedCompletionBlock?(enabledConfiguration, nil)

    connection.capturedCompletion?(nil, [autoEventSetupEnabled: true], nil)

    XCTAssertNil(capturedError)
    XCTAssertTrue(
      capturedIsEnabled,
      "Should complete with the enabled value from the network result"
    )
    XCTAssertEqual(
      dataStore.capturedSetObjectKey,
      codelessSettingStorageKey,
      "Should persist the fetched setting"
    )
  }

  func testCompletingLoadingNewSettingWithExpiredCachedSetting() throws {
    advertiserIDProvider.advertiserID = name
    dataStore.set(try archivedSetting(date: .distantPast), forKey: codelessSettingStorageKey)

    _CodelessIndexer.loadCodelessSetting { isEnabled, potentialError in
      self.capturedIsEnabled = isEnabled
      self.capturedError = potentialError
    }

    serverConfigurationProvider.capturedCompletionBlock?(enabledConfiguration, nil)

    connection.capturedCompletion?(nil, [autoEventSetupEnabled: true], nil)

    XCTAssertNil(capturedError)
    XCTAssertTrue(
      capturedIsEnabled,
      "Should complete with the enabled value from the network result"
    )
    XCTAssertEqual(
      dataStore.capturedSetObjectKey,
      codelessSettingStorageKey,
      "Should persist the fetched setting"
    )
  }

  // MARK: - Uploading Indexing

  enum SampleViewHierarchyTrees {
    static let empty = ""
    static let valid = "UIButton"
  }

  func testUploadingWithoutAppID() {
    settings.appID = nil
    _CodelessIndexer.uploadIndexing(SampleViewHierarchyTrees.empty)

    XCTAssertNotNil(
      graphRequestFactory.capturedGraphPath,
      "Should not create an upload request without an app identifier but it will"
    )
  }

  func testUploadingWithoutViewHierarchy() {
    _CodelessIndexer.uploadIndexing(nil)

    XCTAssertNil(
      graphRequestFactory.capturedGraphPath,
      "Should not create an upload request without a view hierarchy"
    )
  }

  func testUploadingWithEmptyViewHierarchy() {
    _CodelessIndexer.uploadIndexing(SampleViewHierarchyTrees.empty)

    XCTAssertNotNil(
      graphRequestFactory.capturedGraphPath,
      "Should not create an upload request with an empty view hierarchy but it will"
    )
  }

  func testUploadingWhileUploadInProgress() {
    _CodelessIndexer.uploadIndexing(SampleViewHierarchyTrees.valid)
    // Reset the test evidence to be able to check that the second call
    // does not create a graph request
    graphRequestFactory.capturedGraphPath = nil
    _CodelessIndexer.uploadIndexing(SampleViewHierarchyTrees.valid)

    XCTAssertNil(
      graphRequestFactory.capturedGraphPath,
      "Should not create an upload request if an upload is in progress"
    )
  }

  func testUploadingIdenticalViewHierarchy() {
    _CodelessIndexer.uploadIndexing(SampleViewHierarchyTrees.valid)
    // Reset the flag so that it treats the first call as completed
    _CodelessIndexer.resetIsCodelessIndexing()
    // Reset the test evidence to be able to check that the second call
    // does not create a graph request
    graphRequestFactory.capturedGraphPath = nil
    _CodelessIndexer.uploadIndexing(SampleViewHierarchyTrees.valid)

    XCTAssertNil(
      graphRequestFactory.capturedGraphPath,
      "Should not create an upload request for a hierarchy that is identical to one that was previously uploaded"
    )
  }

  func testUploadRequest() {
    _CodelessIndexer.uploadIndexing(SampleViewHierarchyTrees.valid)

    XCTAssertEqual(
      graphRequestFactory.capturedGraphPath,
      "\(settings.appID!)/app_indexing", // swiftlint:disable:this force_unwrapping
      "Should create a request with the expected graph path"
    )
    XCTAssertEqual(
      graphRequestFactory.capturedHTTPMethod,
      .post,
      "Should create a request with the expected http method"
    )

    let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""

    XCTAssertEqual(
      graphRequestFactory.capturedParameters as? [String: String],
      [
        "app_version": appVersion,
        Keys.deviceSessionID: _CodelessIndexer.currentSessionDeviceID!, // swiftlint:disable:this force_unwrapping
        "platform": "iOS",
        "tree": "UIButton",
      ],
      "Should create a request with the expected parameters"
    )
    XCTAssertNil(
      graphRequestFactory.capturedFlags,
      "Should create a request with the expected flags"
    )
  }

  func testCompletingUploadWithoutInformation() {
    let sessionID = _CodelessIndexer.currentSessionDeviceID
    _CodelessIndexer.uploadIndexing(SampleViewHierarchyTrees.valid)

    guard let completion = graphRequestFactory.capturedRequests.first?.capturedCompletionHandler else {
      return XCTFail("Should start a request with a completion handler")
    }
    completion(nil, nil, nil)

    XCTAssertEqual(
      sessionID,
      _CodelessIndexer.currentSessionDeviceID,
      "Completing with no results or error should be treated as a noop"
    )
  }

  func testCompletingUploadWithErrorOnly() {
    let sessionID = _CodelessIndexer.currentSessionDeviceID
    _CodelessIndexer.uploadIndexing(SampleViewHierarchyTrees.valid)

    guard let completion = graphRequestFactory.capturedRequests.first?.capturedCompletionHandler else {
      return XCTFail("Should start a request with a completion handler")
    }
    completion(nil, nil, SampleError())

    XCTAssertEqual(
      sessionID,
      _CodelessIndexer.currentSessionDeviceID,
      "Completing with only an error should be treated as a noop"
    )
  }

  func testCompletingUploadWithResultsIndicatingThatCodelessIsEnabled() {
    let sessionID = _CodelessIndexer.currentSessionDeviceID
    _CodelessIndexer.uploadIndexing(SampleViewHierarchyTrees.valid)

    guard let completion = graphRequestFactory.capturedRequests.first?.capturedCompletionHandler else {
      return XCTFail("Should start a request with a completion handler")
    }
    completion(nil, [Keys.codelessEnabled: true], nil)

    XCTAssertEqual(
      sessionID,
      _CodelessIndexer.currentSessionDeviceID,
      "Completing with results indicating that codeless is enabled should be treated as a noop"
    )
  }

  func testCompletingUploadWithResultsIndicatingThatCodelessIsDisabled() {
    let sessionID = _CodelessIndexer.currentSessionDeviceID
    _CodelessIndexer.uploadIndexing(SampleViewHierarchyTrees.valid)

    guard let completion = graphRequestFactory.capturedRequests.first?.capturedCompletionHandler else {
      return XCTFail("Should start a request with a completion handler")
    }
    completion(nil, [Keys.codelessEnabled: false], nil)

    XCTAssertNotEqual(
      sessionID,
      _CodelessIndexer.currentSessionDeviceID,
      """
      Completing with results indicating that codeless is disabled should
      reset the session identifier
      """
    )
  }

  // MARK: - Checking Indexing Session

  func testCheckingIndexingSessionWhileIndexing() {
    _CodelessIndexer.checkCodelessIndexingSession()
    graphRequestFactory.capturedRequests.first?.capturedCompletionHandler = nil
    _CodelessIndexer.checkCodelessIndexingSession()

    XCTAssertNil(
      graphRequestFactory.capturedRequests.first?.capturedCompletionHandler,
      "Should not create a second request to cehck the indexing status if the first is incomplete"
    )
  }

  func testCheckingIndexingSessionRequest() {
    _CodelessIndexer.checkCodelessIndexingSession()

    guard let sessionID = _CodelessIndexer.currentSessionDeviceID else {
      return XCTFail("Should provide a session device identifier")
    }

    let expectedParameters = [
      Keys.deviceSessionID: sessionID,
      Keys.extInfo: _CodelessIndexer.extInfo,
    ]

    XCTAssertEqual(
      graphRequestFactory.capturedGraphPath,
      "\(name)/app_indexing_session",
      "Should request the session with the expected graph path"
    )
    XCTAssertEqual(
      graphRequestFactory.capturedParameters as? [String: String],
      expectedParameters,
      "Should request the session with the expected parameters"
    )
    XCTAssertEqual(
      graphRequestFactory.capturedHTTPMethod,
      .post,
      "Should request the session with the expected http method"
    )
  }

  func testCompleteCheckingIndexingSessionWithNoInput() {
    _CodelessIndexer.checkCodelessIndexingSession()

    graphRequestFactory.capturedRequests.first?.capturedCompletionHandler?(nil, nil, nil)

    XCTAssertFalse(
      _CodelessIndexer.isCheckingSession,
      "Should reset the ability to check a session after the request completes"
    )
    XCTAssertNil(
      _CodelessIndexer.appIndexingTimer,
      "Should not create an indexing timer without a result"
    )
  }

  func testCompleteCheckingIndexingSessionWithErrorOnly() {
    _CodelessIndexer.checkCodelessIndexingSession()

    graphRequestFactory.capturedRequests.first?.capturedCompletionHandler?(nil, nil, SampleError())

    XCTAssertFalse(_CodelessIndexer.isCheckingSession)
    XCTAssertNil(
      _CodelessIndexer.appIndexingTimer,
      "Should not create an indexing timer if there is an error"
    )
  }

  func testCompleteCheckingIndexingSessionWithInvalidResults() {
    _CodelessIndexer.checkCodelessIndexingSession()

    (1 ... 20).forEach { _ in
      graphRequestFactory.capturedRequests.first?.capturedCompletionHandler?(nil, Fuzzer.random, nil)

      XCTAssertNil(
        _CodelessIndexer.appIndexingTimer,
        "Should not create an indexing timer if the result is invalid"
      )
    }
  }

  func testCompletingCheckingIndexingWithCodelessEnabledResult() {
    _CodelessIndexer.checkCodelessIndexingSession()

    graphRequestFactory.capturedRequests.first?.capturedCompletionHandler?(nil, [Keys.codelessEnabled: true], nil)

    XCTAssertFalse(_CodelessIndexer.isCheckingSession)
    XCTAssertNotNil(
      _CodelessIndexer.appIndexingTimer,
      "Should create an indexing timer if the result indicates that codeless is enabled"
    )
    // Invalidate for cleanup
    _CodelessIndexer.appIndexingTimer?.invalidate()
  }

  func testCompletingCheckingIndexingWithCodelessDisabledResult() {
    let sessionIdentifier = _CodelessIndexer.currentSessionDeviceID
    _CodelessIndexer.checkCodelessIndexingSession()

    graphRequestFactory.capturedRequests.first?.capturedCompletionHandler?(nil, [Keys.codelessEnabled: false], nil)

    XCTAssertFalse(_CodelessIndexer.isCheckingSession)
    XCTAssertNil(
      _CodelessIndexer.appIndexingTimer,
      "Should not create an indexing timer if the result indicates that codeless is disabled"
    )
    XCTAssertNotEqual(
      sessionIdentifier,
      _CodelessIndexer.currentSessionDeviceID,
      "Should reset the current session device identifier if the result indicates that codeless is disabled"
    )
  }

  // MARK: - Miscellaneous

  func testExtraInfo() throws {
    var systemInfo = utsname()
    uname(&systemInfo)
    let data = Data(bytes: &systemInfo.machine, count: Int(_SYS_NAMELEN))
    let machine = try XCTUnwrap(
      String(bytes: data, encoding: .ascii)?.trimmingCharacters(in: .controlCharacters),
      "Unable to find host architecture"
    )

    XCTAssertEqual(
      _CodelessIndexer.extInfo,
      """
      ["\(machine)","","1","1","en_US"]
      """,
      "Should be able to provide extra info as a string representation of an array of strings"
    )
  }

  func testCurrentSessionDeviceID() {
    let identifier = _CodelessIndexer.currentSessionDeviceID

    XCTAssertEqual(
      _CodelessIndexer.currentSessionDeviceID,
      identifier,
      "Should only create a single session device ID"
    )

    _CodelessIndexer.reset()

    XCTAssertNotEqual(
      _CodelessIndexer.currentSessionDeviceID,
      identifier,
      "Should create unique session device IDs per sdk launch"
    )
  }

  func testDimensionOfNonView() {
    let dimensions = _CodelessIndexer.dimension(of: "" as NSString)

    XCTAssertEqual(
      dimensions,
      [
        CODELESS_VIEW_TREE_TOP_KEY: 0,
        CODELESS_VIEW_TREE_LEFT_KEY: 0,
        CODELESS_VIEW_TREE_WIDTH_KEY: 0,
        CODELESS_VIEW_TREE_HEIGHT_KEY: 0,
        CODELESS_VIEW_TREE_OFFSET_X_KEY: 0,
        CODELESS_VIEW_TREE_OFFSET_Y_KEY: 0,
        CODELESS_VIEW_TREE_VISIBILITY_KEY: 0,
      ],
      "Dimension of a non-view should be considered to be zero"
    )
  }

  func testDimensionOfView() {
    let dimensions = _CodelessIndexer.dimension(of: view)

    XCTAssertEqual(
      dimensions,
      [
        CODELESS_VIEW_TREE_TOP_KEY: 20,
        CODELESS_VIEW_TREE_LEFT_KEY: 20,
        CODELESS_VIEW_TREE_WIDTH_KEY: 36,
        CODELESS_VIEW_TREE_HEIGHT_KEY: 36,
        CODELESS_VIEW_TREE_OFFSET_X_KEY: 0,
        CODELESS_VIEW_TREE_OFFSET_Y_KEY: 0,
        CODELESS_VIEW_TREE_VISIBILITY_KEY: 0,
      ],
      "Should calculate the correct view dimensions"
    )
  }

  func testDimensionOfViewController() {
    let controller = UIViewController()
    controller.view = view
    let dimensions = _CodelessIndexer.dimension(of: controller)

    XCTAssertEqual(
      dimensions,
      [
        CODELESS_VIEW_TREE_TOP_KEY: 20,
        CODELESS_VIEW_TREE_LEFT_KEY: 20,
        CODELESS_VIEW_TREE_WIDTH_KEY: 36,
        CODELESS_VIEW_TREE_HEIGHT_KEY: 36,
        CODELESS_VIEW_TREE_OFFSET_X_KEY: 0,
        CODELESS_VIEW_TREE_OFFSET_Y_KEY: 0,
        CODELESS_VIEW_TREE_VISIBILITY_KEY: 0,
      ],
      "Should calculate the correct view dimensions"
    )
  }

  func testDimensionOfScrollView() {
    let view = UIScrollView(frame: frame)
    view.setContentOffset(CGPoint(x: 100, y: 100), animated: false)
    let dimensions = _CodelessIndexer.dimension(of: view)

    XCTAssertEqual(
      dimensions,
      [
        CODELESS_VIEW_TREE_TOP_KEY: 20,
        CODELESS_VIEW_TREE_LEFT_KEY: 20,
        CODELESS_VIEW_TREE_WIDTH_KEY: 36,
        CODELESS_VIEW_TREE_HEIGHT_KEY: 36,
        CODELESS_VIEW_TREE_OFFSET_X_KEY: 100,
        CODELESS_VIEW_TREE_OFFSET_Y_KEY: 100,
        CODELESS_VIEW_TREE_VISIBILITY_KEY: 0,
      ],
      "Should calculate the correct view dimensions"
    )
  }

  func testDimensionOfHiddenView() {
    view.isHidden = true
    let dimensions = _CodelessIndexer.dimension(of: view)

    XCTAssertEqual(
      dimensions,
      [
        CODELESS_VIEW_TREE_TOP_KEY: 20,
        CODELESS_VIEW_TREE_LEFT_KEY: 20,
        CODELESS_VIEW_TREE_WIDTH_KEY: 36,
        CODELESS_VIEW_TREE_HEIGHT_KEY: 36,
        CODELESS_VIEW_TREE_OFFSET_X_KEY: 0,
        CODELESS_VIEW_TREE_OFFSET_Y_KEY: 0,
        CODELESS_VIEW_TREE_VISIBILITY_KEY: 4,
      ],
      "Should calculate the correct view dimensions and indicate hidden status"
    )
  }

  // MARK: - Helpers

  func archivedSetting(
    isEnabled: Bool = true,
    date: Date = Date()
  ) throws -> Data {
    try NSKeyedArchiver.archivedData(
      withRootObject: [
        "codeless_setup_enabled": isEnabled,
        "codeless_setting_timestamp": date,
      ],
      requiringSecureCoding: 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.
 */

import XCTest

extension EventBindingManagerTests {

  var firstIndexPath: IndexPath {
    IndexPath(row: 0, section: 0)
  }

  // MARK: - Touch Handling

  func testDispatchingTouchesToRnWithRnViewInHierarchy() {
    let touch = TestTouch()
    let (reactView, view) = ViewHierarchies.viewWithReactNativeAncestor(interactionEnabled: true)
    touch.stubbedView = view

    guard let expectedReactTag = (reactView as? TestReactNativeView)?.reactTag else {
      return XCTFail("Should have a tagged react native view")
    }
    let binding = TestEventBinding()
    manager.reactBindings = [expectedReactTag: binding]

    manager.handleReactNativeTouches(
      withHandler: nil,
      command: nil,
      touches: Set([touch]),
      eventName: "touchEnd"
    )
    XCTAssertTrue(
      binding.trackEventWasCalled,
      "Should track a touch event for a bound react native view that is user interaction enabled"
    )
  }

  func testDispatchingTouchesToRnWithUserInteractionDisabled() {
    let touch = TestTouch()
    let hierarchy = ViewHierarchies.viewWithReactNativeAncestor(interactionEnabled: false)
    touch.stubbedView = hierarchy.leaf

    let binding = TestEventBinding()
    manager.reactBindings = [5: binding]

    manager.handleReactNativeTouches(
      withHandler: nil,
      command: nil,
      touches: Set([touch]),
      eventName: "touchEnd"
    )
    XCTAssertTrue(
      binding.trackEventWasCalled,
      "Will track a touch event for a bound react native view that is not user interaction enabled"
    )
  }

  func testDispatchingTouchesToRnWithoutRnViewInHierarchy() {
    let touch = TestTouch()
    let hierarchy = ViewHierarchies.viewWithReactNativeAncestor(interactionEnabled: false)
    touch.stubbedView = hierarchy.leaf

    let binding = TestEventBinding()
    manager.reactBindings = [3: binding]

    manager.handleReactNativeTouches(
      withHandler: nil,
      command: nil,
      touches: Set([touch]),
      eventName: "touchEnd"
    )
    XCTAssertFalse(
      binding.trackEventWasCalled,
      "Should not track a touch event for a non-bound bound react native view"
    )
  }

  func testSelectingTableViewCellMatchingIndexPath() {
    let (tableView, cell) = ViewHierarchies.nestedTableViewAndCell
    tableView.stub(cell: cell, forIndexPath: firstIndexPath)
    let binding = TestEventBinding(view: cell)

    manager.eventBindings = [binding]

    manager.handleDidSelectRow(
      with: [binding],
      target: nil,
      command: nil,
      tableView: tableView,
      indexPath: firstIndexPath
    )

    XCTAssertTrue(
      binding.trackEventWasCalled,
      "Should track an event when a cell can be retrieved using the given index path"
    )
  }

  func testSelectingMissingTableViewCell() {
    let (tableView, _) = ViewHierarchies.nestedTableViewAndCell
    let binding = TestEventBinding()

    manager.eventBindings = [binding]

    manager.handleDidSelectRow(
      with: [binding],
      target: nil,
      command: nil,
      tableView: tableView,
      indexPath: firstIndexPath
    )

    XCTAssertFalse(
      binding.trackEventWasCalled,
      "Should not track an event when a cell cannot be retrieved using the given index path"
    )
  }

  func testSelectingCollectionViewCellMatchingIndexPath() {
    let (collection, cell) = ViewHierarchies.nestedCollectionViewAndCell
    collection.stub(cell: cell, forIndexPath: firstIndexPath)
    let binding = TestEventBinding(view: cell)

    manager.eventBindings = [binding]

    manager.handleDidSelectItem(
      with: [binding],
      target: nil,
      command: nil,
      collectionView: collection,
      indexPath: firstIndexPath
    )

    XCTAssertTrue(
      binding.trackEventWasCalled,
      "Should track an event when an item can be retrieved using the given index path"
    )
  }

  func testSelectingMissingCollectionViewCell() {
    let (collection, _) = ViewHierarchies.nestedCollectionViewAndCell
    let binding = TestEventBinding()

    manager.eventBindings = [binding]

    manager.handleDidSelectItem(
      with: [binding],
      target: nil,
      command: nil,
      collectionView: collection,
      indexPath: firstIndexPath
    )

    XCTAssertFalse(
      binding.trackEventWasCalled,
      "Should not track an event when an item cannot be retrieved using the given index path"
    )
  }
}


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

import XCTest

extension EventBindingManagerTests {

  // MARK: - View Matching

  func testMatchingViewWithoutEventBindings() {
    let control = TestControl()
    let window = UIWindow()
    control.stubbedWindow = window
    manager.eventBindings = nil
    manager.match(control, delegate: self)

    XCTAssertNil(
      control.capturedAction,
      "Should not add an action to the control when there are no event bindings"
    )
  }

  func testMatchingViewWithoutWindow() {
    let control = TestControl()
    XCTAssertNotNil(manager.eventBindings, "There should be event bindings from setup")
    manager.match(control, delegate: self)

    XCTAssertNil(
      control.capturedAction,
      "Should not add an action to a control that is not in a window"
    )
  }

  func testMatchingControlWithWindow() {
    let control = TestControl()
    let window = UIWindow()
    control.stubbedWindow = window
    let binding = TestEventBinding(view: control)
    manager.eventBindings = [TestEventBinding(), binding]
    manager.match(control, delegate: self)

    XCTAssertNotNil(
      control.capturedAction,
      "Should add an action to a control that is in a window"
    )
  }

  func testMatchingReactNativeView() {
    let window = UIWindow()
    let view = TestReactNativeView()
    view.stubbedWindow = window

    let binding = TestEventBinding(view: view)
    manager = EventBindingManager(swizzler: TestSwizzler.self, eventLogger: eventLogger)
    manager.eventBindings = [TestEventBinding(), binding]
    manager.match(view, delegate: self)

    XCTAssertEqual(
      manager.reactBindings?[view.reactTag] as? TestEventBinding,
      binding,
      "Should store the binding for the react tag that corresponds to the view"
    )
  }

  func testMatchingTableView() {
    let (tableView, cell) = ViewHierarchies.nestedTableViewAndCell
    let binding = TestEventBinding(view: cell)

    manager.eventBindings = [binding]
    manager.match(tableView, delegate: self)

    XCTAssertTrue(
      TestSwizzler.evidence.contains {
        $0.selector == #selector(UITableViewDelegate.tableView(_:didSelectRowAt:))
      },
      "Should replace did select row at when matching a table view and a delegate"
    )
  }

  func testMatchingCollectionView() {
    let (collection, cell) = ViewHierarchies.nestedCollectionViewAndCell
    let binding = TestEventBinding(view: cell)

    manager.eventBindings = [binding]
    manager.match(collection, delegate: self)

    XCTAssertTrue(
      TestSwizzler.evidence.contains {
        $0.selector == #selector(UICollectionViewDelegate.collectionView(_:didSelectItemAt:))
      },
      "Should replace did select item at when matching a collection view and a 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.
 */

@testable import FBSDKCoreKit

import XCTest

@objc
protocol WindowMoving {
  func didMoveToWindow()
}

final class EventBindingManagerTests: XCTestCase, UITableViewDelegate, UICollectionViewDelegate {

  lazy var manager = EventBindingManager(
    json: SampleRawRemoteEventBindings.sampleDictionary,
    swizzler: TestSwizzler.self,
    eventLogger: eventLogger
  )

  var bindings = SampleEventBinding.validEventBindings
  let eventLogger = TestEventLogger()

  let expectedEvidenceWithoutReactNative = [
    SwizzleEvidence(selector: #selector(UIControl.didMoveToWindow), class: UIControl.self),
    SwizzleEvidence(selector: #selector(setter: UITableView.delegate), class: UITableView.self),
    SwizzleEvidence(selector: #selector(setter: UICollectionView.delegate), class: UICollectionView.self),
  ]

  override func setUp() {
    super.setUp()

    registerReactNativeClasses()
    TestSwizzler.reset()
  }

  // MARK: - Dependencies

  func testCreatingCustom() {
    manager = EventBindingManager(swizzler: TestSwizzler.self, eventLogger: eventLogger)
    XCTAssertTrue(
      manager.swizzler is TestSwizzler.Type,
      "Should be created with the provided swizzling type"
    )
    XCTAssertEqual(
      manager.eventLogger as? TestEventLogger,
      eventLogger,
      "Should be created with the provided event logger"
    )
  }

  func testCreatingCustomWithJson() {
    manager = EventBindingManager(
      json: ["some": "stuff"],
      swizzler: TestSwizzler.self,
      eventLogger: eventLogger
    )
    XCTAssertTrue(
      manager.swizzler is TestSwizzler.Type,
      "Should be created with the provided swizzling type"
    )
  }

  func testCreatingWithReactNativeUnavailable() {
    deregisterReactNativeClasses()
    manager = EventBindingManager(swizzler: TestSwizzler.self, eventLogger: eventLogger)
    XCTAssertFalse(
      manager.hasReactNative,
      "Should detect if react native is in the runtime"
    )
    manager.validClasses.forEach { item in
      switch item.base {
      case is UIControl.Type: break
      case is UICollectionView.Type: break
      case is UITableView.Type: break
      default: XCTFail("\(item.description) should not be considered valid")
      }
    }
  }

  func testCreatingWithReactNativeAvailable() {
    manager = EventBindingManager(swizzler: TestSwizzler.self, eventLogger: eventLogger)
    XCTAssertTrue(
      manager.hasReactNative,
      "Should detect if react native is in the runtime"
    )
    let classNames = Set(manager.validClasses.map { $0.description })
    let expected = Set([
      "RCTTextView",
      "RCTImageView",
      "UITableView",
      "UIControl",
      "UICollectionView",
      "RCTView",
    ])
    XCTAssertEqual(
      classNames,
      expected,
      "Should have a known set of valid classes"
    )
  }

  // MARK: - Starting

  func testStartingWithEventsWhenStarted() {
    manager.isStarted = true
    manager.start()

    XCTAssertTrue(TestSwizzler.evidence.isEmpty)
  }

  func testStartingWithEventsWhenNotStarted() {
    deregisterReactNativeClasses()
    manager = EventBindingManager(
      json: SampleRawRemoteEventBindings.sampleDictionary,
      swizzler: TestSwizzler.self,
      eventLogger: eventLogger
    )
    manager.isStarted = false
    manager.start()

    XCTAssertEqual(
      TestSwizzler.evidence,
      expectedEvidenceWithoutReactNative
    )
  }

  func testStartingWithoutEventsWhenStarted() {
    manager.eventBindings = []
    manager.isStarted = true
    manager.start()

    XCTAssertTrue(TestSwizzler.evidence.isEmpty)
  }

  func testStartingWithoutEventsWhenNotStarted() {
    manager.eventBindings = []
    manager.isStarted = false
    manager.start()

    XCTAssertTrue(TestSwizzler.evidence.isEmpty)
  }

  func testStartingWithReactNativeClasses() {
    manager = EventBindingManager(swizzler: TestSwizzler.self, eventLogger: eventLogger)

    // Updating bindings will actually call start if it is not in a started state
    manager.isStarted = true
    manager.updateBindings(bindings)

    manager.isStarted = false
    manager.start()

    // This is ugly but there is no good way to do this in Swift.
    let expected = "["
      .appending(
        [
          "FBSDKCoreKitTests.SwizzleEvidence(selector: didMoveToWindow, class: UIControl)",
          "FBSDKCoreKitTests.SwizzleEvidence(selector: didMoveToWindow, class: RCTView)",
          "FBSDKCoreKitTests.SwizzleEvidence(selector: didMoveToWindow, class: RCTTextView)",
          "FBSDKCoreKitTests.SwizzleEvidence(selector: didMoveToWindow, class: RCTImageView)",
          "FBSDKCoreKitTests.SwizzleEvidence(selector: _updateAndDispatchTouches:eventName:, class: RCTTouchHandler)",
          "FBSDKCoreKitTests.SwizzleEvidence(selector: setDelegate:, class: UITableView)",
          "FBSDKCoreKitTests.SwizzleEvidence(selector: setDelegate:, class: UICollectionView)",
        ]
          .joined(separator: ", ")
      )
      .appending("]")

    XCTAssertEqual(
      TestSwizzler.evidence.description,
      expected
    )
  }

  // MARK: - Updating Bindings

  func testUpdatingEventBindings() {
    manager = EventBindingManager(swizzler: TestSwizzler.self, eventLogger: eventLogger)
    manager.reactBindings = ["foo": SampleEventBinding.createValid(withName: "foo")]
    manager.updateBindings(bindings)

    XCTAssertEqual(
      manager.eventBindings,
      bindings,
      "Should persist updated event bindings"
    )
    XCTAssertEqual(
      manager.reactBindings?.count,
      0,
      "Should clear react bindings when updating bindings"
    )
  }

  func testUpdatingEventBindingsWithIdenticalBindings() {
    manager = EventBindingManager(swizzler: TestSwizzler.self, eventLogger: eventLogger)
    manager.updateBindings(bindings)
    manager.updateBindings(bindings)

    XCTAssertEqual(
      manager.eventBindings,
      bindings,
      "Should not add duplicate event bindings"
    )
  }

  func testUpdatingEventBindingsWithRecreatedBindings() {
    manager.updateBindings(bindings)

    guard let eventBindings = manager.eventBindings else {
      return XCTFail("There should be event bindings on the manager")
    }

    eventBindings.enumerated().forEach { pair in
      let (index, element) = pair
      XCTAssertTrue(
        element.isEqual(to: bindings[index]),
        "Bindings with the same information should be considered equal"
      )
    }
  }

  func testUpdatingEventBindingsWithDifferentBindingsDifferentNumberOfBindings() {
    bindings.append(SampleEventBinding.createValid(withName: "foo"))
    manager.updateBindings(bindings)

    XCTAssertEqual(
      manager.eventBindings,
      bindings,
      "Setting a different number of bindings from the number of stored bindings should overwrite the stored bindings"
    )
  }

  func testUpdatingEventBindingsWithDifferentBindingsSameNumberOfBindings() {
    let binding = SampleEventBinding.createValid(withName: "foo")
    let binding2 = SampleEventBinding.createValid(withName: "bar")
    let binding3 = SampleEventBinding.createValid(withName: "baz")

    manager.updateBindings([binding, binding2])
    manager.updateBindings([binding2, binding3])

    XCTAssertEqual(
      manager.eventBindings,
      [binding2, binding3],
      "Setting different bindings from the stored bindings should overwrite the stored bindings"
    )
  }

  func testUpdatingBindingsStarts() {
    manager.isStarted = false
    manager = EventBindingManager(swizzler: TestSwizzler.self, eventLogger: eventLogger)
    manager.updateBindings(bindings)

    XCTAssertTrue(
      manager.isStarted,
      "Updating bindings should start the manager if it is not started"
    )
  }

  // MARK: - Helpers

  func registerReactNativeClasses() {
    if objc_lookUpClass("RCTRootView") == nil,
       let touchHandler: AnyClass = objc_allocateClassPair(NSObject.self, "RCTTouchHandler", 0),
       let reactRootView: AnyClass = objc_allocateClassPair(NSObject.self, "RCTRootView", 0),
       let imageView: AnyClass = objc_allocateClassPair(NSObject.self, "RCTImageView", 0),
       let textView: AnyClass = objc_allocateClassPair(NSObject.self, "RCTTextView", 0),
       let view: AnyClass = objc_allocateClassPair(NSObject.self, "RCTView", 0) {
      objc_registerClassPair(touchHandler)
      objc_registerClassPair(reactRootView)
      objc_registerClassPair(imageView)
      objc_registerClassPair(textView)
      objc_registerClassPair(view)
    }
  }

  func deregisterReactNativeClasses() {
    if let touchHandler = objc_lookUpClass("RCTTouchHandler"),
       let rootViewClass = objc_lookUpClass("RCTRootView"),
       let imageView = objc_lookUpClass("RCTImageView"),
       let textView = objc_lookUpClass("RCTTextView"),
       let view = objc_lookUpClass("RCTView") {
      objc_disposeClassPair(touchHandler)
      objc_disposeClassPair(rootViewClass)
      objc_disposeClassPair(imageView)
      objc_disposeClassPair(textView)
      objc_disposeClassPair(view)
    }
  }

  enum ViewHierarchies {

    static func viewWithReactNativeAncestor(interactionEnabled: Bool) -> (root: UIView, leaf: UIView) {
      let reactView1 = TestReactNativeView()
      reactView1.isUserInteractionEnabled = false
      let reactView2 = TestReactNativeView()
      reactView2.isUserInteractionEnabled = interactionEnabled
      let view = UIView()
      reactView2.addSubview(reactView1)
      reactView1.addSubview(view)

      return (root: reactView2, leaf: view)
    }

    static var nestedTableViewAndCell: (tableView: TestTableView, cell: UITableViewCell) {
      let window = UIWindow()
      let tableView = TestTableView()
      tableView.stubbedWindow = window
      let view = TestView()
      view.stubbedWindow = window
      let cell = UITableViewCell()
      view.addSubview(tableView)
      tableView.addSubview(cell)

      return (tableView, cell)
    }

    static var nestedCollectionViewAndCell: (tableView: TestCollectionView, cell: UICollectionViewCell) {
      let window = UIWindow()
      let collectionView = TestCollectionView(
        frame: .zero,
        collectionViewLayout: UICollectionViewFlowLayout()
      )
      collectionView.stubbedWindow = window
      let view = TestView()
      view.stubbedWindow = window
      let cell = UICollectionViewCell()
      view.addSubview(collectionView)
      collectionView.addSubview(cell)

      return (collectionView, cell)
    }
  }
}