///|
pub fn parse_observation_records(text : String) -> ObservationParseResult {
  let observations : Array[ScenarioObservation] = []
  let issues : Array[ScenarioIssue] = []
  let lines = split_lines(text)
  let mut index = 0
  while index < lines.length() {
    let line_number = index + 1
    let line = trim_ascii(lines[index])
    if line.length() == 0 || starts_with(line, "#") {
      index += 1
      continue
    }
    let tokens = split_words_quoted(line)
    if tokens.length() == 0 || tokens[0] != "observation" {
      issues.push(
        scenario_issue(
          "observation-directive-invalid",
          Error,
          "",
          "line:" + line_number.to_string(),
          "observation record must start with the observation directive",
          "Use observation id=... target=... status=... .",
        ),
      )
      index += 1
      continue
    }
    let parsed = parse_observation_tokens(tokens, line_number)
    let mut issue_index = 0
    while issue_index < parsed.issues.length() {
      issues.push(parsed.issues[issue_index])
      issue_index += 1
    }
    match parsed.observation {
      None => ()
      Some(observation) => observations.push(observation)
    }
    index += 1
  }
  ObservationParseResult::{ observations, issues }
}

///|
pub fn observations_to_text(
  observations : Array[ScenarioObservation],
) -> String {
  let mut out = "# CakeCheck Scenario Matrix observations\n"
  let mut index = 0
  while index < observations.length() {
    out = out + observation_to_text(observations[index]) + "\n"
    index += 1
  }
  out
}

///|
pub fn observation_to_text(observation : ScenarioObservation) -> String {
  "observation id=" +
  observation.id +
  " target=" +
  observation.target.name() +
  " status=" +
  observation.status.name() +
  " duration_ms=" +
  observation.duration_ms.to_string() +
  " stdout=" +
  quote_field(observation.stdout) +
  " stderr=" +
  quote_field(observation.stderr) +
  " digest=" +
  quote_field(observation.evidence_digest)
}

///|
pub fn observations_to_json(
  observations : Array[ScenarioObservation],
) -> String {
  let mut out = "["
  let mut index = 0
  while index < observations.length() {
    if index > 0 {
      out = out + ","
    }
    out = out + observation_to_json(observations[index])
    index += 1
  }
  out + "]"
}

///|
pub fn observation_to_json(observation : ScenarioObservation) -> String {
  let mut out = "{\"id\":\"" + json_escape(observation.id) + "\","
  out = out + "\"target\":\"" + observation.target.name() + "\","
  out = out + "\"status\":\"" + observation.status.name() + "\","
  out = out + "\"duration_ms\":" + observation.duration_ms.to_string() + ","
  out = out + "\"stdout\":\"" + json_escape(observation.stdout) + "\","
  out = out + "\"stderr\":\"" + json_escape(observation.stderr) + "\","
  out + "\"digest\":\"" + json_escape(observation.evidence_digest) + "\"}"
}

///|
pub fn observation_key(observation : ScenarioObservation) -> String {
  observation.id + "@" + observation.target.name()
}

///|
pub fn observation_parse_summary(result : ObservationParseResult) -> String {
  "observations=" +
  result.observations.length().to_string() +
  ", issues=" +
  result.issues.length().to_string() +
  ", valid=" +
  bool_text(result.issues.length() == 0)
}

///|
fn parse_observation_tokens(
  tokens : Array[String],
  line_number : Int,
) -> ParsedObservation {
  let issues : Array[ScenarioIssue] = []
  let id = token_or_empty(tokens, "id")
  let target_text = token_or_empty(tokens, "target")
  let status_text = token_or_empty(tokens, "status")
  let duration_text = token_or_empty(tokens, "duration_ms")
  let stdout = token_or_default(tokens, "stdout", "")
  let stderr = token_or_default(tokens, "stderr", "")
  let digest = token_or_default(tokens, "digest", "")
  let target = match scenario_target(target_text) {
    None => {
      issues.push(
        scenario_issue(
          "observation-target-invalid",
          Error,
          id,
          target_text,
          "observation target is invalid",
          "Use native, js, wasm or wasm-gc.",
        ),
      )
      Native
    }
    Some(value) => value
  }
  let status = match parse_observation_status(status_text) {
    None => {
      issues.push(
        scenario_issue(
          "observation-status-invalid",
          Error,
          id,
          target_text,
          "observation status is invalid",
          "Use passed, failed or skipped.",
        ),
      )
      Skipped
    }
    Some(value) => value
  }
  let duration = parse_int(duration_text, -1)
  if id.length() == 0 {
    issues.push(
      scenario_issue(
        "observation-id-missing",
        Error,
        id,
        "line:" + line_number.to_string(),
        "observation id is missing",
        "Use the id from the corresponding scenario declaration.",
      ),
    )
  }
  if target_text.length() == 0 {
    issues.push(
      scenario_issue(
        "observation-target-missing",
        Error,
        id,
        "line:" + line_number.to_string(),
        "observation target is missing",
        "Declare the MoonBit target that produced the output.",
      ),
    )
  }
  if status_text.length() == 0 {
    issues.push(
      scenario_issue(
        "observation-status-missing",
        Error,
        id,
        target_text,
        "observation status is missing",
        "Record passed, failed or skipped.",
      ),
    )
  }
  if duration < 0 {
    issues.push(
      scenario_issue(
        "observation-duration-invalid",
        Error,
        id,
        target_text,
        "observation duration must be a non-negative integer",
        "Record elapsed milliseconds from the runner.",
      ),
    )
  }
  let observation = if has_error_issue(issues) {
    None
  } else {
    Some(ScenarioObservation::{
      id,
      target,
      status,
      stdout,
      stderr,
      duration_ms: duration,
      evidence_digest: digest,
    })
  }
  ParsedObservation::{ observation, issues }
}

///|
fn parse_observation_status(value : String) -> ObservationStatus? {
  match ascii_lower(trim_ascii(value)) {
    "passed" => Some(Passed)
    "pass" => Some(Passed)
    "failed" => Some(Failed)
    "fail" => Some(Failed)
    "skipped" => Some(Skipped)
    "skip" => Some(Skipped)
    _ => None
  }
}

///|
fn quote_field(value : String) -> String {
  let mut out = "\""
  let mut index = 0
  while index < value.length() {
    let code = code_at(value, index)
    if code == 34 || code == 92 {
      out = out + "\\"
    }
    out = out + slice(value, index, index + 1)
    index += 1
  }
  out + "\""
}