///|
pub fn parse_scenario_manifest(text : String) -> ScenarioManifest {
  let mut name = ""
  let mut version = ""
  let specs : Array[ScenarioSpec] = []
  let issues : Array[ScenarioIssue] = []
  let lines = split_lines(text)
  let mut header_seen = false
  let mut line_index = 0
  while line_index < lines.length() {
    let line_number = line_index + 1
    let line = trim_ascii(lines[line_index])
    if line.length() == 0 || starts_with(line, "#") {
      line_index += 1
      continue
    }
    let tokens = split_words_quoted(line)
    if tokens.length() == 0 {
      line_index += 1
      continue
    }
    match tokens[0] {
      "matrix" => {
        header_seen = true
        match token_value(tokens, "name") {
          Some(value) => name = value
          None =>
            issues.push(
              parser_issue(
                "matrix-name-missing", line_number, "matrix name is required",
              ),
            )
        }
        match token_value(tokens, "version") {
          Some(value) => version = value
          None =>
            issues.push(
              parser_issue(
                "matrix-version-missing", line_number, "matrix version is required",
              ),
            )
        }
      }
      "scenario" => {
        let parsed = parse_scenario_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.spec {
          None => ()
          Some(spec) => specs.push(spec)
        }
      }
      _ =>
        issues.push(
          parser_issue(
            "unknown-directive", line_number, "only matrix and scenario directives are supported",
          ),
        )
    }
    line_index += 1
  }
  if !header_seen {
    issues.push(
      scenario_issue(
        "matrix-header-missing",
        Error,
        "",
        "",
        "scenario manifest has no matrix header",
        "Add: matrix name=examples version=0.3.0",
      ),
    )
  }
  if specs.length() == 0 {
    issues.push(
      scenario_issue(
        "scenario-empty",
        Error,
        "",
        "",
        "scenario manifest contains no valid scenarios",
        "Add at least one scenario line with an id, command and target.",
      ),
    )
  }
  ScenarioManifest::{ name, version, specs, issues }
}

///|
fn parse_scenario_tokens(
  tokens : Array[String],
  line_number : Int,
) -> ParsedScenario {
  let issues : Array[ScenarioIssue] = []
  let line_label = "line:" + line_number.to_string()
  let id = token_or_empty(tokens, "id")
  let title = token_or_default(tokens, "title", id)
  let command = token_or_empty(tokens, "command")
  let expected_value = token_or_empty(tokens, "expected")
  let output = token_or_default(tokens, "output", "")
  let timeout = parse_int(token_or_default(tokens, "timeout_ms", "30000"), -1)
  let required = parse_bool(token_or_default(tokens, "required", "true"), false)
  let tags = split_csv(token_or_default(tokens, "tags", ""))
  let targets : Array[ScenarioTarget] = []
  let targets_valid = match token_value(tokens, "targets") {
    None => {
      issues.push(
        scenario_issue(
          "scenario-targets-missing",
          Error,
          id,
          line_label,
          "scenario has no target list",
          "Use targets=native or targets=native,js.",
        ),
      )
      false
    }
    Some(value) => {
      let names = split_csv(value)
      let mut valid = names.length() > 0
      let mut index = 0
      while index < names.length() {
        match scenario_target(names[index]) {
          None => {
            valid = false
            issues.push(
              scenario_issue(
                "scenario-target-invalid",
                Error,
                id,
                names[index],
                "scenario target is not a supported MoonBit target",
                "Use native, js, wasm or wasm-gc.",
              ),
            )
          }
          Some(target) =>
            if contains_target(targets, target) {
              valid = false
              issues.push(
                scenario_issue(
                  "scenario-target-duplicate",
                  Warning,
                  id,
                  target.name(),
                  "scenario lists the same target more than once",
                  "Keep one entry per target.",
                ),
              )
            } else {
              targets.push(target)
            }
        }
        index += 1
      }
      valid
    }
  }
  if id.length() == 0 || !is_valid_identifier(id) {
    issues.push(
      scenario_issue(
        "scenario-id-invalid",
        Error,
        id,
        line_label,
        "scenario id must be a non-empty identifier",
        "Use lowercase letters, digits, hyphens or underscores.",
      ),
    )
  }
  if command.length() == 0 {
    issues.push(
      scenario_issue(
        "scenario-command-missing",
        Error,
        id,
        line_label,
        "scenario command is empty",
        "Declare the exact command a maintainer can run to reproduce the case.",
      ),
    )
  }
  let expected = match expected_status(expected_value) {
    None => {
      issues.push(
        scenario_issue(
          "scenario-expected-invalid",
          Error,
          id,
          line_label,
          "scenario expected value must be success or failure",
          "Use expected=success or expected=failure.",
        ),
      )
      Success
    }
    Some(value) => value
  }
  if expected_value.length() == 0 {
    issues.push(
      scenario_issue(
        "scenario-expected-missing",
        Error,
        id,
        line_label,
        "scenario expected value is missing",
        "Declare whether the command should succeed or fail.",
      ),
    )
  }
  if timeout <= 0 {
    issues.push(
      scenario_issue(
        "scenario-timeout-invalid",
        Error,
        id,
        line_label,
        "scenario timeout must be positive milliseconds",
        "Use a bounded timeout such as timeout_ms=30000.",
      ),
    )
  }
  if !targets_valid {
    issues.push(
      scenario_issue(
        "scenario-targets-invalid",
        Error,
        id,
        line_label,
        "scenario target list is invalid",
        "Fix the target list before producing a matrix.",
      ),
    )
  }
  let has_blocking_issue = has_error_issue(issues)
  let spec : ScenarioSpec? = if has_blocking_issue {
    None
  } else {
    Some(ScenarioSpec::{
      id,
      title,
      command,
      targets,
      expected,
      expected_output: output,
      timeout_ms: timeout,
      required,
      tags,
    })
  }
  ParsedScenario::{ spec, issues }
}

///|
fn token_or_empty(tokens : Array[String], key : String) -> String {
  match token_value(tokens, key) {
    None => ""
    Some(value) => value
  }
}

///|
fn token_or_default(
  tokens : Array[String],
  key : String,
  fallback : String,
) -> String {
  match token_value(tokens, key) {
    None => fallback
    Some(value) => value
  }
}

///|
fn parser_issue(
  code : String,
  line_number : Int,
  message : String,
) -> ScenarioIssue {
  scenario_issue(
    code,
    Error,
    "",
    "line:" + line_number.to_string(),
    message,
    "Check the matrix header syntax and required fields.",
  )
}

///|
fn has_error_issue(items : Array[ScenarioIssue]) -> Bool {
  let mut index = 0
  while index < items.length() {
    if items[index].severity == Error {
      return true
    }
    index += 1
  }
  false
}