///|
/// Deterministic outcome injected into a protocol simulation.
pub(all) enum ScenarioOutcome {
  ScenarioSuccess
  ScenarioTimeout
  ScenarioException
  ScenarioCorrupt
  ScenarioDropped
  ScenarioDelayed
} derive(Debug, Eq)

///|
/// Fault applied to a selected step in a reproducible test scenario.
pub(all) enum ScenarioFault {
  NoFault
  DropNext
  DelayNext(Int)
  CorruptByte(Int)
  ForceException(Byte)
  Repeat(Int)
} derive(Debug, Eq)

///|
/// One input/output expectation used by integration and boundary tests.
pub(all) struct ScenarioStep {
  id : String
  timestamp : Int
  request : Frame
  expected_function : Byte
  expected_outcome : ScenarioOutcome
  mut fault : ScenarioFault
  note : String
}

///|
pub fn ScenarioStep::new(
  id : String,
  timestamp : Int,
  request : Frame,
  expected_outcome : ScenarioOutcome,
  note? : String = "",
) -> Result[ScenarioStep, ModbusError] {
  if id == "" || timestamp < 0 || request.pdu.function == 0 {
    return Err(InvalidData)
  }
  Ok({
    id,
    timestamp,
    request,
    expected_function: request.pdu.function,
    expected_outcome,
    fault: NoFault,
    note,
  })
}

///|
pub fn ScenarioStep::id(self : ScenarioStep) -> String {
  self.id
}

///|
pub fn ScenarioStep::timestamp(self : ScenarioStep) -> Int {
  self.timestamp
}

///|
pub fn ScenarioStep::request(self : ScenarioStep) -> Frame {
  self.request
}

///|
pub fn ScenarioStep::expected_function(self : ScenarioStep) -> Byte {
  self.expected_function
}

///|
pub fn ScenarioStep::expected_outcome(self : ScenarioStep) -> ScenarioOutcome {
  self.expected_outcome
}

///|
pub fn ScenarioStep::fault(self : ScenarioStep) -> ScenarioFault {
  self.fault
}

///|
pub fn ScenarioStep::note(self : ScenarioStep) -> String {
  self.note
}

///|
pub fn ScenarioStep::with_fault(
  self : ScenarioStep,
  fault : ScenarioFault,
) -> Unit {
  self.fault = fault
}

///|
pub fn scenario_outcome_name(outcome : ScenarioOutcome) -> String {
  match outcome {
    ScenarioSuccess => "success"
    ScenarioTimeout => "timeout"
    ScenarioException => "exception"
    ScenarioCorrupt => "corrupt"
    ScenarioDropped => "dropped"
    ScenarioDelayed => "delayed"
  }
}

///|
pub fn scenario_fault_name(fault : ScenarioFault) -> String {
  match fault {
    NoFault => "none"
    DropNext => "drop"
    DelayNext(_) => "delay"
    CorruptByte(_) => "corrupt"
    ForceException(_) => "exception"
    Repeat(_) => "repeat"
  }
}

///|
/// Scenario definition containing steps and shared test variables.
pub(all) struct ModbusScenario {
  id : String
  description : String
  steps : Array[ScenarioStep]
  max_steps : Int
  mut revision : Int
}

///|
pub fn ModbusScenario::new(
  id : String,
  description : String,
  max_steps? : Int = 1024,
) -> Result[ModbusScenario, ModbusError] {
  if id == "" || description == "" || max_steps < 1 {
    Err(InvalidData)
  } else {
    Ok({ id, description, steps: [], max_steps, revision: 0 })
  }
}

///|
pub fn ModbusScenario::id(self : ModbusScenario) -> String {
  self.id
}

///|
pub fn ModbusScenario::description(self : ModbusScenario) -> String {
  self.description
}

///|
pub fn ModbusScenario::step_count(self : ModbusScenario) -> Int {
  self.steps.length()
}

///|
pub fn ModbusScenario::revision(self : ModbusScenario) -> Int {
  self.revision
}

///|
pub fn ModbusScenario::add(
  self : ModbusScenario,
  step : ScenarioStep,
) -> Result[Unit, ModbusError] {
  if self.steps.length() >= self.max_steps {
    return Err(CapacityExceeded)
  }
  for existing in self.steps {
    if existing.id() == step.id() {
      return Err(InvalidData)
    }
    if step.timestamp() < existing.timestamp() {
      return Err(InvalidData)
    }
  }
  self.steps.push(step)
  self.revision += 1
  Ok(())
}

///|
pub fn ModbusScenario::step(
  self : ModbusScenario,
  id : String,
) -> ScenarioStep? {
  for step in self.steps {
    if step.id() == id {
      return Some(step)
    }
  }
  None
}

///|
pub fn ModbusScenario::steps(self : ModbusScenario) -> Array[ScenarioStep] {
  self.steps
}

///|
pub fn ModbusScenario::at(
  self : ModbusScenario,
  index : Int,
) -> Result[ScenarioStep, ModbusError] {
  if index < 0 || index >= self.steps.length() {
    Err(InvalidAddress)
  } else {
    Ok(self.steps[index])
  }
}

///|
pub fn ModbusScenario::faults(self : ModbusScenario) -> Int {
  let mut count = 0
  for step in self.steps {
    match step.fault() {
      NoFault => ()
      _ => count += 1
    }
  }
  count
}

///|
/// A single result emitted by the deterministic runner.
pub(all) struct ScenarioResult {
  step_id : String
  outcome : ScenarioOutcome
  started_at : Int
  finished_at : Int
  response : Frame?
  error : ModbusError?
  detail : String
}

///|
pub fn ScenarioResult::new(
  step_id : String,
  outcome : ScenarioOutcome,
  started_at : Int,
  finished_at : Int,
  response? : Frame = Frame::new(0, 0, []),
  error? : ModbusError = Incomplete,
  detail? : String = "",
) -> Result[ScenarioResult, ModbusError] {
  if step_id == "" || started_at < 0 || finished_at < started_at {
    return Err(InvalidData)
  }
  let selected_response = if response.unit_id == 0 {
    None
  } else {
    Some(response)
  }
  let selected_error = if outcome == ScenarioSuccess {
    None
  } else {
    Some(error)
  }
  Ok({
    step_id,
    outcome,
    started_at,
    finished_at,
    response: selected_response,
    error: selected_error,
    detail,
  })
}

///|
pub fn ScenarioResult::step_id(self : ScenarioResult) -> String {
  self.step_id
}

///|
pub fn ScenarioResult::outcome(self : ScenarioResult) -> ScenarioOutcome {
  self.outcome
}

///|
pub fn ScenarioResult::started_at(self : ScenarioResult) -> Int {
  self.started_at
}

///|
pub fn ScenarioResult::finished_at(self : ScenarioResult) -> Int {
  self.finished_at
}

///|
pub fn ScenarioResult::duration(self : ScenarioResult) -> Int {
  self.finished_at - self.started_at
}

///|
pub fn ScenarioResult::response(self : ScenarioResult) -> Frame? {
  self.response
}

///|
pub fn ScenarioResult::error(self : ScenarioResult) -> ModbusError? {
  self.error
}

///|
pub fn ScenarioResult::detail(self : ScenarioResult) -> String {
  self.detail
}

///|
/// Deterministic runner with no wall-clock or random dependencies.
pub(all) struct ScenarioRunner {
  scenario : ModbusScenario
  results : Array[ScenarioResult]
  mut cursor : Int
  mut now : Int
}

///|
pub fn ScenarioRunner::new(
  scenario : ModbusScenario,
  start_at? : Int = 0,
) -> Result[ScenarioRunner, ModbusError] {
  if start_at < 0 {
    Err(InvalidData)
  } else {
    Ok({ scenario, results: [], cursor: 0, now: start_at })
  }
}

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

///|
pub fn ScenarioRunner::now(self : ScenarioRunner) -> Int {
  self.now
}

///|
pub fn ScenarioRunner::result_count(self : ScenarioRunner) -> Int {
  self.results.length()
}

///|
pub fn ScenarioRunner::results(self : ScenarioRunner) -> Array[ScenarioResult] {
  self.results
}

///|
/// Apply a fault to a request without mutating the scenario definition.
fn scenario_execute(
  step : ScenarioStep,
  now : Int,
) -> Result[ScenarioResult, ModbusError] {
  let delay = match step.fault() {
    DelayNext(value) => value
    _ => 0
  }
  if delay < 0 {
    return Err(InvalidData)
  }
  let finished = now + delay
  match step.fault() {
    DropNext =>
      ScenarioResult::new(
        step.id(),
        ScenarioDropped,
        now,
        finished,
        detail="dropped-by-fault",
      )
    DelayNext(_) =>
      ScenarioResult::new(
        step.id(),
        ScenarioDelayed,
        now,
        finished,
        detail="delayed-by-fault",
      )
    CorruptByte(index) =>
      if index < 0 || index >= step.request().pdu.data.length() {
        Err(InvalidAddress)
      } else {
        ScenarioResult::new(
          step.id(),
          ScenarioCorrupt,
          now,
          finished,
          error=InvalidChecksum,
          detail="corrupt-byte",
        )
      }
    ForceException(_) =>
      ScenarioResult::new(
        step.id(),
        ScenarioException,
        now,
        finished,
        error=InvalidFunction,
        detail="forced-exception",
      )
    Repeat(count) =>
      if count < 1 {
        Err(InvalidQuantity)
      } else {
        ScenarioResult::new(
          step.id(),
          ScenarioSuccess,
          now,
          finished,
          response=step.request(),
          detail="repeat=" + count.to_string(),
        )
      }
    NoFault =>
      ScenarioResult::new(
        step.id(),
        step.expected_outcome(),
        now,
        finished,
        response=if step.expected_outcome() == ScenarioSuccess {
          step.request()
        } else {
          Frame::new(0, 0, [])
        },
        detail=step.note(),
      )
  }
}

///|
/// Execute the next step and return its deterministic result.
pub fn ScenarioRunner::run_next(
  self : ScenarioRunner,
) -> Result[ScenarioResult, ModbusError] {
  if self.cursor >= self.scenario.step_count() {
    return Err(Incomplete)
  }
  let step = match self.scenario.at(self.cursor) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let result = match
    scenario_execute(
      step,
      if self.now < step.timestamp() {
        step.timestamp()
      } else {
        self.now
      },
    ) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  self.cursor += 1
  self.now = result.finished_at()
  self.results.push(result)
  Ok(result)
}

///|
/// Run all remaining steps, stopping at the first execution error.
pub fn ScenarioRunner::run_all(
  self : ScenarioRunner,
) -> Result[Array[ScenarioResult], ModbusError] {
  while self.cursor < self.scenario.step_count() {
    match self.run_next() {
      Ok(_) => ()
      Err(error) => return Err(error)
    }
  }
  Ok(self.results)
}

///|
/// Verify that all outcomes match the scenario's declared expectations.
pub fn ScenarioRunner::passed(self : ScenarioRunner) -> Bool {
  if self.results.length() != self.scenario.step_count() {
    return false
  }
  for i in 0.. value
      Err(_) => return false
    }
    let result = self.results[i]
    match step.fault() {
      NoFault => if result.outcome() != step.expected_outcome() { return false }
      _ => ()
    }
  }
  true
}

///|
/// Count each deterministic outcome in runner results.
pub fn ScenarioRunner::outcome_counts(self : ScenarioRunner) -> Array[Int] {
  let counts : Array[Int] = [0, 0, 0, 0, 0, 0]
  for result in self.results {
    let index = match result.outcome() {
      ScenarioSuccess => 0
      ScenarioTimeout => 1
      ScenarioException => 2
      ScenarioCorrupt => 3
      ScenarioDropped => 4
      ScenarioDelayed => 5
    }
    counts[index] += 1
  }
  counts
}

///|
/// Build a common boundary scenario for a unit and function pair.
pub fn scenario_boundary_suite(
  unit_id : Byte,
  function : Byte,
) -> Result[ModbusScenario, ModbusError] {
  if !is_valid_unit_id(unit_id) || function == 0 {
    return Err(InvalidData)
  }
  let scenario = match
    ModbusScenario::new(
      "boundary-suite", "valid, delayed, corrupt, and dropped requests",
    ) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let valid = match
    ScenarioStep::new(
      "valid",
      0,
      Frame::new(unit_id, function, [0, 0, 0, 1]),
      ScenarioSuccess,
    ) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  ignore(scenario.add(valid))
  let delayed = match
    ScenarioStep::new(
      "delayed",
      10,
      Frame::new(unit_id, function, [0, 1, 0, 1]),
      ScenarioDelayed,
    ) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  delayed.with_fault(DelayNext(25))
  ignore(scenario.add(delayed))
  let corrupt = match
    ScenarioStep::new(
      "corrupt",
      40,
      Frame::new(unit_id, function, [0, 2, 0, 1]),
      ScenarioCorrupt,
    ) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  corrupt.with_fault(CorruptByte(1))
  ignore(scenario.add(corrupt))
  Ok(scenario)
}