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;
96use crate::tags::Tags;
97
98// Re-export watcher types
99pub use watcher::{
100    CallbackHandler, ConfigEvent, ConfigEventHandler, ConfigSource, ConfigWatcher,
101    SharedConfigWatcher, WatcherState, create_config_watcher,
102};
103
104// Re-export loader types
105pub use loader::{ConfigFormat, ConfigLoader, ConfigDiscovery, LayeredConfigBuilder};
106
107// Re-export environment variable types
108pub use env::{
109    EnvOverrides, EnvRule, EnvRuleBuilder, EnvApplyResult, EnvConfigurable,
110    EnvSnapshot, EnvVarDoc, DEFAULT_PREFIX,
111    get_env, get_env_or, get_env_bool, get_env_bool_or,
112};
113
114// Re-export validation types
115pub use validation::{
116    Validatable, Validator, ValidationContext, ValidationRule,
117    RangeRule, StringLengthRule, PathExistsRule, SocketAddrRule,
118    CrossFieldValidator,
119};
120
121// Re-export file watcher types
122pub use file_watcher::{
123    FileWatcherService, FileWatcherServiceBuilder, FileWatcherConfig,
124    DEFAULT_DEBOUNCE_MS,
125};
126
127// Re-export hot reload types
128pub use hot_reload::{
129    HotReloadManager, HotReloadManagerBuilder, ReloadEvent, ReloadStrategy,
130    ConfigChange,
131};
132
133/// Main engine configuration.
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct EngineConfig {
136    /// Engine name.
137    #[serde(default = "default_engine_name")]
138    pub name: String,
139
140    /// Maximum number of devices.
141    #[serde(default = "default_max_devices")]
142    pub max_devices: usize,
143
144    /// Maximum number of data points.
145    #[serde(default = "default_max_points")]
146    pub max_points: usize,
147
148    /// Tick interval in milliseconds.
149    #[serde(default = "default_tick_interval_ms")]
150    pub tick_interval_ms: u64,
151
152    /// Number of worker threads.
153    #[serde(default = "default_workers")]
154    pub workers: usize,
155
156    /// Enable metrics collection.
157    #[serde(default = "default_true")]
158    pub enable_metrics: bool,
159
160    /// Metrics export interval in seconds.
161    #[serde(default = "default_metrics_interval")]
162    pub metrics_interval_secs: u64,
163
164    /// Log level.
165    #[serde(default = "default_log_level")]
166    pub log_level: String,
167
168    /// Protocol configurations.
169    #[serde(default)]
170    pub protocols: HashMap<String, ProtocolConfig>,
171}
172
173fn default_engine_name() -> String {
174    "trap-simulator".to_string()
175}
176
177fn default_max_devices() -> usize {
178    10_000
179}
180
181fn default_max_points() -> usize {
182    1_000_000
183}
184
185fn default_tick_interval_ms() -> u64 {
186    100
187}
188
189fn default_workers() -> usize {
190    num_cpus::get().max(4)
191}
192
193fn default_true() -> bool {
194    true
195}
196
197fn default_metrics_interval() -> u64 {
198    10
199}
200
201fn default_log_level() -> String {
202    "info".to_string()
203}
204
205impl Default for EngineConfig {
206    fn default() -> Self {
207        Self {
208            name: default_engine_name(),
209            max_devices: default_max_devices(),
210            max_points: default_max_points(),
211            tick_interval_ms: default_tick_interval_ms(),
212            workers: default_workers(),
213            enable_metrics: true,
214            metrics_interval_secs: default_metrics_interval(),
215            log_level: default_log_level(),
216            protocols: HashMap::new(),
217        }
218    }
219}
220
221impl EngineConfig {
222    /// Create a new engine config with default values.
223    pub fn new() -> Self {
224        Self::default()
225    }
226
227    /// Load config from a YAML file.
228    pub fn from_yaml_file(path: impl Into<PathBuf>) -> crate::Result<Self> {
229        ConfigLoader::load_with_format(path.into(), ConfigFormat::Yaml)
230    }
231
232    /// Load config from a JSON file.
233    pub fn from_json_file(path: impl Into<PathBuf>) -> crate::Result<Self> {
234        ConfigLoader::load_with_format(path.into(), ConfigFormat::Json)
235    }
236
237    /// Load config from a TOML file.
238    pub fn from_toml_file(path: impl Into<PathBuf>) -> crate::Result<Self> {
239        ConfigLoader::load_with_format(path.into(), ConfigFormat::Toml)
240    }
241
242    /// Load config from any supported file (auto-detect format).
243    pub fn from_file(path: impl Into<PathBuf>) -> crate::Result<Self> {
244        ConfigLoader::load(path.into())
245    }
246
247    /// Get tick interval as Duration.
248    pub fn tick_interval(&self) -> Duration {
249        Duration::from_millis(self.tick_interval_ms)
250    }
251
252    /// Set maximum devices.
253    pub fn with_max_devices(mut self, max: usize) -> Self {
254        self.max_devices = max;
255        self
256    }
257
258    /// Set maximum data points.
259    pub fn with_max_points(mut self, max: usize) -> Self {
260        self.max_points = max;
261        self
262    }
263
264    /// Set tick interval.
265    pub fn with_tick_interval(mut self, interval: Duration) -> Self {
266        self.tick_interval_ms = interval.as_millis() as u64;
267        self
268    }
269
270    /// Set number of worker threads.
271    pub fn with_workers(mut self, workers: usize) -> Self {
272        self.workers = workers;
273        self
274    }
275
276    /// Set log level.
277    pub fn with_log_level(mut self, level: impl Into<String>) -> Self {
278        self.log_level = level.into();
279        self
280    }
281
282    /// Enable or disable metrics.
283    pub fn with_metrics(mut self, enable: bool) -> Self {
284        self.enable_metrics = enable;
285        self
286    }
287
288    /// Add protocol config.
289    pub fn with_protocol(mut self, name: impl Into<String>, config: ProtocolConfig) -> Self {
290        self.protocols.insert(name.into(), config);
291        self
292    }
293
294    /// Apply environment variable overrides.
295    ///
296    /// Reads environment variables with the `TRAP_SIM_` prefix and applies
297    /// matching values to the configuration.
298    pub fn apply_env_overrides(&mut self) -> EnvApplyResult {
299        Self::env_overrides().apply(self)
300    }
301
302    /// Create environment overrides configuration.
303    pub fn env_overrides() -> EnvOverrides<Self> {
304        EnvOverrides::with_prefix(DEFAULT_PREFIX)
305            .add_rule(
306                EnvRuleBuilder::new("ENGINE_NAME")
307                    .field_path("name")
308                    .description("Engine instance name")
309                    .as_string(|c: &mut Self, v| c.name = v),
310            )
311            .add_rule(
312                EnvRuleBuilder::new("ENGINE_MAX_DEVICES")
313                    .field_path("max_devices")
314                    .description("Maximum number of devices")
315                    .parse_into(|c: &mut Self, v: usize| c.max_devices = v),
316            )
317            .add_rule(
318                EnvRuleBuilder::new("ENGINE_MAX_POINTS")
319                    .field_path("max_points")
320                    .description("Maximum number of data points")
321                    .parse_into(|c: &mut Self, v: usize| c.max_points = v),
322            )
323            .add_rule(
324                EnvRuleBuilder::new("ENGINE_TICK_INTERVAL_MS")
325                    .field_path("tick_interval_ms")
326                    .description("Tick interval in milliseconds")
327                    .parse_into(|c: &mut Self, v: u64| c.tick_interval_ms = v),
328            )
329            .add_rule(
330                EnvRuleBuilder::new("ENGINE_WORKERS")
331                    .field_path("workers")
332                    .description("Number of worker threads")
333                    .parse_into(|c: &mut Self, v: usize| c.workers = v),
334            )
335            .add_rule(
336                EnvRuleBuilder::new("ENGINE_METRICS")
337                    .field_path("enable_metrics")
338                    .description("Enable metrics collection")
339                    .as_bool(|c: &mut Self, v| c.enable_metrics = v),
340            )
341            .add_rule(
342                EnvRuleBuilder::new("ENGINE_METRICS_INTERVAL")
343                    .field_path("metrics_interval_secs")
344                    .description("Metrics export interval in seconds")
345                    .parse_into(|c: &mut Self, v: u64| c.metrics_interval_secs = v),
346            )
347            .add_rule(
348                EnvRuleBuilder::new("LOG_LEVEL")
349                    .field_path("log_level")
350                    .description("Log level (trace, debug, info, warn, error)")
351                    .as_string(|c: &mut Self, v| c.log_level = v),
352            )
353    }
354}
355
356impl EnvConfigurable for EngineConfig {
357    fn env_overrides() -> EnvOverrides<Self> {
358        Self::env_overrides()
359    }
360}
361
362impl Validatable for EngineConfig {
363    fn validate(&self) -> crate::Result<()> {
364        let mut errors = crate::error::ValidationErrors::new();
365        self.validate_collect(&mut errors);
366        errors.into_result(())
367    }
368
369    fn validate_collect(&self, errors: &mut crate::error::ValidationErrors) {
370        // Validate name
371        if self.name.trim().is_empty() {
372            errors.add("name", "Engine name cannot be empty");
373        }
374
375        // Validate max_devices
376        if self.max_devices == 0 {
377            errors.add("max_devices", "Max devices must be greater than 0");
378        }
379        if self.max_devices > 1_000_000 {
380            errors.add("max_devices", "Max devices cannot exceed 1,000,000");
381        }
382
383        // Validate max_points
384        if self.max_points == 0 {
385            errors.add("max_points", "Max points must be greater than 0");
386        }
387        if self.max_points > 100_000_000 {
388            errors.add("max_points", "Max points cannot exceed 100,000,000");
389        }
390
391        // Validate tick_interval_ms
392        if self.tick_interval_ms < 1 {
393            errors.add("tick_interval_ms", "Tick interval must be at least 1ms");
394        }
395        if self.tick_interval_ms > 60_000 {
396            errors.add("tick_interval_ms", "Tick interval cannot exceed 60 seconds");
397        }
398
399        // Validate workers
400        if self.workers == 0 {
401            errors.add("workers", "Workers must be greater than 0");
402        }
403        if self.workers > 1024 {
404            errors.add("workers", "Workers cannot exceed 1024");
405        }
406
407        // Validate metrics_interval_secs
408        if self.enable_metrics && self.metrics_interval_secs == 0 {
409            errors.add("metrics_interval_secs", "Metrics interval must be greater than 0 when metrics are enabled");
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.iter().any(|d| d.var_name == "TRAP_SIM_ENGINE_MAX_DEVICES"));
895        assert!(docs.iter().any(|d| d.var_name == "TRAP_SIM_LOG_LEVEL"));
896    }
897
898    #[test]
899    fn test_protocol_config_modbus_tcp() {
900        let config = ProtocolConfig::ModbusTcp(ModbusTcpConfig::default());
901        let yaml = serde_yaml::to_string(&config).unwrap();
902        assert!(yaml.contains("type: modbustcp"));
903    }
904
905    #[test]
906    fn test_protocol_config_opcua() {
907        let config = ProtocolConfig::OpcUa(OpcUaConfig::default());
908        let yaml = serde_yaml::to_string(&config).unwrap();
909        assert!(yaml.contains("type: opcua"));
910    }
911
912    #[test]
913    fn test_protocol_config_bacnet() {
914        let config = ProtocolConfig::BacnetIp(BacnetIpConfig::default());
915        assert_eq!(BacnetIpConfig::default().device_instance, 1234);
916    }
917
918    #[test]
919    fn test_protocol_config_knx() {
920        let config = ProtocolConfig::KnxIp(KnxIpConfig::default());
921        assert_eq!(KnxIpConfig::default().individual_address, "1.1.1");
922    }
923
924    #[test]
925    fn test_engine_config_with_protocol() {
926        let config = EngineConfig::default()
927            .with_protocol("modbus", ProtocolConfig::ModbusTcp(ModbusTcpConfig::default()));
928
929        assert!(config.protocols.contains_key("modbus"));
930    }
931
932    #[test]
933    fn test_config_format_detection() {
934        assert_eq!(ConfigFormat::from_path("config.yaml"), Some(ConfigFormat::Yaml));
935        assert_eq!(ConfigFormat::from_path("config.yml"), Some(ConfigFormat::Yaml));
936        assert_eq!(ConfigFormat::from_path("config.json"), Some(ConfigFormat::Json));
937        assert_eq!(ConfigFormat::from_path("config.toml"), Some(ConfigFormat::Toml));
938        assert_eq!(ConfigFormat::from_path("config.txt"), None);
939    }
940}