///|
/// Actions available in deterministic station integration scenarios.
pub enum ScenarioAction {
  WaitScenario(Int)
  HeartbeatScenario(Int)
  ReceiveApduScenario(Bytes)
  SendApduScenario(Bytes)
  SetPointScenario(InformationAddress, Int)
  RaiseDiagnosticScenario(String)
  ClearDiagnosticScenario
  CheckpointScenario(String)
} derive(Debug)

///|
pub struct ScenarioStep {
  index : Int
  at : Int
  action : ScenarioAction
  label : String
} derive(Debug)

///|
pub fn ScenarioStep::new(
  index : Int,
  at : Int,
  action : ScenarioAction,
  label : String,
) -> Result[ScenarioStep, String] {
  if index < 0 || at < 0 {
    Err("scenario step coordinates cannot be negative")
  } else if label.length() == 0 {
    Err("scenario step label is empty")
  } else {
    Ok({ index, at, action, label })
  }
}

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

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

///|
pub fn ScenarioStep::action(self : ScenarioStep) -> ScenarioAction {
  self.action
}

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

///|
pub struct Scenario {
  name : String
  duration : Int
  steps : Array[ScenarioStep]
  fail_fast : Bool
} derive(Debug)

///|
pub fn Scenario::new(
  name : String,
  duration : Int,
  fail_fast? : Bool = true,
) -> Result[Scenario, String] {
  if name.length() == 0 || duration < 0 {
    Err("scenario name and duration are invalid")
  } else {
    Ok({ name, duration, steps: [], fail_fast })
  }
}

///|
pub fn Scenario::name(self : Scenario) -> String {
  self.name
}

///|
pub fn Scenario::duration(self : Scenario) -> Int {
  self.duration
}

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

///|
pub fn Scenario::fail_fast(self : Scenario) -> Bool {
  self.fail_fast
}

///|
pub fn Scenario::add(
  self : Scenario,
  action : ScenarioAction,
  at : Int,
  label : String,
) -> Result[Unit, String] {
  match ScenarioStep::new(self.steps.length(), at, action, label) {
    Err(error) => Err(error)
    Ok(step) =>
      if at > self.duration {
        Err("scenario step exceeds scenario duration")
      } else {
        self.steps.push(step)
        self.steps.sort_by((left, right) => {
          if left.at() != right.at() {
            left.at() - right.at()
          } else {
            left.index() - right.index()
          }
        })
        Ok(())
      }
  }
}

///|
pub struct ScenarioObservation {
  step : Int
  timestamp : Int
  label : String
  success : Bool
  detail : String
} derive(Eq, Debug)

///|
pub fn ScenarioObservation::step(self : ScenarioObservation) -> Int {
  self.step
}

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

///|
pub fn ScenarioObservation::label(self : ScenarioObservation) -> String {
  self.label
}

///|
pub fn ScenarioObservation::success(self : ScenarioObservation) -> Bool {
  self.success
}

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

///|
pub struct ScenarioResult {
  name : String
  completed : Bool
  elapsed : Int
  observations : Array[ScenarioObservation]
  checkpoints : Array[String]
  failures : Array[String]
} derive(Debug)

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

///|
pub fn ScenarioResult::completed(self : ScenarioResult) -> Bool {
  self.completed
}

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

///|
pub fn ScenarioResult::observations(
  self : ScenarioResult,
) -> Array[ScenarioObservation] {
  self.observations.copy()
}

///|
pub fn ScenarioResult::checkpoints(self : ScenarioResult) -> Array[String] {
  self.checkpoints.copy()
}

///|
pub fn ScenarioResult::failures(self : ScenarioResult) -> Array[String] {
  self.failures.copy()
}

///|
pub struct ScenarioContext {
  mut now : Int
  mut received : Int
  mut sent : Int
  mut heartbeats : Int
  points : Map[Int, Int]
  diagnostics : Array[String]
} derive(Debug)

///|
pub fn ScenarioContext::new() -> ScenarioContext {
  { now: 0, received: 0, sent: 0, heartbeats: 0, points: {}, diagnostics: [] }
}

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

///|
pub fn ScenarioContext::received(self : ScenarioContext) -> Int {
  self.received
}

///|
pub fn ScenarioContext::sent(self : ScenarioContext) -> Int {
  self.sent
}

///|
pub fn ScenarioContext::heartbeats(self : ScenarioContext) -> Int {
  self.heartbeats
}

///|
pub fn ScenarioContext::point(
  self : ScenarioContext,
  address : InformationAddress,
) -> Int? {
  self.points.get(address.number())
}

///|
pub fn ScenarioContext::diagnostics(self : ScenarioContext) -> Array[String] {
  self.diagnostics.copy()
}

///|
fn apply_scenario_action(
  context : ScenarioContext,
  action : ScenarioAction,
  timestamp : Int,
) -> Result[String, String] {
  context.now = timestamp
  match action {
    WaitScenario(seconds) =>
      if seconds < 0 {
        Err("scenario wait cannot be negative")
      } else {
        context.now += seconds
        Ok("wait completed")
      }
    HeartbeatScenario(count) =>
      if count < 1 {
        Err("scenario heartbeat count must be positive")
      } else {
        context.heartbeats += count
        Ok("heartbeat processed")
      }
    ReceiveApduScenario(data) =>
      if data.length() < 6 {
        Err("scenario APDU is too short")
      } else {
        context.received += 1
        Ok("APDU received")
      }
    SendApduScenario(data) =>
      if data.length() < 6 {
        Err("scenario APDU is too short")
      } else {
        context.sent += 1
        Ok("APDU sent")
      }
    SetPointScenario(address, value) => {
      context.points[address.number()] = value
      Ok("point updated")
    }
    RaiseDiagnosticScenario(message) => {
      context.diagnostics.push(message)
      Err("scenario diagnostic raised")
    }
    ClearDiagnosticScenario => {
      context.diagnostics.clear()
      Ok("diagnostics cleared")
    }
    CheckpointScenario(name) =>
      if name.length() == 0 {
        Err("scenario checkpoint name is empty")
      } else {
        Ok("checkpoint:\{name}")
      }
  }
}

///|
pub struct ScenarioRunner {
  context : ScenarioContext
  results : Array[ScenarioResult]
  mut run_count : Int
} derive(Debug)

///|
pub fn ScenarioRunner::new() -> ScenarioRunner {
  { context: ScenarioContext::new(), results: [], run_count: 0 }
}

///|
pub fn ScenarioRunner::context(self : ScenarioRunner) -> ScenarioContext {
  self.context
}

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

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

///|
pub fn ScenarioRunner::run(
  self : ScenarioRunner,
  scenario : Scenario,
) -> ScenarioResult {
  let observations : Array[ScenarioObservation] = []
  let checkpoints : Array[String] = []
  let failures : Array[String] = []
  let completed = true
  let mut elapsed = 0
  for step in scenario.steps() {
    if step.at() < elapsed {
      failures.push("scenario steps are out of order")
    } else {
      elapsed = step.at()
    }
    match apply_scenario_action(self.context, step.action(), step.at()) {
      Ok(detail) => {
        observations.push({
          step: step.index(),
          timestamp: self.context.now(),
          label: step.label(),
          success: true,
          detail,
        })
        if step.action() is CheckpointScenario(name) {
          checkpoints.push(name)
        }
      }
      Err(error) => {
        observations.push({
          step: step.index(),
          timestamp: self.context.now(),
          label: step.label(),
          success: false,
          detail: error,
        })
        failures.push(error)
        if scenario.fail_fast() {
          break
        }
      }
    }
  }
  let result = {
    name: scenario.name(),
    completed: completed && failures.length() == 0,
    elapsed,
    observations,
    checkpoints,
    failures,
  }
  self.results.push(result)
  self.run_count += 1
  result
}

///|
pub fn scenario_action_examples() -> Array[ScenarioAction] {
  [
    WaitScenario(1),
    HeartbeatScenario(1),
    ReceiveApduScenario(b"\x68\x04\x00\x00\x00\x00"),
    SendApduScenario(b"\x68\x04\x00\x00\x00\x00"),
    SetPointScenario(InformationAddress::new(1).unwrap(), 1),
    RaiseDiagnosticScenario("test"),
    ClearDiagnosticScenario,
    CheckpointScenario("ready"),
  ]
}