use std::cell::{Cell, RefCell};
use std::time::{SystemTime, UNIX_EPOCH};
use chrono::{DateTime, Utc};
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use uuid::Uuid;
pub trait Environment {
fn now_iso(&self) -> String;
fn now_millis(&self) -> u64;
fn random_f64(&self) -> f64;
fn random_uuid(&self) -> String;
fn elapsed_millis(&self, since: u64) -> u64;
fn timestamp(&self) -> u64;
}
pub struct RealEnvironment {
_private: (),
}
impl RealEnvironment {
pub fn new() -> Self {
Self { _private: () }
}
pub const fn new_const() -> Self {
Self { _private: () }
}
}
impl Default for RealEnvironment {
fn default() -> Self {
Self::new()
}
}
impl Environment for RealEnvironment {
fn now_iso(&self) -> String {
let now: DateTime<Utc> = Utc::now();
now.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
}
fn now_millis(&self) -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before Unix epoch")
.as_millis() as u64
}
fn random_f64(&self) -> f64 {
rand::rng().random::<f64>()
}
fn random_uuid(&self) -> String {
Uuid::new_v4().to_string()
}
fn elapsed_millis(&self, since: u64) -> u64 {
let now = self.now_millis();
now.saturating_sub(since)
}
fn timestamp(&self) -> u64 {
self.now_millis()
}
}
pub struct MockEnvironment {
seed: u64,
clock_millis: Cell<u64>,
rng: RefCell<StdRng>,
}
impl MockEnvironment {
pub fn new(seed: u64) -> Self {
Self {
seed,
clock_millis: Cell::new(1_000_000_000_000), rng: RefCell::new(StdRng::seed_from_u64(seed)),
}
}
pub fn seed(&self) -> u64 {
self.seed
}
pub fn advance_clock(&self, delta_ms: u64) {
let now = self.clock_millis.get();
self.clock_millis.set(now.saturating_add(delta_ms));
}
pub fn set_clock(&self, millis: u64) {
self.clock_millis.set(millis);
}
pub fn reset_rng(&self) {
*self.rng.borrow_mut() = StdRng::seed_from_u64(self.seed);
}
}
impl Environment for MockEnvironment {
fn now_iso(&self) -> String {
let millis = self.clock_millis.get();
let secs = (millis / 1000) as i64;
let nsecs = ((millis % 1000) * 1_000_000) as u32;
let dt = DateTime::from_timestamp(secs, nsecs).expect("valid timestamp");
dt.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
}
fn now_millis(&self) -> u64 {
self.clock_millis.get()
}
fn random_f64(&self) -> f64 {
self.rng.borrow_mut().random::<f64>()
}
fn random_uuid(&self) -> String {
let mut bytes = [0u8; 16];
self.rng.borrow_mut().fill(&mut bytes);
bytes[6] = (bytes[6] & 0x0f) | 0x40; bytes[8] = (bytes[8] & 0x3f) | 0x80; Uuid::from_bytes(bytes).to_string()
}
fn elapsed_millis(&self, since: u64) -> u64 {
self.clock_millis.get().saturating_sub(since)
}
fn timestamp(&self) -> u64 {
self.clock_millis.get()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mock_deterministic_across_instances() {
let env1 = MockEnvironment::new(42);
let env2 = MockEnvironment::new(42);
assert_eq!(env1.now_millis(), env2.now_millis());
assert_eq!(env1.now_iso(), env2.now_iso());
assert_eq!(env1.random_f64(), env2.random_f64());
assert_eq!(env1.random_uuid(), env2.random_uuid());
}
#[test]
fn mock_rng_advances() {
let env = MockEnvironment::new(42);
let r1 = env.random_f64();
let r2 = env.random_f64();
assert_ne!(r1, r2, "RNG should advance on each call");
}
#[test]
fn mock_clock_advance() {
let env = MockEnvironment::new(42);
let start = env.timestamp();
assert_eq!(env.elapsed_millis(start), 0);
env.advance_clock(5000);
assert_eq!(env.elapsed_millis(start), 5000);
env.advance_clock(3000);
assert_eq!(env.elapsed_millis(start), 8000);
}
#[test]
fn mock_clock_iso_format() {
let env = MockEnvironment::new(42);
let iso = env.now_iso();
assert!(iso.contains("T"));
assert!(iso.ends_with("Z"));
}
#[test]
fn mock_reset_rng() {
let env = MockEnvironment::new(42);
let r1 = env.random_f64();
let _ = env.random_f64();
let _ = env.random_f64();
env.reset_rng();
let r1_again = env.random_f64();
assert_eq!(r1, r1_again, "RNG should replay after reset");
}
#[test]
fn mock_uuid_format() {
let env = MockEnvironment::new(42);
let uuid = env.random_uuid();
assert_eq!(uuid.len(), 36);
assert_eq!(&uuid[14..15], "4", "UUID version should be 4");
}
#[test]
fn real_environment_basics() {
let env = RealEnvironment::new();
let millis = env.now_millis();
assert!(millis > 1_700_000_000_000, "should be after 2023");
let iso = env.now_iso();
assert!(iso.starts_with("20"), "should start with 20xx");
let r = env.random_f64();
assert!((0.0..1.0).contains(&r));
let uuid = env.random_uuid();
assert_eq!(uuid.len(), 36);
}
#[test]
fn real_elapsed() {
let env = RealEnvironment::new();
let start = env.timestamp();
let elapsed = env.elapsed_millis(start);
assert!(elapsed < 100);
}
}