///|
/// A timestamped TAP report record for trend analysis.
pub(all) struct TapRunRecord {
  id : String
  label : String
  timestamp : String
  report : TapReport
} derive(Debug, Eq)

///|
/// Difference between two TAP summaries.
pub(all) struct TapSummaryDelta {
  planned : Int
  total : Int
  passed : Int
  failed : Int
  skipped : Int
  todo : Int
  diagnostics : Int
  yaml_blocks : Int
  issues : Int
  health : Int
} derive(Debug, Eq)

///|
/// Trend classification for release notes.
pub(all) enum TapTrendKind {
  TrendImproved
  TrendRegressed
  TrendStable
  TrendMixed
} derive(Debug, Eq)

///|
/// Comparison between two records.
pub(all) struct TapTrendReport {
  before : TapRunRecord
  after : TapRunRecord
  delta : TapSummaryDelta
  kind : TapTrendKind
  notes : Array[String]
} derive(Debug, Eq)

///|
/// Construct a timestamped run record.
pub fn run_record(
  id : String,
  label : String,
  timestamp : String,
  report : TapReport,
) -> TapRunRecord {
  TapRunRecord::{ id, label, timestamp, report }
}

///|
/// Compare two report summaries.
pub fn compare_summaries(
  before : TapReport,
  after : TapReport,
) -> TapSummaryDelta {
  TapSummaryDelta::{
    planned: after.summary.planned - before.summary.planned,
    total: after.summary.total - before.summary.total,
    passed: after.summary.passed - before.summary.passed,
    failed: after.summary.failed - before.summary.failed,
    skipped: after.summary.skipped - before.summary.skipped,
    todo: after.summary.todo - before.summary.todo,
    diagnostics: after.summary.diagnostics - before.summary.diagnostics,
    yaml_blocks: after.summary.yaml_blocks - before.summary.yaml_blocks,
    issues: after.issues.length() - before.issues.length(),
    health: health_score(after) - health_score(before),
  }
}

///|
/// Compare two timestamped records.
pub fn compare_runs(
  before : TapRunRecord,
  after : TapRunRecord,
) -> TapTrendReport {
  let delta = compare_summaries(before.report, after.report)
  let notes = trend_notes(before, after, delta)
  TapTrendReport::{ before, after, delta, kind: classify_trend(delta), notes }
}

///|
/// Classify a summary delta into a compact trend kind.
pub fn classify_trend(delta : TapSummaryDelta) -> TapTrendKind {
  let improved = delta.failed < 0 || delta.issues < 0 || delta.health > 0
  let regressed = delta.failed > 0 || delta.issues > 0 || delta.health < 0
  if improved && regressed {
    TrendMixed
  } else if improved {
    TrendImproved
  } else if regressed {
    TrendRegressed
  } else {
    TrendStable
  }
}

///|
/// Human-readable trend kind label.
pub fn trend_kind_label(kind : TapTrendKind) -> String {
  match kind {
    TrendImproved => "improved"
    TrendRegressed => "regressed"
    TrendStable => "stable"
    TrendMixed => "mixed"
  }
}

///|
/// Render a summary delta as Markdown.
pub fn trend_to_markdown(trend : TapTrendReport) -> String {
  let sb = StringBuilder()
  sb.write_string("# TAPTrend Comparison\n\n")
  sb.write_string("- before: ")
  sb.write_string(trend.before.label)
  sb.write_string(" (")
  sb.write_string(trend.before.id)
  sb.write_string(")\n")
  sb.write_string("- after: ")
  sb.write_string(trend.after.label)
  sb.write_string(" (")
  sb.write_string(trend.after.id)
  sb.write_string(")\n")
  sb.write_string("- kind: ")
  sb.write_string(trend_kind_label(trend.kind))
  sb.write_string("\n")
  sb.write_string("- health_delta: \{signed_int(trend.delta.health)}\n")
  sb.write_string("\n")
  sb.write_string("| Metric | Delta |\n")
  sb.write_string("| --- | ---: |\n")
  sb.write_string("| planned | \{signed_int(trend.delta.planned)} |\n")
  sb.write_string("| total | \{signed_int(trend.delta.total)} |\n")
  sb.write_string("| passed | \{signed_int(trend.delta.passed)} |\n")
  sb.write_string("| failed | \{signed_int(trend.delta.failed)} |\n")
  sb.write_string("| skipped | \{signed_int(trend.delta.skipped)} |\n")
  sb.write_string("| todo | \{signed_int(trend.delta.todo)} |\n")
  sb.write_string("| diagnostics | \{signed_int(trend.delta.diagnostics)} |\n")
  sb.write_string("| yaml_blocks | \{signed_int(trend.delta.yaml_blocks)} |\n")
  sb.write_string("| issues | \{signed_int(trend.delta.issues)} |\n")
  if trend.notes.length() > 0 {
    sb.write_string("\n## Notes\n\n")
    for note in trend.notes {
      sb.write_string("- ")
      sb.write_string(note)
      sb.write_string("\n")
    }
  }
  sb.to_string()
}

///|
/// Render a trend report as compact JSON.
pub fn trend_to_json(trend : TapTrendReport) -> String {
  let sb = StringBuilder()
  sb.write_string("{")
  sb.write_string("\"before\":\"")
  sb.write_string(json_escape(trend.before.id))
  sb.write_string("\",\"after\":\"")
  sb.write_string(json_escape(trend.after.id))
  sb.write_string("\",\"kind\":\"")
  sb.write_string(trend_kind_label(trend.kind))
  sb.write_string("\",\"delta\":{")
  sb.write_string("\"planned\":\{trend.delta.planned}")
  sb.write_string(",\"total\":\{trend.delta.total}")
  sb.write_string(",\"passed\":\{trend.delta.passed}")
  sb.write_string(",\"failed\":\{trend.delta.failed}")
  sb.write_string(",\"skipped\":\{trend.delta.skipped}")
  sb.write_string(",\"todo\":\{trend.delta.todo}")
  sb.write_string(",\"diagnostics\":\{trend.delta.diagnostics}")
  sb.write_string(",\"yaml_blocks\":\{trend.delta.yaml_blocks}")
  sb.write_string(",\"issues\":\{trend.delta.issues}")
  sb.write_string(",\"health\":\{trend.delta.health}")
  sb.write_string("},\"notes\":[")
  for note in trend.notes; first = true {
    if first {
      ()
    } else {
      sb.write_string(",")
    }
    sb.write_string("\"")
    sb.write_string(json_escape(note))
    sb.write_string("\"")
    continue false
  }
  sb.write_string("]}")
  sb.to_string()
}

///|
/// Render a trend report as one CSV row with header.
pub fn trend_to_csv(trend : TapTrendReport) -> String {
  let sb = StringBuilder()
  sb.write_string(
    "before,after,kind,planned,total,passed,failed,skipped,todo,issues,health\n",
  )
  sb.write_string(csv_escape(trend.before.id))
  sb.write_string(",")
  sb.write_string(csv_escape(trend.after.id))
  sb.write_string(",")
  sb.write_string(trend_kind_label(trend.kind))
  sb.write_string(",\{trend.delta.planned}")
  sb.write_string(",\{trend.delta.total}")
  sb.write_string(",\{trend.delta.passed}")
  sb.write_string(",\{trend.delta.failed}")
  sb.write_string(",\{trend.delta.skipped}")
  sb.write_string(",\{trend.delta.todo}")
  sb.write_string(",\{trend.delta.issues}")
  sb.write_string(",\{trend.delta.health}\n")
  sb.to_string()
}

///|
/// Render a list of runs as Markdown history.
pub fn history_to_markdown(records : Array[TapRunRecord]) -> String {
  let sb = StringBuilder()
  sb.write_string("# TAPTrail History\n\n")
  sb.write_string(
    "| ID | Label | Timestamp | Status | Total | Passed | Failed | Issues | Health |\n",
  )
  sb.write_string(
    "| --- | --- | --- | --- | ---: | ---: | ---: | ---: | ---: |\n",
  )
  for item in records {
    sb.write_string("| ")
    sb.write_string(history_cell(item.id))
    sb.write_string(" | ")
    sb.write_string(history_cell(item.label))
    sb.write_string(" | ")
    sb.write_string(history_cell(item.timestamp))
    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()}")
    sb.write_string(" | \{health_score(item.report)} |\n")
  }
  sb.to_string()
}

///|
/// Render a run history as compact JSON.
pub fn history_to_json(records : Array[TapRunRecord]) -> String {
  let sb = StringBuilder()
  sb.write_string("[")
  for item in records; first = true {
    if first {
      ()
    } else {
      sb.write_string(",")
    }
    sb.write_string("{\"id\":\"")
    sb.write_string(json_escape(item.id))
    sb.write_string("\",\"label\":\"")
    sb.write_string(json_escape(item.label))
    sb.write_string("\",\"timestamp\":\"")
    sb.write_string(json_escape(item.timestamp))
    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(",\"passed\":\{item.report.summary.passed}")
    sb.write_string(",\"failed\":\{item.report.summary.failed}")
    sb.write_string(",\"issues\":\{item.report.issues.length()}")
    sb.write_string(",\"health\":\{health_score(item.report)}")
    sb.write_string("}")
    continue false
  }
  sb.write_string("]")
  sb.to_string()
}

///|
/// Render pairwise trends for adjacent run records.
pub fn history_trends_to_markdown(records : Array[TapRunRecord]) -> String {
  let sb = StringBuilder()
  sb.write_string("# TAPTrail History Trends\n\n")
  if records.length() < 2 {
    sb.write_string("Not enough records to calculate trends.\n")
    return sb.to_string()
  }
  sb.write_string("| From | To | Kind | Failed | Issues | Health |\n")
  sb.write_string("| --- | --- | --- | ---: | ---: | ---: |\n")
  for i in 1.. TapRunRecord? {
  let mut best_index = -1
  let mut best_score = -1
  for i in 0.. best_score {
      best_score = score
      best_index = i
    }
  }
  if best_index >= 0 {
    Some(records[best_index])
  } else {
    None
  }
}

///|
/// Return the worst health-score record.
pub fn worst_run(records : Array[TapRunRecord]) -> TapRunRecord? {
  let mut worst_index = -1
  let mut worst_score = 101
  for i in 0..= 0 {
    Some(records[worst_index])
  } else {
    None
  }
}

///|
/// Return true if the last run improved from the previous run.
pub fn latest_run_improved(records : Array[TapRunRecord]) -> Bool {
  if records.length() < 2 {
    return false
  }
  let trend = compare_runs(
    records[records.length() - 2],
    records[records.length() - 1],
  )
  trend.kind == TrendImproved
}

///|
/// Return true if the last run regressed from the previous run.
pub fn latest_run_regressed(records : Array[TapRunRecord]) -> Bool {
  if records.length() < 2 {
    return false
  }
  let trend = compare_runs(
    records[records.length() - 2],
    records[records.length() - 1],
  )
  trend.kind == TrendRegressed || trend.kind == TrendMixed
}

///|
/// Return records whose report failed validation.
pub fn failed_runs(records : Array[TapRunRecord]) -> Array[TapRunRecord] {
  let items : Array[TapRunRecord] = []
  for item in records {
    if !item.report.ok {
      items.push(item)
    }
  }
  items
}

///|
/// Return records whose report passed validation.
pub fn passed_runs(records : Array[TapRunRecord]) -> Array[TapRunRecord] {
  let items : Array[TapRunRecord] = []
  for item in records {
    if item.report.ok {
      items.push(item)
    }
  }
  items
}

///|
/// Count issue occurrences across run history.
pub fn history_issue_count(records : Array[TapRunRecord], code : String) -> Int {
  let mut count = 0
  for record in records {
    for item in record.report.issues {
      if item.code == code {
        count += 1
      }
    }
  }
  count
}

///|
/// Return unique issue codes seen across a history.
pub fn history_issue_codes(records : Array[TapRunRecord]) -> Array[String] {
  let codes : Array[String] = []
  for record in records {
    for item in record.report.issues {
      if !codes.contains(item.code) {
        codes.push(item.code)
      }
    }
  }
  codes
}

///|
/// Render issue frequency as Markdown.
pub fn history_issue_frequency_markdown(
  records : Array[TapRunRecord],
) -> String {
  let sb = StringBuilder()
  sb.write_string("| Code | Severity | Count | Title |\n")
  sb.write_string("| --- | --- | ---: | --- |\n")
  for code in history_issue_codes(records) {
    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(" | \{history_issue_count(records, code)} | ")
    sb.write_string(history_cell(meta.title))
    sb.write_string(" |\n")
  }
  sb.to_string()
}

///|
fn trend_notes(
  before : TapRunRecord,
  after : TapRunRecord,
  delta : TapSummaryDelta,
) -> Array[String] {
  let notes : Array[String] = []
  if delta.failed < 0 {
    notes.push("fewer failing tests than \{before.label}")
  } else if delta.failed > 0 {
    notes.push("more failing tests than \{before.label}")
  }
  if delta.issues < 0 {
    notes.push("validation issue count decreased")
  } else if delta.issues > 0 {
    notes.push("validation issue count increased")
  }
  if delta.health > 0 {
    notes.push("health score improved by \{delta.health}")
  } else if delta.health < 0 {
    notes.push("health score regressed by \{0 - delta.health}")
  }
  if delta.total > 0 {
    notes.push("test coverage expanded by \{delta.total} points")
  }
  if after.report.ok && !before.report.ok {
    notes.push("latest run changed from failing to passing")
  }
  if !after.report.ok && before.report.ok {
    notes.push("latest run changed from passing to failing")
  }
  if notes.is_empty() {
    notes.push("no material trend detected")
  }
  notes
}

///|
fn signed_int(value : Int) -> String {
  if value > 0 {
    "+\{value}"
  } else {
    "\{value}"
  }
}

///|
fn history_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() }