///|
/// Output level for CI annotations.
pub(all) enum TapAnnotationLevel {
  AnnotationNotice
  AnnotationWarning
  AnnotationFailure
} derive(Debug, Eq)

///|
/// One issue annotation that can be rendered for CI systems.
pub(all) struct TapAnnotation {
  suite : String
  path : String
  line : Int
  end_line : Int
  column : Int
  level : TapAnnotationLevel
  code : String
  title : String
  message : String
  remediation : String
} derive(Debug, Eq)

///|
/// A simple named text artifact such as Markdown, JSON, XML, or properties.
pub(all) struct TapArtifact {
  name : String
  media_type : String
  content : String
} derive(Debug, Eq)

///|
/// Convert a report issue to an annotation.
pub fn annotation_for_issue(
  suite : String,
  path : String,
  item : TapIssue,
) -> TapAnnotation {
  let meta = issue_meta(item.code)
  TapAnnotation::{
    suite,
    path,
    line: if item.line > 0 {
      item.line
    } else {
      1
    },
    end_line: if item.line > 0 {
      item.line
    } else {
      1
    },
    column: 1,
    level: annotation_level_for_severity(meta.severity),
    code: item.code,
    title: meta.title,
    message: item.message,
    remediation: meta.remediation,
  }
}

///|
/// Build annotations for a single TAP report.
pub fn report_annotations(
  suite : String,
  path : String,
  report : TapReport,
) -> Array[TapAnnotation] {
  let items : Array[TapAnnotation] = []
  for issue_item in report.issues {
    items.push(annotation_for_issue(suite, path, issue_item))
  }
  items
}

///|
/// Build annotations for every member of a suite.
pub fn suite_annotations(
  suite : TapSuiteReport,
  path_prefix : String,
) -> Array[TapAnnotation] {
  let items : Array[TapAnnotation] = []
  for entry in suite.reports {
    let path = if path_prefix.length() == 0 {
      entry.name
    } else {
      "\{path_prefix}/\{entry.name}"
    }
    for issue_item in entry.report.issues {
      items.push(annotation_for_issue(entry.name, path, issue_item))
    }
  }
  items
}

///|
/// Render annotations as GitHub workflow command lines.
pub fn annotations_to_github_commands(
  annotations : Array[TapAnnotation],
) -> String {
  let sb = StringBuilder()
  for item in annotations {
    sb.write_string("::")
    sb.write_string(github_level_label(item.level))
    sb.write_string(" file=")
    sb.write_string(github_escape_property(item.path))
    sb.write_string(
      ",line=\{item.line},endLine=\{item.end_line},col=\{item.column},title=",
    )
    sb.write_string(github_escape_property("\{item.code}: \{item.title}"))
    sb.write_string("::")
    sb.write_string(github_escape_message(item.message))
    sb.write_string("\n")
  }
  sb.to_string()
}

///|
/// Render annotations as Markdown.
pub fn annotations_to_markdown(annotations : Array[TapAnnotation]) -> String {
  let sb = StringBuilder()
  sb.write_string(
    "| Suite | Level | Code | Location | Message | Remediation |\n",
  )
  sb.write_string("| --- | --- | --- | --- | --- | --- |\n")
  for item in annotations {
    sb.write_string("| ")
    sb.write_string(annotation_cell(item.suite))
    sb.write_string(" | ")
    sb.write_string(annotation_level_label(item.level))
    sb.write_string(" | `")
    sb.write_string(item.code)
    sb.write_string("` | ")
    sb.write_string(annotation_cell("\{item.path}:\{item.line}"))
    sb.write_string(" | ")
    sb.write_string(annotation_cell(item.message))
    sb.write_string(" | ")
    sb.write_string(annotation_cell(item.remediation))
    sb.write_string(" |\n")
  }
  sb.to_string()
}

///|
/// Render annotations as a compact JSON array.
pub fn annotations_to_json(annotations : Array[TapAnnotation]) -> String {
  let sb = StringBuilder()
  sb.write_string("[")
  for item in annotations; first = true {
    if first {
      ()
    } else {
      sb.write_string(",")
    }
    sb.write_string("{\"suite\":\"")
    sb.write_string(json_escape(item.suite))
    sb.write_string("\",\"level\":\"")
    sb.write_string(annotation_level_label(item.level))
    sb.write_string("\",\"code\":\"")
    sb.write_string(json_escape(item.code))
    sb.write_string("\",\"path\":\"")
    sb.write_string(json_escape(item.path))
    sb.write_string("\",\"line\":\{item.line}")
    sb.write_string(",\"title\":\"")
    sb.write_string(json_escape(item.title))
    sb.write_string("\",\"message\":\"")
    sb.write_string(json_escape(item.message))
    sb.write_string("\",\"remediation\":\"")
    sb.write_string(json_escape(item.remediation))
    sb.write_string("\"}")
    continue false
  }
  sb.write_string("]")
  sb.to_string()
}

///|
/// Render report key-value pairs for shell-independent CI artifact files.
pub fn report_to_properties(report : TapReport, prefix : String) -> String {
  let key = normalize_key_prefix(prefix)
  let sb = StringBuilder()
  sb.write_string("\{key}.ok=\{report.ok}\n")
  sb.write_string("\{key}.planned=\{report.summary.planned}\n")
  sb.write_string("\{key}.total=\{report.summary.total}\n")
  sb.write_string("\{key}.passed=\{report.summary.passed}\n")
  sb.write_string("\{key}.failed=\{report.summary.failed}\n")
  sb.write_string("\{key}.skipped=\{report.summary.skipped}\n")
  sb.write_string("\{key}.todo=\{report.summary.todo}\n")
  sb.write_string("\{key}.diagnostics=\{report.summary.diagnostics}\n")
  sb.write_string("\{key}.yaml_blocks=\{report.summary.yaml_blocks}\n")
  sb.write_string("\{key}.issues=\{report.issues.length()}\n")
  sb.write_string("\{key}.score=\{health_score(report)}\n")
  sb.to_string()
}

///|
/// Render suite key-value pairs for artifact files.
pub fn suite_to_properties(suite : TapSuiteReport, prefix : String) -> String {
  let key = normalize_key_prefix(prefix)
  let sb = StringBuilder()
  sb.write_string("\{key}.ok=\{suite.ok}\n")
  sb.write_string("\{key}.suites=\{suite.summary.suites}\n")
  sb.write_string("\{key}.passed_suites=\{suite.summary.passed_suites}\n")
  sb.write_string("\{key}.failed_suites=\{suite.summary.failed_suites}\n")
  sb.write_string("\{key}.planned=\{suite.summary.planned}\n")
  sb.write_string("\{key}.total=\{suite.summary.total}\n")
  sb.write_string("\{key}.passed=\{suite.summary.passed}\n")
  sb.write_string("\{key}.failed=\{suite.summary.failed}\n")
  sb.write_string("\{key}.skipped=\{suite.summary.skipped}\n")
  sb.write_string("\{key}.todo=\{suite.summary.todo}\n")
  sb.write_string("\{key}.issues=\{suite.summary.issues}\n")
  sb.write_string("\{key}.score=\{suite_health_score(suite)}\n")
  sb.to_string()
}

///|
/// Render one report as GitHub environment lines.
pub fn report_to_env_lines(report : TapReport, prefix : String) -> String {
  let name = normalize_env_prefix(prefix)
  let sb = StringBuilder()
  sb.write_string("\{name}_OK=\{report.ok}\n")
  sb.write_string("\{name}_STATUS=\{if report.ok { "ok" } else { "failed" }}\n")
  sb.write_string("\{name}_PLANNED=\{report.summary.planned}\n")
  sb.write_string("\{name}_TOTAL=\{report.summary.total}\n")
  sb.write_string("\{name}_PASSED=\{report.summary.passed}\n")
  sb.write_string("\{name}_FAILED=\{report.summary.failed}\n")
  sb.write_string("\{name}_SKIPPED=\{report.summary.skipped}\n")
  sb.write_string("\{name}_TODO=\{report.summary.todo}\n")
  sb.write_string("\{name}_ISSUES=\{report.issues.length()}\n")
  sb.write_string("\{name}_SCORE=\{health_score(report)}\n")
  sb.to_string()
}

///|
/// Render one report as TeamCity service messages.
pub fn report_to_teamcity_messages(
  report : TapReport,
  flow_id : String,
) -> String {
  let flow = if flow_id.length() == 0 { "taptrail" } else { flow_id }
  let sb = StringBuilder()
  for point in report.document.points {
    let name = teamcity_escape(teamcity_test_name(point))
    sb.write_string("##teamcity[testStarted name='")
    sb.write_string(name)
    sb.write_string("' flowId='")
    sb.write_string(teamcity_escape(flow))
    sb.write_string("']\n")
    if !point.ok {
      match point.directive {
        Todo(reason) => {
          sb.write_string("##teamcity[testIgnored name='")
          sb.write_string(name)
          sb.write_string("' message='TODO: ")
          sb.write_string(teamcity_escape(reason))
          sb.write_string("' flowId='")
          sb.write_string(teamcity_escape(flow))
          sb.write_string("']\n")
        }
        _ => {
          sb.write_string("##teamcity[testFailed name='")
          sb.write_string(name)
          sb.write_string("' message='not ok' details='")
          sb.write_string(teamcity_escape(point.raw))
          sb.write_string("' flowId='")
          sb.write_string(teamcity_escape(flow))
          sb.write_string("']\n")
        }
      }
    } else {
      match point.directive {
        Skip(reason) => {
          sb.write_string("##teamcity[testIgnored name='")
          sb.write_string(name)
          sb.write_string("' message='")
          sb.write_string(teamcity_escape(reason))
          sb.write_string("' flowId='")
          sb.write_string(teamcity_escape(flow))
          sb.write_string("']\n")
        }
        Todo(reason) => {
          sb.write_string("##teamcity[testIgnored name='")
          sb.write_string(name)
          sb.write_string("' message='TODO: ")
          sb.write_string(teamcity_escape(reason))
          sb.write_string("' flowId='")
          sb.write_string(teamcity_escape(flow))
          sb.write_string("']\n")
        }
        NoDirective => ()
      }
    }
    sb.write_string("##teamcity[testFinished name='")
    sb.write_string(name)
    sb.write_string("' flowId='")
    sb.write_string(teamcity_escape(flow))
    sb.write_string("']\n")
  }
  sb.to_string()
}

///|
/// Render Checkstyle-compatible XML for issue dashboards.
pub fn report_to_checkstyle_xml(
  report : TapReport,
  file_name : String,
) -> String {
  let file = if file_name.length() == 0 { "tap.tap" } else { file_name }
  let sb = StringBuilder()
  sb.write_string("\n")
  sb.write_string("\n")
  sb.write_string("  \n")
  for item in report.issues {
    let meta = issue_meta(item.code)
    sb.write_string("     0 { item.line } else { 1 }}")
    sb.write_string("\" column=\"1\" severity=\"")
    sb.write_string(checkstyle_severity(meta.severity))
    sb.write_string("\" message=\"")
    sb.write_string(xml_escape(item.message))
    sb.write_string("\" source=\"taptrail.")
    sb.write_string(xml_escape(item.code))
    sb.write_string("\" />\n")
  }
  sb.write_string("  \n")
  sb.write_string("\n")
  sb.to_string()
}

///|
/// Render Checkstyle-compatible XML for suite issues.
pub fn suite_to_checkstyle_xml(suite : TapSuiteReport) -> String {
  let sb = StringBuilder()
  sb.write_string("\n")
  sb.write_string("\n")
  for entry in suite.reports {
    sb.write_string("  \n")
    for item in entry.report.issues {
      let meta = issue_meta(item.code)
      sb.write_string("     0 { item.line } else { 1 }}")
      sb.write_string("\" column=\"1\" severity=\"")
      sb.write_string(checkstyle_severity(meta.severity))
      sb.write_string("\" message=\"")
      sb.write_string(xml_escape(item.message))
      sb.write_string("\" source=\"taptrail.")
      sb.write_string(xml_escape(item.code))
      sb.write_string("\" />\n")
    }
    sb.write_string("  \n")
  }
  sb.write_string("\n")
  sb.to_string()
}

///|
/// Render a Shields endpoint JSON badge for a report.
pub fn report_to_shields_json(report : TapReport, label : String) -> String {
  let badge_label = if label.length() == 0 { "taptrail" } else { label }
  let message = if report.ok {
    "passing"
  } else {
    "\{report.issues.length()} issues"
  }
  let color = if report.ok { "brightgreen" } else { "red" }
  "{\"schemaVersion\":1,\"label\":\"\{json_escape(badge_label)}\",\"message\":\"\{json_escape(message)}\",\"color\":\"\{color}\"}"
}

///|
/// Render a Shields endpoint JSON badge for a suite.
pub fn suite_to_shields_json(suite : TapSuiteReport, label : String) -> String {
  let badge_label = if label.length() == 0 { "taptrail suite" } else { label }
  let message = if suite.ok {
    "\{suite.summary.passed_suites}/\{suite.summary.suites} suites"
  } else {
    "\{suite.summary.failed_suites} failed"
  }
  let color = if suite.ok { "brightgreen" } else { "red" }
  "{\"schemaVersion\":1,\"label\":\"\{json_escape(badge_label)}\",\"message\":\"\{json_escape(message)}\",\"color\":\"\{color}\"}"
}

///|
/// Render a Markdown badge link for GitHub README files.
pub fn report_badge_markdown(
  report : TapReport,
  label : String,
  target_url : String,
) -> String {
  let title = if label.length() == 0 { "taptrail" } else { label }
  let status = if report.ok { "passing" } else { "failing" }
  let color = if report.ok { "brightgreen" } else { "red" }
  let url = "https://img.shields.io/badge/\{url_component(title)}-\{url_component(status)}-\{color}"
  if target_url.length() == 0 {
    "![\{title}](\{url})"
  } else {
    "[![\{title}](\{url})](\{target_url})"
  }
}

///|
/// Render a Markdown badge link for suite status.
pub fn suite_badge_markdown(
  suite : TapSuiteReport,
  label : String,
  target_url : String,
) -> String {
  let title = if label.length() == 0 { "taptrail suite" } else { label }
  let status = if suite.ok {
    "\{suite.summary.passed_suites} of \{suite.summary.suites} ok"
  } else {
    "\{suite.summary.failed_suites} failed"
  }
  let color = if suite.ok { "brightgreen" } else { "red" }
  let url = "https://img.shields.io/badge/\{url_component(title)}-\{url_component(status)}-\{color}"
  if target_url.length() == 0 {
    "![\{title}](\{url})"
  } else {
    "[![\{title}](\{url})](\{target_url})"
  }
}

///|
/// Package common report outputs in one call.
pub fn report_artifacts(
  name : String,
  report : TapReport,
) -> Array[TapArtifact] {
  [
    TapArtifact::{
      name: "\{name}.md",
      media_type: "text/markdown",
      content: to_markdown(report),
    },
    TapArtifact::{
      name: "\{name}.json",
      media_type: "application/json",
      content: to_compact_json(report),
    },
    TapArtifact::{
      name: "\{name}.xml",
      media_type: "application/xml",
      content: to_junit_xml(report, suite_name=name),
    },
    TapArtifact::{
      name: "\{name}.properties",
      media_type: "text/plain",
      content: report_to_properties(report, "taptrail"),
    },
  ]
}

///|
/// Package common suite outputs in one call.
pub fn suite_artifacts(suite : TapSuiteReport) -> Array[TapArtifact] {
  [
    TapArtifact::{
      name: "\{suite.name}.md",
      media_type: "text/markdown",
      content: suite_to_markdown(suite),
    },
    TapArtifact::{
      name: "\{suite.name}.json",
      media_type: "application/json",
      content: suite_to_compact_json(suite),
    },
    TapArtifact::{
      name: "\{suite.name}.xml",
      media_type: "application/xml",
      content: suite_to_junit_xml(suite),
    },
    TapArtifact::{
      name: "\{suite.name}.csv",
      media_type: "text/csv",
      content: suite_to_csv(suite),
    },
  ]
}

///|
/// Render a high-signal Markdown review for one report.
pub fn report_review_markdown(
  name : String,
  report : TapReport,
  policy : TapPolicy,
) -> String {
  let result = evaluate_policy(report, policy)
  let sb = StringBuilder()
  sb.write_string("# TAP Review: ")
  sb.write_string(name)
  sb.write_string("\n\n")
  sb.write_string("- status: ")
  sb.write_string(if result.ok { "ok" } else { "failed" })
  sb.write_string("\n")
  sb.write_string("- policy: ")
  sb.write_string(result.policy_name)
  sb.write_string("\n")
  sb.write_string("- health: ")
  sb.write_string("\{health_score(report)}")
  sb.write_string(" (")
  sb.write_string(score_grade(health_score(report)))
  sb.write_string(")\n")
  sb.write_string("- planned: \{report.summary.planned}\n")
  sb.write_string("- total: \{report.summary.total}\n")
  sb.write_string("- passed: \{report.summary.passed}\n")
  sb.write_string("- failed: \{report.summary.failed}\n")
  sb.write_string("- skipped: \{report.summary.skipped}\n")
  sb.write_string("- todo: \{report.summary.todo}\n")
  if report.issues.length() > 0 {
    sb.write_string("\n## Findings\n\n")
    for issue_item in report.issues {
      let meta = issue_meta(issue_item.code)
      sb.write_string("- ")
      sb.write_string(severity_label(meta.severity))
      sb.write_string(" `")
      sb.write_string(issue_item.code)
      sb.write_string("`: ")
      sb.write_string(issue_item.message)
      sb.write_string(" Remedy: ")
      sb.write_string(meta.remediation)
      sb.write_string("\n")
    }
  } else {
    sb.write_string("\nNo findings.\n")
  }
  sb.to_string()
}

///|
/// Map issue severity to annotation level.
pub fn annotation_level_for_severity(
  severity : TapSeverity,
) -> TapAnnotationLevel {
  match severity {
    SeverityInfo => AnnotationNotice
    SeverityWarning => AnnotationWarning
    SeverityError => AnnotationFailure
  }
}

///|
/// Human-readable annotation level label.
pub fn annotation_level_label(level : TapAnnotationLevel) -> String {
  match level {
    AnnotationNotice => "notice"
    AnnotationWarning => "warning"
    AnnotationFailure => "failure"
  }
}

///|
fn github_level_label(level : TapAnnotationLevel) -> String {
  match level {
    AnnotationNotice => "notice"
    AnnotationWarning => "warning"
    AnnotationFailure => "error"
  }
}

///|
fn checkstyle_severity(severity : TapSeverity) -> String {
  match severity {
    SeverityInfo => "info"
    SeverityWarning => "warning"
    SeverityError => "error"
  }
}

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

///|
fn normalize_key_prefix(prefix : String) -> String {
  if prefix.length() == 0 {
    "taptrail"
  } else {
    prefix
  }
}

///|
fn normalize_env_prefix(prefix : String) -> String {
  let raw = if prefix.length() == 0 { "TAPTRAIL" } else { prefix }
  let sb = StringBuilder()
  for ch in raw {
    let code = ch.to_int()
    if code >= 97 && code <= 122 {
      sb.write_char(Int::unsafe_to_char(code - 32))
    } else if code >= 65 && code <= 90 {
      sb.write_char(ch)
    } else if code >= 48 && code <= 57 {
      sb.write_char(ch)
    } else {
      sb.write_string("_")
    }
  }
  sb.to_string()
}

///|
fn github_escape_property(text : String) -> String {
  let sb = StringBuilder()
  for ch in text {
    match ch {
      '%' => sb.write_string("%25")
      '\r' => sb.write_string("%0D")
      '\n' => sb.write_string("%0A")
      ':' => sb.write_string("%3A")
      ',' => sb.write_string("%2C")
      _ => sb.write_char(ch)
    }
  }
  sb.to_string()
}

///|
fn github_escape_message(text : String) -> String {
  let sb = StringBuilder()
  for ch in text {
    match ch {
      '%' => sb.write_string("%25")
      '\r' => sb.write_string("%0D")
      '\n' => sb.write_string("%0A")
      _ => sb.write_char(ch)
    }
  }
  sb.to_string()
}

///|
fn teamcity_escape(text : String) -> String {
  let sb = StringBuilder()
  for ch in text {
    match ch {
      '|' => sb.write_string("||")
      '\'' => sb.write_string("|'")
      '\n' => sb.write_string("|n")
      '\r' => sb.write_string("|r")
      '[' => sb.write_string("|[")
      ']' => sb.write_string("|]")
      _ => sb.write_char(ch)
    }
  }
  sb.to_string()
}

///|
fn annotation_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 url_component(text : String) -> String { let sb = StringBuilder() for ch in text { match ch { ' ' => sb.write_string("%20") '-' => sb.write_string("--") '_' => sb.write_string("__") '/' => sb.write_string("%2F") ':' => sb.write_string("%3A") '?' => sb.write_string("%3F") '&' => sb.write_string("%26") '#' => sb.write_string("%23") _ => sb.write_char(ch) } } sb.to_string() }