///|
/// A named TAP input stream for suite-level reporting.
pub(all) struct TapNamedInput {
  name : String
  input : String
} derive(Debug, Eq)

///|
/// A parsed and validated named TAP stream.
pub(all) struct TapNamedReport {
  name : String
  report : TapReport
} derive(Debug, Eq)

///|
/// Aggregate counters for a collection of named reports.
pub(all) struct TapSuiteSummary {
  suites : Int
  passed_suites : Int
  failed_suites : Int
  planned : Int
  total : Int
  passed : Int
  failed : Int
  skipped : Int
  todo : Int
  diagnostics : Int
  yaml_blocks : Int
  issues : Int
} derive(Debug, Eq)

///|
/// A CI-friendly suite report built from many TAP streams.
pub(all) struct TapSuiteReport {
  name : String
  reports : Array[TapNamedReport]
  summary : TapSuiteSummary
  ok : Bool
} derive(Debug, Eq)

///|
/// Construct a named input value.
pub fn named_input(name : String, input : String) -> TapNamedInput {
  TapNamedInput::{ name, input }
}

///|
/// Construct a named report from already parsed data.
pub fn named_report(name : String, report : TapReport) -> TapNamedReport {
  TapNamedReport::{ name, report }
}

///|
/// Parse multiple TAP streams into a suite report.
pub fn parse_suite(
  name : String,
  inputs : Array[TapNamedInput],
) -> TapSuiteReport {
  let reports : Array[TapNamedReport] = []
  for item in inputs {
    reports.push(TapNamedReport::{
      name: item.name,
      report: parse_and_report(item.input),
    })
  }
  build_suite_report(name, reports)
}

///|
/// Build a suite report from named reports.
pub fn build_suite_report(
  name : String,
  reports : Array[TapNamedReport],
) -> TapSuiteReport {
  let summary = summarize_suite(reports)
  TapSuiteReport::{ name, reports, summary, ok: summary.failed_suites == 0 }
}

///|
/// Aggregate suite-level counters.
pub fn summarize_suite(reports : Array[TapNamedReport]) -> TapSuiteSummary {
  let mut passed_suites = 0
  let mut failed_suites = 0
  let mut planned = 0
  let mut total = 0
  let mut passed = 0
  let mut failed = 0
  let mut skipped = 0
  let mut todo = 0
  let mut diagnostics = 0
  let mut yaml_blocks = 0
  let mut issues = 0
  for item in reports {
    if item.report.ok {
      passed_suites += 1
    } else {
      failed_suites += 1
    }
    planned += item.report.summary.planned
    total += item.report.summary.total
    passed += item.report.summary.passed
    failed += item.report.summary.failed
    skipped += item.report.summary.skipped
    todo += item.report.summary.todo
    diagnostics += item.report.summary.diagnostics
    yaml_blocks += item.report.summary.yaml_blocks
    issues += item.report.issues.length()
  }
  TapSuiteSummary::{
    suites: reports.length(),
    passed_suites,
    failed_suites,
    planned,
    total,
    passed,
    failed,
    skipped,
    todo,
    diagnostics,
    yaml_blocks,
    issues,
  }
}

///|
/// Return true when every named report passes.
pub fn suite_passes(suite : TapSuiteReport) -> Bool {
  suite.ok
}

///|
/// Return a stable status label for the suite.
pub fn suite_status_label(suite : TapSuiteReport) -> String {
  if suite.ok {
    "ok"
  } else {
    "failed"
  }
}

///|
/// Return the first failing report, if any.
pub fn first_failing_report(suite : TapSuiteReport) -> TapNamedReport? {
  for item in suite.reports {
    if !item.report.ok {
      return Some(item)
    }
  }
  None
}

///|
/// Return all passing reports.
pub fn passing_reports(suite : TapSuiteReport) -> Array[TapNamedReport] {
  let items : Array[TapNamedReport] = []
  for item in suite.reports {
    if item.report.ok {
      items.push(item)
    }
  }
  items
}

///|
/// Return all failing reports.
pub fn failing_reports(suite : TapSuiteReport) -> Array[TapNamedReport] {
  let items : Array[TapNamedReport] = []
  for item in suite.reports {
    if !item.report.ok {
      items.push(item)
    }
  }
  items
}

///|
/// Return reports containing a specific issue code.
pub fn reports_with_issue(
  suite : TapSuiteReport,
  code : String,
) -> Array[TapNamedReport] {
  let items : Array[TapNamedReport] = []
  for item in suite.reports {
    if report_has_issue(item.report, code) {
      items.push(item)
    }
  }
  items
}

///|
/// Return true when a report contains the issue code.
pub fn report_has_issue(report : TapReport, code : String) -> Bool {
  for item in report.issues {
    if item.code == code {
      return true
    }
  }
  false
}

///|
/// Return true when any suite report contains the issue code.
pub fn suite_has_issue(suite : TapSuiteReport, code : String) -> Bool {
  for item in suite.reports {
    if report_has_issue(item.report, code) {
      return true
    }
  }
  false
}

///|
/// Return unique issue codes in first-seen order.
pub fn suite_issue_codes(suite : TapSuiteReport) -> Array[String] {
  let codes : Array[String] = []
  for item in suite.reports {
    for issue_item in item.report.issues {
      if !codes.contains(issue_item.code) {
        codes.push(issue_item.code)
      }
    }
  }
  codes
}

///|
/// Count report-level blocking failures across the suite.
pub fn suite_blocking_failures(suite : TapSuiteReport) -> Int {
  let mut total = 0
  for item in suite.reports {
    total += count_blocking_failures(item.report)
  }
  total
}

///|
/// Calculate suite-level health score.
pub fn suite_health_score(suite : TapSuiteReport) -> Int {
  if suite.reports.length() == 0 {
    return 0
  }
  let mut total = 0
  for item in suite.reports {
    total += health_score(item.report)
  }
  total / suite.reports.length()
}

///|
/// Apply a policy to every suite member and aggregate the result.
pub fn evaluate_suite_policy(
  suite : TapSuiteReport,
  policy : TapPolicy,
) -> TapPolicyResult {
  let messages : Array[String] = []
  let mut errors = 0
  let mut warnings = 0
  let mut infos = 0
  for item in suite.reports {
    let result = evaluate_policy(item.report, policy)
    if !result.ok {
      errors += 1
      messages.push("\{item.name}: policy failed")
    } else {
      infos += 1
      messages.push("\{item.name}: policy passed")
    }
    warnings += result.warnings
    infos += result.infos
    errors += result.errors
  }
  if suite.reports.length() == 0 {
    errors += 1
    messages.push("suite contains no reports")
  }
  let score = suite_health_score(suite) - errors * 8 - warnings * 3
  TapPolicyResult::{
    policy_name: "\{policy.name}/suite",
    ok: errors == 0,
    score: clamp_suite_score(score),
    errors,
    warnings,
    infos,
    messages,
  }
}

///|
/// Render a suite as Markdown for GitHub job summaries.
pub fn suite_to_markdown(suite : TapSuiteReport) -> String {
  let sb = StringBuilder()
  sb.write_string("# TAPTrail Suite Report\n\n")
  sb.write_string("- suite: ")
  sb.write_string(suite.name)
  sb.write_string("\n")
  sb.write_string("- status: ")
  sb.write_string(suite_status_label(suite))
  sb.write_string("\n")
  sb.write_string("- suites: \{suite.summary.suites}\n")
  sb.write_string("- passed_suites: \{suite.summary.passed_suites}\n")
  sb.write_string("- failed_suites: \{suite.summary.failed_suites}\n")
  sb.write_string("- planned: \{suite.summary.planned}\n")
  sb.write_string("- total: \{suite.summary.total}\n")
  sb.write_string("- passed: \{suite.summary.passed}\n")
  sb.write_string("- failed: \{suite.summary.failed}\n")
  sb.write_string("- skipped: \{suite.summary.skipped}\n")
  sb.write_string("- todo: \{suite.summary.todo}\n")
  sb.write_string("- diagnostics: \{suite.summary.diagnostics}\n")
  sb.write_string("- yaml_blocks: \{suite.summary.yaml_blocks}\n")
  sb.write_string("- issues: \{suite.summary.issues}\n")
  sb.write_string("- health_score: \{suite_health_score(suite)}\n")
  if suite.reports.is_empty() {
    sb.write_string("\nNo TAP streams were provided.\n")
  } else {
    sb.write_string("\n## Members\n\n")
    sb.write_string("| Name | Status | Total | Passed | Failed | Issues |\n")
    sb.write_string("| --- | --- | ---: | ---: | ---: | ---: |\n")
    for item in suite.reports {
      sb.write_string("| ")
      sb.write_string(markdown_cell(item.name))
      sb.write_string(" | ")
      sb.write_string(if item.report.ok { "ok" } else { "failed" })
      sb.write_string(" | \{item.report.summary.total}")
      sb.write_string(" | \{item.report.summary.passed}")
      sb.write_string(" | \{item.report.summary.failed}")
      sb.write_string(" | \{item.report.issues.length()} |\n")
    }
  }
  let codes = suite_issue_codes(suite)
  if codes.length() > 0 {
    sb.write_string("\n## Issue Codes\n\n")
    for code in codes {
      let meta = issue_meta(code)
      sb.write_string("- `")
      sb.write_string(code)
      sb.write_string("` ")
      sb.write_string(severity_label(meta.severity))
      sb.write_string(": ")
      sb.write_string(meta.title)
      sb.write_string("\n")
    }
  }
  sb.to_string()
}

///|
/// Render a compact JSON object for suite dashboards.
pub fn suite_to_compact_json(suite : TapSuiteReport) -> String {
  let sb = StringBuilder()
  sb.write_string("{")
  sb.write_string("\"name\":\"")
  sb.write_string(json_escape(suite.name))
  sb.write_string("\",\"ok\":")
  sb.write_string(if suite.ok { "true" } else { "false" })
  sb.write_string(",\"suites\":\{suite.summary.suites}")
  sb.write_string(",\"passed_suites\":\{suite.summary.passed_suites}")
  sb.write_string(",\"failed_suites\":\{suite.summary.failed_suites}")
  sb.write_string(",\"total\":\{suite.summary.total}")
  sb.write_string(",\"passed\":\{suite.summary.passed}")
  sb.write_string(",\"failed\":\{suite.summary.failed}")
  sb.write_string(",\"skipped\":\{suite.summary.skipped}")
  sb.write_string(",\"todo\":\{suite.summary.todo}")
  sb.write_string(",\"issues\":\{suite.summary.issues}")
  sb.write_string(",\"reports\":[")
  for item in suite.reports; first = true {
    if first {
      ()
    } else {
      sb.write_string(",")
    }
    sb.write_string("{\"name\":\"")
    sb.write_string(json_escape(item.name))
    sb.write_string("\",\"ok\":")
    sb.write_string(if item.report.ok { "true" } else { "false" })
    sb.write_string(",\"total\":\{item.report.summary.total}")
    sb.write_string(",\"issues\":\{item.report.issues.length()}")
    sb.write_string("}")
    continue false
  }
  sb.write_string("]}")
  sb.to_string()
}

///|
/// Render suite rows as CSV with a header.
pub fn suite_to_csv(suite : TapSuiteReport) -> String {
  let sb = StringBuilder()
  sb.write_string(
    "name,status,planned,total,passed,failed,skipped,todo,issues\n",
  )
  for item in suite.reports {
    sb.write_string(csv_escape(item.name))
    sb.write_string(",")
    sb.write_string(if item.report.ok { "ok" } else { "failed" })
    sb.write_string(",\{item.report.summary.planned}")
    sb.write_string(",\{item.report.summary.total}")
    sb.write_string(",\{item.report.summary.passed}")
    sb.write_string(",\{item.report.summary.failed}")
    sb.write_string(",\{item.report.summary.skipped}")
    sb.write_string(",\{item.report.summary.todo}")
    sb.write_string(",\{item.report.issues.length()}\n")
  }
  sb.to_string()
}

///|
/// Render suite rows as tab-separated values.
pub fn suite_to_tsv(suite : TapSuiteReport) -> String {
  let sb = StringBuilder()
  sb.write_string(
    "name\tstatus\tplanned\ttotal\tpassed\tfailed\tskipped\ttodo\tissues\n",
  )
  for item in suite.reports {
    sb.write_string(tsv_escape(item.name))
    sb.write_string("\t")
    sb.write_string(if item.report.ok { "ok" } else { "failed" })
    sb.write_string("\t\{item.report.summary.planned}")
    sb.write_string("\t\{item.report.summary.total}")
    sb.write_string("\t\{item.report.summary.passed}")
    sb.write_string("\t\{item.report.summary.failed}")
    sb.write_string("\t\{item.report.summary.skipped}")
    sb.write_string("\t\{item.report.summary.todo}")
    sb.write_string("\t\{item.report.issues.length()}\n")
  }
  sb.to_string()
}

///|
/// Render one merged JUnit XML suite containing named reports.
pub fn suite_to_junit_xml(suite : TapSuiteReport) -> String {
  let sb = StringBuilder()
  sb.write_string("\n")
  sb.write_string("\n")
  for item in suite.reports {
    for point in item.report.document.points {
      sb.write_string("  ")
      if point.ok {
        match point.directive {
          Skip(reason) => {
            sb.write_string("\n    \n  ")
          }
          Todo(reason) => {
            sb.write_string("\n    \n  ")
          }
          NoDirective => ()
        }
      } else {
        match point.directive {
          Todo(reason) => {
            sb.write_string("\n    \n  ")
          }
          _ => {
            sb.write_string("\n    ")
            sb.write_string(xml_escape(point.raw))
            sb.write_string("\n  ")
          }
        }
      }
      sb.write_string("\n")
    }
  }
  if suite.summary.issues > 0 {
    sb.write_string("  ")
    for item in suite.reports {
      for issue_item in item.report.issues {
        sb.write_string(
          xml_escape(
            "[\{item.name}] [\{issue_item.code}] \{issue_item.message}\n",
          ),
        )
      }
    }
    sb.write_string("\n")
  }
  sb.write_string("\n")
  sb.to_string()
}

///|
/// Render a compact status block for CI environment files.
pub fn suite_to_env_lines(suite : TapSuiteReport, prefix : String) -> String {
  let name = if prefix.length() == 0 { "TAPTRAIL" } else { prefix }
  let sb = StringBuilder()
  sb.write_string("\{name}_STATUS=\{suite_status_label(suite)}\n")
  sb.write_string("\{name}_SUITES=\{suite.summary.suites}\n")
  sb.write_string("\{name}_PASSED_SUITES=\{suite.summary.passed_suites}\n")
  sb.write_string("\{name}_FAILED_SUITES=\{suite.summary.failed_suites}\n")
  sb.write_string("\{name}_TOTAL=\{suite.summary.total}\n")
  sb.write_string("\{name}_PASSED=\{suite.summary.passed}\n")
  sb.write_string("\{name}_FAILED=\{suite.summary.failed}\n")
  sb.write_string("\{name}_ISSUES=\{suite.summary.issues}\n")
  sb.write_string("\{name}_SCORE=\{suite_health_score(suite)}\n")
  sb.to_string()
}

///|
/// Render release-note bullets from a suite.
pub fn suite_to_release_notes(suite : TapSuiteReport) -> String {
  let sb = StringBuilder()
  sb.write_string("Test suite `")
  sb.write_string(suite.name)
  sb.write_string("` ")
  sb.write_string(if suite.ok { "passed" } else { "failed" })
  sb.write_string(
    " with \{suite.summary.passed}/\{suite.summary.total} passing tests.",
  )
  if suite.summary.skipped > 0 {
    sb.write_string(" Skipped: \{suite.summary.skipped}.")
  }
  if suite.summary.todo > 0 {
    sb.write_string(" TODO: \{suite.summary.todo}.")
  }
  if suite.summary.issues > 0 {
    sb.write_string(" Issues: \{suite.summary.issues}.")
  }
  sb.to_string()
}

///|
/// Render a Markdown checklist useful for manual release reviews.
pub fn suite_to_checklist(suite : TapSuiteReport) -> String {
  let sb = StringBuilder()
  sb.write_string("# Release Test Checklist\n\n")
  for item in suite.reports {
    sb.write_string("- [")
    sb.write_string(if item.report.ok { "x" } else { " " })
    sb.write_string("] ")
    sb.write_string(item.name)
    sb.write_string(" (")
    sb.write_string(if item.report.ok { "ok" } else { "failed" })
    sb.write_string(
      ", \{item.report.summary.passed}/\{item.report.summary.total} passed",
    )
    if item.report.issues.length() > 0 {
      sb.write_string(", \{item.report.issues.length()} issues")
    }
    sb.write_string(")\n")
  }
  sb.to_string()
}

///|
/// Render issue details as a Markdown table.
pub fn suite_issues_to_markdown(suite : TapSuiteReport) -> String {
  let sb = StringBuilder()
  sb.write_string("| Suite | Severity | Code | Line | Message |\n")
  sb.write_string("| --- | --- | --- | ---: | --- |\n")
  for item in suite.reports {
    for issue_item in item.report.issues {
      let meta = issue_meta(issue_item.code)
      sb.write_string("| ")
      sb.write_string(markdown_cell(item.name))
      sb.write_string(" | ")
      sb.write_string(severity_label(meta.severity))
      sb.write_string(" | `")
      sb.write_string(issue_item.code)
      sb.write_string("` | \{issue_item.line} | ")
      sb.write_string(markdown_cell(issue_item.message))
      sb.write_string(" |\n")
    }
  }
  sb.to_string()
}

///|
/// Find the report with the most issues.
pub fn report_with_most_issues(suite : TapSuiteReport) -> TapNamedReport? {
  let mut best_index = -1
  let mut best_count = -1
  for i in 0.. best_count {
      best_count = issue_count
      best_index = i
    }
  }
  if best_index >= 0 {
    Some(suite.reports[best_index])
  } else {
    None
  }
}

///|
fn suite_point_name(point : TapPoint) -> String {
  if point.name.length() > 0 {
    point.name
  } else if point.number > 0 {
    "test \{point.number}"
  } else {
    "unnumbered at line \{point.line}"
  }
}

///|
fn markdown_cell(text : String) -> String {
  let sb = StringBuilder()
  for ch in text {
    match ch {
      '|' => sb.write_string("\\|")
      '\n' => sb.write_string("
") '\r' => () _ => sb.write_char(ch) } } sb.to_string() } ///| fn csv_escape(text : String) -> String { let must_quote = text.contains(",") || text.contains("\"") || text.contains("\n") if !must_quote { return text } let sb = StringBuilder() sb.write_string("\"") for ch in text { match ch { '"' => sb.write_string("\"\"") _ => sb.write_char(ch) } } sb.write_string("\"") sb.to_string() } ///| fn tsv_escape(text : String) -> String { let sb = StringBuilder() for ch in text { match ch { '\t' => sb.write_string(" ") '\n' => sb.write_string(" ") '\r' => () _ => sb.write_char(ch) } } sb.to_string() } ///| fn clamp_suite_score(score : Int) -> Int { if score < 0 { 0 } else if score > 100 { 100 } else { score } }