///|
pub fn format_candidate(candidate : MutationCandidate) -> String {
  "#\{candidate.id} " +
  "\{candidate.span.file}:\{candidate.span.line}:\{candidate.span.column} " +
  "\{candidate.rule.label} " +
  "\{candidate.original} -> \{candidate.replacement} | " +
  candidate.line_text
}

///|
pub fn format_candidate_list(
  candidates : ArrayView[MutationCandidate],
) -> String {
  if candidates.is_empty() {
    "No mutation candidates found."
  } else {
    [ for candidate in candidates => format_candidate(candidate) ].join("\n")
  }
}

///|
pub fn format_summary(summary : MutationSummary) -> String {
  [
    "Mutation candidates for \{summary.file}",
    "total: \{summary.candidate_count}",
    "boolean: \{summary.boolean_count}",
    "equality: \{summary.equality_count}",
    "relational: \{summary.relational_count}",
    "arithmetic: \{summary.arithmetic_count}",
    "logical: \{summary.logical_count}",
    "numeric: \{summary.numeric_count}",
  ].join("\n")
}

///|
pub fn format_manifest(source : String, file? : String = "") -> String {
  let candidates = discover(source, file~)
  let summary = summarize_candidates(file, candidates)
  format_summary(summary) + "\n\n" + format_candidate_list(candidates)
}

///|
pub fn format_manifest_with_profile(
  source : String,
  profile : RuleProfile,
  file? : String = "",
) -> String {
  let candidates = discover_with_profile(source, profile, file~)
  let summary = summarize_candidates(file, candidates)
  format_summary(summary) + "\n\n" + format_candidate_list(candidates)
}