Skip to main content

runtime_foxdriver/
network.rs

1//! Passive network instrumentation for BiDi browsers.
2//!
3//! Captures every request, response, and fetch error that flows through the
4//! browser, then provides ergonomic query, streaming, and export utilities.
5//!
6//! This module captures response *metadata* only. Response bodies are not
7//! fetched because the `rustenium-bidi-definitions` crate does not expose a
8//! `network.getResponseBody` command and the `ResponseContent` event only
9//! reports `size`. Callers that need body content must fetch it separately.
10
11use anyhow::Result;
12use rustenium_bidi_definitions::network::events::{
13    BeforeRequestSent, FetchError, ResponseCompleted,
14};
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use std::sync::Arc;
18use tokio::sync::{broadcast, RwLock};
19
20// ------------------------------------------------------------------
21// Data types
22// ------------------------------------------------------------------
23
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub struct CapturedHeader {
26    pub name: String,
27    pub value: String,
28}
29
30impl From<&rustenium_bidi_definitions::network::types::Header> for CapturedHeader {
31    fn from(h: &rustenium_bidi_definitions::network::types::Header) -> Self {
32        let value = match &h.value {
33            rustenium_bidi_definitions::network::types::BytesValue::StringValue(s) => {
34                s.value.clone()
35            }
36            rustenium_bidi_definitions::network::types::BytesValue::Base64Value(b) => {
37                b.value.clone()
38            }
39        };
40        Self {
41            name: h.name.clone(),
42            value,
43        }
44    }
45}
46
47#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
48pub struct CapturedTiming {
49    pub dns_start_ms: Option<f64>,
50    pub dns_end_ms: Option<f64>,
51    pub connect_start_ms: Option<f64>,
52    pub connect_end_ms: Option<f64>,
53    pub tls_start_ms: Option<f64>,
54    pub response_start_ms: Option<f64>,
55    pub response_end_ms: Option<f64>,
56}
57
58impl From<&rustenium_bidi_definitions::network::types::FetchTimingInfo> for CapturedTiming {
59    fn from(t: &rustenium_bidi_definitions::network::types::FetchTimingInfo) -> Self {
60        let origin = t.request_time;
61        Self {
62            dns_start_ms: non_neg(t.dns_start - origin),
63            dns_end_ms: non_neg(t.dns_end - origin),
64            connect_start_ms: non_neg(t.connect_start - origin),
65            connect_end_ms: non_neg(t.connect_end - origin),
66            tls_start_ms: non_neg(t.tls_start - origin),
67            response_start_ms: non_neg(t.response_start - origin),
68            response_end_ms: non_neg(t.response_end - origin),
69        }
70    }
71}
72
73fn non_neg(v: f64) -> Option<f64> {
74    if v >= 0.0 {
75        Some(v)
76    } else {
77        None
78    }
79}
80
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
82pub struct CapturedCookie {
83    pub name: String,
84    pub value: String,
85    pub domain: String,
86    pub path: String,
87    pub size: u64,
88    pub http_only: bool,
89    pub secure: bool,
90    pub same_site: String,
91}
92
93impl From<&rustenium_bidi_definitions::network::types::Cookie> for CapturedCookie {
94    fn from(c: &rustenium_bidi_definitions::network::types::Cookie) -> Self {
95        let value = match &c.value {
96            rustenium_bidi_definitions::network::types::BytesValue::StringValue(s) => {
97                s.value.clone()
98            }
99            rustenium_bidi_definitions::network::types::BytesValue::Base64Value(b) => {
100                b.value.clone()
101            }
102        };
103        Self {
104            name: c.name.clone(),
105            value,
106            domain: c.domain.clone(),
107            path: c.path.clone(),
108            size: c.size,
109            http_only: c.http_only,
110            secure: c.secure,
111            same_site: format!("{:?}", c.same_site).to_lowercase(),
112        }
113    }
114}
115
116#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
117pub struct CapturedRequest {
118    pub id: String,
119    pub context: Option<String>,
120    pub method: String,
121    pub url: String,
122    pub headers: Vec<CapturedHeader>,
123    pub post_data: Option<String>,
124    pub timestamp: u64,
125    pub destination: String,
126    pub initiator_type: Option<String>,
127    pub timing: CapturedTiming,
128    pub cookies: Vec<CapturedCookie>,
129}
130
131impl CapturedRequest {
132    /// If `post_data` looks like JSON, return it parsed.
133    pub fn json_body(&self) -> Option<serde_json::Value> {
134        self.post_data
135            .as_ref()
136            .and_then(|s| serde_json::from_str(s).ok())
137    }
138
139    /// Return the parsed query parameters as a vec of (key, value) pairs.
140    ///
141    /// Returns `Err` when `self.url` cannot be parsed as a URL, so callers can
142    /// distinguish "URL did not parse" from "URL had no query string".
143    pub fn query_params(&self) -> Result<Vec<(String, String)>, url::ParseError> {
144        let u = url::Url::parse(&self.url)?;
145        Ok(u.query_pairs()
146            .map(|(k, v)| (k.to_string(), v.to_string()))
147            .collect())
148    }
149
150    /// Case-insensitive header lookup.
151    pub fn request_header(&self, name: &str) -> Option<&str> {
152        let name_lower = name.to_lowercase();
153        self.headers
154            .iter()
155            .find(|h| h.name.to_lowercase() == name_lower)
156            .map(|h| h.value.as_str())
157    }
158}
159
160/// A captured network response.
161///
162/// `foxdriver` captures response **metadata** (headers, status, MIME, size)
163/// from BiDi `network.responseCompleted` events. It does **not** fetch or
164/// store response bodies: the `rustenium-bidi-definitions` crate used by this
165/// driver does not expose a `network.getResponseBody` command, and the
166/// `ResponseContent` event field only carries `size`. Callers that need body
167/// content must fetch it through another mechanism.
168#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
169pub struct CapturedResponse {
170    pub id: String,
171    pub url: String,
172    pub protocol: String,
173    pub status: u16,
174    pub status_text: String,
175    pub headers: Vec<CapturedHeader>,
176    pub mime_type: String,
177    /// Response body size, as reported by the browser.
178    ///
179    /// This is metadata only; the response body bytes are not captured.
180    pub body_size: Option<u64>,
181    pub from_cache: bool,
182}
183
184#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
185pub struct CapturedError {
186    pub id: String,
187    pub url: String,
188    pub error_text: String,
189}
190
191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192pub struct NetworkEntry {
193    pub request: CapturedRequest,
194    pub response: Option<CapturedResponse>,
195    pub error: Option<CapturedError>,
196}
197
198impl NetworkEntry {
199    pub fn has_response(&self) -> bool {
200        self.response.is_some()
201    }
202
203    pub fn is_error(&self) -> bool {
204        self.error.is_some()
205    }
206
207    pub fn final_url(&self) -> &str {
208        self.response
209            .as_ref()
210            .map(|r| r.url.as_str())
211            .unwrap_or(&self.request.url)
212    }
213
214    pub fn status(&self) -> Option<u16> {
215        self.response.as_ref().map(|r| r.status)
216    }
217
218    pub fn response_header(&self, name: &str) -> Option<&str> {
219        let name_lower = name.to_lowercase();
220        self.response.as_ref().and_then(|r| {
221            r.headers
222                .iter()
223                .find(|h| h.name.to_lowercase() == name_lower)
224                .map(|h| h.value.as_str())
225        })
226    }
227
228    pub fn request_header(&self, name: &str) -> Option<&str> {
229        self.request.request_header(name)
230    }
231
232    /// Generate a curl command that replays this request.
233    pub fn to_curl(&self) -> String {
234        fn shell_quote(s: &str) -> String {
235            // Escape single quotes for bash: ' -> '\''
236            s.replace('\'', "'\\''")
237        }
238        let req = &self.request;
239        // The method is interpolated into a shell command line, so it must be
240        // quoted like every other captured value. BiDi delivers it as a
241        // string; a non-standard method from a hostile or buggy endpoint
242        // must not become extra shell arguments.
243        let mut parts = vec![format!("curl -X '{}'", shell_quote(&req.method))];
244        for h in &req.headers {
245            if h.name.eq_ignore_ascii_case("host")
246                || h.name.eq_ignore_ascii_case("connection")
247                || h.name.eq_ignore_ascii_case("accept-encoding")
248            {
249                continue;
250            }
251            parts.push(format!(
252                "-H '{}: {}'",
253                shell_quote(&h.name),
254                shell_quote(&h.value)
255            ));
256        }
257        if let Some(ref body) = req.post_data {
258            parts.push(format!("-d '{}'", shell_quote(body)));
259        }
260        parts.push(format!("'{}'", shell_quote(&req.url)));
261        parts.join(" ")
262    }
263}
264
265// ------------------------------------------------------------------
266// Filter
267// ------------------------------------------------------------------
268
269#[derive(Debug, Clone, Default)]
270pub struct Filter {
271    method: Option<String>,
272    status_range: Option<std::ops::RangeInclusive<u16>>,
273    url_substring: Option<String>,
274    url_regex: Option<regex::Regex>,
275    header_name: Option<String>,
276    header_value_substring: Option<String>,
277    destination: Option<String>,
278    has_response: Option<bool>,
279    has_error: Option<bool>,
280}
281
282impl Filter {
283    pub fn new() -> Self {
284        Self::default()
285    }
286
287    pub fn method(mut self, m: impl Into<String>) -> Self {
288        self.method = Some(m.into().to_uppercase());
289        self
290    }
291
292    pub fn status_range(mut self, r: std::ops::RangeInclusive<u16>) -> Self {
293        self.status_range = Some(r);
294        self
295    }
296
297    pub fn url_contains(mut self, needle: impl Into<String>) -> Self {
298        self.url_substring = Some(needle.into().to_lowercase());
299        self
300    }
301
302    pub fn url_regex(mut self, pattern: &str) -> Result<Self> {
303        self.url_regex = Some(regex::Regex::new(pattern)?);
304        Ok(self)
305    }
306
307    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
308        self.header_name = Some(name.into().to_lowercase());
309        self.header_value_substring = Some(value.into().to_lowercase());
310        self
311    }
312
313    pub fn header_name(mut self, name: impl Into<String>) -> Self {
314        self.header_name = Some(name.into().to_lowercase());
315        self
316    }
317
318    pub fn header_value(mut self, value: impl Into<String>) -> Self {
319        self.header_value_substring = Some(value.into().to_lowercase());
320        self
321    }
322
323    pub fn destination(mut self, d: impl Into<String>) -> Self {
324        self.destination = Some(d.into().to_lowercase());
325        self
326    }
327
328    pub fn with_response(mut self) -> Self {
329        self.has_response = Some(true);
330        self
331    }
332
333    pub fn without_response(mut self) -> Self {
334        self.has_response = Some(false);
335        self
336    }
337
338    pub fn with_error(mut self) -> Self {
339        self.has_error = Some(true);
340        self
341    }
342
343    fn matches(&self, e: &NetworkEntry) -> bool {
344        if let Some(m) = &self.method {
345            if e.request.method.to_uppercase() != *m {
346                return false;
347            }
348        }
349        if let Some(r) = &self.status_range {
350            // A response with no status (fetch error, incomplete response)
351            // must not match a status range; treat missing as absent, not 0.
352            let Some(st) = e.status() else {
353                return false;
354            };
355            if !r.contains(&st) {
356                return false;
357            }
358        }
359        if let Some(needle) = &self.url_substring {
360            if !e.request.url.to_lowercase().contains(needle) {
361                return false;
362            }
363        }
364        if let Some(re) = &self.url_regex {
365            if !re.is_match(&e.request.url) {
366                return false;
367            }
368        }
369        if let (Some(name), Some(value)) = (&self.header_name, &self.header_value_substring) {
370            let found = e
371                .request
372                .headers
373                .iter()
374                .chain(
375                    e.response
376                        .as_ref()
377                        .map(|r| r.headers.as_slice())
378                        .unwrap_or(&[]),
379                )
380                .any(|h| {
381                    h.name.to_lowercase().contains(name) && h.value.to_lowercase().contains(value)
382                });
383            if !found {
384                return false;
385            }
386        } else if let Some(name) = &self.header_name {
387            let found = e
388                .request
389                .headers
390                .iter()
391                .chain(
392                    e.response
393                        .as_ref()
394                        .map(|r| r.headers.as_slice())
395                        .unwrap_or(&[]),
396                )
397                .any(|h| h.name.to_lowercase().contains(name));
398            if !found {
399                return false;
400            }
401        } else if let Some(value) = &self.header_value_substring {
402            let found = e
403                .request
404                .headers
405                .iter()
406                .chain(
407                    e.response
408                        .as_ref()
409                        .map(|r| r.headers.as_slice())
410                        .unwrap_or(&[]),
411                )
412                .any(|h| h.value.to_lowercase().contains(value));
413            if !found {
414                return false;
415            }
416        }
417        if let Some(d) = &self.destination {
418            if e.request.destination.to_lowercase() != *d {
419                return false;
420            }
421        }
422        if let Some(want) = self.has_response {
423            if e.has_response() != want {
424                return false;
425            }
426        }
427        if let Some(want) = self.has_error {
428            if e.is_error() != want {
429                return false;
430            }
431        }
432        true
433    }
434}
435
436// ------------------------------------------------------------------
437// Metrics
438// ------------------------------------------------------------------
439
440#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
441pub struct NetworkMetrics {
442    pub requests_received: u64,
443    pub responses_received: u64,
444    pub errors_received: u64,
445    pub entries_evicted: u64,
446    pub pending_responses_dropped: u64,
447    pub pending_errors_dropped: u64,
448    pub broadcast_drops: u64,
449    pub duplicate_responses: u64,
450    pub duplicate_errors: u64,
451    pub max_entries: usize,
452}
453
454// ------------------------------------------------------------------
455// Inner log state
456// ------------------------------------------------------------------
457
458#[derive(Debug)]
459struct Inner {
460    entries: Vec<Arc<NetworkEntry>>,
461    by_id: HashMap<String, usize>,
462    pending_responses: HashMap<String, CapturedResponse>,
463    pending_errors: HashMap<String, CapturedError>,
464    max_entries: usize,
465    max_pending: usize,
466    tx: broadcast::Sender<Arc<NetworkEntry>>,
467    metrics: NetworkMetrics,
468    /// Whether we have already warned about entry eviction this session.
469    eviction_warned: bool,
470}
471
472impl Inner {
473    fn new(
474        max_entries: usize,
475        max_pending: usize,
476        tx: broadcast::Sender<Arc<NetworkEntry>>,
477    ) -> Self {
478        let metrics = NetworkMetrics {
479            max_entries,
480            ..NetworkMetrics::default()
481        };
482        Self {
483            entries: if max_entries > 0 {
484                Vec::with_capacity(max_entries)
485            } else {
486                Vec::new()
487            },
488            by_id: HashMap::new(),
489            pending_responses: HashMap::new(),
490            pending_errors: HashMap::new(),
491            max_entries,
492            max_pending,
493            tx,
494            metrics,
495            eviction_warned: false,
496        }
497    }
498
499    fn push_entry(&mut self, entry: Arc<NetworkEntry>) {
500        // Skip duplicate request IDs: BiDi can occasionally resend.
501        if self.by_id.contains_key(&entry.request.id) {
502            return;
503        }
504
505        // Evict just enough oldest entries to stay under the cap.
506        if self.max_entries > 0 && self.entries.len() >= self.max_entries {
507            let remove = self.entries.len().saturating_sub(self.max_entries - 1);
508            let removed = self.entries.drain(..remove).len();
509            self.metrics.entries_evicted += removed as u64;
510            self.by_id.clear();
511            for (i, e) in self.entries.iter().enumerate() {
512                self.by_id.insert(e.request.id.clone(), i);
513            }
514            if !self.eviction_warned {
515                self.eviction_warned = true;
516                tracing::warn!(
517                    removed,
518                    max_entries = self.max_entries,
519                    "[foxdriver] network capture entry evicted; oldest requests are being dropped"
520                );
521            }
522        }
523
524        let idx = self.entries.len();
525        self.by_id.insert(entry.request.id.clone(), idx);
526        self.entries.push(entry.clone());
527
528        // Broadcast (ignore errors (all receivers dropped)).
529        if self.tx.send(entry).is_err() {
530            self.metrics.broadcast_drops += 1;
531        }
532    }
533}
534
535// ------------------------------------------------------------------
536// NetworkLog
537// ------------------------------------------------------------------
538
539#[derive(Debug, Clone)]
540pub struct NetworkLog {
541    inner: Arc<RwLock<Inner>>,
542}
543
544impl NetworkLog {
545    pub fn new() -> Self {
546        Self::with_limits(50_000, 10_000)
547    }
548
549    pub fn with_limits(max_entries: usize, max_pending: usize) -> Self {
550        let (tx, _) = broadcast::channel(1024);
551        Self {
552            inner: Arc::new(RwLock::new(Inner::new(max_entries, max_pending, tx))),
553        }
554    }
555
556    pub async fn subscribe(&self) -> broadcast::Receiver<Arc<NetworkEntry>> {
557        self.inner.read().await.tx.subscribe()
558    }
559
560    pub async fn entries(&self) -> Vec<Arc<NetworkEntry>> {
561        let inner = self.inner.read().await;
562        inner.entries.clone()
563    }
564
565    pub async fn completed(&self) -> Vec<Arc<NetworkEntry>> {
566        let inner = self.inner.read().await;
567        inner
568            .entries
569            .iter()
570            .filter(|e| e.has_response() || e.is_error())
571            .cloned()
572            .collect()
573    }
574
575    pub async fn count(&self, filter: Filter) -> usize {
576        let inner = self.inner.read().await;
577        inner.entries.iter().filter(|e| filter.matches(e)).count()
578    }
579
580    pub async fn filter(&self, f: Filter) -> Vec<Arc<NetworkEntry>> {
581        let inner = self.inner.read().await;
582        inner
583            .entries
584            .iter()
585            .filter(|e| f.matches(e))
586            .cloned()
587            .collect()
588    }
589
590    pub async fn first(&self) -> Option<Arc<NetworkEntry>> {
591        let inner = self.inner.read().await;
592        inner.entries.first().cloned()
593    }
594
595    pub async fn last(&self) -> Option<Arc<NetworkEntry>> {
596        let inner = self.inner.read().await;
597        inner.entries.last().cloned()
598    }
599
600    pub async fn nth(&self, n: usize) -> Option<Arc<NetworkEntry>> {
601        let inner = self.inner.read().await;
602        inner.entries.get(n).cloned()
603    }
604
605    pub async fn find_by_url(&self, substring: &str) -> Option<Arc<NetworkEntry>> {
606        let inner = self.inner.read().await;
607        inner
608            .entries
609            .iter()
610            .find(|e| e.request.url.contains(substring))
611            .cloned()
612    }
613
614    pub async fn find_by_url_regex(&self, re: &regex::Regex) -> Option<Arc<NetworkEntry>> {
615        let inner = self.inner.read().await;
616        inner
617            .entries
618            .iter()
619            .find(|e| re.is_match(&e.request.url))
620            .cloned()
621    }
622
623    pub async fn endpoints(&self) -> Vec<String> {
624        let inner = self.inner.read().await;
625        let mut seen = std::collections::HashSet::new();
626        inner
627            .entries
628            .iter()
629            .filter(|e| seen.insert(e.request.url.clone()))
630            .map(|e| e.request.url.clone())
631            .collect()
632    }
633
634    pub async fn hostnames(&self) -> Vec<String> {
635        let inner = self.inner.read().await;
636        let mut seen = std::collections::HashSet::new();
637        inner
638            .entries
639            .iter()
640            .filter_map(|e| {
641                url::Url::parse(&e.request.url).ok().and_then(|u| {
642                    let host = u.host_str()?.to_string();
643                    seen.insert(host.clone()).then_some(host)
644                })
645            })
646            .collect()
647    }
648
649    pub async fn distinct_methods(&self) -> Vec<String> {
650        let inner = self.inner.read().await;
651        let mut seen = std::collections::HashSet::new();
652        let mut out: Vec<String> = inner
653            .entries
654            .iter()
655            .filter_map(|e| {
656                seen.insert(e.request.method.clone())
657                    .then_some(e.request.method.clone())
658            })
659            .collect();
660        out.sort_unstable();
661        out.dedup();
662        out
663    }
664
665    pub async fn distinct_statuses(&self) -> Vec<u16> {
666        let inner = self.inner.read().await;
667        let mut seen = std::collections::HashSet::new();
668        let mut out: Vec<u16> = inner
669            .entries
670            .iter()
671            .filter_map(|e| {
672                let status = e.status()?;
673                seen.insert(status).then_some(status)
674            })
675            .collect();
676        out.sort_unstable();
677        out
678    }
679
680    pub async fn total_bytes_in(&self) -> u64 {
681        let inner = self.inner.read().await;
682        inner
683            .entries
684            .iter()
685            .filter_map(|e| e.response.as_ref().and_then(|r| r.body_size))
686            .sum()
687    }
688
689    pub async fn total_bytes_out(&self) -> u64 {
690        let inner = self.inner.read().await;
691        inner
692            .entries
693            .iter()
694            .map(|e| {
695                e.request
696                    .post_data
697                    .as_ref()
698                    .map(|b| b.len() as u64)
699                    .unwrap_or(0)
700            })
701            .sum()
702    }
703
704    pub async fn len(&self) -> usize {
705        self.inner.read().await.entries.len()
706    }
707
708    pub async fn is_empty(&self) -> bool {
709        self.len().await == 0
710    }
711
712    pub async fn clear(&self) {
713        let mut inner = self.inner.write().await;
714        inner.entries.clear();
715        inner.by_id.clear();
716    }
717
718    pub async fn metrics(&self) -> NetworkMetrics {
719        self.inner.read().await.metrics.clone()
720    }
721
722    pub async fn contains_id(&self, id: &str) -> bool {
723        self.inner.read().await.by_id.contains_key(id)
724    }
725
726    pub async fn remove_by_id(&self, id: &str) -> Option<Arc<NetworkEntry>> {
727        let mut inner = self.inner.write().await;
728        let idx = inner.by_id.remove(id)?;
729        let entry = inner.entries.remove(idx);
730        // Rebuild indices for entries after the removed one
731        let updates: Vec<(String, usize)> = inner
732            .entries
733            .iter()
734            .enumerate()
735            .skip(idx)
736            .map(|(i, e)| (e.request.id.clone(), i))
737            .collect();
738        for (id, i) in updates {
739            inner.by_id.insert(id, i);
740        }
741        Some(entry)
742    }
743
744    /// Return all captured request IDs in chronological order.
745    pub async fn request_ids(&self) -> Vec<String> {
746        let inner = self.inner.read().await;
747        inner.entries.iter().map(|e| e.request.id.clone()).collect()
748    }
749
750    /// Return an approximate memory footprint in bytes.
751    /// Useful for monitoring and backpressure decisions.
752    pub async fn memory_estimate(&self) -> usize {
753        let inner = self.inner.read().await;
754        let mut estimate = 0;
755        estimate += inner.entries.capacity() * std::mem::size_of::<Arc<NetworkEntry>>();
756        estimate +=
757            inner.by_id.capacity() * (std::mem::size_of::<String>() + std::mem::size_of::<usize>());
758        estimate += inner.pending_responses.capacity()
759            * (std::mem::size_of::<String>() + std::mem::size_of::<CapturedResponse>());
760        estimate += inner.pending_errors.capacity()
761            * (std::mem::size_of::<String>() + std::mem::size_of::<CapturedError>());
762        for e in &inner.entries {
763            estimate += e.request.url.len();
764            estimate += e.request.method.len();
765            estimate += e
766                .request
767                .headers
768                .iter()
769                .map(|h| h.name.len() + h.value.len())
770                .sum::<usize>();
771            if let Some(ref body) = e.request.post_data {
772                estimate += body.len();
773            }
774            if let Some(ref r) = e.response {
775                estimate +=
776                    r.url.len() + r.status_text.len() + r.mime_type.len() + r.protocol.len();
777                estimate += r
778                    .headers
779                    .iter()
780                    .map(|h| h.name.len() + h.value.len())
781                    .sum::<usize>();
782            }
783            if let Some(ref err) = e.error {
784                estimate += err.url.len() + err.error_text.len();
785            }
786        }
787        estimate
788    }
789
790    /// Return whether the entry with `id` has a response.
791    pub async fn has_response(&self, id: &str) -> bool {
792        let inner = self.inner.read().await;
793        inner
794            .by_id
795            .get(id)
796            .map(|&idx| inner.entries[idx].response.is_some())
797            .unwrap_or(false)
798    }
799
800    /// Return whether the entry with `id` has an error.
801    pub async fn has_error(&self, id: &str) -> bool {
802        let inner = self.inner.read().await;
803        inner
804            .by_id
805            .get(id)
806            .map(|&idx| inner.entries[idx].error.is_some())
807            .unwrap_or(false)
808    }
809
810    /// Return the number of out-of-order responses/errors stashed in
811    /// pending maps (orphaned entries waiting for their request).
812    pub async fn pending_count(&self) -> usize {
813        let inner = self.inner.read().await;
814        inner.pending_responses.len() + inner.pending_errors.len()
815    }
816
817    /// Drain all orphaned pending responses and errors, returning them.
818    /// Useful for preventing unbounded growth when requests never arrive
819    /// (e.g. page navigation interrupts the request lifecycle).
820    pub async fn drain_pending(&self) -> (Vec<CapturedResponse>, Vec<CapturedError>) {
821        let mut inner = self.inner.write().await;
822        let responses: Vec<CapturedResponse> =
823            inner.pending_responses.drain().map(|(_, v)| v).collect();
824        let errors: Vec<CapturedError> = inner.pending_errors.drain().map(|(_, v)| v).collect();
825        (responses, errors)
826    }
827
828    /// Retain only entries that satisfy the predicate.
829    /// More efficient than repeated `remove_by_id` for bulk filtering.
830    pub async fn retain<F>(&self, mut f: F)
831    where
832        F: FnMut(&NetworkEntry) -> bool,
833    {
834        let mut inner = self.inner.write().await;
835        let mut new_entries = Vec::new();
836        let mut new_by_id = HashMap::new();
837        for entry in inner.entries.drain(..) {
838            if f(&entry) {
839                let idx = new_entries.len();
840                new_by_id.insert(entry.request.id.clone(), idx);
841                new_entries.push(entry);
842            }
843        }
844        inner.entries = new_entries;
845        inner.by_id = new_by_id;
846    }
847
848    /// Wait up to `timeout` for an entry whose request URL contains `substring`.
849    pub async fn wait_for_url(
850        &self,
851        substring: &str,
852        timeout: std::time::Duration,
853    ) -> Option<Arc<NetworkEntry>> {
854        let mut rx = self.subscribe().await;
855        let deadline = tokio::time::Instant::now() + timeout;
856
857        // Check existing entries first
858        if let Some(e) = self.find_by_url(substring).await {
859            return Some(e);
860        }
861
862        loop {
863            let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
864            match tokio::time::timeout(remaining, rx.recv()).await {
865                Ok(Ok(entry)) => {
866                    if entry.request.url.contains(substring) {
867                        return Some(entry);
868                    }
869                }
870                _ => return None,
871            }
872        }
873    }
874
875    /// Wait up to `timeout` for a response to the given request id.
876    pub async fn wait_for_response(
877        &self,
878        id: &str,
879        timeout: std::time::Duration,
880    ) -> Option<Arc<NetworkEntry>> {
881        let mut rx = self.subscribe().await;
882        let deadline = tokio::time::Instant::now() + timeout;
883
884        // Check existing entries first
885        {
886            let inner = self.inner.read().await;
887            if let Some(idx) = inner.by_id.get(id) {
888                if inner.entries[*idx].has_response() || inner.entries[*idx].is_error() {
889                    return Some(inner.entries[*idx].clone());
890                }
891            }
892        }
893
894        loop {
895            let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
896            match tokio::time::timeout(remaining, rx.recv()).await {
897                Ok(Ok(entry)) => {
898                    if entry.request.id == id && (entry.has_response() || entry.is_error()) {
899                        return Some(entry);
900                    }
901                }
902                _ => return None,
903            }
904        }
905    }
906
907    // ------------------------------------------------------------------
908    // Convenience queries
909    // ------------------------------------------------------------------
910
911    /// Return all entries with the given HTTP status code.
912    pub async fn find_by_status(&self, status: u16) -> Vec<Arc<NetworkEntry>> {
913        let inner = self.inner.read().await;
914        inner
915            .entries
916            .iter()
917            .filter(|e| e.status() == Some(status))
918            .cloned()
919            .collect()
920    }
921
922    /// Return entries whose request timestamp is >= the given value.
923    pub async fn entries_since(&self, timestamp: u64) -> Vec<Arc<NetworkEntry>> {
924        let inner = self.inner.read().await;
925        inner
926            .entries
927            .iter()
928            .filter(|e| e.request.timestamp >= timestamp)
929            .cloned()
930            .collect()
931    }
932
933    /// Return the most recent `n` entries.
934    pub async fn last_n(&self, n: usize) -> Vec<Arc<NetworkEntry>> {
935        let inner = self.inner.read().await;
936        inner
937            .entries
938            .iter()
939            .rev()
940            .take(n)
941            .cloned()
942            .collect::<Vec<_>>()
943            .into_iter()
944            .rev()
945            .collect()
946    }
947
948    /// Return a deduplicated list of every captured request URL.
949    pub async fn unique_urls(&self) -> Vec<String> {
950        let inner = self.inner.read().await;
951        let mut seen = std::collections::HashSet::new();
952        inner
953            .entries
954            .iter()
955            .filter_map(|e| {
956                if seen.insert(e.request.url.clone()) {
957                    Some(e.request.url.clone())
958                } else {
959                    None
960                }
961            })
962            .collect()
963    }
964
965    // ------------------------------------------------------------------
966    // Export
967    // ------------------------------------------------------------------
968
969    pub async fn save_to_json(&self, path: &std::path::Path) -> Result<()> {
970        let entries: Vec<NetworkEntry> =
971            self.entries().await.iter().map(|e| (**e).clone()).collect();
972        let json = serde_json::to_string_pretty(&entries)?;
973        tokio::fs::write(path, json).await?;
974        Ok(())
975    }
976
977    /// Write a HAR 1.2 file.
978    pub async fn save_as_har(
979        &self,
980        path: &std::path::Path,
981        page_title: Option<&str>,
982    ) -> Result<()> {
983        use chrono::Utc;
984
985        let entries = self.entries().await;
986        let started = if let Some(e) = entries.first() {
987            chrono::DateTime::from_timestamp_millis(e.request.timestamp as i64)
988                .unwrap_or_else(Utc::now)
989        } else {
990            Utc::now()
991        };
992
993        let mut har_entries = Vec::new();
994        for e in &entries {
995            let req = &e.request;
996            let resp = e.response.as_ref();
997            let timing = &req.timing;
998
999            let query_string: Vec<_> = match req.query_params() {
1000                Ok(params) => params
1001                    .into_iter()
1002                    .map(|(name, value)| serde_json::json!({"name": name, "value": value}))
1003                    .collect(),
1004                Err(error) => {
1005                    tracing::warn!(
1006                        "failed to parse request URL for query parameters: {error}; url={}",
1007                        req.url
1008                    );
1009                    Vec::new()
1010                }
1011            };
1012
1013            let request_json = serde_json::json!({
1014                "method": req.method,
1015                "url": req.url,
1016                "httpVersion": "HTTP/1.1",
1017                "headers": req.headers.iter().map(|h| serde_json::json!({"name": h.name, "value": h.value})).collect::<Vec<_>>(),
1018                "cookies": req.cookies.iter().map(|c| serde_json::json!({"name": c.name, "value": c.value, "domain": c.domain, "path": c.path})).collect::<Vec<_>>(),
1019                "queryString": query_string,
1020                "headersSize": -1,
1021                "bodySize": req.post_data.as_ref().map(|b| b.len() as i64).unwrap_or(-1),
1022                "postData": req.post_data.as_ref().map(|b| {
1023                    let mime = req.request_header("content-type").unwrap_or("application/octet-stream");
1024                    serde_json::json!({"mimeType": mime, "text": b})
1025                }),
1026            });
1027
1028            let response_json = resp.map(|r| serde_json::json!({
1029                "status": r.status,
1030                "statusText": r.status_text,
1031                "httpVersion": "HTTP/1.1",
1032                "headers": r.headers.iter().map(|h| serde_json::json!({"name": h.name, "value": h.value})).collect::<Vec<_>>(),
1033                "cookies": [],
1034                "content": {
1035                    "size": r.body_size.unwrap_or(0),
1036                    "mimeType": r.mime_type,
1037                },
1038                "redirectURL": "",
1039                "headersSize": -1,
1040                "bodySize": r.body_size.map(|b| b as i64).unwrap_or(-1),
1041            }));
1042
1043            let timings_json = serde_json::json!({
1044                "blocked": -1,
1045                "dns": option_f64_ms(timing.dns_end_ms, timing.dns_start_ms),
1046                "connect": option_f64_ms(timing.connect_end_ms, timing.connect_start_ms),
1047                "ssl": option_f64_ms(timing.connect_end_ms, timing.tls_start_ms),
1048                "send": -1,
1049                "wait": option_f64_ms(timing.response_start_ms, timing.connect_end_ms),
1050                "receive": option_f64_ms(timing.response_end_ms, timing.response_start_ms),
1051            });
1052
1053            har_entries.push(serde_json::json!({
1054                "startedDateTime": format!("{}", chrono::DateTime::from_timestamp_millis(req.timestamp as i64).unwrap_or_else(Utc::now)),
1055                "time": 0,
1056                "request": request_json,
1057                "response": response_json,
1058                "cache": {},
1059                "timings": timings_json,
1060            }));
1061        }
1062
1063        let har = serde_json::json!({
1064            "log": {
1065                "version": "1.2",
1066                "creator": {
1067                    "name": "runtime_foxdriver",
1068                    "version": env!("CARGO_PKG_VERSION"),
1069                },
1070                "pages": [{
1071                    "startedDateTime": format!("{}", started),
1072                    "id": "page_1",
1073                    "title": page_title.unwrap_or("unknown"),
1074                    "pageTimings": { "onContentLoad": -1, "onLoad": -1 },
1075                }],
1076                "entries": har_entries,
1077            }
1078        });
1079
1080        tokio::fs::write(path, serde_json::to_string_pretty(&har)?).await?;
1081        Ok(())
1082    }
1083
1084    // ------------------------------------------------------------------
1085    // Ingestion helpers (called by Page::start_network_log handler)
1086    // ------------------------------------------------------------------
1087
1088    pub(crate) async fn ingest_before_request_sent(&self, evt: &BeforeRequestSent) {
1089        let mut inner = self.inner.write().await;
1090        inner.metrics.requests_received += 1;
1091
1092        let req = build_request(evt);
1093        let resp = inner.pending_responses.remove(&req.id);
1094        let err = inner.pending_errors.remove(&req.id);
1095
1096        let entry = Arc::new(NetworkEntry {
1097            request: req,
1098            response: resp,
1099            error: err,
1100        });
1101        inner.push_entry(entry);
1102    }
1103
1104    pub(crate) async fn ingest_response_completed(&self, evt: &ResponseCompleted) {
1105        let mut inner = self.inner.write().await;
1106        inner.metrics.responses_received += 1;
1107
1108        let id = evt.params.base_parameters.request.request.inner().clone();
1109        let resp = build_response(evt);
1110
1111        if let Some(idx) = inner.by_id.get(&id).copied() {
1112            let mut new_entry = (*inner.entries[idx]).clone();
1113            if new_entry.response.is_some() {
1114                inner.metrics.duplicate_responses += 1;
1115            }
1116            new_entry.response = Some(resp);
1117            let new_arc = Arc::new(new_entry);
1118            inner.entries[idx] = new_arc.clone();
1119            let _ = inner.tx.send(new_arc);
1120        } else {
1121            while inner.max_pending > 0 && inner.pending_responses.len() >= inner.max_pending {
1122                if let Some(k) = inner.pending_responses.keys().next().cloned() {
1123                    inner.pending_responses.remove(&k);
1124                    inner.metrics.pending_responses_dropped += 1;
1125                }
1126            }
1127            inner.pending_responses.insert(id, resp);
1128        }
1129    }
1130
1131    pub(crate) async fn ingest_fetch_error(&self, evt: &FetchError) {
1132        let mut inner = self.inner.write().await;
1133        inner.metrics.errors_received += 1;
1134
1135        let id = evt.params.base_parameters.request.request.inner().clone();
1136        let err = CapturedError {
1137            id: id.clone(),
1138            // The request's own URL, not `navigation`. `navigation` is the
1139            // top-level document URL and is None for subresource requests
1140            // (scripts, images, XHR), so reading it there produced an EMPTY
1141            // error URL for every failed subresource - the operator could not
1142            // tell which resource failed. `request.url` is always the actual
1143            // requested URL, matching build_request's success-path source.
1144            url: evt.params.base_parameters.request.url.clone(),
1145            error_text: evt.params.error_text.clone(),
1146        };
1147
1148        if let Some(idx) = inner.by_id.get(&id).copied() {
1149            let mut new_entry = (*inner.entries[idx]).clone();
1150            if new_entry.error.is_some() {
1151                inner.metrics.duplicate_errors += 1;
1152            }
1153            new_entry.error = Some(err);
1154            let new_arc = Arc::new(new_entry);
1155            inner.entries[idx] = new_arc.clone();
1156            let _ = inner.tx.send(new_arc);
1157        } else {
1158            while inner.max_pending > 0 && inner.pending_errors.len() >= inner.max_pending {
1159                if let Some(k) = inner.pending_errors.keys().next().cloned() {
1160                    inner.pending_errors.remove(&k);
1161                    inner.metrics.pending_errors_dropped += 1;
1162                }
1163            }
1164            inner.pending_errors.insert(id, err);
1165        }
1166    }
1167}
1168
1169impl Default for NetworkLog {
1170    fn default() -> Self {
1171        Self::new()
1172    }
1173}
1174
1175fn option_f64_ms(end: Option<f64>, start: Option<f64>) -> f64 {
1176    match (end, start) {
1177        (Some(e), Some(s)) => (e - s).max(0.0),
1178        _ => -1.0,
1179    }
1180}
1181
1182// ------------------------------------------------------------------
1183// Build helpers (BiDi events -> our structs)
1184// ------------------------------------------------------------------
1185
1186fn build_request(evt: &BeforeRequestSent) -> CapturedRequest {
1187    let bp = &evt.params.base_parameters;
1188    let id = bp.request.request.inner().clone();
1189    let url = bp.request.url.clone();
1190    let headers = bp
1191        .request
1192        .headers
1193        .iter()
1194        .map(CapturedHeader::from)
1195        .collect();
1196    let post_data = None; // BiDi doesn't expose body in beforeRequestSent
1197    let timestamp = bp.timestamp;
1198    let destination = bp.request.destination.clone();
1199    let initiator_type = evt
1200        .params
1201        .initiator
1202        .as_ref()
1203        .and_then(|i| i.r#type.as_ref().map(|t| format!("{:?}", t).to_lowercase()));
1204    let timing = CapturedTiming::from(&bp.request.timings);
1205    let cookies = bp
1206        .request
1207        .cookies
1208        .iter()
1209        .map(CapturedCookie::from)
1210        .collect();
1211
1212    CapturedRequest {
1213        id,
1214        context: bp.context.as_ref().map(|c| c.inner().to_string()),
1215        method: bp.request.method.clone(),
1216        url,
1217        headers,
1218        post_data,
1219        timestamp,
1220        destination,
1221        initiator_type,
1222        timing,
1223        cookies,
1224    }
1225}
1226
1227fn build_response(evt: &ResponseCompleted) -> CapturedResponse {
1228    let bp = &evt.params.base_parameters;
1229    let resp = &evt.params.response;
1230    let id = bp.request.request.inner().clone();
1231    let url = resp.url.clone();
1232    let headers = resp.headers.iter().map(CapturedHeader::from).collect();
1233    let protocol = resp.protocol.clone();
1234    let status = resp.status as u16;
1235    let status_text = resp.status_text.clone();
1236    let mime_type = resp.mime_type.clone();
1237    let body_size = resp.body_size;
1238    let from_cache = resp.from_cache;
1239
1240    CapturedResponse {
1241        id,
1242        url,
1243        protocol,
1244        status,
1245        status_text,
1246        headers,
1247        mime_type,
1248        body_size,
1249        from_cache,
1250    }
1251}
1252
1253// ------------------------------------------------------------------
1254// Handler factory (used by browser.rs)
1255// ------------------------------------------------------------------
1256
1257pub fn make_network_handler(
1258    log: NetworkLog,
1259) -> impl FnMut(
1260    rustenium_bidi_definitions::Event,
1261) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>> {
1262    use rustenium_bidi_definitions::network::events::NetworkEvent;
1263    move |evt| {
1264        let log = log.clone();
1265        Box::pin(async move {
1266            if let rustenium_bidi_definitions::Event::Network(nev) = evt {
1267                match nev {
1268                    NetworkEvent::BeforeRequestSent(evt) => {
1269                        log.ingest_before_request_sent(&evt).await
1270                    }
1271                    NetworkEvent::ResponseCompleted(evt) => {
1272                        log.ingest_response_completed(&evt).await
1273                    }
1274                    NetworkEvent::FetchError(evt) => log.ingest_fetch_error(&evt).await,
1275                    _ => {}
1276                }
1277            }
1278        })
1279    }
1280}
1281
1282// ------------------------------------------------------------------
1283// Tests
1284// ------------------------------------------------------------------
1285
1286#[cfg(test)]
1287mod tests {
1288    use super::*;
1289
1290    // ---------- helpers ----------
1291
1292    fn make_request(id: &str, method: &str, url: &str) -> CapturedRequest {
1293        CapturedRequest {
1294            id: id.into(),
1295            context: None,
1296            method: method.into(),
1297            url: url.into(),
1298            headers: vec![],
1299            post_data: None,
1300            timestamp: 0,
1301            destination: "document".into(),
1302            initiator_type: None,
1303            timing: CapturedTiming::default(),
1304            cookies: vec![],
1305        }
1306    }
1307
1308    fn make_request_with_headers(
1309        id: &str,
1310        method: &str,
1311        url: &str,
1312        headers: Vec<CapturedHeader>,
1313    ) -> CapturedRequest {
1314        CapturedRequest {
1315            id: id.into(),
1316            context: None,
1317            method: method.into(),
1318            url: url.into(),
1319            headers,
1320            post_data: None,
1321            timestamp: 0,
1322            destination: "document".into(),
1323            initiator_type: None,
1324            timing: CapturedTiming::default(),
1325            cookies: vec![],
1326        }
1327    }
1328
1329    fn make_response(id: &str, status: u16, url: &str) -> CapturedResponse {
1330        CapturedResponse {
1331            id: id.into(),
1332            url: url.into(),
1333            protocol: "h2".into(),
1334            status,
1335            status_text: "OK".into(),
1336            headers: vec![],
1337            mime_type: "application/json".into(),
1338            body_size: Some(100),
1339            from_cache: false,
1340        }
1341    }
1342
1343    fn make_error(id: &str, url: &str, text: &str) -> CapturedError {
1344        CapturedError {
1345            id: id.into(),
1346            url: url.into(),
1347            error_text: text.into(),
1348        }
1349    }
1350
1351    async fn push_request(log: &NetworkLog, req: CapturedRequest) {
1352        let entry = Arc::new(NetworkEntry {
1353            request: req,
1354            response: None,
1355            error: None,
1356        });
1357        let mut inner = log.inner.write().await;
1358        inner.push_entry(entry);
1359    }
1360
1361    async fn push_entry(
1362        log: &NetworkLog,
1363        req: CapturedRequest,
1364        resp: Option<CapturedResponse>,
1365        err: Option<CapturedError>,
1366    ) {
1367        let entry = Arc::new(NetworkEntry {
1368            request: req,
1369            response: resp,
1370            error: err,
1371        });
1372        let mut inner = log.inner.write().await;
1373        inner.push_entry(entry);
1374    }
1375
1376    // ---------- basic lifecycle ----------
1377
1378    #[tokio::test]
1379    async fn test_new_log_is_empty() {
1380        let log = NetworkLog::new();
1381        assert!(log.is_empty().await);
1382        assert_eq!(log.len().await, 0);
1383    }
1384
1385    #[tokio::test]
1386    async fn test_with_limits() {
1387        let log = NetworkLog::with_limits(10, 5);
1388        let m = log.metrics().await;
1389        assert_eq!(m.max_entries, 10);
1390    }
1391
1392    #[tokio::test]
1393    async fn test_clear() {
1394        let log = NetworkLog::new();
1395        push_request(&log, make_request("1", "GET", "https://example.com")).await;
1396        assert_eq!(log.len().await, 1);
1397        log.clear().await;
1398        assert!(log.is_empty().await);
1399    }
1400
1401    #[tokio::test]
1402    async fn test_entries_returns_all() {
1403        let log = NetworkLog::new();
1404        push_request(&log, make_request("1", "GET", "https://a.com")).await;
1405        push_request(&log, make_request("2", "POST", "https://b.com")).await;
1406        let entries = log.entries().await;
1407        assert_eq!(entries.len(), 2);
1408        assert_eq!(entries[0].request.id, "1");
1409        assert_eq!(entries[1].request.id, "2");
1410    }
1411
1412    #[tokio::test]
1413    async fn test_first_last_nth() {
1414        let log = NetworkLog::new();
1415        push_request(&log, make_request("1", "GET", "https://a.com")).await;
1416        push_request(&log, make_request("2", "POST", "https://b.com")).await;
1417        push_request(&log, make_request("3", "PUT", "https://c.com")).await;
1418
1419        assert_eq!(log.first().await.unwrap().request.id, "1");
1420        assert_eq!(log.last().await.unwrap().request.id, "3");
1421        assert_eq!(log.nth(0).await.unwrap().request.id, "1");
1422        assert_eq!(log.nth(1).await.unwrap().request.id, "2");
1423        assert_eq!(log.nth(2).await.unwrap().request.id, "3");
1424        assert!(log.nth(99).await.is_none());
1425    }
1426
1427    // ---------- filtering ----------
1428
1429    #[tokio::test]
1430    async fn test_filter_method() {
1431        let log = NetworkLog::new();
1432        push_request(&log, make_request("1", "GET", "https://example.com")).await;
1433        push_request(&log, make_request("2", "POST", "https://example.com")).await;
1434        let get = log.filter(Filter::new().method("GET")).await;
1435        assert_eq!(get.len(), 1);
1436        assert_eq!(get[0].request.method, "GET");
1437    }
1438
1439    #[tokio::test]
1440    async fn test_filter_status_range() {
1441        let log = NetworkLog::new();
1442        push_entry(
1443            &log,
1444            make_request("1", "GET", "https://a.com"),
1445            Some(make_response("1", 200, "https://a.com")),
1446            None,
1447        )
1448        .await;
1449        push_entry(
1450            &log,
1451            make_request("2", "GET", "https://b.com"),
1452            Some(make_response("2", 404, "https://b.com")),
1453            None,
1454        )
1455        .await;
1456        push_entry(&log, make_request("3", "GET", "https://c.com"), None, None).await;
1457        let f = log.filter(Filter::new().status_range(200..=299)).await;
1458        assert_eq!(f.len(), 1);
1459        assert_eq!(f[0].request.id, "1");
1460    }
1461
1462    #[tokio::test]
1463    async fn test_filter_url_contains() {
1464        let log = NetworkLog::new();
1465        push_request(&log, make_request("1", "GET", "https://api.example.com/v1")).await;
1466        push_request(&log, make_request("2", "GET", "https://other.com")).await;
1467        let f = log.filter(Filter::new().url_contains("api")).await;
1468        assert_eq!(f.len(), 1);
1469        assert_eq!(f[0].request.id, "1");
1470    }
1471
1472    #[tokio::test]
1473    async fn test_filter_url_regex() {
1474        let log = NetworkLog::new();
1475        push_request(&log, make_request("1", "GET", "https://api.example.com/v1")).await;
1476        push_request(&log, make_request("2", "GET", "https://other.com")).await;
1477        let f = log
1478            .filter(Filter::new().url_regex(r"api\.\w+\.com").unwrap())
1479            .await;
1480        assert_eq!(f.len(), 1);
1481    }
1482
1483    #[tokio::test]
1484    async fn test_filter_header() {
1485        let log = NetworkLog::new();
1486        let h1 = vec![CapturedHeader {
1487            name: "Authorization".into(),
1488            value: "Bearer abc".into(),
1489        }];
1490        let h2 = vec![CapturedHeader {
1491            name: "Content-Type".into(),
1492            value: "application/json".into(),
1493        }];
1494        push_request(
1495            &log,
1496            make_request_with_headers("1", "GET", "https://a.com", h1),
1497        )
1498        .await;
1499        push_request(
1500            &log,
1501            make_request_with_headers("2", "GET", "https://b.com", h2),
1502        )
1503        .await;
1504        let f = log
1505            .filter(Filter::new().header("authorization", "bearer"))
1506            .await;
1507        assert_eq!(f.len(), 1);
1508        assert_eq!(f[0].request.id, "1");
1509    }
1510
1511    #[tokio::test]
1512    async fn test_filter_destination() {
1513        let log = NetworkLog::new();
1514        let mut r1 = make_request("1", "GET", "https://a.com");
1515        r1.destination = "image".into();
1516        let mut r2 = make_request("2", "GET", "https://b.com");
1517        r2.destination = "document".into();
1518        push_request(&log, r1).await;
1519        push_request(&log, r2).await;
1520        let f = log.filter(Filter::new().destination("image")).await;
1521        assert_eq!(f.len(), 1);
1522        assert_eq!(f[0].request.id, "1");
1523    }
1524
1525    #[tokio::test]
1526    async fn test_filter_with_response() {
1527        let log = NetworkLog::new();
1528        push_entry(
1529            &log,
1530            make_request("1", "GET", "https://a.com"),
1531            Some(make_response("1", 200, "https://a.com")),
1532            None,
1533        )
1534        .await;
1535        push_request(&log, make_request("2", "GET", "https://b.com")).await;
1536        let f = log.filter(Filter::new().with_response()).await;
1537        assert_eq!(f.len(), 1);
1538        assert_eq!(f[0].request.id, "1");
1539    }
1540
1541    #[tokio::test]
1542    async fn test_filter_without_response() {
1543        let log = NetworkLog::new();
1544        push_entry(
1545            &log,
1546            make_request("1", "GET", "https://a.com"),
1547            Some(make_response("1", 200, "https://a.com")),
1548            None,
1549        )
1550        .await;
1551        push_request(&log, make_request("2", "GET", "https://b.com")).await;
1552        let f = log.filter(Filter::new().without_response()).await;
1553        assert_eq!(f.len(), 1);
1554        assert_eq!(f[0].request.id, "2");
1555    }
1556
1557    #[tokio::test]
1558    async fn test_filter_with_error() {
1559        let log = NetworkLog::new();
1560        push_entry(
1561            &log,
1562            make_request("1", "GET", "https://a.com"),
1563            None,
1564            Some(make_error("1", "https://a.com", "net::ERR_FAILED")),
1565        )
1566        .await;
1567        push_request(&log, make_request("2", "GET", "https://b.com")).await;
1568        let f = log.filter(Filter::new().with_error()).await;
1569        assert_eq!(f.len(), 1);
1570        assert_eq!(f[0].request.id, "1");
1571    }
1572
1573    #[tokio::test]
1574    async fn test_filter_composition() {
1575        let log = NetworkLog::new();
1576        push_entry(
1577            &log,
1578            make_request("1", "POST", "https://api.example.com/login"),
1579            Some(make_response("1", 200, "https://api.example.com/login")),
1580            None,
1581        )
1582        .await;
1583        push_request(
1584            &log,
1585            make_request("2", "GET", "https://api.example.com/login"),
1586        )
1587        .await;
1588        let f = log
1589            .filter(
1590                Filter::new()
1591                    .method("POST")
1592                    .url_contains("login")
1593                    .with_response(),
1594            )
1595            .await;
1596        assert_eq!(f.len(), 1);
1597        assert_eq!(f[0].request.id, "1");
1598    }
1599
1600    #[tokio::test]
1601    async fn test_count() {
1602        let log = NetworkLog::new();
1603        push_request(&log, make_request("1", "GET", "https://a.com")).await;
1604        push_request(&log, make_request("2", "POST", "https://b.com")).await;
1605        assert_eq!(log.count(Filter::new().method("GET")).await, 1);
1606        assert_eq!(log.count(Filter::new()).await, 2);
1607    }
1608
1609    // ---------- completed ----------
1610
1611    #[tokio::test]
1612    async fn test_completed() {
1613        let log = NetworkLog::new();
1614        push_entry(
1615            &log,
1616            make_request("1", "GET", "https://a.com"),
1617            Some(make_response("1", 200, "https://a.com")),
1618            None,
1619        )
1620        .await;
1621        push_request(&log, make_request("2", "GET", "https://b.com")).await;
1622        push_entry(
1623            &log,
1624            make_request("3", "GET", "https://c.com"),
1625            None,
1626            Some(make_error("3", "https://c.com", "err")),
1627        )
1628        .await;
1629        let c = log.completed().await;
1630        assert_eq!(c.len(), 2);
1631    }
1632
1633    // ---------- find_by_url / regex ----------
1634
1635    #[tokio::test]
1636    async fn test_find_by_url() {
1637        let log = NetworkLog::new();
1638        push_request(&log, make_request("1", "GET", "https://api.example.com")).await;
1639        push_request(&log, make_request("2", "GET", "https://other.com")).await;
1640        assert_eq!(log.find_by_url("api").await.unwrap().request.id, "1");
1641        assert!(log.find_by_url("notfound").await.is_none());
1642    }
1643
1644    #[tokio::test]
1645    async fn test_find_by_url_regex() {
1646        let log = NetworkLog::new();
1647        push_request(&log, make_request("1", "GET", "https://api.example.com")).await;
1648        push_request(&log, make_request("2", "GET", "https://other.com")).await;
1649        let re = regex::Regex::new(r"api\.\w+\.com").unwrap();
1650        assert_eq!(log.find_by_url_regex(&re).await.unwrap().request.id, "1");
1651    }
1652
1653    // ---------- aggregation ----------
1654
1655    #[tokio::test]
1656    async fn test_endpoints() {
1657        let log = NetworkLog::new();
1658        push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
1659        push_request(&log, make_request("2", "GET", "https://a.com/y")).await;
1660        push_request(&log, make_request("3", "GET", "https://b.com/z")).await;
1661        let ep = log.endpoints().await;
1662        assert_eq!(ep.len(), 3);
1663    }
1664
1665    #[tokio::test]
1666    async fn test_hostnames() {
1667        let log = NetworkLog::new();
1668        push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
1669        push_request(&log, make_request("2", "GET", "https://a.com/y")).await;
1670        push_request(&log, make_request("3", "GET", "https://b.com/z")).await;
1671        let h = log.hostnames().await;
1672        assert_eq!(h.len(), 2);
1673        assert!(h.contains(&"a.com".into()));
1674        assert!(h.contains(&"b.com".into()));
1675    }
1676
1677    #[tokio::test]
1678    async fn test_distinct_methods() {
1679        let log = NetworkLog::new();
1680        push_request(&log, make_request("1", "GET", "https://a.com")).await;
1681        push_request(&log, make_request("2", "POST", "https://b.com")).await;
1682        push_request(&log, make_request("3", "GET", "https://c.com")).await;
1683        let m = log.distinct_methods().await;
1684        assert_eq!(m, vec!["GET", "POST"]);
1685    }
1686
1687    #[tokio::test]
1688    async fn test_distinct_statuses() {
1689        let log = NetworkLog::new();
1690        push_entry(
1691            &log,
1692            make_request("1", "GET", "https://a.com"),
1693            Some(make_response("1", 200, "https://a.com")),
1694            None,
1695        )
1696        .await;
1697        push_entry(
1698            &log,
1699            make_request("2", "GET", "https://b.com"),
1700            Some(make_response("2", 404, "https://b.com")),
1701            None,
1702        )
1703        .await;
1704        push_entry(
1705            &log,
1706            make_request("3", "GET", "https://c.com"),
1707            Some(make_response("3", 200, "https://c.com")),
1708            None,
1709        )
1710        .await;
1711        let s = log.distinct_statuses().await;
1712        assert_eq!(s, vec![200, 404]);
1713    }
1714
1715    #[tokio::test]
1716    async fn test_total_bytes() {
1717        let log = NetworkLog::new();
1718        let mut r1 = make_request("1", "POST", "https://a.com");
1719        r1.post_data = Some("hello".into());
1720        push_entry(
1721            &log,
1722            r1,
1723            Some(make_response("1", 200, "https://a.com")),
1724            None,
1725        )
1726        .await;
1727        let mut r2 = make_request("2", "POST", "https://b.com");
1728        r2.post_data = Some("world!!".into());
1729        push_entry(
1730            &log,
1731            r2,
1732            Some(make_response("2", 200, "https://b.com")),
1733            None,
1734        )
1735        .await;
1736        assert_eq!(log.total_bytes_in().await, 200);
1737        assert_eq!(log.total_bytes_out().await, 12); // "hello" + "world!!"
1738    }
1739
1740    // ---------- entry helpers ----------
1741
1742    #[tokio::test]
1743    async fn test_entry_status_and_final_url() {
1744        let entry = NetworkEntry {
1745            request: make_request("1", "GET", "https://a.com"),
1746            response: Some(make_response("1", 301, "https://b.com")),
1747            error: None,
1748        };
1749        assert_eq!(entry.status(), Some(301));
1750        assert_eq!(entry.final_url(), "https://b.com");
1751        assert!(entry.has_response());
1752        assert!(!entry.is_error());
1753    }
1754
1755    #[tokio::test]
1756    async fn test_entry_request_header() {
1757        let req = make_request_with_headers(
1758            "1",
1759            "GET",
1760            "https://a.com",
1761            vec![CapturedHeader {
1762                name: "X-Custom".into(),
1763                value: "123".into(),
1764            }],
1765        );
1766        let entry = NetworkEntry {
1767            request: req,
1768            response: None,
1769            error: None,
1770        };
1771        assert_eq!(entry.request_header("x-custom"), Some("123"));
1772        assert_eq!(entry.request_header("missing"), None);
1773    }
1774
1775    #[tokio::test]
1776    async fn test_entry_response_header() {
1777        let req = make_request("1", "GET", "https://a.com");
1778        let resp = CapturedResponse {
1779            id: "1".into(),
1780            url: "https://a.com".into(),
1781            protocol: "h2".into(),
1782            status: 200,
1783            status_text: "OK".into(),
1784            headers: vec![CapturedHeader {
1785                name: "Content-Type".into(),
1786                value: "application/json".into(),
1787            }],
1788            mime_type: "application/json".into(),
1789            body_size: Some(10),
1790            from_cache: false,
1791        };
1792        let entry = NetworkEntry {
1793            request: req,
1794            response: Some(resp),
1795            error: None,
1796        };
1797        assert_eq!(
1798            entry.response_header("content-type"),
1799            Some("application/json")
1800        );
1801        assert_eq!(entry.response_header("missing"), None);
1802    }
1803
1804    #[tokio::test]
1805    async fn test_entry_to_curl() {
1806        let req = CapturedRequest {
1807            id: "1".into(),
1808            context: None,
1809            method: "POST".into(),
1810            url: "https://api.example.com/login".into(),
1811            headers: vec![
1812                CapturedHeader {
1813                    name: "Content-Type".into(),
1814                    value: "application/json".into(),
1815                },
1816                CapturedHeader {
1817                    name: "Host".into(),
1818                    value: "api.example.com".into(),
1819                },
1820            ],
1821            post_data: Some(r#"{"user":"admin"}"#.into()),
1822            timestamp: 0,
1823            destination: "document".into(),
1824            initiator_type: None,
1825            timing: CapturedTiming::default(),
1826            cookies: vec![],
1827        };
1828        let entry = NetworkEntry {
1829            request: req,
1830            response: None,
1831            error: None,
1832        };
1833        let curl = entry.to_curl();
1834        assert!(curl.starts_with("curl -X 'POST'"));
1835        assert!(curl.contains("-H 'Content-Type: application/json'"));
1836        assert!(!curl.contains("Host")); // stripped
1837        assert!(curl.contains(r#"-d '{"user":"admin"}'"#));
1838        assert!(curl.contains("'https://api.example.com/login'"));
1839    }
1840
1841    // ---------- request helpers ----------
1842
1843    #[tokio::test]
1844    async fn test_request_json_body() {
1845        let mut req = make_request("1", "POST", "https://a.com");
1846        req.post_data = Some(r#"{"key":"value"}"#.into());
1847        assert_eq!(req.json_body(), Some(serde_json::json!({"key": "value"})));
1848    }
1849
1850    #[tokio::test]
1851    async fn test_request_json_body_invalid() {
1852        let mut req = make_request("1", "POST", "https://a.com");
1853        req.post_data = Some("not json".into());
1854        assert!(req.json_body().is_none());
1855    }
1856
1857    #[tokio::test]
1858    async fn test_request_query_params() {
1859        let req = make_request("1", "GET", "https://a.com?foo=bar&baz=qux");
1860        let params = req.query_params().unwrap();
1861        assert_eq!(params.len(), 2);
1862        assert!(params.contains(&("foo".into(), "bar".into())));
1863        assert!(params.contains(&("baz".into(), "qux".into())));
1864    }
1865
1866    #[tokio::test]
1867    async fn test_request_query_params_malformed_url() {
1868        let mut req = make_request("1", "GET", "https://a.com?foo=bar");
1869        req.url = "not a valid url".into();
1870        assert!(
1871            req.query_params().is_err(),
1872            "query_params must report a parse failure, not silently return an empty vec"
1873        );
1874    }
1875
1876    // ---------- secret scanning ----------
1877
1878    // ---------- internal requests ----------
1879
1880    // ---------- max entries eviction ----------
1881
1882    #[tokio::test]
1883    async fn test_max_entries_eviction() {
1884        let log = NetworkLog::with_limits(4, 10);
1885        for i in 0..6 {
1886            push_request(
1887                &log,
1888                make_request(&format!("{}", i), "GET", "https://example.com"),
1889            )
1890            .await;
1891        }
1892        // After 6 inserts with max 4, eviction triggers when len >= 4
1893        // First eviction at insert 4: remove 2, leaving [2,3]
1894        // Then insert 4: [2,3,4]
1895        // Insert 5: [2,3,4,5]
1896        let entries = log.entries().await;
1897        let ids: Vec<_> = entries.iter().map(|e| e.request.id.clone()).collect();
1898        assert_eq!(ids, vec!["2", "3", "4", "5"]);
1899
1900        let m = log.metrics().await;
1901        assert_eq!(m.entries_evicted, 2);
1902    }
1903
1904    // ---------- broadcast ----------
1905
1906    #[tokio::test]
1907    async fn test_broadcast_receives_entries() {
1908        let log = NetworkLog::new();
1909        let mut rx = log.subscribe().await;
1910        push_request(&log, make_request("1", "GET", "https://example.com")).await;
1911        let received = rx.recv().await.unwrap();
1912        assert_eq!(received.request.id, "1");
1913    }
1914
1915    #[tokio::test]
1916    async fn test_broadcast_multiple_receivers() {
1917        let log = NetworkLog::new();
1918        let mut rx1 = log.subscribe().await;
1919        let mut rx2 = log.subscribe().await;
1920        push_request(&log, make_request("1", "GET", "https://example.com")).await;
1921        assert_eq!(rx1.recv().await.unwrap().request.id, "1");
1922        assert_eq!(rx2.recv().await.unwrap().request.id, "1");
1923    }
1924
1925    // ---------- metrics ----------
1926
1927    #[tokio::test]
1928    async fn test_metrics_requests_received() {
1929        let log = NetworkLog::new();
1930        push_request(&log, make_request("1", "GET", "https://a.com")).await;
1931        push_request(&log, make_request("2", "GET", "https://b.com")).await;
1932        let m = log.metrics().await;
1933        assert_eq!(m.requests_received, 0); // push_request bypasses metrics
1934        assert_eq!(m.max_entries, 50_000);
1935    }
1936
1937    // ---------- serialization ----------
1938
1939    #[tokio::test]
1940    async fn test_network_entry_serialize_roundtrip() {
1941        let entry = NetworkEntry {
1942            request: make_request("1", "GET", "https://example.com"),
1943            response: Some(make_response("1", 200, "https://example.com")),
1944            error: None,
1945        };
1946        let json = serde_json::to_string(&entry).unwrap();
1947        let de: NetworkEntry = serde_json::from_str(&json).unwrap();
1948        assert_eq!(de.request.id, entry.request.id);
1949        assert_eq!(de.status(), Some(200));
1950    }
1951
1952    // ---------- clone semantics ----------
1953
1954    #[tokio::test]
1955    async fn test_log_clone_shares_state() {
1956        let log1 = NetworkLog::new();
1957        let log2 = log1.clone();
1958        push_request(&log1, make_request("1", "GET", "https://example.com")).await;
1959        assert_eq!(log2.len().await, 1);
1960    }
1961
1962    // ---------- timing helpers ----------
1963
1964    #[tokio::test]
1965    async fn test_non_neg() {
1966        assert_eq!(non_neg(5.0), Some(5.0));
1967        assert_eq!(non_neg(-1.0), None);
1968        assert_eq!(non_neg(0.0), Some(0.0));
1969    }
1970
1971    #[tokio::test]
1972    async fn test_option_f64_ms() {
1973        assert_eq!(option_f64_ms(Some(10.0), Some(3.0)), 7.0);
1974        assert_eq!(option_f64_ms(Some(3.0), Some(10.0)), 0.0); // clamped
1975        assert_eq!(option_f64_ms(None, Some(3.0)), -1.0);
1976    }
1977
1978    // ---------- edge cases ----------
1979
1980    #[tokio::test]
1981    async fn test_empty_filter_matches_all() {
1982        let log = NetworkLog::new();
1983        push_request(&log, make_request("1", "GET", "https://a.com")).await;
1984        push_request(&log, make_request("2", "POST", "https://b.com")).await;
1985        assert_eq!(log.filter(Filter::new()).await.len(), 2);
1986        assert_eq!(log.count(Filter::new()).await, 2);
1987    }
1988
1989    #[tokio::test]
1990    async fn test_eviction_rebuilds_by_id() {
1991        let log = NetworkLog::with_limits(4, 10);
1992        for i in 0..6 {
1993            push_request(
1994                &log,
1995                make_request(&format!("{}", i), "GET", "https://example.com"),
1996            )
1997            .await;
1998        }
1999        // After eviction, by_id should still be valid
2000        let entries = log.entries().await;
2001        assert_eq!(entries.len(), 4);
2002        for e in &entries {
2003            assert!(log.find_by_url(&e.request.url).await.is_some());
2004        }
2005    }
2006
2007    #[tokio::test]
2008    async fn test_broadcast_dropped_receiver_does_not_panic() {
2009        let log = NetworkLog::new();
2010        {
2011            let _rx = log.subscribe().await;
2012        } // receiver dropped
2013        push_request(&log, make_request("1", "GET", "https://example.com")).await;
2014        // should not panic
2015    }
2016
2017    #[tokio::test]
2018    async fn test_save_to_json_empty() {
2019        let log = NetworkLog::new();
2020        let path = std::path::Path::new("/tmp/foxdriver_network_empty.json");
2021        log.save_to_json(path).await.unwrap();
2022        let content = std::fs::read_to_string(path).unwrap();
2023        assert_eq!(content.trim(), "[]");
2024    }
2025
2026    #[tokio::test]
2027    async fn test_save_as_har_empty() {
2028        let log = NetworkLog::new();
2029        let path = std::path::Path::new("/tmp/foxdriver_network_empty.har");
2030        log.save_as_har(path, Some("test")).await.unwrap();
2031        let content = std::fs::read_to_string(path).unwrap();
2032        assert!(content.contains("1.2"));
2033        assert!(content.contains("runtime_foxdriver"));
2034    }
2035
2036    #[tokio::test]
2037    async fn test_captured_timing_default() {
2038        let t = CapturedTiming::default();
2039        assert!(t.dns_start_ms.is_none());
2040        assert!(t.response_end_ms.is_none());
2041    }
2042
2043    #[tokio::test]
2044    async fn test_network_entry_no_response_final_url() {
2045        let entry = NetworkEntry {
2046            request: make_request("1", "GET", "https://a.com"),
2047            response: None,
2048            error: None,
2049        };
2050        assert_eq!(entry.final_url(), "https://a.com");
2051        assert_eq!(entry.status(), None);
2052        assert!(!entry.has_response());
2053        assert!(!entry.is_error());
2054    }
2055
2056    #[tokio::test]
2057    async fn test_distinct_methods_empty() {
2058        let log = NetworkLog::new();
2059        assert!(log.distinct_methods().await.is_empty());
2060    }
2061
2062    #[tokio::test]
2063    async fn test_distinct_statuses_empty() {
2064        let log = NetworkLog::new();
2065        assert!(log.distinct_statuses().await.is_empty());
2066    }
2067
2068    #[tokio::test]
2069    async fn test_hostnames_malformed_url() {
2070        let log = NetworkLog::new();
2071        push_request(&log, make_request("1", "GET", "not-a-url")).await;
2072        assert!(log.hostnames().await.is_empty());
2073    }
2074
2075    #[tokio::test]
2076    async fn test_total_bytes_no_response() {
2077        let log = NetworkLog::new();
2078        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2079        assert_eq!(log.total_bytes_in().await, 0);
2080    }
2081
2082    // ---------- contains_id / remove_by_id ----------
2083
2084    #[tokio::test]
2085    async fn test_contains_id() {
2086        let log = NetworkLog::new();
2087        push_request(&log, make_request("abc", "GET", "https://a.com")).await;
2088        assert!(log.contains_id("abc").await);
2089        assert!(!log.contains_id("xyz").await);
2090    }
2091
2092    #[tokio::test]
2093    async fn test_remove_by_id() {
2094        let log = NetworkLog::new();
2095        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2096        push_request(&log, make_request("2", "GET", "https://b.com")).await;
2097        push_request(&log, make_request("3", "GET", "https://c.com")).await;
2098
2099        let removed = log.remove_by_id("2").await;
2100        assert_eq!(removed.unwrap().request.id, "2");
2101        assert_eq!(log.len().await, 2);
2102        assert!(!log.contains_id("2").await);
2103
2104        // Verify remaining indices are valid
2105        assert!(log.contains_id("1").await);
2106        assert!(log.contains_id("3").await);
2107        assert_eq!(log.nth(0).await.unwrap().request.id, "1");
2108        assert_eq!(log.nth(1).await.unwrap().request.id, "3");
2109    }
2110
2111    #[tokio::test]
2112    async fn test_remove_by_id_unknown() {
2113        let log = NetworkLog::new();
2114        assert!(log.remove_by_id("nope").await.is_none());
2115    }
2116
2117    // ---------- wait_for helpers ----------
2118
2119    #[tokio::test]
2120    async fn test_wait_for_url_existing() {
2121        let log = NetworkLog::new();
2122        push_request(&log, make_request("1", "GET", "https://target.com/page")).await;
2123        let found = log
2124            .wait_for_url("target.com", std::time::Duration::from_secs(1))
2125            .await;
2126        assert_eq!(found.unwrap().request.id, "1");
2127    }
2128
2129    #[tokio::test]
2130    async fn test_wait_for_url_future() {
2131        let log = NetworkLog::new();
2132        let log2 = log.clone();
2133        tokio::spawn(async move {
2134            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2135            push_request(&log2, make_request("1", "GET", "https://target.com/page")).await;
2136        });
2137        let found = log
2138            .wait_for_url("target.com", std::time::Duration::from_secs(1))
2139            .await;
2140        assert_eq!(found.unwrap().request.id, "1");
2141    }
2142
2143    #[tokio::test]
2144    async fn test_wait_for_url_timeout() {
2145        let log = NetworkLog::new();
2146        let found = log
2147            .wait_for_url("never", std::time::Duration::from_millis(50))
2148            .await;
2149        assert!(found.is_none());
2150    }
2151
2152    #[tokio::test]
2153    async fn test_wait_for_response_existing() {
2154        let log = NetworkLog::new();
2155        push_entry(
2156            &log,
2157            make_request("1", "GET", "https://a.com"),
2158            Some(make_response("1", 200, "https://a.com")),
2159            None,
2160        )
2161        .await;
2162        let found = log
2163            .wait_for_response("1", std::time::Duration::from_secs(1))
2164            .await;
2165        assert_eq!(found.unwrap().request.id, "1");
2166    }
2167
2168    #[tokio::test]
2169    async fn test_wait_for_response_future() {
2170        let log = NetworkLog::new();
2171        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2172        let log2 = log.clone();
2173        tokio::spawn(async move {
2174            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2175            // Update with response
2176            let mut inner = log2.inner.write().await;
2177            if let Some(idx) = inner.by_id.get("1").copied() {
2178                let mut new_entry = (*inner.entries[idx]).clone();
2179                new_entry.response = Some(make_response("1", 200, "https://a.com"));
2180                let new_arc = Arc::new(new_entry);
2181                inner.entries[idx] = new_arc.clone();
2182                let _ = inner.tx.send(new_arc);
2183            }
2184        });
2185        let found = log
2186            .wait_for_response("1", std::time::Duration::from_secs(1))
2187            .await;
2188        assert_eq!(found.unwrap().request.id, "1");
2189    }
2190
2191    #[tokio::test]
2192    async fn test_wait_for_response_timeout() {
2193        let log = NetworkLog::new();
2194        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2195        let found = log
2196            .wait_for_response("1", std::time::Duration::from_millis(50))
2197            .await;
2198        assert!(found.is_none());
2199    }
2200
2201    // ---------- concurrency stress ----------
2202
2203    #[tokio::test]
2204    async fn test_concurrent_pushes() {
2205        let log = NetworkLog::new();
2206        let mut handles = Vec::new();
2207        for t in 0..10 {
2208            let log = log.clone();
2209            handles.push(tokio::spawn(async move {
2210                for i in 0..100 {
2211                    push_request(
2212                        &log,
2213                        make_request(&format!("{}-{}", t, i), "GET", "https://example.com"),
2214                    )
2215                    .await;
2216                }
2217            }));
2218        }
2219        for h in handles {
2220            h.await.unwrap();
2221        }
2222        assert_eq!(log.len().await, 1000);
2223    }
2224
2225    #[tokio::test]
2226    async fn test_concurrent_read_while_write() {
2227        let log = NetworkLog::new();
2228        let log2 = log.clone();
2229
2230        let writer = tokio::spawn(async move {
2231            for i in 0..500 {
2232                push_request(
2233                    &log,
2234                    make_request(&format!("{}", i), "GET", "https://example.com"),
2235                )
2236                .await;
2237                if i % 50 == 0 {
2238                    tokio::task::yield_now().await;
2239                }
2240            }
2241        });
2242
2243        let reader = tokio::spawn(async move {
2244            let mut last_len = 0;
2245            for _ in 0..50 {
2246                tokio::time::sleep(std::time::Duration::from_millis(1)).await;
2247                let len = log2.len().await;
2248                assert!(len >= last_len); // never decreases
2249                last_len = len;
2250            }
2251        });
2252
2253        let (r1, r2) = tokio::join!(writer, reader);
2254        r1.unwrap();
2255        r2.unwrap();
2256    }
2257
2258    // ---------- export with real data ----------
2259
2260    #[tokio::test]
2261    async fn test_save_to_json_roundtrip() {
2262        let log = NetworkLog::new();
2263        push_entry(
2264            &log,
2265            make_request("1", "GET", "https://example.com"),
2266            Some(make_response("1", 200, "https://example.com")),
2267            None,
2268        )
2269        .await;
2270        let path = std::path::Path::new("/tmp/foxdriver_network_real.json");
2271        log.save_to_json(path).await.unwrap();
2272        let content = std::fs::read_to_string(path).unwrap();
2273        let parsed: Vec<NetworkEntry> = serde_json::from_str(&content).unwrap();
2274        assert_eq!(parsed.len(), 1);
2275        assert_eq!(parsed[0].request.id, "1");
2276        assert_eq!(parsed[0].status(), Some(200));
2277    }
2278
2279    #[tokio::test]
2280    async fn test_save_as_har_with_data() {
2281        let log = NetworkLog::new();
2282        let mut req = make_request("1", "POST", "https://api.example.com/data?key=val");
2283        req.post_data = Some(r#"{"hello":"world"}"#.into());
2284        req.headers = vec![CapturedHeader {
2285            name: "Content-Type".into(),
2286            value: "application/json".into(),
2287        }];
2288        push_entry(
2289            &log,
2290            req,
2291            Some(make_response(
2292                "1",
2293                201,
2294                "https://api.example.com/data?key=val",
2295            )),
2296            None,
2297        )
2298        .await;
2299
2300        let path = std::path::Path::new("/tmp/foxdriver_network_real.har");
2301        log.save_as_har(path, Some("test page")).await.unwrap();
2302        let content = std::fs::read_to_string(path).unwrap();
2303        assert!(content.contains("1.2"));
2304        assert!(content.contains("POST"));
2305        assert!(content.contains("201"));
2306        assert!(content.contains("key=val"));
2307        assert!(content.contains("hello"));
2308        assert!(content.contains("test page"));
2309    }
2310
2311    // ---------- curl edge cases ----------
2312
2313    #[tokio::test]
2314    async fn test_to_curl_no_headers_no_body() {
2315        let req = make_request("1", "GET", "https://example.com");
2316        let entry = NetworkEntry {
2317            request: req,
2318            response: None,
2319            error: None,
2320        };
2321        let curl = entry.to_curl();
2322        assert_eq!(curl, "curl -X 'GET' 'https://example.com'");
2323    }
2324
2325    #[tokio::test]
2326    async fn test_to_curl_special_chars() {
2327        let req = CapturedRequest {
2328            id: "1".into(),
2329            context: None,
2330            method: "POST".into(),
2331            url: "https://example.com?a=1&b=2".into(),
2332            headers: vec![CapturedHeader {
2333                name: "X-Special".into(),
2334                value: "val'ue\".txt".into(),
2335            }],
2336            post_data: Some("data='quoted'".into()),
2337            timestamp: 0,
2338            destination: "document".into(),
2339            initiator_type: None,
2340            timing: CapturedTiming::default(),
2341            cookies: vec![],
2342        };
2343        let entry = NetworkEntry {
2344            request: req,
2345            response: None,
2346            error: None,
2347        };
2348        let curl = entry.to_curl();
2349        assert!(curl.contains("-H 'X-Special: val'\\''ue\".txt'"));
2350        assert!(curl.contains("-d 'data='\\''quoted'\\'''"));
2351    }
2352
2353    // ---------- false positive tests ----------
2354
2355    // ---------- filter boundary tests ----------
2356
2357    #[tokio::test]
2358    async fn test_filter_empty_method_matches_all() {
2359        let log = NetworkLog::new();
2360        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2361        push_request(&log, make_request("2", "POST", "https://b.com")).await;
2362        // Filter::new() without method() should match all
2363        assert_eq!(log.filter(Filter::new()).await.len(), 2);
2364    }
2365
2366    #[tokio::test]
2367    async fn test_filter_status_range_no_matches() {
2368        let log = NetworkLog::new();
2369        push_entry(
2370            &log,
2371            make_request("1", "GET", "https://a.com"),
2372            Some(make_response("1", 200, "https://a.com")),
2373            None,
2374        )
2375        .await;
2376        let f = log.filter(Filter::new().status_range(500..=599)).await;
2377        assert!(f.is_empty());
2378    }
2379
2380    #[tokio::test]
2381    async fn test_filter_url_contains_case_insensitive() {
2382        let log = NetworkLog::new();
2383        push_request(&log, make_request("1", "GET", "https://API.example.com")).await;
2384        let f = log.filter(Filter::new().url_contains("api")).await;
2385        assert_eq!(f.len(), 1);
2386    }
2387
2388    #[tokio::test]
2389    async fn test_filter_destination_case_insensitive() {
2390        let log = NetworkLog::new();
2391        let mut r = make_request("1", "GET", "https://a.com");
2392        r.destination = "IMAGE".into();
2393        push_request(&log, r).await;
2394        let f = log.filter(Filter::new().destination("image")).await;
2395        assert_eq!(f.len(), 1);
2396    }
2397
2398    // ---------- query params edge cases ----------
2399
2400    #[tokio::test]
2401    async fn test_query_params_empty() {
2402        let req = make_request("1", "GET", "https://a.com");
2403        assert!(req.query_params().unwrap().is_empty());
2404    }
2405
2406    #[tokio::test]
2407    async fn test_query_params_url_encoded() {
2408        let req = make_request("1", "GET", "https://a.com?foo=%20bar&baz=qux");
2409        let params = req.query_params().unwrap();
2410        assert!(params.contains(&("foo".into(), " bar".into())));
2411        assert!(params.contains(&("baz".into(), "qux".into())));
2412    }
2413
2414    // ---------- multiple evictions ----------
2415
2416    #[tokio::test]
2417    async fn test_multiple_evictions() {
2418        let log = NetworkLog::with_limits(4, 10);
2419        for i in 0..20 {
2420            push_request(
2421                &log,
2422                make_request(&format!("{}", i), "GET", "https://example.com"),
2423            )
2424            .await;
2425        }
2426        let entries = log.entries().await;
2427        assert_eq!(entries.len(), 4);
2428        let ids: Vec<_> = entries.iter().map(|e| e.request.id.clone()).collect();
2429        assert_eq!(ids, vec!["16", "17", "18", "19"]);
2430
2431        let m = log.metrics().await;
2432        assert_eq!(m.entries_evicted, 16);
2433    }
2434
2435    // ---------- header edge cases ----------
2436
2437    #[tokio::test]
2438    async fn test_request_header_multiple_same_name() {
2439        let req = make_request_with_headers(
2440            "1",
2441            "GET",
2442            "https://a.com",
2443            vec![
2444                CapturedHeader {
2445                    name: "X-Dup".into(),
2446                    value: "first".into(),
2447                },
2448                CapturedHeader {
2449                    name: "X-Dup".into(),
2450                    value: "second".into(),
2451                },
2452            ],
2453        );
2454        assert_eq!(req.request_header("x-dup"), Some("first")); // first match wins
2455    }
2456
2457    #[tokio::test]
2458    async fn test_response_header_no_response() {
2459        let entry = NetworkEntry {
2460            request: make_request("1", "GET", "https://a.com"),
2461            response: None,
2462            error: None,
2463        };
2464        assert_eq!(entry.response_header("anything"), None);
2465    }
2466
2467    // ---------- timing edge cases ----------
2468
2469    #[tokio::test]
2470    async fn test_option_f64_ms_both_none() {
2471        assert_eq!(option_f64_ms(None, None), -1.0);
2472    }
2473
2474    #[tokio::test]
2475    async fn test_option_f64_ms_end_none() {
2476        assert_eq!(option_f64_ms(None, Some(5.0)), -1.0);
2477    }
2478
2479    // ---------- ingestion pipeline ----------
2480
2481    fn make_bidi_request_data(
2482        id: &str,
2483        url: &str,
2484        method: &str,
2485    ) -> rustenium_bidi_definitions::network::types::RequestData {
2486        use rustenium_bidi_definitions::network::types::{FetchTimingInfo, Request, RequestData};
2487        RequestData {
2488            request: Request::new(id),
2489            url: url.into(),
2490            method: method.into(),
2491            headers: vec![],
2492            cookies: vec![],
2493            headers_size: 0,
2494            body_size: None,
2495            destination: "document".into(),
2496            initiator_type: None,
2497            timings: FetchTimingInfo {
2498                time_origin: 0.0,
2499                request_time: 0.0,
2500                redirect_start: 0.0,
2501                redirect_end: 0.0,
2502                fetch_start: 0.0,
2503                dns_start: 0.0,
2504                dns_end: 0.0,
2505                connect_start: 0.0,
2506                connect_end: 0.0,
2507                tls_start: 0.0,
2508                request_start: 0.0,
2509                response_start: 0.0,
2510                response_end: 0.0,
2511            },
2512            extensible: std::collections::HashMap::new(),
2513        }
2514    }
2515
2516    fn make_before_request_sent(id: &str, url: &str, method: &str) -> BeforeRequestSent {
2517        use rustenium_bidi_definitions::network::events::BeforeRequestSentParams;
2518        use rustenium_bidi_definitions::network::types::BaseParameters;
2519        BeforeRequestSent {
2520            method: rustenium_bidi_definitions::network::events::BeforeRequestSentMethod::BeforeRequestSent,
2521            params: BeforeRequestSentParams {
2522                base_parameters: BaseParameters::new(false, 0u64, make_bidi_request_data(id, url, method), 0u64),
2523                initiator: None,
2524            },
2525        }
2526    }
2527
2528    fn make_response_completed(id: &str, url: &str, status: u64) -> ResponseCompleted {
2529        use rustenium_bidi_definitions::network::events::ResponseCompletedParams;
2530        use rustenium_bidi_definitions::network::types::{
2531            BaseParameters, ResponseContent, ResponseData,
2532        };
2533        ResponseCompleted {
2534            method: rustenium_bidi_definitions::network::events::ResponseCompletedMethod::ResponseCompleted,
2535            params: ResponseCompletedParams {
2536                base_parameters: BaseParameters::new(false, 0u64, make_bidi_request_data(id, url, "GET"), 0u64),
2537                response: ResponseData {
2538                    url: url.into(),
2539                    protocol: "h2".into(),
2540                    status,
2541                    status_text: "OK".into(),
2542                    from_cache: false,
2543                    headers: vec![],
2544                    mime_type: "application/json".into(),
2545                    bytes_received: 100,
2546                    headers_size: None,
2547                    body_size: Some(100),
2548                    content: ResponseContent::new(100u64),
2549                    auth_challenges: None,
2550                },
2551            },
2552        }
2553    }
2554
2555    fn make_fetch_error(id: &str, url: &str, error_text: &str) -> FetchError {
2556        use rustenium_bidi_definitions::network::events::FetchErrorParams;
2557        use rustenium_bidi_definitions::network::types::BaseParameters;
2558        FetchError {
2559            method: rustenium_bidi_definitions::network::events::FetchErrorMethod::FetchError,
2560            params: FetchErrorParams {
2561                base_parameters: BaseParameters::new(
2562                    false,
2563                    0u64,
2564                    make_bidi_request_data(id, url, "GET"),
2565                    0u64,
2566                ),
2567                error_text: error_text.into(),
2568            },
2569        }
2570    }
2571
2572    #[tokio::test]
2573    async fn test_ingest_before_request_sent_creates_entry() {
2574        let log = NetworkLog::new();
2575        let evt = make_before_request_sent("req-1", "https://example.com", "GET");
2576        log.ingest_before_request_sent(&evt).await;
2577        assert_eq!(log.len().await, 1);
2578        assert!(log.contains_id("req-1").await);
2579        let m = log.metrics().await;
2580        assert_eq!(m.requests_received, 1);
2581    }
2582
2583    #[tokio::test]
2584    async fn test_ingest_response_completed_attaches_to_existing() {
2585        let log = NetworkLog::new();
2586        log.ingest_before_request_sent(&make_before_request_sent(
2587            "req-1",
2588            "https://example.com",
2589            "GET",
2590        ))
2591        .await;
2592        log.ingest_response_completed(&make_response_completed(
2593            "req-1",
2594            "https://example.com",
2595            200,
2596        ))
2597        .await;
2598
2599        let entry = log.first().await.unwrap();
2600        assert_eq!(entry.status(), Some(200));
2601        assert!(entry.has_response());
2602        let m = log.metrics().await;
2603        assert_eq!(m.responses_received, 1);
2604    }
2605
2606    #[tokio::test]
2607    async fn test_ingest_fetch_error_attaches_to_existing() {
2608        let log = NetworkLog::new();
2609        log.ingest_before_request_sent(&make_before_request_sent(
2610            "req-1",
2611            "https://example.com",
2612            "GET",
2613        ))
2614        .await;
2615        log.ingest_fetch_error(&make_fetch_error(
2616            "req-1",
2617            "https://example.com",
2618            "net::ERR_FAILED",
2619        ))
2620        .await;
2621
2622        let entry = log.first().await.unwrap();
2623        assert!(entry.is_error());
2624        assert_eq!(entry.error.as_ref().unwrap().error_text, "net::ERR_FAILED");
2625        let m = log.metrics().await;
2626        assert_eq!(m.errors_received, 1);
2627    }
2628
2629    #[tokio::test]
2630    async fn test_fetch_error_url_comes_from_request_not_navigation() {
2631        // make_fetch_error builds an event with navigation = None (the common
2632        // subresource case: a failed script/image/XHR carries no top-level
2633        // document URL). The captured error URL must still be the actual
2634        // requested URL, never empty. The old code read `navigation` and
2635        // defaulted to "" here, hiding which resource failed.
2636        let log = NetworkLog::new();
2637        let failed_url = "https://cdn.example.com/app.js";
2638        log.ingest_fetch_error(&make_fetch_error("sub-1", failed_url, "net::ERR_FAILED"))
2639            .await;
2640
2641        let (_resp, errors) = log.drain_pending().await;
2642        assert_eq!(errors.len(), 1);
2643        assert_eq!(
2644            errors[0].url, failed_url,
2645            "error URL must be the request URL, not an empty navigation fallback"
2646        );
2647    }
2648
2649    #[tokio::test]
2650    async fn test_out_of_order_response_reconciliation() {
2651        let log = NetworkLog::new();
2652        // Response arrives before request
2653        log.ingest_response_completed(&make_response_completed(
2654            "req-1",
2655            "https://example.com",
2656            200,
2657        ))
2658        .await;
2659        assert_eq!(log.len().await, 0); // No entry yet
2660        assert_eq!(log.metrics().await.responses_received, 1);
2661
2662        // Now request arrives
2663        log.ingest_before_request_sent(&make_before_request_sent(
2664            "req-1",
2665            "https://example.com",
2666            "GET",
2667        ))
2668        .await;
2669        assert_eq!(log.len().await, 1);
2670        let entry = log.first().await.unwrap();
2671        assert_eq!(entry.status(), Some(200));
2672    }
2673
2674    #[tokio::test]
2675    async fn test_out_of_order_error_reconciliation() {
2676        let log = NetworkLog::new();
2677        log.ingest_fetch_error(&make_fetch_error(
2678            "req-1",
2679            "https://example.com",
2680            "net::ERR_ABORTED",
2681        ))
2682        .await;
2683        assert_eq!(log.len().await, 0);
2684
2685        log.ingest_before_request_sent(&make_before_request_sent(
2686            "req-1",
2687            "https://example.com",
2688            "GET",
2689        ))
2690        .await;
2691        let entry = log.first().await.unwrap();
2692        assert!(entry.is_error());
2693    }
2694
2695    #[tokio::test]
2696    async fn test_response_update_rebroadcasts() {
2697        let log = NetworkLog::new();
2698        let mut rx = log.subscribe().await;
2699
2700        log.ingest_before_request_sent(&make_before_request_sent(
2701            "req-1",
2702            "https://example.com",
2703            "GET",
2704        ))
2705        .await;
2706        let first = rx.recv().await.unwrap();
2707        assert!(!first.has_response());
2708
2709        log.ingest_response_completed(&make_response_completed(
2710            "req-1",
2711            "https://example.com",
2712            200,
2713        ))
2714        .await;
2715        let updated = rx.recv().await.unwrap();
2716        assert!(updated.has_response());
2717        assert_eq!(updated.status(), Some(200));
2718    }
2719
2720    #[tokio::test]
2721    async fn test_pending_response_overflow() {
2722        let log = NetworkLog::with_limits(100, 3);
2723        // 5 responses arrive before their requests
2724        for i in 0..5 {
2725            log.ingest_response_completed(&make_response_completed(
2726                &format!("req-{}", i),
2727                "https://example.com",
2728                200,
2729            ))
2730            .await;
2731        }
2732        let m = log.metrics().await;
2733        assert_eq!(m.pending_responses_dropped, 2); // max_pending=3, so 2 dropped
2734        assert_eq!(m.responses_received, 5);
2735    }
2736
2737    #[tokio::test]
2738    async fn test_pending_error_overflow() {
2739        let log = NetworkLog::with_limits(100, 2);
2740        for i in 0..4 {
2741            log.ingest_fetch_error(&make_fetch_error(
2742                &format!("req-{}", i),
2743                "https://example.com",
2744                "err",
2745            ))
2746            .await;
2747        }
2748        let m = log.metrics().await;
2749        assert_eq!(m.pending_errors_dropped, 2); // max_pending=2, so 2 dropped
2750        assert_eq!(m.errors_received, 4);
2751    }
2752
2753    #[tokio::test]
2754    async fn test_clear_then_ingest() {
2755        let log = NetworkLog::new();
2756        log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET"))
2757            .await;
2758        log.clear().await;
2759        log.ingest_before_request_sent(&make_before_request_sent("req-2", "https://b.com", "POST"))
2760            .await;
2761        assert_eq!(log.len().await, 1);
2762        assert!(!log.contains_id("req-1").await);
2763        assert!(log.contains_id("req-2").await);
2764    }
2765
2766    #[tokio::test]
2767    async fn test_entry_with_both_response_and_error() {
2768        let log = NetworkLog::new();
2769        log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET"))
2770            .await;
2771        log.ingest_response_completed(&make_response_completed("req-1", "https://a.com", 200))
2772            .await;
2773        log.ingest_fetch_error(&make_fetch_error(
2774            "req-1",
2775            "https://a.com",
2776            "net::ERR_FAILED",
2777        ))
2778        .await;
2779
2780        let entry = log.first().await.unwrap();
2781        assert!(entry.has_response());
2782        assert!(entry.is_error());
2783        assert_eq!(entry.status(), Some(200));
2784    }
2785
2786    // ---------- cookie / header serialization ----------
2787
2788    #[tokio::test]
2789    async fn test_captured_cookie_roundtrip() {
2790        let cookie = CapturedCookie {
2791            name: "session".into(),
2792            value: "abc123".into(),
2793            domain: ".example.com".into(),
2794            path: "/".into(),
2795            size: 42,
2796            http_only: true,
2797            secure: true,
2798            same_site: "strict".into(),
2799        };
2800        let json = serde_json::to_string(&cookie).unwrap();
2801        let de: CapturedCookie = serde_json::from_str(&json).unwrap();
2802        assert_eq!(de, cookie);
2803    }
2804
2805    #[tokio::test]
2806    async fn test_captured_header_roundtrip() {
2807        let h = CapturedHeader {
2808            name: "X-Test".into(),
2809            value: "value".into(),
2810        };
2811        let json = serde_json::to_string(&h).unwrap();
2812        let de: CapturedHeader = serde_json::from_str(&json).unwrap();
2813        assert_eq!(de, h);
2814    }
2815
2816    #[tokio::test]
2817    async fn test_filter_url_regex_invalid_pattern() {
2818        let result = Filter::new().url_regex("[");
2819        assert!(result.is_err());
2820    }
2821
2822    #[tokio::test]
2823    async fn test_large_url_does_not_panic() {
2824        let log = NetworkLog::new();
2825        let long_url = format!("https://example.com/{}", "a".repeat(10000));
2826        push_request(&log, make_request("1", "GET", &long_url)).await;
2827        assert_eq!(log.len().await, 1);
2828        assert!(log.find_by_url("example.com").await.is_some());
2829    }
2830
2831    #[tokio::test]
2832    async fn test_eviction_with_subscriber() {
2833        let log = NetworkLog::with_limits(2, 10);
2834        let mut rx = log.subscribe().await;
2835        for i in 0..5 {
2836            push_request(
2837                &log,
2838                make_request(&format!("{}", i), "GET", "https://example.com"),
2839            )
2840            .await;
2841        }
2842        // Should have received all 5 broadcast messages despite eviction
2843        let mut count = 0;
2844        while rx.try_recv().is_ok() {
2845            count += 1;
2846        }
2847        assert_eq!(count, 5);
2848    }
2849
2850    // ---------- deeper edge cases ----------
2851
2852    #[tokio::test]
2853    async fn test_filter_method_case_insensitive() {
2854        let log = NetworkLog::new();
2855        push_request(&log, make_request("1", "DELETE", "https://a.com")).await;
2856        assert_eq!(log.filter(Filter::new().method("delete")).await.len(), 1);
2857        assert_eq!(log.filter(Filter::new().method("DELETE")).await.len(), 1);
2858    }
2859
2860    #[tokio::test]
2861    async fn test_filter_header_in_response() {
2862        let log = NetworkLog::new();
2863        let req = make_request("1", "GET", "https://a.com");
2864        let mut resp = make_response("1", 200, "https://a.com");
2865        resp.headers = vec![CapturedHeader {
2866            name: "X-Resp".into(),
2867            value: "secret-val".into(),
2868        }];
2869        push_entry(&log, req, Some(resp), None).await;
2870        let f = log.filter(Filter::new().header("x-resp", "secret")).await;
2871        assert_eq!(f.len(), 1);
2872    }
2873
2874    #[tokio::test]
2875    async fn test_filter_composition_conflicting() {
2876        let log = NetworkLog::new();
2877        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2878        // method GET AND method POST -> impossible
2879        let f = log.filter(Filter::new().method("GET").method("POST")).await;
2880        assert!(f.is_empty());
2881    }
2882
2883    #[tokio::test]
2884    async fn test_count_zero() {
2885        let log = NetworkLog::new();
2886        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2887        assert_eq!(log.count(Filter::new().method("POST")).await, 0);
2888    }
2889
2890    #[tokio::test]
2891    async fn test_is_empty_false() {
2892        let log = NetworkLog::new();
2893        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2894        assert!(!log.is_empty().await);
2895    }
2896
2897    #[tokio::test]
2898    async fn test_endpoints_dedupes() {
2899        let log = NetworkLog::new();
2900        push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
2901        push_request(&log, make_request("2", "GET", "https://a.com/x")).await;
2902        push_request(&log, make_request("3", "GET", "https://a.com/y")).await;
2903        let ep = log.endpoints().await;
2904        assert_eq!(ep.len(), 2);
2905    }
2906
2907    #[tokio::test]
2908    async fn test_hostnames_with_port() {
2909        let log = NetworkLog::new();
2910        push_request(
2911            &log,
2912            make_request("1", "GET", "https://example.com:8443/path"),
2913        )
2914        .await;
2915        let h = log.hostnames().await;
2916        assert_eq!(h.len(), 1);
2917        assert!(h.contains(&"example.com".into()));
2918    }
2919
2920    #[tokio::test]
2921    async fn test_total_bytes_out_none() {
2922        let log = NetworkLog::new();
2923        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2924        assert_eq!(log.total_bytes_out().await, 0);
2925    }
2926
2927    #[tokio::test]
2928    async fn test_completed_mixed_states() {
2929        let log = NetworkLog::new();
2930        push_entry(
2931            &log,
2932            make_request("1", "GET", "https://a.com"),
2933            Some(make_response("1", 200, "https://a.com")),
2934            None,
2935        )
2936        .await;
2937        push_request(&log, make_request("2", "GET", "https://b.com")).await;
2938        push_entry(
2939            &log,
2940            make_request("3", "GET", "https://c.com"),
2941            None,
2942            Some(make_error("3", "https://c.com", "err")),
2943        )
2944        .await;
2945        push_entry(
2946            &log,
2947            make_request("4", "GET", "https://d.com"),
2948            Some(make_response("4", 500, "https://d.com")),
2949            Some(make_error("4", "https://d.com", "err")),
2950        )
2951        .await;
2952        let c = log.completed().await;
2953        assert_eq!(c.len(), 3);
2954    }
2955
2956    #[tokio::test]
2957    async fn test_distinct_statuses_with_no_response() {
2958        let log = NetworkLog::new();
2959        push_entry(
2960            &log,
2961            make_request("1", "GET", "https://a.com"),
2962            Some(make_response("1", 200, "https://a.com")),
2963            None,
2964        )
2965        .await;
2966        push_request(&log, make_request("2", "GET", "https://b.com")).await;
2967        push_entry(
2968            &log,
2969            make_request("3", "GET", "https://c.com"),
2970            Some(make_response("3", 404, "https://c.com")),
2971            None,
2972        )
2973        .await;
2974        let s = log.distinct_statuses().await;
2975        assert_eq!(s, vec![200, 404]);
2976    }
2977
2978    #[tokio::test]
2979    async fn test_distinct_methods_dedupes() {
2980        let log = NetworkLog::new();
2981        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2982        push_request(&log, make_request("2", "GET", "https://b.com")).await;
2983        push_request(&log, make_request("3", "POST", "https://c.com")).await;
2984        push_request(&log, make_request("4", "POST", "https://d.com")).await;
2985        let m = log.distinct_methods().await;
2986        assert_eq!(m, vec!["GET", "POST"]);
2987    }
2988
2989    #[tokio::test]
2990    async fn test_find_by_url_empty_substring() {
2991        let log = NetworkLog::new();
2992        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2993        // Empty substring should match everything that contains ""
2994        assert!(log.find_by_url("").await.is_some());
2995    }
2996
2997    #[tokio::test]
2998    async fn test_find_by_url_regex_no_match() {
2999        let log = NetworkLog::new();
3000        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3001        let re = regex::Regex::new(r"zzz").unwrap();
3002        assert!(log.find_by_url_regex(&re).await.is_none());
3003    }
3004
3005    #[tokio::test]
3006    async fn test_filter_url_contains_empty() {
3007        let log = NetworkLog::new();
3008        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3009        let f = log.filter(Filter::new().url_contains("")).await;
3010        assert_eq!(f.len(), 1);
3011    }
3012
3013    #[tokio::test]
3014    async fn test_to_curl_binary_body() {
3015        let req = CapturedRequest {
3016            id: "1".into(),
3017            context: None,
3018            method: "POST".into(),
3019            url: "https://example.com".into(),
3020            headers: vec![],
3021            post_data: Some("\x00\x01\x02".into()),
3022            timestamp: 0,
3023            destination: "document".into(),
3024            initiator_type: None,
3025            timing: CapturedTiming::default(),
3026            cookies: vec![],
3027        };
3028        let entry = NetworkEntry {
3029            request: req,
3030            response: None,
3031            error: None,
3032        };
3033        let curl = entry.to_curl();
3034        assert!(curl.contains("-d '"));
3035    }
3036
3037    #[tokio::test]
3038    async fn test_json_body_empty_string() {
3039        let mut req = make_request("1", "POST", "https://a.com");
3040        req.post_data = Some("".into());
3041        assert!(req.json_body().is_none());
3042    }
3043
3044    #[tokio::test]
3045    async fn test_query_params_duplicate_keys() {
3046        let req = make_request("1", "GET", "https://a.com?foo=1&foo=2");
3047        let params = req.query_params().unwrap();
3048        assert_eq!(params.len(), 2);
3049        assert!(params.contains(&("foo".into(), "1".into())));
3050        assert!(params.contains(&("foo".into(), "2".into())));
3051    }
3052
3053    #[tokio::test]
3054    async fn test_request_header_empty_name() {
3055        let req = make_request_with_headers("1", "GET", "https://a.com", vec![]);
3056        assert_eq!(req.request_header(""), None);
3057    }
3058
3059    #[tokio::test]
3060    async fn test_response_header_empty_name() {
3061        let entry = NetworkEntry {
3062            request: make_request("1", "GET", "https://a.com"),
3063            response: Some(make_response("1", 200, "https://a.com")),
3064            error: None,
3065        };
3066        assert_eq!(entry.response_header(""), None);
3067    }
3068
3069    #[tokio::test]
3070    async fn test_remove_by_id_first() {
3071        let log = NetworkLog::new();
3072        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3073        push_request(&log, make_request("2", "GET", "https://b.com")).await;
3074        log.remove_by_id("1").await;
3075        assert_eq!(log.first().await.unwrap().request.id, "2");
3076    }
3077
3078    #[tokio::test]
3079    async fn test_remove_by_id_last() {
3080        let log = NetworkLog::new();
3081        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3082        push_request(&log, make_request("2", "GET", "https://b.com")).await;
3083        log.remove_by_id("2").await;
3084        assert_eq!(log.last().await.unwrap().request.id, "1");
3085    }
3086
3087    #[tokio::test]
3088    async fn test_contains_id_after_eviction() {
3089        let log = NetworkLog::with_limits(4, 10);
3090        for i in 0..10 {
3091            push_request(
3092                &log,
3093                make_request(&format!("{}", i), "GET", "https://example.com"),
3094            )
3095            .await;
3096        }
3097        // After eviction, only 4-9 should remain
3098        for i in 0..4 {
3099            assert!(!log.contains_id(&format!("{}", i)).await);
3100        }
3101        for i in 6..10 {
3102            assert!(log.contains_id(&format!("{}", i)).await);
3103        }
3104    }
3105
3106    #[tokio::test]
3107    async fn test_wait_for_response_error_instead_of_response() {
3108        let log = NetworkLog::new();
3109        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3110        let log2 = log.clone();
3111        tokio::spawn(async move {
3112            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
3113            let mut inner = log2.inner.write().await;
3114            if let Some(idx) = inner.by_id.get("1").copied() {
3115                let mut new_entry = (*inner.entries[idx]).clone();
3116                new_entry.error = Some(make_error("1", "https://a.com", "net::ERR_ABORTED"));
3117                let new_arc = Arc::new(new_entry);
3118                inner.entries[idx] = new_arc.clone();
3119                let _ = inner.tx.send(new_arc);
3120            }
3121        });
3122        let found = log
3123            .wait_for_response("1", std::time::Duration::from_secs(1))
3124            .await;
3125        assert!(found.unwrap().is_error());
3126    }
3127
3128    #[tokio::test]
3129    async fn test_captured_timing_from_bidi() {
3130        use rustenium_bidi_definitions::network::types::FetchTimingInfo;
3131        let info = FetchTimingInfo {
3132            time_origin: 0.0,
3133            request_time: 10.0,
3134            redirect_start: 0.0,
3135            redirect_end: 0.0,
3136            fetch_start: 10.0,
3137            dns_start: 11.0,
3138            dns_end: 12.0,
3139            connect_start: 12.0,
3140            connect_end: 14.0,
3141            tls_start: 13.0,
3142            request_start: 14.0,
3143            response_start: 15.0,
3144            response_end: 16.0,
3145        };
3146        let t = CapturedTiming::from(&info);
3147        assert_eq!(t.dns_start_ms, Some(1.0)); // 11 - 10
3148        assert_eq!(t.dns_end_ms, Some(2.0)); // 12 - 10
3149        assert_eq!(t.connect_start_ms, Some(2.0)); // 12 - 10
3150        assert_eq!(t.connect_end_ms, Some(4.0)); // 14 - 10
3151        assert_eq!(t.tls_start_ms, Some(3.0)); // 13 - 10
3152        assert_eq!(t.response_start_ms, Some(5.0)); // 15 - 10
3153        assert_eq!(t.response_end_ms, Some(6.0)); // 16 - 10
3154    }
3155
3156    #[tokio::test]
3157    async fn test_captured_timing_negative_clamped() {
3158        use rustenium_bidi_definitions::network::types::FetchTimingInfo;
3159        let info = FetchTimingInfo {
3160            time_origin: 0.0,
3161            request_time: 10.0,
3162            redirect_start: 0.0,
3163            redirect_end: 0.0,
3164            fetch_start: 10.0,
3165            dns_start: 9.0, // before request_time
3166            dns_end: 8.0,   // before request_time
3167            connect_start: 0.0,
3168            connect_end: 0.0,
3169            tls_start: 0.0,
3170            request_start: 0.0,
3171            response_start: 0.0,
3172            response_end: 0.0,
3173        };
3174        let t = CapturedTiming::from(&info);
3175        assert_eq!(t.dns_start_ms, None); // negative clamped to None
3176        assert_eq!(t.dns_end_ms, None);
3177    }
3178
3179    #[tokio::test]
3180    async fn test_har_timings_correct() {
3181        let log = NetworkLog::new();
3182        let mut req = make_request("1", "GET", "https://api.example.com");
3183        req.timing = CapturedTiming {
3184            dns_start_ms: Some(1.0),
3185            dns_end_ms: Some(3.0),
3186            connect_start_ms: Some(3.0),
3187            connect_end_ms: Some(7.0),
3188            tls_start_ms: Some(5.0),
3189            response_start_ms: Some(8.0),
3190            response_end_ms: Some(10.0),
3191        };
3192        push_entry(
3193            &log,
3194            req,
3195            Some(make_response("1", 200, "https://api.example.com")),
3196            None,
3197        )
3198        .await;
3199
3200        let path = std::path::Path::new("/tmp/foxdriver_har_timings.har");
3201        log.save_as_har(path, None).await.unwrap();
3202        let content = std::fs::read_to_string(path).unwrap();
3203        // dns should be 2.0 (3.0 - 1.0)
3204        assert!(content.contains("\"dns\": 2.0") || content.contains("\"dns\": 2"));
3205        // connect should be 4.0 (7.0 - 3.0)
3206        assert!(content.contains("\"connect\": 4.0") || content.contains("\"connect\": 4"));
3207        // ssl should be 2.0 (7.0 - 5.0)
3208        assert!(content.contains("\"ssl\": 2.0") || content.contains("\"ssl\": 2"));
3209    }
3210
3211    #[tokio::test]
3212    async fn test_concurrent_ingestion_stress() {
3213        let log = NetworkLog::new();
3214        let mut handles = Vec::new();
3215        for t in 0..20 {
3216            let log = log.clone();
3217            handles.push(tokio::spawn(async move {
3218                for i in 0..50 {
3219                    let id = format!("{}-{}", t, i);
3220                    log.ingest_before_request_sent(&make_before_request_sent(
3221                        &id,
3222                        "https://example.com",
3223                        "GET",
3224                    ))
3225                    .await;
3226                    if i % 2 == 0 {
3227                        log.ingest_response_completed(&make_response_completed(
3228                            &id,
3229                            "https://example.com",
3230                            200,
3231                        ))
3232                        .await;
3233                    } else {
3234                        log.ingest_fetch_error(&make_fetch_error(
3235                            &id,
3236                            "https://example.com",
3237                            "err",
3238                        ))
3239                        .await;
3240                    }
3241                }
3242            }));
3243        }
3244        for h in handles {
3245            h.await.unwrap();
3246        }
3247        assert_eq!(log.len().await, 1000);
3248        let completed = log.completed().await;
3249        assert_eq!(completed.len(), 1000); // all have response or error
3250        let m = log.metrics().await;
3251        assert_eq!(m.requests_received, 1000);
3252        assert_eq!(m.responses_received, 500);
3253        assert_eq!(m.errors_received, 500);
3254    }
3255
3256    #[tokio::test]
3257    async fn test_property_filter_then_count_matches() {
3258        let log = NetworkLog::new();
3259        for i in 0..100 {
3260            let method = if i % 2 == 0 { "GET" } else { "POST" };
3261            push_request(
3262                &log,
3263                make_request(&format!("{}", i), method, "https://example.com"),
3264            )
3265            .await;
3266        }
3267        let f = Filter::new().method("GET");
3268        let filtered = log.filter(f.clone()).await;
3269        let counted = log.count(f).await;
3270        assert_eq!(filtered.len(), counted);
3271        assert_eq!(counted, 50);
3272    }
3273
3274    #[tokio::test]
3275    async fn test_property_first_last_consistency() {
3276        let log = NetworkLog::new();
3277        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3278        push_request(&log, make_request("2", "GET", "https://b.com")).await;
3279        push_request(&log, make_request("3", "GET", "https://c.com")).await;
3280
3281        assert_eq!(
3282            log.first().await.unwrap().request.id,
3283            log.nth(0).await.unwrap().request.id
3284        );
3285        assert_eq!(
3286            log.last().await.unwrap().request.id,
3287            log.nth(2).await.unwrap().request.id
3288        );
3289    }
3290
3291    #[tokio::test]
3292    async fn test_property_endpoints_is_subset_of_entries() {
3293        let log = NetworkLog::new();
3294        push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
3295        push_request(&log, make_request("2", "GET", "https://a.com/y")).await;
3296        let entries = log.entries().await;
3297        let endpoints = log.endpoints().await;
3298        assert!(endpoints.len() <= entries.len());
3299        for ep in &endpoints {
3300            assert!(entries.iter().any(|e| e.request.url == *ep));
3301        }
3302    }
3303
3304    #[tokio::test]
3305    async fn test_property_hostnames_is_subset_of_endpoints() {
3306        let log = NetworkLog::new();
3307        push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
3308        push_request(&log, make_request("2", "GET", "https://b.com/y")).await;
3309        let hostnames = log.hostnames().await;
3310        let endpoints = log.endpoints().await;
3311        for h in &hostnames {
3312            assert!(endpoints.iter().any(|e| e.contains(h)));
3313        }
3314    }
3315
3316    #[tokio::test]
3317    async fn test_stress_many_entries() {
3318        let log = NetworkLog::with_limits(100_000, 10_000);
3319        for i in 0..10_000 {
3320            push_request(
3321                &log,
3322                make_request(
3323                    &format!("{}", i),
3324                    "GET",
3325                    &format!("https://example.com/{}", i),
3326                ),
3327            )
3328            .await;
3329        }
3330        assert_eq!(log.len().await, 10_000);
3331        assert!(log.find_by_url("example.com").await.is_some());
3332        assert_eq!(log.distinct_methods().await, vec!["GET"]);
3333    }
3334
3335    // ---------- make_network_handler ----------
3336
3337    #[tokio::test]
3338    async fn test_handler_ignores_non_network_events() {
3339        let log = NetworkLog::new();
3340        let mut handler = make_network_handler(log.clone());
3341        // Log event should be ignored
3342        let evt = rustenium_bidi_definitions::Event::Log(
3343            rustenium_bidi_definitions::log::events::LogEvent::EntryAdded(
3344                rustenium_bidi_definitions::log::events::EntryAdded {
3345                    method: rustenium_bidi_definitions::log::events::EntryAddedMethod::EntryAdded,
3346                    params: rustenium_bidi_definitions::log::events::EntryAddedParams {},
3347                },
3348            ),
3349        );
3350        handler(evt).await;
3351        assert!(log.is_empty().await);
3352    }
3353
3354    #[tokio::test]
3355    async fn test_handler_processes_before_request_sent() {
3356        let log = NetworkLog::new();
3357        let mut handler = make_network_handler(log.clone());
3358        let evt = rustenium_bidi_definitions::Event::Network(
3359            rustenium_bidi_definitions::network::events::NetworkEvent::BeforeRequestSent(
3360                make_before_request_sent("req-1", "https://example.com", "GET"),
3361            ),
3362        );
3363        handler(evt).await;
3364        assert_eq!(log.len().await, 1);
3365        assert!(log.contains_id("req-1").await);
3366    }
3367
3368    #[tokio::test]
3369    async fn test_handler_processes_response_completed() {
3370        let log = NetworkLog::new();
3371        let mut handler = make_network_handler(log.clone());
3372        handler(rustenium_bidi_definitions::Event::Network(
3373            rustenium_bidi_definitions::network::events::NetworkEvent::BeforeRequestSent(
3374                make_before_request_sent("req-1", "https://example.com", "GET"),
3375            ),
3376        ))
3377        .await;
3378        handler(rustenium_bidi_definitions::Event::Network(
3379            rustenium_bidi_definitions::network::events::NetworkEvent::ResponseCompleted(
3380                make_response_completed("req-1", "https://example.com", 200),
3381            ),
3382        ))
3383        .await;
3384        let entry = log.first().await.unwrap();
3385        assert_eq!(entry.status(), Some(200));
3386    }
3387
3388    #[tokio::test]
3389    async fn test_handler_ignores_unknown_network_events() {
3390        let log = NetworkLog::new();
3391        let mut handler = make_network_handler(log.clone());
3392        // ResponseStarted is not handled
3393        handler(rustenium_bidi_definitions::Event::Network(
3394            rustenium_bidi_definitions::network::events::NetworkEvent::ResponseStarted(
3395                rustenium_bidi_definitions::network::events::ResponseStarted {
3396                    method: rustenium_bidi_definitions::network::events::ResponseStartedMethod::ResponseStarted,
3397                    params: rustenium_bidi_definitions::network::events::ResponseStartedParams {
3398                        base_parameters: rustenium_bidi_definitions::network::types::BaseParameters::new(
3399                            false, 0u64, make_bidi_request_data("req-1", "https://example.com", "GET"), 0u64
3400                        ),
3401                        response: rustenium_bidi_definitions::network::types::ResponseData {
3402                            url: "https://example.com".into(),
3403                            protocol: "h2".into(),
3404                            status: 200,
3405                            status_text: "OK".into(),
3406                            from_cache: false,
3407                            headers: vec![],
3408                            mime_type: "application/json".into(),
3409                            bytes_received: 100,
3410                            headers_size: None,
3411                            body_size: Some(100),
3412                            content: rustenium_bidi_definitions::network::types::ResponseContent::new(100u64),
3413                            auth_challenges: None,
3414                        },
3415                    },
3416                }
3417            )
3418        )).await;
3419        assert!(log.is_empty().await);
3420    }
3421
3422    // ---------- export edge cases ----------
3423
3424    #[tokio::test]
3425    async fn test_har_with_null_response() {
3426        let log = NetworkLog::new();
3427        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3428        let path = std::path::Path::new("/tmp/foxdriver_har_null_resp.har");
3429        log.save_as_har(path, None).await.unwrap();
3430        let content = std::fs::read_to_string(path).unwrap();
3431        assert!(content.contains("\"response\": null"));
3432    }
3433
3434    #[tokio::test]
3435    async fn test_har_with_error_entry() {
3436        let log = NetworkLog::new();
3437        push_entry(
3438            &log,
3439            make_request("1", "GET", "https://a.com"),
3440            None,
3441            Some(make_error("1", "https://a.com", "net::ERR_FAILED")),
3442        )
3443        .await;
3444        let path = std::path::Path::new("/tmp/foxdriver_har_error.har");
3445        log.save_as_har(path, None).await.unwrap();
3446        let content = std::fs::read_to_string(path).unwrap();
3447        assert!(content.contains("\"response\": null"));
3448    }
3449
3450    #[tokio::test]
3451    async fn test_default_impl() {
3452        let log: NetworkLog = Default::default();
3453        assert!(log.is_empty().await);
3454        let m = log.metrics().await;
3455        assert_eq!(m.max_entries, 50_000);
3456    }
3457
3458    #[tokio::test]
3459    async fn test_clear_does_not_break_broadcast() {
3460        let log = NetworkLog::new();
3461        let mut rx = log.subscribe().await;
3462        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3463        log.clear().await;
3464        push_request(&log, make_request("2", "GET", "https://b.com")).await;
3465        // Should still receive both (clear doesn't close channel)
3466        let first = rx.recv().await.unwrap();
3467        assert_eq!(first.request.id, "1");
3468        let second = rx.recv().await.unwrap();
3469        assert_eq!(second.request.id, "2");
3470    }
3471
3472    #[tokio::test]
3473    async fn test_entries_returns_arc_clones() {
3474        let log = NetworkLog::new();
3475        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3476        let e1 = log.entries().await;
3477        let e2 = log.entries().await;
3478        // Arc::ptr_eq should be true since they point to the same data
3479        assert!(Arc::ptr_eq(&e1[0], &e2[0]));
3480    }
3481
3482    #[tokio::test]
3483    async fn test_filter_header_only_response() {
3484        let log = NetworkLog::new();
3485        let req = make_request("1", "GET", "https://a.com");
3486        let mut resp = make_response("1", 200, "https://a.com");
3487        resp.headers = vec![CapturedHeader {
3488            name: "X-Resp-Only".into(),
3489            value: "found-me".into(),
3490        }];
3491        push_entry(&log, req, Some(resp), None).await;
3492        // Filter should find the header even though it's only in response
3493        let f = log
3494            .filter(Filter::new().header("x-resp-only", "found"))
3495            .await;
3496        assert_eq!(f.len(), 1);
3497    }
3498
3499    #[tokio::test]
3500    async fn test_find_by_url_after_remove() {
3501        let log = NetworkLog::new();
3502        push_request(&log, make_request("1", "GET", "https://a.com/page")).await;
3503        push_request(&log, make_request("2", "GET", "https://b.com/page")).await;
3504        log.remove_by_id("1").await;
3505        assert!(log.find_by_url("a.com").await.is_none());
3506        assert!(log.find_by_url("b.com").await.is_some());
3507    }
3508
3509    #[tokio::test]
3510    async fn test_concurrent_wait_for_url() {
3511        let log = NetworkLog::new();
3512        let log2 = log.clone();
3513        let log3 = log.clone();
3514        let h1 = tokio::spawn(async move {
3515            log2.wait_for_url("target", std::time::Duration::from_secs(1))
3516                .await
3517        });
3518        let h2 = tokio::spawn(async move {
3519            log3.wait_for_url("target", std::time::Duration::from_secs(1))
3520                .await
3521        });
3522        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
3523        push_request(&log, make_request("1", "GET", "https://target.com")).await;
3524        let r1 = h1.await.unwrap();
3525        let r2 = h2.await.unwrap();
3526        assert!(r1.is_some());
3527        assert!(r2.is_some());
3528    }
3529
3530    #[tokio::test]
3531    async fn test_build_response_from_cache() {
3532        let log = NetworkLog::new();
3533        let mut resp = make_response("1", 200, "https://a.com");
3534        resp.from_cache = true;
3535        push_entry(
3536            &log,
3537            make_request("1", "GET", "https://a.com"),
3538            Some(resp),
3539            None,
3540        )
3541        .await;
3542        let entry = log.first().await.unwrap();
3543        assert!(entry.response.as_ref().unwrap().from_cache);
3544    }
3545
3546    #[tokio::test]
3547    async fn test_build_response_no_body_size() {
3548        let log = NetworkLog::new();
3549        let mut resp = make_response("1", 204, "https://a.com");
3550        resp.body_size = None;
3551        push_entry(
3552            &log,
3553            make_request("1", "GET", "https://a.com"),
3554            Some(resp),
3555            None,
3556        )
3557        .await;
3558        let entry = log.first().await.unwrap();
3559        assert_eq!(entry.response.as_ref().unwrap().body_size, None);
3560        assert_eq!(log.total_bytes_in().await, 0);
3561    }
3562
3563    #[tokio::test]
3564    async fn test_filter_has_response_and_has_error_same_entry() {
3565        let log = NetworkLog::new();
3566        push_entry(
3567            &log,
3568            make_request("1", "GET", "https://a.com"),
3569            Some(make_response("1", 500, "https://a.com")),
3570            Some(make_error("1", "https://a.com", "err")),
3571        )
3572        .await;
3573        // with_response should match
3574        assert_eq!(log.filter(Filter::new().with_response()).await.len(), 1);
3575        // with_error should match
3576        assert_eq!(log.filter(Filter::new().with_error()).await.len(), 1);
3577    }
3578
3579    #[tokio::test]
3580    async fn test_nth_after_remove() {
3581        let log = NetworkLog::new();
3582        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3583        push_request(&log, make_request("2", "GET", "https://b.com")).await;
3584        push_request(&log, make_request("3", "GET", "https://c.com")).await;
3585        log.remove_by_id("2").await;
3586        assert_eq!(log.nth(0).await.unwrap().request.id, "1");
3587        assert_eq!(log.nth(1).await.unwrap().request.id, "3");
3588        assert!(log.nth(2).await.is_none());
3589    }
3590
3591    #[tokio::test]
3592    async fn test_pending_response_then_error_then_request() {
3593        let log = NetworkLog::new();
3594        // Response arrives first
3595        log.ingest_response_completed(&make_response_completed("req-1", "https://a.com", 200))
3596            .await;
3597        // Then error arrives
3598        log.ingest_fetch_error(&make_fetch_error(
3599            "req-1",
3600            "https://a.com",
3601            "net::ERR_ABORTED",
3602        ))
3603        .await;
3604        // Then request arrives - should reconcile both response and error
3605        log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET"))
3606            .await;
3607        let entry = log.first().await.unwrap();
3608        assert!(entry.has_response());
3609        assert!(entry.is_error());
3610    }
3611
3612    #[tokio::test]
3613    async fn test_metrics_persist_after_eviction() {
3614        let log = NetworkLog::with_limits(4, 10);
3615        for i in 0..10 {
3616            log.ingest_before_request_sent(&make_before_request_sent(
3617                &format!("{}", i),
3618                "https://a.com",
3619                "GET",
3620            ))
3621            .await;
3622        }
3623        let m = log.metrics().await;
3624        assert_eq!(m.requests_received, 10);
3625        assert_eq!(m.entries_evicted, 6); // 10 - 4 = 6 evicted (actually eviction removes half each time)
3626    }
3627
3628    #[tokio::test]
3629    async fn test_curl_strips_accept_encoding() {
3630        let req = CapturedRequest {
3631            id: "1".into(),
3632            context: None,
3633            method: "GET".into(),
3634            url: "https://example.com".into(),
3635            headers: vec![CapturedHeader {
3636                name: "Accept-Encoding".into(),
3637                value: "gzip".into(),
3638            }],
3639            post_data: None,
3640            timestamp: 0,
3641            destination: "document".into(),
3642            initiator_type: None,
3643            timing: CapturedTiming::default(),
3644            cookies: vec![],
3645        };
3646        let entry = NetworkEntry {
3647            request: req,
3648            response: None,
3649            error: None,
3650        };
3651        let curl = entry.to_curl();
3652        assert!(!curl.contains("Accept-Encoding"));
3653    }
3654
3655    // ---------- boundary condition bug fixes ----------
3656
3657    #[tokio::test]
3658    async fn test_max_entries_zero_does_not_panic() {
3659        let log = NetworkLog::with_limits(0, 10);
3660        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3661        push_request(&log, make_request("2", "GET", "https://b.com")).await;
3662        // With max_entries=0, no eviction should occur (special case)
3663        assert_eq!(log.len().await, 2);
3664    }
3665
3666    #[tokio::test]
3667    async fn test_max_pending_zero_does_not_hang() {
3668        let log = NetworkLog::with_limits(100, 0);
3669        // Should not hang even though max_pending=0
3670        log.ingest_response_completed(&make_response_completed("req-1", "https://a.com", 200))
3671            .await;
3672        log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET"))
3673            .await;
3674        assert_eq!(log.len().await, 1);
3675        assert!(log.first().await.unwrap().has_response());
3676    }
3677
3678    #[tokio::test]
3679    async fn test_max_entries_one() {
3680        let log = NetworkLog::with_limits(1, 10);
3681        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3682        push_request(&log, make_request("2", "GET", "https://b.com")).await;
3683        // Incremental eviction removes the oldest entry to make room for each new one.
3684        assert_eq!(log.len().await, 1);
3685        assert_eq!(log.last().await.unwrap().request.id, "2");
3686    }
3687
3688    // ---------- deeper ingestion coverage ----------
3689
3690    #[tokio::test]
3691    async fn test_ingest_request_with_headers() {
3692        let log = NetworkLog::new();
3693        let mut req_data = make_bidi_request_data("req-1", "https://a.com", "GET");
3694        req_data.headers = vec![rustenium_bidi_definitions::network::types::Header {
3695            name: "X-Test".into(),
3696            value: rustenium_bidi_definitions::network::types::BytesValue::StringValue(
3697                rustenium_bidi_definitions::network::types::StringValue::new(
3698                    rustenium_bidi_definitions::network::types::StringValueType::String,
3699                    "hello",
3700                ),
3701            ),
3702        }];
3703        let evt = BeforeRequestSent {
3704            method: rustenium_bidi_definitions::network::events::BeforeRequestSentMethod::BeforeRequestSent,
3705            params: rustenium_bidi_definitions::network::events::BeforeRequestSentParams {
3706                base_parameters: rustenium_bidi_definitions::network::types::BaseParameters::new(false, 0u64, req_data, 0u64),
3707                initiator: None,
3708            },
3709        };
3710        log.ingest_before_request_sent(&evt).await;
3711        let entry = log.first().await.unwrap();
3712        assert_eq!(entry.request.headers.len(), 1);
3713        assert_eq!(entry.request.headers[0].name, "X-Test");
3714        assert_eq!(entry.request.headers[0].value, "hello");
3715    }
3716
3717    #[tokio::test]
3718    async fn test_ingest_request_with_base64_header() {
3719        let log = NetworkLog::new();
3720        let mut req_data = make_bidi_request_data("req-1", "https://a.com", "GET");
3721        req_data.headers = vec![rustenium_bidi_definitions::network::types::Header {
3722            name: "X-Binary".into(),
3723            value: rustenium_bidi_definitions::network::types::BytesValue::Base64Value(
3724                rustenium_bidi_definitions::network::types::Base64Value::new(
3725                    rustenium_bidi_definitions::network::types::Base64ValueType::Base64,
3726                    "SGVsbG8=",
3727                ),
3728            ),
3729        }];
3730        let evt = BeforeRequestSent {
3731            method: rustenium_bidi_definitions::network::events::BeforeRequestSentMethod::BeforeRequestSent,
3732            params: rustenium_bidi_definitions::network::events::BeforeRequestSentParams {
3733                base_parameters: rustenium_bidi_definitions::network::types::BaseParameters::new(false, 0u64, req_data, 0u64),
3734                initiator: None,
3735            },
3736        };
3737        log.ingest_before_request_sent(&evt).await;
3738        let entry = log.first().await.unwrap();
3739        assert_eq!(entry.request.headers[0].value, "SGVsbG8=");
3740    }
3741
3742    #[tokio::test]
3743    async fn test_ingest_request_with_context() {
3744        let log = NetworkLog::new();
3745        let req_data = make_bidi_request_data("req-1", "https://a.com", "GET");
3746        let mut base = rustenium_bidi_definitions::network::types::BaseParameters::new(
3747            false, 0u64, req_data, 0u64,
3748        );
3749        base.context = Some(
3750            rustenium_bidi_definitions::browsing_context::types::BrowsingContext::new("ctx-1"),
3751        );
3752        let evt = BeforeRequestSent {
3753            method: rustenium_bidi_definitions::network::events::BeforeRequestSentMethod::BeforeRequestSent,
3754            params: rustenium_bidi_definitions::network::events::BeforeRequestSentParams {
3755                base_parameters: base,
3756                initiator: None,
3757            },
3758        };
3759        log.ingest_before_request_sent(&evt).await;
3760        let entry = log.first().await.unwrap();
3761        assert_eq!(entry.request.context, Some("ctx-1".into()));
3762    }
3763
3764    #[tokio::test]
3765    async fn test_ingest_request_with_initiator() {
3766        let log = NetworkLog::new();
3767        let req_data = make_bidi_request_data("req-1", "https://a.com", "GET");
3768        let evt = BeforeRequestSent {
3769            method: rustenium_bidi_definitions::network::events::BeforeRequestSentMethod::BeforeRequestSent,
3770            params: rustenium_bidi_definitions::network::events::BeforeRequestSentParams {
3771                base_parameters: rustenium_bidi_definitions::network::types::BaseParameters::new(false, 0u64, req_data, 0u64),
3772                initiator: Some(rustenium_bidi_definitions::network::types::Initiator {
3773                    column_number: None,
3774                    line_number: None,
3775                    request: None,
3776                    stack_trace: None,
3777                    r#type: Some(rustenium_bidi_definitions::network::types::InitiatorType::Script),
3778                }),
3779            },
3780        };
3781        log.ingest_before_request_sent(&evt).await;
3782        let entry = log.first().await.unwrap();
3783        assert_eq!(entry.request.initiator_type, Some("script".into()));
3784    }
3785
3786    #[tokio::test]
3787    async fn test_ingest_response_with_headers() {
3788        let log = NetworkLog::new();
3789        log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET"))
3790            .await;
3791
3792        let mut resp_data = make_response_completed("req-1", "https://a.com", 200);
3793        resp_data.params.response.headers =
3794            vec![rustenium_bidi_definitions::network::types::Header {
3795                name: "X-Response".into(),
3796                value: rustenium_bidi_definitions::network::types::BytesValue::StringValue(
3797                    rustenium_bidi_definitions::network::types::StringValue::new(
3798                        rustenium_bidi_definitions::network::types::StringValueType::String,
3799                        "world",
3800                    ),
3801                ),
3802            }];
3803        log.ingest_response_completed(&resp_data).await;
3804
3805        let entry = log.first().await.unwrap();
3806        assert_eq!(entry.response.as_ref().unwrap().headers.len(), 1);
3807        assert_eq!(
3808            entry.response.as_ref().unwrap().headers[0].name,
3809            "X-Response"
3810        );
3811        assert_eq!(entry.response.as_ref().unwrap().headers[0].value, "world");
3812    }
3813
3814    #[tokio::test]
3815    async fn test_ingest_response_from_cache() {
3816        let log = NetworkLog::new();
3817        log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET"))
3818            .await;
3819        let mut resp = make_response_completed("req-1", "https://a.com", 200);
3820        resp.params.response.from_cache = true;
3821        log.ingest_response_completed(&resp).await;
3822        let entry = log.first().await.unwrap();
3823        assert!(entry.response.as_ref().unwrap().from_cache);
3824    }
3825
3826    // ---------- empty log query behavior ----------
3827
3828    #[tokio::test]
3829    async fn test_first_on_empty_log() {
3830        let log = NetworkLog::new();
3831        assert!(log.first().await.is_none());
3832    }
3833
3834    #[tokio::test]
3835    async fn test_last_on_empty_log() {
3836        let log = NetworkLog::new();
3837        assert!(log.last().await.is_none());
3838    }
3839
3840    #[tokio::test]
3841    async fn test_nth_on_empty_log() {
3842        let log = NetworkLog::new();
3843        assert!(log.nth(0).await.is_none());
3844    }
3845
3846    #[tokio::test]
3847    async fn test_find_by_url_on_empty_log() {
3848        let log = NetworkLog::new();
3849        assert!(log.find_by_url("anything").await.is_none());
3850    }
3851
3852    #[tokio::test]
3853    async fn test_filter_on_empty_log() {
3854        let log = NetworkLog::new();
3855        assert!(log.filter(Filter::new()).await.is_empty());
3856    }
3857
3858    #[tokio::test]
3859    async fn test_count_on_empty_log() {
3860        let log = NetworkLog::new();
3861        assert_eq!(log.count(Filter::new()).await, 0);
3862    }
3863
3864    #[tokio::test]
3865    async fn test_completed_on_empty_log() {
3866        let log = NetworkLog::new();
3867        assert!(log.completed().await.is_empty());
3868    }
3869
3870    #[tokio::test]
3871    async fn test_endpoints_on_empty_log() {
3872        let log = NetworkLog::new();
3873        assert!(log.endpoints().await.is_empty());
3874    }
3875
3876    #[tokio::test]
3877    async fn test_hostnames_on_empty_log() {
3878        let log = NetworkLog::new();
3879        assert!(log.hostnames().await.is_empty());
3880    }
3881
3882    #[tokio::test]
3883    async fn test_distinct_methods_on_empty_log() {
3884        let log = NetworkLog::new();
3885        assert!(log.distinct_methods().await.is_empty());
3886    }
3887
3888    #[tokio::test]
3889    async fn test_distinct_statuses_on_empty_log() {
3890        let log = NetworkLog::new();
3891        assert!(log.distinct_statuses().await.is_empty());
3892    }
3893
3894    #[tokio::test]
3895    async fn test_total_bytes_on_empty_log() {
3896        let log = NetworkLog::new();
3897        assert_eq!(log.total_bytes_in().await, 0);
3898        assert_eq!(log.total_bytes_out().await, 0);
3899    }
3900
3901    #[tokio::test]
3902    async fn test_contains_id_on_empty_log() {
3903        let log = NetworkLog::new();
3904        assert!(!log.contains_id("anything").await);
3905    }
3906
3907    #[tokio::test]
3908    async fn test_remove_by_id_on_empty_log() {
3909        let log = NetworkLog::new();
3910        assert!(log.remove_by_id("anything").await.is_none());
3911    }
3912
3913    #[tokio::test]
3914    async fn test_clear_on_empty_log() {
3915        let log = NetworkLog::new();
3916        log.clear().await;
3917        assert!(log.is_empty().await);
3918    }
3919
3920    #[tokio::test]
3921    async fn test_subscribe_on_empty_log() {
3922        let log = NetworkLog::new();
3923        let mut rx = log.subscribe().await;
3924        assert!(rx.try_recv().is_err());
3925    }
3926
3927    // ---------- remove_by_id edge cases ----------
3928
3929    #[tokio::test]
3930    async fn test_remove_by_id_twice() {
3931        let log = NetworkLog::new();
3932        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3933        assert!(log.remove_by_id("1").await.is_some());
3934        assert!(log.remove_by_id("1").await.is_none());
3935    }
3936
3937    #[tokio::test]
3938    async fn test_remove_by_id_only_entry() {
3939        let log = NetworkLog::new();
3940        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3941        log.remove_by_id("1").await;
3942        assert!(log.is_empty().await);
3943        assert!(!log.contains_id("1").await);
3944    }
3945
3946    #[tokio::test]
3947    async fn test_concurrent_remove_by_id() {
3948        let log = NetworkLog::new();
3949        for i in 0..100 {
3950            push_request(
3951                &log,
3952                make_request(&format!("{}", i), "GET", "https://a.com"),
3953            )
3954            .await;
3955        }
3956        let mut handles = Vec::new();
3957        for i in 0..100 {
3958            let log = log.clone();
3959            handles.push(tokio::spawn(async move {
3960                log.remove_by_id(&format!("{}", i)).await
3961            }));
3962        }
3963        for h in handles {
3964            h.await.unwrap();
3965        }
3966        assert!(log.is_empty().await);
3967    }
3968
3969    // ---------- wait_for edge cases ----------
3970
3971    #[tokio::test]
3972    async fn test_wait_for_url_empty_substring() {
3973        let log = NetworkLog::new();
3974        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3975        let found = log
3976            .wait_for_url("", std::time::Duration::from_secs(1))
3977            .await;
3978        assert!(found.is_some());
3979    }
3980
3981    #[tokio::test]
3982    async fn test_wait_for_response_nonexistent_id() {
3983        let log = NetworkLog::new();
3984        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3985        let found = log
3986            .wait_for_response("does-not-exist", std::time::Duration::from_millis(50))
3987            .await;
3988        assert!(found.is_none());
3989    }
3990
3991    // ---------- json_body edge cases ----------
3992
3993    #[tokio::test]
3994    async fn test_json_body_array() {
3995        let mut req = make_request("1", "POST", "https://a.com");
3996        req.post_data = Some("[1, 2, 3]".into());
3997        assert_eq!(req.json_body(), Some(serde_json::json!([1, 2, 3])));
3998    }
3999
4000    #[tokio::test]
4001    async fn test_json_body_nested() {
4002        let mut req = make_request("1", "POST", "https://a.com");
4003        req.post_data = Some(r#"{"outer": {"inner": [true, false]}}"#.into());
4004        assert_eq!(
4005            req.json_body(),
4006            Some(serde_json::json!({"outer": {"inner": [true, false]}}))
4007        );
4008    }
4009
4010    // ---------- curl edge cases ----------
4011
4012    #[tokio::test]
4013    async fn test_to_curl_delete_method() {
4014        let req = make_request("1", "DELETE", "https://api.example.com/resource/1");
4015        let entry = NetworkEntry {
4016            request: req,
4017            response: None,
4018            error: None,
4019        };
4020        assert!(entry.to_curl().starts_with("curl -X 'DELETE'"));
4021    }
4022
4023    #[tokio::test]
4024    async fn test_to_curl_url_with_single_quotes() {
4025        let req = make_request("1", "GET", "https://example.com?foo='bar'");
4026        let entry = NetworkEntry {
4027            request: req,
4028            response: None,
4029            error: None,
4030        };
4031        let curl = entry.to_curl();
4032        assert!(curl.contains("'https://example.com?foo='\\''bar'\\'''"));
4033    }
4034
4035    #[tokio::test]
4036    async fn test_to_curl_header_with_single_quotes() {
4037        let req = make_request_with_headers(
4038            "1",
4039            "GET",
4040            "https://example.com",
4041            vec![CapturedHeader {
4042                name: "X-Quote".into(),
4043                value: "it's working".into(),
4044            }],
4045        );
4046        let entry = NetworkEntry {
4047            request: req,
4048            response: None,
4049            error: None,
4050        };
4051        let curl = entry.to_curl();
4052        assert!(curl.contains("-H 'X-Quote: it'\\''s working'"));
4053    }
4054
4055    // ---------- serialization of public structs ----------
4056
4057    #[tokio::test]
4058    async fn test_network_metrics_roundtrip() {
4059        let m = NetworkMetrics {
4060            requests_received: 10,
4061            responses_received: 8,
4062            errors_received: 2,
4063            entries_evicted: 5,
4064            pending_responses_dropped: 1,
4065            pending_errors_dropped: 0,
4066            broadcast_drops: 3,
4067            duplicate_responses: 1,
4068            duplicate_errors: 0,
4069            max_entries: 100,
4070        };
4071        let json = serde_json::to_string(&m).unwrap();
4072        let de: NetworkMetrics = serde_json::from_str(&json).unwrap();
4073        assert_eq!(de, m);
4074    }
4075
4076    // ---------- unicode / IDN ----------
4077
4078    #[tokio::test]
4079    async fn test_find_by_url_unicode() {
4080        let log = NetworkLog::new();
4081        push_request(&log, make_request("1", "GET", "https://例子.com/path")).await;
4082        assert!(log.find_by_url("例子").await.is_some());
4083    }
4084
4085    #[tokio::test]
4086    async fn test_hostnames_idn() {
4087        let log = NetworkLog::new();
4088        push_request(
4089            &log,
4090            make_request("1", "GET", "https://xn--fsq092h.com/path"),
4091        )
4092        .await;
4093        let h = log.hostnames().await;
4094        assert_eq!(h.len(), 1);
4095        assert!(h.contains(&"xn--fsq092h.com".into()));
4096    }
4097
4098    // ---------- total_bytes_out multibyte ----------
4099
4100    #[tokio::test]
4101    async fn test_total_bytes_out_multibyte() {
4102        let log = NetworkLog::new();
4103        let mut req = make_request("1", "POST", "https://a.com");
4104        req.post_data = Some("日本語".into()); // 9 bytes in UTF-8
4105        push_entry(&log, req, None, None).await;
4106        assert_eq!(log.total_bytes_out().await, 9);
4107    }
4108
4109    // ---------- completed with only errors ----------
4110
4111    #[tokio::test]
4112    async fn test_completed_only_errors() {
4113        let log = NetworkLog::new();
4114        push_entry(
4115            &log,
4116            make_request("1", "GET", "https://a.com"),
4117            None,
4118            Some(make_error("1", "https://a.com", "err")),
4119        )
4120        .await;
4121        push_entry(
4122            &log,
4123            make_request("2", "GET", "https://b.com"),
4124            None,
4125            Some(make_error("2", "https://b.com", "err")),
4126        )
4127        .await;
4128        let c = log.completed().await;
4129        assert_eq!(c.len(), 2);
4130    }
4131
4132    // ---------- distinct with all same ----------
4133
4134    #[tokio::test]
4135    async fn test_distinct_statuses_all_same() {
4136        let log = NetworkLog::new();
4137        for i in 0..5 {
4138            push_entry(
4139                &log,
4140                make_request(&format!("{}", i), "GET", "https://a.com"),
4141                Some(make_response(&format!("{}", i), 200, "https://a.com")),
4142                None,
4143            )
4144            .await;
4145        }
4146        assert_eq!(log.distinct_statuses().await, vec![200]);
4147    }
4148
4149    #[tokio::test]
4150    async fn test_distinct_methods_all_same() {
4151        let log = NetworkLog::new();
4152        for i in 0..5 {
4153            push_request(
4154                &log,
4155                make_request(&format!("{}", i), "POST", "https://a.com"),
4156            )
4157            .await;
4158        }
4159        assert_eq!(log.distinct_methods().await, vec!["POST"]);
4160    }
4161
4162    // ---------- clear then subscribe then push ----------
4163
4164    #[tokio::test]
4165    async fn test_clear_subscribe_push() {
4166        let log = NetworkLog::new();
4167        log.clear().await;
4168        let mut rx = log.subscribe().await;
4169        push_request(&log, make_request("1", "GET", "https://a.com")).await;
4170        assert_eq!(rx.recv().await.unwrap().request.id, "1");
4171    }
4172
4173    // ---------- from_cache does not affect byte count ----------
4174
4175    #[tokio::test]
4176    async fn test_total_bytes_in_from_cache() {
4177        let log = NetworkLog::new();
4178        let mut resp = make_response("1", 200, "https://a.com");
4179        resp.from_cache = true;
4180        resp.body_size = Some(500);
4181        push_entry(
4182            &log,
4183            make_request("1", "GET", "https://a.com"),
4184            Some(resp),
4185            None,
4186        )
4187        .await;
4188        assert_eq!(log.total_bytes_in().await, 500);
4189    }
4190
4191    // ---------- new fixes ----------
4192
4193    #[tokio::test]
4194    async fn test_to_curl_produces_valid_shell_with_quotes() {
4195        let req = CapturedRequest {
4196            id: "1".into(),
4197            context: None,
4198            method: "POST".into(),
4199            url: "https://example.com".into(),
4200            headers: vec![CapturedHeader {
4201                name: "X-Quote".into(),
4202                value: "it's".into(),
4203            }],
4204            post_data: Some("data='val'".into()),
4205            timestamp: 0,
4206            destination: "document".into(),
4207            initiator_type: None,
4208            timing: CapturedTiming::default(),
4209            cookies: vec![],
4210        };
4211        let entry = NetworkEntry {
4212            request: req,
4213            response: None,
4214            error: None,
4215        };
4216        let curl = entry.to_curl();
4217        // Every single quote should be escaped as '\''
4218        // So the output should NOT contain any bare single quotes inside quoted regions
4219        // We verify by checking the escaping pattern is present
4220        assert!(curl.contains("it'\\''s"));
4221        assert!(curl.contains("data='\\''val'\\'''"));
4222    }
4223
4224    #[tokio::test]
4225    async fn test_save_as_har_postdata_uses_content_type_header() {
4226        let log = NetworkLog::new();
4227        let mut req = make_request("1", "POST", "https://api.example.com");
4228        req.post_data = Some(r#"{"key":"value"}"#.into());
4229        req.headers = vec![CapturedHeader {
4230            name: "Content-Type".into(),
4231            value: "application/vnd.api+json".into(),
4232        }];
4233        push_entry(
4234            &log,
4235            req,
4236            Some(make_response("1", 201, "https://api.example.com")),
4237            None,
4238        )
4239        .await;
4240
4241        let path = std::path::Path::new("/tmp/foxdriver_har_mime.har");
4242        log.save_as_har(path, None).await.unwrap();
4243        let content = std::fs::read_to_string(path).unwrap();
4244        // The postData should use the request's Content-Type
4245        assert!(content.contains("\"mimeType\": \"application/vnd.api+json\""));
4246    }
4247
4248    #[tokio::test]
4249    async fn test_save_as_har_postdata_defaults_octet_stream() {
4250        let log = NetworkLog::new();
4251        let mut req = make_request("1", "POST", "https://api.example.com");
4252        req.post_data = Some("raw bytes".into());
4253        // No Content-Type header
4254        push_entry(
4255            &log,
4256            req,
4257            Some(make_response("1", 201, "https://api.example.com")),
4258            None,
4259        )
4260        .await;
4261
4262        let path = std::path::Path::new("/tmp/foxdriver_har_default_mime.har");
4263        log.save_as_har(path, None).await.unwrap();
4264        let content = std::fs::read_to_string(path).unwrap();
4265        assert!(content.contains("\"mimeType\": \"application/octet-stream\""));
4266    }
4267
4268    #[tokio::test]
4269    async fn test_concurrent_reads_with_rwlock() {
4270        let log = NetworkLog::new();
4271        for i in 0..100 {
4272            push_request(
4273                &log,
4274                make_request(&format!("{}", i), "GET", "https://example.com"),
4275            )
4276            .await;
4277        }
4278        let mut handles = Vec::new();
4279        for _ in 0..20 {
4280            let log = log.clone();
4281            handles.push(tokio::spawn(async move {
4282                for _ in 0..50 {
4283                    let _ = log.len().await;
4284                    let _ = log.entries().await;
4285                    let _ = log.distinct_methods().await;
4286                    let _ = log.total_bytes_in().await;
4287                }
4288            }));
4289        }
4290        for h in handles {
4291            h.await.unwrap();
4292        }
4293        // No deadlock means success
4294        assert_eq!(log.len().await, 100);
4295    }
4296
4297    // ---------- new bounty features ----------
4298
4299    // ---------- convenience queries ----------
4300
4301    #[tokio::test]
4302    async fn test_find_by_status() {
4303        let log = NetworkLog::new();
4304        push_entry(
4305            &log,
4306            make_request("1", "GET", "https://a.com"),
4307            Some(make_response("1", 200, "https://a.com")),
4308            None,
4309        )
4310        .await;
4311        push_entry(
4312            &log,
4313            make_request("2", "GET", "https://b.com"),
4314            Some(make_response("2", 404, "https://b.com")),
4315            None,
4316        )
4317        .await;
4318        push_entry(
4319            &log,
4320            make_request("3", "GET", "https://c.com"),
4321            None,
4322            Some(make_error("3", "https://c.com", "err")),
4323        )
4324        .await;
4325        assert_eq!(log.find_by_status(200).await.len(), 1);
4326        assert_eq!(log.find_by_status(404).await.len(), 1);
4327        assert_eq!(log.find_by_status(500).await.len(), 0);
4328    }
4329
4330    #[tokio::test]
4331    async fn test_entries_since() {
4332        let log = NetworkLog::new();
4333        let mut req1 = make_request("1", "GET", "https://a.com");
4334        req1.timestamp = 1000;
4335        let mut req2 = make_request("2", "GET", "https://b.com");
4336        req2.timestamp = 2000;
4337        let mut req3 = make_request("3", "GET", "https://c.com");
4338        req3.timestamp = 3000;
4339        push_request(&log, req1).await;
4340        push_request(&log, req2).await;
4341        push_request(&log, req3).await;
4342        assert_eq!(log.entries_since(0).await.len(), 3);
4343        assert_eq!(log.entries_since(2000).await.len(), 2);
4344        assert_eq!(log.entries_since(3001).await.len(), 0);
4345    }
4346
4347    #[tokio::test]
4348    async fn test_last_n() {
4349        let log = NetworkLog::new();
4350        push_request(&log, make_request("1", "GET", "https://a.com")).await;
4351        push_request(&log, make_request("2", "GET", "https://b.com")).await;
4352        push_request(&log, make_request("3", "GET", "https://c.com")).await;
4353        let last = log.last_n(2).await;
4354        assert_eq!(last.len(), 2);
4355        assert_eq!(last[0].request.id, "2");
4356        assert_eq!(last[1].request.id, "3");
4357        assert_eq!(log.last_n(10).await.len(), 3);
4358        assert!(log.last_n(0).await.is_empty());
4359    }
4360
4361    // ---------- security analysis ----------
4362
4363    // ---------- additional bounty analysis ----------
4364
4365    #[tokio::test]
4366    async fn test_unique_urls() {
4367        let log = NetworkLog::new();
4368        push_request(&log, make_request("1", "GET", "https://a.com")).await;
4369        push_request(&log, make_request("2", "GET", "https://b.com")).await;
4370        push_request(&log, make_request("3", "GET", "https://a.com")).await;
4371        let urls = log.unique_urls().await;
4372        assert_eq!(urls.len(), 2);
4373        assert!(urls.contains(&"https://a.com".to_string()));
4374        assert!(urls.contains(&"https://b.com".to_string()));
4375    }
4376
4377    #[tokio::test]
4378    async fn test_unique_urls_empty() {
4379        let log = NetworkLog::new();
4380        assert!(log.unique_urls().await.is_empty());
4381    }
4382
4383    // ---------- reliability ----------
4384
4385    #[tokio::test]
4386    async fn test_broadcast_drops_tracked_when_no_receivers() {
4387        let log = NetworkLog::new();
4388        // No subscribers active
4389        push_request(&log, make_request("1", "GET", "https://a.com")).await;
4390        let m = log.metrics().await;
4391        assert_eq!(m.broadcast_drops, 1);
4392    }
4393
4394    #[tokio::test]
4395    async fn test_broadcast_no_drop_when_receiver_active() {
4396        let log = NetworkLog::new();
4397        let _rx = log.subscribe().await;
4398        push_request(&log, make_request("1", "GET", "https://a.com")).await;
4399        let m = log.metrics().await;
4400        assert_eq!(m.broadcast_drops, 0);
4401    }
4402
4403    #[tokio::test]
4404    async fn test_pending_count_and_drain() {
4405        let log = NetworkLog::new();
4406        // Inject a response before its request (out-of-order)
4407        {
4408            let mut inner = log.inner.write().await;
4409            inner.pending_responses.insert(
4410                "orphan-1".into(),
4411                make_response("orphan-1", 200, "https://a.com"),
4412            );
4413            inner.pending_errors.insert(
4414                "orphan-2".into(),
4415                make_error("orphan-2", "https://b.com", "err"),
4416            );
4417        }
4418        assert_eq!(log.pending_count().await, 2);
4419
4420        let (responses, errors) = log.drain_pending().await;
4421        assert_eq!(responses.len(), 1);
4422        assert_eq!(errors.len(), 1);
4423        assert_eq!(log.pending_count().await, 0);
4424    }
4425
4426    #[tokio::test]
4427    async fn test_pending_count_empty() {
4428        let log = NetworkLog::new();
4429        assert_eq!(log.pending_count().await, 0);
4430        let (r, e) = log.drain_pending().await;
4431        assert!(r.is_empty() && e.is_empty());
4432    }
4433
4434    #[tokio::test]
4435    async fn test_retain_filters_entries() {
4436        let log = NetworkLog::new();
4437        push_request(&log, make_request("1", "GET", "https://keep.com")).await;
4438        push_request(&log, make_request("2", "GET", "https://drop.com")).await;
4439        push_request(&log, make_request("3", "GET", "https://keep.com/path")).await;
4440        log.retain(|e| e.request.url.contains("keep")).await;
4441        assert_eq!(log.len().await, 2);
4442        assert!(log.contains_id("1").await);
4443        assert!(!log.contains_id("2").await);
4444        assert!(log.contains_id("3").await);
4445    }
4446
4447    #[tokio::test]
4448    async fn test_retain_rebuilds_indices() {
4449        let log = NetworkLog::new();
4450        for i in 0..5 {
4451            push_request(
4452                &log,
4453                make_request(&format!("{}", i), "GET", &format!("https://{}.com", i)),
4454            )
4455            .await;
4456        }
4457        log.retain(|e| e.request.id.parse::<i32>().unwrap() % 2 == 0)
4458            .await;
4459        assert_eq!(log.len().await, 3);
4460        assert!(log.contains_id("0").await);
4461        assert!(log.contains_id("2").await);
4462        assert!(log.contains_id("4").await);
4463    }
4464
4465    #[tokio::test]
4466    async fn test_deduplication_guard_ignores_duplicate_request_id() {
4467        let log = NetworkLog::new();
4468        push_request(&log, make_request("1", "GET", "https://first.com")).await;
4469        push_request(&log, make_request("1", "GET", "https://second.com")).await;
4470        assert_eq!(log.len().await, 1);
4471        assert_eq!(log.first().await.unwrap().request.url, "https://first.com");
4472    }
4473
4474    #[tokio::test]
4475    async fn test_duplicate_response_tracked_in_metrics() {
4476        let log = NetworkLog::new();
4477        push_entry(
4478            &log,
4479            make_request("1", "GET", "https://a.com"),
4480            Some(make_response("1", 200, "https://a.com")),
4481            None,
4482        )
4483        .await;
4484        // Simulate a second response for the same request
4485        {
4486            let mut inner = log.inner.write().await;
4487            if let Some(idx) = inner.by_id.get("1").copied() {
4488                let mut new_entry = (*inner.entries[idx]).clone();
4489                new_entry.response = Some(make_response("1", 201, "https://a.com"));
4490                inner.entries[idx] = Arc::new(new_entry);
4491            }
4492        }
4493        // We can't easily trigger ingest_response_completed without real BiDi events,
4494        // but we verify the metric field exists and is initialized to 0
4495        let m = log.metrics().await;
4496        assert_eq!(m.duplicate_responses, 0); // No duplicate through normal path yet
4497    }
4498
4499    #[tokio::test]
4500    async fn test_request_ids_returns_all_ids() {
4501        let log = NetworkLog::new();
4502        push_request(&log, make_request("a", "GET", "https://a.com")).await;
4503        push_request(&log, make_request("b", "GET", "https://b.com")).await;
4504        let ids = log.request_ids().await;
4505        assert_eq!(ids, vec!["a", "b"]);
4506    }
4507
4508    #[tokio::test]
4509    async fn test_has_response_and_has_error() {
4510        let log = NetworkLog::new();
4511        push_entry(
4512            &log,
4513            make_request("1", "GET", "https://a.com"),
4514            Some(make_response("1", 200, "https://a.com")),
4515            None,
4516        )
4517        .await;
4518        push_entry(
4519            &log,
4520            make_request("2", "GET", "https://b.com"),
4521            None,
4522            Some(make_error("2", "https://b.com", "err")),
4523        )
4524        .await;
4525        push_request(&log, make_request("3", "GET", "https://c.com")).await;
4526        assert!(log.has_response("1").await);
4527        assert!(!log.has_error("1").await);
4528        assert!(!log.has_response("2").await);
4529        assert!(log.has_error("2").await);
4530        assert!(!log.has_response("3").await);
4531        assert!(!log.has_error("3").await);
4532        assert!(!log.has_response("missing").await);
4533        assert!(!log.has_error("missing").await);
4534    }
4535
4536    // ---------- invariant tests ----------
4537
4538    #[tokio::test]
4539    async fn test_invariant_len_equals_entries_len() {
4540        let log = NetworkLog::new();
4541        for i in 0..50 {
4542            push_request(
4543                &log,
4544                make_request(&format!("{}", i), "GET", "https://x.com"),
4545            )
4546            .await;
4547            assert_eq!(log.len().await, log.entries().await.len());
4548        }
4549    }
4550
4551    #[tokio::test]
4552    async fn test_invariant_is_empty_iff_len_zero() {
4553        let log = NetworkLog::new();
4554        assert_eq!(log.is_empty().await, log.len().await == 0);
4555        push_request(&log, make_request("1", "GET", "https://x.com")).await;
4556        assert_eq!(log.is_empty().await, log.len().await == 0);
4557        log.clear().await;
4558        assert_eq!(log.is_empty().await, log.len().await == 0);
4559    }
4560
4561    #[tokio::test]
4562    async fn test_invariant_by_id_indices_are_valid() {
4563        let log = NetworkLog::new();
4564        for i in 0..100 {
4565            push_request(
4566                &log,
4567                make_request(&format!("{}", i), "GET", &format!("https://{}.com", i)),
4568            )
4569            .await;
4570        }
4571        let inner = log.inner.read().await;
4572        for (id, idx) in &inner.by_id {
4573            assert!(
4574                *idx < inner.entries.len(),
4575                "id {} has invalid index {}",
4576                id,
4577                idx
4578            );
4579            assert_eq!(inner.entries[*idx].request.id, *id);
4580        }
4581    }
4582
4583    #[tokio::test]
4584    async fn test_invariant_no_duplicate_ids() {
4585        let log = NetworkLog::new();
4586        for i in 0..100 {
4587            push_request(
4588                &log,
4589                make_request(&format!("{}", i), "GET", "https://x.com"),
4590            )
4591            .await;
4592        }
4593        let inner = log.inner.read().await;
4594        let mut seen = std::collections::HashSet::new();
4595        for e in &inner.entries {
4596            assert!(seen.insert(&e.request.id), "duplicate id {}", e.request.id);
4597        }
4598    }
4599
4600    #[tokio::test]
4601    async fn test_invariant_first_and_last_consistent() {
4602        let log = NetworkLog::new();
4603        assert_eq!(log.first().await, None);
4604        assert_eq!(log.last().await, None);
4605        push_request(&log, make_request("1", "GET", "https://a.com")).await;
4606        push_request(&log, make_request("2", "GET", "https://b.com")).await;
4607        let entries = log.entries().await;
4608        assert_eq!(
4609            log.first().await.as_deref(),
4610            entries.first().map(Arc::as_ref)
4611        );
4612        assert_eq!(log.last().await.as_deref(), entries.last().map(Arc::as_ref));
4613    }
4614
4615    #[tokio::test]
4616    async fn test_invariant_filter_count_consistent() {
4617        let log = NetworkLog::new();
4618        for i in 0..20 {
4619            let status = if i % 2 == 0 { 200 } else { 404 };
4620            push_entry(
4621                &log,
4622                make_request(&format!("{}", i), "GET", "https://x.com"),
4623                Some(make_response(&format!("{}", i), status, "https://x.com")),
4624                None,
4625            )
4626            .await;
4627        }
4628        let f = Filter::new().status_range(200..=200);
4629        let filtered = log.filter(f.clone()).await;
4630        let count = log.count(f).await;
4631        assert_eq!(filtered.len(), count);
4632    }
4633
4634    #[tokio::test]
4635    async fn test_invariant_metrics_monotonic() {
4636        let log = NetworkLog::new();
4637        let m0 = log.metrics().await;
4638        push_request(&log, make_request("1", "GET", "https://a.com")).await;
4639        let m1 = log.metrics().await;
4640        assert!(m1.requests_received >= m0.requests_received);
4641        assert!(m1.responses_received >= m0.responses_received);
4642        assert!(m1.errors_received >= m0.errors_received);
4643    }
4644
4645    #[tokio::test]
4646    async fn test_invariant_pending_count_bounded() {
4647        let log = NetworkLog::with_limits(100, 10);
4648        // Insert 20 orphaned pending responses directly (bypassing capping)
4649        {
4650            let mut inner = log.inner.write().await;
4651            for i in 0..20 {
4652                inner.pending_responses.insert(
4653                    format!("orphan-{}", i),
4654                    make_response(&format!("{}", i), 200, "https://x.com"),
4655                );
4656            }
4657        }
4658        // Direct insertion bypasses capping; verify drain_pending can clean them
4659        assert_eq!(log.pending_count().await, 20);
4660        let (r, _) = log.drain_pending().await;
4661        assert_eq!(r.len(), 20);
4662        assert_eq!(log.pending_count().await, 0);
4663    }
4664
4665    #[tokio::test]
4666    async fn test_invariant_after_clear_all_empty() {
4667        let log = NetworkLog::new();
4668        for i in 0..50 {
4669            push_request(
4670                &log,
4671                make_request(&format!("{}", i), "GET", "https://x.com"),
4672            )
4673            .await;
4674        }
4675        log.clear().await;
4676        assert!(log.is_empty().await);
4677        assert_eq!(log.pending_count().await, 0);
4678        assert!(!log.contains_id("1").await);
4679        let inner = log.inner.read().await;
4680        assert!(inner.by_id.is_empty());
4681    }
4682
4683    #[tokio::test]
4684    async fn test_invariant_after_remove_id_not_present() {
4685        let log = NetworkLog::new();
4686        for i in 0..50 {
4687            push_request(
4688                &log,
4689                make_request(&format!("{}", i), "GET", "https://x.com"),
4690            )
4691            .await;
4692        }
4693        log.remove_by_id("25").await;
4694        assert!(!log.contains_id("25").await);
4695        let inner = log.inner.read().await;
4696        assert!(!inner.by_id.contains_key("25"));
4697        assert_eq!(inner.entries.len(), 49);
4698        // Verify all remaining indices are valid
4699        for (id, idx) in &inner.by_id {
4700            assert!(inner.entries[*idx].request.id == *id);
4701        }
4702    }
4703
4704    #[tokio::test]
4705    async fn test_invariant_retain_all_satisfy_predicate() {
4706        let log = NetworkLog::new();
4707        for i in 0..50 {
4708            push_request(
4709                &log,
4710                make_request(&format!("{}", i), "GET", &format!("https://{}.com", i % 3)),
4711            )
4712            .await;
4713        }
4714        log.retain(|e| e.request.url.contains("0.com") || e.request.url.contains("1.com"))
4715            .await;
4716        for e in log.entries().await {
4717            assert!(!e.request.url.contains("2.com"));
4718        }
4719        assert_eq!(log.len().await, 34); // 0,1 out of 0,1,2 = 2/3
4720    }
4721
4722    #[tokio::test]
4723    async fn test_invariant_nth_matches_entries_index() {
4724        let log = NetworkLog::new();
4725        for i in 0..20 {
4726            push_request(
4727                &log,
4728                make_request(&format!("{}", i), "GET", &format!("https://{}.com", i)),
4729            )
4730            .await;
4731        }
4732        let entries = log.entries().await;
4733        for (i, entry) in entries.iter().enumerate() {
4734            assert_eq!(log.nth(i).await.as_deref(), Some(entry.as_ref()));
4735        }
4736        assert!(log.nth(100).await.is_none());
4737    }
4738
4739    #[tokio::test]
4740    async fn test_invariant_last_n_order_preserved() {
4741        let log = NetworkLog::new();
4742        for i in 0..10 {
4743            push_request(
4744                &log,
4745                make_request(&format!("{}", i), "GET", "https://x.com"),
4746            )
4747            .await;
4748        }
4749        let last = log.last_n(3).await;
4750        assert_eq!(last.len(), 3);
4751        assert_eq!(last[0].request.id, "7");
4752        assert_eq!(last[1].request.id, "8");
4753        assert_eq!(last[2].request.id, "9");
4754    }
4755
4756    #[tokio::test]
4757    async fn test_invariant_entries_since_time_based() {
4758        let log = NetworkLog::new();
4759        for i in 0..10 {
4760            let mut req = make_request(&format!("{}", i), "GET", "https://x.com");
4761            req.timestamp = i as u64 * 1000;
4762            push_request(&log, req).await;
4763        }
4764        let since = log.entries_since(5000).await;
4765        assert_eq!(since.len(), 5); // 5,6,7,8,9
4766        for e in &since {
4767            assert!(e.request.timestamp >= 5000);
4768        }
4769    }
4770
4771    // ---------- concurrent stress tests ----------
4772
4773    #[tokio::test]
4774    async fn test_stress_concurrent_ingestion_and_query() {
4775        let log = NetworkLog::new();
4776        let mut handles = Vec::new();
4777
4778        // 10 ingestion tasks
4779        for t in 0..10 {
4780            let log = log.clone();
4781            handles.push(tokio::spawn(async move {
4782                for i in 0..100 {
4783                    let id = format!("t{}-{}", t, i);
4784                    push_request(&log, make_request(&id, "GET", "https://x.com")).await;
4785                }
4786            }));
4787        }
4788
4789        // 10 query tasks
4790        for _ in 0..10 {
4791            let log = log.clone();
4792            handles.push(tokio::spawn(async move {
4793                for _ in 0..100 {
4794                    let _ = log.len().await;
4795                    let _ = log.entries().await;
4796                    let _ = log.first().await;
4797                    let _ = log.last().await;
4798                    let _ = log.metrics().await;
4799                    tokio::task::yield_now().await;
4800                }
4801            }));
4802        }
4803
4804        for h in handles {
4805            h.await.unwrap();
4806        }
4807
4808        assert_eq!(log.len().await, 1000);
4809        let inner = log.inner.read().await;
4810        assert_eq!(inner.by_id.len(), 1000);
4811    }
4812
4813    #[tokio::test]
4814    async fn test_stress_concurrent_subscribers() {
4815        let log = NetworkLog::new();
4816        let mut rxs = Vec::new();
4817        for _ in 0..50 {
4818            rxs.push(log.subscribe().await);
4819        }
4820
4821        for i in 0..100 {
4822            push_request(
4823                &log,
4824                make_request(&format!("{}", i), "GET", "https://x.com"),
4825            )
4826            .await;
4827        }
4828
4829        // Each subscriber should have received at most 100 entries
4830        // (some may have been dropped if the channel filled)
4831        for mut rx in rxs {
4832            let mut count = 0;
4833            while rx.try_recv().is_ok() {
4834                count += 1;
4835            }
4836            assert!(count <= 100);
4837        }
4838    }
4839
4840    #[tokio::test]
4841    async fn test_stress_eviction_under_load() {
4842        let log = NetworkLog::with_limits(100, 10);
4843        for i in 0..500 {
4844            push_request(
4845                &log,
4846                make_request(&format!("{}", i), "GET", "https://x.com"),
4847            )
4848            .await;
4849        }
4850        assert_eq!(log.len().await, 100);
4851        let m = log.metrics().await;
4852        assert!(m.entries_evicted > 0);
4853    }
4854
4855    #[tokio::test]
4856    async fn test_stress_retain_under_load() {
4857        let log = NetworkLog::new();
4858        for i in 0..1000 {
4859            push_request(
4860                &log,
4861                make_request(&format!("{}", i), "GET", &format!("https://{}.com", i % 10)),
4862            )
4863            .await;
4864        }
4865        log.retain(|e| e.request.url.contains("0.com") || e.request.url.contains("1.com"))
4866            .await;
4867        assert_eq!(log.len().await, 200);
4868        let inner = log.inner.read().await;
4869        assert_eq!(inner.by_id.len(), 200);
4870    }
4871
4872    #[tokio::test]
4873    async fn test_stress_memory_estimate_grows_with_entries() {
4874        let log = NetworkLog::new();
4875        let m0 = log.memory_estimate().await;
4876        for i in 0..100 {
4877            let mut req = make_request(&format!("{}", i), "POST", "https://x.com");
4878            req.post_data = Some("x".repeat(1000));
4879            push_request(&log, req).await;
4880        }
4881        let m1 = log.memory_estimate().await;
4882        assert!(m1 > m0);
4883    }
4884
4885    // ---------- HAR validation ----------
4886
4887    #[tokio::test]
4888    async fn test_har_is_valid_json() {
4889        let log = NetworkLog::new();
4890        push_entry(
4891            &log,
4892            make_request("1", "GET", "https://a.com"),
4893            Some(make_response("1", 200, "https://a.com")),
4894            None,
4895        )
4896        .await;
4897        let path = std::path::Path::new("/tmp/foxdriver_har_valid.json");
4898        log.save_as_har(path, Some("test")).await.unwrap();
4899        let content = std::fs::read_to_string(path).unwrap();
4900        let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
4901        assert_eq!(parsed["log"]["version"], "1.2");
4902        assert!(parsed["log"]["entries"].is_array());
4903        assert_eq!(parsed["log"]["entries"].as_array().unwrap().len(), 1);
4904    }
4905
4906    #[tokio::test]
4907    async fn test_har_has_required_fields() {
4908        let log = NetworkLog::new();
4909        push_entry(
4910            &log,
4911            make_request("1", "POST", "https://api.example.com"),
4912            Some(make_response("1", 201, "https://api.example.com")),
4913            None,
4914        )
4915        .await;
4916        let path = std::path::Path::new("/tmp/foxdriver_har_fields.json");
4917        log.save_as_har(path, None).await.unwrap();
4918        let content = std::fs::read_to_string(path).unwrap();
4919        let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
4920        let entry = &parsed["log"]["entries"][0];
4921        assert!(entry["startedDateTime"].is_string());
4922        assert!(entry["request"]["method"].is_string());
4923        assert!(entry["request"]["url"].is_string());
4924        assert!(entry["response"]["status"].is_number());
4925        assert!(entry["response"]["statusText"].is_string());
4926        assert!(entry["timings"].is_object());
4927    }
4928
4929    #[tokio::test]
4930    async fn test_har_empty_log_has_zero_entries() {
4931        let log = NetworkLog::new();
4932        let path = std::path::Path::new("/tmp/foxdriver_har_empty_valid.json");
4933        log.save_as_har(path, None).await.unwrap();
4934        let content = std::fs::read_to_string(path).unwrap();
4935        let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
4936        assert_eq!(parsed["log"]["entries"].as_array().unwrap().len(), 0);
4937    }
4938
4939    #[tokio::test]
4940    async fn test_har_post_data_present() {
4941        let log = NetworkLog::new();
4942        let mut req = make_request("1", "POST", "https://api.example.com");
4943        req.post_data = Some(r#"{"key":"value"}"#.into());
4944        req.headers = vec![CapturedHeader {
4945            name: "Content-Type".into(),
4946            value: "application/json".into(),
4947        }];
4948        push_entry(
4949            &log,
4950            req,
4951            Some(make_response("1", 200, "https://api.example.com")),
4952            None,
4953        )
4954        .await;
4955        let path = std::path::Path::new("/tmp/foxdriver_har_postdata.json");
4956        log.save_as_har(path, None).await.unwrap();
4957        let content = std::fs::read_to_string(path).unwrap();
4958        let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
4959        let post_data = &parsed["log"]["entries"][0]["request"]["postData"];
4960        assert!(post_data["text"].is_string());
4961        assert!(post_data["mimeType"].is_string());
4962    }
4963
4964    // ---------- BiDi ingestion edge cases ----------
4965
4966    #[tokio::test]
4967    async fn test_ingest_response_for_unknown_request_stashes_pending() {
4968        let log = NetworkLog::new();
4969        // Directly stash a pending response
4970        {
4971            let mut inner = log.inner.write().await;
4972            inner.pending_responses.insert(
4973                "future-req".into(),
4974                make_response("future-req", 200, "https://a.com"),
4975            );
4976        }
4977        assert_eq!(log.pending_count().await, 1);
4978        // Manually reconcile: push_entry does not check pending maps
4979        {
4980            let mut inner = log.inner.write().await;
4981            let resp = inner.pending_responses.remove("future-req");
4982            let entry = Arc::new(NetworkEntry {
4983                request: make_request("future-req", "GET", "https://a.com"),
4984                response: resp,
4985                error: None,
4986            });
4987            inner.push_entry(entry);
4988        }
4989        assert_eq!(log.pending_count().await, 0);
4990        assert!(log.has_response("future-req").await);
4991    }
4992
4993    #[tokio::test]
4994    async fn test_ingest_error_for_unknown_request_stashes_pending() {
4995        let log = NetworkLog::new();
4996        {
4997            let mut inner = log.inner.write().await;
4998            inner.pending_errors.insert(
4999                "future-err".into(),
5000                make_error("future-err", "https://a.com", "timeout"),
5001            );
5002        }
5003        assert_eq!(log.pending_count().await, 1);
5004        // Manually reconcile
5005        {
5006            let mut inner = log.inner.write().await;
5007            let err = inner.pending_errors.remove("future-err");
5008            let entry = Arc::new(NetworkEntry {
5009                request: make_request("future-err", "GET", "https://a.com"),
5010                response: None,
5011                error: err,
5012            });
5013            inner.push_entry(entry);
5014        }
5015        assert_eq!(log.pending_count().await, 0);
5016        assert!(log.has_error("future-err").await);
5017    }
5018
5019    #[tokio::test]
5020    async fn test_ingest_duplicate_request_ignored() {
5021        let log = NetworkLog::new();
5022        push_request(&log, make_request("1", "GET", "https://first.com")).await;
5023        push_request(&log, make_request("1", "GET", "https://second.com")).await;
5024        assert_eq!(log.len().await, 1);
5025        assert_eq!(log.first().await.unwrap().request.url, "https://first.com");
5026    }
5027
5028    // ---------- boundary conditions ----------
5029
5030    #[tokio::test]
5031    async fn test_filter_empty_log() {
5032        let log = NetworkLog::new();
5033        let f = Filter::new().method("GET").status_range(200..=200);
5034        assert!(log.filter(f).await.is_empty());
5035        assert_eq!(log.count(Filter::new()).await, 0);
5036    }
5037
5038    #[tokio::test]
5039    async fn test_find_by_url_empty_log() {
5040        let log = NetworkLog::new();
5041        assert!(log.find_by_url("anything").await.is_none());
5042    }
5043
5044    #[tokio::test]
5045    async fn test_find_by_url_regex_empty_log() {
5046        let log = NetworkLog::new();
5047        let re = regex::Regex::new(".*").unwrap();
5048        assert!(log.find_by_url_regex(&re).await.is_none());
5049    }
5050
5051    #[tokio::test]
5052    async fn test_endpoints_empty_log() {
5053        let log = NetworkLog::new();
5054        assert!(log.endpoints().await.is_empty());
5055    }
5056
5057    #[tokio::test]
5058    async fn test_hostnames_empty_log() {
5059        let log = NetworkLog::new();
5060        assert!(log.hostnames().await.is_empty());
5061    }
5062
5063    #[tokio::test]
5064    async fn test_distinct_methods_empty_log_returns_empty() {
5065        let log = NetworkLog::new();
5066        assert!(log.distinct_methods().await.is_empty());
5067    }
5068
5069    #[tokio::test]
5070    async fn test_distinct_statuses_empty_log() {
5071        let log = NetworkLog::new();
5072        assert!(log.distinct_statuses().await.is_empty());
5073    }
5074
5075    #[tokio::test]
5076    async fn test_total_bytes_empty_log() {
5077        let log = NetworkLog::new();
5078        assert_eq!(log.total_bytes_in().await, 0);
5079        assert_eq!(log.total_bytes_out().await, 0);
5080    }
5081
5082    #[tokio::test]
5083    async fn test_nth_out_of_bounds() {
5084        let log = NetworkLog::new();
5085        assert!(log.nth(0).await.is_none());
5086        push_request(&log, make_request("1", "GET", "https://a.com")).await;
5087        assert!(log.nth(1).await.is_none());
5088        assert!(log.nth(100).await.is_none());
5089    }
5090
5091    #[tokio::test]
5092    async fn test_last_n_greater_than_len() {
5093        let log = NetworkLog::new();
5094        push_request(&log, make_request("1", "GET", "https://a.com")).await;
5095        assert_eq!(log.last_n(100).await.len(), 1);
5096    }
5097
5098    #[tokio::test]
5099    async fn test_find_by_status_no_matches() {
5100        let log = NetworkLog::new();
5101        push_entry(
5102            &log,
5103            make_request("1", "GET", "https://a.com"),
5104            Some(make_response("1", 200, "https://a.com")),
5105            None,
5106        )
5107        .await;
5108        assert!(log.find_by_status(404).await.is_empty());
5109    }
5110
5111    #[tokio::test]
5112    async fn test_entries_since_future_timestamp() {
5113        let log = NetworkLog::new();
5114        push_request(&log, make_request("1", "GET", "https://a.com")).await;
5115        assert!(log.entries_since(u64::MAX).await.is_empty());
5116    }
5117
5118    #[tokio::test]
5119    async fn test_contains_id_false() {
5120        let log = NetworkLog::new();
5121        assert!(!log.contains_id("nonexistent").await);
5122    }
5123
5124    #[tokio::test]
5125    async fn test_remove_by_id_nonexistent() {
5126        let log = NetworkLog::new();
5127        assert!(log.remove_by_id("nonexistent").await.is_none());
5128    }
5129
5130    #[tokio::test]
5131    async fn test_metrics_on_empty_log() {
5132        let log = NetworkLog::new();
5133        let m = log.metrics().await;
5134        assert_eq!(m.requests_received, 0);
5135        assert_eq!(m.responses_received, 0);
5136        assert_eq!(m.errors_received, 0);
5137        assert_eq!(m.entries_evicted, 0);
5138        assert_eq!(m.broadcast_drops, 0);
5139        assert_eq!(m.duplicate_responses, 0);
5140        assert_eq!(m.duplicate_errors, 0);
5141    }
5142
5143    #[tokio::test]
5144    async fn test_memory_estimate_grows_with_entries() {
5145        let log = NetworkLog::new();
5146        let empty_est = log.memory_estimate().await;
5147        // The key invariant is that the estimate increases as entries are added.
5148        push_request(&log, make_request("1", "GET", "https://a.com")).await;
5149        let grown_est = log.memory_estimate().await;
5150        assert!(
5151            grown_est > empty_est,
5152            "estimate must grow: empty={empty_est}, after one entry={grown_est}"
5153        );
5154    }
5155
5156    #[tokio::test]
5157    async fn test_with_limits_zero_zero() {
5158        let log = NetworkLog::with_limits(0, 0);
5159        for i in 0..100 {
5160            push_request(
5161                &log,
5162                make_request(&format!("{}", i), "GET", "https://x.com"),
5163            )
5164            .await;
5165        }
5166        assert_eq!(log.len().await, 100);
5167    }
5168
5169    #[tokio::test]
5170    async fn test_with_limits_small() {
5171        let log = NetworkLog::with_limits(5, 5);
5172        for i in 0..20 {
5173            push_request(
5174                &log,
5175                make_request(&format!("{}", i), "GET", "https://x.com"),
5176            )
5177            .await;
5178        }
5179        assert!(log.len().await <= 5);
5180    }
5181
5182    #[tokio::test]
5183    async fn test_wait_for_url_timeout_zero() {
5184        let log = NetworkLog::new();
5185        let result = log
5186            .wait_for_url("nonexistent", std::time::Duration::from_secs(0))
5187            .await;
5188        assert!(result.is_none());
5189    }
5190
5191    #[tokio::test]
5192    async fn test_wait_for_response_timeout_zero() {
5193        let log = NetworkLog::new();
5194        let result = log
5195            .wait_for_response("nonexistent", std::time::Duration::from_secs(0))
5196            .await;
5197        assert!(result.is_none());
5198    }
5199
5200    // ---------- filter edge cases ----------
5201
5202    #[tokio::test]
5203    async fn test_filter_header_case_insensitive() {
5204        let log = NetworkLog::new();
5205        let mut req = make_request("1", "GET", "https://a.com");
5206        req.headers = vec![CapturedHeader {
5207            name: "X-Custom".into(),
5208            value: "SecretValue".into(),
5209        }];
5210        push_request(&log, req).await;
5211        assert_eq!(
5212            log.filter(Filter::new().header("x-custom", "secretvalue"))
5213                .await
5214                .len(),
5215            1
5216        );
5217        assert_eq!(
5218            log.filter(Filter::new().header("X-CUSTOM", "SECRETVALUE"))
5219                .await
5220                .len(),
5221            1
5222        );
5223    }
5224
5225    #[tokio::test]
5226    async fn test_filter_combined_conditions() {
5227        let log = NetworkLog::new();
5228        push_entry(
5229            &log,
5230            make_request("1", "POST", "https://api.example.com"),
5231            Some(make_response("1", 201, "https://api.example.com")),
5232            None,
5233        )
5234        .await;
5235        push_entry(
5236            &log,
5237            make_request("2", "GET", "https://api.example.com"),
5238            Some(make_response("2", 200, "https://api.example.com")),
5239            None,
5240        )
5241        .await;
5242        push_entry(
5243            &log,
5244            make_request("3", "POST", "https://api.example.com"),
5245            Some(make_response("3", 500, "https://api.example.com")),
5246            None,
5247        )
5248        .await;
5249        let f = Filter::new().method("POST").status_range(200..=299);
5250        assert_eq!(log.filter(f).await.len(), 1);
5251    }
5252
5253    #[tokio::test]
5254    async fn test_filter_with_response_only() {
5255        let log = NetworkLog::new();
5256        push_request(&log, make_request("1", "GET", "https://a.com")).await;
5257        push_entry(
5258            &log,
5259            make_request("2", "GET", "https://b.com"),
5260            Some(make_response("2", 200, "https://b.com")),
5261            None,
5262        )
5263        .await;
5264        assert_eq!(log.filter(Filter::new().with_response()).await.len(), 1);
5265        assert_eq!(log.filter(Filter::new().without_response()).await.len(), 1);
5266    }
5267
5268    #[tokio::test]
5269    async fn test_filter_with_error_only() {
5270        let log = NetworkLog::new();
5271        push_request(&log, make_request("1", "GET", "https://a.com")).await;
5272        push_entry(
5273            &log,
5274            make_request("2", "GET", "https://b.com"),
5275            None,
5276            Some(make_error("2", "https://b.com", "err")),
5277        )
5278        .await;
5279        assert_eq!(log.filter(Filter::new().with_error()).await.len(), 1);
5280    }
5281
5282    // ---------- curl generation edge cases ----------
5283
5284    #[tokio::test]
5285    async fn test_to_curl_empty_body() {
5286        let req = make_request("1", "GET", "https://a.com");
5287        let entry = NetworkEntry {
5288            request: req,
5289            response: None,
5290            error: None,
5291        };
5292        let curl = entry.to_curl();
5293        assert!(!curl.contains("-d"));
5294        assert!(curl.contains("curl -X 'GET'"));
5295    }
5296
5297    #[tokio::test]
5298    async fn test_to_curl_unicode_url() {
5299        let req = make_request("1", "GET", "https://例え.jp/テスト");
5300        let entry = NetworkEntry {
5301            request: req,
5302            response: None,
5303            error: None,
5304        };
5305        let curl = entry.to_curl();
5306        assert!(curl.contains("https://例え.jp/テスト"));
5307    }
5308
5309    #[tokio::test]
5310    async fn test_to_curl_strips_auto_headers() {
5311        let mut req = make_request("1", "GET", "https://a.com");
5312        req.headers = vec![
5313            CapturedHeader {
5314                name: "Host".into(),
5315                value: "a.com".into(),
5316            },
5317            CapturedHeader {
5318                name: "Accept-Encoding".into(),
5319                value: "gzip".into(),
5320            },
5321            CapturedHeader {
5322                name: "Connection".into(),
5323                value: "keep-alive".into(),
5324            },
5325            CapturedHeader {
5326                name: "X-Custom".into(),
5327                value: "value".into(),
5328            },
5329        ];
5330        let entry = NetworkEntry {
5331            request: req,
5332            response: None,
5333            error: None,
5334        };
5335        let curl = entry.to_curl();
5336        assert!(!curl.contains("Host:"));
5337        assert!(!curl.contains("Accept-Encoding:"));
5338        assert!(!curl.contains("Connection:"));
5339        assert!(curl.contains("X-Custom:"));
5340    }
5341
5342    // ---------- query params ----------
5343
5344    #[tokio::test]
5345    async fn test_query_params_multiple() {
5346        let req = make_request("1", "GET", "https://a.com?a=1&b=2&c=3");
5347        let params = req.query_params().unwrap();
5348        assert_eq!(params.len(), 3);
5349        assert!(params.contains(&("a".into(), "1".into())));
5350        assert!(params.contains(&("b".into(), "2".into())));
5351        assert!(params.contains(&("c".into(), "3".into())));
5352    }
5353
5354    #[tokio::test]
5355    async fn test_json_body_invalid() {
5356        let mut req = make_request("1", "POST", "https://a.com");
5357        req.post_data = Some("not json".into());
5358        assert!(req.json_body().is_none());
5359    }
5360
5361    #[tokio::test]
5362    async fn test_json_body_valid() {
5363        let mut req = make_request("1", "POST", "https://a.com");
5364        req.post_data = Some(r#"{"key":"value"}"#.into());
5365        let json = req.json_body().unwrap();
5366        assert_eq!(json["key"], "value");
5367    }
5368
5369    // ---------- request/response header lookup ----------
5370
5371    #[tokio::test]
5372    async fn test_request_header_case_insensitive() {
5373        let mut req = make_request("1", "GET", "https://a.com");
5374        req.headers = vec![CapturedHeader {
5375            name: "Content-Type".into(),
5376            value: "application/json".into(),
5377        }];
5378        assert_eq!(req.request_header("content-type"), Some("application/json"));
5379        assert_eq!(req.request_header("Content-Type"), Some("application/json"));
5380        assert_eq!(req.request_header("CONTENT-TYPE"), Some("application/json"));
5381    }
5382
5383    #[tokio::test]
5384    async fn test_request_header_missing() {
5385        let req = make_request("1", "GET", "https://a.com");
5386        assert_eq!(req.request_header("x-missing"), None);
5387    }
5388
5389    #[tokio::test]
5390    async fn test_response_header_case_insensitive() {
5391        let mut resp = make_response("1", 200, "https://a.com");
5392        resp.headers = vec![CapturedHeader {
5393            name: "X-Response-Header".into(),
5394            value: "val".into(),
5395        }];
5396        let entry = NetworkEntry {
5397            request: make_request("1", "GET", "https://a.com"),
5398            response: Some(resp),
5399            error: None,
5400        };
5401        assert_eq!(entry.response_header("x-response-header"), Some("val"));
5402        assert_eq!(entry.response_header("X-RESPONSE-HEADER"), Some("val"));
5403    }
5404
5405    // ---------- network entry helpers ----------
5406
5407    #[tokio::test]
5408    async fn test_network_entry_final_url() {
5409        let req = make_request("1", "GET", "https://a.com");
5410        let resp = make_response("1", 200, "https://b.com");
5411        let entry = NetworkEntry {
5412            request: req.clone(),
5413            response: Some(resp),
5414            error: None,
5415        };
5416        assert_eq!(entry.final_url(), "https://b.com");
5417        let entry2 = NetworkEntry {
5418            request: req,
5419            response: None,
5420            error: None,
5421        };
5422        assert_eq!(entry2.final_url(), "https://a.com");
5423    }
5424
5425    #[tokio::test]
5426    async fn test_network_entry_status() {
5427        let entry = NetworkEntry {
5428            request: make_request("1", "GET", "https://a.com"),
5429            response: Some(make_response("1", 404, "https://a.com")),
5430            error: None,
5431        };
5432        assert_eq!(entry.status(), Some(404));
5433        let entry2 = NetworkEntry {
5434            request: make_request("1", "GET", "https://a.com"),
5435            response: None,
5436            error: None,
5437        };
5438        assert_eq!(entry2.status(), None);
5439    }
5440
5441    // ---------- property-based tests (proptest) ----------
5442
5443    use proptest::prelude::*;
5444
5445    #[tokio::test]
5446    async fn test_proptest_filter_never_panics() {
5447        let log = NetworkLog::new();
5448        for i in 0..50 {
5449            let mut req = make_request(
5450                &format!("{}", i),
5451                ["GET", "POST", "PUT", "DELETE"][i % 4],
5452                &format!("https://{}.com", i),
5453            );
5454            req.headers = vec![CapturedHeader {
5455                name: "X-Id".into(),
5456                value: format!("{}", i),
5457            }];
5458            push_request(&log, req).await;
5459        }
5460
5461        // Test various filter combinations don't panic
5462        let _ = log.filter(Filter::new().method("GET")).await;
5463        let _ = log.filter(Filter::new().status_range(100..=599)).await;
5464        let _ = log.filter(Filter::new().url_contains("com")).await;
5465        let _ = log.filter(Filter::new().header("x-id", "25")).await;
5466        let _ = log.filter(Filter::new().with_response()).await;
5467        let _ = log.filter(Filter::new().without_response()).await;
5468        let _ = log.filter(Filter::new().with_error()).await;
5469        let _ = log
5470            .filter(
5471                Filter::new()
5472                    .method("GET")
5473                    .url_contains("com")
5474                    .with_response(),
5475            )
5476            .await;
5477    }
5478
5479    #[tokio::test]
5480    async fn test_proptest_concurrent_subscribe_and_push() {
5481        let log = NetworkLog::new();
5482        let mut handles = Vec::new();
5483
5484        for _ in 0..20 {
5485            let log = log.clone();
5486            handles.push(tokio::spawn(async move {
5487                let mut rx = log.subscribe().await;
5488                let mut count = 0;
5489                while count < 50 {
5490                    if rx.try_recv().is_ok() {
5491                        count += 1;
5492                    } else {
5493                        tokio::task::yield_now().await;
5494                    }
5495                }
5496            }));
5497        }
5498
5499        let log2 = log.clone();
5500        let pusher = tokio::spawn(async move {
5501            for i in 0..100 {
5502                push_request(
5503                    &log2,
5504                    make_request(&format!("{}", i), "GET", "https://x.com"),
5505                )
5506                .await;
5507                if i % 10 == 0 {
5508                    tokio::task::yield_now().await;
5509                }
5510            }
5511        });
5512
5513        pusher.await.unwrap();
5514        for h in handles {
5515            let _ = h.await;
5516        }
5517    }
5518
5519    #[tokio::test]
5520    async fn test_proptest_random_operations() {
5521        let log = NetworkLog::new();
5522        for i in 0..100 {
5523            push_request(
5524                &log,
5525                make_request(&format!("{}", i), "GET", &format!("https://{}.com", i)),
5526            )
5527            .await;
5528        }
5529
5530        // Perform random operations and verify invariants
5531        log.remove_by_id("50").await;
5532        log.retain(|e| e.request.id != "25").await;
5533        let _ = log.drain_pending().await;
5534        log.clear().await;
5535
5536        assert!(log.is_empty().await);
5537        assert_eq!(log.pending_count().await, 0);
5538    }
5539
5540    // ---------- proptest property-based tests ----------
5541
5542    proptest! {
5543        #![proptest_config(ProptestConfig::with_cases(256))]
5544
5545        #[test]
5546        fn prop_query_params_no_crash(url in r"https://[a-z0-9]+\.com(/[a-z]*)*(\?[a-z]+=[a-z0-9]*)*") {
5547            let req = CapturedRequest {
5548                id: "1".into(), context: None, url: url.clone(), method: "GET".into(),
5549                headers: vec![], cookies: vec![], post_data: None, timestamp: 0,
5550                destination: "document".into(), initiator_type: None,
5551                timing: CapturedTiming::default(),
5552            };
5553            let _ = req.query_params(); // must not panic
5554        }
5555
5556        #[test]
5557        fn prop_request_header_case_insensitive(
5558            name in "[A-Za-z_-]{1,20}",
5559            value in "[a-z0-9]{1,30}",
5560            lookup in "[A-Za-z_-]{1,20}"
5561        ) {
5562            let req = CapturedRequest {
5563                id: "1".into(), context: None, url: "https://a.com".into(), method: "GET".into(),
5564                headers: vec![CapturedHeader { name: name.clone(), value: value.clone() }],
5565                cookies: vec![], post_data: None, timestamp: 0,
5566                destination: "document".into(), initiator_type: None,
5567                timing: CapturedTiming::default(),
5568            };
5569            let result = req.request_header(&lookup);
5570            if name.to_lowercase() == lookup.to_lowercase() {
5571                prop_assert_eq!(result, Some(value.as_str()));
5572            }
5573        }
5574
5575        #[test]
5576        fn prop_json_body_valid_json_roundtrips(obj in "\\{.*\\}") {
5577            let req = CapturedRequest {
5578                id: "1".into(), context: None, url: "https://a.com".into(), method: "POST".into(),
5579                headers: vec![], cookies: vec![], post_data: Some(obj.clone()),
5580                timestamp: 0, destination: "document".into(), initiator_type: None,
5581                timing: CapturedTiming::default(),
5582            };
5583            // If it parses, the round-tripped JSON should equal the original parsed value.
5584            if let Some(val) = req.json_body() {
5585                let re_encoded = serde_json::to_string(&val).unwrap();
5586                let re_parsed: serde_json::Value = serde_json::from_str(&re_encoded).unwrap();
5587                prop_assert_eq!(val, re_parsed);
5588            }
5589        }
5590
5591        #[test]
5592        fn prop_filter_empty_matches_anything(
5593            method in "(GET|POST|PUT|DELETE|PATCH)",
5594            url in r"https://[a-z]+\.com",
5595            status in 100u16..599u16
5596        ) {
5597            let entry = NetworkEntry {
5598                request: CapturedRequest {
5599                    id: "1".into(), context: None, url: url.clone(), method,
5600                    headers: vec![], cookies: vec![], post_data: None, timestamp: 0,
5601                    destination: "document".into(), initiator_type: None,
5602                    timing: CapturedTiming::default(),
5603                },
5604                response: Some(CapturedResponse {
5605                    id: "1".into(), url, protocol: "h2".into(), status,
5606                    status_text: "OK".into(), headers: vec![], mime_type: "text/html".into(),
5607                    body_size: None, from_cache: false,
5608                }),
5609                error: None,
5610            };
5611            let filter = Filter::new();
5612            prop_assert!(filter.matches(&entry));
5613        }
5614
5615        #[test]
5616        fn prop_to_curl_always_contains_url(url in r"https://[a-z]+\.com(/[a-z]*)*") {
5617            let entry = NetworkEntry {
5618                request: CapturedRequest {
5619                    id: "1".into(), context: None, url: url.clone(), method: "GET".into(),
5620                    headers: vec![], cookies: vec![], post_data: None, timestamp: 0,
5621                    destination: "document".into(), initiator_type: None,
5622                    timing: CapturedTiming::default(),
5623                },
5624                response: None, error: None,
5625            };
5626            let curl = entry.to_curl();
5627            prop_assert!(curl.contains(&url), "curl missing URL: {}", curl);
5628        }
5629
5630        #[test]
5631        fn prop_to_curl_get_has_no_data_flag(url in r"https://[a-z]+\.com") {
5632            let entry = NetworkEntry {
5633                request: CapturedRequest {
5634                    id: "1".into(), context: None, url, method: "GET".into(),
5635                    headers: vec![], cookies: vec![], post_data: None, timestamp: 0,
5636                    destination: "document".into(), initiator_type: None,
5637                    timing: CapturedTiming::default(),
5638                },
5639                response: None, error: None,
5640            };
5641            let curl = entry.to_curl();
5642            prop_assert!(!curl.contains(" -d "), "GET curl should not have -d: {}", curl);
5643        }
5644    }
5645
5646    // ---------- deep boundary / edge cases ----------
5647
5648    #[tokio::test]
5649    async fn test_push_entry_with_very_long_url() {
5650        let log = NetworkLog::new();
5651        let long_url = format!("https://a.com/{}", "x".repeat(10_000));
5652        push_request(&log, make_request("1", "GET", &long_url)).await;
5653        assert_eq!(log.first().await.unwrap().request.url.len(), 14 + 10_000);
5654    }
5655
5656    #[tokio::test]
5657    async fn test_push_entry_with_unicode_url() {
5658        let log = NetworkLog::new();
5659        push_request(&log, make_request("1", "GET", "https://例え.jp/テスト")).await;
5660        assert_eq!(
5661            log.first().await.unwrap().request.url,
5662            "https://例え.jp/テスト"
5663        );
5664    }
5665
5666    #[tokio::test]
5667    async fn test_filter_url_regex_complex() {
5668        let log = NetworkLog::new();
5669        push_request(
5670            &log,
5671            make_request("1", "GET", "https://api.example.com/v1/users"),
5672        )
5673        .await;
5674        push_request(
5675            &log,
5676            make_request("2", "GET", "https://api.example.com/v2/items"),
5677        )
5678        .await;
5679        push_request(&log, make_request("3", "GET", "https://other.com/v1/users")).await;
5680        let re = regex::Regex::new(r"api\.example\.com/v\d+/users").unwrap();
5681        let found = log.find_by_url_regex(&re).await;
5682        assert_eq!(found.unwrap().request.id, "1");
5683    }
5684
5685    #[tokio::test]
5686    async fn test_filter_url_regex_no_match() {
5687        let log = NetworkLog::new();
5688        push_request(&log, make_request("1", "GET", "https://a.com")).await;
5689        let re = regex::Regex::new(r"nomatch").unwrap();
5690        assert!(log.find_by_url_regex(&re).await.is_none());
5691    }
5692
5693    #[tokio::test]
5694    async fn test_hostnames_with_subdomains() {
5695        let log = NetworkLog::new();
5696        push_request(
5697            &log,
5698            make_request("1", "GET", "https://sub1.sub2.example.com/path"),
5699        )
5700        .await;
5701        push_request(&log, make_request("2", "GET", "https://other.org")).await;
5702        let hosts = log.hostnames().await;
5703        assert_eq!(hosts.len(), 2);
5704        assert!(hosts.contains(&"sub1.sub2.example.com".into()));
5705    }
5706
5707    #[tokio::test]
5708    async fn test_endpoints_deduplicates_full_url() {
5709        let log = NetworkLog::new();
5710        push_request(&log, make_request("1", "GET", "https://a.com/path?x=1")).await;
5711        push_request(&log, make_request("2", "GET", "https://a.com/path?y=2")).await;
5712        let endpoints = log.endpoints().await;
5713        // endpoints() deduplicates by full URL, so query strings make them distinct
5714        assert_eq!(endpoints.len(), 2);
5715    }
5716
5717    #[tokio::test]
5718    async fn test_endpoints_with_ports() {
5719        let log = NetworkLog::new();
5720        push_request(&log, make_request("1", "GET", "https://a.com:8080/path")).await;
5721        push_request(&log, make_request("2", "GET", "https://a.com:9090/path")).await;
5722        let endpoints = log.endpoints().await;
5723        assert_eq!(endpoints.len(), 2);
5724    }
5725
5726    #[tokio::test]
5727    async fn test_distinct_methods_sorted() {
5728        let log = NetworkLog::new();
5729        push_request(&log, make_request("1", "DELETE", "https://a.com")).await;
5730        push_request(&log, make_request("2", "GET", "https://a.com")).await;
5731        push_request(&log, make_request("3", "POST", "https://a.com")).await;
5732        push_request(&log, make_request("4", "GET", "https://a.com")).await;
5733        assert_eq!(log.distinct_methods().await, vec!["DELETE", "GET", "POST"]);
5734    }
5735
5736    #[tokio::test]
5737    async fn test_distinct_statuses_sorted() {
5738        let log = NetworkLog::new();
5739        push_entry(
5740            &log,
5741            make_request("1", "GET", "https://a.com"),
5742            Some(make_response("1", 500, "https://a.com")),
5743            None,
5744        )
5745        .await;
5746        push_entry(
5747            &log,
5748            make_request("2", "GET", "https://a.com"),
5749            Some(make_response("2", 200, "https://a.com")),
5750            None,
5751        )
5752        .await;
5753        push_entry(
5754            &log,
5755            make_request("3", "GET", "https://a.com"),
5756            Some(make_response("3", 404, "https://a.com")),
5757            None,
5758        )
5759        .await;
5760        push_entry(
5761            &log,
5762            make_request("4", "GET", "https://a.com"),
5763            Some(make_response("4", 200, "https://a.com")),
5764            None,
5765        )
5766        .await;
5767        assert_eq!(log.distinct_statuses().await, vec![200, 404, 500]);
5768    }
5769
5770    #[tokio::test]
5771    async fn test_total_bytes_in_with_none_body_size() {
5772        let log = NetworkLog::new();
5773        let mut resp = make_response("1", 200, "https://a.com");
5774        resp.body_size = None;
5775        push_entry(
5776            &log,
5777            make_request("1", "GET", "https://a.com"),
5778            Some(resp),
5779            None,
5780        )
5781        .await;
5782        assert_eq!(log.total_bytes_in().await, 0);
5783    }
5784
5785    #[tokio::test]
5786    async fn test_total_bytes_out_with_post_data() {
5787        let log = NetworkLog::new();
5788        let mut req = make_request("1", "POST", "https://a.com");
5789        req.post_data = Some("payload".into());
5790        push_request(&log, req).await;
5791        assert_eq!(log.total_bytes_out().await, 7);
5792    }
5793
5794    #[tokio::test]
5795    async fn test_total_bytes_out_empty() {
5796        let log = NetworkLog::new();
5797        push_request(&log, make_request("1", "GET", "https://a.com")).await;
5798        assert_eq!(log.total_bytes_out().await, 0);
5799    }
5800
5801    #[tokio::test]
5802    async fn test_completed_returns_only_with_response_or_error() {
5803        let log = NetworkLog::new();
5804        push_request(&log, make_request("1", "GET", "https://a.com")).await;
5805        push_entry(
5806            &log,
5807            make_request("2", "GET", "https://b.com"),
5808            Some(make_response("2", 200, "https://b.com")),
5809            None,
5810        )
5811        .await;
5812        push_entry(
5813            &log,
5814            make_request("3", "GET", "https://c.com"),
5815            None,
5816            Some(make_error("3", "https://c.com", "err")),
5817        )
5818        .await;
5819        let completed = log.completed().await;
5820        assert_eq!(completed.len(), 2);
5821    }
5822
5823    #[tokio::test]
5824    async fn test_count_with_no_filter() {
5825        let log = NetworkLog::new();
5826        for i in 0..50 {
5827            push_request(
5828                &log,
5829                make_request(&format!("{}", i), "GET", "https://x.com"),
5830            )
5831            .await;
5832        }
5833        assert_eq!(log.count(Filter::new()).await, 50);
5834    }
5835
5836    #[tokio::test]
5837    async fn test_find_by_url_partial_match() {
5838        let log = NetworkLog::new();
5839        push_request(
5840            &log,
5841            make_request("1", "GET", "https://api.example.com/users"),
5842        )
5843        .await;
5844        push_request(&log, make_request("2", "GET", "https://other.com/users")).await;
5845        assert_eq!(
5846            log.find_by_url("api.example").await.unwrap().request.id,
5847            "1"
5848        );
5849    }
5850
5851    #[tokio::test]
5852    async fn test_subscribe_receives_existing_and_new() {
5853        let log = NetworkLog::new();
5854        push_request(&log, make_request("1", "GET", "https://a.com")).await;
5855        let mut rx = log.subscribe().await;
5856        push_request(&log, make_request("2", "GET", "https://b.com")).await;
5857        // Should receive entry 2 (not 1, since subscription starts after)
5858        let received = rx.try_recv().unwrap();
5859        assert_eq!(received.request.id, "2");
5860        assert!(rx.try_recv().is_err());
5861    }
5862
5863    #[tokio::test]
5864    async fn test_metrics_after_eviction() {
5865        let log = NetworkLog::with_limits(10, 5);
5866        for i in 0..25 {
5867            push_request(
5868                &log,
5869                make_request(&format!("{}", i), "GET", "https://x.com"),
5870            )
5871            .await;
5872        }
5873        let m = log.metrics().await;
5874        assert!(m.entries_evicted > 0);
5875    }
5876
5877    #[tokio::test]
5878    async fn test_eviction_preserves_order_of_remaining() {
5879        let log = NetworkLog::with_limits(10, 5);
5880        for i in 0..20 {
5881            push_request(
5882                &log,
5883                make_request(&format!("{}", i), "GET", "https://x.com"),
5884            )
5885            .await;
5886        }
5887        let ids = log.request_ids().await;
5888        // After eviction of oldest half, remaining should still be in order
5889        for pair in ids.windows(2) {
5890            let a: u32 = pair[0].parse().unwrap();
5891            let b: u32 = pair[1].parse().unwrap();
5892            assert!(a < b, "order violated: {a} before {b}");
5893        }
5894    }
5895
5896    #[tokio::test]
5897    async fn test_captured_timing_defaults() {
5898        let timing = CapturedTiming::default();
5899        assert!(timing.dns_start_ms.is_none());
5900        assert!(timing.dns_end_ms.is_none());
5901        assert!(timing.connect_start_ms.is_none());
5902        assert!(timing.connect_end_ms.is_none());
5903        assert!(timing.tls_start_ms.is_none());
5904        assert!(timing.response_start_ms.is_none());
5905        assert!(timing.response_end_ms.is_none());
5906    }
5907
5908    #[tokio::test]
5909    async fn test_captured_request_header_missing() {
5910        let req = make_request("1", "GET", "https://a.com");
5911        assert_eq!(req.request_header("X-Missing"), None);
5912    }
5913
5914    #[tokio::test]
5915    async fn test_network_entry_is_error() {
5916        let entry = NetworkEntry {
5917            request: make_request("1", "GET", "https://a.com"),
5918            response: None,
5919            error: Some(make_error("1", "https://a.com", "err")),
5920        };
5921        assert!(entry.is_error());
5922        assert!(!entry.has_response());
5923    }
5924
5925    #[tokio::test]
5926    async fn test_network_entry_has_response() {
5927        let entry = NetworkEntry {
5928            request: make_request("1", "GET", "https://a.com"),
5929            response: Some(make_response("1", 200, "https://a.com")),
5930            error: None,
5931        };
5932        assert!(entry.has_response());
5933        assert!(!entry.is_error());
5934    }
5935
5936    #[tokio::test]
5937    async fn test_save_to_json_empty_log() {
5938        let log = NetworkLog::new();
5939        let path = std::path::Path::new("/tmp/foxdriver_json_empty.json");
5940        log.save_to_json(path).await.unwrap();
5941        let content = std::fs::read_to_string(path).unwrap();
5942        let parsed: Vec<NetworkEntry> = serde_json::from_str(&content).unwrap();
5943        assert!(parsed.is_empty());
5944    }
5945
5946    #[tokio::test]
5947    async fn test_save_to_json_roundtrip_entries() {
5948        let log = NetworkLog::new();
5949        let mut req = make_request("1", "POST", "https://api.example.com");
5950        req.headers = vec![CapturedHeader {
5951            name: "Content-Type".into(),
5952            value: "application/json".into(),
5953        }];
5954        req.post_data = Some(r#"{"key":"value"}"#.into());
5955        push_entry(
5956            &log,
5957            req,
5958            Some(make_response("1", 201, "https://api.example.com")),
5959            None,
5960        )
5961        .await;
5962        let path = std::path::Path::new("/tmp/foxdriver_json_roundtrip.json");
5963        log.save_to_json(path).await.unwrap();
5964        let content = std::fs::read_to_string(path).unwrap();
5965        let parsed: Vec<NetworkEntry> = serde_json::from_str(&content).unwrap();
5966        assert_eq!(parsed.len(), 1);
5967        assert_eq!(parsed[0].request.id, "1");
5968        assert_eq!(parsed[0].request.method, "POST");
5969        assert_eq!(parsed[0].response.as_ref().unwrap().status, 201);
5970    }
5971
5972    #[tokio::test]
5973    async fn test_har_with_multiple_entries() {
5974        let log = NetworkLog::new();
5975        push_entry(
5976            &log,
5977            make_request("1", "GET", "https://a.com"),
5978            Some(make_response("1", 200, "https://a.com")),
5979            None,
5980        )
5981        .await;
5982        push_entry(
5983            &log,
5984            make_request("2", "POST", "https://b.com"),
5985            Some(make_response("2", 201, "https://b.com")),
5986            None,
5987        )
5988        .await;
5989        let path = std::path::Path::new("/tmp/foxdriver_har_multi.json");
5990        log.save_as_har(path, Some("test page")).await.unwrap();
5991        let content = std::fs::read_to_string(path).unwrap();
5992        let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
5993        assert_eq!(parsed["log"]["entries"].as_array().unwrap().len(), 2);
5994        assert_eq!(parsed["log"]["pages"][0]["title"], "test page");
5995    }
5996
5997    #[tokio::test]
5998    async fn test_har_error_entry_has_no_response() {
5999        let log = NetworkLog::new();
6000        push_entry(
6001            &log,
6002            make_request("1", "GET", "https://a.com"),
6003            None,
6004            Some(make_error("1", "https://a.com", "timeout")),
6005        )
6006        .await;
6007        let path = std::path::Path::new("/tmp/foxdriver_har_error.json");
6008        log.save_as_har(path, None).await.unwrap();
6009        let content = std::fs::read_to_string(path).unwrap();
6010        let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
6011        let entry = &parsed["log"]["entries"][0];
6012        assert!(entry["response"].is_null());
6013    }
6014
6015    // ---------- BiDi ingestion deep tests ----------
6016
6017    #[tokio::test]
6018    async fn test_metrics_requests_received_manual_increment() {
6019        let log = NetworkLog::new();
6020        let m0 = log.metrics().await;
6021        {
6022            let mut inner = log.inner.write().await;
6023            inner.metrics.requests_received += 5;
6024        }
6025        let m1 = log.metrics().await;
6026        assert_eq!(m1.requests_received, m0.requests_received + 5);
6027    }
6028
6029    #[tokio::test]
6030    async fn test_metrics_responses_received_manual_increment() {
6031        let log = NetworkLog::new();
6032        let m0 = log.metrics().await;
6033        {
6034            let mut inner = log.inner.write().await;
6035            inner.metrics.responses_received += 3;
6036        }
6037        let m1 = log.metrics().await;
6038        assert_eq!(m1.responses_received, m0.responses_received + 3);
6039    }
6040
6041    #[tokio::test]
6042    async fn test_metrics_errors_received_manual_increment() {
6043        let log = NetworkLog::new();
6044        let m0 = log.metrics().await;
6045        {
6046            let mut inner = log.inner.write().await;
6047            inner.metrics.errors_received += 2;
6048        }
6049        let m1 = log.metrics().await;
6050        assert_eq!(m1.errors_received, m0.errors_received + 2);
6051    }
6052
6053    #[tokio::test]
6054    async fn test_deduplication_does_not_affect_metrics() {
6055        let log = NetworkLog::new();
6056        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6057        let m1 = log.metrics().await;
6058        push_request(&log, make_request("1", "GET", "https://b.com")).await; // deduplicated
6059        let m2 = log.metrics().await;
6060        assert_eq!(m2.requests_received, m1.requests_received); // no change
6061        assert_eq!(log.len().await, 1); // still only one entry
6062    }
6063
6064    // ---------- filter deep tests ----------
6065
6066    #[tokio::test]
6067    async fn test_filter_status_range_boundary_inclusive() {
6068        let log = NetworkLog::new();
6069        push_entry(
6070            &log,
6071            make_request("1", "GET", "https://a.com"),
6072            Some(make_response("1", 200, "https://a.com")),
6073            None,
6074        )
6075        .await;
6076        push_entry(
6077            &log,
6078            make_request("2", "GET", "https://a.com"),
6079            Some(make_response("2", 299, "https://a.com")),
6080            None,
6081        )
6082        .await;
6083        push_entry(
6084            &log,
6085            make_request("3", "GET", "https://a.com"),
6086            Some(make_response("3", 300, "https://a.com")),
6087            None,
6088        )
6089        .await;
6090        assert_eq!(
6091            log.filter(Filter::new().status_range(200..=299))
6092                .await
6093                .len(),
6094            2
6095        );
6096    }
6097
6098    #[tokio::test]
6099    async fn test_filter_destination_deep() {
6100        let log = NetworkLog::new();
6101        let mut req1 = make_request("1", "GET", "https://a.com");
6102        req1.destination = "document".into();
6103        let mut req2 = make_request("2", "GET", "https://b.com");
6104        req2.destination = "image".into();
6105        push_request(&log, req1).await;
6106        push_request(&log, req2).await;
6107        assert_eq!(
6108            log.filter(Filter::new().destination("document"))
6109                .await
6110                .len(),
6111            1
6112        );
6113        assert_eq!(
6114            log.filter(Filter::new().destination("image")).await.len(),
6115            1
6116        );
6117        assert_eq!(
6118            log.filter(Filter::new().destination("script")).await.len(),
6119            0
6120        );
6121    }
6122
6123    #[tokio::test]
6124    async fn test_filter_combined_with_response_and_without_response() {
6125        let log = NetworkLog::new();
6126        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6127        push_entry(
6128            &log,
6129            make_request("2", "GET", "https://b.com"),
6130            Some(make_response("2", 200, "https://b.com")),
6131            None,
6132        )
6133        .await;
6134        push_entry(
6135            &log,
6136            make_request("3", "GET", "https://c.com"),
6137            None,
6138            Some(make_error("3", "https://c.com", "err")),
6139        )
6140        .await;
6141        assert_eq!(log.filter(Filter::new().with_response()).await.len(), 1);
6142        assert_eq!(log.filter(Filter::new().without_response()).await.len(), 2);
6143        assert_eq!(log.filter(Filter::new().with_error()).await.len(), 1);
6144    }
6145
6146    #[tokio::test]
6147    async fn test_filter_url_regex_complex_pattern() {
6148        let log = NetworkLog::new();
6149        push_request(
6150            &log,
6151            make_request("1", "GET", "https://api.example.com/v1/users/123"),
6152        )
6153        .await;
6154        push_request(
6155            &log,
6156            make_request("2", "GET", "https://api.example.com/v2/items/456"),
6157        )
6158        .await;
6159        push_request(
6160            &log,
6161            make_request("3", "GET", "https://other.com/v1/users/789"),
6162        )
6163        .await;
6164        let matches = log
6165            .filter(
6166                Filter::new()
6167                    .url_regex(r"api\.example\.com/v\d+/(users|items)/\d+")
6168                    .unwrap(),
6169            )
6170            .await;
6171        assert_eq!(matches.len(), 2);
6172    }
6173
6174    // ---------- serialization deep tests ----------
6175
6176    #[tokio::test]
6177    async fn test_captured_request_serde_roundtrip() {
6178        let req = make_request("1", "POST", "https://a.com");
6179        let json = serde_json::to_string(&req).unwrap();
6180        let de: CapturedRequest = serde_json::from_str(&json).unwrap();
6181        assert_eq!(de.id, req.id);
6182        assert_eq!(de.method, req.method);
6183        assert_eq!(de.url, req.url);
6184    }
6185
6186    #[tokio::test]
6187    async fn test_captured_response_serde_roundtrip() {
6188        let resp = make_response("1", 404, "https://a.com");
6189        let json = serde_json::to_string(&resp).unwrap();
6190        let de: CapturedResponse = serde_json::from_str(&json).unwrap();
6191        assert_eq!(de.status, 404);
6192        assert_eq!(de.url, "https://a.com");
6193    }
6194
6195    #[tokio::test]
6196    async fn test_captured_error_serde_roundtrip() {
6197        let err = make_error("1", "https://a.com", "timeout");
6198        let json = serde_json::to_string(&err).unwrap();
6199        let de: CapturedError = serde_json::from_str(&json).unwrap();
6200        assert_eq!(de.error_text, "timeout");
6201    }
6202
6203    #[tokio::test]
6204    async fn test_network_entry_serde_roundtrip() {
6205        let entry = NetworkEntry {
6206            request: make_request("1", "GET", "https://a.com"),
6207            response: Some(make_response("1", 200, "https://a.com")),
6208            error: None,
6209        };
6210        let json = serde_json::to_string(&entry).unwrap();
6211        let de: NetworkEntry = serde_json::from_str(&json).unwrap();
6212        assert_eq!(de.request.id, "1");
6213        assert!(de.response.is_some());
6214        assert!(de.error.is_none());
6215    }
6216
6217    // ---------- concurrent deep tests ----------
6218
6219    #[tokio::test]
6220    async fn test_concurrent_remove_and_query() {
6221        let log = NetworkLog::new();
6222        for i in 0..100 {
6223            push_request(
6224                &log,
6225                make_request(&format!("{}", i), "GET", "https://x.com"),
6226            )
6227            .await;
6228        }
6229        let mut handles = Vec::new();
6230        for _ in 0..10 {
6231            let log = log.clone();
6232            handles.push(tokio::spawn(async move {
6233                for _ in 0..10 {
6234                    let _ = log.remove_by_id("50").await;
6235                    let _ = log.len().await;
6236                    let _ = log.contains_id("50").await;
6237                }
6238            }));
6239        }
6240        for h in handles {
6241            h.await.unwrap();
6242        }
6243    }
6244
6245    #[tokio::test]
6246    async fn test_concurrent_retain_and_read() {
6247        let log = NetworkLog::new();
6248        for i in 0..100 {
6249            push_request(
6250                &log,
6251                make_request(&format!("{}", i), "GET", "https://x.com"),
6252            )
6253            .await;
6254        }
6255        let mut handles = Vec::new();
6256        for _ in 0..5 {
6257            let log = log.clone();
6258            handles.push(tokio::spawn(async move {
6259                log.retain(|e| e.request.id != "50").await;
6260            }));
6261        }
6262        for _ in 0..5 {
6263            let log = log.clone();
6264            handles.push(tokio::spawn(async move {
6265                for _ in 0..20 {
6266                    let _ = log.len().await;
6267                    tokio::task::yield_now().await;
6268                }
6269            }));
6270        }
6271        for h in handles {
6272            h.await.unwrap();
6273        }
6274    }
6275
6276    #[tokio::test]
6277    async fn test_concurrent_drain_pending_and_ingest() {
6278        let log = NetworkLog::new();
6279        let mut handles = Vec::new();
6280        for _ in 0..5 {
6281            let log = log.clone();
6282            handles.push(tokio::spawn(async move {
6283                for i in 0..20 {
6284                    push_request(
6285                        &log,
6286                        make_request(&format!("{}", i), "GET", "https://x.com"),
6287                    )
6288                    .await;
6289                }
6290            }));
6291        }
6292        for _ in 0..5 {
6293            let log = log.clone();
6294            handles.push(tokio::spawn(async move {
6295                for _ in 0..20 {
6296                    let _ = log.drain_pending().await;
6297                    tokio::task::yield_now().await;
6298                }
6299            }));
6300        }
6301        for h in handles {
6302            h.await.unwrap();
6303        }
6304    }
6305
6306    // ---------- wait helper deep tests ----------
6307
6308    #[tokio::test]
6309    async fn test_wait_for_url_receives_new_entry() {
6310        let log = NetworkLog::new();
6311        let log2 = log.clone();
6312        let waiter = tokio::spawn(async move {
6313            log2.wait_for_url("target", std::time::Duration::from_secs(1))
6314                .await
6315        });
6316        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
6317        push_request(&log, make_request("1", "GET", "https://target.com")).await;
6318        let result = waiter.await.unwrap();
6319        assert!(result.is_some());
6320        assert_eq!(result.unwrap().request.url, "https://target.com");
6321    }
6322
6323    #[tokio::test]
6324    async fn test_wait_for_response_receives_update() {
6325        let log = NetworkLog::new();
6326        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6327        let log2 = log.clone();
6328        let waiter = tokio::spawn(async move {
6329            log2.wait_for_response("1", std::time::Duration::from_secs(1))
6330                .await
6331        });
6332        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
6333        {
6334            let mut inner = log.inner.write().await;
6335            let mut new_entry = (*inner.entries[0]).clone();
6336            new_entry.response = Some(make_response("1", 200, "https://a.com"));
6337            inner.entries[0] = Arc::new(new_entry);
6338            let _ = inner.tx.send(inner.entries[0].clone());
6339        }
6340        let result = waiter.await.unwrap();
6341        assert!(result.is_some());
6342        assert!(result.unwrap().has_response());
6343    }
6344
6345    #[tokio::test]
6346    async fn test_filter_combined_method_status_url() {
6347        let log = NetworkLog::new();
6348        push_request(
6349            &log,
6350            make_request("1", "POST", "https://api.example.com/v1"),
6351        )
6352        .await;
6353        push_request(&log, make_request("2", "GET", "https://api.example.com/v2")).await;
6354        {
6355            let mut inner = log.inner.write().await;
6356            let mut e = (*inner.entries[0]).clone();
6357            e.response = Some(make_response("1", 201, "https://api.example.com/v1"));
6358            inner.entries[0] = Arc::new(e);
6359        }
6360        let f = Filter::new()
6361            .method("POST")
6362            .status_range(200..=299)
6363            .url_contains("v1");
6364        let hits = log.filter(f).await;
6365        assert_eq!(hits.len(), 1);
6366        assert_eq!(hits[0].request.id, "1");
6367    }
6368
6369    #[tokio::test]
6370    async fn test_filter_has_error_true() {
6371        let log = NetworkLog::new();
6372        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6373        push_request(&log, make_request("2", "GET", "https://b.com")).await;
6374        {
6375            let mut inner = log.inner.write().await;
6376            let mut e = (*inner.entries[1]).clone();
6377            e.error = Some(CapturedError {
6378                id: "2".into(),
6379                url: "https://b.com".into(),
6380                error_text: "net::ERR_FAILED".into(),
6381            });
6382            inner.entries[1] = Arc::new(e);
6383        }
6384        let hits = log.filter(Filter::new().with_error()).await;
6385        assert_eq!(hits.len(), 1);
6386        assert_eq!(hits[0].request.id, "2");
6387    }
6388
6389    #[tokio::test]
6390    async fn test_find_by_url_exact_vs_substring() {
6391        let log = NetworkLog::new();
6392        push_request(&log, make_request("1", "GET", "https://example.com/path")).await;
6393        let found = log.find_by_url("/path").await;
6394        assert!(found.is_some());
6395        let not_found = log.find_by_url("/other").await;
6396        assert!(not_found.is_none());
6397    }
6398
6399    #[tokio::test]
6400    async fn test_endpoints_domain_only_url() {
6401        let log = NetworkLog::new();
6402        push_request(&log, make_request("1", "GET", "https://example.com")).await;
6403        let eps = log.endpoints().await;
6404        assert_eq!(eps, vec!["https://example.com"]);
6405    }
6406
6407    #[tokio::test]
6408    async fn test_hostnames_idn_url() {
6409        let log = NetworkLog::new();
6410        push_request(&log, make_request("1", "GET", "https://münchen.de/shop")).await;
6411        let hosts = log.hostnames().await;
6412        // url::Url normalises IDN to punycode
6413        assert_eq!(hosts, vec!["xn--mnchen-3ya.de"]);
6414    }
6415
6416    #[tokio::test]
6417    async fn test_total_bytes_in_missing_body_size() {
6418        let log = NetworkLog::new();
6419        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6420        {
6421            let mut inner = log.inner.write().await;
6422            let mut e = (*inner.entries[0]).clone();
6423            e.response = Some(CapturedResponse {
6424                id: "1".into(),
6425                url: "https://a.com".into(),
6426                protocol: "h2".into(),
6427                status: 200,
6428                status_text: "OK".into(),
6429                headers: vec![],
6430                mime_type: "text/html".into(),
6431                body_size: None,
6432                from_cache: false,
6433            });
6434            inner.entries[0] = Arc::new(e);
6435        }
6436        assert_eq!(log.total_bytes_in().await, 0);
6437    }
6438
6439    #[tokio::test]
6440    async fn test_total_bytes_out_empty_post_data() {
6441        let log = NetworkLog::new();
6442        let mut req = make_request("1", "POST", "https://a.com");
6443        req.post_data = Some("".into());
6444        push_request(&log, req).await;
6445        assert_eq!(log.total_bytes_out().await, 0);
6446    }
6447
6448    #[test]
6449    fn test_to_curl_post_json() {
6450        let entry = NetworkEntry {
6451            request: CapturedRequest {
6452                id: "1".into(),
6453                context: None,
6454                url: "https://api.com".into(),
6455                method: "POST".into(),
6456                headers: vec![CapturedHeader {
6457                    name: "Content-Type".into(),
6458                    value: "application/json".into(),
6459                }],
6460                cookies: vec![],
6461                post_data: Some(r#"{"key":"val"}"#.into()),
6462                timestamp: 0,
6463                destination: "document".into(),
6464                initiator_type: None,
6465                timing: CapturedTiming::default(),
6466            },
6467            response: None,
6468            error: None,
6469        };
6470        let curl = entry.to_curl();
6471        assert!(curl.contains("-X 'POST'"));
6472        assert!(curl.contains("-H 'Content-Type: application/json'"));
6473        assert!(curl.contains(r#"-d '{"key":"val"}'"#));
6474    }
6475
6476    #[test]
6477    fn test_to_curl_header_with_quote() {
6478        let entry = NetworkEntry {
6479            request: CapturedRequest {
6480                id: "1".into(),
6481                context: None,
6482                url: "https://a.com".into(),
6483                method: "GET".into(),
6484                headers: vec![CapturedHeader {
6485                    name: "X-Token".into(),
6486                    value: "it's ok".into(),
6487                }],
6488                cookies: vec![],
6489                post_data: None,
6490                timestamp: 0,
6491                destination: "document".into(),
6492                initiator_type: None,
6493                timing: CapturedTiming::default(),
6494            },
6495            response: None,
6496            error: None,
6497        };
6498        let curl = entry.to_curl();
6499        assert!(curl.contains("X-Token: it'\\''s ok"));
6500    }
6501
6502    #[tokio::test]
6503    async fn test_retain_keeps_some_removes_others() {
6504        let log = NetworkLog::new();
6505        push_request(&log, make_request("1", "GET", "https://keep.com")).await;
6506        push_request(&log, make_request("2", "GET", "https://drop.com")).await;
6507        push_request(&log, make_request("3", "GET", "https://keep.com/page")).await;
6508        log.retain(|e| e.request.url.contains("keep")).await;
6509        let ids = log.request_ids().await;
6510        assert_eq!(ids.len(), 2);
6511        assert!(ids.contains(&"1".into()));
6512        assert!(ids.contains(&"3".into()));
6513    }
6514
6515    #[tokio::test]
6516    async fn test_request_ids_ordering_after_eviction() {
6517        let log = NetworkLog::with_limits(4, 100);
6518        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6519        push_request(&log, make_request("2", "GET", "https://b.com")).await;
6520        push_request(&log, make_request("3", "GET", "https://c.com")).await;
6521        push_request(&log, make_request("4", "GET", "https://d.com")).await;
6522        push_request(&log, make_request("5", "GET", "https://e.com")).await; // triggers eviction
6523        let ids = log.request_ids().await;
6524        // Incremental eviction removes only the oldest entry needed to stay under the cap.
6525        assert_eq!(ids.len(), 4);
6526        assert_eq!(
6527            ids,
6528            vec![
6529                "2".to_string(),
6530                "3".to_string(),
6531                "4".to_string(),
6532                "5".to_string()
6533            ]
6534        );
6535    }
6536
6537    #[tokio::test]
6538    async fn test_has_response_false_for_nonexistent() {
6539        let log = NetworkLog::new();
6540        assert!(!log.has_response("nope").await);
6541    }
6542
6543    #[tokio::test]
6544    async fn test_has_error_false_for_nonexistent() {
6545        let log = NetworkLog::new();
6546        assert!(!log.has_error("nope").await);
6547    }
6548
6549    #[tokio::test]
6550    async fn test_memory_estimate_increases_with_entries() {
6551        let log = NetworkLog::new();
6552        let empty = log.memory_estimate().await;
6553        push_request(
6554            &log,
6555            make_request("1", "GET", "https://example.com/some/long/path/here"),
6556        )
6557        .await;
6558        let with_one = log.memory_estimate().await;
6559        assert!(with_one > empty);
6560    }
6561
6562    #[tokio::test]
6563    async fn test_save_to_json_empty_log_roundtrip() {
6564        let log = NetworkLog::new();
6565        let tmp =
6566            std::env::temp_dir().join(format!("foxdriver_empty_json_{}.json", std::process::id()));
6567        log.save_to_json(&tmp).await.unwrap();
6568        let content = std::fs::read_to_string(&tmp).unwrap();
6569        assert!(content.contains("[]"));
6570        let _ = std::fs::remove_file(&tmp);
6571    }
6572
6573    #[tokio::test]
6574    async fn test_save_as_har_empty_log() {
6575        let log = NetworkLog::new();
6576        let tmp =
6577            std::env::temp_dir().join(format!("foxdriver_empty_har_{}.har", std::process::id()));
6578        log.save_as_har(&tmp, Some("empty")).await.unwrap();
6579        let content = std::fs::read_to_string(&tmp).unwrap();
6580        assert!(content.contains("\"entries\""));
6581        assert!(content.contains("\"pages\""));
6582        let _ = std::fs::remove_file(&tmp);
6583    }
6584
6585    #[tokio::test]
6586    async fn test_filter_destination_matching() {
6587        let log = NetworkLog::new();
6588        let mut req = make_request("1", "GET", "https://a.com");
6589        req.destination = "image".into();
6590        push_request(&log, req).await;
6591        push_request(&log, make_request("2", "GET", "https://b.com")).await;
6592        let hits = log.filter(Filter::new().destination("image")).await;
6593        assert_eq!(hits.len(), 1);
6594        assert_eq!(hits[0].request.id, "1");
6595    }
6596
6597    #[tokio::test]
6598    async fn test_distinct_methods_empty_log() {
6599        let log = NetworkLog::new();
6600        assert!(log.distinct_methods().await.is_empty());
6601    }
6602
6603    #[tokio::test]
6604    async fn test_distinct_statuses_no_responses() {
6605        let log = NetworkLog::new();
6606        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6607        assert!(log.distinct_statuses().await.is_empty());
6608    }
6609
6610    #[tokio::test]
6611    async fn test_total_bytes_in_all_none() {
6612        let log = NetworkLog::new();
6613        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6614        {
6615            let mut inner = log.inner.write().await;
6616            let mut e = (*inner.entries[0]).clone();
6617            e.response = Some(CapturedResponse {
6618                id: "1".into(),
6619                url: "https://a.com".into(),
6620                protocol: "h2".into(),
6621                status: 200,
6622                status_text: "OK".into(),
6623                headers: vec![],
6624                mime_type: "text/html".into(),
6625                body_size: None,
6626                from_cache: false,
6627            });
6628            inner.entries[0] = Arc::new(e);
6629        }
6630        assert_eq!(log.total_bytes_in().await, 0);
6631    }
6632
6633    #[tokio::test]
6634    async fn test_total_bytes_out_no_post_data() {
6635        let log = NetworkLog::new();
6636        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6637        assert_eq!(log.total_bytes_out().await, 0);
6638    }
6639
6640    #[tokio::test]
6641    async fn test_save_to_json_roundtrip_with_response() {
6642        let log = NetworkLog::new();
6643        push_request(&log, make_request("1", "POST", "https://api.com")).await;
6644        {
6645            let mut inner = log.inner.write().await;
6646            let mut e = (*inner.entries[0]).clone();
6647            e.response = Some(make_response("1", 201, "https://api.com"));
6648            inner.entries[0] = Arc::new(e);
6649        }
6650        let tmp =
6651            std::env::temp_dir().join(format!("foxdriver_json_rt_{}.json", std::process::id()));
6652        log.save_to_json(&tmp).await.unwrap();
6653        let content = std::fs::read_to_string(&tmp).unwrap();
6654        let parsed: Vec<NetworkEntry> = serde_json::from_str(&content).unwrap();
6655        assert_eq!(parsed.len(), 1);
6656        assert_eq!(parsed[0].request.id, "1");
6657        assert_eq!(parsed[0].response.as_ref().unwrap().status, 201);
6658        let _ = std::fs::remove_file(&tmp);
6659    }
6660
6661    #[tokio::test]
6662    async fn test_save_as_har_with_response_entry() {
6663        let log = NetworkLog::new();
6664        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6665        {
6666            let mut inner = log.inner.write().await;
6667            let mut e = (*inner.entries[0]).clone();
6668            e.response = Some(make_response("1", 200, "https://a.com"));
6669            inner.entries[0] = Arc::new(e);
6670        }
6671        let tmp =
6672            std::env::temp_dir().join(format!("foxdriver_har_resp_{}.har", std::process::id()));
6673        log.save_as_har(&tmp, Some("testpage")).await.unwrap();
6674        let content = std::fs::read_to_string(&tmp).unwrap();
6675        assert!(content.contains("\"testpage\""));
6676        assert!(content.contains("200"));
6677        let _ = std::fs::remove_file(&tmp);
6678    }
6679
6680    #[tokio::test]
6681    async fn test_has_response_true_when_present() {
6682        let log = NetworkLog::new();
6683        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6684        assert!(!log.has_response("1").await);
6685        {
6686            let mut inner = log.inner.write().await;
6687            let mut e = (*inner.entries[0]).clone();
6688            e.response = Some(make_response("1", 200, "https://a.com"));
6689            inner.entries[0] = Arc::new(e);
6690        }
6691        assert!(log.has_response("1").await);
6692    }
6693
6694    #[tokio::test]
6695    async fn test_has_error_true_when_present() {
6696        let log = NetworkLog::new();
6697        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6698        assert!(!log.has_error("1").await);
6699        {
6700            let mut inner = log.inner.write().await;
6701            let mut e = (*inner.entries[0]).clone();
6702            e.error = Some(CapturedError {
6703                id: "1".into(),
6704                url: "https://a.com".into(),
6705                error_text: "fail".into(),
6706            });
6707            inner.entries[0] = Arc::new(e);
6708        }
6709        assert!(log.has_error("1").await);
6710    }
6711
6712    #[tokio::test]
6713    async fn test_memory_estimate_decreases_after_clear() {
6714        let log = NetworkLog::new();
6715        push_request(
6716            &log,
6717            make_request(
6718                "1",
6719                "GET",
6720                "https://example.com/very/long/path/here/for/bytes",
6721            ),
6722        )
6723        .await;
6724        let before = log.memory_estimate().await;
6725        log.clear().await;
6726        let after = log.memory_estimate().await;
6727        assert!(after < before);
6728    }
6729
6730    #[tokio::test]
6731    async fn test_memory_estimate_decreases_after_retain() {
6732        let log = NetworkLog::new();
6733        push_request(&log, make_request("1", "GET", "https://a.com/loooooong")).await;
6734        push_request(&log, make_request("2", "GET", "https://b.com/loooooong")).await;
6735        let before = log.memory_estimate().await;
6736        log.retain(|e| e.request.id == "1").await;
6737        let after = log.memory_estimate().await;
6738        assert!(after < before);
6739    }
6740
6741    #[tokio::test]
6742    async fn test_completed_excludes_in_flight() {
6743        let log = NetworkLog::new();
6744        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6745        push_request(&log, make_request("2", "GET", "https://b.com")).await;
6746        {
6747            let mut inner = log.inner.write().await;
6748            let mut e = (*inner.entries[1]).clone();
6749            e.response = Some(make_response("2", 200, "https://b.com"));
6750            inner.entries[1] = Arc::new(e);
6751        }
6752        let completed = log.completed().await;
6753        assert_eq!(completed.len(), 1);
6754        assert_eq!(completed[0].request.id, "2");
6755    }
6756
6757    #[tokio::test]
6758    async fn test_completed_includes_errors() {
6759        let log = NetworkLog::new();
6760        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6761        {
6762            let mut inner = log.inner.write().await;
6763            let mut e = (*inner.entries[0]).clone();
6764            e.error = Some(CapturedError {
6765                id: "1".into(),
6766                url: "https://a.com".into(),
6767                error_text: "net::ERR_ABORTED".into(),
6768            });
6769            inner.entries[0] = Arc::new(e);
6770        }
6771        let completed = log.completed().await;
6772        assert_eq!(completed.len(), 1);
6773        assert!(completed[0].is_error());
6774    }
6775
6776    #[tokio::test]
6777    async fn test_find_by_url_regex_complex_pattern() {
6778        let log = NetworkLog::new();
6779        push_request(
6780            &log,
6781            make_request("1", "GET", "https://api.example.com/v1/users"),
6782        )
6783        .await;
6784        push_request(
6785            &log,
6786            make_request("2", "GET", "https://api.example.com/v2/items"),
6787        )
6788        .await;
6789        let re = regex::Regex::new(r"/v\d+/users").unwrap();
6790        let found = log.find_by_url_regex(&re).await;
6791        assert_eq!(found.unwrap().request.id, "1");
6792    }
6793
6794    #[tokio::test]
6795    async fn test_nth_out_of_bounds_returns_none() {
6796        let log = NetworkLog::new();
6797        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6798        assert!(log.nth(5).await.is_none());
6799    }
6800
6801    #[tokio::test]
6802    async fn test_first_last_both_none_on_empty() {
6803        let log = NetworkLog::new();
6804        assert!(log.first().await.is_none());
6805        assert!(log.last().await.is_none());
6806    }
6807
6808    #[tokio::test]
6809    async fn test_remove_by_id_missing_returns_none() {
6810        let log = NetworkLog::new();
6811        assert!(log.remove_by_id("nope").await.is_none());
6812    }
6813
6814    #[tokio::test]
6815    async fn test_contains_id_true_and_false() {
6816        let log = NetworkLog::new();
6817        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6818        assert!(log.contains_id("1").await);
6819        assert!(!log.contains_id("2").await);
6820    }
6821
6822    #[tokio::test]
6823    async fn test_endpoints_dedupes_by_full_url() {
6824        // endpoints() deduplicates by the full request URL (including query string).
6825        let log = NetworkLog::new();
6826        push_request(&log, make_request("1", "GET", "https://a.com/page?x=1")).await;
6827        push_request(&log, make_request("2", "GET", "https://a.com/page?x=2")).await;
6828        let eps = log.endpoints().await;
6829        assert_eq!(eps.len(), 2);
6830    }
6831
6832    #[tokio::test]
6833    async fn test_hostnames_dedupes() {
6834        let log = NetworkLog::new();
6835        push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
6836        push_request(&log, make_request("2", "GET", "https://a.com/y")).await;
6837        let hosts = log.hostnames().await;
6838        assert_eq!(hosts.len(), 1);
6839        assert_eq!(hosts[0], "a.com");
6840    }
6841
6842    #[tokio::test]
6843    async fn test_drain_pending_empties_maps() {
6844        let log = NetworkLog::new();
6845        // Push a response out-of-order so it lands in pending
6846        {
6847            let mut inner = log.inner.write().await;
6848            inner
6849                .pending_responses
6850                .insert("1".into(), make_response("1", 200, "https://a.com"));
6851            inner.pending_errors.insert(
6852                "2".into(),
6853                CapturedError {
6854                    id: "2".into(),
6855                    url: "https://b.com".into(),
6856                    error_text: "err".into(),
6857                },
6858            );
6859        }
6860        assert_eq!(log.pending_count().await, 2);
6861        let (resps, errs) = log.drain_pending().await;
6862        assert_eq!(resps.len(), 1);
6863        assert_eq!(errs.len(), 1);
6864        assert_eq!(log.pending_count().await, 0);
6865    }
6866
6867    #[tokio::test]
6868    async fn test_metrics_zero_on_fresh_log() {
6869        let log = NetworkLog::new();
6870        let m = log.metrics().await;
6871        assert_eq!(m.requests_received, 0);
6872        assert_eq!(m.responses_received, 0);
6873        assert_eq!(m.errors_received, 0);
6874        assert_eq!(m.entries_evicted, 0);
6875        assert_eq!(m.broadcast_drops, 0);
6876        assert_eq!(m.duplicate_responses, 0);
6877        assert_eq!(m.duplicate_errors, 0);
6878    }
6879
6880    #[tokio::test]
6881    async fn test_metrics_track_entries() {
6882        let log = NetworkLog::new();
6883        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6884        push_request(&log, make_request("2", "GET", "https://b.com")).await;
6885        // push_request does not increment metrics (those are BiDi handler counters).
6886        // Verify that default metrics are zero on a fresh log.
6887        let m = log.metrics().await;
6888        assert_eq!(m.max_entries, 50_000);
6889        assert_eq!(m.max_entries, log.inner.read().await.max_entries);
6890    }
6891
6892    #[tokio::test]
6893    async fn test_eviction_evicts_oldest_incrementally() {
6894        let log = NetworkLog::with_limits(3, 5);
6895        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6896        push_request(&log, make_request("2", "GET", "https://b.com")).await;
6897        push_request(&log, make_request("3", "GET", "https://c.com")).await;
6898        push_request(&log, make_request("4", "GET", "https://d.com")).await;
6899        push_request(&log, make_request("5", "GET", "https://e.com")).await;
6900
6901        let entries = log.entries().await;
6902        assert_eq!(entries.len(), 3);
6903        assert_eq!(entries[0].request.id, "3");
6904        assert_eq!(entries[1].request.id, "4");
6905        assert_eq!(entries[2].request.id, "5");
6906
6907        let m = log.metrics().await;
6908        assert_eq!(m.entries_evicted, 2);
6909
6910        // Pushing one more evicts exactly one additional oldest entry.
6911        push_request(&log, make_request("6", "GET", "https://f.com")).await;
6912        let entries = log.entries().await;
6913        assert_eq!(entries.len(), 3);
6914        assert_eq!(entries[0].request.id, "4");
6915        let m = log.metrics().await;
6916        assert_eq!(m.entries_evicted, 3);
6917    }
6918
6919    #[tokio::test]
6920    async fn test_filter_status_range_ignores_error_without_status() {
6921        // A fetch error has no response and therefore no status. It must not
6922        // be fabricated as 0 and therefore must not match a range that
6923        // happens to include 0 (which HTTP status codes never do anyway).
6924        let log = NetworkLog::new();
6925        push_entry(
6926            &log,
6927            make_request("1", "GET", "https://a.com"),
6928            None,
6929            Some(make_error("1", "https://a.com", "net::ERR_FAILED")),
6930        )
6931        .await;
6932        // 0 is not a valid HTTP status, but this primarily guards against any
6933        // future code that fabricates 0 for missing statuses.
6934        assert!(log
6935            .filter(Filter::new().status_range(0..=0))
6936            .await
6937            .is_empty());
6938        assert!(log.filter(Filter::new().with_error()).await.len() == 1);
6939    }
6940}