use peat_btle::config::{BleConfig, DiscoveryConfig};
use peat_btle::document::PeatDocument;
use peat_btle::gossip::{GossipStrategy, RandomFanout};
use peat_btle::peat_mesh::{PeatMesh, PeatMeshConfig};
use peat_btle::platform::mock::{MockBleAdapter, MockNetwork};
use peat_btle::platform::BleAdapter;
use peat_btle::sync::delta_document::DeltaDocument;
use peat_btle::NodeId;
async fn create_test_node(
node_id: u32,
callsign: &str,
network: MockNetwork,
) -> (MockBleAdapter, PeatMesh) {
let node = NodeId::new(node_id);
let mut adapter = MockBleAdapter::new(node, network);
adapter.init(&BleConfig::default()).await.unwrap();
adapter
.start_advertising(&DiscoveryConfig::default())
.await
.unwrap();
let config = PeatMeshConfig::new(node, callsign, "TEST");
let mesh = PeatMesh::new(config);
(adapter, mesh)
}
#[tokio::test]
async fn test_two_node_discovery() {
let network = MockNetwork::new();
let (adapter1, _mesh1) = create_test_node(0x111, "ALPHA-1", network.clone()).await;
let (_adapter2, _mesh2) = create_test_node(0x222, "BRAVO-1", network.clone()).await;
let devices = network.discover_nodes(&NodeId::new(0x111));
assert_eq!(devices.len(), 1);
assert_eq!(devices[0].node_id, Some(NodeId::new(0x222)));
assert!(adapter1.is_advertising());
}
#[tokio::test]
async fn test_two_node_connection() {
let network = MockNetwork::new();
let (adapter1, _mesh1) = create_test_node(0x111, "ALPHA-1", network.clone()).await;
let (_adapter2, _mesh2) = create_test_node(0x222, "BRAVO-1", network.clone()).await;
let conn = adapter1.connect(&NodeId::new(0x222)).await.unwrap();
assert!(conn.is_alive());
assert_eq!(adapter1.peer_count(), 1);
assert!(network.is_connected(&NodeId::new(0x111), &NodeId::new(0x222)));
}
#[tokio::test]
async fn test_document_sync_basic() {
let network = MockNetwork::new();
let (adapter1, mesh1) = create_test_node(0x111, "ALPHA-1", network.clone()).await;
let (_adapter2, mesh2) = create_test_node(0x222, "BRAVO-1", network.clone()).await;
adapter1.connect(&NodeId::new(0x222)).await.unwrap();
let now_ms = 1000u64;
let sync_data = mesh1.tick(now_ms);
if let Some(data) = sync_data {
let result = mesh2.on_ble_data_received("device-111", &data, now_ms + 100);
assert!(result.is_some() || result.is_none()); }
assert_eq!(mesh1.node_id().as_u32(), 0x111);
assert_eq!(mesh2.node_id().as_u32(), 0x222);
}
#[tokio::test]
async fn test_counter_increment_and_sync() {
let mut doc1 = PeatDocument::new(NodeId::new(0x111));
let mut doc2 = PeatDocument::new(NodeId::new(0x222));
doc1.increment_counter();
doc1.increment_counter();
doc2.increment_counter();
assert_eq!(doc1.total_count(), 2);
assert_eq!(doc2.total_count(), 1);
let changed = doc1.merge(&doc2);
assert!(changed);
assert_eq!(doc1.total_count(), 3);
let changed = doc2.merge(&doc1);
assert!(changed);
assert_eq!(doc2.total_count(), 3); }
#[tokio::test]
async fn test_gossip_fanout_selection() {
let network = MockNetwork::new();
let mut adapters = Vec::new();
for i in 1..=5 {
let (adapter, _mesh) =
create_test_node(i * 0x111, &format!("NODE-{}", i), network.clone()).await;
adapters.push(adapter);
}
for i in 2..=5 {
adapters[0].connect(&NodeId::new(i * 0x111)).await.unwrap();
}
assert_eq!(adapters[0].peer_count(), 4);
let strategy = RandomFanout::new(2);
let peers = adapters[0].connected_peers();
let peat_peers: Vec<_> = peers
.iter()
.map(|id| peat_btle::peer::PeatPeer {
node_id: *id,
identifier: format!("device-{}", id.as_u32()),
mesh_id: Some("TEST".to_string()),
name: None,
rssi: -60,
is_connected: true,
last_seen_ms: 0,
})
.collect();
let selected = strategy.select_peers(&peat_peers);
assert_eq!(selected.len(), 2); }
#[tokio::test]
async fn test_multi_hop_document_propagation() {
let mut doc_a = PeatDocument::new(NodeId::new(0xAAA));
let mut doc_b = PeatDocument::new(NodeId::new(0xBBB));
let mut doc_c = PeatDocument::new(NodeId::new(0xCCC));
doc_a.increment_counter();
assert_eq!(doc_a.total_count(), 1);
let data_from_a = doc_a.encode();
let decoded = PeatDocument::decode(&data_from_a).unwrap();
let b_changed = doc_b.merge(&decoded);
assert!(b_changed);
assert_eq!(doc_b.total_count(), 1);
doc_b.increment_counter();
assert_eq!(doc_b.total_count(), 2);
let data_from_b = doc_b.encode();
let decoded = PeatDocument::decode(&data_from_b).unwrap();
let c_changed = doc_c.merge(&decoded);
assert!(c_changed);
assert_eq!(doc_c.total_count(), 2);
doc_c.increment_counter();
assert_eq!(doc_c.total_count(), 3);
let data_from_c = doc_c.encode();
let decoded = PeatDocument::decode(&data_from_c).unwrap();
doc_a.merge(&decoded);
doc_b.merge(&decoded);
assert_eq!(doc_a.total_count(), 3);
assert_eq!(doc_b.total_count(), 3);
assert_eq!(doc_c.total_count(), 3);
}
#[tokio::test]
async fn test_concurrent_increments_convergence() {
let mut doc1 = PeatDocument::new(NodeId::new(0x111));
let mut doc2 = PeatDocument::new(NodeId::new(0x222));
let mut doc3 = PeatDocument::new(NodeId::new(0x333));
doc1.increment_counter();
doc1.increment_counter();
doc2.increment_counter();
doc2.increment_counter();
doc2.increment_counter();
doc3.increment_counter();
assert_eq!(doc1.total_count(), 2);
assert_eq!(doc2.total_count(), 3);
assert_eq!(doc3.total_count(), 1);
let data1 = doc1.encode();
let data2 = doc2.encode();
let data3 = doc3.encode();
doc2.merge(&PeatDocument::decode(&data1).unwrap());
doc3.merge(&PeatDocument::decode(&data2).unwrap());
doc1.merge(&PeatDocument::decode(&data3).unwrap());
let data1 = doc1.encode();
let data2 = doc2.encode();
let data3 = doc3.encode();
doc2.merge(&PeatDocument::decode(&data1).unwrap());
doc3.merge(&PeatDocument::decode(&data2).unwrap());
doc1.merge(&PeatDocument::decode(&data3).unwrap());
assert_eq!(doc1.total_count(), 6);
assert_eq!(doc2.total_count(), 6);
assert_eq!(doc3.total_count(), 6);
}
#[tokio::test]
async fn test_delta_document_build_full() {
let config = PeatMeshConfig::new(NodeId::new(0x111), "ALPHA-1", "TEST");
let mesh = PeatMesh::new(config);
let now_ms = 1000u64;
let data = mesh.build_full_delta_document(now_ms);
assert!(DeltaDocument::is_delta_document(&data));
let delta = DeltaDocument::decode(&data).unwrap();
assert_eq!(delta.origin_node.as_u32(), 0x111);
assert_eq!(delta.timestamp_ms, now_ms);
assert!(!delta.operations.is_empty());
}
#[tokio::test]
async fn test_delta_document_for_peer_first_sync() {
let config = PeatMeshConfig::new(NodeId::new(0x111), "ALPHA-1", "TEST");
let mesh = PeatMesh::new(config);
let peer_id = NodeId::new(0x222);
mesh.register_peer_for_delta(&peer_id);
let now_ms = 1000u64;
let data = mesh.build_delta_document_for_peer(&peer_id, now_ms);
assert!(data.is_some());
let data = data.unwrap();
assert!(DeltaDocument::is_delta_document(&data));
}
#[tokio::test]
async fn test_delta_document_for_peer_no_changes() {
let config = PeatMeshConfig::new(NodeId::new(0x111), "ALPHA-1", "TEST");
let mesh = PeatMesh::new(config);
let peer_id = NodeId::new(0x222);
mesh.register_peer_for_delta(&peer_id);
let now_ms = 1000u64;
let _first = mesh.build_delta_document_for_peer(&peer_id, now_ms);
let second = mesh.build_delta_document_for_peer(&peer_id, now_ms + 100);
assert!(
second.is_none(),
"Should not send delta when nothing changed"
);
}
#[tokio::test]
async fn test_delta_document_receive_basic() {
let network = MockNetwork::new();
let (adapter1, mesh1) = create_test_node(0x111, "ALPHA-1", network.clone()).await;
let (_adapter2, mesh2) = create_test_node(0x222, "BRAVO-1", network.clone()).await;
adapter1.connect(&NodeId::new(0x222)).await.unwrap();
mesh2.on_ble_discovered(
"device-111",
Some("PEAT_TEST-00000111"),
-60,
Some("TEST"),
1000,
);
mesh2.on_ble_connected("device-111", 1000);
let now_ms = 1000u64;
let data = mesh1.build_full_delta_document(now_ms);
let result = mesh2.on_ble_data_received("device-111", &data, now_ms + 100);
assert!(result.is_some(), "Should process delta document");
}
#[tokio::test]
async fn test_delta_sync_round_trip() {
let network = MockNetwork::new();
let (adapter1, mesh1) = create_test_node(0x111, "ALPHA-1", network.clone()).await;
let (_adapter2, mesh2) = create_test_node(0x222, "BRAVO-1", network.clone()).await;
adapter1.connect(&NodeId::new(0x222)).await.unwrap();
let peer1_id = NodeId::new(0x111);
let peer2_id = NodeId::new(0x222);
mesh1.register_peer_for_delta(&peer2_id);
mesh2.register_peer_for_delta(&peer1_id);
mesh2.on_ble_discovered(
"device-111",
Some("PEAT_TEST-00000111"),
-60,
Some("TEST"),
1000,
);
mesh2.on_ble_connected("device-111", 1000);
let now_ms = 1000u64;
let data1 = mesh1.build_delta_document_for_peer(&peer2_id, now_ms);
assert!(data1.is_some(), "First sync should produce data");
let result1 = mesh2.on_ble_data_received("device-111", &data1.unwrap(), now_ms + 50);
assert!(result1.is_some(), "mesh2 should process delta from mesh1");
let data2 = mesh1.build_delta_document_for_peer(&peer2_id, now_ms + 100);
assert!(
data2.is_none(),
"Second sync without changes should be None"
);
}
#[tokio::test]
async fn test_delta_stats_tracking() {
let config = PeatMeshConfig::new(NodeId::new(0x111), "ALPHA-1", "TEST");
let mesh = PeatMesh::new(config);
let peer_id = NodeId::new(0x222);
mesh.register_peer_for_delta(&peer_id);
let stats_before = mesh.peer_delta_stats(&peer_id);
assert!(stats_before.is_some());
let (sent_before, recv_before, count_before) = stats_before.unwrap();
assert_eq!(sent_before, 0);
assert_eq!(recv_before, 0);
assert_eq!(count_before, 0);
let now_ms = 1000u64;
mesh.build_delta_document_for_peer(&peer_id, now_ms);
let stats_after = mesh.peer_delta_stats(&peer_id);
assert!(stats_after.is_some());
let (sent_after, _, count_after) = stats_after.unwrap();
assert!(sent_after > 0, "Should track bytes sent");
assert_eq!(count_after, 1, "Should track sync count");
}
#[tokio::test]
async fn test_delta_peer_reset() {
let config = PeatMeshConfig::new(NodeId::new(0x111), "ALPHA-1", "TEST");
let mesh = PeatMesh::new(config);
let peer_id = NodeId::new(0x222);
mesh.register_peer_for_delta(&peer_id);
let now_ms = 1000u64;
let first = mesh.build_delta_document_for_peer(&peer_id, now_ms);
assert!(first.is_some());
let second = mesh.build_delta_document_for_peer(&peer_id, now_ms + 100);
assert!(second.is_none());
mesh.reset_peer_delta_state(&peer_id);
let third = mesh.build_delta_document_for_peer(&peer_id, now_ms + 200);
assert!(third.is_some(), "After reset should send full state");
}
const TEST_SECRET: [u8; 32] = [
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10,
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
];
#[tokio::test]
async fn test_encrypted_document_includes_location() {
let sender_config =
PeatMeshConfig::new(NodeId::new(0x111), "SENDER", "TEST").with_encryption(TEST_SECRET);
let sender = PeatMesh::new(sender_config);
let receiver_config =
PeatMeshConfig::new(NodeId::new(0x222), "RECEIVER", "TEST").with_encryption(TEST_SECRET);
let receiver = PeatMesh::new(receiver_config);
sender.update_location(37.7749, -122.4194, Some(10.0));
sender.update_callsign("ALPHA-1");
let doc_bytes = sender.build_document();
assert!(!doc_bytes.is_empty(), "Document should not be empty");
assert_eq!(doc_bytes[0], 0xAE, "Document should be encrypted");
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
let result = receiver.on_ble_data("device-sender", &doc_bytes, now_ms);
assert!(
result.is_some(),
"Receiver should decrypt and process document"
);
let receiver_doc = receiver.build_document();
assert!(
!receiver_doc.is_empty(),
"Receiver document should contain merged state"
);
println!("Encrypted document size: {} bytes", doc_bytes.len());
println!("Receiver processed document successfully with location data");
}
#[cfg(feature = "legacy-chat")]
#[tokio::test]
async fn test_chat_sync_unencrypted() {
let sender_config = PeatMeshConfig::new(NodeId::new(0x111), "SENDER", "TEST");
let sender = PeatMesh::new(sender_config);
let receiver_config = PeatMeshConfig::new(NodeId::new(0x222), "RECEIVER", "TEST");
let receiver = PeatMesh::new(receiver_config);
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
let msg_doc = sender.send_chat("ALPHA", "Hello from sender!", now_ms);
assert!(msg_doc.is_some(), "Sender should add chat message");
let sender_chat_count = sender.chat_count();
assert_eq!(sender_chat_count, 1, "Sender should have 1 chat message");
let doc_bytes = sender.build_document();
println!("Unencrypted document size: {} bytes", doc_bytes.len());
assert_ne!(doc_bytes[0], 0xAE, "Document should NOT be encrypted");
let decoded = PeatDocument::decode(&doc_bytes);
assert!(decoded.is_some(), "Document should decode");
let doc = decoded.unwrap();
assert!(doc.chat.is_some(), "Document should have chat CRDT");
let chat = doc.chat.unwrap();
assert!(!chat.is_empty(), "Chat CRDT should have messages");
assert_eq!(chat.len(), 1, "Chat CRDT should have 1 message");
receiver.on_ble_discovered(
"device-sender",
Some("PEAT_TEST-00000111"),
-60,
Some("TEST"),
now_ms,
);
receiver.on_ble_connected("device-sender", now_ms);
let result = receiver.on_ble_data_received("device-sender", &doc_bytes, now_ms + 100);
assert!(result.is_some(), "Receiver should process document");
let receiver_chat_count = receiver.chat_count();
assert_eq!(
receiver_chat_count, 1,
"Receiver should have 1 chat message after sync"
);
println!("Chat sync (unencrypted) works correctly!");
}
#[cfg(feature = "legacy-chat")]
#[tokio::test]
async fn test_chat_sync_encrypted() {
let sender_config =
PeatMeshConfig::new(NodeId::new(0x111), "SENDER", "TEST").with_encryption(TEST_SECRET);
let sender = PeatMesh::new(sender_config);
let receiver_config =
PeatMeshConfig::new(NodeId::new(0x222), "RECEIVER", "TEST").with_encryption(TEST_SECRET);
let receiver = PeatMesh::new(receiver_config);
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
let msg_doc = sender.send_chat("ALPHA", "Hello from encrypted sender!", now_ms);
assert!(msg_doc.is_some(), "Sender should add chat message");
let sender_chat_count = sender.chat_count();
assert_eq!(sender_chat_count, 1, "Sender should have 1 chat message");
let doc_bytes = sender.build_document();
println!("Encrypted document size: {} bytes", doc_bytes.len());
assert_eq!(doc_bytes[0], 0xAE, "Document should be encrypted");
receiver.on_ble_discovered(
"device-sender",
Some("PEAT_TEST-00000111"),
-60,
Some("TEST"),
now_ms,
);
receiver.on_ble_connected("device-sender", now_ms);
let result = receiver.on_ble_data_received("device-sender", &doc_bytes, now_ms + 100);
assert!(
result.is_some(),
"Receiver should decrypt and process document"
);
let receiver_chat_count = receiver.chat_count();
assert_eq!(
receiver_chat_count, 1,
"Receiver should have 1 chat message after encrypted sync"
);
let messages = receiver.all_chat_messages();
assert_eq!(messages.len(), 1, "Should have exactly 1 message");
let (origin, _timestamp, sender_name, text, _reply_node, _reply_ts) = &messages[0];
assert_eq!(*origin, 0x111, "Message origin should be sender's node ID");
assert_eq!(sender_name, "ALPHA", "Sender name should be preserved");
assert_eq!(
text, "Hello from encrypted sender!",
"Message text should be preserved"
);
println!("Chat sync (encrypted) works correctly!");
}