Skip to main content

peat_btle/platform/
mock.rs

1// Copyright (c) 2025-2026 (r)evolve - Revolve Team LLC
2// SPDX-License-Identifier: Apache-2.0
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Mock BLE adapter for testing
17//!
18//! This module provides a simulated BLE adapter that enables unit and integration
19//! testing of Peat mesh logic without requiring actual BLE hardware.
20//!
21//! ## Features
22//!
23//! - Simulated device discovery and advertising
24//! - Configurable connection behavior (success, failure, latency)
25//! - Event tracking for test assertions
26//! - Multi-node simulation via shared state
27//!
28//! ## Example
29//!
30//! ```rust,no_run
31//! use peat_btle::platform::mock::{MockBleAdapter, MockNetwork};
32//! use peat_btle::platform::BleAdapter;
33//! use peat_btle::config::{BleConfig, DiscoveryConfig};
34//! use peat_btle::NodeId;
35//!
36//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
37//! // Create a shared network for multiple mock nodes
38//! let network = MockNetwork::new();
39//!
40//! // Create two mock adapters on the same network
41//! let mut adapter1 = MockBleAdapter::new(NodeId::new(0x111), network.clone());
42//! let mut adapter2 = MockBleAdapter::new(NodeId::new(0x222), network.clone());
43//!
44//! // Initialize and start both adapters
45//! adapter1.init(&BleConfig::default()).await?;
46//! adapter2.init(&BleConfig::default()).await?;
47//!
48//! // Start advertising on adapter2 so it can be discovered
49//! adapter2.start_advertising(&DiscoveryConfig::default()).await?;
50//!
51//! // Connect adapter1 to adapter2
52//! let conn = adapter1.connect(&NodeId::new(0x222)).await?;
53//! assert!(conn.is_alive());
54//! # Ok(())
55//! # }
56//! ```
57
58use std::collections::HashMap;
59use std::sync::atomic::{AtomicBool, AtomicI8, AtomicU16, Ordering};
60use std::sync::{Arc, Mutex, RwLock};
61use std::time::{Duration, Instant};
62
63use async_trait::async_trait;
64
65use crate::config::{BleConfig, BlePhy, DiscoveryConfig};
66use crate::error::{BleError, Result};
67use crate::platform::{
68    BleAdapter, ConnectionCallback, ConnectionEvent, DisconnectReason, DiscoveredDevice,
69    DiscoveryCallback,
70};
71use crate::transport::BleConnection;
72use crate::NodeId;
73
74/// Shared network state for multiple mock adapters
75///
76/// Allows multiple `MockBleAdapter` instances to "see" each other and
77/// simulate BLE discovery and connections.
78#[derive(Clone, Default)]
79pub struct MockNetwork {
80    inner: Arc<MockNetworkInner>,
81}
82
83#[derive(Default)]
84struct MockNetworkInner {
85    /// All nodes currently advertising on the network
86    advertising_nodes: RwLock<HashMap<NodeId, AdvertisingNode>>,
87    /// Active connections (bidirectional)
88    connections: RwLock<HashMap<(NodeId, NodeId), ConnectionState>>,
89    /// Data sent between nodes
90    data_queue: Mutex<HashMap<NodeId, Vec<DataPacket>>>,
91}
92
93/// An advertising node on the mock network
94#[derive(Clone)]
95struct AdvertisingNode {
96    node_id: NodeId,
97    address: String,
98    name: Option<String>,
99    rssi: i8,
100    adv_data: Vec<u8>,
101}
102
103/// State of a connection
104#[derive(Clone)]
105struct ConnectionState {
106    alive: Arc<AtomicBool>,
107}
108
109/// A data packet sent between nodes
110#[derive(Clone)]
111pub struct DataPacket {
112    /// Source node
113    pub from: NodeId,
114    /// Destination node
115    pub to: NodeId,
116    /// Payload data
117    pub data: Vec<u8>,
118    /// When it was sent
119    pub timestamp: Instant,
120}
121
122impl MockNetwork {
123    /// Create a new mock network
124    pub fn new() -> Self {
125        Self::default()
126    }
127
128    /// Register a node as advertising
129    pub fn start_advertising(&self, node_id: NodeId, address: &str, name: Option<&str>) {
130        let mut nodes = self.inner.advertising_nodes.write().unwrap();
131        nodes.insert(
132            node_id,
133            AdvertisingNode {
134                node_id,
135                address: address.to_string(),
136                name: name.map(|s| s.to_string()),
137                rssi: -50, // Default good signal
138                adv_data: vec![],
139            },
140        );
141    }
142
143    /// Stop advertising for a node
144    pub fn stop_advertising(&self, node_id: &NodeId) {
145        let mut nodes = self.inner.advertising_nodes.write().unwrap();
146        nodes.remove(node_id);
147    }
148
149    /// Get all advertising nodes visible to a given node
150    pub fn discover_nodes(&self, observer: &NodeId) -> Vec<DiscoveredDevice> {
151        let nodes = self.inner.advertising_nodes.read().unwrap();
152        nodes
153            .values()
154            .filter(|n| &n.node_id != observer)
155            .map(|n| DiscoveredDevice {
156                address: n.address.clone(),
157                name: n.name.clone(),
158                rssi: n.rssi,
159                is_peat_node: true,
160                node_id: Some(n.node_id),
161                adv_data: n.adv_data.clone(),
162            })
163            .collect()
164    }
165
166    /// Establish a connection between two nodes
167    pub fn connect(&self, from: &NodeId, to: &NodeId) -> Result<()> {
168        // Check if target is advertising
169        {
170            let nodes = self.inner.advertising_nodes.read().unwrap();
171            if !nodes.contains_key(to) {
172                return Err(BleError::ConnectionFailed(format!(
173                    "Node {} is not advertising",
174                    to
175                )));
176            }
177        }
178
179        // Create connection state
180        let state = ConnectionState {
181            alive: Arc::new(AtomicBool::new(true)),
182        };
183
184        // Store bidirectionally
185        let mut connections = self.inner.connections.write().unwrap();
186        connections.insert((*from, *to), state.clone());
187        connections.insert((*to, *from), state);
188
189        Ok(())
190    }
191
192    /// Disconnect two nodes
193    pub fn disconnect(&self, from: &NodeId, to: &NodeId) {
194        let mut connections = self.inner.connections.write().unwrap();
195        if let Some(state) = connections.remove(&(*from, *to)) {
196            state.alive.store(false, Ordering::SeqCst);
197        }
198        if let Some(state) = connections.remove(&(*to, *from)) {
199            state.alive.store(false, Ordering::SeqCst);
200        }
201    }
202
203    /// Check if two nodes are connected
204    pub fn is_connected(&self, from: &NodeId, to: &NodeId) -> bool {
205        let connections = self.inner.connections.read().unwrap();
206        connections
207            .get(&(*from, *to))
208            .is_some_and(|c| c.alive.load(Ordering::SeqCst))
209    }
210
211    /// Send data from one node to another
212    pub fn send_data(&self, from: &NodeId, to: &NodeId, data: Vec<u8>) -> Result<()> {
213        // Check if connection exists
214        {
215            let connections = self.inner.connections.read().unwrap();
216            if !connections.contains_key(&(*from, *to)) {
217                return Err(BleError::ConnectionFailed(format!(
218                    "No connection from {} to {}",
219                    from, to
220                )));
221            }
222        }
223
224        // Queue the data
225        let mut queue = self.inner.data_queue.lock().unwrap();
226        let packets = queue.entry(*to).or_default();
227        packets.push(DataPacket {
228            from: *from,
229            to: *to,
230            data,
231            timestamp: Instant::now(),
232        });
233
234        Ok(())
235    }
236
237    /// Receive pending data for a node
238    pub fn receive_data(&self, node_id: &NodeId) -> Vec<DataPacket> {
239        let mut queue = self.inner.data_queue.lock().unwrap();
240        queue.remove(node_id).unwrap_or_default()
241    }
242
243    /// Get all connected peers for a node
244    pub fn connected_peers(&self, node_id: &NodeId) -> Vec<NodeId> {
245        let connections = self.inner.connections.read().unwrap();
246        connections
247            .keys()
248            .filter(|(from, _)| from == node_id)
249            .map(|(_, to)| *to)
250            .collect()
251    }
252
253    /// Clear all network state (for test cleanup)
254    pub fn reset(&self) {
255        self.inner.advertising_nodes.write().unwrap().clear();
256        self.inner.connections.write().unwrap().clear();
257        self.inner.data_queue.lock().unwrap().clear();
258    }
259}
260
261/// Mock BLE connection
262pub struct MockConnection {
263    peer_id: NodeId,
264    mtu: AtomicU16,
265    phy: BlePhy,
266    rssi: AtomicI8,
267    alive: Arc<AtomicBool>,
268    established_at: Instant,
269}
270
271impl Clone for MockConnection {
272    fn clone(&self) -> Self {
273        Self {
274            peer_id: self.peer_id,
275            mtu: AtomicU16::new(self.mtu.load(Ordering::SeqCst)),
276            phy: self.phy,
277            rssi: AtomicI8::new(self.rssi.load(Ordering::SeqCst)),
278            alive: self.alive.clone(),
279            established_at: self.established_at,
280        }
281    }
282}
283
284impl MockConnection {
285    /// Create a new mock connection
286    pub fn new(peer_id: NodeId, mtu: u16, phy: BlePhy) -> Self {
287        Self {
288            peer_id,
289            mtu: AtomicU16::new(mtu),
290            phy,
291            rssi: AtomicI8::new(-50), // Default RSSI
292            alive: Arc::new(AtomicBool::new(true)),
293            established_at: Instant::now(),
294        }
295    }
296
297    /// Kill this connection
298    pub fn kill(&self) {
299        self.alive.store(false, Ordering::SeqCst);
300    }
301
302    /// Set RSSI value
303    pub fn set_rssi(&self, rssi: i8) {
304        self.rssi.store(rssi, Ordering::SeqCst);
305    }
306
307    /// Set MTU value
308    pub fn set_mtu(&self, mtu: u16) {
309        self.mtu.store(mtu, Ordering::SeqCst);
310    }
311}
312
313impl BleConnection for MockConnection {
314    fn peer_id(&self) -> &NodeId {
315        &self.peer_id
316    }
317
318    fn is_alive(&self) -> bool {
319        self.alive.load(Ordering::SeqCst)
320    }
321
322    fn mtu(&self) -> u16 {
323        self.mtu.load(Ordering::SeqCst)
324    }
325
326    fn phy(&self) -> BlePhy {
327        self.phy
328    }
329
330    fn rssi(&self) -> Option<i8> {
331        Some(self.rssi.load(Ordering::SeqCst))
332    }
333
334    fn connected_duration(&self) -> Duration {
335        self.established_at.elapsed()
336    }
337}
338
339/// Configuration for mock adapter behavior
340#[derive(Clone, Debug)]
341pub struct MockAdapterConfig {
342    /// Simulate connection failures (0.0 = never, 1.0 = always)
343    pub connection_failure_rate: f32,
344    /// Simulated connection latency
345    pub connection_latency: Duration,
346    /// Simulated scan latency before discovering devices
347    pub scan_latency: Duration,
348    /// Support Coded PHY
349    pub supports_coded_phy: bool,
350    /// Support extended advertising
351    pub supports_extended_advertising: bool,
352    /// Maximum MTU
353    pub max_mtu: u16,
354    /// Maximum connections
355    pub max_connections: u8,
356}
357
358impl Default for MockAdapterConfig {
359    fn default() -> Self {
360        Self {
361            connection_failure_rate: 0.0,
362            connection_latency: Duration::from_millis(50),
363            scan_latency: Duration::from_millis(10),
364            supports_coded_phy: true,
365            supports_extended_advertising: true,
366            max_mtu: 517,
367            max_connections: 8,
368        }
369    }
370}
371
372/// Mock BLE adapter for testing
373///
374/// Provides a fully simulated BLE adapter that can be used in unit tests
375/// without requiring actual BLE hardware.
376pub struct MockBleAdapter {
377    node_id: NodeId,
378    network: MockNetwork,
379    config: MockAdapterConfig,
380    powered: AtomicBool,
381    scanning: AtomicBool,
382    advertising: AtomicBool,
383    address: String,
384    discovery_callback: Mutex<Option<DiscoveryCallback>>,
385    connection_callback: Mutex<Option<ConnectionCallback>>,
386    connections: RwLock<HashMap<NodeId, Arc<MockConnection>>>,
387    /// Events recorded for test assertions
388    events: Mutex<Vec<MockEvent>>,
389}
390
391/// Events recorded by the mock adapter for test assertions
392#[derive(Clone, Debug)]
393pub enum MockEvent {
394    /// Adapter was initialized
395    Initialized,
396    /// Adapter was started
397    Started,
398    /// Adapter was stopped
399    Stopped,
400    /// Scanning for devices started
401    ScanStarted,
402    /// Scanning stopped
403    ScanStopped,
404    /// Advertising started
405    AdvertisingStarted,
406    /// Advertising stopped
407    AdvertisingStopped,
408    /// Connected to a peer
409    Connected(NodeId),
410    /// Disconnected from a peer
411    Disconnected(NodeId, DisconnectReason),
412    /// GATT service was registered
413    GattServiceRegistered,
414    /// GATT service was unregistered
415    GattServiceUnregistered,
416}
417
418impl MockBleAdapter {
419    /// Create a new mock adapter with default configuration
420    pub fn new(node_id: NodeId, network: MockNetwork) -> Self {
421        Self::with_config(node_id, network, MockAdapterConfig::default())
422    }
423
424    /// Create a new mock adapter with custom configuration
425    pub fn with_config(node_id: NodeId, network: MockNetwork, config: MockAdapterConfig) -> Self {
426        let address = format!(
427            "00:11:22:{:02X}:{:02X}:{:02X}",
428            (node_id.as_u32() >> 16) & 0xFF,
429            (node_id.as_u32() >> 8) & 0xFF,
430            node_id.as_u32() & 0xFF
431        );
432        Self {
433            node_id,
434            network,
435            config,
436            powered: AtomicBool::new(false),
437            scanning: AtomicBool::new(false),
438            advertising: AtomicBool::new(false),
439            address,
440            discovery_callback: Mutex::new(None),
441            connection_callback: Mutex::new(None),
442            connections: RwLock::new(HashMap::new()),
443            events: Mutex::new(Vec::new()),
444        }
445    }
446
447    /// Get recorded events for test assertions
448    pub fn events(&self) -> Vec<MockEvent> {
449        self.events.lock().unwrap().clone()
450    }
451
452    /// Clear recorded events
453    pub fn clear_events(&self) {
454        self.events.lock().unwrap().clear();
455    }
456
457    /// Record an event
458    fn record_event(&self, event: MockEvent) {
459        self.events.lock().unwrap().push(event);
460    }
461
462    /// Trigger discovery of nearby nodes
463    ///
464    /// Call this in tests to simulate device discovery.
465    pub fn trigger_discovery(&self) {
466        let devices = self.network.discover_nodes(&self.node_id);
467        if let Some(ref callback) = *self.discovery_callback.lock().unwrap() {
468            for device in devices {
469                callback(device);
470            }
471        }
472    }
473
474    /// Simulate receiving data from a peer
475    ///
476    /// Call this in tests to inject data as if received over BLE.
477    pub fn inject_data(&self, from: &NodeId, data: Vec<u8>) {
478        if let Some(ref callback) = *self.connection_callback.lock().unwrap() {
479            callback(*from, ConnectionEvent::DataReceived { data });
480        }
481    }
482
483    /// Simulate a peer disconnecting
484    pub fn simulate_disconnect(&self, peer_id: &NodeId, reason: DisconnectReason) {
485        // Remove from our connections
486        {
487            let mut conns = self.connections.write().unwrap();
488            if let Some(conn) = conns.remove(peer_id) {
489                conn.kill();
490            }
491        }
492
493        // Disconnect from network
494        self.network.disconnect(&self.node_id, peer_id);
495
496        // Notify callback
497        if let Some(ref callback) = *self.connection_callback.lock().unwrap() {
498            callback(*peer_id, ConnectionEvent::Disconnected { reason });
499        }
500
501        self.record_event(MockEvent::Disconnected(*peer_id, reason));
502    }
503
504    /// Get the node ID
505    pub fn node_id(&self) -> &NodeId {
506        &self.node_id
507    }
508
509    /// Check if scanning
510    pub fn is_scanning(&self) -> bool {
511        self.scanning.load(Ordering::SeqCst)
512    }
513
514    /// Check if advertising
515    pub fn is_advertising(&self) -> bool {
516        self.advertising.load(Ordering::SeqCst)
517    }
518}
519
520#[async_trait]
521impl BleAdapter for MockBleAdapter {
522    async fn init(&mut self, _config: &BleConfig) -> Result<()> {
523        self.powered.store(true, Ordering::SeqCst);
524        self.record_event(MockEvent::Initialized);
525        Ok(())
526    }
527
528    async fn start(&self) -> Result<()> {
529        self.record_event(MockEvent::Started);
530        Ok(())
531    }
532
533    async fn stop(&self) -> Result<()> {
534        self.scanning.store(false, Ordering::SeqCst);
535        self.advertising.store(false, Ordering::SeqCst);
536        self.network.stop_advertising(&self.node_id);
537        self.record_event(MockEvent::Stopped);
538        Ok(())
539    }
540
541    fn is_powered(&self) -> bool {
542        self.powered.load(Ordering::SeqCst)
543    }
544
545    fn address(&self) -> Option<String> {
546        Some(self.address.clone())
547    }
548
549    async fn start_scan(&self, _config: &DiscoveryConfig) -> Result<()> {
550        self.scanning.store(true, Ordering::SeqCst);
551        self.record_event(MockEvent::ScanStarted);
552
553        // Optionally auto-discover after scan latency
554        // In real tests, call trigger_discovery() manually for more control
555
556        Ok(())
557    }
558
559    async fn stop_scan(&self) -> Result<()> {
560        self.scanning.store(false, Ordering::SeqCst);
561        self.record_event(MockEvent::ScanStopped);
562        Ok(())
563    }
564
565    async fn start_advertising(&self, _config: &DiscoveryConfig) -> Result<()> {
566        self.advertising.store(true, Ordering::SeqCst);
567        self.network
568            .start_advertising(self.node_id, &self.address, Some("PEAT"));
569        self.record_event(MockEvent::AdvertisingStarted);
570        Ok(())
571    }
572
573    async fn stop_advertising(&self) -> Result<()> {
574        self.advertising.store(false, Ordering::SeqCst);
575        self.network.stop_advertising(&self.node_id);
576        self.record_event(MockEvent::AdvertisingStopped);
577        Ok(())
578    }
579
580    fn set_discovery_callback(&mut self, callback: Option<DiscoveryCallback>) {
581        *self.discovery_callback.lock().unwrap() = callback;
582    }
583
584    async fn connect(&self, peer_id: &NodeId) -> Result<Box<dyn BleConnection>> {
585        // Check connection limit
586        if self.connections.read().unwrap().len() >= self.config.max_connections as usize {
587            return Err(BleError::ConnectionFailed(
588                "Maximum connections reached".to_string(),
589            ));
590        }
591
592        // Note: connection_latency is available in config for tests that want to
593        // add delays, but we don't block by default in the mock
594
595        // Establish connection via network
596        self.network.connect(&self.node_id, peer_id)?;
597
598        // Create connection object
599        let conn = Arc::new(MockConnection::new(
600            *peer_id,
601            self.config.max_mtu,
602            BlePhy::Le1M,
603        ));
604
605        // Store connection
606        {
607            let mut conns = self.connections.write().unwrap();
608            conns.insert(*peer_id, conn.clone());
609        }
610
611        // Notify callback
612        if let Some(ref callback) = *self.connection_callback.lock().unwrap() {
613            callback(
614                *peer_id,
615                ConnectionEvent::Connected {
616                    mtu: conn.mtu(),
617                    phy: conn.phy(),
618                },
619            );
620        }
621
622        self.record_event(MockEvent::Connected(*peer_id));
623        Ok(Box::new(conn.as_ref().clone()))
624    }
625
626    async fn disconnect(&self, peer_id: &NodeId) -> Result<()> {
627        self.simulate_disconnect(peer_id, DisconnectReason::LocalRequest);
628        Ok(())
629    }
630
631    fn get_connection(&self, peer_id: &NodeId) -> Option<Box<dyn BleConnection>> {
632        let conns = self.connections.read().unwrap();
633        conns
634            .get(peer_id)
635            .filter(|c| c.is_alive())
636            .map(|c| Box::new(c.as_ref().clone()) as Box<dyn BleConnection>)
637    }
638
639    fn peer_count(&self) -> usize {
640        self.connections
641            .read()
642            .unwrap()
643            .values()
644            .filter(|c| c.is_alive())
645            .count()
646    }
647
648    fn connected_peers(&self) -> Vec<NodeId> {
649        self.connections
650            .read()
651            .unwrap()
652            .iter()
653            .filter(|(_, c)| c.is_alive())
654            .map(|(id, _)| *id)
655            .collect()
656    }
657
658    fn set_connection_callback(&mut self, callback: Option<ConnectionCallback>) {
659        *self.connection_callback.lock().unwrap() = callback;
660    }
661
662    fn peer_link_info(&self, peer_id: &NodeId) -> Option<crate::peer::BlePeerLinkInfo> {
663        // Mock keeps a `MockConnection` per peer in `connections`. If
664        // present, surface its current alive/dead state and the most
665        // recent RSSI it tracks. Per ADR-032 §Amendment A, a known but
666        // disconnected peer returns `Some` with `Disconnected`, not
667        // `None`.
668        let conns = self.connections.read().unwrap();
669        let conn = conns.get(peer_id)?;
670        let state = if conn.is_alive() {
671            crate::peer::ConnectionState::Connected
672        } else {
673            crate::peer::ConnectionState::Disconnected
674        };
675        Some(crate::peer::BlePeerLinkInfo {
676            state,
677            last_rssi: conn.rssi(),
678        })
679    }
680
681    async fn register_gatt_service(&self) -> Result<()> {
682        self.record_event(MockEvent::GattServiceRegistered);
683        Ok(())
684    }
685
686    async fn unregister_gatt_service(&self) -> Result<()> {
687        self.record_event(MockEvent::GattServiceUnregistered);
688        Ok(())
689    }
690
691    async fn write_to_peer(
692        &self,
693        peer_id: &NodeId,
694        _char_uuid: uuid::Uuid,
695        data: &[u8],
696    ) -> Result<()> {
697        self.network
698            .send_data(&self.node_id, peer_id, data.to_vec())
699    }
700
701    fn supports_coded_phy(&self) -> bool {
702        self.config.supports_coded_phy
703    }
704
705    fn supports_extended_advertising(&self) -> bool {
706        self.config.supports_extended_advertising
707    }
708
709    fn max_mtu(&self) -> u16 {
710        self.config.max_mtu
711    }
712
713    fn max_connections(&self) -> u8 {
714        self.config.max_connections
715    }
716}
717
718#[cfg(test)]
719mod tests {
720    use super::*;
721
722    #[tokio::test]
723    async fn test_mock_adapter_init() {
724        let network = MockNetwork::new();
725        let mut adapter = MockBleAdapter::new(NodeId::new(0x111), network);
726
727        assert!(!adapter.is_powered());
728        adapter.init(&BleConfig::default()).await.unwrap();
729        assert!(adapter.is_powered());
730
731        let events = adapter.events();
732        assert!(matches!(events[0], MockEvent::Initialized));
733    }
734
735    #[tokio::test]
736    async fn test_mock_network_discovery() {
737        let network = MockNetwork::new();
738
739        let mut adapter1 = MockBleAdapter::new(NodeId::new(0x111), network.clone());
740        let mut adapter2 = MockBleAdapter::new(NodeId::new(0x222), network.clone());
741
742        adapter1.init(&BleConfig::default()).await.unwrap();
743        adapter2.init(&BleConfig::default()).await.unwrap();
744
745        // Start advertising on adapter2
746        adapter2
747            .start_advertising(&DiscoveryConfig::default())
748            .await
749            .unwrap();
750
751        // Discover from adapter1
752        let devices = network.discover_nodes(&NodeId::new(0x111));
753        assert_eq!(devices.len(), 1);
754        assert_eq!(devices[0].node_id, Some(NodeId::new(0x222)));
755    }
756
757    #[tokio::test]
758    async fn test_mock_connection() {
759        let network = MockNetwork::new();
760
761        let mut adapter1 = MockBleAdapter::new(NodeId::new(0x111), network.clone());
762        let mut adapter2 = MockBleAdapter::new(NodeId::new(0x222), network.clone());
763
764        adapter1.init(&BleConfig::default()).await.unwrap();
765        adapter2.init(&BleConfig::default()).await.unwrap();
766
767        // Adapter2 must be advertising to accept connections
768        adapter2
769            .start_advertising(&DiscoveryConfig::default())
770            .await
771            .unwrap();
772
773        // Connect adapter1 to adapter2
774        let conn = adapter1.connect(&NodeId::new(0x222)).await.unwrap();
775        assert!(conn.is_alive());
776        assert_eq!(conn.peer_id(), &NodeId::new(0x222));
777
778        // Verify connection tracking
779        assert_eq!(adapter1.peer_count(), 1);
780        assert!(adapter1.connected_peers().contains(&NodeId::new(0x222)));
781    }
782
783    #[tokio::test]
784    async fn test_mock_disconnect() {
785        let network = MockNetwork::new();
786
787        let mut adapter1 = MockBleAdapter::new(NodeId::new(0x111), network.clone());
788        let mut adapter2 = MockBleAdapter::new(NodeId::new(0x222), network.clone());
789
790        adapter1.init(&BleConfig::default()).await.unwrap();
791        adapter2.init(&BleConfig::default()).await.unwrap();
792        adapter2
793            .start_advertising(&DiscoveryConfig::default())
794            .await
795            .unwrap();
796
797        let conn = adapter1.connect(&NodeId::new(0x222)).await.unwrap();
798        assert!(conn.is_alive());
799
800        // Disconnect
801        adapter1.disconnect(&NodeId::new(0x222)).await.unwrap();
802        assert_eq!(adapter1.peer_count(), 0);
803    }
804
805    #[tokio::test]
806    async fn test_connection_limit() {
807        let network = MockNetwork::new();
808
809        let config = MockAdapterConfig {
810            max_connections: 2,
811            ..Default::default()
812        };
813        let mut adapter1 = MockBleAdapter::with_config(NodeId::new(0x111), network.clone(), config);
814        adapter1.init(&BleConfig::default()).await.unwrap();
815
816        // Create 3 other adapters
817        for i in 2..=4 {
818            let mut other = MockBleAdapter::new(NodeId::new(i * 0x111), network.clone());
819            other.init(&BleConfig::default()).await.unwrap();
820            other
821                .start_advertising(&DiscoveryConfig::default())
822                .await
823                .unwrap();
824        }
825
826        // First two connections should succeed
827        adapter1.connect(&NodeId::new(0x222)).await.unwrap();
828        adapter1.connect(&NodeId::new(0x333)).await.unwrap();
829
830        // Third should fail
831        let result = adapter1.connect(&NodeId::new(0x444)).await;
832        assert!(result.is_err());
833    }
834
835    #[tokio::test]
836    async fn test_event_tracking() {
837        let network = MockNetwork::new();
838        let mut adapter = MockBleAdapter::new(NodeId::new(0x111), network.clone());
839
840        adapter.init(&BleConfig::default()).await.unwrap();
841        adapter.start().await.unwrap();
842        adapter
843            .start_scan(&DiscoveryConfig::default())
844            .await
845            .unwrap();
846        adapter.stop_scan().await.unwrap();
847        adapter
848            .start_advertising(&DiscoveryConfig::default())
849            .await
850            .unwrap();
851        adapter.stop_advertising().await.unwrap();
852        adapter.stop().await.unwrap();
853
854        let events = adapter.events();
855        assert!(matches!(events[0], MockEvent::Initialized));
856        assert!(matches!(events[1], MockEvent::Started));
857        assert!(matches!(events[2], MockEvent::ScanStarted));
858        assert!(matches!(events[3], MockEvent::ScanStopped));
859        assert!(matches!(events[4], MockEvent::AdvertisingStarted));
860        assert!(matches!(events[5], MockEvent::AdvertisingStopped));
861        assert!(matches!(events[6], MockEvent::Stopped));
862    }
863
864    #[tokio::test]
865    async fn test_write_to_peer() {
866        let network = MockNetwork::new();
867
868        let mut adapter1 = MockBleAdapter::new(NodeId::new(0x111), network.clone());
869        let mut adapter2 = MockBleAdapter::new(NodeId::new(0x222), network.clone());
870
871        adapter1.init(&BleConfig::default()).await.unwrap();
872        adapter2.init(&BleConfig::default()).await.unwrap();
873        adapter2
874            .start_advertising(&DiscoveryConfig::default())
875            .await
876            .unwrap();
877
878        // Connect first
879        adapter1.connect(&NodeId::new(0x222)).await.unwrap();
880
881        // Write data via write_to_peer
882        let char_uuid = uuid::Uuid::from_fields(
883            0x0003,
884            0x0000,
885            0x1000,
886            &[0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB],
887        );
888        let result = adapter1
889            .write_to_peer(&NodeId::new(0x222), char_uuid, &[1, 2, 3, 4])
890            .await;
891        assert!(result.is_ok());
892
893        // Verify data arrived
894        let packets = network.receive_data(&NodeId::new(0x222));
895        assert_eq!(packets.len(), 1);
896        assert_eq!(packets[0].data, vec![1, 2, 3, 4]);
897    }
898
899    #[tokio::test]
900    async fn test_write_to_peer_no_connection() {
901        let network = MockNetwork::new();
902        let mut adapter = MockBleAdapter::new(NodeId::new(0x111), network);
903        adapter.init(&BleConfig::default()).await.unwrap();
904
905        let char_uuid = uuid::Uuid::from_fields(
906            0x0003,
907            0x0000,
908            0x1000,
909            &[0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB],
910        );
911        let result = adapter
912            .write_to_peer(&NodeId::new(0x999), char_uuid, &[1, 2, 3])
913            .await;
914        assert!(result.is_err());
915    }
916
917    #[tokio::test]
918    async fn test_data_injection() {
919        let network = MockNetwork::new();
920        let mut adapter = MockBleAdapter::new(NodeId::new(0x111), network.clone());
921        adapter.init(&BleConfig::default()).await.unwrap();
922
923        // Track received data
924        let received = Arc::new(Mutex::new(Vec::new()));
925        let received_clone = received.clone();
926
927        adapter.set_connection_callback(Some(Arc::new(move |node_id, event| {
928            if let ConnectionEvent::DataReceived { data } = event {
929                received_clone.lock().unwrap().push((node_id, data));
930            }
931        })));
932
933        // Inject data
934        adapter.inject_data(&NodeId::new(0x222), vec![1, 2, 3, 4]);
935
936        let data = received.lock().unwrap();
937        assert_eq!(data.len(), 1);
938        assert_eq!(data[0].0, NodeId::new(0x222));
939        assert_eq!(data[0].1, vec![1, 2, 3, 4]);
940    }
941
942    // =========================================================================
943    // ADR-032 §Amendment A — peer_link_info accessor
944    // =========================================================================
945
946    #[tokio::test]
947    async fn peer_link_info_unknown_peer_returns_none() {
948        // Pin the docstring contract: "no record of this peer" returns
949        // None, not Some(default).
950        let network = MockNetwork::new();
951        let mut adapter = MockBleAdapter::new(NodeId::new(0x111), network);
952        adapter.init(&BleConfig::default()).await.unwrap();
953
954        assert!(adapter.peer_link_info(&NodeId::new(0x999)).is_none());
955    }
956
957    #[tokio::test]
958    async fn peer_link_info_connected_peer_reports_state_and_rssi() {
959        let network = MockNetwork::new();
960        let mut a = MockBleAdapter::new(NodeId::new(0x111), network.clone());
961        let mut b = MockBleAdapter::new(NodeId::new(0x222), network.clone());
962        a.init(&BleConfig::default()).await.unwrap();
963        b.init(&BleConfig::default()).await.unwrap();
964        b.start_advertising(&DiscoveryConfig::default())
965            .await
966            .unwrap();
967        let _conn = a.connect(&NodeId::new(0x222)).await.unwrap();
968
969        let info = a
970            .peer_link_info(&NodeId::new(0x222))
971            .expect("connected peer should have link info");
972        assert_eq!(info.state, crate::peer::ConnectionState::Connected);
973        // Mock connection seeds RSSI to -50 by default.
974        assert_eq!(info.last_rssi, Some(-50));
975    }
976
977    #[tokio::test]
978    async fn peer_link_info_after_mock_disconnect_returns_none() {
979        // Mock-specific behavior: `MockBleAdapter::simulate_disconnect`
980        // removes the peer from its `connections` map entirely, so
981        // `peer_link_info` returns `None` after disconnect.
982        //
983        // This is a property of the mock's simplified bookkeeping, not
984        // a violation of ADR-032 §Amendment A. The ADR's contract —
985        // "disconnected but known" returns `Some` — applies to adapters
986        // that retain a per-peer record across disconnects (the
987        // production Linux/Android/Apple adapters do, via the per-peer
988        // `PeerConnectionState` record on the `ConnectionStateGraph`
989        // peat-btle's `PeatMesh` facade holds). The
990        // mock's role here is testing the trait + transport pass-through,
991        // not modeling the full lifecycle. Adapters that DO retain
992        // disconnected-but-known records get their own per-platform
993        // tests once their `peer_link_info` is implemented.
994        let network = MockNetwork::new();
995        let mut a = MockBleAdapter::new(NodeId::new(0x111), network.clone());
996        let mut b = MockBleAdapter::new(NodeId::new(0x222), network.clone());
997        a.init(&BleConfig::default()).await.unwrap();
998        b.init(&BleConfig::default()).await.unwrap();
999        b.start_advertising(&DiscoveryConfig::default())
1000            .await
1001            .unwrap();
1002
1003        let conn = a.connect(&NodeId::new(0x222)).await.unwrap();
1004        assert!(conn.is_alive());
1005        a.disconnect(&NodeId::new(0x222)).await.unwrap();
1006
1007        assert!(a.peer_link_info(&NodeId::new(0x222)).is_none());
1008    }
1009
1010    #[tokio::test]
1011    async fn peer_link_info_reflects_rssi_updates() {
1012        // Pin that rssi() flows through into the surfaced info — the
1013        // visualization layer's quality bucket is downstream of this.
1014        let network = MockNetwork::new();
1015        let mut a = MockBleAdapter::new(NodeId::new(0x111), network.clone());
1016        let mut b = MockBleAdapter::new(NodeId::new(0x222), network.clone());
1017        a.init(&BleConfig::default()).await.unwrap();
1018        b.init(&BleConfig::default()).await.unwrap();
1019        b.start_advertising(&DiscoveryConfig::default())
1020            .await
1021            .unwrap();
1022        let _conn = a.connect(&NodeId::new(0x222)).await.unwrap();
1023
1024        // Reach into the underlying MockConnection and update its RSSI
1025        // (mirrors what an adapter does on advertisement reception).
1026        {
1027            let conns = a.connections.read().unwrap();
1028            let mc = conns.get(&NodeId::new(0x222)).unwrap();
1029            mc.set_rssi(-72);
1030        }
1031
1032        let info = a.peer_link_info(&NodeId::new(0x222)).unwrap();
1033        assert_eq!(info.last_rssi, Some(-72));
1034    }
1035
1036    #[tokio::test]
1037    async fn bluetooth_le_transport_delegates_peer_link_info_to_adapter() {
1038        // BluetoothLETransport's pass-through is a one-line delegation;
1039        // the test pins that the wrapper actually calls through, so a
1040        // future refactor that, e.g., caches state at the transport
1041        // layer doesn't silently drop adapter-side updates.
1042        use crate::transport::BluetoothLETransport;
1043
1044        let network = MockNetwork::new();
1045        let mut a = MockBleAdapter::new(NodeId::new(0x111), network.clone());
1046        let mut b = MockBleAdapter::new(NodeId::new(0x222), network.clone());
1047        a.init(&BleConfig::default()).await.unwrap();
1048        b.init(&BleConfig::default()).await.unwrap();
1049        b.start_advertising(&DiscoveryConfig::default())
1050            .await
1051            .unwrap();
1052        let _conn = a.connect(&NodeId::new(0x222)).await.unwrap();
1053
1054        let mut config = BleConfig::default();
1055        config.node_id = NodeId::new(0x111);
1056        let transport = BluetoothLETransport::new(config, a);
1057
1058        let info = transport
1059            .peer_link_info(&NodeId::new(0x222))
1060            .expect("transport pass-through should surface adapter info");
1061        assert_eq!(info.state, crate::peer::ConnectionState::Connected);
1062        assert_eq!(info.last_rssi, Some(-50));
1063        assert!(transport.peer_link_info(&NodeId::new(0x999)).is_none());
1064    }
1065}