///|
pub(all) enum TraceViolationSeverity {
  Warning
  Error
} derive(Debug, Eq, ToJson)

///|
pub struct TraceViolation {
  contract : String
  code : String
  severity : TraceViolationSeverity
  step : Int
  targets : Array[TargetRef]
  message : String
  evidence : Array[TraceAttribute]
} derive(Debug, Eq, ToJson)

///|
pub fn TraceViolation::new(
  contract~ : String,
  code~ : String,
  severity? : TraceViolationSeverity = Error,
  step? : Int = -1,
  targets? : Array[TargetRef] = [],
  message~ : String,
  evidence? : Array[TraceAttribute] = [],
) -> TraceViolation {
  { contract, code, severity, step, targets, message, evidence }
}

///|
pub struct ContractReport {
  report_version : String
  contract_name : String
  passed : Bool
  violations : Array[TraceViolation]
  first_failure : TraceViolation?
  counterexample : TraceCounterexample?
} derive(Debug, Eq, ToJson)

///|
pub fn ContractReport::to_json_string(self : ContractReport) -> String {
  self.to_json().stringify(indent=2)
}

///|
pub fn ContractReport::report(self : ContractReport) -> String {
  let out = StringBuilder()
  out.write_string(
    "Contract \{self.contract_name}: \{if self.passed { "PASS" } else { "FAIL" }}\n",
  )
  for violation in self.violations {
    let where_text = if violation.step < 0 {
      "trace"
    } else {
      "step \{violation.step}"
    }
    out.write_string(
      "- [\{contract_severity_name(violation.severity)}] \{violation.code} at \{where_text}: \{violation.message}\n",
    )
  }
  out.to_string()
}

///|
pub fn ContractReport::to_markdown(self : ContractReport) -> String {
  let out = StringBuilder()
  out.write_string("# FrontierLab contract report\n\n")
  out.write_string("- Contract: \{self.contract_name}\n")
  out.write_string("- Passed: \{self.passed}\n")
  out.write_string("- Violations: \{self.violations.length()}\n\n")
  out.write_string("| Severity | Code | Step | Message |\n")
  out.write_string("|---|---|---:|---|\n")
  for violation in self.violations {
    out.write_string(
      "| \{contract_severity_name(violation.severity)} | \{contract_escape(violation.code)} | \{violation.step} | \{contract_escape(violation.message)} |\n",
    )
  }
  out.to_string()
}

///|
/// An extensible semantic contract backed by a pure MoonBit checker callback.
pub struct TraceContract {
  name : String
  description : String
  checker : (AlgorithmTrace) -> Array[TraceViolation]
}

///|
pub fn TraceContract::new(
  name~ : String,
  description~ : String,
  checker~ : (AlgorithmTrace) -> Array[TraceViolation],
) -> TraceContract {
  { name, description, checker }
}

///|
pub fn TraceContract::check(
  self : TraceContract,
  trace : AlgorithmTrace,
) -> ContractReport {
  let violations = (self.checker)(trace)
  let first_failure = violations.search_by(fn(value) { value.severity is Error })
  let first = match first_failure {
    Some(index) => Some(violations[index])
    None => None
  }
  let counterexample = match first {
    Some(violation) if violation.step >= 0 =>
      Some(try! trace.slice(center=violation.step))
    _ => None
  }
  {
    report_version: debug_report_version,
    contract_name: self.name,
    passed: first is None,
    violations,
    first_failure: first,
    counterexample,
  }
}

///|
pub fn sequence_transition_contract(object_id~ : String) -> TraceContract {
  TraceContract::new(
    name="sequence-transition",
    description="Stable sequence identity and event transition semantics.",
    checker=fn(trace) { check_sequence_transitions(trace, object_id) },
  )
}

///|
pub fn insertion_sort_int_contract(object_id~ : String) -> TraceContract {
  TraceContract::new(
    name="insertion-sort-int",
    description="Sequence transitions plus a nondecreasing integer result.",
    checker=fn(trace) {
      let violations = check_sequence_transitions(trace, object_id)
      check_integer_sort_result(
        trace, object_id, "insertion-sort-int", violations,
      )
      violations
    },
  )
}

///|
/// Check stable sequence transitions and a nondecreasing integer result.
///
/// Unlike `insertion_sort_int_contract`, this name is algorithm-neutral and
/// is suitable for selection sort, merge sort, agent-generated traces, and
/// other sorting implementations.
pub fn sorted_int_sequence_contract(object_id~ : String) -> TraceContract {
  TraceContract::new(
    name="sorted-int-sequence",
    description="Stable sequence transitions plus a nondecreasing integer result.",
    checker=fn(trace) {
      let violations = check_sequence_transitions(trace, object_id)
      check_integer_sort_result(
        trace, object_id, "sorted-int-sequence", violations,
      )
      violations
    },
  )
}

///|
pub fn grid_path_contract(object_id~ : String) -> TraceContract {
  TraceContract::new(
    name="grid-path",
    description="A continuous, walkable start-to-end result path.",
    checker=fn(trace) { check_grid_path(trace, object_id) },
  )
}

///|
fn check_sequence_transitions(
  trace : AlgorithmTrace,
  object_id : String,
) -> Array[TraceViolation] {
  let violations : Array[TraceViolation] = []
  let mut previous = contract_sequence(trace.initial_scene, object_id)
  if previous is None {
    violations.push(
      TraceViolation::new(
        contract="sequence-transition",
        code="missing-sequence",
        message="Initial scene has no sequence object named \{object_id}.",
        targets=[TargetRef::object(object_id)],
      ),
    )
    return violations
  }
  for step in trace.steps {
    let current = contract_sequence(step.scene, object_id)
    match (previous, current) {
      (Some(before), Some(after)) => {
        check_sequence_identity(
          before,
          after,
          step.index,
          object_id,
          violations,
        )
        match step.event {
          Compare(_) =>
            if contract_sequence_ids(before) != contract_sequence_ids(after) {
              violations.push(
                TraceViolation::new(
                  contract="sequence-transition",
                  code="compare-mutated-order",
                  step=step.index,
                  message="Compare changed sequence order.",
                  targets=[TargetRef::object(object_id)],
                ),
              )
            }
          Swap(left, right) if left.object_id == object_id &&
            right.object_id == object_id =>
            check_swap_transition(
              before,
              after,
              left,
              right,
              step.index,
              violations,
            )
          _ => ()
        }
      }
      (_, None) =>
        violations.push(
          TraceViolation::new(
            contract="sequence-transition",
            code="sequence-disappeared",
            step=step.index,
            message="Sequence \{object_id} disappeared from the scene.",
            targets=[TargetRef::object(object_id)],
          ),
        )
      _ => ()
    }
    previous = current
  }
  violations
}

///|
fn check_sequence_identity(
  before : SequenceState,
  after : SequenceState,
  step : Int,
  object_id : String,
  violations : Array[TraceViolation],
) -> Unit {
  for item in before.items {
    match after.items.search_by(fn(value) { value.id == item.id }) {
      Some(index) =>
        if after.items[index].value != item.value {
          violations.push(
            TraceViolation::new(
              contract="sequence-transition",
              code="stable-value-changed",
              step~,
              targets=[TargetRef::entity(object_id, item.id)],
              message="Stable entity \{item.id} changed value.",
              evidence=[
                TraceAttribute::new(key="before", value=item.value),
                TraceAttribute::new(key="after", value=after.items[index].value),
              ],
            ),
          )
        }
      None =>
        violations.push(
          TraceViolation::new(
            contract="sequence-transition",
            code="stable-entity-removed",
            step~,
            targets=[TargetRef::entity(object_id, item.id)],
            message="Stable entity \{item.id} was removed.",
          ),
        )
    }
  }
  for item in after.items {
    if !before.items.any(fn(value) { value.id == item.id }) {
      violations.push(
        TraceViolation::new(
          contract="sequence-transition",
          code="stable-entity-added",
          step~,
          targets=[TargetRef::entity(object_id, item.id)],
          message="Unexpected stable entity \{item.id} was added.",
        ),
      )
    }
  }
}

///|
fn check_swap_transition(
  before : SequenceState,
  after : SequenceState,
  left : TargetRef,
  right : TargetRef,
  step : Int,
  violations : Array[TraceViolation],
) -> Unit {
  match (left.entity_id, right.entity_id) {
    (Some(left_id), Some(right_id)) => {
      let expected = contract_sequence_ids(before)
      let left_index = expected.search_by(fn(id) { id == left_id })
      let right_index = expected.search_by(fn(id) { id == right_id })
      match (left_index, right_index) {
        (Some(a), Some(b)) => {
          let temporary = expected[a]
          expected[a] = expected[b]
          expected[b] = temporary
          if expected != contract_sequence_ids(after) {
            violations.push(
              TraceViolation::new(
                contract="sequence-transition",
                code="invalid-swap-transition",
                step~,
                targets=[left, right],
                message="Swap changed more than the two declared entities or did not swap them.",
                evidence=[
                  TraceAttribute::new(
                    key="expected_ids",
                    value=expected.join(","),
                  ),
                  TraceAttribute::new(
                    key="actual_ids",
                    value=contract_sequence_ids(after).join(","),
                  ),
                ],
              ),
            )
          }
        }
        _ => ()
      }
    }
    _ => ()
  }
}

///|
fn check_integer_sort_result(
  trace : AlgorithmTrace,
  object_id : String,
  contract_name : String,
  violations : Array[TraceViolation],
) -> Unit {
  let final_scene = if trace.steps.is_empty() {
    trace.initial_scene
  } else {
    trace.steps.last().unwrap().scene
  }
  match contract_sequence(final_scene, object_id) {
    Some(sequence) =>
      for index in 1.. {
            violations.push(
              TraceViolation::new(
                contract=contract_name,
                code="non-integer-value",
                step=trace.steps.length() - 1,
                targets=[
                  TargetRef::entity(object_id, sequence.items[index - 1].id),
                ],
                message="Sequence value is not an integer.",
              ),
            )
            0
          }
        }
        let right = @strconv.parse_int(sequence.items[index].value) catch {
          _ => {
            violations.push(
              TraceViolation::new(
                contract=contract_name,
                code="non-integer-value",
                step=trace.steps.length() - 1,
                targets=[TargetRef::entity(object_id, sequence.items[index].id)],
                message="Sequence value is not an integer.",
              ),
            )
            0
          }
        }
        if left > right {
          violations.push(
            TraceViolation::new(
              contract=contract_name,
              code="result-not-sorted",
              step=trace.steps.length() - 1,
              targets=[
                TargetRef::entity(object_id, sequence.items[index - 1].id),
                TargetRef::entity(object_id, sequence.items[index].id),
              ],
              message="Final sequence contains inversion \{left} > \{right}.",
            ),
          )
          return
        }
      }
    None => ()
  }
}

///|
fn check_grid_path(
  trace : AlgorithmTrace,
  object_id : String,
) -> Array[TraceViolation] {
  let violations : Array[TraceViolation] = []
  let final_scene = if trace.steps.is_empty() {
    trace.initial_scene
  } else {
    trace.steps.last().unwrap().scene
  }
  match contract_grid(final_scene, object_id) {
    None =>
      violations.push(
        TraceViolation::new(
          contract="grid-path",
          code="missing-grid",
          targets=[TargetRef::object(object_id)],
          message="Final scene has no grid object named \{object_id}.",
        ),
      )
    Some(grid) => {
      let result_targets = final_scene.highlights.filter_map(fn(highlight) {
        if highlight.role is Result && highlight.target.object_id == object_id {
          highlight.target.entity_id
        } else {
          None
        }
      })
      let result_cells : Array[GridCellState] = []
      for id in result_targets {
        match grid.cells.search_by(fn(cell) { cell.id == id }) {
          Some(index) => {
            let cell = grid.cells[index]
            if cell.blocked {
              violations.push(
                TraceViolation::new(
                  contract="grid-path",
                  code="path-crosses-blocked-cell",
                  step=trace.steps.length() - 1,
                  targets=[TargetRef::entity(object_id, id)],
                  message="Result path crosses blocked cell \{id}.",
                ),
              )
            }
            result_cells.push(cell)
          }
          None => ()
        }
      }
      for index in 1.. SequenceState? {
  match scene.objects.search_by(fn(object) { object.id() == id }) {
    Some(index) =>
      match scene.objects[index] {
        Sequence(state) => Some(state)
        _ => None
      }
    None => None
  }
}

///|
fn contract_grid(scene : Scene, id : String) -> GridState? {
  match scene.objects.search_by(fn(object) { object.id() == id }) {
    Some(index) =>
      match scene.objects[index] {
        Grid(state) => Some(state)
        _ => None
      }
    None => None
  }
}

///|
fn contract_sequence_ids(sequence : SequenceState) -> Array[String] {
  sequence.items.map(fn(item) { item.id })
}

///|
fn contract_severity_name(severity : TraceViolationSeverity) -> String {
  match severity {
    Warning => "warning"
    Error => "error"
  }
}

///|
fn contract_escape(value : String) -> String {
  value.replace_all(old="|", new="\\|").replace_all(old="\n", new="
") }