Skip to main content

offline_protocol_transport/
internet.rs

1//! Internet transport (TCP/WebSocket relay) queue engine.
2//!
3//! No sockets are opened here. The platform side owns the actual TCP or
4//! WebSocket connection to the relay server; the Rust side manages the
5//! send/receive queues, metrics, heartbeat bookkeeping, and the
6//! confirmation loop.
7//!
8//! The bridge contract: the platform reports connectivity via
9//! [`InternetTransport::on_status_changed`] (nothing else makes this
10//! transport `Available`), polls [`InternetTransport::get_next_message`]
11//! for outbound wire bytes — unlike the other transports there is no
12//! messages-available callback — reports outcomes via
13//! [`InternetTransport::confirm_sent`] /
14//! [`InternetTransport::report_send_failure`], and injects inbound bytes
15//! via [`InternetTransport::on_data_received`].
16
17use crate::constants::{
18    INTERNET_DEFAULT_SERVER_ADDRESS, INTERNET_PENDING_CONFIRMATION_TIMEOUT_SECS,
19};
20use crate::{Result, Transport, TransportMetrics, TransportStatus, TransportType};
21use offline_protocol_core::{Message, MutexExt};
22use std::collections::{HashMap, VecDeque};
23use std::sync::{Arc, Mutex};
24use std::time::{Duration, Instant};
25
26use crate::common::recalculate_delivery_ratios;
27
28/// Internet transport configuration.
29///
30/// Connection lifecycle (timeouts, reconnection, keepalive) is owned by the
31/// platform bridge that manages the actual socket — this config carries only
32/// what the Rust side itself consumes.
33#[derive(Debug, Clone)]
34pub struct InternetConfig {
35    /// Server address (WebSocket URL or TCP address)
36    pub server_address: String,
37}
38
39impl Default for InternetConfig {
40    fn default() -> Self {
41        Self {
42            server_address: INTERNET_DEFAULT_SERVER_ADDRESS.to_string(),
43        }
44    }
45}
46
47/// Internet transport implementation.
48///
49/// Queue engine for a platform-managed TCP/WebSocket connection to a
50/// central relay server. Useful for hybrid online/offline scenarios.
51///
52/// ## Lock ordering
53///
54/// When acquiring more than one lock in a single scope, follow this order to
55/// prevent deadlocks:
56///
57/// 1. `status`
58/// 2. `pending_confirmation`
59/// 3. `send_queue`
60/// 4. `metrics`
61/// 5. `receive_queue`
62///
63/// Not every method needs all of these — the ordering only matters when
64/// multiple locks are held simultaneously.
65pub struct InternetTransport {
66    /// Local device ID
67    device_id: String,
68    /// Configuration
69    config: InternetConfig,
70    /// Transport status
71    status: Arc<Mutex<TransportStatus>>,
72    /// Received message queue
73    receive_queue: Arc<Mutex<VecDeque<Message>>>,
74    /// Send queue — messages waiting to be polled by the platform via `get_next_message()`
75    send_queue: Arc<Mutex<VecDeque<Message>>>,
76    /// Messages dequeued by the platform but not yet confirmed as sent.
77    /// Key: message_id string, Value: when the message was dequeued.
78    ///
79    /// Bounded by: (a) `drain_expired_pending()` which runs on every
80    /// `get_next_message()` poll, and (b) `fail_all_pending()` which runs
81    /// on transport disconnect. If the platform stops polling *and* the
82    /// transport status stays `Available`, entries accumulate up to the
83    /// number of messages enqueued during that window. Under normal
84    /// operation (platform polls at ≥1 Hz) the map stays small.
85    pending_confirmation: Arc<Mutex<HashMap<String, Instant>>>,
86    /// Transport metrics
87    metrics: Arc<Mutex<TransportMetrics>>,
88}
89
90impl InternetTransport {
91    /// Creates a new internet transport.
92    pub fn new(device_id: impl Into<String>) -> Self {
93        Self::with_config(device_id, InternetConfig::default())
94    }
95
96    /// Creates a new internet transport with custom configuration.
97    pub fn with_config(device_id: impl Into<String>, config: InternetConfig) -> Self {
98        Self {
99            device_id: device_id.into(),
100            config,
101            status: Arc::new(Mutex::new(TransportStatus::Unavailable)),
102            receive_queue: Arc::new(Mutex::new(VecDeque::new())),
103            send_queue: Arc::new(Mutex::new(VecDeque::new())),
104            pending_confirmation: Arc::new(Mutex::new(HashMap::new())),
105            metrics: Arc::new(Mutex::new(TransportMetrics::default())),
106        }
107    }
108
109    /// Gets the local device ID.
110    pub fn device_id(&self) -> &str {
111        &self.device_id
112    }
113
114    /// Gets the configuration.
115    pub fn config(&self) -> &InternetConfig {
116        &self.config
117    }
118
119    /// Called when a message is received from the server.
120    pub fn on_message_received(&self, message: Message) {
121        crate::common::on_message_received(&self.receive_queue, message);
122    }
123
124    /// Like [`on_message_received`](Self::on_message_received), but attaches a
125    /// transport-verified `peer_id` to the message.
126    ///
127    /// # Parameters
128    ///
129    /// * `peer_id` — The **user-level identifier** (i.e., the remote peer's
130    ///   `UserId` string) that the relay server has authenticated for this
131    ///   connection (e.g. via WebSocket session token). This is **not** the raw
132    ///   transport address (IP, session token, etc.). The protocol layer
133    ///   strict-matches this value against `message.sender` for control
134    ///   frames claiming direct origin (`hop_count == 0`); mesh-relayed
135    ///   frames (`hop_count > 0`) are exempt because there the authenticated
136    ///   uploader is the carrier, not the origin.
137    pub fn on_message_received_from(&self, message: Message, peer_id: String) {
138        crate::common::on_message_received_from(&self.receive_queue, message, peer_id);
139    }
140
141    /// Serializes a message to JSON bytes.
142    pub fn serialize_message(&self, message: &Message) -> Result<Vec<u8>> {
143        crate::common::serialize_message(message)
144    }
145
146    /// Drains messages that have been pending confirmation longer than the timeout,
147    /// recording each as a failure.
148    fn drain_expired_pending(&self) {
149        let timeout = Duration::from_secs(INTERNET_PENDING_CONFIRMATION_TIMEOUT_SECS);
150        let now = Instant::now();
151        let mut expired_count: u32 = 0;
152
153        {
154            let mut pending = self.pending_confirmation.lock_or_recover();
155            pending.retain(|_id, enqueued_at| {
156                if now.duration_since(*enqueued_at) >= timeout {
157                    expired_count += 1;
158                    false
159                } else {
160                    true
161                }
162            });
163        }
164
165        if expired_count > 0 {
166            tracing::warn!(
167                expired_count = expired_count,
168                "Pending confirmations expired, recorded as failures"
169            );
170            self.record_bulk_failures(expired_count);
171        }
172    }
173
174    /// Fails all pending confirmations immediately.
175    ///
176    /// Called when the transport disconnects or the transport is stopped so
177    /// that metrics reflect the loss right away rather than waiting for the
178    /// per-message expiry timeout.
179    fn fail_all_pending(&self) {
180        let count = {
181            let mut pending = self.pending_confirmation.lock_or_recover();
182            let count = u32::try_from(pending.len()).unwrap_or(u32::MAX);
183            pending.clear();
184            count
185        };
186
187        if count > 0 {
188            tracing::warn!(
189                count = count,
190                "Failing all pending confirmations due to transport disconnect"
191            );
192            self.record_bulk_failures(count);
193        }
194    }
195
196    /// Records `count` delivery failures in transport metrics.
197    fn record_bulk_failures(&self, count: u32) {
198        let mut metrics = self.metrics.lock_or_recover();
199        metrics.failure_count = metrics.failure_count.saturating_add(count);
200        recalculate_delivery_ratios(&mut metrics);
201    }
202
203    /// Returns the count of messages currently awaiting confirmation from the platform.
204    pub fn pending_confirmation_count(&self) -> usize {
205        self.pending_confirmation.lock_or_recover().len()
206    }
207
208    /// Updates transport metrics while preserving confirmation-loop delivery counts.
209    ///
210    /// The confirmation loop (`confirm_sent` / `report_send_failure`) owns
211    /// `success_count`, `failure_count`, `delivery_ratio`, and `drop_rate`.
212    /// This method only copies externally-managed fields from `incoming`,
213    /// so adding a new confirmation-loop field doesn't require updating this
214    /// function — it's preserved by default.
215    pub fn update_metrics(&self, incoming: TransportMetrics) {
216        let mut current = self.metrics.lock_or_recover();
217        current.rssi = incoming.rssi;
218        current.latency_ms = incoming.latency_ms;
219        current.bandwidth_bps = incoming.bandwidth_bps;
220        current.congestion = incoming.congestion;
221        current.queue_depth = incoming.queue_depth;
222        current.battery_level = incoming.battery_level;
223        current.is_charging = incoming.is_charging;
224        current.relay_connection_count = incoming.relay_connection_count;
225        current.is_active_relay = incoming.is_active_relay;
226        current.average_hop_count = incoming.average_hop_count;
227        current.energy_cost = incoming.energy_cost;
228    }
229}
230
231impl Transport for InternetTransport {
232    fn as_any(&self) -> &dyn std::any::Any {
233        self
234    }
235
236    fn transport_type(&self) -> TransportType {
237        TransportType::Internet
238    }
239
240    fn status(&self) -> TransportStatus {
241        *self.status.lock_or_recover()
242    }
243
244    fn metrics(&self) -> TransportMetrics {
245        self.metrics.lock_or_recover().clone()
246    }
247
248    fn send(&self, message: &Message) -> Result<()> {
249        // Check status
250        if self.status() != TransportStatus::Available {
251            return Err(crate::Error::TransportNotAvailable(
252                "Internet transport is not available".to_string(),
253            ));
254        }
255
256        // Add to send queue
257        let mut queue = self.send_queue.lock_or_recover();
258        queue.push_back(message.clone());
259
260        // Update metrics
261        let mut metrics = self.metrics.lock_or_recover();
262        metrics.queue_depth = queue.len();
263        metrics.congestion = ((metrics.queue_depth as f32) / 25.0).clamp(0.0, 1.0);
264
265        Ok(())
266    }
267
268    fn receive(&self) -> Result<Option<Message>> {
269        let mut queue = self.receive_queue.lock_or_recover();
270        Ok(queue.pop_front())
271    }
272
273    fn start(&self) -> Result<()> {
274        // Status will be updated by platform implementation via on_status_changed()
275        // Platform code should establish connection and call on_status_changed()
276        Ok(())
277    }
278
279    fn stop(&self) -> Result<()> {
280        self.fail_all_pending();
281        *self.status.lock_or_recover() = TransportStatus::Disconnected;
282        Ok(())
283    }
284
285    /// Called when connection status changes.
286    ///
287    /// EDGE CASE HANDLING:
288    /// - Messages in send_queue are preserved during disconnection
289    /// - They will be sent when transport becomes available again
290    fn on_status_changed(&self, status: TransportStatus) {
291        let previous_status = {
292            let mut guard = self.status.lock_or_recover();
293            let prev = *guard;
294            *guard = status;
295            prev
296        };
297
298        if status == TransportStatus::Available {
299            let queue_len = self.send_queue.lock_or_recover().len();
300
301            if queue_len > 0 {
302                tracing::info!(
303                    pending_messages = queue_len,
304                    "Internet transport available, {} messages pending in queue",
305                    queue_len
306                );
307            }
308        } else if previous_status == TransportStatus::Available
309            && status != TransportStatus::Available
310        {
311            // Fail all pending confirmations — the connection is gone so the
312            // platform can no longer report outcomes for in-flight messages.
313            self.fail_all_pending();
314
315            let queue_len = self.send_queue.lock_or_recover().len();
316            if queue_len > 0 {
317                tracing::warn!(
318                    pending_messages = queue_len,
319                    new_status = ?status,
320                    "Internet transport disconnected with {} messages in queue (will retry)",
321                    queue_len
322                );
323            }
324        }
325    }
326
327    /// Called when raw data is received (platform callback).
328    ///
329    /// This deserializes the message and queues it.
330    fn on_data_received(&self, data: Vec<u8>) -> Result<()> {
331        crate::common::on_data_received(&self.receive_queue, data)
332    }
333
334    /// Like [`Transport::on_data_received`], but attaches a
335    /// transport-verified `peer_id` to the deserialized message.
336    ///
337    /// This is the production ingest path for relay traffic: the FFI layer
338    /// routes every inbound relay frame with a non-empty authenticated
339    /// sender through here.
340    ///
341    /// # Parameters
342    ///
343    /// * `peer_id` — The **user-level identifier** (i.e., the remote peer's
344    ///   `UserId` string) that the relay server has authenticated for this
345    ///   connection. This is **not** the raw transport address (IP, session
346    ///   token, etc.). The protocol layer strict-matches this value against
347    ///   `message.sender` for control frames claiming direct origin
348    ///   (`hop_count == 0`); mesh-relayed frames (`hop_count > 0`) are
349    ///   exempt because there the authenticated uploader is the carrier,
350    ///   not the origin.
351    fn on_data_received_from(&self, data: Vec<u8>, peer_id: String) -> Result<()> {
352        crate::common::on_data_received_from(&self.receive_queue, data, peer_id)
353    }
354
355    /// Gets the next message to send (for platform implementation).
356    ///
357    /// Returns the message_id and serialized bytes, or None if no messages to send.
358    /// The message enters the pending-confirmation state until the platform calls
359    /// `confirm_sent()` or `report_send_failure()`.
360    ///
361    /// Also drains any pending confirmations that have exceeded the timeout,
362    /// recording them as failures to keep DORS metrics accurate.
363    fn get_next_message(&self) -> Result<Option<(String, Vec<u8>)>> {
364        // Expire stale pending confirmations before processing the next message.
365        // This is the only call site — it runs each time the platform polls, which
366        // is frequent enough to bound the pending map and keep metrics fresh.
367        self.drain_expired_pending();
368
369        let (message, queue_depth) = {
370            let mut queue = self.send_queue.lock_or_recover();
371            match queue.pop_front() {
372                Some(m) => {
373                    let depth = queue.len();
374                    (m, depth)
375                }
376                None => return Ok(None),
377            }
378        };
379
380        let message_id = message.id.as_str().to_string();
381
382        // Serialize before inserting into pending_confirmation so a
383        // serialization failure does not orphan an entry that the platform
384        // can never confirm or fail.
385        let data = self.serialize_message(&message)?;
386
387        {
388            let mut pending = self.pending_confirmation.lock_or_recover();
389            pending.insert(message_id.clone(), Instant::now());
390        }
391
392        {
393            let mut metrics = self.metrics.lock_or_recover();
394            metrics.queue_depth = queue_depth;
395        }
396
397        Ok(Some((message_id, data)))
398    }
399
400    /// Platform confirms that a message was successfully sent over the wire (e.g., WebSocket).
401    ///
402    /// This updates transport metrics to reflect real delivery outcomes,
403    /// enabling DORS to make accurate routing decisions.
404    fn confirm_sent(&self, message_id: &str) {
405        let was_pending = {
406            let mut pending = self.pending_confirmation.lock_or_recover();
407            pending.remove(message_id).is_some()
408        };
409
410        if was_pending {
411            let mut metrics = self.metrics.lock_or_recover();
412            metrics.success_count = metrics.success_count.saturating_add(1);
413            recalculate_delivery_ratios(&mut metrics);
414            tracing::trace!(
415                message_id = message_id,
416                success_count = metrics.success_count,
417                "confirm_sent: recorded success"
418            );
419        } else {
420            tracing::debug!(
421                message_id = message_id,
422                "confirm_sent: unknown or already-resolved message"
423            );
424        }
425    }
426
427    /// Platform reports that a message failed to send (e.g., WebSocket error).
428    fn report_send_failure(&self, message_id: &str) {
429        let was_pending = {
430            let mut pending = self.pending_confirmation.lock_or_recover();
431            pending.remove(message_id).is_some()
432        };
433
434        if was_pending {
435            let mut metrics = self.metrics.lock_or_recover();
436            metrics.failure_count = metrics.failure_count.saturating_add(1);
437            recalculate_delivery_ratios(&mut metrics);
438            tracing::trace!(
439                message_id = message_id,
440                failure_count = metrics.failure_count,
441                "report_send_failure: recorded failure"
442            );
443        } else {
444            tracing::debug!(
445                message_id = message_id,
446                "report_send_failure: unknown or already-resolved message"
447            );
448        }
449    }
450}
451
452/// Internet transport builder for configuration.
453pub struct InternetTransportBuilder {
454    device_id: String,
455    config: InternetConfig,
456}
457
458impl InternetTransportBuilder {
459    /// Creates a new builder.
460    pub fn new(device_id: impl Into<String>) -> Self {
461        Self {
462            device_id: device_id.into(),
463            config: InternetConfig::default(),
464        }
465    }
466
467    /// Sets the server address.
468    pub fn server_address(mut self, address: impl Into<String>) -> Self {
469        self.config.server_address = address.into();
470        self
471    }
472
473    /// Builds the transport.
474    pub fn build(self) -> InternetTransport {
475        InternetTransport::with_config(self.device_id, self.config)
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482    use crate::constants::DEFAULT_MAX_MESSAGE_SIZE;
483    use offline_protocol_core::{AppId, UserId, WireCodec};
484
485    fn create_test_message() -> Message {
486        Message::new(
487            UserId::new("alice").unwrap(),
488            UserId::new("bob").unwrap(),
489            AppId::new("test").unwrap(),
490            "Test message",
491        )
492    }
493
494    /// The internet transport is the relay path, and its platform bridge treats
495    /// the payload as UTF-8 text. It must therefore always emit JSON — even when
496    /// the message carries a `BinaryV1` stamp from per-peer negotiation (only the
497    /// mesh transports honor that stamp). This locks the "internet relay stays
498    /// JSON" invariant against a refactor that swaps `serialize_message` for the
499    /// codec-aware `serialize_message_with`.
500    #[test]
501    fn serialize_stays_json_even_when_message_stamped_binary() {
502        let transport = InternetTransport::new("test-device");
503        let mut message = create_test_message();
504        message.set_wire_codec(WireCodec::BinaryV1);
505
506        let data = transport.serialize_message(&message).unwrap();
507
508        assert_eq!(
509            data.first(),
510            Some(&b'{'),
511            "internet transport must emit JSON regardless of the wire-codec stamp"
512        );
513        // And the emitted bytes must still round-trip through the JSON decoder.
514        assert_eq!(transport.deserialize_message(&data).unwrap().id, message.id);
515    }
516
517    #[test]
518    fn test_internet_transport_creation() {
519        let transport = InternetTransport::new("test-device");
520        assert_eq!(transport.device_id(), "test-device");
521        assert_eq!(transport.transport_type(), TransportType::Internet);
522        assert_eq!(transport.status(), TransportStatus::Unavailable);
523    }
524
525    #[test]
526    fn test_builder() {
527        let transport = InternetTransportBuilder::new("test-device")
528            .server_address("ws://example.com:8080")
529            .build();
530
531        assert_eq!(transport.config().server_address, "ws://example.com:8080");
532    }
533
534    #[test]
535    fn test_send_receive() {
536        let transport = InternetTransport::new("test-device");
537
538        // Mark as available
539        transport.on_status_changed(TransportStatus::Available);
540
541        // Send message
542        let message = create_test_message();
543        let message_id = message.id.as_str().to_string();
544        assert!(transport.send(&message).is_ok());
545
546        // Should have message in queue; get_next_message returns (id, bytes)
547        let next = transport.get_next_message().unwrap();
548        assert!(next.is_some());
549        let (returned_id, data) = next.unwrap();
550        assert_eq!(returned_id, message_id);
551        assert!(!data.is_empty());
552
553        // Message should now be in pending confirmation
554        assert_eq!(transport.pending_confirmation_count(), 1);
555
556        // Confirm sent — should update success metrics
557        transport.confirm_sent(&returned_id);
558        assert_eq!(transport.pending_confirmation_count(), 0);
559        let metrics = transport.metrics();
560        assert_eq!(metrics.success_count, 1);
561    }
562
563    #[test]
564    fn test_send_failure_tracking() {
565        let transport = InternetTransport::new("test-device");
566        transport.on_status_changed(TransportStatus::Available);
567
568        let message = create_test_message();
569        assert!(transport.send(&message).is_ok());
570
571        let (msg_id, _data) = transport.get_next_message().unwrap().unwrap();
572        assert_eq!(transport.pending_confirmation_count(), 1);
573
574        // Report failure
575        transport.report_send_failure(&msg_id);
576        assert_eq!(transport.pending_confirmation_count(), 0);
577        let metrics = transport.metrics();
578        assert_eq!(metrics.failure_count, 1);
579        assert_eq!(metrics.success_count, 0);
580    }
581
582    #[test]
583    fn test_pending_expiry_on_drain() {
584        let transport = InternetTransport::new("test-device");
585        transport.on_status_changed(TransportStatus::Available);
586
587        let message = create_test_message();
588        assert!(transport.send(&message).is_ok());
589
590        let (msg_id, _data) = transport.get_next_message().unwrap().unwrap();
591        assert_eq!(transport.pending_confirmation_count(), 1);
592
593        // Backdate the pending entry so it appears expired.
594        {
595            let mut pending = transport.pending_confirmation.lock().unwrap();
596            let expired_time = Instant::now()
597                - Duration::from_secs(INTERNET_PENDING_CONFIRMATION_TIMEOUT_SECS + 1);
598            pending.insert(msg_id.clone(), expired_time);
599        }
600
601        // Calling get_next_message (even with an empty queue) triggers drain.
602        let next = transport.get_next_message().unwrap();
603        assert!(next.is_none());
604
605        // The expired entry should have been drained and counted as a failure.
606        assert_eq!(transport.pending_confirmation_count(), 0);
607        let metrics = transport.metrics();
608        assert_eq!(metrics.failure_count, 1);
609        assert_eq!(metrics.success_count, 0);
610    }
611
612    #[test]
613    fn test_fail_all_pending_on_disconnect() {
614        let transport = InternetTransport::new("test-device");
615        transport.on_status_changed(TransportStatus::Available);
616
617        // Enqueue and dequeue two messages to put them in pending state.
618        let msg1 = create_test_message();
619        let msg2 = create_test_message();
620        assert!(transport.send(&msg1).is_ok());
621        assert!(transport.send(&msg2).is_ok());
622
623        let _ = transport.get_next_message().unwrap().unwrap();
624        let _ = transport.get_next_message().unwrap().unwrap();
625        assert_eq!(transport.pending_confirmation_count(), 2);
626
627        // Simulate disconnect — should fail all pending.
628        transport.on_status_changed(TransportStatus::Disconnected);
629        assert_eq!(transport.pending_confirmation_count(), 0);
630        let metrics = transport.metrics();
631        assert_eq!(metrics.failure_count, 2);
632        assert_eq!(metrics.success_count, 0);
633    }
634
635    #[test]
636    fn test_serialization() {
637        let transport = InternetTransport::new("test-device");
638        let message = create_test_message();
639
640        // Serialize
641        let data = transport.serialize_message(&message).unwrap();
642        assert!(!data.is_empty());
643
644        // Deserialize
645        let deserialized = transport.deserialize_message(&data).unwrap();
646        assert_eq!(deserialized.id, message.id);
647    }
648
649    #[test]
650    fn test_stop_fails_pending() {
651        let transport = InternetTransport::new("test-device");
652        transport.on_status_changed(TransportStatus::Available);
653
654        let msg = create_test_message();
655        assert!(transport.send(&msg).is_ok());
656        let _ = transport.get_next_message().unwrap().unwrap();
657        assert_eq!(transport.pending_confirmation_count(), 1);
658
659        transport.stop().unwrap();
660        assert_eq!(transport.pending_confirmation_count(), 0);
661        assert_eq!(transport.metrics().failure_count, 1);
662    }
663
664    /// Exhaustive destructuring guard for `update_metrics`.
665    ///
666    /// When a field is added to `TransportMetrics`, this test will fail to
667    /// compile — forcing the developer to decide whether
668    /// `InternetTransport::update_metrics` should copy the new field (platform-
669    /// reported) or leave it alone (confirmation-loop-managed).
670    #[test]
671    fn update_metrics_covers_all_fields() {
672        let m = TransportMetrics::default();
673        // Confirmation-loop-managed fields (preserved by update_metrics):
674        //   success_count, failure_count, delivery_ratio, drop_rate
675        // Platform-reported fields (copied by update_metrics):
676        //   rssi, latency_ms, bandwidth_bps, congestion, queue_depth,
677        //   battery_level, is_charging, relay_connection_count,
678        //   is_active_relay, average_hop_count, energy_cost
679        let TransportMetrics {
680            rssi: _,
681            latency_ms: _,
682            bandwidth_bps: _,
683            congestion: _,
684            queue_depth: _,
685            success_count: _,
686            failure_count: _,
687            battery_level: _,
688            is_charging: _,
689            relay_connection_count: _,
690            is_active_relay: _,
691            delivery_ratio: _,
692            drop_rate: _,
693            average_hop_count: _,
694            energy_cost: _,
695        } = m;
696    }
697
698    #[test]
699    fn test_send_when_unavailable_fails() {
700        let transport = InternetTransport::new("test-device");
701        let message = create_test_message();
702        let result = transport.send(&message);
703        assert!(result.is_err());
704        assert!(matches!(
705            result.unwrap_err(),
706            crate::Error::TransportNotAvailable(_)
707        ));
708    }
709
710    #[test]
711    fn test_receive_when_empty_returns_none() {
712        let transport = InternetTransport::new("test-device");
713        let received = transport.receive().unwrap();
714        assert!(received.is_none());
715    }
716
717    #[test]
718    fn test_on_data_received_invalid_json_drops_ok() {
719        let transport = InternetTransport::new("test-device");
720        let result = transport.on_data_received(b"not valid json".to_vec());
721        assert!(result.is_ok());
722        assert!(transport.receive().unwrap().is_none());
723    }
724
725    #[test]
726    fn test_on_data_received_valid_json_queues_message() {
727        let transport = InternetTransport::new("test-device");
728        let message = create_test_message();
729        let data = transport.serialize_message(&message).unwrap();
730        transport.on_data_received(data).unwrap();
731        let received = transport.receive().unwrap();
732        assert!(received.is_some());
733        assert_eq!(received.unwrap().id, message.id);
734    }
735
736    #[test]
737    fn test_update_metrics_preserves_confirmation_counts() {
738        let transport = InternetTransport::new("test-device");
739        transport.on_status_changed(TransportStatus::Available);
740        let msg = create_test_message();
741        transport.send(&msg).unwrap();
742        let (id, _) = transport.get_next_message().unwrap().unwrap();
743        transport.confirm_sent(&id);
744        assert_eq!(transport.metrics().success_count, 1);
745
746        let mut incoming = TransportMetrics::default();
747        incoming.rssi = Some(-70);
748        incoming.latency_ms = Some(100);
749        transport.update_metrics(incoming);
750
751        let m = transport.metrics();
752        assert_eq!(m.rssi, Some(-70));
753        assert_eq!(m.latency_ms, Some(100));
754        assert_eq!(m.success_count, 1);
755    }
756
757    #[test]
758    fn test_confirm_sent_unknown_id_no_panic() {
759        let transport = InternetTransport::new("test-device");
760        transport.confirm_sent("nonexistent-id");
761        assert_eq!(transport.metrics().success_count, 0);
762    }
763
764    #[test]
765    fn test_report_send_failure_unknown_id_no_panic() {
766        let transport = InternetTransport::new("test-device");
767        transport.report_send_failure("nonexistent-id");
768        assert_eq!(transport.metrics().failure_count, 0);
769    }
770
771    #[test]
772    fn test_start_does_not_require_connection() {
773        let transport = InternetTransport::new("test-device");
774        assert!(transport.start().is_ok());
775        assert_eq!(transport.status(), TransportStatus::Unavailable);
776    }
777
778    #[test]
779    fn test_on_data_received_rejects_oversized_payload() {
780        let transport = InternetTransport::new("test-device");
781        let oversized = vec![0u8; DEFAULT_MAX_MESSAGE_SIZE + 1];
782        let result = transport.on_data_received(oversized);
783        assert!(result.is_err());
784        assert!(matches!(
785            result.unwrap_err(),
786            crate::Error::MessageTooLarge(_, _)
787        ));
788    }
789
790    #[test]
791    fn test_on_data_received_from_rejects_oversized_payload() {
792        let transport = InternetTransport::new("test-device");
793        let oversized = vec![0u8; DEFAULT_MAX_MESSAGE_SIZE + 1];
794        let result = transport.on_data_received_from(oversized, "peer1".to_string());
795        assert!(result.is_err());
796        assert!(matches!(
797            result.unwrap_err(),
798            crate::Error::MessageTooLarge(_, _)
799        ));
800    }
801}