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