///|
/// A replay step kind derived from one witness case.
pub(all) enum ReplayStepKind {
  ExpectSourceAccept
  ExpectTargetReject
  PreserveRegression
} derive(Eq, Debug)

///|
/// One deterministic action a downstream test runner can perform.
pub(all) struct ReplayStep {
  id : String
  case_id : String
  kind : ReplayStepKind
  contract_version : String
  expected_valid : Bool
  path : String
  payload : String
  assertion : String
  note : String
} derive(Eq, Debug)

///|
/// A portable replay plan for turning witness cases into negative tests.
pub(all) struct ReplaySuite {
  contract_name : String
  old_version : String
  new_version : String
  mode : CompatibilityMode
  case_count : Int
  steps : Array[ReplayStep]
} derive(Eq, Debug)

///|
pub fn ReplayStepKind::render(self : ReplayStepKind) -> String {
  match self {
    ExpectSourceAccept => "expect-source-accept"
    ExpectTargetReject => "expect-target-reject"
    PreserveRegression => "preserve-regression"
  }
}

///|
pub fn ReplayStepKind::title(self : ReplayStepKind) -> String {
  match self {
    ExpectSourceAccept => "Accept source side"
    ExpectTargetReject => "Reject target side"
    PreserveRegression => "Keep regression guard"
  }
}

///|
pub fn WitnessPack::to_replay_suite(self : WitnessPack) -> ReplaySuite {
  let steps : Array[ReplayStep] = []
  for case in self.cases {
    append_case_replay_steps(case, steps)
  }
  {
    contract_name: self.contract_name,
    old_version: self.old_version,
    new_version: self.new_version,
    mode: self.mode,
    case_count: self.cases.length(),
    steps,
  }
}

///|
pub fn AnalysisReport::to_replay_suite(self : AnalysisReport) -> ReplaySuite {
  self.to_witness_pack().to_replay_suite()
}

///|
pub fn ReplaySuite::accept_step_count(self : ReplaySuite) -> Int {
  self.steps.count_if(step => step.kind == ExpectSourceAccept)
}

///|
pub fn ReplaySuite::reject_step_count(self : ReplaySuite) -> Int {
  self.steps.count_if(step => step.kind == ExpectTargetReject)
}

///|
pub fn ReplaySuite::guard_step_count(self : ReplaySuite) -> Int {
  self.steps.count_if(step => step.kind == PreserveRegression)
}

///|
pub fn ReplaySuite::to_json(self : ReplaySuite) -> String {
  let steps = self.steps.map(replay_step_to_json)
  "{" +
  "\"schemaVersion\":\"1.0\"," +
  "\"kind\":\"evowitness-replay-suite\"," +
  "\"contract\":" +
  json_string(self.contract_name) +
  "," +
  "\"oldVersion\":" +
  json_string(self.old_version) +
  "," +
  "\"newVersion\":" +
  json_string(self.new_version) +
  "," +
  "\"mode\":" +
  json_string(self.mode.render()) +
  "," +
  "\"summary\":{" +
  "\"cases\":" +
  self.case_count.to_string() +
  "," +
  "\"steps\":" +
  self.steps.length().to_string() +
  "," +
  "\"acceptSteps\":" +
  self.accept_step_count().to_string() +
  "," +
  "\"rejectSteps\":" +
  self.reject_step_count().to_string() +
  "," +
  "\"guardSteps\":" +
  self.guard_step_count().to_string() +
  "}," +
  "\"steps\":[" +
  steps.join(",") +
  "]}"
}

///|
pub fn ReplaySuite::to_markdown(self : ReplaySuite) -> String {
  let out = StringBuilder()
  out.write_string("# EvoWitness replay suite\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("- Witness cases: " + self.case_count.to_string() + "\n")
  out.write_string(
    "- Replay steps: " + self.steps.length().to_string() + "\n\n",
  )
  if self.steps.is_empty() {
    out.write_string("No replay steps were generated.\n")
    return out.to_string()
  }
  out.write_string("| Step | Kind | Contract | Expected | Case | Path |\n")
  out.write_string("|---|---|---|---|---|---|\n")
  for step in self.steps {
    out.write_string("| `" + step.id + "` | ")
    out.write_string(step.kind.render() + " | `")
    out.write_string(markdown_escape(step.contract_version) + "` | ")
    out.write_string(expected_word(step.expected_valid) + " | `")
    out.write_string(markdown_escape(step.case_id) + "` | `")
    out.write_string(markdown_escape(step.path) + "` |\n")
  }
  out.write_string("\n## Replay details\n\n")
  for step in self.steps {
    out.write_string("### " + step.id + "\n\n")
    out.write_string("- Kind: `" + step.kind.render() + "`\n")
    out.write_string(
      "- Contract: `" + markdown_escape(step.contract_version) + "`\n",
    )
    out.write_string(
      "- Expected: `" + expected_word(step.expected_valid) + "`\n",
    )
    out.write_string("- Assertion: " + markdown_escape(step.assertion) + "\n")
    out.write_string("- Note: " + markdown_escape(step.note) + "\n\n")
    out.write_string("```json\n")
    out.write_string(step.payload + "\n")
    out.write_string("```\n\n")
  }
  out.to_string()
}

///|
pub fn ReplaySuite::to_checklist(self : ReplaySuite) -> String {
  let out = StringBuilder()
  out.write_string("# Replay checklist\n\n")
  for step in self.steps {
    out.write_string("- [ ] `" + step.id + "` ")
    out.write_string(step.kind.title() + " with `")
    out.write_string(markdown_escape(step.contract_version) + "` should be `")
    out.write_string(expected_word(step.expected_valid) + "` for `")
    out.write_string(markdown_escape(step.case_id) + "` at `")
    out.write_string(markdown_escape(step.path) + "`.\n")
  }
  if self.steps.is_empty() {
    out.write_string("- [ ] No witness cases to replay.\n")
  }
  out.to_string()
}

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

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

///|
pub fn AnalysisReport::to_replay_checklist(self : AnalysisReport) -> String {
  self.to_replay_suite().to_checklist()
}

///|
fn append_case_replay_steps(
  case : WitnessCase,
  steps : Array[ReplayStep],
) -> Unit {
  steps.push(
    replay_step(
      case,
      "accept",
      ExpectSourceAccept,
      case.accepted_by,
      true,
      "Replay the payload against the contract version that originally accepted it.",
    ),
  )
  steps.push(
    replay_step(
      case,
      "reject",
      ExpectTargetReject,
      case.rejected_by,
      false,
      "Replay the same payload against the contract version that should reject it.",
    ),
  )
  steps.push(
    replay_step(
      case,
      "guard",
      PreserveRegression,
      case.rejected_by,
      false,
      "Keep this case as a regression guard until the compatibility break is retired.",
    ),
  )
}

///|
fn replay_step(
  case : WitnessCase,
  suffix : String,
  kind : ReplayStepKind,
  contract_version : String,
  expected_valid : Bool,
  note : String,
) -> ReplayStep {
  {
    id: case.id + "-" + suffix,
    case_id: case.id,
    kind,
    contract_version,
    expected_valid,
    path: case.path,
    payload: case.payload,
    assertion: case.assertion,
    note,
  }
}

///|
fn replay_step_to_json(step : ReplayStep) -> String {
  "{" +
  "\"id\":" +
  json_string(step.id) +
  "," +
  "\"caseId\":" +
  json_string(step.case_id) +
  "," +
  "\"kind\":" +
  json_string(step.kind.render()) +
  "," +
  "\"contractVersion\":" +
  json_string(step.contract_version) +
  "," +
  "\"expectedValid\":" +
  bool_json(step.expected_valid) +
  "," +
  "\"path\":" +
  json_string(step.path) +
  "," +
  "\"payload\":" +
  json_string(step.payload) +
  "," +
  "\"assertion\":" +
  json_string(step.assertion) +
  "," +
  "\"note\":" +
  json_string(step.note) +
  "}"
}

///|
fn expected_word(value : Bool) -> String {
  if value {
    "valid"
  } else {
    "invalid"
  }
}