///|
/// Prepared run metadata and lifecycle shared by both entry points.
#internal(internal, "runner scheduling adapter; no stability guarantee")
#doc(hidden)
pub struct PreparedRun {
  priv cache : FeatureCache
  priv filtered : Array[@cucumber_messages.Pickle]
  priv locations : Map[String, String]
  priv sinks : Array[&@core.MessageSink]
  priv tc_mappings : Map[String, TestCaseMapping]
  priv setup : @core.Setup
  priv id_gen : IdGenerator
  priv run_id : String
  priv all_parse_errors : Array[ParseErrorInfo]
}

///|
#internal(internal, "runner scheduling adapter; no stability guarantee")
#doc(hidden)
pub fn[W : @core.World] prepare_run(
  factory : () -> W,
  options : RunOptions,
) -> PreparedRun raise Error {
  let features = options.features()
  let sinks = options.get_sinks()
  let tag_expr = options.get_tag_expr()
  let scenario_name = options.get_scenario_name()
  let cache = FeatureCache::new()
  if sinks.length() > 0 {
    emit(sinks, make_meta_envelope())
  }
  let all_parse_errors : Array[ParseErrorInfo] = []
  for source in features {
    if sinks.length() > 0 {
      let (uri, data) = match source {
        FeatureSource::Text(path, content) => (path, content)
        FeatureSource::File(path) => {
          let content = @fs.read_file_to_string(path) catch { _ => "" }
          (path, content)
        }
        FeatureSource::Parsed(path, _) => (path, "")
      }
      emit(sinks, make_source_envelope(uri, data))
    }
    let errors = cache.load_from_source(source)
    all_parse_errors.append(errors)
  }
  // Emit GherkinDocument envelopes
  if sinks.length() > 0 {
    for entry in cache.features() {
      let uri = entry.0
      let json : Json = {
        "gherkinDocument": {
          "uri": Json(uri),
          "comments": Json(([] : Array[Int32])),
        },
      }
      let envelope : @cucumber_messages.Envelope = @json.from_json(json) catch {
        _ => continue
      }
      emit(sinks, envelope)
    }
  }
  // Emit ParseError envelopes
  if sinks.length() > 0 {
    for pe in all_parse_errors {
      let source_ref : Map[String, Json] = Map([])
      source_ref["uri"] = Json(pe.uri)
      match pe.line {
        Some(line) => source_ref["location"] = { "line": Json(line) }
        None => ()
      }
      let json : Json = {
        "parseError": {
          "source": Json(source_ref),
          "message": Json(pe.message),
        },
      }
      let envelope : @cucumber_messages.Envelope = @json.from_json(json) catch {
        _ => continue
      }
      emit(sinks, envelope)
    }
  }
  let pickles = compile_pickles(cache)
  let locations : Map[String, String] = Map([])
  for pickle in pickles {
    if pickle_line(cache, pickle) is Some(line) {
      locations[pickle.id] = pickle.uri + ":" + line.to_string()
    }
  }
  // Emit Pickle envelopes
  if sinks.length() > 0 {
    for pickle in pickles {
      let json : Json = { "pickle": pickle.to_json() }
      let envelope : @cucumber_messages.Envelope = @json.from_json(json) catch {
        _ => continue
      }
      emit(sinks, envelope)
    }
  }
  let filter = PickleFilter::new()
  let filter = if tag_expr != "" { filter.with_tags(tag_expr) } else { filter }
  let filter = if scenario_name != "" {
    filter.with_names([scenario_name])
  } else {
    filter
  }
  let filtered = filter.apply(pickles)
  // Build shared registry for envelope metadata
  let setup = @core.Setup::new()
  let world0 = factory()
  @core.World::configure(world0, setup)
  let registry = setup.step_registry()
  let custom_param_types = setup.custom_param_types()
  // Emit StepDefinition envelopes
  let id_gen = IdGenerator::new()
  if sinks.length() > 0 {
    let sd_envelopes = build_step_definition_envelopes(registry)
    for env in sd_envelopes {
      emit(sinks, env)
    }
  }
  // Emit ParameterType envelopes for custom parameter types
  if sinks.length() > 0 {
    for cpt in custom_param_types {
      let regex_json : Array[Json] = cpt.patterns.map(fn(p) { Json(p) })
      let json : Json = {
        "parameterType": {
          "id": Json(id_gen.next_param_type_id()),
          "name": Json(cpt.name),
          "regularExpressions": Json(regex_json),
          "preferForRegularExpressionMatch": Json(false),
          "useForSnippets": Json(true),
        },
      }
      let envelope : @cucumber_messages.Envelope = @json.from_json(json) catch {
        _ => continue
      }
      emit(sinks, envelope)
    }
  }
  // Emit Hook envelopes
  if sinks.length() > 0 {
    let hook_reg = setup.hook_registry()
    for hook in hook_reg.hooks() {
      let source_ref : Map[String, Json] = Map([])
      match hook.source {
        Some(src) => {
          match src.uri {
            Some(uri) => source_ref["uri"] = Json(uri)
            None => ()
          }
          match src.line {
            Some(line) => source_ref["location"] = { "line": Json(line) }
            None => ()
          }
        }
        None => ()
      }
      let hook_type_str = match hook.type_ {
        @core.HookType::BeforeTestRun => "BEFORE_TEST_RUN"
        @core.HookType::AfterTestRun => "AFTER_TEST_RUN"
        @core.HookType::BeforeTestCase => "BEFORE_TEST_CASE"
        @core.HookType::AfterTestCase => "AFTER_TEST_CASE"
        @core.HookType::BeforeTestStep => "BEFORE_TEST_STEP"
        @core.HookType::AfterTestStep => "AFTER_TEST_STEP"
      }
      let json : Json = {
        "hook": {
          "id": Json(hook.id),
          "sourceReference": Json(source_ref),
          "type": Json(hook_type_str),
        },
      }
      let envelope : @cucumber_messages.Envelope = @json.from_json(json) catch {
        _ => continue
      }
      emit(sinks, envelope)
    }
  }
  // Test planning phase
  let tc_mappings : Map[String, TestCaseMapping] = if sinks.length() > 0 {
    let tc_envelopes = build_test_cases(
      registry,
      setup.hook_registry(),
      filtered,
      id_gen,
    )
    for env in tc_envelopes {
      emit(sinks, env)
    }
    extract_test_case_mappings(tc_envelopes)
  } else {
    {}
  }
  // Emit TestRunStarted
  let run_id = id_gen.next("tr")
  if sinks.length() > 0 {
    emit(sinks, make_test_run_started_envelope(run_id))
  }
  // Execute before_test_run hooks
  let before_run_hooks = setup
    .hook_registry()
    .by_type(@core.HookType::BeforeTestRun)
  for hook in before_run_hooks {
    let trhs_id = id_gen.next("trhs")
    if sinks.length() > 0 {
      let json : Json = {
        "testRunHookStarted": {
          "id": Json(trhs_id),
          "testRunStartedId": Json(run_id),
          "hookId": Json(hook.id),
          "timestamp": {
            "seconds": Json((0 : Int32)),
            "nanos": Json((0 : Int32)),
          },
        },
      }
      let envelope : @cucumber_messages.Envelope = @json.from_json(json) catch {
        _ => continue
      }
      emit(sinks, envelope)
    }
    let hook_ctx = @core.RunHookCtx::new()
    let result_status = try {
      match hook.handler {
        @core.HookHandler::RunHandler(h) => h(hook_ctx)
        _ => ()
      }
      "PASSED"
    } catch {
      _ => "FAILED"
    }
    if sinks.length() > 0 && hook_ctx.pending_attachments().length() > 0 {
      emit_run_hook_attachments(sinks, hook_ctx.pending_attachments(), trhs_id)
      hook_ctx.pending_attachments().clear()
    }
    if sinks.length() > 0 {
      let json : Json = {
        "testRunHookFinished": {
          "testRunHookStartedId": Json(trhs_id),
          "result": {
            "duration": {
              "seconds": Json((0 : Int32)),
              "nanos": Json((0 : Int32)),
            },
            "status": Json(result_status),
          },
          "timestamp": {
            "seconds": Json((0 : Int32)),
            "nanos": Json((0 : Int32)),
          },
        },
      }
      let envelope : @cucumber_messages.Envelope = @json.from_json(json) catch {
        _ => continue
      }
      emit(sinks, envelope)
    }
  }
  {
    cache,
    filtered,
    locations,
    sinks,
    tc_mappings,
    setup,
    id_gen,
    run_id,
    all_parse_errors,
  }
}

///|
fn finish_run(
  prepared : PreparedRun,
  options : RunOptions,
  paired_results : Array[PickleResult],
  strict_output~ : Bool = false,
) -> (RunResult, Map[String, String]) raise SyncExecutionError {
  let cache = prepared.cache
  let locations = prepared.locations
  let sinks = prepared.sinks
  let setup = prepared.setup
  let id_gen = prepared.id_gen
  let run_id = prepared.run_id
  let all_parse_errors = prepared.all_parse_errors
  let results : Array[ScenarioResult] = paired_results.map(fn(pr) { pr.result })
  let retried_count = paired_results
    .iter()
    .fold(init=0, fn(acc, pr) { if pr.was_retried { acc + 1 } else { acc } })
  // Execute after_test_run hooks
  let run_errors : Array[@core.HookError] = []
  for r in results {
    if r.status != ScenarioStatus::Passed {
      let msg = match
        r.steps
        .iter()
        .find_first(fn(s) {
          match s.status {
            StepStatus::Failed(_) => true
            _ => false
          }
        }) {
        Some(s) =>
          match s.status {
            StepStatus::Failed(m) => m
            _ => "scenario failed"
          }
        None => "scenario failed"
      }
      run_errors.push(
        @core.HookError::ScenarioFailed(
          feature_name=r.feature_name,
          scenario_name=r.scenario_name,
          message=msg,
        ),
      )
    }
  }
  let run_hook_result : @core.HookResult = if run_errors.length() > 0 {
    @core.HookResult::Failed(run_errors)
  } else {
    @core.HookResult::Passed
  }
  let after_run_hooks = setup
    .hook_registry()
    .by_type(@core.HookType::AfterTestRun)
  for hook in after_run_hooks {
    let trhs_id = id_gen.next("trhs")
    if sinks.length() > 0 {
      let json : Json = {
        "testRunHookStarted": {
          "id": Json(trhs_id),
          "testRunStartedId": Json(run_id),
          "hookId": Json(hook.id),
          "timestamp": {
            "seconds": Json((0 : Int32)),
            "nanos": Json((0 : Int32)),
          },
        },
      }
      let envelope : @cucumber_messages.Envelope = @json.from_json(json) catch {
        _ => continue
      }
      emit(sinks, envelope)
    }
    let hook_ctx = @core.RunHookCtx::new()
    let result_status = try {
      match hook.handler {
        @core.HookHandler::RunAfterHandler(h) => h(hook_ctx, run_hook_result)
        _ => ()
      }
      "PASSED"
    } catch {
      _ => "FAILED"
    }
    if sinks.length() > 0 && hook_ctx.pending_attachments().length() > 0 {
      emit_run_hook_attachments(sinks, hook_ctx.pending_attachments(), trhs_id)
      hook_ctx.pending_attachments().clear()
    }
    if sinks.length() > 0 {
      let json : Json = {
        "testRunHookFinished": {
          "testRunHookStartedId": Json(trhs_id),
          "result": {
            "duration": {
              "seconds": Json((0 : Int32)),
              "nanos": Json((0 : Int32)),
            },
            "status": Json(result_status),
          },
          "timestamp": {
            "seconds": Json((0 : Int32)),
            "nanos": Json((0 : Int32)),
          },
        },
      }
      let envelope : @cucumber_messages.Envelope = @json.from_json(json) catch {
        _ => continue
      }
      emit(sinks, envelope)
    }
  }
  // Emit TestRunFinished
  if sinks.length() > 0 {
    let all_passed = results
      .iter()
      .all(fn(r) { r.status == ScenarioStatus::Passed })
    emit(sinks, make_test_run_finished_envelope(all_passed, run_id))
  }
  let feature_results = group_by_feature(results, cache)
  let summary = compute_summary(feature_results, retried=retried_count)
  // Write formatter output to configured destinations
  for entry in options.get_formatters() {
    write_formatter_output(entry, strict=strict_output)
  }
  (
    { features: feature_results, summary, parse_errors: all_parse_errors, },
    locations,
  )
}

///|
fn[W : @core.World] run_prepared_sync(
  factory : () -> W,
  prepared : PreparedRun,
  options : RunOptions,
) -> Array[PickleResult] raise Error {
  prepared.tasks(factory, options).map(fn(task) raise { task() })
}

///|
/// Build scenario tasks using one snapshot of the execution options.
#internal(internal, "runner scheduling adapter; no stability guarantee")
#doc(hidden)
pub fn[W : @core.World] PreparedRun::tasks(
  self : PreparedRun,
  factory : () -> W,
  options : RunOptions,
) -> Array[() -> PickleResult raise Error] {
  let retries = options.get_retries()
  let dry_run = options.is_dry_run()
  let skip_tags = options.get_skip_tags().copy()
  self.filtered.map(fn(pickle) {
    fn() raise {
      execute_pickle(
        factory,
        pickle,
        sinks=self.sinks,
        tc_mappings=self.tc_mappings,
        id_gen=self.id_gen,
        retries~,
        dry_run~,
        skip_tags~,
      )
    }
  })
}

///|
#internal(internal, "runner scheduling adapter; no stability guarantee")
#doc(hidden)
pub fn PreparedRun::finish(
  self : PreparedRun,
  options : RunOptions,
  results : Array[PickleResult],
  strict_output~ : Bool = false,
) -> RunResult raise Error {
  finish_run(self, options, results, strict_output~).0
}

///|
#internal(internal, "runner scheduling adapter; no stability guarantee")
#doc(hidden)
pub fn PreparedRun::check(
  self : PreparedRun,
  result : RunResult,
  options : RunOptions,
) -> Unit raise Error {
  check_run_result(result, self.locations, options)
}

///|
/// Run features sequentially without an event loop.
/// Rejects parallel execution before effects. Formatter file and stderr write
/// failures raise `SyncExecutionError::HostIoFailed`.
pub fn[W : @core.World] run_sync(
  factory : () -> W,
  options : RunOptions,
) -> RunResult raise Error {
  run_sync_detailed(factory, options).0
}

///|
fn[W : @core.World] run_sync_detailed(
  factory : () -> W,
  options : RunOptions,
) -> (RunResult, Map[String, String]) raise Error {
  if options.is_parallel() {
    raise SyncExecutionError::ParallelNotSupported
  }
  let prepared = prepare_run(factory, options)
  finish_run(
    prepared,
    options,
    run_prepared_sync(factory, prepared, options),
    strict_output=true,
  )
}

///|
/// Run features synchronously, raising a structured report for any failure.
pub fn[W : @core.World] run_or_fail_sync(
  factory : () -> W,
  options : RunOptions,
) -> Unit raise Error {
  let (result, locations) = run_sync_detailed(factory, options)
  check_run_result(result, locations, options)
}

///|
fn check_run_result(
  result : RunResult,
  locations : Map[String, String],
  options : RunOptions,
) -> Unit raise Error {
  if result.parse_errors.length() > 0 ||
    result.summary.failed > 0 ||
    result.summary.undefined > 0 ||
    result.summary.pending > 0 {
    let errors : Array[@core.MoonspecError] = result.parse_errors.map(fn(pe) {
      @core.parse_failed_error(uri=pe.uri, line=pe.line, message=pe.message)
    })
    errors.append(collect_scenario_errors(result, locations))
    let summary = format_run_summary(
      result.summary,
      parse_errors=result.parse_errors.length(),
    )
    raise @core.run_failed_error(
      summary~,
      errors~,
      color=@core.color_enabled(no_color=options.get_no_color()),
    )
  }
}