Skip to main content

mabi_core/config/
mod.rs

1//! Configuration types and hot reload support.
2//!
3//! This module provides comprehensive configuration management for the simulator including:
4//! - Engine configuration
5//! - Protocol-specific configurations (Modbus, OPC UA, BACnet, KNX)
6//! - Device configurations
7//! - Multi-format file loading (YAML, JSON, TOML)
8//! - Environment variable overrides
9//! - Configuration validation
10//! - Hot reload with file watching
11//!
12//! # Quick Start
13//!
14//! ```rust,no_run
15//! use mabi_core::config::{EngineConfig, ConfigLoader, ConfigFormat};
16//! use mabi_core::Validatable;
17//!
18//! // Load configuration from file (auto-detect format)
19//! let config: EngineConfig = ConfigLoader::load("config.yaml").unwrap();
20//!
21//! // Or use builder pattern with defaults
22//! let config = EngineConfig::default()
23//!     .with_max_devices(50_000);
24//!
25//! // Validate configuration
26//! config.validate().unwrap();
27//! ```
28//!
29//! # Configuration Formats
30//!
31//! The configuration system supports multiple formats:
32//!
33//! - **YAML** (`.yaml`, `.yml`): Human-readable, good for complex configs
34//! - **JSON** (`.json`): Wide compatibility, strict syntax
35//! - **TOML** (`.toml`): Rust-native, good for nested structures
36//!
37//! # Environment Variable Overrides
38//!
39//! Configuration values can be overridden via environment variables:
40//!
41//! ```text
42//! TRAP_SIM_ENGINE_MAX_DEVICES=50000
43//! TRAP_SIM_ENGINE_TICK_INTERVAL_MS=50
44//! TRAP_SIM_LOG_LEVEL=debug
45//! ```
46//!
47//! # Hot Reload
48//!
49//! The configuration system supports hot reload through file watching:
50//!
51//! ```rust,ignore
52//! use mabi_core::config::{ConfigWatcher, FileWatcherService, ConfigSource};
53//!
54//! let watcher = Arc::new(ConfigWatcher::new());
55//! let service = FileWatcherService::new(watcher.clone());
56//!
57//! service.watch(ConfigSource::Main(PathBuf::from("config.yaml")))?;
58//! service.start()?;
59//!
60//! // Subscribe to changes
61//! let mut rx = watcher.subscribe();
62//! while let Ok(event) = rx.recv().await {
63//!     println!("Config changed: {:?}", event);
64//! }
65//! ```
66//!
67//! # Validation
68//!
69//! Configurations can be validated using the `Validatable` trait:
70//!
71//! ```rust,ignore
72//! use mabi_core::config::{EngineConfig, Validatable};
73//!
74//! let config = EngineConfig::default();
75//! if let Err(errors) = config.validate() {
76//!     for (field, messages) in errors.iter() {
77//!         println!("{}: {:?}", field, messages);
78//!     }
79//! }
80//! ```
81
82pub mod env;
83pub mod file_watcher;
84pub mod hot_reload;
85pub mod loader;
86pub mod validation;
87pub mod watcher;
88
89use std::collections::HashMap;
90use std::net::SocketAddr;
91use std::path::PathBuf;
92use std::time::Duration;
93
94use serde::{Deserialize, Serialize};
95
96use crate::protocol::Protocol;
97use crate::tags::Tags;
98
99// Re-export watcher types
100pub use watcher::{
101    create_config_watcher, CallbackHandler, ConfigEvent, ConfigEventHandler, ConfigSource,
102    ConfigWatcher, SharedConfigWatcher, WatcherState,
103};
104
105// Re-export loader types
106pub use loader::{ConfigDiscovery, ConfigFormat, ConfigLoader, LayeredConfigBuilder};
107
108// Re-export environment variable types
109pub use env::{
110    get_env, get_env_bool, get_env_bool_or, get_env_or, EnvApplyResult, EnvConfigurable,
111    EnvOverrides, EnvRule, EnvRuleBuilder, EnvSnapshot, EnvVarDoc, DEFAULT_PREFIX,
112};
113
114// Re-export validation types
115pub use validation::{
116    CrossFieldValidator, PathExistsRule, RangeRule, SocketAddrRule, StringLengthRule, Validatable,
117    ValidationContext, ValidationRule, Validator,
118};
119
120// Re-export file watcher types
121pub use file_watcher::{
122    FileWatcherConfig, FileWatcherService, FileWatcherServiceBuilder, DEFAULT_DEBOUNCE_MS,
123};
124
125// Re-export hot reload types
126pub use hot_reload::{
127    ConfigChange, HotReloadManager, HotReloadManagerBuilder, ReloadEvent, ReloadStrategy,
128};
129
130/// Main engine configuration.
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct EngineConfig {
133    /// Engine name.
134    #[serde(default = "default_engine_name")]
135    pub name: String,
136
137    /// Maximum number of devices.
138    #[serde(default = "default_max_devices")]
139    pub max_devices: usize,
140
141    /// Maximum number of data points.
142    #[serde(default = "default_max_points")]
143    pub max_points: usize,
144
145    /// Tick interval in milliseconds.
146    #[serde(default = "default_tick_interval_ms")]
147    pub tick_interval_ms: u64,
148
149    /// Number of worker threads.
150    #[serde(default = "default_workers")]
151    pub workers: usize,
152
153    /// Enable metrics collection.
154    #[serde(default = "default_true")]
155    pub enable_metrics: bool,
156
157    /// Metrics export interval in seconds.
158    #[serde(default = "default_metrics_interval")]
159    pub metrics_interval_secs: u64,
160
161    /// Log level.
162    #[serde(default = "default_log_level")]
163    pub log_level: String,
164
165    /// Protocol configurations.
166    #[serde(default)]
167    pub protocols: HashMap<String, ProtocolConfig>,
168}
169
170fn default_engine_name() -> String {
171    "trap-simulator".to_string()
172}
173
174fn default_max_devices() -> usize {
175    10_000
176}
177
178fn default_max_points() -> usize {
179    1_000_000
180}
181
182fn default_tick_interval_ms() -> u64 {
183    100
184}
185
186fn default_workers() -> usize {
187    num_cpus::get().max(4)
188}
189
190fn default_true() -> bool {
191    true
192}
193
194fn default_metrics_interval() -> u64 {
195    10
196}
197
198fn default_log_level() -> String {
199    "info".to_string()
200}
201
202impl Default for EngineConfig {
203    fn default() -> Self {
204        Self {
205            name: default_engine_name(),
206            max_devices: default_max_devices(),
207            max_points: default_max_points(),
208            tick_interval_ms: default_tick_interval_ms(),
209            workers: default_workers(),
210            enable_metrics: true,
211            metrics_interval_secs: default_metrics_interval(),
212            log_level: default_log_level(),
213            protocols: HashMap::new(),
214        }
215    }
216}
217
218impl EngineConfig {
219    /// Create a new engine config with default values.
220    pub fn new() -> Self {
221        Self::default()
222    }
223
224    /// Load config from a YAML file.
225    pub fn from_yaml_file(path: impl Into<PathBuf>) -> crate::Result<Self> {
226        ConfigLoader::load_with_format(path.into(), ConfigFormat::Yaml)
227    }
228
229    /// Load config from a JSON file.
230    pub fn from_json_file(path: impl Into<PathBuf>) -> crate::Result<Self> {
231        ConfigLoader::load_with_format(path.into(), ConfigFormat::Json)
232    }
233
234    /// Load config from a TOML file.
235    pub fn from_toml_file(path: impl Into<PathBuf>) -> crate::Result<Self> {
236        ConfigLoader::load_with_format(path.into(), ConfigFormat::Toml)
237    }
238
239    /// Load config from any supported file (auto-detect format).
240    pub fn from_file(path: impl Into<PathBuf>) -> crate::Result<Self> {
241        ConfigLoader::load(path.into())
242    }
243
244    /// Get tick interval as Duration.
245    pub fn tick_interval(&self) -> Duration {
246        Duration::from_millis(self.tick_interval_ms)
247    }
248
249    /// Set maximum devices.
250    pub fn with_max_devices(mut self, max: usize) -> Self {
251        self.max_devices = max;
252        self
253    }
254
255    /// Set maximum data points.
256    pub fn with_max_points(mut self, max: usize) -> Self {
257        self.max_points = max;
258        self
259    }
260
261    /// Set tick interval.
262    pub fn with_tick_interval(mut self, interval: Duration) -> Self {
263        self.tick_interval_ms = interval.as_millis() as u64;
264        self
265    }
266
267    /// Set number of worker threads.
268    pub fn with_workers(mut self, workers: usize) -> Self {
269        self.workers = workers;
270        self
271    }
272
273    /// Set log level.
274    pub fn with_log_level(mut self, level: impl Into<String>) -> Self {
275        self.log_level = level.into();
276        self
277    }
278
279    /// Enable or disable metrics.
280    pub fn with_metrics(mut self, enable: bool) -> Self {
281        self.enable_metrics = enable;
282        self
283    }
284
285    /// Add protocol config.
286    pub fn with_protocol(mut self, name: impl Into<String>, config: ProtocolConfig) -> Self {
287        self.protocols.insert(name.into(), config);
288        self
289    }
290
291    /// Apply environment variable overrides.
292    ///
293    /// Reads environment variables with the `TRAP_SIM_` prefix and applies
294    /// matching values to the configuration.
295    pub fn apply_env_overrides(&mut self) -> EnvApplyResult {
296        Self::env_overrides().apply(self)
297    }
298
299    /// Create environment overrides configuration.
300    pub fn env_overrides() -> EnvOverrides<Self> {
301        EnvOverrides::with_prefix(DEFAULT_PREFIX)
302            .add_rule(
303                EnvRuleBuilder::new("ENGINE_NAME")
304                    .field_path("name")
305                    .description("Engine instance name")
306                    .as_string(|c: &mut Self, v| c.name = v),
307            )
308            .add_rule(
309                EnvRuleBuilder::new("ENGINE_MAX_DEVICES")
310                    .field_path("max_devices")
311                    .description("Maximum number of devices")
312                    .parse_into(|c: &mut Self, v: usize| c.max_devices = v),
313            )
314            .add_rule(
315                EnvRuleBuilder::new("ENGINE_MAX_POINTS")
316                    .field_path("max_points")
317                    .description("Maximum number of data points")
318                    .parse_into(|c: &mut Self, v: usize| c.max_points = v),
319            )
320            .add_rule(
321                EnvRuleBuilder::new("ENGINE_TICK_INTERVAL_MS")
322                    .field_path("tick_interval_ms")
323                    .description("Tick interval in milliseconds")
324                    .parse_into(|c: &mut Self, v: u64| c.tick_interval_ms = v),
325            )
326            .add_rule(
327                EnvRuleBuilder::new("ENGINE_WORKERS")
328                    .field_path("workers")
329                    .description("Number of worker threads")
330                    .parse_into(|c: &mut Self, v: usize| c.workers = v),
331            )
332            .add_rule(
333                EnvRuleBuilder::new("ENGINE_METRICS")
334                    .field_path("enable_metrics")
335                    .description("Enable metrics collection")
336                    .as_bool(|c: &mut Self, v| c.enable_metrics = v),
337            )
338            .add_rule(
339                EnvRuleBuilder::new("ENGINE_METRICS_INTERVAL")
340                    .field_path("metrics_interval_secs")
341                    .description("Metrics export interval in seconds")
342                    .parse_into(|c: &mut Self, v: u64| c.metrics_interval_secs = v),
343            )
344            .add_rule(
345                EnvRuleBuilder::new("LOG_LEVEL")
346                    .field_path("log_level")
347                    .description("Log level (trace, debug, info, warn, error)")
348                    .as_string(|c: &mut Self, v| c.log_level = v),
349            )
350    }
351}
352
353impl EnvConfigurable for EngineConfig {
354    fn env_overrides() -> EnvOverrides<Self> {
355        Self::env_overrides()
356    }
357}
358
359impl Validatable for EngineConfig {
360    fn validate(&self) -> crate::Result<()> {
361        let mut errors = crate::error::ValidationErrors::new();
362        self.validate_collect(&mut errors);
363        errors.into_result(())
364    }
365
366    fn validate_collect(&self, errors: &mut crate::error::ValidationErrors) {
367        // Validate name
368        if self.name.trim().is_empty() {
369            errors.add("name", "Engine name cannot be empty");
370        }
371
372        // Validate max_devices
373        if self.max_devices == 0 {
374            errors.add("max_devices", "Max devices must be greater than 0");
375        }
376        if self.max_devices > 1_000_000 {
377            errors.add("max_devices", "Max devices cannot exceed 1,000,000");
378        }
379
380        // Validate max_points
381        if self.max_points == 0 {
382            errors.add("max_points", "Max points must be greater than 0");
383        }
384        if self.max_points > 100_000_000 {
385            errors.add("max_points", "Max points cannot exceed 100,000,000");
386        }
387
388        // Validate tick_interval_ms
389        if self.tick_interval_ms < 1 {
390            errors.add("tick_interval_ms", "Tick interval must be at least 1ms");
391        }
392        if self.tick_interval_ms > 60_000 {
393            errors.add("tick_interval_ms", "Tick interval cannot exceed 60 seconds");
394        }
395
396        // Validate workers
397        if self.workers == 0 {
398            errors.add("workers", "Workers must be greater than 0");
399        }
400        if self.workers > 1024 {
401            errors.add("workers", "Workers cannot exceed 1024");
402        }
403
404        // Validate metrics_interval_secs
405        if self.enable_metrics && self.metrics_interval_secs == 0 {
406            errors.add(
407                "metrics_interval_secs",
408                "Metrics interval must be greater than 0 when metrics are enabled",
409            );
410        }
411
412        // Validate log_level
413        let valid_levels = ["trace", "debug", "info", "warn", "error"];
414        if !valid_levels.contains(&self.log_level.to_lowercase().as_str()) {
415            errors.add(
416                "log_level",
417                format!(
418                    "Invalid log level '{}', must be one of: {:?}",
419                    self.log_level, valid_levels
420                ),
421            );
422        }
423
424        // Cross-field validation: max_points should be sensible relative to max_devices
425        let points_per_device = self.max_points / self.max_devices.max(1);
426        if points_per_device > 10_000 {
427            errors.add(
428                "max_points, max_devices",
429                format!(
430                    "Average points per device ({}) seems too high",
431                    points_per_device
432                ),
433            );
434        }
435    }
436}
437
438/// Protocol-specific configuration.
439#[derive(Debug, Clone, Serialize, Deserialize)]
440#[serde(tag = "type", rename_all = "lowercase")]
441pub enum ProtocolConfig {
442    /// Modbus TCP configuration.
443    ModbusTcp(ModbusTcpConfig),
444    /// Modbus RTU configuration.
445    ModbusRtu(ModbusRtuConfig),
446    /// OPC UA configuration.
447    OpcUa(OpcUaConfig),
448    /// BACnet/IP configuration.
449    BacnetIp(BacnetIpConfig),
450    /// KNXnet/IP configuration.
451    KnxIp(KnxIpConfig),
452}
453
454/// Modbus TCP configuration.
455#[derive(Debug, Clone, Serialize, Deserialize)]
456pub struct ModbusTcpConfig {
457    /// Bind address.
458    #[serde(default = "default_modbus_bind")]
459    pub bind_address: SocketAddr,
460
461    /// Maximum connections.
462    #[serde(default = "default_max_connections")]
463    pub max_connections: usize,
464
465    /// Connection timeout in seconds.
466    #[serde(default = "default_timeout")]
467    pub timeout_secs: u64,
468
469    /// Enable keep-alive.
470    #[serde(default = "default_true")]
471    pub keep_alive: bool,
472}
473
474fn default_modbus_bind() -> SocketAddr {
475    "0.0.0.0:502".parse().unwrap()
476}
477
478fn default_max_connections() -> usize {
479    1000
480}
481
482fn default_timeout() -> u64 {
483    30
484}
485
486impl Default for ModbusTcpConfig {
487    fn default() -> Self {
488        Self {
489            bind_address: default_modbus_bind(),
490            max_connections: default_max_connections(),
491            timeout_secs: default_timeout(),
492            keep_alive: true,
493        }
494    }
495}
496
497/// Modbus RTU configuration.
498#[derive(Debug, Clone, Serialize, Deserialize)]
499pub struct ModbusRtuConfig {
500    /// Serial port path.
501    pub serial_port: String,
502
503    /// Baud rate.
504    #[serde(default = "default_baud_rate")]
505    pub baud_rate: u32,
506
507    /// Data bits.
508    #[serde(default = "default_data_bits")]
509    pub data_bits: u8,
510
511    /// Parity.
512    #[serde(default = "default_parity")]
513    pub parity: String,
514
515    /// Stop bits.
516    #[serde(default = "default_stop_bits")]
517    pub stop_bits: u8,
518}
519
520fn default_baud_rate() -> u32 {
521    9600
522}
523
524fn default_data_bits() -> u8 {
525    8
526}
527
528fn default_parity() -> String {
529    "none".to_string()
530}
531
532fn default_stop_bits() -> u8 {
533    1
534}
535
536impl Default for ModbusRtuConfig {
537    fn default() -> Self {
538        Self {
539            serial_port: "/dev/ttyUSB0".to_string(),
540            baud_rate: default_baud_rate(),
541            data_bits: default_data_bits(),
542            parity: default_parity(),
543            stop_bits: default_stop_bits(),
544        }
545    }
546}
547
548/// OPC UA configuration.
549#[derive(Debug, Clone, Serialize, Deserialize)]
550pub struct OpcUaConfig {
551    /// Endpoint URL.
552    #[serde(default = "default_opcua_endpoint")]
553    pub endpoint_url: String,
554
555    /// Server name.
556    #[serde(default = "default_opcua_server_name")]
557    pub server_name: String,
558
559    /// Security policy.
560    #[serde(default = "default_security_policy")]
561    pub security_policy: String,
562
563    /// Certificate path.
564    pub certificate_path: Option<PathBuf>,
565
566    /// Private key path.
567    pub private_key_path: Option<PathBuf>,
568
569    /// Max subscriptions.
570    #[serde(default = "default_max_subscriptions")]
571    pub max_subscriptions: usize,
572}
573
574fn default_opcua_endpoint() -> String {
575    "opc.tcp://0.0.0.0:4840".to_string()
576}
577
578fn default_opcua_server_name() -> String {
579    "TRAP Simulator OPC UA Server".to_string()
580}
581
582fn default_security_policy() -> String {
583    "None".to_string()
584}
585
586fn default_max_subscriptions() -> usize {
587    100
588}
589
590impl Default for OpcUaConfig {
591    fn default() -> Self {
592        Self {
593            endpoint_url: default_opcua_endpoint(),
594            server_name: default_opcua_server_name(),
595            security_policy: default_security_policy(),
596            certificate_path: None,
597            private_key_path: None,
598            max_subscriptions: default_max_subscriptions(),
599        }
600    }
601}
602
603/// BACnet/IP configuration.
604#[derive(Debug, Clone, Serialize, Deserialize)]
605pub struct BacnetIpConfig {
606    /// Bind address.
607    #[serde(default = "default_bacnet_bind")]
608    pub bind_address: SocketAddr,
609
610    /// BACnet device instance.
611    #[serde(default = "default_device_instance")]
612    pub device_instance: u32,
613
614    /// BACnet device name.
615    #[serde(default = "default_bacnet_device_name")]
616    pub device_name: String,
617
618    /// Enable BBMD (BACnet Broadcast Management Device).
619    #[serde(default)]
620    pub enable_bbmd: bool,
621
622    /// BBMD table.
623    #[serde(default)]
624    pub bbmd_table: Vec<String>,
625}
626
627fn default_bacnet_bind() -> SocketAddr {
628    "0.0.0.0:47808".parse().unwrap()
629}
630
631fn default_device_instance() -> u32 {
632    1234
633}
634
635fn default_bacnet_device_name() -> String {
636    "TRAP Simulator BACnet Device".to_string()
637}
638
639impl Default for BacnetIpConfig {
640    fn default() -> Self {
641        Self {
642            bind_address: default_bacnet_bind(),
643            device_instance: default_device_instance(),
644            device_name: default_bacnet_device_name(),
645            enable_bbmd: false,
646            bbmd_table: Vec::new(),
647        }
648    }
649}
650
651/// KNXnet/IP configuration.
652#[derive(Debug, Clone, Serialize, Deserialize)]
653pub struct KnxIpConfig {
654    /// Bind address.
655    #[serde(default = "default_knx_bind")]
656    pub bind_address: SocketAddr,
657
658    /// Individual address.
659    #[serde(default = "default_individual_address")]
660    pub individual_address: String,
661
662    /// Enable tunneling.
663    #[serde(default = "default_true")]
664    pub enable_tunneling: bool,
665
666    /// Enable routing.
667    #[serde(default)]
668    pub enable_routing: bool,
669
670    /// Multicast address for routing.
671    #[serde(default = "default_multicast_address")]
672    pub multicast_address: String,
673}
674
675fn default_knx_bind() -> SocketAddr {
676    "0.0.0.0:3671".parse().unwrap()
677}
678
679fn default_individual_address() -> String {
680    "1.1.1".to_string()
681}
682
683fn default_multicast_address() -> String {
684    "224.0.23.12".to_string()
685}
686
687impl Default for KnxIpConfig {
688    fn default() -> Self {
689        Self {
690            bind_address: default_knx_bind(),
691            individual_address: default_individual_address(),
692            enable_tunneling: true,
693            enable_routing: false,
694            multicast_address: default_multicast_address(),
695        }
696    }
697}
698
699/// Device configuration.
700#[derive(Debug, Clone, Serialize, Deserialize)]
701pub struct DeviceConfig {
702    /// Device ID.
703    pub id: String,
704
705    /// Device name.
706    pub name: String,
707
708    /// Device description.
709    #[serde(default)]
710    pub description: String,
711
712    /// Protocol.
713    pub protocol: Protocol,
714
715    /// Protocol-specific address (e.g., unit ID for Modbus).
716    #[serde(default)]
717    pub address: Option<String>,
718
719    /// Data points.
720    #[serde(default)]
721    pub points: Vec<DataPointConfig>,
722
723    /// Custom metadata.
724    #[serde(default)]
725    pub metadata: HashMap<String, String>,
726
727    /// Device tags for organization and filtering.
728    #[serde(default, skip_serializing_if = "Tags::is_empty")]
729    pub tags: Tags,
730}
731
732/// Data point configuration.
733#[derive(Debug, Clone, Serialize, Deserialize)]
734pub struct DataPointConfig {
735    /// Point ID.
736    pub id: String,
737
738    /// Point name.
739    pub name: String,
740
741    /// Data type.
742    pub data_type: String,
743
744    /// Access mode.
745    #[serde(default = "default_access")]
746    pub access: String,
747
748    /// Protocol-specific address.
749    #[serde(default)]
750    pub address: Option<String>,
751
752    /// Initial value.
753    #[serde(default)]
754    pub initial_value: Option<serde_json::Value>,
755
756    /// Engineering units.
757    #[serde(default)]
758    pub units: Option<String>,
759
760    /// Minimum value.
761    #[serde(default)]
762    pub min: Option<f64>,
763
764    /// Maximum value.
765    #[serde(default)]
766    pub max: Option<f64>,
767}
768
769fn default_access() -> String {
770    "rw".to_string()
771}
772
773#[cfg(test)]
774mod tests {
775    use super::*;
776    use std::env;
777
778    #[test]
779    fn test_engine_config_default() {
780        let config = EngineConfig::default();
781        assert_eq!(config.max_devices, 10_000);
782        assert_eq!(config.tick_interval_ms, 100);
783        assert_eq!(config.name, "trap-simulator");
784    }
785
786    #[test]
787    fn test_engine_config_builder() {
788        let config = EngineConfig::new()
789            .with_max_devices(50_000)
790            .with_max_points(5_000_000)
791            .with_tick_interval(Duration::from_millis(50))
792            .with_workers(8)
793            .with_log_level("debug")
794            .with_metrics(false);
795
796        assert_eq!(config.max_devices, 50_000);
797        assert_eq!(config.max_points, 5_000_000);
798        assert_eq!(config.tick_interval_ms, 50);
799        assert_eq!(config.workers, 8);
800        assert_eq!(config.log_level, "debug");
801        assert!(!config.enable_metrics);
802    }
803
804    #[test]
805    fn test_modbus_tcp_config_default() {
806        let config = ModbusTcpConfig::default();
807        assert_eq!(config.bind_address.port(), 502);
808        assert_eq!(config.max_connections, 1000);
809    }
810
811    #[test]
812    fn test_config_serialization_yaml() {
813        let config = EngineConfig::default();
814        let yaml = ConfigLoader::serialize(&config, ConfigFormat::Yaml).unwrap();
815        let parsed: EngineConfig = ConfigLoader::parse(&yaml, ConfigFormat::Yaml).unwrap();
816        assert_eq!(config.max_devices, parsed.max_devices);
817        assert_eq!(config.name, parsed.name);
818    }
819
820    #[test]
821    fn test_config_serialization_json() {
822        let config = EngineConfig::default();
823        let json = ConfigLoader::serialize(&config, ConfigFormat::Json).unwrap();
824        let parsed: EngineConfig = ConfigLoader::parse(&json, ConfigFormat::Json).unwrap();
825        assert_eq!(config.max_devices, parsed.max_devices);
826    }
827
828    #[test]
829    fn test_config_serialization_toml() {
830        let config = EngineConfig::default();
831        let toml = ConfigLoader::serialize(&config, ConfigFormat::Toml).unwrap();
832        let parsed: EngineConfig = ConfigLoader::parse(&toml, ConfigFormat::Toml).unwrap();
833        assert_eq!(config.max_devices, parsed.max_devices);
834    }
835
836    #[test]
837    fn test_config_validation_valid() {
838        let config = EngineConfig::default();
839        assert!(config.validate().is_ok());
840    }
841
842    #[test]
843    fn test_config_validation_invalid_max_devices() {
844        let config = EngineConfig::default().with_max_devices(0);
845        let result = config.validate();
846        assert!(result.is_err());
847    }
848
849    #[test]
850    fn test_config_validation_invalid_log_level() {
851        let mut config = EngineConfig::default();
852        config.log_level = "invalid".to_string();
853        let result = config.validate();
854        assert!(result.is_err());
855    }
856
857    #[test]
858    fn test_config_validation_cross_field() {
859        // Very high points per device ratio
860        let config = EngineConfig::default()
861            .with_max_devices(10)
862            .with_max_points(1_000_000);
863        let result = config.validate();
864        assert!(result.is_err());
865    }
866
867    #[test]
868    fn test_env_overrides() {
869        // Set environment variables
870        env::set_var("TRAP_SIM_ENGINE_MAX_DEVICES", "25000");
871        env::set_var("TRAP_SIM_ENGINE_WORKERS", "16");
872        env::set_var("TRAP_SIM_LOG_LEVEL", "debug");
873
874        let mut config = EngineConfig::default();
875        let result = config.apply_env_overrides();
876
877        assert!(result.has_changes());
878        assert_eq!(config.max_devices, 25000);
879        assert_eq!(config.workers, 16);
880        assert_eq!(config.log_level, "debug");
881
882        // Cleanup
883        env::remove_var("TRAP_SIM_ENGINE_MAX_DEVICES");
884        env::remove_var("TRAP_SIM_ENGINE_WORKERS");
885        env::remove_var("TRAP_SIM_LOG_LEVEL");
886    }
887
888    #[test]
889    fn test_env_overrides_documentation() {
890        let overrides = EngineConfig::env_overrides();
891        let docs = overrides.documentation();
892
893        assert!(docs.len() > 0);
894        assert!(docs
895            .iter()
896            .any(|d| d.var_name == "TRAP_SIM_ENGINE_MAX_DEVICES"));
897        assert!(docs.iter().any(|d| d.var_name == "TRAP_SIM_LOG_LEVEL"));
898    }
899
900    #[test]
901    fn test_protocol_config_modbus_tcp() {
902        let config = ProtocolConfig::ModbusTcp(ModbusTcpConfig::default());
903        let yaml = serde_yaml::to_string(&config).unwrap();
904        assert!(yaml.contains("type: modbustcp"));
905    }
906
907    #[test]
908    fn test_protocol_config_opcua() {
909        let config = ProtocolConfig::OpcUa(OpcUaConfig::default());
910        let yaml = serde_yaml::to_string(&config).unwrap();
911        assert!(yaml.contains("type: opcua"));
912    }
913
914    #[test]
915    fn test_protocol_config_bacnet() {
916        let _config = ProtocolConfig::BacnetIp(BacnetIpConfig::default());
917        assert_eq!(BacnetIpConfig::default().device_instance, 1234);
918    }
919
920    #[test]
921    fn test_protocol_config_knx() {
922        let _config = ProtocolConfig::KnxIp(KnxIpConfig::default());
923        assert_eq!(KnxIpConfig::default().individual_address, "1.1.1");
924    }
925
926    #[test]
927    fn test_engine_config_with_protocol() {
928        let config = EngineConfig::default().with_protocol(
929            "modbus",
930            ProtocolConfig::ModbusTcp(ModbusTcpConfig::default()),
931        );
932
933        assert!(config.protocols.contains_key("modbus"));
934    }
935
936    #[test]
937    fn test_config_format_detection() {
938        assert_eq!(
939            ConfigFormat::from_path("config.yaml"),
940            Some(ConfigFormat::Yaml)
941        );
942        assert_eq!(
943            ConfigFormat::from_path("config.yml"),
944            Some(ConfigFormat::Yaml)
945        );
946        assert_eq!(
947            ConfigFormat::from_path("config.json"),
948            Some(ConfigFormat::Json)
949        );
950        assert_eq!(
951            ConfigFormat::from_path("config.toml"),
952            Some(ConfigFormat::Toml)
953        );
954        assert_eq!(ConfigFormat::from_path("config.txt"), None);
955    }
956}