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