///|
/// Runs repeated expiry, takeover, and stale-writer scenarios.
pub fn run_failover_simulation(
  cycles : Int,
  ttl? : Int = 5,
) -> SimulationReport {
  if cycles <= 0 || ttl <= 0 {
    return {
      cycles: 0,
      leases_acquired: 0,
      writes_accepted: 0,
      stale_writes_rejected: 0,
      final_token: 0,
      audit_valid: true,
      evidence: 0,
    }
  }
  let mut table = LeaseTable::new()
  let mut fence = FenceGuard::new()
  let mut leases_acquired = 0
  let mut writes_accepted = 0
  let mut stale_rejected = 0
  let mut previous_holder = ""
  let mut previous_token = 0
  let mut final_token = 0
  let mut evidence = 17
  for cycle = 0; cycle < cycles; cycle = cycle + 1 {
    let now = cycle * (ttl + 1)
    let holder = "worker-\{cycle % 7}"
    let acquisition = table.apply(
      "cluster/leader",
      Acquire(holder, ttl, None),
      now,
    )
    table = acquisition.table
    if acquisition.decision.accepted {
      leases_acquired = leases_acquired + 1
      guard acquisition.decision.lease is Some(lease) else { continue }
      final_token = lease.token
      let current = fence.check({
        resource: lease.resource,
        holder,
        token: lease.token,
        operation: "commit",
      })
      fence = current.next_guard
      if current.accepted {
        writes_accepted = writes_accepted + 1
      }
      if previous_token > 0 {
        let stale = fence.check({
          resource: lease.resource,
          holder: previous_holder,
          token: previous_token,
          operation: "late-commit",
        })
        fence = stale.next_guard
        if !stale.accepted {
          stale_rejected = stale_rejected + 1
        }
      }
      previous_holder = holder
      previous_token = lease.token
      evidence = (
          evidence * 31 +
          lease.token * 17 +
          writes_accepted * 7 +
          stale_rejected
        ) %
        1000003
    }
  }
  let audit = audit_events(table.events)
  {
    cycles,
    leases_acquired,
    writes_accepted,
    stale_writes_rejected: stale_rejected,
    final_token,
    audit_valid: audit.valid,
    evidence,
  }
}

///|
/// Encodes reproducible simulation evidence as stable JSON.
pub fn SimulationReport::to_json(self : SimulationReport) -> String {
  "{\"cycles\":\{self.cycles},\"leases_acquired\":\{self.leases_acquired},\"writes_accepted\":\{self.writes_accepted},\"stale_writes_rejected\":\{self.stale_writes_rejected},\"final_token\":\{self.final_token},\"audit_valid\":\{self.audit_valid},\"evidence\":\{self.evidence}}"
}