html2pdf-api 0.3.3

Thread-safe headless browser pool for high-performance HTML to PDF conversion with native Rust web framework integration.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
//! Configuration for browser pool behavior and limits.
//!
//! This module provides [`BrowserPoolConfig`] and [`BrowserPoolConfigBuilder`]
//! for configuring pool size, browser lifecycle, and health monitoring parameters.
//!
//! # Example
//!
//! ```rust
//! use std::time::Duration;
//! use html2pdf_api::BrowserPoolConfigBuilder;
//!
//! let config = BrowserPoolConfigBuilder::new()
//!     .max_pool_size(10)
//!     .warmup_count(5)
//!     .browser_ttl(Duration::from_secs(7200))
//!     .build()
//!     .expect("Invalid configuration");
//!
//! assert_eq!(config.max_pool_size, 10);
//! assert_eq!(config.warmup_count, 5);
//! ```
//!
//! # Environment Configuration
//!
//! When the `env-config` feature is enabled, you can load configuration
//! from environment variables and an optional `app.env` file:
//!
//! ```rust,ignore
//! use html2pdf_api::config::env::from_env;
//!
//! let config = from_env()?;
//! ```
//!
//! See [`mod@env`] module for available environment variables.

use std::time::Duration;

/// Configuration for browser pool behavior and limits.
///
/// Controls pool size, browser lifecycle, and health monitoring parameters.
/// Use [`BrowserPoolConfigBuilder`] for validation and convenience.
///
/// # Fields Overview
///
/// | Field | Default | Description |
/// |-------|---------|-------------|
/// | `max_pool_size` | 5 | Maximum browsers in pool |
/// | `warmup_count` | 3 | Browsers to pre-create |
/// | `ping_interval` | 15s | Health check frequency |
/// | `browser_ttl` | 1 hour | Browser lifetime |
/// | `max_ping_failures` | 3 | Failures before removal |
/// | `warmup_timeout` | 60s | Warmup time limit |
///
/// # Example
///
/// ```rust
/// use html2pdf_api::BrowserPoolConfig;
///
/// // Use defaults
/// let config = BrowserPoolConfig::default();
/// assert_eq!(config.max_pool_size, 5);
/// ```
#[derive(Debug, Clone)]
pub struct BrowserPoolConfig {
    /// Maximum number of browsers to keep in the pool (idle + active).
    ///
    /// This is a soft limit - active browsers may temporarily exceed this during high load.
    ///
    /// # Default
    ///
    /// 5 browsers
    ///
    /// # Considerations
    ///
    /// - Higher values = more memory usage, better concurrency
    /// - Lower values = less memory, potential queuing under load
    pub max_pool_size: usize,

    /// Number of browsers to pre-create during warmup phase.
    ///
    /// Must be d `max_pool_size`. Reduces first-request latency.
    ///
    /// # Default
    ///
    /// 3 browsers
    ///
    /// # Considerations
    ///
    /// - Set to `max_pool_size` for fastest first requests
    /// - Set to 0 for lazy initialization (browsers created on demand)
    pub warmup_count: usize,

    /// Interval between health check pings for active browsers.
    ///
    /// Shorter intervals = faster failure detection, higher overhead.
    ///
    /// # Default
    ///
    /// 15 seconds
    ///
    /// # Considerations
    ///
    /// - Too short: Unnecessary CPU/memory overhead
    /// - Too long: Slow detection of crashed browsers
    pub ping_interval: Duration,

    /// Time-to-live for each browser instance before forced retirement.
    ///
    /// Prevents memory leaks from long-running browser processes.
    ///
    /// # Default
    ///
    /// 1 hour (3600 seconds)
    ///
    /// # Considerations
    ///
    /// - Chrome can accumulate memory over time
    /// - Shorter TTL = more browser restarts, fresher instances
    /// - Longer TTL = fewer restarts, potential memory growth
    pub browser_ttl: Duration,

    /// Maximum consecutive ping failures before removing a browser.
    ///
    /// Higher values = more tolerance for transient failures.
    ///
    /// # Default
    ///
    /// 3 consecutive failures
    ///
    /// # Considerations
    ///
    /// - Set to 1 for aggressive failure detection
    /// - Set higher if experiencing transient network issues
    pub max_ping_failures: u32,

    /// Maximum time allowed for warmup process to complete.
    ///
    /// If warmup doesn't complete in this time, it fails with timeout error.
    ///
    /// # Default
    ///
    /// 60 seconds
    ///
    /// # Considerations
    ///
    /// - Should be at least `warmup_count * ~5 seconds` per browser
    /// - Increase if running on slow hardware or with many warmup browsers
    pub warmup_timeout: Duration,

    /// Interval between pre-created browser startups during warmup.
    ///
    /// Distributes expiration intervals so the entire pool doesn't crash simultaneously.
    /// Default: 30 seconds
    pub warmup_stagger: Duration,
}

impl Default for BrowserPoolConfig {
    /// Production-ready default configuration.
    ///
    /// - Pool size: 5 browsers
    /// - Warmup: 3 browsers
    /// - Health checks: Every 15 seconds
    /// - TTL: 1 hour
    /// - Failure tolerance: 3 consecutive failures
    /// - Warmup timeout: 60 seconds
    ///
    /// # Example
    ///
    /// ```rust
    /// use html2pdf_api::BrowserPoolConfig;
    /// use std::time::Duration;
    ///
    /// let config = BrowserPoolConfig::default();
    ///
    /// assert_eq!(config.max_pool_size, 5);
    /// assert_eq!(config.warmup_count, 3);
    /// assert_eq!(config.ping_interval, Duration::from_secs(15));
    /// assert_eq!(config.browser_ttl, Duration::from_secs(3600));
    /// assert_eq!(config.max_ping_failures, 3);
    /// assert_eq!(config.warmup_timeout, Duration::from_secs(60));
    /// ```
    fn default() -> Self {
        Self {
            max_pool_size: 5,
            warmup_count: 3,
            ping_interval: Duration::from_secs(15),
            browser_ttl: Duration::from_secs(3600), // 1 hour
            max_ping_failures: 3,
            warmup_timeout: Duration::from_secs(60),
            warmup_stagger: Duration::from_secs(30),
        }
    }
}

/// Builder for [`BrowserPoolConfig`] with validation.
///
/// Provides a fluent API for constructing validated configurations.
/// All setter methods can be chained together.
///
/// # Example
///
/// ```rust
/// use std::time::Duration;
/// use html2pdf_api::BrowserPoolConfigBuilder;
///
/// let config = BrowserPoolConfigBuilder::new()
///     .max_pool_size(10)
///     .warmup_count(5)
///     .browser_ttl(Duration::from_secs(7200))
///     .build()
///     .expect("Invalid configuration");
/// ```
///
/// # Validation
///
/// The [`build()`](Self::build) method validates:
/// - `max_pool_size` must be greater than 0
/// - `warmup_count` must be d `max_pool_size`
pub struct BrowserPoolConfigBuilder {
    config: BrowserPoolConfig,
}

impl BrowserPoolConfigBuilder {
    /// Create a new builder with default values.
    ///
    /// # Example
    ///
    /// ```rust
    /// use html2pdf_api::BrowserPoolConfigBuilder;
    ///
    /// let builder = BrowserPoolConfigBuilder::new();
    /// let config = builder.build().unwrap();
    ///
    /// // Has default values
    /// assert_eq!(config.max_pool_size, 5);
    /// ```
    pub fn new() -> Self {
        Self {
            config: BrowserPoolConfig::default(),
        }
    }

    /// Set maximum pool size (must be > 0).
    ///
    /// # Parameters
    ///
    /// * `size` - Maximum number of browsers in the pool.
    ///
    /// # Example
    ///
    /// ```rust
    /// use html2pdf_api::BrowserPoolConfigBuilder;
    ///
    /// let config = BrowserPoolConfigBuilder::new()
    ///     .max_pool_size(10)
    ///     .build()
    ///     .unwrap();
    ///
    /// assert_eq!(config.max_pool_size, 10);
    /// ```
    pub fn max_pool_size(mut self, size: usize) -> Self {
        self.config.max_pool_size = size;
        self
    }

    /// Set warmup count (must be d max_pool_size).
    ///
    /// # Parameters
    ///
    /// * `count` - Number of browsers to pre-create during warmup.
    ///
    /// # Example
    ///
    /// ```rust
    /// use html2pdf_api::BrowserPoolConfigBuilder;
    ///
    /// let config = BrowserPoolConfigBuilder::new()
    ///     .max_pool_size(10)
    ///     .warmup_count(5)
    ///     .build()
    ///     .unwrap();
    ///
    /// assert_eq!(config.warmup_count, 5);
    /// ```
    pub fn warmup_count(mut self, count: usize) -> Self {
        self.config.warmup_count = count;
        self
    }

    /// Set health check interval.
    ///
    /// # Parameters
    ///
    /// * `interval` - Duration between health check pings.
    ///
    /// # Example
    ///
    /// ```rust
    /// use std::time::Duration;
    /// use html2pdf_api::BrowserPoolConfigBuilder;
    ///
    /// let config = BrowserPoolConfigBuilder::new()
    ///     .ping_interval(Duration::from_secs(30))
    ///     .build()
    ///     .unwrap();
    ///
    /// assert_eq!(config.ping_interval, Duration::from_secs(30));
    /// ```
    pub fn ping_interval(mut self, interval: Duration) -> Self {
        self.config.ping_interval = interval;
        self
    }

    /// Set browser time-to-live before forced retirement.
    ///
    /// # Parameters
    ///
    /// * `ttl` - Maximum lifetime for each browser instance.
    ///
    /// # Example
    ///
    /// ```rust
    /// use std::time::Duration;
    /// use html2pdf_api::BrowserPoolConfigBuilder;
    ///
    /// let config = BrowserPoolConfigBuilder::new()
    ///     .browser_ttl(Duration::from_secs(7200)) // 2 hours
    ///     .build()
    ///     .unwrap();
    ///
    /// assert_eq!(config.browser_ttl, Duration::from_secs(7200));
    /// ```
    pub fn browser_ttl(mut self, ttl: Duration) -> Self {
        self.config.browser_ttl = ttl;
        self
    }

    /// Set maximum consecutive ping failures before removal.
    ///
    /// # Parameters
    ///
    /// * `failures` - Number of consecutive failures tolerated.
    ///
    /// # Example
    ///
    /// ```rust
    /// use html2pdf_api::BrowserPoolConfigBuilder;
    ///
    /// let config = BrowserPoolConfigBuilder::new()
    ///     .max_ping_failures(5)
    ///     .build()
    ///     .unwrap();
    ///
    /// assert_eq!(config.max_ping_failures, 5);
    /// ```
    pub fn max_ping_failures(mut self, failures: u32) -> Self {
        self.config.max_ping_failures = failures;
        self
    }

    /// Set warmup timeout.
    ///
    /// # Parameters
    ///
    /// * `timeout` - Maximum time allowed for warmup to complete.
    ///
    /// # Example
    ///
    /// ```rust
    /// use std::time::Duration;
    /// use html2pdf_api::BrowserPoolConfigBuilder;
    ///
    /// let config = BrowserPoolConfigBuilder::new()
    ///     .warmup_timeout(Duration::from_secs(120))
    ///     .build()
    ///     .unwrap();
    ///
    /// assert_eq!(config.warmup_timeout, Duration::from_secs(120));
    /// ```
    pub fn warmup_timeout(mut self, timeout: Duration) -> Self {
        self.config.warmup_timeout = timeout;
        self
    }

    /// Set warmup stagger interval.
    ///
    /// # Parameters
    ///
    /// * `stagger` - Delay between browser spawns during initial warmup.
    pub fn warmup_stagger(mut self, stagger: Duration) -> Self {
        self.config.warmup_stagger = stagger;
        self
    }

    /// Build and validate the configuration.
    ///
    /// # Errors
    ///
    /// - Returns error if `max_pool_size` is 0
    /// - Returns error if `warmup_count` > `max_pool_size`
    ///
    /// # Example
    ///
    /// ```rust
    /// use html2pdf_api::BrowserPoolConfigBuilder;
    ///
    /// // Valid configuration
    /// let config = BrowserPoolConfigBuilder::new()
    ///     .max_pool_size(10)
    ///     .warmup_count(5)
    ///     .build();
    /// assert!(config.is_ok());
    ///
    /// // Invalid: pool size is 0
    /// let config = BrowserPoolConfigBuilder::new()
    ///     .max_pool_size(0)
    ///     .build();
    /// assert!(config.is_err());
    ///
    /// // Invalid: warmup exceeds pool size
    /// let config = BrowserPoolConfigBuilder::new()
    ///     .max_pool_size(5)
    ///     .warmup_count(10)
    ///     .build();
    /// assert!(config.is_err());
    /// ```
    pub fn build(self) -> std::result::Result<BrowserPoolConfig, String> {
        // Validation: Pool size must be positive
        if self.config.max_pool_size == 0 {
            return Err("max_pool_size must be greater than 0".to_string());
        }

        // Validation: Can't warmup more browsers than pool can hold
        if self.config.warmup_count > self.config.max_pool_size {
            return Err("warmup_count cannot exceed max_pool_size".to_string());
        }

        Ok(self.config)
    }
}

impl Default for BrowserPoolConfigBuilder {
    fn default() -> Self {
        Self::new()
    }
}

// ============================================================================
// Environment Configuration (feature-gated)
// ============================================================================

/// Environment-based configuration loading.
///
/// This module is only available when the `env-config` feature is enabled.
///
/// # Environment File
///
/// This module uses `dotenvy` to load environment variables from an `app.env`
/// file in the current directory. The file is optional - if not found,
/// environment variables and defaults are used.
///
/// # Environment Variables
///
/// | Variable | Type | Default | Description |
/// |----------|------|---------|-------------|
/// | `BROWSER_POOL_SIZE` | usize | 5 | Maximum pool size |
/// | `BROWSER_WARMUP_COUNT` | usize | 3 | Warmup browser count |
/// | `BROWSER_TTL_SECONDS` | u64 | 3600 | Browser TTL in seconds |
/// | `BROWSER_WARMUP_TIMEOUT_SECONDS` | u64 | 60 | Warmup timeout |
/// | `BROWSER_PING_INTERVAL_SECONDS` | u64 | 15 | Health check interval |
/// | `BROWSER_MAX_PING_FAILURES` | u32 | 3 | Max ping failures |
/// | `CHROME_PATH` | String | auto | Custom Chrome binary path |
///
/// # Example `app.env` File
///
/// ```text
/// # Browser Pool Configuration
/// BROWSER_POOL_SIZE=5
/// BROWSER_WARMUP_COUNT=3
/// BROWSER_TTL_SECONDS=3600
/// BROWSER_WARMUP_TIMEOUT_SECONDS=60
/// BROWSER_PING_INTERVAL_SECONDS=15
/// BROWSER_MAX_PING_FAILURES=3
///
/// # Chrome Configuration (optional)
/// # CHROME_PATH=/usr/bin/google-chrome
/// ```
#[cfg(feature = "env-config")]
pub mod env {
    use super::*;
    use crate::error::BrowserPoolError;

    /// Default environment file name.
    pub const ENV_FILE_NAME: &str = "app.env";

    /// Load environment variables from `app.env` file.
    ///
    /// Call this early in your application startup to ensure environment
    /// variables are loaded before any configuration functions are called.
    ///
    /// This function is automatically called by [`from_env`], but you can
    /// call it explicitly if you need to load the file earlier or check
    /// for errors.
    ///
    /// # Returns
    ///
    /// - `Ok(PathBuf)` if the file was found and loaded successfully
    /// - `Err(dotenvy::Error)` if the file was not found or couldn't be parsed
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// use html2pdf_api::config::env::load_env_file;
    ///
    /// // Load at application startup
    /// match load_env_file() {
    ///     Ok(path) => println!("Loaded environment from: {:?}", path),
    ///     Err(e) => println!("No app.env file found: {}", e),
    /// }
    /// ```
    pub fn load_env_file() -> Result<std::path::PathBuf, dotenvy::Error> {
        dotenvy::from_filename(ENV_FILE_NAME)
    }

    /// Load configuration from environment variables.
    ///
    /// Reads configuration from environment variables with sensible defaults.
    /// Also loads `app.env` file if present (via `dotenvy`).
    ///
    /// # Environment File
    ///
    /// This function looks for an `app.env` file in the current directory
    /// and loads it if present. The file is optional - if not found,
    /// environment variables and defaults are used.
    ///
    /// # Environment Variables
    ///
    /// - `BROWSER_POOL_SIZE`: Maximum pool size (default: 5)
    /// - `BROWSER_WARMUP_COUNT`: Warmup browser count (default: 3)
    /// - `BROWSER_TTL_SECONDS`: Browser TTL in seconds (default: 3600)
    /// - `BROWSER_WARMUP_TIMEOUT_SECONDS`: Warmup timeout (default: 60)
    /// - `BROWSER_PING_INTERVAL_SECONDS`: Health check interval (default: 15)
    /// - `BROWSER_MAX_PING_FAILURES`: Max ping failures (default: 3)
    ///
    /// # Errors
    ///
    /// Returns [`BrowserPoolError::Configuration`] if configuration values are invalid.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// use html2pdf_api::config::env::from_env;
    ///
    /// // Set environment variables before calling
    /// std::env::set_var("BROWSER_POOL_SIZE", "10");
    ///
    /// let config = from_env()?;
    /// assert_eq!(config.max_pool_size, 10);
    /// ```
    pub fn from_env() -> Result<BrowserPoolConfig, BrowserPoolError> {
        // Load app.env file if present (ignore errors if not found)
        match load_env_file() {
            Ok(path) => {
                log::info!("⚙️ Loaded configuration from: {:?}", path);
            }
            Err(e) => {
                log::debug!(
                    "⚠️ No {} file found or failed to load: {} (using environment variables and defaults)",
                    ENV_FILE_NAME,
                    e
                );
            }
        }

        let max_pool_size = std::env::var("BROWSER_POOL_SIZE")
            .ok()
            .and_then(|s| s.parse().ok())
            .unwrap_or(5);

        let warmup_count = std::env::var("BROWSER_WARMUP_COUNT")
            .ok()
            .and_then(|s| s.parse().ok())
            .unwrap_or(3);

        let ttl_seconds = std::env::var("BROWSER_TTL_SECONDS")
            .ok()
            .and_then(|s| s.parse().ok())
            .unwrap_or(3600u64);

        let warmup_timeout_seconds = std::env::var("BROWSER_WARMUP_TIMEOUT_SECONDS")
            .ok()
            .and_then(|s| s.parse().ok())
            .unwrap_or(60u64);

        let ping_interval_seconds = std::env::var("BROWSER_PING_INTERVAL_SECONDS")
            .ok()
            .and_then(|s| s.parse().ok())
            .unwrap_or(15u64);

        let max_ping_failures = std::env::var("BROWSER_MAX_PING_FAILURES")
            .ok()
            .and_then(|s| s.parse().ok())
            .unwrap_or(3);

        log::info!("' Loading pool configuration from environment:");
        log::info!("   - Max pool size: {}", max_pool_size);
        log::info!("   - Warmup count: {}", warmup_count);
        log::info!(
            "   - Browser TTL: {}s ({}min)",
            ttl_seconds,
            ttl_seconds / 60
        );
        log::info!("   - Warmup timeout: {}s", warmup_timeout_seconds);
        log::info!("   - Ping interval: {}s", ping_interval_seconds);
        log::info!("   - Max ping failures: {}", max_ping_failures);

        BrowserPoolConfigBuilder::new()
            .max_pool_size(max_pool_size)
            .warmup_count(warmup_count)
            .browser_ttl(Duration::from_secs(ttl_seconds))
            .warmup_timeout(Duration::from_secs(warmup_timeout_seconds))
            .ping_interval(Duration::from_secs(ping_interval_seconds))
            .max_ping_failures(max_ping_failures)
            .build()
            .map_err(BrowserPoolError::Configuration)
    }

    /// Get Chrome path from environment.
    ///
    /// Reads `CHROME_PATH` environment variable.
    ///
    /// **Note:** Call [`from_env`] or [`load_env_file`] first to ensure
    /// `app.env` is loaded if you're using a configuration file.
    ///
    /// # Returns
    ///
    /// - `Some(path)` if `CHROME_PATH` is set
    /// - `None` if not set (will use auto-detection)
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// use html2pdf_api::config::env::{load_env_file, chrome_path_from_env};
    ///
    /// // Ensure app.env is loaded first
    /// let _ = load_env_file();
    ///
    /// let path = chrome_path_from_env();
    /// if let Some(p) = path {
    ///     println!("Using Chrome at: {}", p);
    /// }
    /// ```
    pub fn chrome_path_from_env() -> Option<String> {
        std::env::var("CHROME_PATH").ok()
    }
}

// ============================================================================
// Unit Tests
// ============================================================================

#[cfg(test)]
mod tests {
    use super::*;

    /// Verifies that BrowserPoolConfigBuilder correctly sets all configuration values.
    ///
    /// Tests the happy path where all values are valid and within constraints.
    #[test]
    fn test_config_builder() {
        let config = BrowserPoolConfigBuilder::new()
            .max_pool_size(10)
            .warmup_count(5)
            .browser_ttl(Duration::from_secs(7200))
            .warmup_timeout(Duration::from_secs(120))
            .build()
            .unwrap();

        assert_eq!(config.max_pool_size, 10);
        assert_eq!(config.warmup_count, 5);
        assert_eq!(config.browser_ttl.as_secs(), 7200);
        assert_eq!(config.warmup_timeout.as_secs(), 120);
    }

    /// Verifies that config builder rejects invalid pool size (zero).
    ///
    /// Pool size must be at least 1 to be useful. This test ensures
    /// the validation catches this error at build time.
    #[test]
    fn test_config_validation() {
        let result = BrowserPoolConfigBuilder::new().max_pool_size(0).build();

        assert!(result.is_err());
        let err_msg = result.unwrap_err();
        assert!(
            err_msg.contains("max_pool_size must be greater than 0"),
            "Expected validation error message, got: {}",
            err_msg
        );
    }

    /// Verifies that warmup count cannot exceed pool size.
    ///
    /// It's illogical to warmup more browsers than the pool can hold.
    /// This test ensures the configuration builder catches this mistake.
    #[test]
    fn test_config_warmup_exceeds_pool() {
        let result = BrowserPoolConfigBuilder::new()
            .max_pool_size(5)
            .warmup_count(10)
            .build();

        assert!(result.is_err());
        let err_msg = result.unwrap_err();
        assert!(
            err_msg.contains("warmup_count cannot exceed max_pool_size"),
            "Expected validation error message, got: {}",
            err_msg
        );
    }

    /// Verifies that default configuration values are production-ready.
    ///
    /// These defaults are used when no explicit configuration is provided.
    /// They should be safe and reasonable for most use cases.
    #[test]
    fn test_config_defaults() {
        let config = BrowserPoolConfig::default();

        // Verify production-ready defaults
        assert_eq!(config.max_pool_size, 5, "Default pool size should be 5");
        assert_eq!(config.warmup_count, 3, "Default warmup should be 3");
        assert_eq!(
            config.ping_interval,
            Duration::from_secs(15),
            "Default ping interval should be 15s"
        );
        assert_eq!(
            config.browser_ttl,
            Duration::from_secs(3600),
            "Default TTL should be 1 hour"
        );
        assert_eq!(
            config.max_ping_failures, 3,
            "Default max failures should be 3"
        );
        assert_eq!(
            config.warmup_timeout,
            Duration::from_secs(60),
            "Default warmup timeout should be 60s"
        );
    }

    /// Verifies that config builder supports method chaining.
    ///
    /// The builder pattern should allow fluent API usage where all
    /// setters can be chained together.
    #[test]
    fn test_config_builder_chaining() {
        let config = BrowserPoolConfigBuilder::new()
            .max_pool_size(8)
            .warmup_count(4)
            .ping_interval(Duration::from_secs(30))
            .browser_ttl(Duration::from_secs(1800))
            .max_ping_failures(5)
            .warmup_timeout(Duration::from_secs(90))
            .build()
            .unwrap();

        // Verify all chained values were set correctly
        assert_eq!(config.max_pool_size, 8);
        assert_eq!(config.warmup_count, 4);
        assert_eq!(config.ping_interval.as_secs(), 30);
        assert_eq!(config.browser_ttl.as_secs(), 1800);
        assert_eq!(config.max_ping_failures, 5);
        assert_eq!(config.warmup_timeout.as_secs(), 90);
    }

    /// Verifies that BrowserPoolConfigBuilder implements Default.
    #[test]
    fn test_builder_default() {
        let builder: BrowserPoolConfigBuilder = Default::default();
        let config = builder.build().unwrap();

        // Should have same values as BrowserPoolConfig::default()
        assert_eq!(config.max_pool_size, 5);
        assert_eq!(config.warmup_count, 3);
    }
}