{
  "_meta": {
    "plan_id": "reactnative-e2e",
    "plugin": "reactnative",
    "version": "1.1.0",
    "description": "Pre-publish E2E test plan for the AppsFlyer React Native plugin. Runs against plugin source in example/. Covers six scenarios: cold launch, background deep link, foreground deep link, custom events, identity APIs, and consent/stop. Mapped to E2E-001..E2E-006 in appsflyer-mobile-plugin-tooling/contracts/e2e-test-contract.md.",
    "platforms": ["android", "ios"],
    "schema_version": "1.0.0",
    "tooling_contract_ref": "E2E-001, E2E-002, E2E-003, E2E-004, E2E-005, E2E-006"
  },

  "config": {
    "android": {
      "package_name": "com.appsflyer.engagement",
      "activity": "com.appsflyer.qa.reactnative.MainActivity",
      "apk_path": "example/android/app/build/outputs/apk/debug/app-debug.apk",
      "build_cmd": "cd example/android && ./gradlew assembleDebug"
    },
    "ios": {
      "bundle_id": "com.appsflyer.qa.reactnative",
      "app_path": "example/ios/build/Build/Products/Debug-iphonesimulator/example.app",
      "build_cmd": "cd example/ios && xcodebuild build -workspace example.xcworkspace -scheme example -configuration Debug -destination 'platform=iOS Simulator,id=$IOS_SIMULATOR_UDID' -derivedDataPath build"
    }
  },

  "phases": [
    {
      "id": "phase_1",
      "name": "Cold launch coverage",
      "scenario_ref": "E2E-001",
      "description": "Fresh install. Validate SDK startup, pre/post-start APIs, three auto-launched events with HTTP 200, install conversion data with is_first_launch=true, and onDeepLinking NOT_FOUND on clean launch.",
      "requires_fresh_install": true,
      "wait_after_launch_sec": 420,
      "checks": [
        {
          "id": "sdk_started",
          "description": "startSDK was called",
          "type": "log_contains",
          "pattern": "[AF_QA][startSDK] result:",
          "fail_action": "abort"
        },
        {
          "id": "is_first_launch_true",
          "description": "onInstallConversionData fires with is_first_launch=true",
          "type": "log_contains",
          "pattern": "[AF_QA][CALLBACK][onInstallConversionData]",
          "payload_check": {"field": "is_first_launch", "expected": "true"},
          "fail_action": "abort"
        },
        {
          "id": "pre_start_apis_complete",
          "description": "Pre-start auto APIs ran",
          "type": "log_contains",
          "pattern": "[AF_QA][AUTO_APIS] --- Pre-start auto APIs complete ---",
          "fail_action": "fail"
        },
        {
          "id": "post_start_apis_complete",
          "description": "Post-start auto APIs ran",
          "type": "log_contains",
          "pattern": "[AF_QA][AUTO_APIS] --- Post-start auto APIs complete ---",
          "fail_action": "fail"
        },
        {
          "id": "get_sdk_version",
          "description": "getSDKVersion returns a value",
          "type": "log_contains",
          "pattern": "[AF_QA][getSDKVersion] result:",
          "fail_action": "fail"
        },
        {
          "id": "get_appsflyer_uid",
          "description": "getAppsFlyerUID returns a value",
          "type": "log_contains",
          "pattern": "[AF_QA][getAppsFlyerUID] result:",
          "fail_action": "fail"
        },
        {
          "id": "event_af_demo_launch",
          "description": "af_demo_launch event fires successfully",
          "type": "log_contains",
          "pattern": "[AF_QA][logEvent(af_demo_launch)] result:",
          "fail_action": "fail"
        },
        {
          "id": "event_af_purchase",
          "description": "af_purchase event fires successfully",
          "type": "log_contains",
          "pattern": "[AF_QA][logEvent(af_purchase)] result:",
          "fail_action": "fail"
        },
        {
          "id": "event_af_content_view",
          "description": "af_content_view event fires successfully",
          "type": "log_contains",
          "pattern": "[AF_QA][logEvent(af_content_view)] result:",
          "fail_action": "fail"
        },
        {
          "id": "http_200_count",
          "description": "At least 3 HTTP 200 responses from AppsFlyer servers",
          "type": "count_matches",
          "pattern": "response code:200 OK|response_status=200",
          "minimum": 3,
          "fail_action": "fail"
        },
        {
          "id": "on_deep_linking_callback",
          "description": "onDeepLinking fires (NOT_FOUND expected on clean launch)",
          "type": "log_contains",
          "pattern": "[AF_QA][CALLBACK][onDeepLinking]",
          "fail_action": "fail"
        },
        {
          "id": "no_fatal_errors",
          "description": "No fatal exceptions or SDK errors in logs",
          "type": "absent",
          "patterns": ["Fatal Exception", "FATAL", "[AF_QA][startSDK] error:", "response code:4", "response code:5"],
          "fail_action": "fail"
        }
      ]
    },

    {
      "id": "phase_2",
      "name": "Background deep link",
      "scenario_ref": "E2E-002",
      "description": "App backgrounded after Phase 1, then deep link triggers re-entry. Verifies URL is delivered to native layer. onDeepLinking attribution (FOUND/value) requires real OneLink and is warn-only in CI. LAUNCH event receives HTTP 200.",
      "requires_fresh_install": false,
      "wait_after_trigger_sec": 15,
      "deep_link_url": "afqa-reactnative://deeplink?deep_link_value=qa_deeplink_bg&af_sub1=background_test&pid=testmedia&c=deeplink_test",
      "pre_actions": {
        "android": ["adb shell input keyevent KEYCODE_HOME", "sleep 2"],
        "ios": ["xcrun simctl terminate {{UDID}} {{BUNDLE_ID}}", "sleep 1"]
      },
      "trigger": {
        "android": "adb shell am start -W -a android.intent.action.VIEW -d \"{{DEEP_LINK_URL}}\"",
        "ios": "xcrun simctl launch {{UDID}} {{BUNDLE_ID}} -deepLinkURL \"{{DEEP_LINK_URL}}\""
      },
      "checks": [
        {
          "id": "deeplink_url_received",
          "description": "Deep link URL delivered to native app layer",
          "type": "log_contains",
          "pattern": "[AF_QA][DEEPLINK_NATIVE]",
          "fail_action": "fail"
        },
        {
          "id": "deeplink_callback_fired",
          "description": "onDeepLinking callback fires after deep link (any status)",
          "type": "log_contains",
          "pattern": "[AF_QA][CALLBACK][onDeepLinking]",
          "fail_action": "fail"
        },
        {
          "id": "deeplink_status_found",
          "description": "onDeepLinking fires with Status.FOUND (requires real OneLink attribution — expected to warn in CI)",
          "type": "log_contains",
          "pattern": "status=FOUND",
          "fail_action": "warn"
        },
        {
          "id": "deeplink_value_bg",
          "description": "deepLinkValue matches qa_deeplink_bg (requires server-side attribution — expected to warn in CI)",
          "type": "log_contains",
          "pattern": "deepLinkValue=qa_deeplink_bg",
          "fail_action": "warn"
        },
        {
          "id": "launch_http_200",
          "description": "LAUNCH event receives HTTP 200 after deep link re-entry",
          "type": "count_matches",
          "pattern": "response code:200 OK|response_status=200",
          "minimum": 1,
          "fail_action": "fail"
        },
        {
          "id": "no_fatal_errors",
          "description": "No fatal exceptions after deep link",
          "type": "absent",
          "patterns": ["Fatal Exception", "FATAL"],
          "fail_action": "fail"
        }
      ]
    },

    {
      "id": "phase_3",
      "name": "Foreground deep link",
      "scenario_ref": "E2E-003",
      "description": "Fresh install. App in foreground after SDK start. Brief launcher switch, then deep link. Verifies URL delivery and callback firing. Attribution checks (FOUND/value) are warn-only in CI. SDK start and conversion data are validated in Phase 1.",
      "requires_fresh_install": true,
      "wait_after_launch_sec": 420,
      "wait_after_trigger_sec": 15,
      "deep_link_url": "afqa-reactnative://deeplink?deep_link_value=qa_deeplink_fg&af_sub1=foreground_test&pid=testmedia&c=deeplink_test",
      "pre_actions": {
        "android": ["adb shell am start -a android.intent.action.MAIN -c android.intent.category.HOME", "sleep 1"],
        "ios": ["xcrun simctl terminate {{UDID}} {{BUNDLE_ID}}", "sleep 1"]
      },
      "trigger": {
        "android": "adb shell am start -W -a android.intent.action.VIEW -d \"{{DEEP_LINK_URL}}\"",
        "ios": "xcrun simctl launch {{UDID}} {{BUNDLE_ID}} -deepLinkURL \"{{DEEP_LINK_URL}}\""
      },
      "checks": [
        {
          "id": "deeplink_url_received",
          "description": "Deep link URL delivered to native app layer",
          "type": "log_contains",
          "pattern": "[AF_QA][DEEPLINK_NATIVE]",
          "fail_action": "fail"
        },
        {
          "id": "deeplink_callback_fired",
          "description": "onDeepLinking callback fires after deep link (any status)",
          "type": "log_contains",
          "pattern": "[AF_QA][CALLBACK][onDeepLinking]",
          "fail_action": "fail"
        },
        {
          "id": "deeplink_status_found",
          "description": "onDeepLinking fires with Status.FOUND (requires real OneLink attribution — expected to warn in CI)",
          "type": "log_contains",
          "pattern": "status=FOUND",
          "fail_action": "warn"
        },
        {
          "id": "deeplink_value_fg",
          "description": "deepLinkValue matches qa_deeplink_fg (requires server-side attribution — expected to warn in CI)",
          "type": "log_contains",
          "pattern": "deepLinkValue=qa_deeplink_fg",
          "fail_action": "warn"
        },
        {
          "id": "no_fatal_errors",
          "description": "No fatal exceptions",
          "type": "absent",
          "patterns": ["Fatal Exception", "FATAL"],
          "fail_action": "fail"
        }
      ]
    },

    {
      "id": "phase_4",
      "name": "Custom in-app event with parameters",
      "scenario_ref": "E2E-004",
      "description": "Trigger af_qa_custom_purchase with rich parameters (string, number, bool, nested map). Verify all parameter keys are serialized, HTTP 200 returned, and no logEvent errors.",
      "requires_fresh_install": false,
      "wait_after_launch_sec": 420,
      "checks": [
        {
          "id": "custom_event_logged",
          "description": "af_qa_custom_purchase logEvent fires",
          "type": "log_contains",
          "pattern": "[AF_QA][logEvent(af_qa_custom_purchase)]",
          "fail_action": "fail"
        },
        {
          "id": "params_complete",
          "description": "All 5 parameter keys present in the custom event log line",
          "type": "log_contains",
          "pattern": "[AF_QA][logEvent] name=af_qa_custom_purchase params=",
          "fail_action": "fail"
        },
        {
          "id": "http_200_event",
          "description": "HTTP 200 for the custom event",
          "type": "count_matches",
          "pattern": "response code:200 OK|response_status=200",
          "minimum": 1,
          "fail_action": "fail"
        },
        {
          "id": "no_logEvent_error",
          "description": "No logEvent errors for custom purchase",
          "type": "absent",
          "patterns": ["[AF_QA][logEvent(af_qa_custom_purchase)] error:"],
          "fail_action": "fail"
        },
        {
          "id": "no_fatal_errors",
          "description": "No fatal exceptions or process crashes",
          "type": "absent",
          "patterns": ["Fatal Exception", "FATAL"],
          "fail_action": "fail"
        }
      ]
    },

    {
      "id": "phase_5",
      "name": "Identity APIs round-trip",
      "scenario_ref": "E2E-005",
      "description": "Fresh install. Verify setCustomerUserId, setCurrencyCode, setAdditionalData propagate correctly. Identity-check event receives HTTP 200. is_first_launch=true still fires.",
      "requires_fresh_install": true,
      "wait_after_launch_sec": 420,
      "checks": [
        {
          "id": "customer_user_id_set",
          "description": "setCustomerUserId readback present",
          "type": "log_contains",
          "pattern": "[AF_QA][setCustomerUserId] result:",
          "fail_action": "fail"
        },
        {
          "id": "currency_code",
          "description": "setCurrencyCode readback present",
          "type": "log_contains",
          "pattern": "[AF_QA][setCurrencyCode] result:",
          "fail_action": "fail"
        },
        {
          "id": "additional_data",
          "description": "setAdditionalData readback present",
          "type": "log_contains",
          "pattern": "[AF_QA][setAdditionalData] result:",
          "fail_action": "fail"
        },
        {
          "id": "http_200_identity_event",
          "description": "HTTP 200 for the identity-check event",
          "type": "count_matches",
          "pattern": "response code:200 OK|response_status=200",
          "minimum": 1,
          "fail_action": "fail"
        },
        {
          "id": "is_first_launch_true",
          "description": "onInstallConversionData still fires with is_first_launch=true",
          "type": "log_contains",
          "pattern": "[AF_QA][CALLBACK][onInstallConversionData]",
          "payload_check": {"field": "is_first_launch", "expected": "true"},
          "fail_action": "fail"
        },
        {
          "id": "no_fatal_errors",
          "description": "No fatal exceptions or process crashes",
          "type": "absent",
          "patterns": ["Fatal Exception", "FATAL"],
          "fail_action": "fail"
        }
      ]
    },

    {
      "id": "phase_6",
      "name": "Consent / SDK stop toggle",
      "scenario_ref": "E2E-006",
      "description": "stop(true) suppresses outbound events. stop(false) resumes them. Verify suppressed event does NOT get HTTP 200, resumed event DOES get HTTP 200.",
      "requires_fresh_install": false,
      "wait_after_launch_sec": 420,
      "checks": [
        {
          "id": "stop_true",
          "description": "stop(true) readback present",
          "type": "log_contains",
          "pattern": "[AF_QA][stop] result: true",
          "fail_action": "fail"
        },
        {
          "id": "suppressed_event_no_result",
          "description": "Suppressed event does not get a success result while SDK is stopped",
          "type": "absent",
          "patterns": ["[AF_QA][logEvent(af_qa_suppressed)] result:"],
          "fail_action": "fail"
        },
        {
          "id": "stop_false",
          "description": "stop(false) readback present",
          "type": "log_contains",
          "pattern": "[AF_QA][stop] result: false",
          "fail_action": "fail"
        },
        {
          "id": "resumed_event_http_200",
          "description": "Resumed event fires and receives HTTP 200",
          "type": "log_contains",
          "pattern": "[AF_QA][logEvent(af_qa_resumed)] result:",
          "fail_action": "fail"
        },
        {
          "id": "no_fatal_errors",
          "description": "No fatal exceptions throughout stop/resume cycle",
          "type": "absent",
          "patterns": ["Fatal Exception", "FATAL"],
          "fail_action": "fail"
        }
      ]
    }
  ],

  "report": {
    "output_dir": ".af-e2e/reports",
    "format": "json"
  }
}
