///|
/// Render a human-readable Markdown audit report.
pub fn Audit::to_markdown(self : Audit) -> String {
  let buf = StringBuilder()
  buf.write_string("# Constraint Lens Report\n\n")
  buf.write_string("- Target: \{self.title}\n")
  buf.write_string("- Score: \{self.score}/100 (\{self.grade})\n")
  buf.write_string("- Earned: \{self.earned_score}/\{self.max_score}\n")
  buf.write_string("- Lines: \{self.stats.line_count}\n")
  buf.write_string("- Headings: \{self.stats.heading_count}\n")
  buf.write_string("- Bullets: \{self.stats.bullet_count}\n")
  buf.write_string("- Failed rules: \{self.stats.failed_rules}\n")
  buf.write_string("- Blocking errors: \{self.stats.error_count}\n\n")
  buf.write_string("## Findings\n\n")
  for finding in self.findings {
    let status = if finding.passed { "PASS" } else { "FAIL" }
    buf.write_string("### \{status} \{finding.rule_id}: \{finding.title}\n\n")
    buf.write_string("- Severity: \{finding.severity.label()}\n")
    buf.write_string("- Category: \{finding.category}\n")
    buf.write_string("- Weight: \{finding.earned}/\{finding.weight}\n")
    buf.write_string("- Message: \{finding.message}\n")
    if !finding.hint.is_empty() {
      buf.write_string("- Hint: \{finding.hint}\n")
    }
    if !finding.evidence.is_empty() {
      buf.write_string("- Evidence:\n")
      for item in finding.evidence {
        buf.write_string(
          "  - line \{item.line}, matched `\{item.matched}`: \{item.snippet}\n",
        )
      }
    }
    buf.write_string("\n")
  }
  buf.to_string()
}

///|
/// Render a compact JSON-like string without depending on a JSON package.
pub fn Audit::to_jsonish(self : Audit) -> String {
  let buf = StringBuilder()
  buf.write_string("{")
  buf.write_string("\"title\":\"\{json_escape(self.title)}\",")
  buf.write_string("\"score\":\{self.score},")
  buf.write_string("\"grade\":\"\{self.grade}\",")
  buf.write_string(
    "\"success\":\{if self.is_success() { "true" } else { "false" }},",
  )
  buf.write_string("\"findings\":[")
  for i in 0.. 0 {
      buf.write_string(",")
    }
    let finding = self.findings[i]
    buf.write_string("{")
    buf.write_string("\"id\":\"\{json_escape(finding.rule_id)}\",")
    buf.write_string(
      "\"passed\":\{if finding.passed { "true" } else { "false" }},",
    )
    buf.write_string("\"severity\":\"\{finding.severity.label()}\",")
    buf.write_string("\"earned\":\{finding.earned},")
    buf.write_string("\"weight\":\{finding.weight},")
    buf.write_string("\"message\":\"\{json_escape(finding.message)}\"")
    buf.write_string("}")
  }
  buf.write_string("]}")
  buf.to_string()
}

///|
fn json_escape(input : String) -> String {
  let buf = StringBuilder()
  for ch in input {
    if ch == '"' {
      buf.write_string("\\\"")
    } else if ch == '\\' {
      buf.write_string("\\\\")
    } else if ch == '\n' {
      buf.write_string("\\n")
    } else if ch == '\r' {
      buf.write_string("\\r")
    } else if ch == '\t' {
      buf.write_string("\\t")
    } else {
      buf.write_char(ch)
    }
  }
  buf.to_string()
}