///|
/// Severity used by a machine-readable analysis document.
pub enum DocumentSeverity {
  Notice
  Success
  Warning
  Failure
} derive(Debug, Eq)

///|
/// Standard sections emitted by a verification report.
pub enum DocumentSectionKind {
  Overview
  Input
  Validation
  Performance
  Coverage
  Assertions
  Protocol
  Findings
  Artifacts
} derive(Debug, Eq)

///|
/// A named scalar or textual report metric.
pub struct DocumentMetric {
  name : String
  value : String
  unit : String?
  severity : DocumentSeverity
} derive(Debug)

///|
/// A report section containing metrics and explanatory lines.
pub struct DocumentSection {
  kind : DocumentSectionKind
  title : String
  summary : String
  metrics : Array[DocumentMetric]
  lines : Array[String]
} derive(Debug)

///|
/// A portable analysis document that can be rendered in several formats.
pub struct AnalysisDocument {
  title : String
  version : String
  verdict : DocumentSeverity
  sections : Array[DocumentSection]
  generated_by : String
} derive(Debug)

///|
/// List report severities.
pub fn document_severity_catalog() -> Array[DocumentSeverity] {
  [Notice, Success, Warning, Failure]
}

///|
/// List report section kinds.
pub fn document_section_catalog() -> Array[DocumentSectionKind] {
  [
    Overview,
    Input,
    Validation,
    Performance,
    Coverage,
    Assertions,
    Protocol,
    Findings,
    Artifacts,
  ]
}

///|
/// Start an empty analysis document.
pub fn analysis_document(title : String) -> AnalysisDocument {
  {
    title,
    version: "1.0",
    verdict: Notice,
    sections: [],
    generated_by: "moonbit-vcd",
  }
}

///|
/// Set the document version and top-level verdict.
pub fn AnalysisDocument::with_verdict(
  self : AnalysisDocument,
  version : String,
  verdict : DocumentSeverity,
) -> AnalysisDocument {
  { ..self, version, verdict }
}

///|
/// Add a section, replacing an existing section of the same kind.
pub fn AnalysisDocument::add_section(
  self : AnalysisDocument,
  section : DocumentSection,
) -> AnalysisDocument {
  let sections = self.sections.filter(fn(item) { item.kind != section.kind })
  sections.push(section)
  { ..self, sections, }
}

///|
/// Add a metric to a section, creating the section when necessary.
pub fn AnalysisDocument::add_metric(
  self : AnalysisDocument,
  kind : DocumentSectionKind,
  metric : DocumentMetric,
) -> AnalysisDocument {
  let sections = self.sections.copy()
  for index, section in sections {
    if section.kind == kind {
      let metrics = section.metrics.copy()
      metrics.push(metric)
      sections[index] = { ..section, metrics, }
      return { ..self, sections, }
    }
  }
  sections.push({
    kind,
    title: document_section_name(kind),
    summary: "",
    metrics: [metric],
    lines: [],
  })
  { ..self, sections, }
}

///|
/// Add an explanatory line to a section.
pub fn AnalysisDocument::add_line(
  self : AnalysisDocument,
  kind : DocumentSectionKind,
  line : String,
) -> AnalysisDocument {
  let sections = self.sections.copy()
  for index, section in sections {
    if section.kind == kind {
      let lines = section.lines.copy()
      lines.push(line)
      sections[index] = { ..section, lines, }
      return { ..self, sections, }
    }
  }
  sections.push({
    kind,
    title: document_section_name(kind),
    summary: line,
    metrics: [],
    lines: [line],
  })
  { ..self, sections, }
}

///|
/// Construct a section directly.
pub fn document_section(
  kind : DocumentSectionKind,
  title : String,
  summary : String,
) -> DocumentSection {
  { kind, title, summary, metrics: [], lines: [] }
}

///|
/// Construct a metric.
pub fn document_metric(
  name : String,
  value : String,
  unit : String?,
  severity : DocumentSeverity,
) -> DocumentMetric {
  { name, value, unit, severity }
}

///|
/// Return the number of report metrics.
pub fn AnalysisDocument::metric_count(self : AnalysisDocument) -> Int {
  self.sections.fold(init=0, fn(total, item) { total + item.metrics.length() })
}

///|
/// Return all metrics with warning or failure status.
pub fn AnalysisDocument::risk_metrics(
  self : AnalysisDocument,
) -> Array[DocumentMetric] {
  let result : Array[DocumentMetric] = []
  for section in self.sections {
    for metric in section.metrics {
      match metric.severity {
        Warning | Failure => result.push(metric)
        _ => ()
      }
    }
  }
  result
}

///|
/// Render a readable Markdown report.
pub fn AnalysisDocument::to_markdown(self : AnalysisDocument) -> String {
  let out = StringBuilder()
  out.write_string("# " + self.title + "\n\n")
  out.write_string("- Version: " + self.version + "\n")
  out.write_string("- Verdict: " + document_severity_name(self.verdict) + "\n")
  out.write_string("- Generated by: " + self.generated_by + "\n\n")
  for section in self.sections {
    out.write_string("## " + section.title + "\n\n")
    if section.summary.length() > 0 {
      out.write_string(section.summary + "\n\n")
    }
    if section.metrics.length() > 0 {
      out.write_string(
        "| Metric | Value | Unit | Status |\n| --- | --- | --- | --- |\n",
      )
      for metric in section.metrics {
        out.write_string(
          "| " +
          metric.name +
          " | " +
          metric.value +
          " | " +
          document_optional_text(metric.unit) +
          " | " +
          document_severity_name(metric.severity) +
          " |\n",
        )
      }
      out.write_char('\n')
    }
    for line in section.lines {
      out.write_string("- " + line + "\n")
    }
    if section.lines.length() > 0 {
      out.write_char('\n')
    }
  }
  out.to_string()
}

///|
/// Render a simple HTML document for web artifacts.
pub fn AnalysisDocument::to_html(self : AnalysisDocument) -> String {
  let out = StringBuilder()
  out.write_string("")
  out.write_string(document_html_escape(self.title))
  out.write_string("

") out.write_string( document_html_escape(self.title) + "

Verdict: " + document_severity_name(self.verdict) + "

", ) for section in self.sections { out.write_string( "

" + document_html_escape(section.title) + "

" + document_html_escape(section.summary) + "

", ) if section.metrics.length() > 0 { out.write_string( "", ) for metric in section.metrics { out.write_string( "", ) } out.write_string("
MetricValueUnitStatus
" + document_html_escape(metric.name) + "" + document_html_escape(metric.value) + "" + document_html_escape(document_optional_text(metric.unit)) + "" + document_severity_name(metric.severity) + "
") } if section.lines.length() > 0 { out.write_string("
    ") for line in section.lines { out.write_string("
  • " + document_html_escape(line) + "
  • ") } out.write_string("
") } out.write_string("
") } out.write_string("") out.to_string() } ///| /// Render report sections as JUnit XML test cases. pub fn AnalysisDocument::to_junit(self : AnalysisDocument) -> String { let failures = self .risk_metrics() .filter(fn(metric) { metric.severity is Failure }) .length() let out = StringBuilder() out.write_string( "", ) for section in self.sections { out.write_string( "", ) let section_failure = section.metrics.any(fn(metric) { metric.severity is Failure }) if section_failure { out.write_string( "" + document_html_escape(section.summary) + "", ) } out.write_string("") } out.write_string("") out.to_string() } ///| /// Render a compact one-line verdict. pub fn AnalysisDocument::verdict_line(self : AnalysisDocument) -> String { self.title + " verdict=" + document_severity_name(self.verdict) + " sections=" + self.sections.length().to_string() + " metrics=" + self.metric_count().to_string() } ///| /// Stable severity names for all output formats. pub fn document_severity_name(value : DocumentSeverity) -> String { match value { Notice => "notice" Success => "success" Warning => "warning" Failure => "failure" } } ///| /// Stable section names for headings and automation. pub fn document_section_name(value : DocumentSectionKind) -> String { match value { Overview => "Overview" Input => "Input" Validation => "Validation" Performance => "Performance" Coverage => "Coverage" Assertions => "Assertions" Protocol => "Protocol" Findings => "Findings" Artifacts => "Artifacts" } } ///| /// Encode an optional text field. fn document_optional_text(value : String?) -> String { match value { Some(item) => item None => "" } } ///| /// Escape the characters that are significant in HTML and XML. fn document_html_escape(value : String) -> String { value .replace_all(old="&", new="&") .replace_all(old="<", new="<") .replace_all(old=">", new=">") .replace_all(old="\"", new=""") }