///|
/// Scenario-driven CiA 402 closed-loop fixture for offline conformance.

///|
pub fn package_id() -> String {
  "fieldbus_core/cia402_scenario_loop"
}

///|
pub(all) enum Cia402ScenarioVariant {
  Nominal
  StaleCommand
  OutOfOrderStatus
  InjectedFault
} derive(Eq, Debug)

///|
pub fn Cia402ScenarioVariant::label(self : Cia402ScenarioVariant) -> String {
  match self {
    Nominal => "nominal"
    StaleCommand => "stale_command"
    OutOfOrderStatus => "out_of_order_status"
    InjectedFault => "injected_fault"
  }
}

///|
pub(all) struct Cia402ScenarioReport {
  seed : Int
  backend : @core.BackendProfile
  variant : Cia402ScenarioVariant
  trace : @trace.TraceLog
  scenario_digest : @core.SimDigest
  recording_digest : @core.SimDigest
  replay_digest : @core.SimDigest
  portable_digest : @core.SimDigest
  fault_report : @fault.FaultReport
  conformance : @protocol_ast.ConformanceReport
} derive(Debug)

///|
pub(all) struct Cia402ConformanceArtifact {
  scenario : Cia402ScenarioReport
  trace_artifact : @trace.TraceArtifact
  summary : String
} derive(Debug)

///|
pub fn Cia402ScenarioReport::passes(self : Cia402ScenarioReport) -> Bool {
  self.conformance.passes() && self.replay_digest_matches()
}

///|
pub fn Cia402ScenarioReport::replay_digest_matches(
  self : Cia402ScenarioReport,
) -> Bool {
  self.recording_digest == self.replay_digest
}

///|
pub fn Cia402ScenarioReport::conformance_summary(
  self : Cia402ScenarioReport,
) -> String {
  "scenario=cia402" +
  "|variant=" +
  self.variant.label() +
  "|seed=" +
  self.seed.to_string() +
  "|backend=" +
  @core.backend_profile_label(self.backend) +
  "|passes=" +
  bool_text(self.passes()) +
  "|event_count=" +
  self.trace.len().to_string() +
  "|matched_steps=" +
  self.conformance.matched_steps.to_string() +
  "|replay_digest_matches=" +
  bool_text(self.replay_digest_matches()) +
  "|first_failure=" +
  conformance_failure_text(self.conformance.first_failure)
}

///|
pub fn Cia402ScenarioReport::to_conformance_artifact(
  self : Cia402ScenarioReport,
) -> Cia402ConformanceArtifact {
  {
    scenario: self,
    trace_artifact: self.trace.to_artifact(seed=self.seed),
    summary: self.conformance_summary(),
  }
}

///|
pub fn Cia402ConformanceArtifact::passes(
  self : Cia402ConformanceArtifact,
) -> Bool {
  self.scenario.passes()
}

///|
pub fn Cia402ConformanceArtifact::artifact_replay_digest_matches(
  self : Cia402ConformanceArtifact,
) -> Bool raise @trace.TraceArtifactError {
  self.trace_artifact.replay_digest() == self.scenario.recording_digest
}

///|
pub fn run_cia402_conformance_artifact(
  seed~ : Int,
  backend? : @core.BackendProfile = @core.SimNative,
  variant? : Cia402ScenarioVariant = Nominal,
) -> Cia402ConformanceArtifact {
  run_cia402_scenario(seed~, backend~, variant~).to_conformance_artifact()
}

///|
pub fn run_cia402_scenario(
  seed~ : Int,
  backend? : @core.BackendProfile = @core.SimNative,
  variant? : Cia402ScenarioVariant = Nominal,
) -> Cia402ScenarioReport {
  let env = @scenario.SimEnv::new(seed~)
  let recording = @trace.Recording::new()
  let fault_plan = @fault.FaultPlan::new(seed~)
  match variant {
    OutOfOrderStatus => {
      emit_status_read(env, recording, seed, backend, event_id=1, delay_ns=0L)
      emit_shutdown_write(
        env,
        recording,
        seed,
        backend,
        event_id=2,
        delay_ns=10L,
      )
    }
    _ => {
      emit_shutdown_write(
        env,
        recording,
        seed,
        backend,
        event_id=1,
        delay_ns=0L,
        raw_offset_ns=if variant is StaleCommand { 2_000_001L } else { 0L },
      )
      emit_status_read(env, recording, seed, backend, event_id=2, delay_ns=10L)
    }
  }
  emit_mode_write(env, recording, seed, backend, event_id=3, delay_ns=10L)
  emit_target_write(env, recording, seed, backend, event_id=4, delay_ns=10L)
  emit_actual_read(env, recording, seed, backend, event_id=5, delay_ns=10L)
  emit_profile_fault(env, recording, seed, backend, event_id=6, delay_ns=10L)
  let next_id = if variant is InjectedFault {
    fault_plan.add_rule(@fault.FaultRule::new(@fault.Drop, @fault.Once))
    let outcome = fault_plan.apply(b"cia402.injected-fault")
    match outcome.hit {
      Some(hit) =>
        recording.record(
          hit.to_trace_event(
            event_id=7,
            vtime=env.now(),
            clock_domain="sim",
            node_id="gateway",
            medium_id="trace",
            backend~,
          ),
        )
      None => ()
    }
    8
  } else {
    7
  }
  emit_fault_reset(
    env,
    recording,
    seed,
    backend,
    event_id=next_id,
    delay_ns=10L,
  )
  emit_reset_status(
    env,
    recording,
    seed,
    backend,
    event_id=next_id + 1,
    delay_ns=10L,
  )
  let trace = recording.get_log()
  let replay = @trace.Replay::from_log(trace)
  let recording_digest = recording.digest(seed~)
  let replay_digest = replay.digest(seed~)
  let portable_digest = trace.portable_digest(seed~)
  {
    seed,
    backend,
    variant,
    trace,
    scenario_digest: env.checkpoint(),
    recording_digest,
    replay_digest,
    portable_digest,
    fault_report: fault_plan.report(),
    conformance: @moonecat_cia402_adapter.moonecat_cia402_observer_backend().check_trace(
      trace,
    ),
  }
}

///|
fn emit_shutdown_write(
  env : @scenario.SimEnv,
  recording : @trace.Recording,
  seed : Int,
  backend : @core.BackendProfile,
  event_id~ : Int,
  delay_ns~ : Int64,
  raw_offset_ns? : Int64 = 0L,
) -> Unit {
  emit_profile_event(
    env,
    recording,
    seed,
    backend,
    event_id~,
    delay_ns~,
    raw_offset_ns~,
    label="cia402.controlword.write",
    direction=@trace.Tx,
    payload_digest=Some(0x0006),
  )
}

///|
fn emit_status_read(
  env : @scenario.SimEnv,
  recording : @trace.Recording,
  seed : Int,
  backend : @core.BackendProfile,
  event_id~ : Int,
  delay_ns~ : Int64,
) -> Unit {
  emit_profile_event(
    env,
    recording,
    seed,
    backend,
    event_id~,
    delay_ns~,
    label="cia402.statusword.read",
    direction=@trace.Rx,
    payload_digest=Some(0x0021),
  )
}

///|
fn emit_mode_write(
  env : @scenario.SimEnv,
  recording : @trace.Recording,
  seed : Int,
  backend : @core.BackendProfile,
  event_id~ : Int,
  delay_ns~ : Int64,
) -> Unit {
  emit_profile_event(
    env,
    recording,
    seed,
    backend,
    event_id~,
    delay_ns~,
    label="cia402.mode.write",
    direction=@trace.Tx,
    payload_digest=Some(0x08),
  )
}

///|
fn emit_target_write(
  env : @scenario.SimEnv,
  recording : @trace.Recording,
  seed : Int,
  backend : @core.BackendProfile,
  event_id~ : Int,
  delay_ns~ : Int64,
) -> Unit {
  emit_profile_event(
    env,
    recording,
    seed,
    backend,
    event_id~,
    delay_ns~,
    label="cia402.target_position.write",
    direction=@trace.Tx,
    payload_digest=Some(607_000),
  )
}

///|
fn emit_actual_read(
  env : @scenario.SimEnv,
  recording : @trace.Recording,
  seed : Int,
  backend : @core.BackendProfile,
  event_id~ : Int,
  delay_ns~ : Int64,
) -> Unit {
  emit_profile_event(
    env,
    recording,
    seed,
    backend,
    event_id~,
    delay_ns~,
    label="cia402.actual_position.read",
    direction=@trace.Rx,
    payload_digest=Some(606_400),
  )
}

///|
fn emit_profile_fault(
  env : @scenario.SimEnv,
  recording : @trace.Recording,
  seed : Int,
  backend : @core.BackendProfile,
  event_id~ : Int,
  delay_ns~ : Int64,
) -> Unit {
  emit_profile_event(
    env,
    recording,
    seed,
    backend,
    event_id~,
    delay_ns~,
    label="cia402.fault.detected",
    direction=@trace.Rx,
    payload_digest=Some(1),
  )
}

///|
fn emit_fault_reset(
  env : @scenario.SimEnv,
  recording : @trace.Recording,
  seed : Int,
  backend : @core.BackendProfile,
  event_id~ : Int,
  delay_ns~ : Int64,
) -> Unit {
  emit_profile_event(
    env,
    recording,
    seed,
    backend,
    event_id~,
    delay_ns~,
    label="cia402.controlword.write",
    direction=@trace.Tx,
    payload_digest=Some(0x0080),
  )
}

///|
fn emit_reset_status(
  env : @scenario.SimEnv,
  recording : @trace.Recording,
  seed : Int,
  backend : @core.BackendProfile,
  event_id~ : Int,
  delay_ns~ : Int64,
) -> Unit {
  emit_profile_event(
    env,
    recording,
    seed,
    backend,
    event_id~,
    delay_ns~,
    label="cia402.statusword.read",
    direction=@trace.Rx,
    payload_digest=Some(0x0040),
  )
}

///|
fn emit_profile_event(
  env : @scenario.SimEnv,
  recording : @trace.Recording,
  seed : Int,
  backend : @core.BackendProfile,
  event_id~ : Int,
  delay_ns~ : Int64,
  raw_offset_ns? : Int64 = 0L,
  label~ : String,
  direction~ : @trace.TraceDirection,
  payload_digest~ : Int?,
) -> Unit {
  ignore(
    env.run_until_event(
      @scenario.timeout(env, @core.Duration::from_ns(delay_ns)),
    ),
  )
  let time_ns = env.now().ns()
  recording.record(
    @trace.TraceEvent::make(
      event_id~,
      parent_id=None,
      vtime=env.now(),
      clock_domain="sim",
      raw_ns=time_ns + raw_offset_ns,
      node_id="controller",
      medium_id="trace",
      channel_id=None,
      direction~,
      payload_digest~,
      rng_step=event_id,
      seed~,
      backend~,
      label~,
    ),
  )
}

///|
fn bool_text(value : Bool) -> String {
  if value {
    "true"
  } else {
    "false"
  }
}

///|
fn conformance_failure_text(
  failure : @protocol_ast.ConformanceFailure?,
) -> String {
  match failure {
    None => "none"
    Some(failure) =>
      failure.source.label() +
      ":" +
      event_option_text(failure.event_id) +
      ":" +
      role_option_text(failure.role) +
      ":" +
      failure.reason
  }
}

///|
fn event_option_text(value : Int?) -> String {
  match value {
    Some(v) => v.to_string()
    None => "none"
  }
}

///|
fn role_option_text(value : @protocol_ast.Role?) -> String {
  match value {
    Some(role) => role.label()
    None => "none"
  }
}