///|
/// Render a generated suite as CSV with a header row in model order.
pub fn Suite::to_csv(self : Suite, model : Model) -> String {
  cases_to_csv(model, self.cases)
}

///|
/// Render a generated suite as a compact JSON document.
pub fn Suite::to_json(self : Suite, model : Model) -> String {
  let builder = StringBuilder::new()
  let stats = self.stats
  builder.write_string("{\n")
  builder.write_string("  \"strength\": ")
  builder.write_string(self.strength.to_string())
  builder.write_string(",\n")
  builder.write_string("  \"stats\": {\n")
  write_json_int64_field(builder, "cartesian_size", stats.cartesian_size, true)
  write_json_int_field(
    builder,
    "examined_candidates",
    stats.examined_candidates,
    true,
  )
  write_json_int_field(
    builder,
    "feasible_candidates",
    stats.feasible_candidates,
    true,
  )
  write_json_int_field(
    builder,
    "required_interactions",
    stats.required_interactions,
    true,
  )
  write_json_int_field(builder, "selected_cases", stats.selected_cases, true)
  write_json_int_field(
    builder,
    "removed_redundant_cases",
    stats.removed_redundant_cases,
    false,
  )
  builder.write_string("  },\n")
  builder.write_string("  \"parameters\": [\n")
  for i = 0; i < model.parameters.length(); i = i + 1 {
    let parameter = model.parameters[i]
    builder.write_string("    {\"name\": ")
    builder.write_string(json_string(parameter.name))
    builder.write_string(", \"values\": [")
    for j = 0; j < parameter.values.length(); j = j + 1 {
      if j > 0 {
        builder.write_string(", ")
      }
      builder.write_string(json_string(parameter.values[j]))
    }
    builder.write_string("]}")
    if i + 1 < model.parameters.length() {
      builder.write_string(",")
    }
    builder.write_string("\n")
  }
  builder.write_string("  ],\n")
  builder.write_string("  \"cases\": [\n")
  for case_index = 0
      case_index < self.cases.length()
      case_index = case_index + 1 {
    write_json_case(builder, model, self.cases[case_index], "    ")
    if case_index + 1 < self.cases.length() {
      builder.write_string(",")
    }
    builder.write_string("\n")
  }
  builder.write_string("  ]\n")
  builder.write_string("}\n")
  builder.to_string()
}

///|
/// Render a generated suite as a Markdown table for READMEs and issue reports.
pub fn Suite::to_markdown(self : Suite, model : Model) -> String {
  cases_to_markdown(model, self.cases)
}

///|
/// Render arbitrary cases as CSV. This is useful when callers load a suite from
/// another source and want the same escaping rules as generated suites.
pub fn cases_to_csv(model : Model, cases : Array[TestCase]) -> String {
  let builder = StringBuilder::new()
  for parameter_index = 0
      parameter_index < model.parameters.length()
      parameter_index = parameter_index + 1 {
    if parameter_index > 0 {
      builder.write_char(',')
    }
    builder.write_string(csv_field(model.parameters[parameter_index].name))
  }
  builder.write_char('\n')
  for test_case in cases {
    for value_index = 0
        value_index < test_case.values.length()
        value_index = value_index + 1 {
      if value_index > 0 {
        builder.write_char(',')
      }
      builder.write_string(csv_field(test_case.values[value_index]))
    }
    builder.write_char('\n')
  }
  builder.to_string()
}

///|
/// Render arbitrary cases as a Markdown table.
pub fn cases_to_markdown(model : Model, cases : Array[TestCase]) -> String {
  let builder = StringBuilder::new()
  builder.write_string("| # |")
  for parameter in model.parameters {
    builder.write_char(' ')
    builder.write_string(markdown_cell(parameter.name))
    builder.write_string(" |")
  }
  builder.write_char('\n')
  builder.write_string("| --- |")
  for _ in model.parameters {
    builder.write_string(" --- |")
  }
  builder.write_char('\n')
  for case_index = 0; case_index < cases.length(); case_index = case_index + 1 {
    builder.write_string("| ")
    builder.write_string((case_index + 1).to_string())
    builder.write_string(" |")
    let test_case = cases[case_index]
    for value in test_case.values {
      builder.write_char(' ')
      builder.write_string(markdown_cell(value))
      builder.write_string(" |")
    }
    builder.write_char('\n')
  }
  builder.to_string()
}

///|
/// One-line generation summary suitable for CLI output.
pub fn Suite::summary(self : Suite) -> String {
  let stats = self.stats
  "strength=\{self.strength}, cases=\{self.cases.length()}, interactions=\{stats.required_interactions}, candidates=\{stats.feasible_candidates}/\{stats.cartesian_size}, removed=\{stats.removed_redundant_cases}"
}

///|
/// One-line coverage summary suitable for CI logs.
pub fn CoverageReport::summary(self : CoverageReport) -> String {
  "strength=\{self.strength}, cases=\{self.case_count}, coverage=\{self.coverage_percent}%, covered=\{self.covered_interactions}/\{self.total_interactions}, missing=\{self.missing.length()}"
}

///|
/// Render missing interactions as readable lines.
pub fn CoverageReport::missing_text(self : CoverageReport) -> String {
  let builder = StringBuilder::new()
  if self.missing.length() == 0 {
    builder.write_string("no missing interactions\n")
    return builder.to_string()
  }
  for index = 0; index < self.missing.length(); index = index + 1 {
    builder.write_string("- ")
    let interaction = self.missing[index]
    for choice_index = 0
        choice_index < interaction.choices.length()
        choice_index = choice_index + 1 {
      if choice_index > 0 {
        builder.write_string(", ")
      }
      let choice = interaction.choices[choice_index]
      builder.write_string(choice.parameter)
      builder.write_char('=')
      builder.write_string(choice.value)
    }
    builder.write_char('\n')
  }
  builder.to_string()
}

///|
fn write_json_int_field(
  builder : StringBuilder,
  name : String,
  value : Int,
  comma : Bool,
) -> Unit {
  builder.write_string("    ")
  builder.write_string(json_string(name))
  builder.write_string(": ")
  builder.write_string(value.to_string())
  if comma {
    builder.write_string(",")
  }
  builder.write_string("\n")
}

///|
fn write_json_int64_field(
  builder : StringBuilder,
  name : String,
  value : Int64,
  comma : Bool,
) -> Unit {
  builder.write_string("    ")
  builder.write_string(json_string(name))
  builder.write_string(": ")
  builder.write_string(value.to_string())
  if comma {
    builder.write_string(",")
  }
  builder.write_string("\n")
}

///|
fn write_json_case(
  builder : StringBuilder,
  model : Model,
  test_case : TestCase,
  indent : String,
) -> Unit {
  builder.write_string(indent)
  builder.write_char('{')
  for parameter_index = 0
      parameter_index < model.parameters.length()
      parameter_index = parameter_index + 1 {
    if parameter_index > 0 {
      builder.write_string(", ")
    }
    builder.write_string(json_string(model.parameters[parameter_index].name))
    builder.write_string(": ")
    builder.write_string(json_string(test_case.values[parameter_index]))
  }
  builder.write_char('}')
}

///|
fn csv_field(value : String) -> String {
  let chars = value.to_array()
  let mut quoted = false
  for char in chars {
    if char == ',' || char == '"' || char == '\n' || char == '\r' {
      quoted = true
    }
  }
  if !quoted {
    return value
  }
  let builder = StringBuilder::new()
  builder.write_char('"')
  for char in chars {
    if char == '"' {
      builder.write_string("\"\"")
    } else {
      builder.write_char(char)
    }
  }
  builder.write_char('"')
  builder.to_string()
}

///|
fn json_string(value : String) -> String {
  let builder = StringBuilder::new()
  builder.write_char('"')
  for char in value.to_array() {
    match char {
      '"' => builder.write_string("\\\"")
      '\\' => builder.write_string("\\\\")
      '\n' => builder.write_string("\\n")
      '\r' => builder.write_string("\\r")
      '\t' => builder.write_string("\\t")
      _ => builder.write_char(char)
    }
  }
  builder.write_char('"')
  builder.to_string()
}

///|
fn markdown_cell(value : String) -> String {
  let builder = StringBuilder::new()
  for char in value.to_array() {
    match char {
      '|' => builder.write_string("\\|")
      '\\' => builder.write_string("\\\\")
      '\n' => builder.write_string("
") '\r' => () _ => builder.write_char(char) } } builder.to_string() }