///|
/// A replayable evidence case derived from one generated counterexample.
pub(all) struct WitnessCase {
  id : String
  code : String
  direction : String
  path : String
  payload : String
  accepted_by : String
  rejected_by : String
  reason : String
  assertion : String
} derive(Eq, Debug)

///|
/// A deterministic pack of negative compatibility evidence.
pub(all) struct WitnessPack {
  contract_name : String
  old_version : String
  new_version : String
  mode : CompatibilityMode
  cases : Array[WitnessCase]
} derive(Eq, Debug)

///|
/// Build a replayable evidence pack from all generated witnesses in a report.
pub fn AnalysisReport::to_witness_pack(self : AnalysisReport) -> WitnessPack {
  let cases : Array[WitnessCase] = []
  let mut ordinal = 1
  for change in self.changes {
    guard change.witness is Some(witness) else { continue }
    let id = make_witness_case_id(ordinal, change.code, change.path)
    cases.push({
      id,
      code: change.code,
      direction: change.direction,
      path: change.path,
      payload: witness.payload,
      accepted_by: witness.accepted_by,
      rejected_by: witness.rejected_by,
      reason: witness.reason,
      assertion: replay_assertion(witness),
    })
    ordinal = ordinal + 1
  }
  {
    contract_name: self.contract_name,
    old_version: self.old_version,
    new_version: self.new_version,
    mode: self.mode,
    cases,
  }
}

///|
/// Render the witness pack as JSON for archival or review systems.
pub fn WitnessPack::to_json(self : WitnessPack) -> String {
  let cases = self.cases.map(witness_case_to_json)
  "{" +
  "\"schemaVersion\":\"1.0\"," +
  "\"kind\":\"evowitness-witness-pack\"," +
  "\"contract\":" +
  json_string(self.contract_name) +
  "," +
  "\"oldVersion\":" +
  json_string(self.old_version) +
  "," +
  "\"newVersion\":" +
  json_string(self.new_version) +
  "," +
  "\"mode\":" +
  json_string(self.mode.render()) +
  "," +
  "\"caseCount\":" +
  self.cases.length().to_string() +
  "," +
  "\"cases\":[" +
  cases.join(",") +
  "]}"
}

///|
/// Render one witness per line so CI can turn each case into a separate test.
pub fn WitnessPack::to_jsonl(self : WitnessPack) -> String {
  self.cases.map(witness_case_to_json).join("\n")
}

///|
/// Render a human-readable evidence pack centered on replay assertions.
pub fn WitnessPack::to_markdown(self : WitnessPack) -> String {
  let out = StringBuilder()
  out.write_string("# EvoWitness witness pack\n\n")
  out.write_string(
    "- Contract: `" + markdown_escape(self.contract_name) + "`\n",
  )
  out.write_string(
    "- Versions: `" +
    markdown_escape(self.old_version) +
    "` -> `" +
    markdown_escape(self.new_version) +
    "`\n",
  )
  out.write_string("- Mode: `" + self.mode.render() + "`\n")
  out.write_string("- Cases: " + self.cases.length().to_string() + "\n\n")
  if self.cases.is_empty() {
    out.write_string("No witness cases were generated.\n")
    return out.to_string()
  }
  out.write_string("| Id | Direction | Code | Path | Assertion |\n")
  out.write_string("|---|---|---|---|---|\n")
  for case in self.cases {
    out.write_string("| `" + case.id + "` | ")
    out.write_string(markdown_escape(case.direction) + " | `")
    out.write_string(case.code + "` | `" + markdown_escape(case.path) + "` | ")
    out.write_string(markdown_escape(case.assertion) + " |\n")
  }
  for case in self.cases {
    out.write_string("\n## " + case.id + "\n\n")
    out.write_string("Replay assertion: " + case.assertion + "\n\n")
    out.write_string("Payload:\n\n```json\n")
    out.write_string(case.payload + "\n```\n\n")
    out.write_string("Reason: " + case.reason + "\n")
  }
  out.to_string()
}

///|
pub fn AnalysisReport::to_witness_json(self : AnalysisReport) -> String {
  self.to_witness_pack().to_json()
}

///|
pub fn AnalysisReport::to_witness_jsonl(self : AnalysisReport) -> String {
  self.to_witness_pack().to_jsonl()
}

///|
pub fn AnalysisReport::to_witness_markdown(self : AnalysisReport) -> String {
  self.to_witness_pack().to_markdown()
}

///|
fn witness_case_to_json(case : WitnessCase) -> String {
  "{" +
  "\"schemaVersion\":\"1.0\"," +
  "\"kind\":\"evowitness-witness-case\"," +
  "\"id\":" +
  json_string(case.id) +
  "," +
  "\"code\":" +
  json_string(case.code) +
  "," +
  "\"direction\":" +
  json_string(case.direction) +
  "," +
  "\"path\":" +
  json_string(case.path) +
  "," +
  "\"payload\":" +
  json_string(case.payload) +
  "," +
  "\"acceptedBy\":" +
  json_string(case.accepted_by) +
  "," +
  "\"rejectedBy\":" +
  json_string(case.rejected_by) +
  "," +
  "\"reason\":" +
  json_string(case.reason) +
  "," +
  "\"assertion\":" +
  json_string(case.assertion) +
  "}"
}

///|
fn replay_assertion(witness : Witness) -> String {
  "accept under " +
  witness.accepted_by +
  " and reject under " +
  witness.rejected_by
}

///|
fn make_witness_case_id(ordinal : Int, code : String, path : String) -> String {
  "EW-" + pad3(ordinal) + "-" + pack_slug(code + "-" + path)
}

///|
fn pad3(value : Int) -> String {
  if value < 10 {
    "00" + value.to_string()
  } else if value < 100 {
    "0" + value.to_string()
  } else {
    value.to_string()
  }
}

///|
fn pack_slug(value : String) -> String {
  let out = StringBuilder()
  let mut previous_dash = false
  for char in value.to_lower() {
    if char.is_ascii_alphabetic() || char.is_ascii_digit() {
      out.write_char(char)
      previous_dash = false
    } else if !previous_dash && !out.is_empty() {
      out.write_char('-')
      previous_dash = true
    }
  }
  let result = out.to_string()
  if result.has_suffix("-") {
    result[:result.length() - 1].to_owned()
  } else if result.is_empty() {
    "case"
  } else {
    result
  }
}