///|
fn percentage_text(count : CoverageCount) -> String {
  let hundredths = (count.percentage() * 100.0).round().to_int()
  let whole = hundredths / 100
  let fraction = hundredths % 100
  let fraction_text = if fraction < 10 {
    "0\{fraction}"
  } else {
    fraction.to_string()
  }
  "\{whole}.\{fraction_text}%"
}

///|
fn count_text(count : CoverageCount) -> String {
  "\{percentage_text(count)} (\{count.covered}/\{count.total})"
}

///|
fn markdown_cell(value : String) -> String {
  value.replace_all(old="|", new="\\|").replace_all(old="\n", new=" ")
}

///|
fn write_summary_row(
  output : StringBuilder,
  scope : String,
  summary : CoverageSummary,
) -> Unit {
  output.write_string(
    "| \{markdown_cell(scope)} | \{count_text(summary.lines)} | " +
    "\{count_text(summary.branches)} | " +
    "\{count_text(summary.functions)} |\n",
  )
}

///|
/// Render report-wide and optional per-file statistics as a Markdown table.
pub fn to_markdown(
  report : CoverageReport,
  title? : String = "Coverage report",
  include_files? : Bool = true,
) -> String raise CoverageError {
  let output = StringBuilder()
  output.write_string("## \{markdown_cell(title)}\n\n")
  output.write_string("| Scope | Lines | Branches | Functions |\n")
  output.write_string("|:--|--:|--:|--:|\n")
  write_summary_row(output, "**Overall**", summarize(report))
  if include_files {
    for file in summarize_files(report) {
      write_summary_row(output, "`\{file.path}`", file.summary)
    }
  }
  output.to_string()
}

///|
fn count_json(count : CoverageCount) -> Json {
  Json::object({
    "covered": json_integer(count.covered),
    "total": json_integer(count.total),
    "percentage": Json::number(count.percentage()),
  })
}

///|
fn summary_json(summary : CoverageSummary) -> Json {
  Json::object({
    "lines": count_json(summary.lines),
    "branches": count_json(summary.branches),
    "functions": count_json(summary.functions),
  })
}

///|
fn line_json(line : CoverageLine) -> Json {
  Json::object({
    "line": json_integer(line.line),
    "hits": json_integer(line.hits),
  })
}

///|
fn branch_json(branch : CoverageBranch) -> Json {
  Json::object({
    "line": json_integer(branch.line),
    "block": Json::string(branch.block),
    "branch": Json::string(branch.branch),
    "taken": match branch.taken {
      Some(count) => json_integer(count)
      None => Json::null()
    },
  })
}

///|
fn function_json(function : CoverageFunction) -> Json {
  Json::object({
    "name": Json::string(function.name),
    "line": match function.line {
      Some(line) => json_integer(line)
      None => Json::null()
    },
    "hits": json_integer(function.hits),
  })
}

///|
fn unified_file_json(file : FileCoverage) -> Json {
  Json::object({
    "path": Json::string(file.path),
    "test_name": match file.test_name {
      Some(name) => Json::string(name)
      None => Json::null()
    },
    "summary": summary_json(summarize_canonical_file(file)),
    "lines": Json::array([ for line in file.lines => line_json(line) ]),
    "branches": Json::array(
      [
        for branch in file.branches => branch_json(branch)
      ],
    ),
    "functions": Json::array(
      [
        for function in file.functions => function_json(function)
      ],
    ),
  })
}

///|
/// Encode the complete unified model and calculated statistics as JSON.
///
/// Unlike Coveralls JSON, this mooncov-specific format retains function
/// coverage and test names.
pub fn to_json_report(
  report : CoverageReport,
  indent? : Int = 2,
) -> String raise CoverageError {
  if indent < 0 {
    raise InvalidModel("JSON indentation must not be negative")
  }
  let canonical = canonicalize_report(report)
  let root = Json::object({
    "format": Json::string("mooncov-report-v1"),
    "summary": summary_json(
      canonical.files.fold(init=empty_summary(), (acc, file) => {
        add_summaries(acc, summarize_canonical_file(file))
      }),
    ),
    "files": Json::array(
      [
        for file in canonical.files => unified_file_json(file)
      ],
    ),
  })
  root.stringify(indent~)
}

///|
fn metric_name(metric : CoverageMetric) -> String {
  match metric {
    Lines => "lines"
    Branches => "branches"
    Functions => "functions"
  }
}

///|
/// Produce a concise human-readable threshold result for CI logs.
pub fn gate_message(result : GateResult) -> String {
  if result.passed {
    return "coverage gate passed"
  }
  let parts : Array[String] = []
  for violation in result.violations {
    parts.push(
      "\{metric_name(violation.metric)} " +
      "\{violation.actual}% < \{violation.required}%",
    )
  }
  "coverage gate failed: \{parts.join("; ")}"
}