///| A deterministic model adapter for examples, regression tests, and

///| benchmark fixtures. Each step associates a required token context with

///| logits returned by a draft or target model. Production users can replace

///| this adapter with their own inference bridge while preserving the decoder

///|
/// protocol.
pub enum ReplayError {
  EmptyReplay
  EmptyVocabulary(Int)
  VocabularyMismatch(Int)
  ContextMismatch(Int)
  Exhausted(Int)
} derive(Eq, Debug)

///|
/// One response emitted by a replayable logits provider.
pub struct ReplayStep {
  expected_context : Array[Int]
  logits : Array[Double]
}

///|
pub fn ReplayStep::new(
  expected_context : Array[Int],
  logits : Array[Double],
) -> ReplayStep {
  { expected_context, logits }
}

///|
pub fn ReplayStep::context(self : ReplayStep) -> Array[Int] {
  self.expected_context
}

///|
pub fn ReplayStep::logits(self : ReplayStep) -> Array[Double] {
  self.logits
}

///|
/// Stateful cursor over deterministic model responses.
pub struct ReplayModel {
  steps : Array[ReplayStep]
  vocabulary_size : Int
  mut cursor : Int
}

///|
fn same_tokens(left : Array[Int], right : Array[Int]) -> Bool {
  if left.length() != right.length() {
    return false
  }
  for index in 0.. Result[ReplayModel, ReplayError] {
  if steps.length() == 0 {
    return Err(EmptyReplay)
  }
  let vocabulary_size = steps[0].logits.length()
  if vocabulary_size == 0 {
    return Err(EmptyVocabulary(0))
  }
  for index in 1.. Int {
  self.vocabulary_size
}

///|
pub fn ReplayModel::position(self : ReplayModel) -> Int {
  self.cursor
}

///|
pub fn ReplayModel::remaining(self : ReplayModel) -> Int {
  self.steps.length() - self.cursor
}

///| Reset only the fixture cursor. It does not alter model steps, making one

///|
/// fixture safely reusable across multiple strategy comparisons.
pub fn ReplayModel::reset(self : ReplayModel) -> Unit {
  self.cursor = 0
}

///| Read the next logits response, ensuring callers ask the model with the

///|
/// same token prefix recorded by the fixture.
pub fn ReplayModel::next_logits(
  self : ReplayModel,
  context : Array[Int],
) -> Result[Array[Double], ReplayError] {
  if self.cursor >= self.steps.length() {
    return Err(Exhausted(self.cursor))
  }
  let step = self.steps[self.cursor]
  if !same_tokens(step.expected_context, context) {
    return Err(ContextMismatch(self.cursor))
  }
  self.cursor = self.cursor + 1
  Ok(step.logits)
}

///| Return a human-readable compact record for logs and failed fixtures. The

///|
/// full logits are intentionally omitted because they can be very large.
pub fn ReplayModel::describe_next(self : ReplayModel) -> String {
  if self.cursor >= self.steps.length() {
    "replay exhausted at step=" + self.cursor.to_string()
  } else {
    let step = self.steps[self.cursor]
    "replay step=" +
    self.cursor.to_string() +
    " context_length=" +
    step.expected_context.length().to_string() +
    " vocabulary=" +
    step.logits.length().to_string()
  }
}