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

import TestTools
import XCTest

final class AEMAdvertiserMultiEntryRuleTests: XCTestCase {

  enum Keys {
    static let ruleOperator = "operator"
    static let rules = "rules"
  }

  func testIsMatchedEventParametersForAnd() {
    let rule = AEMAdvertiserMultiEntryRule(
      with: .and,
      rules: [SampleAEMSingleEntryRules.cardTypeRule1, SampleAEMSingleEntryRules.valueRule]
    )
    XCTAssertTrue(
      rule.isMatchedEventParameters(
        [
          "card_type": "platium",
          "amount": NSNumber(value: 100),
        ]
      ),
      "Should expect the parameter matched with the rule"
    )
    XCTAssertFalse(
      rule.isMatchedEventParameters(
        [
          "card_type": "platium",
          "amount": NSNumber(value: 1),
        ]
      ),
      "Should not expect the parameter matched with the rule if the amount is low"
    )
    XCTAssertFalse(
      rule.isMatchedEventParameters(
        [
          "card_type": "gold",
          "amount": NSNumber(value: 100),
        ]
      ),
      "Should not expect the parameter matched with the rule if the card type is wrong"
    )
  }

  func testIsMatchedEventParametersForOr() {
    let rule = AEMAdvertiserMultiEntryRule(
      with: .or,
      rules: [SampleAEMSingleEntryRules.cardTypeRule1, SampleAEMSingleEntryRules.valueRule]
    )
    XCTAssertFalse(
      rule.isMatchedEventParameters(
        [
          "card_type": "gold",
          "amount": NSNumber(value: 1),
        ]
      ),
      "Should not expect the parameter matched with the rule"
    )
    XCTAssertTrue(
      rule.isMatchedEventParameters(
        [
          "card_type": "platium",
          "amount": NSNumber(value: 1),
        ]
      ),
      "Should expect the parameter matched with the rule if the card type is the same"
    )
    XCTAssertTrue(
      rule.isMatchedEventParameters(
        [
          "card_type": "gold",
          "amount": NSNumber(value: 100),
        ]
      ),
      "Should expect the parameter matched with the rule if amount is high"
    )
  }

  func testIsMatchedEventParametersForNot() {
    let rule = AEMAdvertiserMultiEntryRule(
      with: .not,
      rules: [SampleAEMSingleEntryRules.cardTypeRule1, SampleAEMSingleEntryRules.valueRule]
    )
    XCTAssertTrue(
      rule.isMatchedEventParameters(
        [
          "card_type": "gold",
          "amount": NSNumber(value: 1),
        ]
      ),
      "Should expect the parameter matched with the rule"
    )
    XCTAssertFalse(
      rule.isMatchedEventParameters(
        [
          "card_type": "platium",
          "amount": NSNumber(value: 1),
        ]
      ),
      "Should not expect the parameter matched with the rule if the card type is the same"
    )
    XCTAssertFalse(
      rule.isMatchedEventParameters(
        [
          "card_type": "gold",
          "amount": NSNumber(value: 100),
        ]
      ),
      "Should not expect the parameter matched with the rule if amount is high"
    )
  }

  func testIsMatchedEventParametersForNestedRules() {
    let andRule = AEMAdvertiserMultiEntryRule(
      with: .and,
      rules: [SampleAEMSingleEntryRules.cardTypeRule2, SampleAEMSingleEntryRules.valueRule]
    )
    let orRule = AEMAdvertiserMultiEntryRule(
      with: .or,
      rules: [SampleAEMSingleEntryRules.contentNameRule, SampleAEMSingleEntryRules.contentCategoryRule]
    )
    let nestedRule = AEMAdvertiserMultiEntryRule(
      with: .and,
      rules: [andRule, orRule, SampleAEMSingleEntryRules.urlRule]
    )
    XCTAssertTrue(
      nestedRule.isMatchedEventParameters(
        [
          "URL": "thankyou.do.com",
          "content_category": "demand",
          "card_type": "blue_credit",
          "amount": NSNumber(value: 100),
        ]
      ),
      "Shoule expect the rule is matched"
    )
    XCTAssertFalse(
      nestedRule.isMatchedEventParameters(
        [
          "URL": "thankyou.com",
          "content_category": "demand",
          "card_type": "blue_credit",
          "amount": NSNumber(value: 100),
        ]
      ),
      "Shoule not expect the rule is matched with wrong URL"
    )
    XCTAssertFalse(
      nestedRule.isMatchedEventParameters(
        [
          "URL": "thankyou.do.com",
          "content_category": "required",
          "card_type": "blue_credit",
          "amount": NSNumber(value: 100),
        ]
      ),
      "Shoule not expect the rule is matched with wrong content_category"
    )
  }

  func testSecureCoding() {
    XCTAssertTrue(
      AEMAdvertiserMultiEntryRule.supportsSecureCoding,
      "AEM Advertiser Multi Entry Rule should support secure coding"
    )
  }

  func testEncodingAndDecoding() throws {
    let entryRule = SampleAEMData.validAdvertiserMultiEntryRule
    let decodedObject = try CodabilityTesting.encodeAndDecode(entryRule)

    // Test Objects
    XCTAssertNotIdentical(decodedObject, entryRule, .isCodable)
    XCTAssertNotEqual(decodedObject, entryRule, .isCodable) // isEqual Method hasn't been implemented

    // Test Properties
    XCTAssertEqual(decodedObject.operator.rawValue, entryRule.operator.rawValue)
    let rules = try XCTUnwrap(
      entryRule.rules as? [AEMAdvertiserSingleEntryRule]
    )
    let decodedRules = try XCTUnwrap(
      decodedObject.rules as? [AEMAdvertiserSingleEntryRule]
    )
    XCTAssertEqual(decodedRules, rules)
  }
}

// MARK: - Assumptions

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


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

@testable import FBAEMKit

import XCTest

final class AEMAdvertiserRuleFactoryTests: XCTestCase {

  let factory = AEMAdvertiserRuleFactory()

  func testCreateRuleWithJson() {
    XCTAssertNil(
      factory.createRule(json: nil),
      "Should not create valid Single Entry Rule with nil"
    )
    XCTAssertNil(
      factory.createRule(json: ""),
      "Should not create valid Single Entry Rule with empty string"
    )
    XCTAssertNil(
      factory.createRule(json: nil),
      "Should not create valid Single Entry Rule with nil"
    )
    XCTAssertNotNil(
      factory.createRule(json: #"{"and": [{"value": {"contains": "abc"}}]}"#),
      "Should create expected Multi Entry Rule"
    )
    XCTAssertNotNil(
      factory.createRule(json: #"{"value": {"contains": "abc"}}"#),
      "Should create expected Single Entry Rule"
    )
    XCTAssertNotNil(
      factory.createRule(json: #"{"and": [{"event": {"eq": "Lead"}}, {"or": [{"URL": {"contains": "achievetestprep.com"}}]}]}"#), // swiftlint:disable:this line_length
      "Should create expected nested Multi Entry Rule"
    )
  }

  func testCreateRuleWithDict() {
    XCTAssertNil(
      factory.createRule(dictionary: [:]),
      "Should not create valid Single Entry Rule with emtpy dictionary"
    )
    XCTAssertNil(
      factory.createRule(dictionary: ["value": ["contains": 10]]),
      "Should not create valid Single Entry Rule with invalid dictionary"
    )
    let multiEntryRule = factory.createRule(dictionary: ["and": [SampleAEMData.validAdvertiserSingleEntryRuleJson1]]) as? AEMAdvertiserMultiEntryRule // swiftlint:disable:this line_length
    XCTAssertNotNil(
      multiEntryRule,
      "Should create expected Multi Entry Rule"
    )
    let singleEntryRule = factory.createRule(dictionary: ["value": ["contains": "abc"]]) as? AEMAdvertiserSingleEntryRule // swiftlint:disable:this line_length
    XCTAssertNotNil(
      singleEntryRule,
      "Should create expected Single Entry Rule"
    )
  }

  func testCreateSingleEntryRuleWithValidDict() {
    var rule: AEMAdvertiserSingleEntryRule?
    rule = factory.createSingleEntryRule(from: SampleAEMData.validAdvertiserSingleEntryRuleJson1)
    XCTAssertTrue(
      SampleAEMData.advertiserSingleEntryRule1.isEqual(rule),
      "Should create the expected Single Entry Rule"
    )
    rule = factory.createSingleEntryRule(from: SampleAEMData.validAdvertiserSingleEntryRuleJson2)
    XCTAssertTrue(
      SampleAEMData.advertiserSingleEntryRule2.isEqual(rule),
      "Should create the expected Single Entry Rule"
    )
    rule = factory.createSingleEntryRule(from: SampleAEMData.validAdvertiserSingleEntryRuleJson3)
    XCTAssertTrue(
      SampleAEMData.advertiserSingleEntryRule3.isEqual(rule),
      "Should create the expected Single Entry Rule"
    )
  }

  func testCreateSingleEntryRuleWithInvalidDict() {
    XCTAssertNil(
      factory.createSingleEntryRule(from: [:]),
      "Should not create valid Single Entry Rule with empty dictionary"
    )
    XCTAssertNil(
      factory.createSingleEntryRule(from: ["contains": []]),
      "Should not create valid Single Entry Rule with invalid dictionary"
    )
    XCTAssertNil(
      factory.createSingleEntryRule(from: ["value": ["contains": 10]]),
      "Should not create valid Single Entry Rule with invalid dictionary"
    )
    XCTAssertNil(
      factory.createSingleEntryRule(from: ["value": ["lt": "abc"]]),
      "Should not create valid Single Entry Rule with invalid dictionary"
    )
  }

  func testCreateMultiEntryRuleWithValidDict() {
    zip(
      ["and", "or", "not"],
      [
        .and,
        .or,
        .not,
      ] as [AEMAdvertiserRuleOperator]
    ).forEach { opString, expectedOperator in
      let rule: AEMAdvertiserMultiEntryRule? = factory.createMultiEntryRule(
        from: [opString: [SampleAEMData.validAdvertiserSingleEntryRuleJson1, SampleAEMData.validAdvertiserSingleEntryRuleJson2]]) // swiftlint:disable:this line_length
      XCTAssertNotNil(
        rule,
        "Should create Multi Entry Rule with valid dictionary"
      )
      XCTAssertEqual(
        expectedOperator,
        rule?.operator,
        "Multi Entry Rule should have the expected operator"
      )
      XCTAssertEqual(
        2,
        rule?.rules.count,
        "Multi Entry Rule should have the expected number of subrules"
      )
    } // swiftlint:disable:this closure_end_indentation
  }

  func testCreateMultiEntryRuleWithInvalidDict() {
    XCTAssertNil(
      factory.createMultiEntryRule(from: [:]),
      "Should not create valid Multi Entry Rule with empty dictionary"
    )
    XCTAssertNil(
      factory.createMultiEntryRule(from: ["contains": [["content": ["starts_with": "abc"]]]]),
      "Should not create valid Multi Entry Rule with invlaid operator"
    )
    XCTAssertNil(
      factory.createMultiEntryRule(from: ["and": []]),
      "Should not create valid Multi Entry Rule with empty subrules"
    )
    XCTAssertNil(
      factory.createMultiEntryRule(from: ["and": ["value": ["contains": 10]]]),
      "Should not create valid Multi Entry Rule with invlaid subrule"
    )
  }

  func testGetPrimaryKey() {
    XCTAssertEqual(
      factory.primaryKey(for: ["test_key": "abc"]),
      "test_key",
      "Should get the expected key of the dictionary"
    )
    XCTAssertNil(
      factory.primaryKey(for: [:]),
      "Should not get the unexpected key while the dictionay is empty"
    )
  }

  func testGetOperator() {
    XCTAssertEqual(
      factory.getOperator(from: ["test_key": "abc"]),
      .unknown,
      "Should get the expected Unknown operator of the dictionary"
    )
    XCTAssertEqual(
      factory.getOperator(from: ["And": "abc"]),
      .and,
      "Should get the expected AND operator of the dictionary"
    )
    XCTAssertEqual(
      factory.getOperator(from: ["and": "abc"]),
      .and,
      "Should get the expected AND operator of the dictionary"
    )
    XCTAssertEqual(
      factory.getOperator(from: ["or": "abc"]),
      .or,
      "Should get the expected OR operator of the dictionary"
    )
    XCTAssertEqual(
      factory.getOperator(from: ["not": "abc"]),
      .not,
      "Should get the expected NOT operator of the dictionary"
    )
    XCTAssertEqual(
      factory.getOperator(from: ["contains": "abc"]),
      .contains,
      "Should get the expected Contains operator of the dictionary"
    )
    XCTAssertEqual(
      factory.getOperator(from: ["not_contains": "abc"]),
      .notContains,
      "Should get the expected NotContains operator of the dictionary"
    )
    XCTAssertEqual(
      factory.getOperator(from: ["starts_with": "abc"]),
      .startsWith,
      "Should get the expected StartsWith operator of the dictionary"
    )
    XCTAssertEqual(
      factory.getOperator(from: ["i_contains": "abc"]),
      .caseInsensitiveContains,
      "Should get the expected i_contains operator of the dictionary"
    )
    XCTAssertEqual(
      factory.getOperator(from: ["i_not_contains": "abc"]),
      .caseInsensitiveNotContains,
      "Should get the expected I_NotContains operator of the dictionary"
    )
    XCTAssertEqual(
      factory.getOperator(from: ["i_starts_with": "abc"]),
      .caseInsensitiveStartsWith,
      "Should get the expected I_StartsWith operator of the dictionary"
    )
    XCTAssertEqual(
      factory.getOperator(from: ["regex_match": "abc"]),
      .regexMatch,
      "Should get the expected REGEX_MATCH operator of the dictionary"
    )
    XCTAssertEqual(
      factory.getOperator(from: ["eq": "abc"]),
      .equal,
      "Should get the expected EQ operator of the dictionary"
    )
    XCTAssertEqual(
      factory.getOperator(from: ["neq": "abc"]),
      .notEqual,
      "Should get the expected NEQ operator of the dictionary"
    )
    XCTAssertEqual(
      factory.getOperator(from: ["lt": 10]),
      .lessThan,
      "Should get the expected LT operator of the dictionary"
    )
    XCTAssertEqual(
      factory.getOperator(from: ["lte": 10]),
      .lessThanOrEqual,
      "Should get the expected LTE operator of the dictionary"
    )
    XCTAssertEqual(
      factory.getOperator(from: ["gt": 10]),
      .greaterThan,
      "Should get the expected GT operator of the dictionary"
    )
    XCTAssertEqual(
      factory.getOperator(from: ["gte": 10]),
      .greaterThanOrEqual,
      "Should get the expected GTE operator of the dictionary"
    )
    XCTAssertEqual(
      factory.getOperator(from: ["i_is_any": ["abc"]]),
      .caseInsensitiveIsAny,
      "Should get the expected I_IsAny operator of the dictionary"
    )
    XCTAssertEqual(
      factory.getOperator(from: ["i_is_not_any": ["abc"]]),
      .caseInsensitiveIsNotAny,
      "Should get the expected I_IsNotAny operator of the dictionary"
    )
    XCTAssertEqual(
      factory.getOperator(from: ["is_any": ["abc"]]),
      .isAny,
      "Should get the expected IsAny operator of the dictionary"
    )
    XCTAssertEqual(
      factory.getOperator(from: ["is_not_any": ["abc"]]),
      .isNotAny,
      "Should get the expected IsNotAny operator of the dictionary"
    )
  }

  func testIsOperatorForMultiEntryRule() {
    for ruleOperator in [
      .and,
      .or,
      .not,
    ] as [AEMAdvertiserRuleOperator] {
      XCTAssertTrue(
        factory.isOperatorForMultiEntryRule(ruleOperator),
        "Should expect the operator for multi entry rule"
      )
    }
    for ruleOperator in [
      .contains,
      .notContains,
      .startsWith,
      .caseInsensitiveContains,
      .caseInsensitiveNotContains,
      .caseInsensitiveStartsWith,
      .regexMatch,
      .equal,
      .notEqual,
      .lessThan,
      .lessThanOrEqual,
      .greaterThan,
      .greaterThanOrEqual,
      .caseInsensitiveIsAny,
      .caseInsensitiveIsNotAny,
      .isAny,
      .isNotAny,
    ] as [AEMAdvertiserRuleOperator] {
      XCTAssertFalse(
        factory.isOperatorForMultiEntryRule(ruleOperator),
        "Should expect the operator not for multi entry rule"
      )
    }
  }
}


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

import TestTools
import XCTest

final class AEMAdvertiserSingleEntryRuleTests: XCTestCase {

  enum Keys {
    static let ruleOperator = "operator"
    static let ruleParamKey = "param_key"
    static let ruleStringValue = "string_value"
    static let ruleNumberValue = "number_value"
    static let ruleArrayValue = "array_value"
  }

  func testIsMatchedWithEventParameters() {
    var rule = AEMAdvertiserSingleEntryRule(
      operator: .contains,
      paramKey: "fb_content.title",
      linguisticCondition: "hello",
      numericalCondition: nil,
      arrayCondition: nil
    )
    XCTAssertTrue(
      rule.isMatchedEventParameters(["fb_content": ["title": "helloworld"]]),
      "Should expect the event parameter matched with the rule"
    )
    XCTAssertTrue(
      rule.isMatchedEventParameters(["fb_content": ["title": "HelloWorld"]]),
      "Should expect the event parameter matched with the rule"
    )
    XCTAssertFalse(
      rule.isMatchedEventParameters(["fb_content": ["tt": "helloworld"]]),
      "Should not expect the event parameter matched with the rule"
    )
    XCTAssertFalse(
      rule.isMatchedEventParameters(["fb_content": ["title": 100]]),
      "Should not expect the event parameter matched with the rule"
    )
    XCTAssertFalse(
      rule.isMatchedEventParameters(["quantitly": ["title": "helloworld"]]),
      "Should not expect the event parameter matched with the rule"
    )

    rule.operator = .notEqual
    XCTAssertTrue(
      rule.isMatchedEventParameters(["fb_content": ["title": "helloworld"]]),
      "Should expect the event parameter matched with the rule"
    )
    XCTAssertFalse(
      rule.isMatchedEventParameters(["fb_content": ["tt": "helloworld"]]),
      "Should not expect the event parameter matched with the rule"
    )

    rule = AEMAdvertiserSingleEntryRule(
      operator: .greaterThan,
      paramKey: "fb_content.product1.quantity",
      linguisticCondition: nil,
      numericalCondition: 10,
      arrayCondition: nil
    )
    XCTAssertTrue(
      rule.isMatchedEventParameters(["fb_content": ["product1": ["quantity": 100.0]]]),
      "Should expect the event parameter matched with the rule"
    )
    XCTAssertFalse(
      rule.isMatchedEventParameters(["fb_content": ["product1": ["quantity": 1]]]),
      "Should expect the event parameter matched with the rule"
    )
  }

  func testIsMatchedWithEventParametersForAsteriskOperator() {
    let rule = AEMAdvertiserSingleEntryRule(
      operator: .contains,
      paramKey: "fb_content[*].id",
      linguisticCondition: "coffee",
      numericalCondition: nil,
      arrayCondition: nil
    )

    XCTAssertTrue(
      rule.isMatchedEventParameters(["fb_content": [["id": "shop"], ["id": "coffeeshop"]]]),
      "Should expect the event parameter matched with the rule"
    )
    XCTAssertFalse(
      rule.isMatchedEventParameters(["fb_content": ["id": "coffeeshop"]]),
      "Should not expect the event parameter matched with the rule without expected item"
    )
    XCTAssertFalse(
      rule.isMatchedEventParameters(["fb_content": [["id": "shop"]]]),
      "Should not expect the event parameter matched with the rule without expected id"
    )
  }

  func testIsMatchedWithEventParametersAndAsterisk() {
    let rule = AEMAdvertiserSingleEntryRule(
      operator: .contains,
      paramKey: "fb_content[*].title",
      linguisticCondition: "hello",
      numericalCondition: nil,
      arrayCondition: nil
    )
    XCTAssertTrue(
      rule.isMatched(
        withAsteriskParam: "fb_content[*]",
        eventParameters: ["fb_content": [["title": "hello"], ["title", "world"]]],
        paramPath: ["fb_content[*]", "title"]
      ),
      "Should expect the event parameter matched with the rule"
    )
    XCTAssertFalse(
      rule.isMatched(
        withAsteriskParam: "fb_content[*]",
        eventParameters: ["fb_content": [["title": "aaaa"], ["title", "world"]]],
        paramPath: ["fb_content[*]", "title"]
      ),
      "Should not expect the event parameter matched with the rule"
    )
  }

  func testIsMatchedWithAsteriskParam() {
    let rule = AEMAdvertiserSingleEntryRule(
      operator: .contains,
      paramKey: "fb_content[*].title",
      linguisticCondition: "hello",
      numericalCondition: nil,
      arrayCondition: nil
    )

    XCTAssertTrue(
      rule.isMatched(
        withAsteriskParam: "fb_content[*]",
        eventParameters: ["fb_content": [["title": "hello"], ["title", "world"]]],
        paramPath: ["fb_content[*]", "title"]
      ),
      "Should expect the event parameter matched with the rule"
    )
    XCTAssertFalse(
      rule.isMatched(
        withAsteriskParam: "fb_content[*]",
        eventParameters: ["fb_content": [["title": "aaaa"], ["title", "world"]]],
        paramPath: ["fb_content[*]", "title"]
      ),
      "Should not expect the event parameter matched with the rule"
    )
    XCTAssertFalse(
      rule.isMatched(
        withAsteriskParam: "fb_content[*]",
        eventParameters: ["fb_content_aaa": [["title": "aaaa"], ["title", "world"]]],
        paramPath: ["fb_content[*]", "title"]
      ),
      "Should not expect the event parameter matched with the rule"
    )
    XCTAssertFalse(
      rule.isMatched(
        withAsteriskParam: "fb_content[*]",
        eventParameters: ["fb_content_aaa": ["title": "aaaa"]],
        paramPath: ["fb_content[*]", "title"]
      ),
      "Should not expect the event parameter matched with the rule"
    )
  }

  func testIsMatchedWithStringComparision() {
    let rule = AEMAdvertiserSingleEntryRule(
      operator: .contains,
      paramKey: "fb_content.title",
      linguisticCondition: "hello",
      numericalCondition: nil,
      arrayCondition: nil
    )
    XCTAssertTrue(
      rule.isMatched(withStringValue: "worldhelloworld", numericalValue: nil),
      "Shoule expect parameter matched with the value"
    )
    XCTAssertFalse(
      rule.isMatched(withStringValue: "worldhellworld", numericalValue: nil),
      "Shoule not expect parameter matched with the value"
    )

    rule.operator = .notContains
    XCTAssertFalse(
      rule.isMatched(withStringValue: "worldhelloworld", numericalValue: nil),
      "Shoule not expect parameter matched with the value"
    )
    XCTAssertFalse(
      rule.isMatched(withStringValue: "WorldHelloWorld", numericalValue: nil),
      "Shoule not expect parameter matched with the value"
    )
    XCTAssertTrue(
      rule.isMatched(withStringValue: "worldhellworld", numericalValue: nil),
      "Shoule expect parameter matched with the value"
    )

    rule.operator = .startsWith
    XCTAssertTrue(
      rule.isMatched(withStringValue: "helloworld", numericalValue: nil),
      "Shoule expect parameter matched with the value"
    )
    XCTAssertTrue(
      rule.isMatched(withStringValue: "HelloWorld", numericalValue: nil),
      "Shoule expect parameter matched with the value"
    )
    XCTAssertFalse(
      rule.isMatched(withStringValue: "worldhelloworld", numericalValue: nil),
      "Shoule not expect parameter matched with the value"
    )

    rule.operator = .caseInsensitiveContains
    XCTAssertTrue(
      rule.isMatched(withStringValue: "worldHELLOworld", numericalValue: nil),
      "Shoule expect parameter matched with the value"
    )
    XCTAssertFalse(
      rule.isMatched(withStringValue: "worldhellworld", numericalValue: nil),
      "Shoule not expect parameter matched with the value"
    )

    rule.operator = .caseInsensitiveNotContains
    XCTAssertFalse(
      rule.isMatched(withStringValue: "worldHELLOworld", numericalValue: nil),
      "Shoule not expect parameter matched with the value"
    )
    XCTAssertTrue(
      rule.isMatched(withStringValue: "worldHELLworld", numericalValue: nil),
      "Shoule expect parameter matched with the value"
    )

    rule.operator = .caseInsensitiveStartsWith
    XCTAssertTrue(
      rule.isMatched(withStringValue: "HELLOworld", numericalValue: nil),
      "Shoule expect parameter matched with the value"
    )
    XCTAssertFalse(
      rule.isMatched(withStringValue: "worldHELLOworld", numericalValue: nil),
      "Shoule not expect parameter matched with the value"
    )

    rule.operator = .equal
    XCTAssertTrue(
      rule.isMatched(withStringValue: "hello", numericalValue: nil),
      "Shoule expect parameter matched with the value"
    )
    XCTAssertTrue(
      rule.isMatched(withStringValue: "Hello", numericalValue: nil),
      "Shoule expect parameter matched with the value"
    )
    XCTAssertFalse(
      rule.isMatched(withStringValue: "hellw", numericalValue: nil),
      "Shoule not expect parameter matched with the value"
    )

    rule.operator = .notEqual
    XCTAssertFalse(
      rule.isMatched(withStringValue: "hello", numericalValue: nil),
      "Shoule not expect parameter matched with the value"
    )
    XCTAssertFalse(
      rule.isMatched(withStringValue: "Hello", numericalValue: nil),
      "Shoule not expect parameter matched with the value"
    )
    XCTAssertTrue(
      rule.isMatched(withStringValue: "hellw", numericalValue: nil),
      "Shoule expect parameter matched with the value"
    )
  }

  func testIsMatchedWithNumberComparision() {
    let rule = AEMAdvertiserSingleEntryRule(
      operator: .lessThan,
      paramKey: "fb_content.title",
      linguisticCondition: nil,
      numericalCondition: 100,
      arrayCondition: nil
    )
    XCTAssertTrue(
      rule.isMatched(withStringValue: nil, numericalValue: 90),
      "Shoule expect parameter matched with value"
    )
    XCTAssertFalse(
      rule.isMatched(withStringValue: nil, numericalValue: 100),
      "Shoule not expect parameter matched with value"
    )
    XCTAssertFalse(
      rule.isMatched(withStringValue: nil, numericalValue: 101),
      "Shoule not expect parameter matched with value"
    )

    rule.operator = .lessThanOrEqual
    XCTAssertTrue(
      rule.isMatched(withStringValue: nil, numericalValue: 99),
      "Shoule expect parameter matched with value"
    )
    XCTAssertTrue(
      rule.isMatched(withStringValue: nil, numericalValue: 100),
      "Shoule expect parameter matched with value"
    )
    XCTAssertFalse(
      rule.isMatched(withStringValue: nil, numericalValue: 100.1),
      "Shoule not expect parameter matched with value"
    )

    rule.operator = .greaterThan
    XCTAssertTrue(
      rule.isMatched(withStringValue: nil, numericalValue: 101.5),
      "Shoule expect parameter matched with value"
    )
    XCTAssertFalse(
      rule.isMatched(withStringValue: nil, numericalValue: 100),
      "Shoule not expect parameter matched with value"
    )
    XCTAssertFalse(
      rule.isMatched(withStringValue: nil, numericalValue: 99),
      "Shoule not expect parameter matched with value"
    )

    rule.operator = .greaterThanOrEqual
    XCTAssertTrue(
      rule.isMatched(withStringValue: nil, numericalValue: 101.5),
      "Shoule expect parameter matched with value"
    )
    XCTAssertTrue(
      rule.isMatched(withStringValue: nil, numericalValue: 100),
      "Shoule expect parameter matched with value"
    )
    XCTAssertFalse(
      rule.isMatched(withStringValue: nil, numericalValue: 99),
      "Shoule not expect parameter matched with value"
    )
  }

  func testIsMatchedWithArrayComparision() {
    let rule = AEMAdvertiserSingleEntryRule(
      operator: .isAny,
      paramKey: "fb_content.title",
      linguisticCondition: nil,
      numericalCondition: nil,
      arrayCondition: ["Abc", "aaa", "ABC", "XXXX"]
    )
    XCTAssertTrue(
      rule.isMatched(withStringValue: "aaa", numericalValue: nil),
      "Shoule expect parameter matched with item in the array"
    )
    XCTAssertFalse(
      rule.isMatched(withStringValue: "bbb", numericalValue: nil),
      "Shoule not expect parameter matched with item in the array"
    )

    rule.operator = .caseInsensitiveIsAny
    XCTAssertTrue(
      rule.isMatched(withStringValue: "abc", numericalValue: nil),
      "Shoule expect parameter matched with item in the array"
    )

    rule.operator = .isNotAny
    XCTAssertTrue(
      rule.isMatched(withStringValue: "xxxx", numericalValue: nil),
      "Shoule expect parameter matched with item in the array"
    )

    rule.operator = .caseInsensitiveIsNotAny
    XCTAssertTrue(
      rule.isMatched(withStringValue: "ab", numericalValue: nil),
      "Shoule expect parameter matched with item in the array"
    )
  }

  func testIsRegexMatch() {
    let rule = AEMAdvertiserSingleEntryRule(
      operator: .isAny,
      paramKey: "fb_content.title",
      linguisticCondition: "eylea.us/support/?$|eylea.us/support/?",
      numericalCondition: nil,
      arrayCondition: nil
    )
    XCTAssertTrue(
      rule.isRegexMatch("eylea.us/support"),
      "Should expect parameter matched with regex"
    )
    XCTAssertFalse(
      rule.isRegexMatch("eylea.us.support"),
      "Should not expect parameter matched with regex"
    )
  }

  func testIsRegexMatchWithEmtpyString() {
    let rule = AEMAdvertiserSingleEntryRule(
      operator: .isAny,
      paramKey: "fb_content.title",
      linguisticCondition: "",
      numericalCondition: nil,
      arrayCondition: nil
    )
    XCTAssertFalse(
      rule.isRegexMatch("eylea.us.support"),
      "Should not expect parameter matched with regex"
    )
  }

  func testIsRegexMatchWithNullableString() {
    let rule = AEMAdvertiserSingleEntryRule(
      operator: .isAny,
      paramKey: "fb_content.title",
      linguisticCondition: nil,
      numericalCondition: nil,
      arrayCondition: nil
    )
    XCTAssertFalse(
      rule.isRegexMatch("eylea.us.support"),
      "Should not expect parameter matched with regex"
    )
  }

  func testIsAnyOf() {
    let rule = AEMAdvertiserSingleEntryRule(
      operator: .isAny,
      paramKey: "fb_content.title",
      linguisticCondition: nil,
      numericalCondition: nil,
      arrayCondition: ["abc", "AAA", "Abc"]
    )
    XCTAssertTrue(
      rule.isAny(of: ["abc", "AAA", "Abc"], stringValue: "AAA", ignoreCase: false),
      "Should expect parameter matched"
    )
    XCTAssertTrue(
      rule.isAny(of: ["abc", "AAA", "Abc"], stringValue: "aaa", ignoreCase: true),
      "Should expect parameter matched"
    )
    XCTAssertTrue(
      rule.isAny(of: ["abc", "AAA", "Abc"], stringValue: "ABC", ignoreCase: true),
      "Should expect parameter matched"
    )
    XCTAssertFalse(
      rule.isAny(of: ["abc", "AAA", "Abc"], stringValue: "aaa", ignoreCase: false),
      "Should not expect parameter matched"
    )
    XCTAssertFalse(
      rule.isAny(of: ["abc", "AAA", "Abc"], stringValue: "ab", ignoreCase: false),
      "Should not expect parameter matched"
    )
  }

  func testSecureCoding() {
    XCTAssertTrue(
      AEMAdvertiserSingleEntryRule.supportsSecureCoding,
      "AEM Advertiser Single Entry Rule should support secure coding"
    )
  }

  func testEncodingAndDecoding() throws {
    let entryRule = SampleAEMData.validAdvertiserSingleEntryRule
    let decodedObject = try CodabilityTesting.encodeAndDecode(entryRule)

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

    // Test Properties
    XCTAssertEqual(entryRule.operator.rawValue, decodedObject.operator.rawValue, .isCodable)
    XCTAssertEqual(entryRule.paramKey, decodedObject.paramKey, .isCodable)
    XCTAssertEqual(
      entryRule.linguisticCondition,
      decodedObject.linguisticCondition,
      .isCodable
    )
    XCTAssertEqual(
      entryRule.numericalCondition,
      decodedObject.numericalCondition,
      .isCodable
    )
    XCTAssertEqual(entryRule.arrayCondition, decodedObject.arrayCondition, .isCodable)
  }
}

// MARK: - Assumptions

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


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

@testable import FBAEMKit

import TestTools
import XCTest

final class AEMConfigurationTests: XCTestCase {

  enum Keys {
    static let defaultCurrency = "default_currency"
    static let cutoffTime = "cutoff_time"
    static let validFrom = "valid_from"
    static let mode = "config_mode"
    static let advertiserID = "advertiser_id"
    static let businessID = "business_id"
    static let paramRule = "param_rule"
    static let conversionValueRules = "conversion_value_rules"
    static let conversionValue = "conversion_value"
    static let priority = "priority"
    static let events = "events"
    static let eventName = "event_name"
    static let values = "values"
    static let currency = "currency"
    static let amount = "amount"
  }

  enum Values {
    static let coffeeBrand = "coffeebrand"
    static let paramRule = #"{"and": [{"fb_content[*].brand": {"eq": "CoffeeShop"}}]}"#
    static let purchase = "fb_mobile_purchase"
    static let donate = "Donate"
    static let defaultMode = "default"
    static let USD = "USD"
    static let JPY = "JPY"
  }

  var sampleData: [String: Any] = [
    Keys.defaultCurrency: Values.USD,
    Keys.cutoffTime: 1,
    Keys.validFrom: 10000,
    Keys.mode: Values.defaultMode,
    Keys.advertiserID: Values.coffeeBrand,
    Keys.paramRule: Values.paramRule,
    Keys.conversionValueRules: [
      [
        Keys.conversionValue: 2,
        Keys.priority: 10,
        Keys.events: [
          [
            Keys.eventName: Values.purchase,
          ],
          [
            Keys.eventName: Values.donate,
          ],
        ],
      ],
    ],
  ]

  var rulesData = [
    [
      Keys.conversionValue: 9,
      Keys.priority: 10,
      Keys.events: [
        [
          Keys.eventName: Values.purchase,
          Keys.values: [
            [
              Keys.currency: Values.USD,
              Keys.amount: 100.0,
            ],
            [
              Keys.currency: Values.JPY,
              Keys.amount: 1000.0,
            ],
          ],
        ],
        [
          Keys.eventName: Values.donate,
        ],
      ],
    ],
    [
      Keys.conversionValue: 15,
      Keys.priority: 7,
      Keys.events: [
        [
          Keys.eventName: Values.donate,
        ],
      ],
    ],
    [
      Keys.conversionValue: 20,
      Keys.priority: 15,
      Keys.events: [
        [
          Keys.eventName: Values.purchase,
        ],
      ],
    ],
  ]

  let advertiserRuleFactory = AEMAdvertiserRuleFactory()

  override func setUp() {
    super.setUp()

    AEMConfiguration.configure(withRuleProvider: advertiserRuleFactory)
  }

  func testConfiguration() {
    XCTAssertIdentical(
      AEMConfiguration.ruleProvider as AnyObject,
      advertiserRuleFactory,
      "Should configure the AEMConfiguration correctly"
    )
  }

  func testValidCases() {
    let configuration = AEMConfiguration(json: sampleData)

    XCTAssertEqual(
      configuration?.defaultCurrency,
      Values.USD,
      "Should parse the expected default_currency with the correct value"
    )
    XCTAssertEqual(
      configuration?.cutoffTime,
      1,
      "Should parse the expected cutoff_time with the correct value"
    )
    XCTAssertEqual(
      configuration?.validFrom,
      10000,
      "Should parse the expected valid_from with the correct value"
    )
    XCTAssertEqual(
      configuration?.mode,
      Values.defaultMode,
      "Should parse the expected config_mode with the correct value"
    )
    XCTAssertEqual(
      configuration?.businessID,
      Values.coffeeBrand,
      "Should parse the expected business_id with the correct value"
    )
    XCTAssertEqual(
      configuration?.conversionValueRules.count,
      1,
      "Should parse the expected conversion_value_rules with the correct value"
    )
  }

  func testInvalidCases() {
    var invalidData: [String: Any] = [:]
    XCTAssertNil(AEMConfiguration(json: invalidData))
    invalidData = [
      Keys.defaultCurrency: 100,
      Keys.cutoffTime: 1,
      Keys.validFrom: 10000,
      Keys.mode: Values.defaultMode,
      Keys.conversionValueRules: [
        [
          Keys.conversionValue: 2,
          Keys.priority: 10,
          Keys.events: [
            [
              Keys.eventName: Values.purchase,
            ],
            [
              Keys.eventName: Values.donate,
            ],
          ],
        ],
      ],
    ]
    XCTAssertNil(
      AEMConfiguration(json: invalidData),
      "Should not consider the configuration json valid with unexpected type for default_currency"
    )
    invalidData = [
      Keys.defaultCurrency: Values.USD,
      Keys.cutoffTime: 1,
      Keys.validFrom: 10000,
      Keys.mode: Values.defaultMode,
    ]
    XCTAssertNil(
      AEMConfiguration(json: invalidData),
      "Should not consider the configuration json valid without any conversion value rules"
    )
    invalidData = [
      Keys.defaultCurrency: Values.USD,
      Keys.cutoffTime: 1,
      Keys.validFrom: 10000,
      Keys.mode: Values.defaultMode,
      Keys.conversionValueRules: [
        [
          Keys.conversionValue: "2",
          Keys.priority: 10,
          Keys.events: [
            [
              Keys.eventName: Values.purchase,
            ],
            [
              Keys.eventName: Values.donate,
            ],
          ],
        ],
      ],
    ]
    XCTAssertNil(
      AEMConfiguration(json: invalidData),
      "Should not consider the configuration json valid with invalid conversion value rule"
    )
  }

  func testGetEventSet() {
    guard let parsedRules: [AEMRule] = AEMConfiguration.parseRules(rulesData)
    else { return XCTFail("Unwrapping Error") }
    let eventSet = AEMConfiguration.getEventSet(from: parsedRules)
    XCTAssertEqual(eventSet, [Values.purchase, Values.donate], "Should get the expected event set")
  }

  func testGetCurrencySet() {
    guard let parsedRules: [AEMRule] = AEMConfiguration.parseRules(rulesData)
    else { return XCTFail("Unwrapping Error") }
    let eventSet = AEMConfiguration.getCurrencySet(from: parsedRules)
    XCTAssertEqual(eventSet, [Values.USD, Values.JPY], "Should get the expected event set")
  }

  func testParseRules() {
    let parsedRules: [AEMRule]? = AEMConfiguration.parseRules(rulesData)
    XCTAssertEqual(
      parsedRules?[0].priority, 15, "Shoule parse the rules in descending priority order"
    )
    XCTAssertEqual(
      parsedRules?[1].priority, 10, "Shoule parse the rules in descending priority order"
    )
    XCTAssertEqual(
      parsedRules?[2].priority, 7, "Shoule parse the rules in descending priority order"
    )
  }

  func testParsing() {
    (1 ... 100).forEach { _ in
      if let data = (Fuzzer.randomize(json: self.sampleData) as? [String: Any]) {
        _ = AEMConfiguration(json: data)
      }
    }
  }

  func testIsSameBusinessID() {
    let configWithBusinessID = SampleAEMConfigurations.createConfigurationWithBusinessID()
    let configWithoutBusinessID = SampleAEMConfigurations.createConfigurationWithoutBusinessID()

    XCTAssertTrue(
      configWithBusinessID.isSameBusinessID("test_advertiserid_123") == true,
      "Should return true for the same business ID"
    )
    XCTAssertFalse(
      configWithBusinessID.isSameBusinessID("test_advertiserid_6666") == true,
      "Should return false for the unexpected business ID"
    )
    XCTAssertFalse(
      configWithBusinessID.isSameBusinessID(nil) == true,
      "Should return false for nil business ID if the configuration has business ID"
    )

    XCTAssertTrue(
      configWithoutBusinessID.isSameBusinessID(nil) == true,
      "Should return true for nil business ID if the configuration doesn't have business ID"
    )
    XCTAssertFalse(
      configWithoutBusinessID.isSameBusinessID("test_advertiserid_123") == true,
      "Should return false for non-nil business ID if the configuration has business ID"
    )
  }

  func testIsSameValidFromAndBusinessID() {
    let configWithBusinessID = SampleAEMConfigurations.createConfigurationWithBusinessID()
    let configWithoutBusinessID = SampleAEMConfigurations.createConfigurationWithoutBusinessID()

    XCTAssertTrue(
      configWithBusinessID.isSame(validFrom: 10000, businessID: "test_advertiserid_123") == true,
      "Should return true for the same validFrom and business ID"
    )
    XCTAssertFalse(
      configWithBusinessID.isSame(validFrom: 10000, businessID: "test_advertiserid_6666") == true,
      "Should return false for the unexpected validFrom and business ID"
    )
    XCTAssertFalse(
      configWithBusinessID.isSame(validFrom: 10001, businessID: "test_advertiserid_123") == true,
      "Should return false for the unexpected validFrom and business ID"
    )
    XCTAssertFalse(
      configWithBusinessID.isSame(validFrom: 10000, businessID: nil) == true,
      "Should return false for the unexpected validFrom and business ID"
    )

    XCTAssertTrue(
      configWithoutBusinessID.isSame(validFrom: 10000, businessID: nil) == true,
      "Should return true for nil business ID if the configuration doesn't have business ID"
    )
    XCTAssertFalse(
      configWithoutBusinessID.isSame(validFrom: 10000, businessID: "test_advertiserid_123") == true,
      "Should return false for the unexpected validFrom and business ID"
    )
    XCTAssertFalse(
      configWithoutBusinessID.isSame(validFrom: 10001, businessID: nil) == true,
      "Should return false for the unexpected validFrom and business ID"
    )
  }

  func testSecureCoding() {
    XCTAssertTrue(
      AEMConfiguration.supportsSecureCoding,
      "AEM Configuration should support secure coding"
    )
  }

  func testEncodingAndDecoding() throws {
    // swiftlint:disable:next force_unwrapping
    let configuration = AEMConfiguration(json: sampleData)!
    let decodedObject = try CodabilityTesting.encodeAndDecode(configuration)

    // Test Object
    XCTAssertNotIdentical(configuration, decodedObject)
    XCTAssertNotEqual(configuration, decodedObject) // isEqual method not added yet

    // Test Properties
    XCTAssertEqual(configuration.defaultCurrency, decodedObject.defaultCurrency, .isCodable)
    XCTAssertEqual(configuration.cutoffTime, decodedObject.cutoffTime, .isCodable)
    XCTAssertEqual(configuration.validFrom, decodedObject.validFrom, .isCodable)
    XCTAssertEqual(configuration.mode, decodedObject.mode, .isCodable)
    XCTAssertEqual(configuration.businessID, decodedObject.businessID, .isCodable)
    XCTAssertEqual(
      configuration.conversionValueRules,
      decodedObject.conversionValueRules,
      .isCodable
    )
  }
}

// MARK: - Assumptions

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


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

@testable import FBAEMKit

import TestTools
import XCTest

final class AEMEventTests: XCTestCase {

  enum Keys {
    static let eventName = "event_name"
    static let values = "values"
    static let currency = "currency"
    static let amount = "amount"
  }

  enum Values {
    static let purchase = "fb_mobile_purchase"
    static let subscribe = "Subscribe"
    static let usd = "usd"
    static let jpy = "jpy"
    static let USD = "USD"
    static let JPY = "JPY"
  }

  var sampleData: [String: Any] = [
    Keys.eventName: Values.purchase,
    Keys.values: [
      [
        Keys.currency: Values.usd,
        Keys.amount: 100,
      ],
      [
        Keys.currency: Values.JPY,
        Keys.amount: 1000,
      ],
    ],
  ]
  var validEventWithValues: AEMEvent? = AEMEvent(dict: [
    Keys.eventName: Values.purchase,
    Keys.values: [
      [
        Keys.currency: Values.usd,
        Keys.amount: 100.0,
      ],
      [
        Keys.currency: Values.JPY,
        Keys.amount: 1000.0,
      ],
    ],
  ])

  var validEventWithoutValues: AEMEvent? = AEMEvent(dict: [
    Keys.eventName: Values.purchase,
  ])

  func testValidCases() {
    var event = validEventWithoutValues
    XCTAssertEqual(
      event?.eventName,
      Values.purchase,
      "AEM event name should match the expected event_name in the json"
    )
    XCTAssertNil(
      event?.values,
      "AEM event should not have unexpected values"
    )
    event = validEventWithValues
    XCTAssertEqual(
      event?.eventName,
      Values.purchase,
      "AEM event name should match the expected event_name in the json"
    )
    let expectedValues: [String: Double] = [
      Values.USD: 100,
      Values.JPY: 1000,
    ]
    XCTAssertEqual(
      event?.values,
      expectedValues,
      "AEM event should have the expected values in the json"
    )
  }

  func testInvalidCases() {
    var invalidData: [String: Any] = [:]
    XCTAssertNil(AEMEvent(dict: invalidData))
    invalidData = [
      Keys.values: [
        [
          Keys.currency: Values.usd,
          Keys.amount: 100,
        ],
        [
          Keys.currency: Values.JPY,
          Keys.amount: 1000,
        ],
      ],
    ]
    XCTAssertNil(AEMEvent(dict: invalidData))
    invalidData = [
      Keys.eventName: Values.purchase,
      Keys.values: [
        [
          Keys.currency: 100,
          Keys.amount: Values.usd,
        ],
        [
          Keys.currency: 1000,
          Keys.amount: Values.jpy,
        ],
      ],
    ]
    XCTAssertNil(AEMEvent(dict: invalidData))
    invalidData = [
      Keys.eventName: [Values.purchase, Values.subscribe],
      Keys.values: [
        [
          Keys.currency: 100,
          Keys.amount: Values.usd,
        ],
        [
          Keys.currency: 1000,
          Keys.amount: Values.jpy,
        ],
      ],
    ]
    XCTAssertNil(AEMEvent(dict: invalidData))
  }

  func testParsing() {
    (1 ... 100).forEach { _ in
      if let data = (Fuzzer.randomize(json: self.sampleData) as? [String: Any]) {
        _ = AEMEvent(dict: data)
      }
    }
  }

  func testSecureCoding() {
    XCTAssertTrue(
      AEMEvent.supportsSecureCoding,
      "AEM Events should support secure coding"
    )
  }

  func testEncodingAndDecodingWithValues() throws {
    let event = validEventWithValues
    // swiftlint:disable:next force_unwrapping
    let decodedObject = try CodabilityTesting.encodeAndDecode(event!)

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

    // Test Properties
    XCTAssertEqual(event?.eventName, decodedObject.eventName)
    XCTAssertEqual(event?.values, decodedObject.values)
  }

  func testEncodingAndDecodingWithoutValues() throws {
    let event = validEventWithoutValues
    // swiftlint:disable:next force_unwrapping
    let decodedObject = try CodabilityTesting.encodeAndDecode(event!)

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

    // Test Properties
    XCTAssertEqual(event?.eventName, decodedObject.eventName)
    XCTAssertEqual(event?.values, decodedObject.values)
  }
}

// MARK: - Assumptions

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


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

@testable import FBAEMKit

import TestTools
import XCTest

final class AEMInvocationTests: XCTestCase {

  enum Keys {
    static let campaignID = "campaign_ids"
    static let acsToken = "acs_token"
    static let acsSharedSecret = "shared_secret"
    static let acsConfigurationID = "acs_config_id"
    static let advertiserID = "advertiser_id"
    static let businessID = "advertiser_id"
    static let catalogID = "catalog_id"
    static let timestamp = "timestamp"
    static let configurationMode = "config_mode"
    static let configurationID = "config_id"
    static let recordedEvents = "recorded_events"
    static let recordedValues = "recorded_values"
    static let conversionValues = "conversion_values"
    static let priority = "priority"
    static let conversionTimestamp = "conversion_timestamp"
    static let isAggregated = "is_aggregated"
    static let hasStoreKitAdNetwork = "has_skan"
    static let defaultCurrency = "default_currency"
    static let cutoffTime = "cutoff_time"
    static let validFrom = "valid_from"
    static let conversionValueRules = "conversion_value_rules"
    static let conversionValue = "conversion_value"
    static let events = "events"
    static let eventName = "event_name"
    static let values = "values"
    static let currency = "currency"
    static let amount = "amount"
    static let paramRule = "param_rule"
    static let content = "fb_content"
    static let contentID = "fb_content_id"
    static let contentType = "fb_content_type"
    static let identity = "id"
    static let itemPrice = "item_price"
    static let quantity = "quantity"
  }

  enum Values {
    static let purchase = "fb_mobile_purchase"
    static let donate = "Donate"
    static let unlock = "fb_unlock_level"
    static let test = "fb_test_event"
    static let defaultMode = "DEFAULT"
    static let brandMode = "BRAND"
    static let cpasMode = "CPAS"
    static let USD = "USD"
  }

  let boostPriority = 32

  var validInvocation = AEMInvocation(
    campaignID: "test_campaign_1234",
    acsToken: "test_token_12345",
    acsSharedSecret: "test_shared_secret",
    acsConfigurationID: "test_config_123",
    businessID: "test_advertiserid_coffee",
    catalogID: "test_catalog_123",
    timestamp: Date(timeIntervalSince1970: 1618383600),
    configurationMode: "DEFAULT",
    configurationID: 10,
    recordedEvents: nil,
    recordedValues: nil,
    conversionValue: -1,
    priority: -1,
    conversionTimestamp: Date(timeIntervalSince1970: 1618383700),
    isAggregated: false,
    isTestMode: false,
    hasStoreKitAdNetwork: false,
    isConversionFilteringEligible: true
  )! // swiftlint:disable:this force_unwrapping

  var configuration1 = AEMConfiguration(json: [
    Keys.defaultCurrency: Values.USD,
    Keys.cutoffTime: 1,
    Keys.validFrom: 10000,
    Keys.configurationMode: Values.defaultMode,
    Keys.conversionValueRules: [
      [
        Keys.conversionValue: 2,
        Keys.priority: 10,
        Keys.events: [
          [
            Keys.eventName: Values.purchase,
          ],
          [
            Keys.eventName: Values.donate,
          ],
        ],
      ],
      [
        Keys.conversionValue: 1,
        Keys.priority: 11,
        Keys.events: [
          [
            Keys.eventName: Values.purchase,
            Keys.values: [
              [
                Keys.currency: Values.USD,
                Keys.amount: 100.0,
              ],
            ],
          ],
          [
            Keys.eventName: Values.unlock,
          ],
        ],
      ],
    ],
  ])! // swiftlint:disable:this force_unwrapping

  var configuration2 = AEMConfiguration(json: [
    Keys.defaultCurrency: Values.USD,
    Keys.cutoffTime: 1,
    Keys.validFrom: 20000,
    Keys.configurationMode: Values.defaultMode,
    Keys.conversionValueRules: [
      [
        Keys.conversionValue: 2,
        Keys.priority: 10,
        Keys.events: [
          [
            Keys.eventName: Values.purchase,
          ],
          [
            Keys.eventName: Values.donate,
          ],
        ],
      ],
    ],
  ])! // swiftlint:disable:this force_unwrapping

  func testInvocationWithInvalidAppLinkData() {
    var invalidData: [String: Any] = [:]

    XCTAssertNil(AEMInvocation(appLinkData: nil))

    invalidData = [
      "acs_token": "test_token_12345",
    ]
    XCTAssertNil(AEMInvocation(appLinkData: invalidData))

    invalidData = [
      "campaign_ids": "test_campaign_1234",
    ]
    XCTAssertNil(AEMInvocation(appLinkData: invalidData))

    invalidData = [
      "advertiser_id": "test_advertiserid_coffee",
    ]
    XCTAssertNil(AEMInvocation(appLinkData: invalidData))

    invalidData = [
      "acs_token": 123,
      "campaign_ids": 123,
    ]
    XCTAssertNil(AEMInvocation(appLinkData: invalidData))
  }

  func testInvocationWithValidAppLinkData() {
    var validData: [String: Any] = [:]
    var invocation: AEMInvocation?

    validData = [
      "acs_token": "test_token_12345",
      "campaign_ids": "test_campaign_1234",
    ]
    invocation = AEMInvocation(appLinkData: validData)
    XCTAssertEqual(invocation?.acsToken, "test_token_12345")
    XCTAssertEqual(invocation?.campaignID, "test_campaign_1234")
    XCTAssertNil(invocation?.businessID)

    validData = [
      "acs_token": "test_token_12345",
      "campaign_ids": "test_campaign_1234",
      "advertiser_id": "test_advertiserid_coffee",
    ]
    invocation = AEMInvocation(appLinkData: validData)
    XCTAssertEqual(invocation?.acsToken, "test_token_12345")
    XCTAssertEqual(invocation?.campaignID, "test_campaign_1234")
    XCTAssertEqual(invocation?.businessID, "test_advertiserid_coffee")
  }

  func testInvocationWithCatalogID() {
    let invocation = AEMInvocation(appLinkData: [
      "acs_token": "test_token_12345",
      "campaign_ids": "test_campaign_1234",
      "advertiser_id": "test_advertiserid_coffee",
      "catalog_id": "test_catalog_1234",
    ])

    XCTAssertEqual(
      invocation?.acsToken,
      "test_token_12345",
      "Invocation's ACS token is not expected"
    )
    XCTAssertEqual(
      invocation?.campaignID,
      "test_campaign_1234",
      "Invocation's campaign ID is not expected"
    )
    XCTAssertEqual(
      invocation?.businessID,
      "test_advertiserid_coffee",
      "Invocation's business ID is not expected"
    )
    XCTAssertEqual(
      invocation?.catalogID,
      "test_catalog_1234",
      "Invocation's catalog ID is not expected"
    )
  }

  func testInvocationWithoutCatalogID() {
    let invocation = AEMInvocation(appLinkData: [
      "acs_token": "test_token_12345",
      "campaign_ids": "test_campaign_1234",
      "advertiser_id": "test_advertiserid_coffee",
    ])

    XCTAssertNotNil(
      invocation,
      "Invocation is not expected to be nil"
    )
    XCTAssertNil(
      invocation?.catalogID,
      "Invocation's catalog ID is expected to be nil"
    )
  }

  func testInvocationWithDebuggingAppLinkData() throws {
    let data = [
      "acs_token": "debuggingtoken",
      "campaign_ids": "test_campaign_1234",
      "advertiser_id": "test_advertiserid_coffee",
      "test_deeplink": 1,
    ] as [String: Any]
    let invocation = try XCTUnwrap(AEMInvocation(appLinkData: data))

    XCTAssertTrue(
      invocation.isTestMode,
      "Invocation is expected to be test mode when test_deeplink is true"
    )
    XCTAssertEqual(
      invocation.acsToken,
      "debuggingtoken",
      "Invocations's acsToken is not expected"
    )
    XCTAssertEqual(
      invocation.campaignID,
      "test_campaign_1234",
      "Invocations's campaignID is not expected"
    )
  }

  func testInvocationWithSKANInfoAppLinkData() throws {
    let data = [
      "acs_token": "debuggingtoken",
      "campaign_ids": "test_campaign_1234",
      "advertiser_id": "test_advertiserid_coffee",
      "has_skan": true,
    ] as [String: Any]
    let invocation = try XCTUnwrap(AEMInvocation(appLinkData: data))

    XCTAssertTrue(
      invocation.hasStoreKitAdNetwork,
      "Invocation's hasStoreKitAdNetwork is expected to be true when has_skan is true"
    )
    XCTAssertEqual(
      invocation.acsToken,
      "debuggingtoken",
      "Invocations's acsToken is not expected"
    )
    XCTAssertEqual(
      invocation.campaignID,
      "test_campaign_1234",
      "Invocations's campaignID is not expected"
    )
  }

  func testProcessedParametersWithValidContentAndContentID() {
    let invocation: AEMInvocation? = validInvocation
    let content: [String: AnyHashable] = ["id": "123", "quantity": 5]
    let contentIDs: [String] = ["id123", "id456"]

    let parameters = invocation?.getProcessedParameters(
      from: [
        Keys.content: #"[{"id": "123", "quantity": 5}]"#,
        Keys.contentID: #"["id123", "id456"]"#,
        Keys.contentType: "product",
      ]
    ) as? [String: AnyHashable]
    XCTAssertEqual(
      parameters,
      [
        Keys.content: [content],
        Keys.contentID: contentIDs,
        Keys.contentType: "product",
      ],
      "Processed parameters are not expected"
    )
  }

  func testProcessedParametersWithValidContent() {
    let invocation: AEMInvocation? = validInvocation
    let content: [String: AnyHashable] = ["id": "123", "quantity": 5]

    let parameters = invocation?.getProcessedParameters(
      from: [
        Keys.content: #"[{"id": "123", "quantity": 5}]"#,
        Keys.contentID: "001",
        Keys.contentType: "product",
      ]
    ) as? [String: AnyHashable]
    XCTAssertEqual(
      parameters,
      [
        Keys.content: [content],
        Keys.contentID: "001",
        Keys.contentType: "product",
      ],
      "Processed parameters are not expected"
    )
  }

  func testProcessedParametersWithInvalidContent() {
    let invocation: AEMInvocation? = validInvocation

    let parameters = invocation?.getProcessedParameters(
      from: [
        Keys.content: #"[{"id": ,"quantity": 5}]"#,
        Keys.contentID: "001",
        Keys.contentType: "product",
      ]
    ) as? [String: AnyHashable]
    XCTAssertEqual(
      parameters,
      [
        Keys.content: #"[{"id": ,"quantity": 5}]"#,
        Keys.contentID: "001",
        Keys.contentType: "product",
      ],
      "Processed parameters are not expected"
    )
  }

  func testFindConfiguration() {
    var invocation: AEMInvocation? = validInvocation
    invocation?.reset()
    invocation?.configurationID = 10
    XCTAssertNil(
      invocation?.findConfiguration(in: [Values.defaultMode: [configuration1, configuration2]]),
      "Should not find the configuration with unmatched configurationID"
    )

    invocation = AEMInvocation(
      campaignID: "test_campaign_1234",
      acsToken: "test_token_12345",
      acsSharedSecret: nil,
      acsConfigurationID: nil,
      businessID: nil,
      catalogID: nil,
      isTestMode: false,
      hasStoreKitAdNetwork: false,
      isConversionFilteringEligible: true
    )
    let configuration = invocation?.findConfiguration(in: [Values.defaultMode: [configuration1, configuration2]])
    XCTAssertEqual(invocation?.configurationID, 20000, "Should set the invocation with expected configurationID")
    XCTAssertEqual(
      invocation?.configurationMode,
      Values.defaultMode,
      "Should set the invocation with expected configurationMode"
    )
    XCTAssertEqual(configuration?.validFrom, configuration2.validFrom, "Should find the expected configuration")
    XCTAssertEqual(
      configuration?.mode,
      configuration2.mode,
      "Should find the expected configuration"
    )
  }

  func testFindConfigWithBusinessID1() {
    let configurationWithBusinessID = SampleAEMConfigurations.createConfigurationWithBusinessID()
    let configurationWithoutBusinessID = SampleAEMConfigurations.createConfigurationWithoutBusinessID()
    let invocation = validInvocation
    invocation.reset()
    invocation.configurationID = 10000

    let configuration = invocation.findConfiguration(
      in: [
        Values.defaultMode: [configurationWithoutBusinessID],
        Values.brandMode: [configurationWithBusinessID],
      ]
    )
    XCTAssertEqual(
      configuration?.validFrom,
      10000,
      "Should have expected validFrom"
    )
    XCTAssertNil(
      configuration?.businessID,
      "Should not have unexpected advertiserID"
    )
  }

  func testFindConfigWithBusinessID2() {
    let configurationWithBusinessID = SampleAEMConfigurations.createConfigurationWithBusinessID()
    let configurationWithoutBusinessID = SampleAEMConfigurations.createConfigurationWithoutBusinessID()
    let invocation = validInvocation
    invocation.reset()
    invocation.configurationID = 10000
    invocation.businessID = "test_advertiserid_123"

    let configuration = invocation.findConfiguration(
      in: [
        Values.defaultMode: [configurationWithoutBusinessID],
        Values.brandMode: [configurationWithBusinessID],
      ]
    )
    XCTAssertEqual(
      configuration?.validFrom,
      10000,
      "Should have expected validFrom"
    )
    XCTAssertEqual(
      configuration?.businessID,
      "test_advertiserid_123",
      "Should have expected advertiserID"
    )
  }

  func testFindConfigWithBusinessID3() {
    let configurationWithBusinessID = SampleAEMConfigurations.createConfigurationWithBusinessID()
    let configurationWithoutBusinessID = SampleAEMConfigurations.createConfigurationWithoutBusinessID()
    let invocation = AEMInvocation(
      campaignID: "test_campaign_1234",
      acsToken: "test_token_12345",
      acsSharedSecret: nil,
      acsConfigurationID: nil,
      businessID: "test_advertiserid_123",
      catalogID: nil,
      isTestMode: false,
      hasStoreKitAdNetwork: false,
      isConversionFilteringEligible: true
    )
    let configuration = invocation?.findConfiguration(
      in: [
        Values.defaultMode: [configurationWithoutBusinessID],
        Values.brandMode: [configurationWithBusinessID],
      ]
    )
    XCTAssertEqual(invocation?.configurationID, 10000, "Should set the invocation with expected configurationID")
    XCTAssertEqual(
      invocation?.configurationMode,
      Values.defaultMode,
      "Should set the invocation with expected configurationMode"
    )
    XCTAssertEqual(
      configuration?.validFrom,
      configurationWithBusinessID.validFrom,
      "Should find the expected configuration"
    )
    XCTAssertEqual(
      configuration?.mode,
      configurationWithBusinessID.mode,
      "Should find the expected configuration"
    )
    XCTAssertEqual(
      configuration?.businessID,
      configurationWithBusinessID.businessID,
      "Should find the expected configuration"
    )
  }

  func testFindConfigWithCpas() {
    let cpasConfiguration = SampleAEMConfigurations.createCpasConfiguration()
    let invocation = AEMInvocation(
      campaignID: "test_campaign_1234",
      acsToken: "test_token_12345",
      acsSharedSecret: nil,
      acsConfigurationID: nil,
      businessID: "test_advertiserid_cpas",
      catalogID: nil,
      isTestMode: false,
      hasStoreKitAdNetwork: false,
      isConversionFilteringEligible: true
    )
    let configuration = invocation?.findConfiguration(
      in: [
        Values.defaultMode: [SampleAEMConfigurations.createConfigurationWithoutBusinessID()],
        Values.brandMode: [SampleAEMConfigurations.createConfigurationWithBusinessIDAndContentRule()],
        Values.cpasMode: [cpasConfiguration],
      ]
    )
    XCTAssertEqual(invocation?.configurationID, 10000, "Should set the invocation with expected configurationID")
    XCTAssertEqual(
      invocation?.configurationMode,
      Values.cpasMode,
      "Should set the invocation with expected configurationMode"
    )
    XCTAssertEqual(
      configuration?.validFrom,
      cpasConfiguration.validFrom,
      "Should find the expected configuration"
    )
    XCTAssertEqual(
      configuration?.mode,
      cpasConfiguration.mode,
      "Should find the expected configuration"
    )
    XCTAssertEqual(
      configuration?.businessID,
      cpasConfiguration.businessID,
      "Should find the expected configuration"
    )
  }

  func testGetConfigurationList() {
    let configurations = [
      Values.defaultMode: [SampleAEMConfigurations.createConfigurationWithoutBusinessID()],
      Values.brandMode: [SampleAEMConfigurations.createConfigurationWithBusinessIDAndContentRule()],
      Values.cpasMode: [SampleAEMConfigurations.createCpasConfiguration()],
    ]
    let invocation = SampleAEMInvocations.createGeneralInvocation1()

    var configurationList = invocation.getConfigurationList(mode: .default, configurations: configurations)
    XCTAssertEqual(configurationList.count, 1, "Should only find the default configuration")

    configurationList = invocation.getConfigurationList(mode: .brand, configurations: configurations)
    XCTAssertEqual(configurationList.count, 2, "Should only find the brand or cpas configuration")
    XCTAssertEqual(
      configurationList.first?.mode,
      Values.cpasMode,
      "Should have the caps configuration first"
    )
    XCTAssertEqual(
      configurationList.last?.mode,
      Values.brandMode,
      "Should have the brand configuration last"
    )
  }

  func testAttributeEventWithValue() {
    let invocation: AEMInvocation = validInvocation
    invocation.reset()
    invocation.setConfiguration(configuration1)

    var isAttributed = invocation.attributeEvent(
      Values.test,
      currency: Values.USD,
      value: 10,
      parameters: nil,
      configurations: [Values.defaultMode: [configuration1, configuration2]],
      shouldUpdateCache: true,
      isRuleMatchInServer: false
    )
    XCTAssertFalse(isAttributed, "Should not attribute unexpected event")
    XCTAssertFalse(
      invocation.recordedEvents.contains(Values.test),
      "Should not add events that cannot be attributed to the invocation"
    )
    XCTAssertEqual(invocation.recordedValues.count, 0, "Should not attribute unexpected values")

    isAttributed = invocation.attributeEvent(
      Values.purchase,
      currency: Values.USD,
      value: 10,
      parameters: nil,
      configurations: [Values.defaultMode: [configuration1, configuration2]],
      shouldUpdateCache: true,
      isRuleMatchInServer: false
    )
    XCTAssertTrue(isAttributed, "Should attribute expected event")
    XCTAssertTrue(
      invocation.recordedEvents.contains(Values.purchase),
      "Should add events that can be attributed to the invocation"
    )
    XCTAssertEqual(
      invocation.recordedValues as? [String: [String: Int]],
      [Values.purchase: [Values.USD: 10]],
      "Should attribute unexpected values"
    )
  }

  func testAttributeUnexpectedEventWithoutValue() {
    let invocation: AEMInvocation = validInvocation
    invocation.reset()
    invocation.setConfiguration(configuration1)

    let isAttributed = invocation.attributeEvent(
      Values.test,
      currency: nil,
      value: nil,
      parameters: nil,
      configurations: [Values.defaultMode: [configuration1, configuration2]],
      shouldUpdateCache: true,
      isRuleMatchInServer: false
    )
    XCTAssertFalse(isAttributed, "Should not attribute unexpected event")
    XCTAssertFalse(invocation.recordedEvents.contains(Values.test))
    XCTAssertEqual(invocation.recordedValues.count, 0, "Should not attribute unexpected values")
  }

  func testAttributeExpectedEventWithoutValue() {
    let invocation: AEMInvocation = validInvocation
    invocation.reset()
    invocation.setConfiguration(configuration1)

    var isAttributed = invocation.attributeEvent(
      Values.purchase,
      currency: nil,
      value: nil,
      parameters: nil,
      configurations: [Values.defaultMode: [configuration1, configuration2]],
      shouldUpdateCache: true,
      isRuleMatchInServer: false
    )
    XCTAssertTrue(isAttributed, "Should attribute the expected event")
    XCTAssertTrue(invocation.recordedEvents.contains(Values.purchase))
    XCTAssertEqual(invocation.recordedValues.count, 0, "Should not attribute unexpected values")

    isAttributed = invocation.attributeEvent(
      Values.donate,
      currency: nil,
      value: nil,
      parameters: nil,
      configurations: [Values.defaultMode: [configuration1, configuration2]],
      shouldUpdateCache: true,
      isRuleMatchInServer: false
    )
    XCTAssertTrue(isAttributed, "Should attribute the expected event")
    XCTAssertTrue(invocation.recordedEvents.contains(Values.donate))
    XCTAssertEqual(invocation.recordedValues.count, 0, "Should not attribute unexpected values")
  }

  func testAttributeEventWithExpectedContent() {
    let configWithBusinessID = SampleAEMConfigurations.createConfigurationWithBusinessIDAndContentRule()
    let configWithoutBusinessID = SampleAEMConfigurations.createConfigurationWithoutBusinessID()
    let invocation = AEMInvocation(
      campaignID: "test_campaign_1234",
      acsToken: "test_token_12345",
      acsSharedSecret: nil,
      acsConfigurationID: nil,
      businessID: "test_advertiserid_content_test",
      catalogID: nil,
      isTestMode: false,
      hasStoreKitAdNetwork: false,
      isConversionFilteringEligible: true
    )! // swiftlint:disable:this force_unwrapping
    let configurations = [
      Values.defaultMode: [configWithoutBusinessID],
      Values.brandMode: [configWithBusinessID],
    ]
    let isAttributed = invocation.attributeEvent(
      Values.purchase,
      currency: Values.USD,
      value: 0,
      parameters: [
        Keys.content: #"[{"id": "abc", "quantity": 5}]"#,
        Keys.contentID: "001",
        Keys.contentType: "product",
      ],
      configurations: configurations,
      shouldUpdateCache: true,
      isRuleMatchInServer: false
    )
    XCTAssertTrue(isAttributed, "Should attribute the event with expected parameters")
  }

  func testAttributeEventWithUnexpectedContent() {
    let configWithBusinessID = SampleAEMConfigurations.createConfigurationWithBusinessIDAndContentRule()
    let configWithoutBusinessID = SampleAEMConfigurations.createConfigurationWithoutBusinessID()
    let invocation = AEMInvocation(
      campaignID: "test_campaign_1234",
      acsToken: "test_token_12345",
      acsSharedSecret: nil,
      acsConfigurationID: nil,
      businessID: "test_advertiserid_content_test",
      catalogID: nil,
      isTestMode: false,
      hasStoreKitAdNetwork: false,
      isConversionFilteringEligible: true
    )! // swiftlint:disable:this force_unwrapping
    let configurations = [
      Values.defaultMode: [configWithoutBusinessID],
      Values.brandMode: [configWithBusinessID],
    ]
    let isAttributed = invocation.attributeEvent(
      Values.purchase,
      currency: Values.USD,
      value: 0,
      parameters: [
        Keys.content: #"[{"id": "123", "quantity": 5}]"#,
        Keys.contentID: "001",
        Keys.contentType: "product",
      ],
      configurations: configurations,
      shouldUpdateCache: true,
      isRuleMatchInServer: false
    )
    XCTAssertFalse(isAttributed, "Should attribute the event with expected parameters")
  }

  func testAttributeCpasEvent() {
    let invocation = AEMInvocation(
      campaignID: "test_campaign_1234",
      acsToken: "test_token_12345",
      acsSharedSecret: nil,
      acsConfigurationID: nil,
      businessID: "test_advertiserid_cpas",
      catalogID: nil,
      isTestMode: false,
      hasStoreKitAdNetwork: false,
      isConversionFilteringEligible: true
    )! // swiftlint:disable:this force_unwrapping
    let configurations = [
      Values.defaultMode: [SampleAEMConfigurations.createConfigurationWithoutBusinessID()],
      Values.cpasMode: [SampleAEMConfigurations.createCpasConfiguration()],
    ]
    let isAttributed = invocation.attributeEvent(
      Values.purchase,
      currency: Values.USD,
      value: NSNumber(value: 5000),
      parameters: [
        Keys.content: [
          [
            Keys.identity: "abc",
            Keys.itemPrice: NSNumber(value: 100),
            Keys.quantity: NSNumber(value: 10),
          ],
          [
            Keys.identity: "test",
            Keys.itemPrice: NSNumber(value: 200),
            Keys.quantity: NSNumber(value: 20),
          ],
        ],
      ],
      configurations: configurations,
      shouldUpdateCache: true,
      isRuleMatchInServer: false
    )
    XCTAssertTrue(
      isAttributed,
      "Should attribute the event"
    )
    XCTAssertEqual(
      invocation.recordedEvents,
      [Values.purchase],
      "Should expect the event is updated in the cache"
    )
    XCTAssertEqual(
      invocation.recordedValues as? [String: [String: Int]],
      [Values.purchase: [Values.USD: 1000]],
      "Should expect the total value is updated in the cache"
    )
  }

  func testAttributeEventWithRuleMatchInServer() {
    let invocation = AEMInvocation(
      campaignID: "test_campaign_1234",
      acsToken: "test_token_12345",
      acsSharedSecret: nil,
      acsConfigurationID: nil,
      businessID: "test_advertiserid_cpas",
      catalogID: nil,
      isTestMode: false,
      hasStoreKitAdNetwork: false,
      isConversionFilteringEligible: true
    )! // swiftlint:disable:this force_unwrapping
    let configurations = [
      Values.defaultMode: [SampleAEMConfigurations.createConfigurationWithoutBusinessID()],
      Values.cpasMode: [SampleAEMConfigurations.createCpasConfiguration()],
    ]
    let isAttributed = invocation.attributeEvent(
      Values.purchase,
      currency: Values.USD,
      value: NSNumber(value: 5000),
      parameters: [
        Keys.content: [
          [
            Keys.identity: "abc",
            Keys.itemPrice: NSNumber(value: 100),
            Keys.quantity: NSNumber(value: 10),
          ],
          [
            Keys.identity: "test",
            Keys.itemPrice: NSNumber(value: 200),
            Keys.quantity: NSNumber(value: 20),
          ],
        ],
      ],
      configurations: configurations,
      shouldUpdateCache: true,
      isRuleMatchInServer: true
    )
    XCTAssertTrue(
      isAttributed,
      "Should attribute the event"
    )
    XCTAssertEqual(
      invocation.recordedEvents,
      [Values.purchase],
      "Should expect the event is updated in the cache"
    )
    XCTAssertEqual(
      invocation.recordedValues as? [String: [String: Int]],
      [Values.purchase: [Values.USD: 5000]],
      "Should expect the value is updated in the cache"
    )
  }

  func testAttributeEventWithoutCache() {
    let invocation: AEMInvocation = validInvocation
    invocation.reset()
    invocation.setConfiguration(configuration1)

    let isAttributed = invocation.attributeEvent(
      Values.purchase,
      currency: nil,
      value: nil,
      parameters: nil,
      configurations: [Values.defaultMode: [configuration1, configuration2]],
      shouldUpdateCache: false,
      isRuleMatchInServer: false
    )
    XCTAssertTrue(isAttributed, "Should attribute the expected event")
    XCTAssertFalse(invocation.recordedEvents.contains(Values.purchase), "Should not update the event cache")
    XCTAssertEqual(invocation.recordedValues.count, 0, "Should not update value cache")
  }

  func testAttributeEventAndValueWithoutCache() {
    let invocation: AEMInvocation = validInvocation
    invocation.reset()
    invocation.setConfiguration(configuration1)

    let isAttributed = invocation.attributeEvent(
      Values.purchase,
      currency: Values.USD,
      value: 10,
      parameters: nil,
      configurations: [Values.defaultMode: [configuration1, configuration2]],
      shouldUpdateCache: false,
      isRuleMatchInServer: false
    )
    XCTAssertTrue(isAttributed, "Should attribute expected event")
    XCTAssertFalse(
      invocation.recordedEvents.contains(Values.purchase),
      "Should add events that can be attributed to the invocation"
    )
    XCTAssertEqual(
      invocation.recordedValues.count,
      0,
      "Should not update value cache"
    )
  }

  func testUpdateConversionWithValue() {
    let invocation: AEMInvocation = validInvocation
    invocation.reset()
    invocation.setConfiguration(configuration1)

    invocation.recordedEvents = [Values.purchase, Values.unlock]
    XCTAssertFalse(
      invocation.updateConversionValue(
        configurations: [Values.defaultMode: [configuration1, configuration2]],
        event: Values.purchase,
        shouldBoostPriority: false
      ),
      "Should not update conversion value"
    )

    invocation.recordedEvents = [Values.purchase, Values.donate]
    XCTAssertTrue(
      invocation.updateConversionValue(
        configurations: [Values.defaultMode: [configuration1, configuration2]],
        event: Values.purchase,
        shouldBoostPriority: false
      ),
      "Should update conversion value"
    )
    XCTAssertEqual(
      invocation.conversionValue,
      2,
      "Should update the expected conversion value"
    )

    invocation.recordedEvents = [Values.purchase, Values.unlock]
    invocation.recordedValues = [Values.purchase: [Values.USD: 100.0]]
    XCTAssertTrue(
      invocation.updateConversionValue(
        configurations: [Values.defaultMode: [configuration1, configuration2]],
        event: Values.purchase,
        shouldBoostPriority: false
      ),
      "Should update conversion value"
    )
    XCTAssertEqual(
      invocation.conversionValue,
      1,
      "Should update the expected conversion value"
    )

    invocation.reset()
    invocation.priority = 100
    invocation.recordedEvents = [Values.purchase, Values.unlock]
    invocation.recordedValues = [Values.purchase: [Values.USD: 100]]
    XCTAssertFalse(
      invocation.updateConversionValue(
        configurations: [Values.defaultMode: [configuration1, configuration2]],
        event: Values.purchase,
        shouldBoostPriority: false
      ),
      "Should not update conversion value under priority"
    )
  }

  func testUpdateConversionWithouValue() {
    let invocation: AEMInvocation = validInvocation
    invocation.reset()
    invocation.setConfiguration(configuration2)

    invocation.recordedEvents = [Values.purchase]
    XCTAssertFalse(
      invocation.updateConversionValue(
        configurations: [Values.defaultMode: [configuration1, configuration2]],
        event: Values.purchase,
        shouldBoostPriority: false
      ),
      "Should not update conversion value"
    )
    XCTAssertEqual(
      invocation.conversionValue,
      -1,
      "Should not update the unexpected conversion value"
    )

    invocation.recordedEvents = [Values.purchase, Values.donate]
    XCTAssertTrue(
      invocation.updateConversionValue(
        configurations: [Values.defaultMode: [configuration1, configuration2]],
        event: Values.purchase,
        shouldBoostPriority: false
      ),
      "Should update conversion value"
    )
    XCTAssertEqual(
      invocation.conversionValue,
      2,
      "Should update the expected conversion value"
    )
  }

  func testUpdateConversionWithBoostPriority() {
    let configuration = SampleAEMConfigurations.createWithMultipleRules()
    let configurations = [Values.defaultMode: [configuration]]
    let invocation = SampleAEMInvocations.createCatalogOptimizedInvocation()

    // swiftlint:disable force_unwrapping
    let lowestPriorityRule = configuration.conversionValueRules.last!
    // Set the highest priority in the conversion rules
    invocation.priority = configuration.conversionValueRules.first!.priority
    // Add the lowest priority event
    invocation.recordedEvents = [lowestPriorityRule.events.first!.eventName]
    // swiftlint:enable force_unwrapping

    XCTAssertTrue(
      invocation.updateConversionValue(configurations: configurations, event: Values.donate, shouldBoostPriority: true),
      "Should expect to update the conversion value"
    )
    XCTAssertEqual(
      invocation.priority,
      lowestPriorityRule.priority + boostPriority,
      "Should expect the updated priority to be boosted"
    )
    XCTAssertEqual(
      invocation.conversionValue,
      lowestPriorityRule.conversionValue,
      "Should expect the conversion value is updated for the optimized event"
    )
  }

  // Test conversion value updating when optimized event has multiple conversion values
  func testUpdateConversionWithHigherBoostPriority() {
    let configuration = SampleAEMConfigurations.createWithMultipleRules()
    let configurations = [Values.defaultMode: [configuration]]
    let invocation = SampleAEMInvocations.createCatalogOptimizedInvocation()
    invocation.campaignID = "83" // The campaign id's modulo is matched to purchase's conversion value

    let highestPriorityRule = configuration.conversionValueRules.first! // swiftlint:disable:this force_unwrapping
    invocation.priority = 42 // Set the second highest priority with boost priority in the conversion rules
    invocation.recordedEvents = [Values.purchase] // Add the optimzied event
    invocation.recordedValues = [Values.purchase: [Values.USD: 100.0]]
    XCTAssertTrue(
      invocation.updateConversionValue(
        configurations: configurations,
        event: Values.purchase,
        shouldBoostPriority: true
      ),
      "Should expect to update the conversion value"
    )
    XCTAssertEqual(
      invocation.priority,
      highestPriorityRule.priority + boostPriority,
      "Should expect the updated priority to be boosted"
    )
    XCTAssertEqual(
      invocation.conversionValue,
      highestPriorityRule.conversionValue,
      "Should expect the conversion value is updated for the optimized event"
    )
  }

  func testUpdateConversionWithBoostPriorityAndNonOptimziedEvent() {
    let configuration = SampleAEMConfigurations.createWithMultipleRules()
    let configurations = [Values.defaultMode: [configuration]]
    let invocation = SampleAEMInvocations.createCatalogOptimizedInvocation()

    let lowestPriorityRule = configuration.conversionValueRules.last! // swiftlint:disable:this force_unwrapping
    // Set the highest priority in the conversion rules
    invocation.priority = configuration.conversionValueRules.first!.priority // swiftlint:disable:this force_unwrapping
    // Add the lowest priority event
    invocation.recordedEvents = [
      Values.purchase,
      lowestPriorityRule.events.first!.eventName, // swiftlint:disable:this force_unwrapping
    ]
    XCTAssertFalse(
      invocation.updateConversionValue(
        configurations: configurations,
        event: Values.purchase,
        shouldBoostPriority: true
      ),
      "Should expect not to update the conversion value"
    )
  }

  func testDecodeBase64UrlSafeString() {
    let decodedString = validInvocation
      .decodeBase64URLSafeString(
        "E_dwjTaF9-SHijRKoD5jrgJoi9pgObKEqrkxgl3iE9-mxpDn-wpseBmtlNFN2HTI5OzzTVqhBwNi2zrwt-TxCw"
      )
    XCTAssertEqual(
      decodedString?.base64EncodedString(),
      "E/dwjTaF9+SHijRKoD5jrgJoi9pgObKEqrkxgl3iE9+mxpDn+wpseBmtlNFN2HTI5OzzTVqhBwNi2zrwt+TxCw==",
      "Should decode the base64 url safe string correctly"
    )
  }

  func testDecodeBase64UrlSafeStringWithEmptyString() {
    let decodedString = validInvocation.decodeBase64URLSafeString("")
    XCTAssertNil(
      decodedString?.base64EncodedString(),
      "Should decode the base64 url safe string as nil with empty string"
    )
  }

  func testGetHmacWithoutACSSecret() {
    let invocation = SampleAEMInvocations.createGeneralInvocation1()
    invocation.acsSharedSecret = nil

    XCTAssertNil(
      invocation.getHMAC(delay: 10),
      "HMAC should be nil when ACS Shared Secret is nil"
    )
  }

  func testGetHmacWithEmptyACSSecret() {
    let invocation = SampleAEMInvocations.createGeneralInvocation1()
    invocation.acsSharedSecret = ""

    XCTAssertNil(
      invocation.getHMAC(delay: 10),
      "HMAC should be nil when ACS Shared Secret is an empty string"
    )
  }

  func testGetHmacWithoutACSConfigurationID() {
    let invocation = SampleAEMInvocations.createGeneralInvocation1()
    invocation.acsConfigurationID = nil

    XCTAssertNil(
      invocation.getHMAC(delay: 10),
      "HMAC should be nil when ACS configuration ID is nil"
    )
  }

  func testGetHmacWithACSSecretAndACSConfigurationID() {
    let invocation = SampleAEMInvocations.createGeneralInvocation1()
    invocation.campaignID = "aaa"
    invocation.acsConfigurationID = "abc"
    invocation.acsSharedSecret =
      "E_dwjTaF9-SHijRKoD5jrgJoi9pgObKEqrkxgl3iE9-mxpDn-wpseBmtlNFN2HTI5OzzTVqhBwNi2zrwt-TxCw"
    invocation.conversionValue = 6

    XCTAssertEqual(
      invocation.getHMAC(delay: 31),
      "Z65Xxo-IevEwpLYNES9QmWRlx-zPH8zxfIJPw6ofQtpDJvKWuNI93SBHlUapS1_DIVl9Ovwoa5Xo7v63zQ5_HA",
      "Should generate the expected HMAC"
    )
  }

  func testIsOptimizedEventWithoutCatalogID() {
    let invocation = SampleAEMInvocations.createGeneralInvocation1()
    let configurations = [
      Values.defaultMode: [configuration1],
    ]

    XCTAssertFalse(
      invocation.isOptimizedEvent(Values.purchase, configurations: configurations),
      "Invocation without catalog ID doesn't have optimized event"
    )
  }

  func testIsOptimizedEventWithoutExpectedEvent() {
    let invocation = SampleAEMInvocations.createCatalogOptimizedInvocation()
    let configurations = [
      Values.defaultMode: [configuration1],
    ]

    XCTAssertFalse(
      invocation.isOptimizedEvent(Values.donate, configurations: configurations),
      "Event is not expected to be optimized"
    )
  }

  func testIsOptimizedEventWithExpectedEvent() {
    let invocation = SampleAEMInvocations.createCatalogOptimizedInvocation()
    let configurations = [
      Values.defaultMode: [configuration1],
    ]

    XCTAssertTrue(
      invocation.isOptimizedEvent(Values.purchase, configurations: configurations),
      "Event is expected to be optimized"
    )
  }

  func testSecureCoding() {
    XCTAssertTrue(
      AEMInvocation.supportsSecureCoding,
      "AEM Invocation should support secure coding"
    )
  }

  func testEncodingAndDecoding() throws {
    // Encode and Decode
    let object = validInvocation
    let decodedObject = try CodabilityTesting.encodeAndDecode(object)

    // Test Objects
    // XCTAssertEqual(decodedObject, invocation, .isCodable) // Doesn't work since isEqual not implemented
    XCTAssertNotIdentical(decodedObject, object, .isCodable)

    // Test Properties
    XCTAssertEqual(decodedObject.campaignID, object.campaignID, .isCodable)
    XCTAssertEqual(decodedObject.acsToken, object.acsToken, .isCodable)
    XCTAssertEqual(decodedObject.acsSharedSecret, object.acsSharedSecret, .isCodable)
    XCTAssertEqual(decodedObject.acsConfigurationID, object.acsConfigurationID, .isCodable)
    XCTAssertEqual(decodedObject.businessID, object.businessID, .isCodable)
    XCTAssertEqual(decodedObject.catalogID, object.catalogID, .isCodable)
    XCTAssertEqual(decodedObject.timestamp, object.timestamp, .isCodable)
    XCTAssertEqual(decodedObject.configurationMode, object.configurationMode, .isCodable)
    XCTAssertEqual(decodedObject.configurationID, object.configurationID, .isCodable)
    XCTAssertEqual(decodedObject.recordedEvents, object.recordedEvents, .isCodable)
    XCTAssertTrue(decodedObject.recordedValues.isEmpty, .isCodable)
    XCTAssertEqual(decodedObject.conversionValue, object.conversionValue, .isCodable)
    XCTAssertEqual(decodedObject.priority, object.priority, .isCodable)
    XCTAssertEqual(decodedObject.conversionTimestamp, object.conversionTimestamp, .isCodable)
    XCTAssertEqual(decodedObject.isAggregated, object.isAggregated, .isCodable)
    XCTAssertEqual(decodedObject.hasStoreKitAdNetwork, object.hasStoreKitAdNetwork, .isCodable)
    XCTAssertEqual(decodedObject.isConversionFilteringEligible, object.isConversionFilteringEligible, .isCodable)
  }
}

// swiftformat:disable extensionaccesscontrol

// MARK: - Assumptions

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