///|
/// Model transition event passed to coverage labelers.
///
/// The standalone `Labeler` helper receives both the model before the command
/// and the model after the transition. The `StateMachine.label` callback used by
/// replay is lighter weight: it receives the concrete model before the command,
/// the concrete command, and the concrete response.
pub(all) struct Event[Model, Command, Response] {
  before_model : Model
  command : Command
  response : Response
  after_model : Model
} derive(Eq, Debug)

///|
/// Callback wrapper for classifying a state-machine event.
pub(all) struct Labeler[Model, Command, Response] {
  classify : (Event[Model, Command, Response]) -> Array[String]
}

///|
/// Applies a labeler to one event.
pub fn[Model, Command, Response] classify(
  labeler : Labeler[Model, Command, Response],
  event : Event[Model, Command, Response],
) -> Array[String] {
  (labeler.classify)(event)
}

///|
// Small local helper used by label and command-name coverage checks.
fn contains_string(values : Array[String], value : String) -> Bool {
  for item in values {
    if item == value {
      return true
    }
  }
  false
}

///|
/// Deduplicates labels while preserving first-seen order.
///
/// The report only needs to show whether a scenario occurred at least once, but
/// preserving order keeps the output stable for tests and saved diagnostics.
pub fn collect_labels(labels : Array[String]) -> Array[String] {
  let unique : Array[String] = []
  for label in labels {
    if !contains_string(unique, label) {
      unique.push(label)
    }
  }
  unique
}

///|
/// Counts how often each command name appears in a generated program.
///
/// `command_name` coverage is a coarse audit that generation is exploring every
/// operation family expected by the test.
pub fn[CSym, RSym] command_names(
  commands : Commands[CSym, RSym],
  name : (CSym) -> String,
) -> Array[(String, Int)] {
  let counts : Map[String, Int] = Map([])
  for command in commands.commands {
    let command_name = name(command.command)
    counts[command_name] = counts.get_or_default(command_name, 0) + 1
  }
  counts.to_array()
}

///|
/// Lists command names in first-seen order without counts.
pub fn[CSym, RSym] command_names_in_order(
  commands : Commands[CSym, RSym],
  name : (CSym) -> String,
) -> Array[String] {
  let names : Array[String] = []
  for command in commands.commands {
    let command_name = name(command.command)
    if !contains_string(names, command_name) {
      names.push(command_name)
    }
  }
  names
}

///|
/// Returns the required command names that did not appear in a run.
pub fn cover_command_names(
  seen : Array[(String, Int)],
  required : Array[String],
) -> Array[String] {
  let missing : Array[String] = []
  for name in required {
    let present = for item in seen {
      if item.0 == name && item.1 > 0 {
        break true
      }
    } nobreak {
      false
    }
    if !present {
      missing.push(name)
    }
  }
  missing
}