// `moui run` command — build, install, and launch a mobile app on a device
// or emulator. Orchestrates the M6 `build_*` functions with platform-specific
// install/launch commands (adb / xcrun / hdc). Optional `--verify` runs the
// M9.2 probe to collect runtime observations. Desktop/web platforms are
// dispatched to `run_desktop.mbt` flows.

///|
priv struct ParsedRun {
  help : Bool
  json : Bool
  platform : String
  app : String
  options : RunOptions?
}

///|
priv struct RunOptions {
  app_config_path : String?
  device : String?
  renderer : String?
  fallback_skia : Bool
  prepare_only : Bool
  build_only : Bool
  no_build : Bool
  no_install : Bool
  no_launch : Bool
  debug : Bool
  filter : String?
  verify : Bool
  custom_args : Array[String]
}

///|
fn run_usage() -> String {
  (
    #|Usage: moui run   [options]
    #|
    #|Run a MoUI app. Mobile platforms build, install, and launch on a
    #|connected device or emulator. Desktop platforms build and launch the
    #|native executable in place; web serves the project through the dev
    #|runner.
    #|
    #|Arguments:
    #|  platform     Target platform: macos, windows, linux, web, android,
    #|               ios, or harmonyos.
    #|  app          Project directory holding the entrypoint packages (or
    #|               the entrypoint package itself) for desktop/web; the
    #|               application id matching moui.mobile.json for mobile.
    #|
    #|Options:
    #|  --mobile-config PATH    Path to moui.mobile.json. Defaults to ./moui.mobile.json.
    #|  --device ID             Target device id. Defaults to the first device.
    #|  --renderer NAME         Renderer override (auto / skia / wgpu).
    #|  --fallback-skia        Allow Skia fallback when primary renderer fails.
    #|  --prepare-only          Stop after preparing native build inputs.
    #|  --build-only            Stop after producing the build artifact.
    #|  --no-build              Skip the build step (use existing artifact).
    #|  --no-install            Skip the install step.
    #|  --no-launch             Skip the launch step.
    #|  --debug                 Follow platform logs after launch (logcat /
    #|                          hilog / oslog) until interrupted.
    #|  --filter FILTER         Logcat/hilog/oslog filter expression.
    #|  --verify                Run the M9.2 runtime probe after launch.
    #|  --             Extra arguments forwarded to the launched app.
    #|  --json                  Emit machine-readable output.
    #|  -h, --help              Show this help.
  )
}

///|
fn parse_run_args(
  args : Array[String],
  cwd : String,
  host : HostPlatform,
) -> Result[ParsedRun, String] {
  let _ = cwd
  let _ = host
  let help = Ref(false)
  let json = Ref(false)
  let positional : Array[String] = []
  let app_config : Ref[String?] = Ref(None)
  let device : Ref[String?] = Ref(None)
  let renderer : Ref[String?] = Ref(None)
  let fallback_skia = Ref(false)
  let prepare_only = Ref(false)
  let build_only = Ref(false)
  let no_build = Ref(false)
  let no_install = Ref(false)
  let no_launch = Ref(false)
  let debug = Ref(false)
  let filter : Ref[String?] = Ref(None)
  let verify = Ref(false)
  let custom_args : Array[String] = []
  let mut index = 0
  let mut passthrough = false
  while index < args.length() {
    let argument = args[index]
    if passthrough {
      custom_args.push(argument)
      index += 1
      continue
    }
    let (name, inline) = option_parts(argument)
    match name {
      "--json" =>
        match flag_without_value(inline, name) {
          Ok(_) => json.val = true
          Err(message) => return Err(message)
        }
      "-h" | "--help" =>
        match flag_without_value(inline, name) {
          Ok(_) => help.val = true
          Err(message) => return Err(message)
        }
      "--fallback-skia"
      | "--prepare-only"
      | "--build-only"
      | "--no-build"
      | "--no-install"
      | "--no-launch"
      | "--debug"
      | "--verify" =>
        match flag_without_value(inline, name) {
          Ok(_) =>
            match name {
              "--fallback-skia" => fallback_skia.val = true
              "--prepare-only" => prepare_only.val = true
              "--build-only" => build_only.val = true
              "--no-build" => no_build.val = true
              "--no-install" => no_install.val = true
              "--no-launch" => no_launch.val = true
              "--debug" => debug.val = true
              "--verify" => verify.val = true
              _ => ()
            }
          Err(message) => return Err(message)
        }
      "--mobile-config" | "--device" | "--renderer" | "--filter" =>
        match option_value(args, index, inline, name) {
          Ok((value, next)) => {
            match name {
              "--mobile-config" => app_config.val = Some(value)
              "--device" => device.val = Some(value)
              "--renderer" => renderer.val = Some(value)
              "--filter" => filter.val = Some(value)
              _ => ()
            }
            index = next
            continue
          }
          Err(message) => return Err(message)
        }
      "--" => {
        passthrough = true
        index += 1
        continue
      }
      _ if argument.has_prefix("-") =>
        return Err("unknown option for moui run: " + name)
      _ => positional.push(argument)
    }
    index += 1
  }
  if help.val {
    return Ok({
      help: true,
      json: json.val,
      platform: "",
      app: "",
      options: None,
    })
  }
  if positional.length() < 2 {
    return Err("usage: moui run   [options]")
  }
  if positional.length() > 2 {
    return Err("moui run accepts exactly two positional arguments")
  }
  let platform = positional[0]
  let app = positional[1]
  let is_mobile = platform == "android" ||
    platform == "ios" ||
    platform == "harmonyos"
  let is_desktop = desktop_platform_supported(platform)
  if !is_mobile && !is_desktop {
    return Err(
      "unsupported platform for moui run: " +
      platform +
      " (expected macos, windows, linux, web, android, ios, or harmonyos)",
    )
  }
  Ok({
    help: false,
    json: json.val,
    platform,
    app,
    options: Some({
      app_config_path: app_config.val,
      device: device.val,
      renderer: renderer.val,
      fallback_skia: fallback_skia.val,
      prepare_only: prepare_only.val,
      build_only: build_only.val,
      no_build: no_build.val,
      no_install: no_install.val,
      no_launch: no_launch.val,
      debug: debug.val,
      filter: filter.val,
      verify: verify.val,
      custom_args,
    }),
  })
}

///|
/// Apply config overrides to `RunOptions`. CLI-provided values win; otherwise
/// fill from the XDG-backed `CliConfig`. Returns a new `RunOptions`.
fn apply_config_overrides_run(
  options : RunOptions,
  config : CliConfig,
) -> RunOptions {
  let device = match options.device {
    Some(v) => Some(v)
    None => None
  }
  let renderer = options.renderer
  let app_config_path = options.app_config_path
  let _ = config
  { ..options, device, renderer, app_config_path, }
}

///|
/// Convert `RunOptions` (from `moui run`) into `BuildCommandOptions` so
/// `moui run`'s build step shares the same pipeline as `moui build`. Most
/// platform-specific overrides stay `None` — they default inside
/// `build_*_options_from_command` from `CliConfig`. `workspace_root` mirrors
/// `parse_build_args`'s `cwd` default — it MUST be the actual cwd (absolute),
/// not `"."`, because `mobile_entrypoint_package` calls `path_relative_to`
/// which only succeeds when `workspace_root` is the absolute prefix of the
/// resolved entrypoint path. With `"."` the relative resolution fails and
/// `entrypoint_package` falls back to an absolute path, which then causes
/// `moonbit_c_path` to drop the `build_dir` prefix and look for the
/// generated C at `//`.
///
/// `sdk_override` lets `moui run` inject the iOS SDK (`iphonesimulator` vs
/// `iphoneos`) based on the resolved target device. Without this, the
/// `build_command.mbt` default (`iphoneos`) is used even when targeting the
/// simulator, which causes `build_ios_core` to skip ad-hoc signing and the
/// bundle fails to launch on the simulator with `SBMainWorkspace` denial.
fn build_options_from_run_options(
  options : RunOptions,
  cwd : String,
  sdk_override : String?,
) -> BuildCommandOptions {
  {
    app_config_path: options.app_config_path,
    workspace_root: cwd,
    moui_root: "",
    skia_root: "",
    build_dir: "",
    output: None,
    renderer: options.renderer,
    fallback_skia: options.fallback_skia,
    prepare_only: options.prepare_only,
    release: false,
    product_name: None,
    abi: None,
    compile_sdk: None,
    target_sdk: None,
    sdk: sdk_override,
    arch: None,
    deployment_target: None,
    sdk_home: None,
    hvigorw: None,
    ohpm: None,
    signing_config_file: None,
  }
}

///|
/// Resolve the target device. If `--device` was supplied, use it; otherwise
/// pick the first available device from `list_all_devices`. Returns an error
/// when no device is available.
fn resolve_target_device(
  options : RunOptions,
  platform : String,
  host : HostPlatform,
) -> Result[Device, String] {
  match options.device {
    Some(id) =>
      Ok({ platform, id, name: id, state: "selected", kind: "unknown", })
    None => {
      let devices = list_all_devices(host)
      let matching : Array[Device] = []
      for device in devices {
        if device.platform == platform {
          matching.push(device)
        }
      }
      if matching.is_empty() {
        return Err(
          "no " +
          platform +
          " device found; connect a device or start an emulator",
        )
      }
      Ok(matching[0])
    }
  }
}

///|
/// Detect whether an iOS device id corresponds to a simulator. iOS simulator
/// UDIDs are UUIDs in the `8-4-4-4-12` hex format (e.g.
/// `33E45CC1-A8C6-4245-B820-CAFBC9AB6E5C`); physical device UDIDs are 25 or
/// 40 hex chars without dashes. Used by `moui run` to auto-select
/// `iphonesimulator` vs `iphoneos` when the user did not pass `--sdk` or set
/// `moui config ios.sdk`. Returns `true` iff the id contains at least one
/// `-`, contains at least one hex char, and matches the UUID character class.
fn is_ios_simulator_udid(id : String) -> Bool {
  if !id.contains("-") {
    return false
  }
  let mut has_hex = false
  for ch in id {
    if is_hex_char(ch) {
      has_hex = true
    } else if ch != '-' {
      return false
    }
  }
  has_hex
}

///|
/// Returns `true` iff `ch` is `[0-9a-fA-F]`. MoonBit's `Char` does not expose
/// `is_hex_digit`, so this is the inline equivalent.
fn is_hex_char(ch : Char) -> Bool {
  (ch >= '0' && ch <= '9') ||
  (ch >= 'a' && ch <= 'f') ||
  (ch >= 'A' && ch <= 'F')
}

///|
/// Resolve the iOS SDK to use for `moui run`. When the target device is a
/// simulator (UUID-format UDID), return `iphonesimulator`. Otherwise return
/// `None` so `build_command.mbt` falls back to its default (`iphoneos`) or
/// the user's `moui config ios.sdk` setting. Returns `None` for non-iOS
/// platforms so `build_options_from_run_options` doesn't override anything.
fn resolve_ios_sdk_override(platform : String, device : Device) -> String? {
  if platform == "ios" && is_ios_simulator_udid(device.id) {
    Some("iphonesimulator")
  } else {
    None
  }
}

///|
/// Locate the build artifact produced by the M6 build step for the given
/// platform. Returns the absolute path to the .apk / .app / .hap file.
fn find_build_artifact(
  platform : String,
  build_dir : String,
  app_id : String,
) -> String {
  match platform {
    "android" => join_path(build_dir, app_id + ".apk")
    "ios" => join_path(build_dir, app_id + ".app")
    "harmonyos" => join_path(build_dir, app_id + ".hap")
    _ => ""
  }
}

///|
/// Install the build artifact on the target device. Returns the install
/// command argv for logging.
fn install_on_device(
  platform : String,
  device : Device,
  artifact : String,
  host : HostPlatform,
) -> Result[Unit, String] {
  match platform {
    "android" => {
      let adb = match find_in_path("adb", host) {
        Some(path) => path
        None => return Err("adb not found on PATH")
      }
      match
        process_exec_argv(
          [adb, "-s", device.id, "install", "-r", artifact],
          capture=true,
        ) {
        Ok(result) if result.exit_code == 0 => Ok(())
        Ok(result) => Err("adb install failed: " + result.stderr)
        Err(message) => Err(message)
      }
    }
    "ios" => {
      let xcrun = match find_in_path("xcrun", host) {
        Some(path) => path
        None => return Err("xcrun not found on PATH")
      }
      match
        process_exec_argv(
          [xcrun, "simctl", "install", device.id, artifact],
          capture=true,
        ) {
        Ok(result) if result.exit_code == 0 => Ok(())
        Ok(result) => Err("simctl install failed: " + result.stderr)
        Err(message) => Err(message)
      }
    }
    "harmonyos" => {
      let hdc = match find_in_path("hdc", host) {
        Some(path) => path
        None => return Err("hdc not found on PATH")
      }
      match
        process_exec_argv(
          [hdc, "-t", device.id, "install", artifact],
          capture=true,
        ) {
        Ok(result) if result.exit_code == 0 &&
          !result.stdout.to_lower().contains("error") &&
          !result.stderr.to_lower().contains("error") &&
          !result.stdout.to_lower().contains("failed") &&
          !result.stderr.to_lower().contains("failed") => Ok(())
        Ok(result) => {
          let detail = if result.stderr != "" {
            result.stderr
          } else {
            result.stdout
          }
          Err("hdc install failed: " + detail)
        }
        Err(message) => Err(message)
      }
    }
    _ => Err("unsupported platform: " + platform)
  }
}

///|

///|
/// Launch an installed iOS app on a simulator via `xcrun simctl launch`.
fn ios_simctl_launch(
  xcrun : String,
  device_id : String,
  bundle_id : String,
  custom_args : Array[String],
) -> Result[Unit, String] {
  let argv = [xcrun, "simctl", "launch", device_id, bundle_id] + custom_args
  match process_exec_argv(argv, capture=true) {
    Ok(result) if result.exit_code == 0 => Ok(())
    Ok(result) => {
      let detail = if result.stderr != "" {
        result.stderr
      } else {
        result.stdout
      }
      Err("simctl launch failed: " + detail)
    }
    Err(message) => Err(message)
  }
}

///|
let android_window_hosted_activity = "dev.wzzc.window.template.HostedActivity"

///|
/// Launch the app on the target device. Returns the bundle id used for
/// launching (used by the JSON output).
fn launch_on_device(
  platform : String,
  device : Device,
  app : MobileApp,
  host : HostPlatform,
  custom_args : Array[String],
) -> Result[String, String] {
  match platform {
    "android" => {
      let bundle_id = match app.android {
        Some(android) => android.application_id
        None => return Err("moui.mobile.json missing android.applicationId")
      }
      let adb = match find_in_path("adb", host) {
        Some(path) => path
        None => return Err("adb not found on PATH")
      }
      let argv = [
        adb,
        "-s",
        device.id,
        "shell",
        "am",
        "start",
        "-W",
        "-n",
        bundle_id + "/" + android_window_hosted_activity,
      ]
      let full_argv = argv + custom_args
      match process_exec_argv(full_argv, capture=true) {
        Ok(result) if result.exit_code == 0 &&
          !result.stdout.to_lower().contains("error") &&
          !result.stderr.to_lower().contains("error") => Ok(bundle_id)
        Ok(result) => {
          let detail = if result.stderr != "" {
            result.stderr
          } else {
            result.stdout
          }
          Err("am start failed: " + detail)
        }
        Err(message) => Err(message)
      }
    }
    "ios" => {
      let bundle_id = match app.ios {
        Some(ios) => ios.bundle_id
        None => return Err("moui.mobile.json missing ios.bundleId")
      }
      let xcrun = match find_in_path("xcrun", host) {
        Some(path) => path
        None => return Err("xcrun not found on PATH")
      }
      // Best-effort: stop a previous instance so install/launch is not racing
      // an already-running process after reinstall.
      let _ = process_exec_argv(
        [xcrun, "simctl", "terminate", device.id, bundle_id],
        capture=true,
      )
      match ios_simctl_launch(xcrun, device.id, bundle_id, custom_args) {
        Ok(_) => Ok(bundle_id)
        Err(_) => {
          // SpringBoard sometimes needs a beat after `simctl install` before
          // the new bundle is registered. Retry once.
          let _ = process_exec_argv(["/bin/sleep", "1"], capture=true)
          match ios_simctl_launch(xcrun, device.id, bundle_id, custom_args) {
            Ok(_) => Ok(bundle_id)
            Err(second) => Err(second)
          }
        }
      }
    }
    "harmonyos" => {
      let harmonyos = match app.harmonyos {
        Some(value) => value
        None => return Err("moui.mobile.json missing harmonyos section")
      }
      let bundle_name = harmonyos.bundle_name
      // Module name is the HAP module id (default "entry"); ability name is
      // always EntryAbility in the managed template's mainElement.
      let module_name = match harmonyos.module_name {
        Some(value) => value
        None => "entry"
      }
      let ability_name = "EntryAbility"
      let hdc = match find_in_path("hdc", host) {
        Some(path) => path
        None => return Err("hdc not found on PATH")
      }
      // Best-effort stop any previous instance so reinstall/launch is clean.
      let _ = process_exec_argv(
        [hdc, "-t", device.id, "shell", "aa", "force-stop", bundle_name],
        capture=true,
      )
      match
        process_exec_argv(
          [
            hdc,
            "-t",
            device.id,
            "shell",
            "aa",
            "start",
            "-a",
            ability_name,
            "-b",
            bundle_name,
            "-m",
            module_name,
          ],
          capture=true,
        ) {
        Ok(result) if result.exit_code == 0 &&
          !result.stdout.to_lower().contains("error") &&
          !result.stderr.to_lower().contains("error") => Ok(bundle_name)
        Ok(result) => {
          let detail = if result.stderr != "" {
            result.stderr
          } else {
            result.stdout
          }
          Err("aa start failed: " + detail)
        }
        Err(message) => Err(message)
      }
    }
    _ => Err("unsupported platform: " + platform)
  }
}

///|
/// Follow platform logs until interrupted. Used by `--debug`. Returns when
/// the log process exits or times out.
fn follow_logs(
  platform : String,
  device : Device,
  filter : String?,
  host : HostPlatform,
) -> Result[Unit, String] {
  match platform {
    "android" => {
      let adb = match find_in_path("adb", host) {
        Some(path) => path
        None => return Err("adb not found on PATH")
      }
      let tag = match filter {
        Some(value) => value
        None => "MoUI:V"
      }
      let _ = process_exec_argv(
        [adb, "-s", device.id, "logcat", "-v", "brief", tag, "*:S"],
        capture=true,
        timeout_ms=0,
      )
      Ok(())
    }
    "ios" => {
      let predicate = match filter {
        Some(value) => "process == '" + value + "'"
        None => "processImagePath CONTAINS 'MoUI'"
      }
      let _ = process_exec_argv(
        ["log", "stream", "--predicate", predicate, "--info", "--debug"],
        capture=true,
        timeout_ms=0,
      )
      Ok(())
    }
    "harmonyos" => {
      let hdc = match find_in_path("hdc", host) {
        Some(path) => path
        None => return Err("hdc not found on PATH")
      }
      let _ = process_exec_argv(
        [hdc, "-t", device.id, "hilog", "-v"],
        capture=true,
        timeout_ms=0,
      )
      Ok(())
    }
    _ => Err("unsupported platform: " + platform)
  }
}

///|
/// Render the run result as JSON for the `--json` output mode.
fn run_result_to_json(
  platform : String,
  app_id : String,
  device : Device,
  bundle_id : String,
  launch_status : String,
  verify : ProbeResult?,
) -> Json {
  let fields : Map[String, Json] = Map([])
  fields["schemaVersion"] = Json::number(1.0)
  fields["ok"] = Json::boolean(launch_status == "launched")
  fields["platform"] = Json::string(platform)
  fields["appId"] = Json::string(app_id)
  fields["deviceId"] = Json::string(device.id)
  fields["bundleId"] = Json::string(bundle_id)
  fields["launchStatus"] = Json::string(launch_status)
  match verify {
    Some(result) => fields["probe"] = probe_result_to_json(result)
    None => ()
  }
  Json::object(fields)
}

///|
/// Render the run result as plain text for the default output mode.
fn run_result_to_text(
  platform : String,
  app_id : String,
  device : Device,
  bundle_id : String,
  launch_status : String,
  verify : ProbeResult?,
) -> String {
  let mut text = "[run] " + platform + " app=" + app_id + "\n"
  text += "  device: " + device.id + " (" + device.name + ")\n"
  text += "  bundle: " + bundle_id + "\n"
  text += "  status: " + launch_status + "\n"
  match verify {
    Some(result) => text + probe_result_to_text(result)
    None => text
  }
}

///|
fn run_run(parsed : ParsedRun, cwd : String, host : HostPlatform) -> CliOutcome {
  if parsed.help {
    return stdout_outcome(run_usage())
  }
  guard parsed.options is Some(options) else {
    return failed_outcome(
      "run command options are unavailable",
      parsed.json,
      run_usage(),
    )
  }
  if desktop_platform_supported(parsed.platform) {
    return run_run_desktop(parsed, cwd, host)
  }
  // 1. Resolve config + apply overrides.
  let config = match read_config(host) {
    Ok(c) => c
    Err(message) => return failed_outcome(message, parsed.json, run_usage())
  }
  let options = apply_config_overrides_run(options, config)
  // 2. Locate the mobile configuration from the supplied path or cwd.
  let app_result = match options.app_config_path {
    Some(path) => read_mobile_app_at(path)
    None => read_mobile_app(cwd)
  }
  let app = match app_result {
    Ok(a) => a
    Err(message) =>
      return failed_outcome(
        "could not read moui.mobile.json: " + message,
        parsed.json,
        run_usage(),
      )
  }
  // 3. Resolve device.
  let device = match resolve_target_device(options, parsed.platform, host) {
    Ok(d) => d
    Err(message) => return failed_outcome(message, parsed.json, run_usage())
  }
  // 4. Build (unless --no-build). Shares the same `build_*` pipeline as
  // `moui build` so defaults stay in sync.
  let predicted_artifact = find_build_artifact(
    parsed.platform,
    default_build_dir(parsed.platform, cwd),
    app.id,
  )
  let artifact = if options.no_build {
    predicted_artifact
  } else {
    let sdk_override = resolve_ios_sdk_override(parsed.platform, device)
    let build_options = build_options_from_run_options(
      options, cwd, sdk_override,
    )
    match
      run_build_internal(build_options, parsed.platform, host, app, config) {
      Ok(result) => {
        log_info("[run] build succeeded: " + result.output_artifact)
        if result.output_artifact != "" {
          result.output_artifact
        } else {
          predicted_artifact
        }
      }
      Err(message) =>
        return failed_outcome(
          "build failed: " + message,
          parsed.json,
          run_usage(),
        )
    }
  }
  if options.prepare_only {
    return stdout_outcome(
      run_result_to_text(
        parsed.platform,
        app.id,
        device,
        "",
        "prepare-only",
        None,
      ),
    )
  }
  if options.build_only {
    return stdout_outcome(
      run_result_to_text(
        parsed.platform,
        app.id,
        device,
        "",
        "build-only",
        None,
      ),
    )
  }
  // 5. Install (unless --no-install).
  // iOS bundles are directories (`*.app`); Android APKs / HarmonyOS HAPs are
  // files. Using only `path_is_file` silently skips iOS install and then
  // `simctl launch` fails with FBSOpenApplicationServiceErrorDomain code=4.
  if !options.no_install && (path_is_file(artifact) || path_is_dir(artifact)) {
    match install_on_device(parsed.platform, device, artifact, host) {
      Ok(_) => ()
      Err(message) => return failed_outcome(message, parsed.json, run_usage())
    }
  } else if !options.no_install {
    return failed_outcome(
      "build artifact not found for install: " + artifact,
      parsed.json,
      run_usage(),
    )
  }
  // 6. Launch (unless --no-launch).
  let (bundle_id, launch_status) = if options.no_launch {
    ("", "skipped")
  } else {
    match
      launch_on_device(parsed.platform, device, app, host, options.custom_args) {
      Ok(b) => (b, "launched")
      Err(message) => return failed_outcome(message, parsed.json, run_usage())
    }
  }
  // 7. Optional --verify probe.
  let verify_result = if options.verify {
    let result = match parsed.platform {
      "android" => probe_android(device, app.id, bundle_id, host, 30)
      "ios" => probe_ios(device, bundle_id, host, 30)
      "harmonyos" => {
        let module_name = match app.harmonyos {
          Some(h) =>
            match h.module_name {
              Some(value) => value
              None => "entry"
            }
          None => "entry"
        }
        probe_harmonyos(device, bundle_id, module_name, host, 30)
      }
      _ => { ok: false, observations: [], }
    }
    Some(result)
  } else {
    None
  }
  // 8. Optional --debug log following (blocking until interrupted).
  if options.debug {
    let _ = follow_logs(parsed.platform, device, options.filter, host)
  }
  // 9. Emit output.
  if parsed.json {
    let json = run_result_to_json(
      parsed.platform,
      app.id,
      device,
      bundle_id,
      launch_status,
      verify_result,
    )
    stdout_outcome(json.stringify(indent=2) + "\n")
  } else {
    stdout_outcome(
      run_result_to_text(
        parsed.platform,
        app.id,
        device,
        bundle_id,
        launch_status,
        verify_result,
      ),
    )
  }
}