Skip to main content

aranet_core/
mock.rs

1//! Mock device implementation for testing.
2//!
3//! This module provides a mock device that can be used for unit testing
4//! without requiring actual BLE hardware.
5//!
6//! The [`MockDevice`] implements the [`AranetDevice`] trait, allowing it to be
7//! used interchangeably with real devices in generic code.
8//!
9//! # Features
10//!
11//! - **Failure injection**: Set the device to fail on specific operations
12//! - **Latency simulation**: Add artificial delays to simulate slow BLE responses
13//! - **Custom behavior**: Inject custom reading generators for dynamic test scenarios
14
15use std::sync::atomic::{AtomicBool, AtomicI16, AtomicU32, AtomicU64, Ordering};
16use std::time::Duration;
17
18use tokio::sync::RwLock;
19
20use aranet_types::{CurrentReading, DeviceInfo, DeviceType, HistoryRecord, Status};
21
22use crate::error::{Error, Result};
23use crate::history::{HistoryInfo, HistoryOptions};
24use crate::settings::{CalibrationData, MeasurementInterval};
25use crate::traits::AranetDevice;
26
27/// A mock Aranet device for testing.
28///
29/// Implements [`AranetDevice`] trait for use in generic code and testing.
30///
31/// # Example
32///
33/// ```
34/// use aranet_core::{MockDevice, AranetDevice};
35/// use aranet_types::DeviceType;
36///
37/// #[tokio::main]
38/// async fn main() {
39///     let device = MockDevice::new("Test", DeviceType::Aranet4);
40///     device.connect().await.unwrap();
41///
42///     // Can use through trait
43///     async fn read_via_trait<D: AranetDevice>(d: &D) {
44///         let _ = d.read_current().await;
45///     }
46///     read_via_trait(&device).await;
47/// }
48/// ```
49pub struct MockDevice {
50    name: String,
51    address: String,
52    device_type: DeviceType,
53    connected: AtomicBool,
54    current_reading: RwLock<CurrentReading>,
55    device_info: RwLock<DeviceInfo>,
56    history: RwLock<Vec<HistoryRecord>>,
57    interval: RwLock<MeasurementInterval>,
58    calibration: RwLock<CalibrationData>,
59    battery: RwLock<u8>,
60    rssi: AtomicI16,
61    read_count: AtomicU32,
62    should_fail: AtomicBool,
63    fail_message: RwLock<String>,
64    /// Simulated read latency in milliseconds (0 = no delay).
65    read_latency_ms: AtomicU64,
66    /// Simulated connect latency in milliseconds (0 = no delay).
67    connect_latency_ms: AtomicU64,
68    /// Number of operations to fail before succeeding (0 = always succeed/fail based on should_fail).
69    fail_count: AtomicU32,
70    /// Current count of failures (decremented on each failure).
71    remaining_failures: AtomicU32,
72}
73
74impl std::fmt::Debug for MockDevice {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        f.debug_struct("MockDevice")
77            .field("name", &self.name)
78            .field("address", &self.address)
79            .field("device_type", &self.device_type)
80            .field("connected", &self.connected.load(Ordering::Relaxed))
81            .finish()
82    }
83}
84
85impl MockDevice {
86    /// Create a new mock device with default values.
87    pub fn new(name: &str, device_type: DeviceType) -> Self {
88        Self {
89            name: name.to_string(),
90            address: format!("MOCK-{:06X}", rand::random::<u32>() % 0xFFFFFF),
91            device_type,
92            connected: AtomicBool::new(false),
93            current_reading: RwLock::new(Self::default_reading()),
94            device_info: RwLock::new(Self::default_info(name)),
95            history: RwLock::new(Vec::new()),
96            interval: RwLock::new(MeasurementInterval::FiveMinutes),
97            calibration: RwLock::new(CalibrationData::default()),
98            battery: RwLock::new(85),
99            rssi: AtomicI16::new(-50),
100            read_count: AtomicU32::new(0),
101            should_fail: AtomicBool::new(false),
102            fail_message: RwLock::new("Mock failure".to_string()),
103            read_latency_ms: AtomicU64::new(0),
104            connect_latency_ms: AtomicU64::new(0),
105            fail_count: AtomicU32::new(0),
106            remaining_failures: AtomicU32::new(0),
107        }
108    }
109
110    fn default_reading() -> CurrentReading {
111        CurrentReading {
112            co2: 800,
113            temperature: 22.5,
114            pressure: 1013.2,
115            humidity: 50,
116            battery: 85,
117            status: Status::Green,
118            interval: 300,
119            age: 60,
120            captured_at: None,
121            radon: None,
122            radiation_rate: None,
123            radiation_total: None,
124            radon_avg_24h: None,
125            radon_avg_7d: None,
126            radon_avg_30d: None,
127        }
128    }
129
130    fn default_info(name: &str) -> DeviceInfo {
131        DeviceInfo {
132            name: name.to_string(),
133            model: "Aranet4".to_string(),
134            serial: "MOCK-12345".to_string(),
135            firmware: "v1.5.0".to_string(),
136            hardware: "1.0".to_string(),
137            software: "1.5.0".to_string(),
138            manufacturer: "SAF Tehnika".to_string(),
139        }
140    }
141
142    /// Connect to the mock device.
143    pub async fn connect(&self) -> Result<()> {
144        use crate::error::DeviceNotFoundReason;
145
146        // Simulate connect latency
147        let latency = self.connect_latency_ms.load(Ordering::Relaxed);
148        if latency > 0 {
149            tokio::time::sleep(Duration::from_millis(latency)).await;
150        }
151
152        // Check for transient failures first
153        if self.remaining_failures.load(Ordering::Relaxed) > 0 {
154            self.remaining_failures.fetch_sub(1, Ordering::Relaxed);
155            return Err(Error::DeviceNotFound(DeviceNotFoundReason::NotFound {
156                identifier: self.name.clone(),
157            }));
158        }
159
160        if self.should_fail.load(Ordering::Relaxed) {
161            return Err(Error::DeviceNotFound(DeviceNotFoundReason::NotFound {
162                identifier: self.name.clone(),
163            }));
164        }
165        self.connected.store(true, Ordering::Relaxed);
166        Ok(())
167    }
168
169    /// Disconnect from the mock device.
170    pub async fn disconnect(&self) -> Result<()> {
171        self.connected.store(false, Ordering::Relaxed);
172        Ok(())
173    }
174
175    /// Check if connected (sync method for internal use).
176    pub fn is_connected_sync(&self) -> bool {
177        self.connected.load(Ordering::Relaxed)
178    }
179
180    /// Get the device name.
181    pub fn name(&self) -> &str {
182        &self.name
183    }
184
185    /// Get the device address.
186    pub fn address(&self) -> &str {
187        &self.address
188    }
189
190    /// Get the device type.
191    pub fn device_type(&self) -> DeviceType {
192        self.device_type
193    }
194
195    /// Read current sensor values.
196    pub async fn read_current(&self) -> Result<CurrentReading> {
197        self.check_connected()?;
198        self.check_should_fail().await?;
199
200        self.read_count.fetch_add(1, Ordering::Relaxed);
201        Ok(*self.current_reading.read().await)
202    }
203
204    /// Read battery level.
205    pub async fn read_battery(&self) -> Result<u8> {
206        self.check_connected()?;
207        self.check_should_fail().await?;
208        Ok(*self.battery.read().await)
209    }
210
211    /// Read RSSI (signal strength).
212    pub async fn read_rssi(&self) -> Result<i16> {
213        self.check_connected()?;
214        self.check_should_fail().await?;
215        Ok(self.rssi.load(Ordering::Relaxed))
216    }
217
218    /// Read device info.
219    pub async fn read_device_info(&self) -> Result<DeviceInfo> {
220        self.check_connected()?;
221        self.check_should_fail().await?;
222        Ok(self.device_info.read().await.clone())
223    }
224
225    /// Get history info.
226    pub async fn get_history_info(&self) -> Result<HistoryInfo> {
227        self.check_connected()?;
228        self.check_should_fail().await?;
229
230        let history = self.history.read().await;
231        let interval = self.interval.read().await;
232
233        Ok(HistoryInfo {
234            total_readings: history.len() as u16,
235            interval_seconds: interval.as_seconds(),
236            seconds_since_update: 60,
237        })
238    }
239
240    /// Download history.
241    pub async fn download_history(&self) -> Result<Vec<HistoryRecord>> {
242        self.check_connected()?;
243        self.check_should_fail().await?;
244        Ok(self.history.read().await.clone())
245    }
246
247    /// Download history with options.
248    pub async fn download_history_with_options(
249        &self,
250        options: HistoryOptions,
251    ) -> Result<Vec<HistoryRecord>> {
252        self.check_connected()?;
253        self.check_should_fail().await?;
254
255        let history = self.history.read().await;
256        // History indices are 1-based inclusive. Convert to 0-based:
257        // start: 1-based → 0-based by subtracting 1
258        // end: 1-based inclusive → 0-based exclusive (value stays the same)
259        let start = options.start_index.unwrap_or(1).saturating_sub(1) as usize;
260        let end = options
261            .end_index
262            .map(|e| e as usize)
263            .unwrap_or(history.len());
264
265        // Report progress if callback provided
266        if let Some(ref _callback) = options.progress_callback {
267            // For mock, we report progress immediately
268            let progress = crate::history::HistoryProgress::new(
269                crate::history::HistoryParam::Co2,
270                1,
271                1,
272                history.len().min(end).saturating_sub(start),
273            );
274            options.report_progress(&progress);
275        }
276
277        Ok(history
278            .iter()
279            .skip(start)
280            .take(end.saturating_sub(start))
281            .cloned()
282            .collect())
283    }
284
285    /// Get the measurement interval.
286    pub async fn get_interval(&self) -> Result<MeasurementInterval> {
287        self.check_connected()?;
288        self.check_should_fail().await?;
289        Ok(*self.interval.read().await)
290    }
291
292    /// Set the measurement interval.
293    pub async fn set_interval(&self, interval: MeasurementInterval) -> Result<()> {
294        self.check_connected()?;
295        self.check_should_fail().await?;
296        *self.interval.write().await = interval;
297        Ok(())
298    }
299
300    /// Get calibration data.
301    pub async fn get_calibration(&self) -> Result<CalibrationData> {
302        self.check_connected()?;
303        self.check_should_fail().await?;
304        Ok(self.calibration.read().await.clone())
305    }
306
307    fn check_connected(&self) -> Result<()> {
308        if !self.connected.load(Ordering::Relaxed) {
309            Err(Error::NotConnected)
310        } else {
311            Ok(())
312        }
313    }
314
315    async fn check_should_fail(&self) -> Result<()> {
316        // Simulate read latency
317        let latency = self.read_latency_ms.load(Ordering::Relaxed);
318        if latency > 0 {
319            tokio::time::sleep(Duration::from_millis(latency)).await;
320        }
321
322        // Check for transient failures first
323        if self.remaining_failures.load(Ordering::Relaxed) > 0 {
324            self.remaining_failures.fetch_sub(1, Ordering::Relaxed);
325            return Err(Error::InvalidData(self.fail_message.read().await.clone()));
326        }
327
328        if self.should_fail.load(Ordering::Relaxed) {
329            Err(Error::InvalidData(self.fail_message.read().await.clone()))
330        } else {
331            Ok(())
332        }
333    }
334
335    // --- Test control methods ---
336
337    /// Set the current reading for testing.
338    pub async fn set_reading(&self, reading: CurrentReading) {
339        *self.current_reading.write().await = reading;
340    }
341
342    /// Set CO2 level directly.
343    pub async fn set_co2(&self, co2: u16) {
344        self.current_reading.write().await.co2 = co2;
345    }
346
347    /// Set temperature directly.
348    pub async fn set_temperature(&self, temp: f32) {
349        self.current_reading.write().await.temperature = temp;
350    }
351
352    /// Set battery level.
353    pub async fn set_battery(&self, level: u8) {
354        *self.battery.write().await = level;
355        self.current_reading.write().await.battery = level;
356    }
357
358    /// Set radon concentration in Bq/m³ (AranetRn+ devices).
359    pub async fn set_radon(&self, radon: u32) {
360        self.current_reading.write().await.radon = Some(radon);
361    }
362
363    /// Set radon averages (AranetRn+ devices).
364    pub async fn set_radon_averages(&self, avg_24h: u32, avg_7d: u32, avg_30d: u32) {
365        let mut reading = self.current_reading.write().await;
366        reading.radon_avg_24h = Some(avg_24h);
367        reading.radon_avg_7d = Some(avg_7d);
368        reading.radon_avg_30d = Some(avg_30d);
369    }
370
371    /// Set radiation values (Aranet Radiation devices).
372    pub async fn set_radiation(&self, rate: f32, total: f64) {
373        let mut reading = self.current_reading.write().await;
374        reading.radiation_rate = Some(rate);
375        reading.radiation_total = Some(total);
376    }
377
378    /// Set RSSI (signal strength) for testing.
379    pub fn set_rssi(&self, rssi: i16) {
380        self.rssi.store(rssi, Ordering::Relaxed);
381    }
382
383    /// Add history records.
384    pub async fn add_history(&self, records: Vec<HistoryRecord>) {
385        self.history.write().await.extend(records);
386    }
387
388    /// Make the device fail on next operation.
389    pub async fn set_should_fail(&self, fail: bool, message: Option<&str>) {
390        self.should_fail.store(fail, Ordering::Relaxed);
391        if let Some(msg) = message {
392            *self.fail_message.write().await = msg.to_string();
393        }
394    }
395
396    /// Get the number of read operations performed.
397    pub fn read_count(&self) -> u32 {
398        self.read_count.load(Ordering::Relaxed)
399    }
400
401    /// Reset read count.
402    pub fn reset_read_count(&self) {
403        self.read_count.store(0, Ordering::Relaxed);
404    }
405
406    /// Set simulated read latency.
407    ///
408    /// Each read operation will be delayed by this duration.
409    /// Set to `Duration::ZERO` to disable latency simulation.
410    pub fn set_read_latency(&self, latency: Duration) {
411        self.read_latency_ms
412            .store(latency.as_millis() as u64, Ordering::Relaxed);
413    }
414
415    /// Set simulated connect latency.
416    ///
417    /// Connect operations will be delayed by this duration.
418    /// Set to `Duration::ZERO` to disable latency simulation.
419    pub fn set_connect_latency(&self, latency: Duration) {
420        self.connect_latency_ms
421            .store(latency.as_millis() as u64, Ordering::Relaxed);
422    }
423
424    /// Configure transient failures.
425    ///
426    /// The device will fail the next `count` operations, then succeed.
427    /// This is useful for testing retry logic.
428    ///
429    /// # Example
430    ///
431    /// ```
432    /// use aranet_core::MockDevice;
433    /// use aranet_types::DeviceType;
434    ///
435    /// let device = MockDevice::new("Test", DeviceType::Aranet4);
436    /// // First 3 connect attempts will fail, 4th will succeed
437    /// device.set_transient_failures(3);
438    /// ```
439    pub fn set_transient_failures(&self, count: u32) {
440        self.fail_count.store(count, Ordering::Relaxed);
441        self.remaining_failures.store(count, Ordering::Relaxed);
442    }
443
444    /// Reset transient failure counter.
445    pub fn reset_transient_failures(&self) {
446        self.remaining_failures
447            .store(self.fail_count.load(Ordering::Relaxed), Ordering::Relaxed);
448    }
449
450    /// Get the number of remaining transient failures.
451    pub fn remaining_failures(&self) -> u32 {
452        self.remaining_failures.load(Ordering::Relaxed)
453    }
454}
455
456// Implement the AranetDevice trait for MockDevice
457impl AranetDevice for MockDevice {
458    // --- Connection Management ---
459
460    async fn is_connected(&self) -> bool {
461        self.is_connected_sync()
462    }
463
464    async fn disconnect(&self) -> Result<()> {
465        MockDevice::disconnect(self).await
466    }
467
468    // --- Device Identity ---
469
470    fn name(&self) -> Option<&str> {
471        Some(MockDevice::name(self))
472    }
473
474    fn address(&self) -> &str {
475        MockDevice::address(self)
476    }
477
478    fn device_type(&self) -> Option<DeviceType> {
479        Some(MockDevice::device_type(self))
480    }
481
482    // --- Current Readings ---
483
484    async fn read_current(&self) -> Result<CurrentReading> {
485        MockDevice::read_current(self).await
486    }
487
488    async fn read_device_info(&self) -> Result<DeviceInfo> {
489        MockDevice::read_device_info(self).await
490    }
491
492    async fn read_rssi(&self) -> Result<i16> {
493        MockDevice::read_rssi(self).await
494    }
495
496    // --- Battery ---
497
498    async fn read_battery(&self) -> Result<u8> {
499        MockDevice::read_battery(self).await
500    }
501
502    // --- History ---
503
504    async fn get_history_info(&self) -> Result<crate::history::HistoryInfo> {
505        MockDevice::get_history_info(self).await
506    }
507
508    async fn download_history(&self) -> Result<Vec<HistoryRecord>> {
509        MockDevice::download_history(self).await
510    }
511
512    async fn download_history_with_options(
513        &self,
514        options: HistoryOptions,
515    ) -> Result<Vec<HistoryRecord>> {
516        MockDevice::download_history_with_options(self, options).await
517    }
518
519    // --- Settings ---
520
521    async fn get_interval(&self) -> Result<MeasurementInterval> {
522        MockDevice::get_interval(self).await
523    }
524
525    async fn set_interval(&self, interval: MeasurementInterval) -> Result<()> {
526        MockDevice::set_interval(self, interval).await
527    }
528
529    async fn get_calibration(&self) -> Result<CalibrationData> {
530        MockDevice::get_calibration(self).await
531    }
532}
533
534/// Builder for creating mock devices with custom settings.
535#[derive(Debug)]
536pub struct MockDeviceBuilder {
537    name: String,
538    device_type: DeviceType,
539    co2: u16,
540    temperature: f32,
541    pressure: f32,
542    humidity: u8,
543    battery: u8,
544    status: Status,
545    auto_connect: bool,
546    radon: Option<u32>,
547    radon_avg_24h: Option<u32>,
548    radon_avg_7d: Option<u32>,
549    radon_avg_30d: Option<u32>,
550    radiation_rate: Option<f32>,
551    radiation_total: Option<f64>,
552}
553
554impl Default for MockDeviceBuilder {
555    fn default() -> Self {
556        Self {
557            name: "Mock Aranet4".to_string(),
558            device_type: DeviceType::Aranet4,
559            co2: 800,
560            temperature: 22.5,
561            pressure: 1013.2,
562            humidity: 50,
563            battery: 85,
564            status: Status::Green,
565            auto_connect: true,
566            radon: None,
567            radon_avg_24h: None,
568            radon_avg_7d: None,
569            radon_avg_30d: None,
570            radiation_rate: None,
571            radiation_total: None,
572        }
573    }
574}
575
576impl MockDeviceBuilder {
577    /// Create a new builder.
578    #[must_use]
579    pub fn new() -> Self {
580        Self::default()
581    }
582
583    /// Set the device name.
584    #[must_use]
585    pub fn name(mut self, name: &str) -> Self {
586        self.name = name.to_string();
587        self
588    }
589
590    /// Set the device type.
591    #[must_use]
592    pub fn device_type(mut self, device_type: DeviceType) -> Self {
593        self.device_type = device_type;
594        self
595    }
596
597    /// Set the CO2 level.
598    #[must_use]
599    pub fn co2(mut self, co2: u16) -> Self {
600        self.co2 = co2;
601        self
602    }
603
604    /// Set the temperature.
605    #[must_use]
606    pub fn temperature(mut self, temp: f32) -> Self {
607        self.temperature = temp;
608        self
609    }
610
611    /// Set the pressure.
612    #[must_use]
613    pub fn pressure(mut self, pressure: f32) -> Self {
614        self.pressure = pressure;
615        self
616    }
617
618    /// Set the humidity.
619    #[must_use]
620    pub fn humidity(mut self, humidity: u8) -> Self {
621        self.humidity = humidity;
622        self
623    }
624
625    /// Set the battery level.
626    #[must_use]
627    pub fn battery(mut self, battery: u8) -> Self {
628        self.battery = battery;
629        self
630    }
631
632    /// Set the status.
633    #[must_use]
634    pub fn status(mut self, status: Status) -> Self {
635        self.status = status;
636        self
637    }
638
639    /// Set whether to auto-connect.
640    #[must_use]
641    pub fn auto_connect(mut self, auto: bool) -> Self {
642        self.auto_connect = auto;
643        self
644    }
645
646    /// Set radon concentration in Bq/m³ (AranetRn+ devices).
647    #[must_use]
648    pub fn radon(mut self, radon: u32) -> Self {
649        self.radon = Some(radon);
650        self
651    }
652
653    /// Set 24-hour average radon concentration in Bq/m³ (AranetRn+ devices).
654    #[must_use]
655    pub fn radon_avg_24h(mut self, avg: u32) -> Self {
656        self.radon_avg_24h = Some(avg);
657        self
658    }
659
660    /// Set 7-day average radon concentration in Bq/m³ (AranetRn+ devices).
661    #[must_use]
662    pub fn radon_avg_7d(mut self, avg: u32) -> Self {
663        self.radon_avg_7d = Some(avg);
664        self
665    }
666
667    /// Set 30-day average radon concentration in Bq/m³ (AranetRn+ devices).
668    #[must_use]
669    pub fn radon_avg_30d(mut self, avg: u32) -> Self {
670        self.radon_avg_30d = Some(avg);
671        self
672    }
673
674    /// Set radiation dose rate in µSv/h (Aranet Radiation devices).
675    #[must_use]
676    pub fn radiation_rate(mut self, rate: f32) -> Self {
677        self.radiation_rate = Some(rate);
678        self
679    }
680
681    /// Set total radiation dose in mSv (Aranet Radiation devices).
682    #[must_use]
683    pub fn radiation_total(mut self, total: f64) -> Self {
684        self.radiation_total = Some(total);
685        self
686    }
687
688    /// Build the mock device.
689    ///
690    /// Note: This is a sync method that sets initial state directly.
691    /// The device is created with the specified reading already set.
692    #[must_use]
693    pub fn build(self) -> MockDevice {
694        let reading = CurrentReading {
695            co2: self.co2,
696            temperature: self.temperature,
697            pressure: self.pressure,
698            humidity: self.humidity,
699            battery: self.battery,
700            status: self.status,
701            interval: 300,
702            age: 60,
703            captured_at: None,
704            radon: self.radon,
705            radiation_rate: self.radiation_rate,
706            radiation_total: self.radiation_total,
707            radon_avg_24h: self.radon_avg_24h,
708            radon_avg_7d: self.radon_avg_7d,
709            radon_avg_30d: self.radon_avg_30d,
710        };
711
712        MockDevice {
713            name: self.name.clone(),
714            address: format!("MOCK-{:06X}", rand::random::<u32>() % 0xFFFFFF),
715            device_type: self.device_type,
716            connected: AtomicBool::new(self.auto_connect),
717            current_reading: RwLock::new(reading),
718            device_info: RwLock::new(MockDevice::default_info(&self.name)),
719            history: RwLock::new(Vec::new()),
720            interval: RwLock::new(MeasurementInterval::FiveMinutes),
721            calibration: RwLock::new(CalibrationData::default()),
722            battery: RwLock::new(self.battery),
723            rssi: AtomicI16::new(-50),
724            read_count: AtomicU32::new(0),
725            should_fail: AtomicBool::new(false),
726            fail_message: RwLock::new("Mock failure".to_string()),
727            read_latency_ms: AtomicU64::new(0),
728            connect_latency_ms: AtomicU64::new(0),
729            fail_count: AtomicU32::new(0),
730            remaining_failures: AtomicU32::new(0),
731        }
732    }
733}
734
735/// Unit tests for MockDevice and MockDeviceBuilder.
736///
737/// These tests verify the mock device implementation used for testing
738/// without requiring actual BLE hardware.
739///
740/// # Test Categories
741///
742/// ## Connection Tests
743/// - `test_mock_device_connect`: Connect/disconnect lifecycle
744/// - `test_mock_device_not_connected`: Error when reading without connection
745///
746/// ## Reading Tests
747/// - `test_mock_device_read`: Basic reading retrieval
748/// - `test_mock_device_read_battery`: Battery level reading
749/// - `test_mock_device_read_rssi`: Signal strength reading
750/// - `test_mock_device_read_device_info`: Device information
751/// - `test_mock_device_set_values`: Dynamic value updates
752///
753/// ## History Tests
754/// - `test_mock_device_history`: History download
755/// - `test_mock_device_history_with_options`: Filtered history download
756/// - `test_mock_device_history_info`: History metadata
757///
758/// ## Settings Tests
759/// - `test_mock_device_interval`: Measurement interval get/set
760/// - `test_mock_device_calibration`: Calibration data
761///
762/// ## Failure Injection Tests
763/// - `test_mock_device_fail`: Permanent failure mode
764/// - `test_mock_device_transient_failures`: Temporary failures for retry testing
765///
766/// ## Builder Tests
767/// - `test_builder_defaults`: Default builder values
768/// - `test_builder_all_options`: Full builder customization
769///
770/// ## Trait Tests
771/// - `test_aranet_device_trait`: Using MockDevice through AranetDevice trait
772/// - `test_trait_methods_match_direct_methods`: Trait/direct method consistency
773///
774/// # Running Tests
775///
776/// ```bash
777/// cargo test -p aranet-core mock::tests
778/// ```
779#[cfg(test)]
780mod tests {
781    use super::*;
782    use crate::traits::AranetDevice;
783
784    #[tokio::test]
785    async fn test_mock_device_connect() {
786        let device = MockDevice::new("Test", DeviceType::Aranet4);
787        assert!(!device.is_connected_sync());
788
789        device.connect().await.unwrap();
790        assert!(device.is_connected_sync());
791
792        device.disconnect().await.unwrap();
793        assert!(!device.is_connected_sync());
794    }
795
796    #[tokio::test]
797    async fn test_mock_device_read() {
798        let device = MockDeviceBuilder::new().co2(1200).temperature(25.0).build();
799
800        let reading = device.read_current().await.unwrap();
801        assert_eq!(reading.co2, 1200);
802        assert!((reading.temperature - 25.0).abs() < 0.01);
803    }
804
805    #[tokio::test]
806    async fn test_mock_device_fail() {
807        let device = MockDeviceBuilder::new().build();
808        device.set_should_fail(true, Some("Test error")).await;
809
810        let result = device.read_current().await;
811        assert!(result.is_err());
812        assert!(result.unwrap_err().to_string().contains("Test error"));
813    }
814
815    #[tokio::test]
816    async fn test_mock_device_not_connected() {
817        let device = MockDeviceBuilder::new().auto_connect(false).build();
818
819        let result = device.read_current().await;
820        assert!(matches!(result, Err(Error::NotConnected)));
821    }
822
823    #[test]
824    fn test_builder_defaults() {
825        let device = MockDeviceBuilder::new().build();
826        assert!(device.is_connected_sync());
827        assert_eq!(device.device_type(), DeviceType::Aranet4);
828    }
829
830    #[tokio::test]
831    async fn test_aranet_device_trait() {
832        let device = MockDeviceBuilder::new().co2(1000).build();
833
834        // Use via trait
835        async fn check_via_trait<D: AranetDevice>(d: &D) -> u16 {
836            d.read_current().await.unwrap().co2
837        }
838
839        assert_eq!(check_via_trait(&device).await, 1000);
840    }
841
842    #[tokio::test]
843    async fn test_mock_device_read_battery() {
844        let device = MockDeviceBuilder::new().battery(75).build();
845        let battery = device.read_battery().await.unwrap();
846        assert_eq!(battery, 75);
847    }
848
849    #[tokio::test]
850    async fn test_mock_device_read_rssi() {
851        let device = MockDeviceBuilder::new().build();
852        device.set_rssi(-65);
853        let rssi = device.read_rssi().await.unwrap();
854        assert_eq!(rssi, -65);
855    }
856
857    #[tokio::test]
858    async fn test_mock_device_read_device_info() {
859        let device = MockDeviceBuilder::new().name("Test Device").build();
860        let info = device.read_device_info().await.unwrap();
861        assert_eq!(info.name, "Test Device");
862        assert_eq!(info.manufacturer, "SAF Tehnika");
863    }
864
865    #[tokio::test]
866    async fn test_mock_device_history() {
867        let device = MockDeviceBuilder::new().build();
868
869        // Initially empty
870        let history = device.download_history().await.unwrap();
871        assert!(history.is_empty());
872
873        // Add some records
874        let records = vec![
875            HistoryRecord {
876                timestamp: time::OffsetDateTime::now_utc(),
877                co2: 800,
878                temperature: 22.5,
879                pressure: 1013.2,
880                humidity: 50,
881                radon: None,
882                radiation_rate: None,
883                radiation_total: None,
884            },
885            HistoryRecord {
886                timestamp: time::OffsetDateTime::now_utc(),
887                co2: 850,
888                temperature: 23.0,
889                pressure: 1013.5,
890                humidity: 48,
891                radon: None,
892                radiation_rate: None,
893                radiation_total: None,
894            },
895        ];
896        device.add_history(records).await;
897
898        let history = device.download_history().await.unwrap();
899        assert_eq!(history.len(), 2);
900        assert_eq!(history[0].co2, 800);
901        assert_eq!(history[1].co2, 850);
902    }
903
904    #[tokio::test]
905    async fn test_mock_device_history_with_options() {
906        let device = MockDeviceBuilder::new().build();
907
908        // Add 5 records
909        let records: Vec<HistoryRecord> = (0..5)
910            .map(|i| HistoryRecord {
911                timestamp: time::OffsetDateTime::now_utc(),
912                co2: 800 + i as u16 * 10,
913                temperature: 22.0,
914                pressure: 1013.0,
915                humidity: 50,
916                radon: None,
917                radiation_rate: None,
918                radiation_total: None,
919            })
920            .collect();
921        device.add_history(records).await;
922
923        // Download with range (1-based inclusive indices)
924        let options = HistoryOptions {
925            start_index: Some(2),
926            end_index: Some(4),
927            ..Default::default()
928        };
929        let history = device.download_history_with_options(options).await.unwrap();
930        assert_eq!(history.len(), 3);
931        assert_eq!(history[0].co2, 810); // Second record (1-based index 2)
932        assert_eq!(history[2].co2, 830); // Fourth record (1-based index 4)
933    }
934
935    #[tokio::test]
936    async fn test_mock_device_interval() {
937        let device = MockDeviceBuilder::new().build();
938
939        let interval = device.get_interval().await.unwrap();
940        assert_eq!(interval, MeasurementInterval::FiveMinutes);
941
942        device
943            .set_interval(MeasurementInterval::TenMinutes)
944            .await
945            .unwrap();
946        let interval = device.get_interval().await.unwrap();
947        assert_eq!(interval, MeasurementInterval::TenMinutes);
948    }
949
950    #[tokio::test]
951    async fn test_mock_device_calibration() {
952        let device = MockDeviceBuilder::new().build();
953        let calibration = device.get_calibration().await.unwrap();
954        // Default calibration should exist
955        assert!(calibration.co2_offset.is_some() || calibration.co2_offset.is_none());
956    }
957
958    #[tokio::test]
959    async fn test_mock_device_read_count() {
960        let device = MockDeviceBuilder::new().build();
961        assert_eq!(device.read_count(), 0);
962
963        device.read_current().await.unwrap();
964        assert_eq!(device.read_count(), 1);
965
966        device.read_current().await.unwrap();
967        device.read_current().await.unwrap();
968        assert_eq!(device.read_count(), 3);
969
970        device.reset_read_count();
971        assert_eq!(device.read_count(), 0);
972    }
973
974    #[tokio::test]
975    async fn test_mock_device_transient_failures() {
976        let device = MockDeviceBuilder::new().build();
977        device.set_transient_failures(2);
978
979        // First two reads should fail
980        assert!(device.read_current().await.is_err());
981        assert!(device.read_current().await.is_err());
982
983        // Third read should succeed
984        assert!(device.read_current().await.is_ok());
985    }
986
987    #[tokio::test]
988    async fn test_mock_device_set_values() {
989        let device = MockDeviceBuilder::new().build();
990
991        device.set_co2(1500).await;
992        device.set_temperature(30.0).await;
993        device.set_battery(50).await;
994
995        let reading = device.read_current().await.unwrap();
996        assert_eq!(reading.co2, 1500);
997        assert!((reading.temperature - 30.0).abs() < 0.01);
998        assert_eq!(reading.battery, 50);
999    }
1000
1001    #[tokio::test]
1002    async fn test_mock_device_history_info() {
1003        let device = MockDeviceBuilder::new().build();
1004
1005        // Add some records
1006        let records: Vec<HistoryRecord> = (0..10)
1007            .map(|_| HistoryRecord {
1008                timestamp: time::OffsetDateTime::now_utc(),
1009                co2: 800,
1010                temperature: 22.0,
1011                pressure: 1013.0,
1012                humidity: 50,
1013                radon: None,
1014                radiation_rate: None,
1015                radiation_total: None,
1016            })
1017            .collect();
1018        device.add_history(records).await;
1019
1020        let info = device.get_history_info().await.unwrap();
1021        assert_eq!(info.total_readings, 10);
1022        assert_eq!(info.interval_seconds, 300); // 5 minutes default
1023    }
1024
1025    #[tokio::test]
1026    async fn test_mock_device_debug() {
1027        let device = MockDevice::new("Debug Test", DeviceType::Aranet4);
1028        let debug_str = format!("{:?}", device);
1029        assert!(debug_str.contains("MockDevice"));
1030        assert!(debug_str.contains("Debug Test"));
1031        assert!(debug_str.contains("Aranet4"));
1032    }
1033
1034    #[test]
1035    fn test_builder_all_options() {
1036        let device = MockDeviceBuilder::new()
1037            .name("Custom Device")
1038            .device_type(DeviceType::Aranet2)
1039            .co2(0)
1040            .temperature(18.5)
1041            .pressure(1020.0)
1042            .humidity(65)
1043            .battery(90)
1044            .status(Status::Yellow)
1045            .auto_connect(false)
1046            .build();
1047
1048        assert_eq!(device.name(), "Custom Device");
1049        assert_eq!(device.device_type(), DeviceType::Aranet2);
1050        assert!(!device.is_connected_sync());
1051    }
1052
1053    #[tokio::test]
1054    async fn test_trait_methods_match_direct_methods() {
1055        let device = MockDeviceBuilder::new()
1056            .name("Trait Test")
1057            .co2(999)
1058            .battery(77)
1059            .build();
1060        device.set_rssi(-55);
1061
1062        // Test that trait methods return same values as direct methods
1063        // Use a generic helper to exercise the trait through static dispatch
1064        async fn check_device(
1065            trait_device: &impl AranetDevice,
1066        ) -> (aranet_types::CurrentReading, u8, i16) {
1067            let reading = trait_device.read_current().await.unwrap();
1068            let battery = trait_device.read_battery().await.unwrap();
1069            let rssi = trait_device.read_rssi().await.unwrap();
1070            (reading, battery, rssi)
1071        }
1072
1073        let trait_device = &device;
1074
1075        assert_eq!(AranetDevice::name(trait_device), Some("Trait Test"));
1076        assert_eq!(
1077            AranetDevice::device_type(trait_device),
1078            Some(DeviceType::Aranet4)
1079        );
1080        assert!(AranetDevice::is_connected(trait_device).await);
1081
1082        let (reading, battery, rssi) = check_device(trait_device).await;
1083        assert_eq!(reading.co2, 999);
1084        assert_eq!(battery, 77);
1085        assert_eq!(rssi, -55);
1086    }
1087}