Skip to main content

html2pdf_api/service/
pdf.rs

1//! Core PDF generation service (framework-agnostic).
2//!
3//! This module contains the core PDF generation logic that is shared across
4//! all web framework integrations. The functions here are **synchronous/blocking**
5//! and should be called from within a blocking context (e.g., `tokio::task::spawn_blocking`,
6//! `actix_web::web::block`, etc.).
7//!
8//! # Architecture
9//!
10//! ```text
11//! ┌─────────────────────────────────────────────────────────────────┐
12//! │                    Framework Integration                        │
13//! │              (Actix-web / Rocket / Axum)                        │
14//! └─────────────────────────┬───────────────────────────────────────┘
15//!                           │ async context
16//!                           ▼
17//! ┌─────────────────────────────────────────────────────────────────┐
18//! │              spawn_blocking / web::block                        │
19//! └─────────────────────────┬───────────────────────────────────────┘
20//!                           │ blocking context
21//!                           ▼
22//! ┌─────────────────────────────────────────────────────────────────┐
23//! │                  This Module (pdf.rs)                           │
24//! │  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐  │
25//! │  │generate_pdf_    │  │generate_pdf_    │  │get_pool_stats   │  │
26//! │  │from_url         │  │from_html        │  │                 │  │
27//! │  └────────┬────────┘  └────────┬────────┘  └─────────────────┘  │
28//! │           │                    │                                │
29//! │           └──────────┬─────────┘                                │
30//! │                      ▼                                          │
31//! │           ┌─────────────────────┐                               │
32//! │           │generate_pdf_internal│                               │
33//! │           └──────────┬──────────┘                               │
34//! └──────────────────────┼──────────────────────────────────────────┘
35//!                        │
36//!                        ▼
37//! ┌─────────────────────────────────────────────────────────────────┐
38//! │                    BrowserPool                                  │
39//! │                 (headless_chrome)                               │
40//! └─────────────────────────────────────────────────────────────────┘
41//! ```
42//!
43//! # Thread Safety
44//!
45//! All functions in this module are designed to be called from multiple threads
46//! concurrently. The browser pool is protected by a `Mutex`, and each PDF
47//! generation operation acquires a browser, uses it, and returns it to the pool
48//! automatically via RAII.
49//!
50//! # Blocking Behavior
51//!
52//! **Important:** These functions block the calling thread. In an async context,
53//! always wrap calls in a blocking task:
54//!
55//! ```rust,ignore
56//! // ✅ Correct: Using spawn_blocking
57//! let result = tokio::task::spawn_blocking(move || {
58//!     generate_pdf_from_url(&pool, &request)
59//! }).await?;
60//!
61//! // ❌ Wrong: Calling directly in async context
62//! // This will block the async runtime!
63//! let result = generate_pdf_from_url(&pool, &request);
64//! ```
65//!
66//! # Usage Examples
67//!
68//! ## Basic URL to PDF Conversion
69//!
70//! ```rust,ignore
71//! use html2pdf_api::service::{generate_pdf_from_url, PdfFromUrlRequest};
72//!
73//! // Assuming `pool` is a BrowserPool
74//! let request = PdfFromUrlRequest {
75//!     url: "https://example.com".to_string(),
76//!     ..Default::default()
77//! };
78//!
79//! // In a blocking context:
80//! let response = generate_pdf_from_url(&pool, &request)?;
81//! println!("Generated PDF: {} bytes", response.data.len());
82//! ```
83//!
84//! ## HTML to PDF Conversion
85//!
86//! ```rust,ignore
87//! use html2pdf_api::service::{generate_pdf_from_html, PdfFromHtmlRequest};
88//!
89//! let request = PdfFromHtmlRequest {
90//!     html: "<html><body><h1>Hello World</h1></body></html>".to_string(),
91//!     filename: Some("hello.pdf".to_string()),
92//!     ..Default::default()
93//! };
94//!
95//! let response = generate_pdf_from_html(&pool, &request)?;
96//! std::fs::write("hello.pdf", &response.data)?;
97//! ```
98//!
99//! ## With Async Web Framework
100//!
101//! ```rust,ignore
102//! use actix_web::{web, HttpResponse};
103//! use html2pdf_api::service::{generate_pdf_from_url, PdfFromUrlRequest};
104//!
105//! async fn handler(
106//!     pool: web::Data<SharedPool>,
107//!     query: web::Query<PdfFromUrlRequest>,
108//! ) -> HttpResponse {
109//!     let pool = pool.into_inner();
110//!     let request = query.into_inner();
111//!
112//!     let result = web::block(move || {
113//!         generate_pdf_from_url(&pool, &request)
114//!     }).await;
115//!
116//!     match result {
117//!         Ok(Ok(pdf)) => HttpResponse::Ok()
118//!             .content_type("application/pdf")
119//!             .body(pdf.data),
120//!         Ok(Err(e)) => HttpResponse::BadRequest().body(e.to_string()),
121//!         Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
122//!     }
123//! }
124//! ```
125//!
126//! # Performance Considerations
127//!
128//! | Operation | Typical Duration | Notes |
129//! |-----------|------------------|-------|
130//! | Pool lock acquisition | < 1ms | Fast, non-blocking |
131//! | Browser checkout | < 1ms | If browser available |
132//! | Browser creation | 500ms - 2s | If pool needs to create new browser |
133//! | Page navigation | 100ms - 10s | Depends on target page |
134//! | JavaScript wait | 0 - 15s | Configurable via `waitsecs` |
135//! | PDF generation | 100ms - 5s | Depends on page complexity |
136//! | Tab cleanup | < 100ms | Best effort, non-blocking |
137//!
138//! # Error Handling
139//!
140//! All functions return `Result<T, PdfServiceError>`. Errors are categorized
141//! and include appropriate HTTP status codes. See [`PdfServiceError`] for
142//! the complete error taxonomy.
143//!
144//! [`PdfServiceError`]: crate::service::PdfServiceError
145
146use headless_chrome::types::PrintToPdfOptions;
147use std::time::{Duration, Instant};
148
149use crate::handle::BrowserHandle;
150use crate::pool::BrowserPool;
151use crate::service::types::*;
152
153// ============================================================================
154// Constants
155// ============================================================================
156
157/// Default timeout for the entire PDF generation operation in seconds.
158///
159/// This timeout encompasses the complete operation including:
160/// - Browser acquisition from pool
161/// - Page navigation
162/// - JavaScript execution wait
163/// - PDF rendering
164/// - Tab cleanup
165///
166/// If the operation exceeds this duration, a [`PdfServiceError::Timeout`]
167/// error is returned.
168///
169/// # Default Value
170///
171/// `60` seconds - sufficient for most web pages, including those with
172/// heavy JavaScript and external resources.
173///
174/// # Customization
175///
176/// This constant is used by framework integrations for their timeout wrappers.
177/// To customize, create your own timeout wrapper around the service functions.
178///
179/// ```rust,ignore
180/// use std::time::Duration;
181/// use tokio::time::timeout;
182///
183/// let custom_timeout = Duration::from_secs(120); // 2 minutes
184///
185/// let result = timeout(custom_timeout, async {
186///     tokio::task::spawn_blocking(move || {
187///         generate_pdf_from_url(&pool, &request)
188///     }).await
189/// }).await;
190/// ```
191pub const DEFAULT_TIMEOUT_SECS: u64 = 60;
192
193/// Default wait time for JavaScript execution in seconds.
194///
195/// After page navigation completes, the service waits for JavaScript to finish
196/// rendering dynamic content. This constant defines the default wait time when
197/// not specified in the request.
198///
199/// # Behavior
200///
201/// During the wait period, the service polls every 200ms for `window.isPageDone === true`.
202/// If the page sets this flag, PDF generation proceeds immediately. Otherwise,
203/// the full wait duration elapses before generating the PDF.
204///
205/// # Default Value
206///
207/// `5` seconds - balances between allowing time for JavaScript execution
208/// and not waiting unnecessarily for simple pages.
209///
210/// # Recommendations
211///
212/// | Page Type | Recommended Wait |
213/// |-----------|------------------|
214/// | Static HTML | 1-2 seconds |
215/// | Light JavaScript (vanilla JS, jQuery) | 3-5 seconds |
216/// | Heavy SPA (React, Vue, Angular) | 5-10 seconds |
217/// | Complex visualizations (D3, charts) | 10-15 seconds |
218/// | Real-time data loading | 10-20 seconds |
219pub const DEFAULT_WAIT_SECS: u64 = 5;
220
221/// Polling interval for JavaScript completion check in milliseconds.
222///
223/// When waiting for JavaScript to complete, the service checks for
224/// `window.isPageDone === true` at this interval.
225///
226/// # Trade-offs
227///
228/// - **Shorter interval**: More responsive but higher CPU usage
229/// - **Longer interval**: Lower CPU usage but may overshoot ready state
230///
231/// # Default Value
232///
233/// `200` milliseconds - provides good responsiveness without excessive polling.
234const JS_POLL_INTERVAL_MS: u64 = 200;
235
236// ============================================================================
237// Public API - Core PDF Generation Functions
238// ============================================================================
239
240/// Generate a PDF from a URL.
241///
242/// Navigates to the specified URL using a browser from the pool, waits for
243/// JavaScript execution, and generates a PDF of the rendered page.
244///
245/// # Thread Safety
246///
247/// This function is thread-safe and can be called concurrently from multiple
248/// threads. The browser pool mutex ensures safe access to shared resources.
249///
250/// # Blocking Behavior
251///
252/// **This function blocks the calling thread.** In async contexts, wrap it
253/// in `tokio::task::spawn_blocking`, `actix_web::web::block`, or similar.
254///
255/// # Arguments
256///
257/// * `pool` - Reference to the browser pool. The pool uses fine-grained internal locks;\n///   browser checkout is fast (~1ms) and concurrent.
258/// * `request` - PDF generation parameters. See [`PdfFromUrlRequest`] for details.
259///
260/// # Returns
261///
262/// * `Ok(PdfResponse)` - Successfully generated PDF with binary data and metadata
263/// * `Err(PdfServiceError)` - Error with details about what went wrong
264///
265/// # Errors
266///
267/// | Error | Cause | Resolution |
268/// |-------|-------|------------|
269/// | [`InvalidUrl`] | URL is empty or malformed | Provide valid HTTP/HTTPS URL |
270/// | [`BrowserUnavailable`] | Pool exhausted | Retry or increase pool size |
271/// | [`TabCreationFailed`] | Browser issue | Automatic recovery |
272/// | [`NavigationFailed`] | URL unreachable | Check URL accessibility |
273/// | [`NavigationTimeout`] | Page too slow | Increase timeout or optimize page |
274/// | [`PdfGenerationFailed`] | Rendering issue | Simplify page or check content |
275///
276/// [`InvalidUrl`]: PdfServiceError::InvalidUrl
277/// [`BrowserUnavailable`]: PdfServiceError::BrowserUnavailable
278/// [`TabCreationFailed`]: PdfServiceError::TabCreationFailed
279/// [`NavigationFailed`]: PdfServiceError::NavigationFailed
280/// [`NavigationTimeout`]: PdfServiceError::NavigationTimeout
281/// [`PdfGenerationFailed`]: PdfServiceError::PdfGenerationFailed
282///
283/// # Examples
284///
285/// ## Basic Usage
286///
287/// ```rust,ignore
288/// use html2pdf_api::service::{generate_pdf_from_url, PdfFromUrlRequest};
289///
290/// let request = PdfFromUrlRequest {
291///     url: "https://example.com".to_string(),
292///     ..Default::default()
293/// };
294///
295/// let response = generate_pdf_from_url(&pool, &request)?;
296/// assert!(response.data.starts_with(b"%PDF-")); // Valid PDF header
297/// ```
298///
299/// ## With Custom Options
300///
301/// ```rust,ignore
302/// let request = PdfFromUrlRequest {
303///     url: "https://example.com/report".to_string(),
304///     filename: Some("quarterly-report.pdf".to_string()),
305///     landscape: Some(true),      // Wide tables
306///     waitsecs: Some(10),         // Complex charts
307///     download: Some(true),       // Force download
308///     print_background: Some(true),
309/// };
310///
311/// let response = generate_pdf_from_url(&pool, &request)?;
312/// println!("Generated {} with {} bytes", response.filename, response.size());
313/// ```
314///
315/// ## Error Handling
316///
317/// ```rust,ignore
318/// match generate_pdf_from_url(&pool, &request) {
319///     Ok(pdf) => {
320///         // Success - use pdf.data
321///     }
322///     Err(PdfServiceError::InvalidUrl(msg)) => {
323///         // Client error - return 400
324///         eprintln!("Bad URL: {}", msg);
325///     }
326///     Err(PdfServiceError::BrowserUnavailable(_)) => {
327///         // Transient error - retry
328///         std::thread::sleep(Duration::from_secs(1));
329///     }
330///     Err(e) => {
331///         // Other error
332///         eprintln!("PDF generation failed: {}", e);
333///     }
334/// }
335/// ```
336///
337/// # Performance
338///
339/// Typical execution time breakdown for a moderately complex page:
340///
341/// ```text
342/// ┌────────────────────────────────────────────────────────────────┐
343/// │ Browser checkout                                       ~1ms    │
344/// │ ├─────────────────────────────────────────────────────────────┤
345/// │ Tab creation                                          ~50ms   │
346/// │ ├─────────────────────────────────────────────────────────────┤
347/// │ Navigation + page load                                ~500ms  │
348/// │ ├─────────────────────────────────────────────────────────────┤
349/// │ JavaScript wait (configurable)                        ~5000ms │
350/// │ ├─────────────────────────────────────────────────────────────┤
351/// │ PDF rendering                                         ~200ms  │
352/// │ ├─────────────────────────────────────────────────────────────┤
353/// │ Tab cleanup                                           ~50ms   │
354/// └────────────────────────────────────────────────────────────────┘
355/// Total: ~5.8 seconds (dominated by JS wait)
356/// ```
357pub fn generate_pdf_from_url(
358    pool: &BrowserPool,
359    request: &PdfFromUrlRequest,
360) -> Result<PdfResponse, PdfServiceError> {
361    // Validate URL before acquiring browser
362    let url = validate_url(&request.url)?;
363
364    log::debug!(
365        "Generating PDF from URL: {} (landscape={}, wait={}s)",
366        url,
367        request.is_landscape(),
368        request.wait_duration().as_secs()
369    );
370
371    // Acquire browser from pool (lock held briefly)
372    let browser = acquire_browser(pool)?;
373
374    // Generate PDF (lock released, browser returned via RAII on completion/error)
375    let pdf_data = generate_pdf_internal(
376        &browser,
377        &url,
378        request.wait_duration(),
379        request.is_landscape(),
380        request.print_background(),
381    )?;
382
383    log::info!(
384        "✅ PDF generated successfully from URL: {} ({} bytes)",
385        url,
386        pdf_data.len()
387    );
388
389    Ok(PdfResponse::new(
390        pdf_data,
391        request.filename_or_default(),
392        request.is_download(),
393    ))
394}
395
396/// Generate a PDF from HTML content.
397///
398/// Loads the provided HTML content into a browser tab using a data URL,
399/// waits for any JavaScript execution, and generates a PDF.
400///
401/// # Thread Safety
402///
403/// This function is thread-safe and can be called concurrently from multiple
404/// threads. See [`generate_pdf_from_url`] for details.
405///
406/// # Blocking Behavior
407///
408/// **This function blocks the calling thread.** See [`generate_pdf_from_url`]
409/// for guidance on async usage.
410///
411/// # How It Works
412///
413/// The HTML content is converted to a data URL:
414///
415/// ```text
416/// data:text/html;charset=utf-8,<encoded-html-content>
417/// ```
418///
419/// This allows loading HTML directly without a web server. The browser
420/// renders the HTML as if it were loaded from a regular URL.
421///
422/// # Arguments
423///
424/// * `pool` - Reference to the mutex-wrapped browser pool
425/// * `request` - HTML content and generation parameters. See [`PdfFromHtmlRequest`].
426///
427/// # Returns
428///
429/// * `Ok(PdfResponse)` - Successfully generated PDF
430/// * `Err(PdfServiceError)` - Error details
431///
432/// # Errors
433///
434/// | Error | Cause | Resolution |
435/// |-------|-------|------------|
436/// | [`EmptyHtml`] | HTML content is empty/whitespace | Provide HTML content |
437/// | [`BrowserUnavailable`] | Pool exhausted | Retry or increase pool size |
438/// | [`NavigationFailed`] | HTML parsing issue | Check HTML validity |
439/// | [`PdfGenerationFailed`] | Rendering issue | Simplify HTML |
440///
441/// [`EmptyHtml`]: PdfServiceError::EmptyHtml
442/// [`BrowserUnavailable`]: PdfServiceError::BrowserUnavailable
443/// [`NavigationFailed`]: PdfServiceError::NavigationFailed
444/// [`PdfGenerationFailed`]: PdfServiceError::PdfGenerationFailed
445///
446/// # Limitations
447///
448/// ## External Resources
449///
450/// Since HTML is loaded via data URL, relative URLs don't work:
451///
452/// ```html
453/// <!-- ❌ Won't work - relative URL -->
454/// <img src="/images/logo.png">
455///
456/// <!-- ✅ Works - absolute URL -->
457/// <img src="https://example.com/images/logo.png">
458///
459/// <!-- ✅ Works - inline base64 -->
460/// <img src="data:image/png;base64,iVBORw0KGgo...">
461/// ```
462///
463/// ## Size Limits
464///
465/// Data URLs have browser-specific size limits. For very large HTML documents
466/// (> 1MB), consider:
467/// - Hosting the HTML on a temporary server
468/// - Using [`generate_pdf_from_url`] instead
469/// - Splitting into multiple PDFs
470///
471/// # Examples
472///
473/// ## Simple HTML
474///
475/// ```rust,ignore
476/// use html2pdf_api::service::{generate_pdf_from_html, PdfFromHtmlRequest};
477///
478/// let request = PdfFromHtmlRequest {
479///     html: "<h1>Hello World</h1><p>This is a test.</p>".to_string(),
480///     ..Default::default()
481/// };
482///
483/// let response = generate_pdf_from_html(&pool, &request)?;
484/// std::fs::write("output.pdf", &response.data)?;
485/// ```
486///
487/// ## Complete Document with Styling
488///
489/// ```rust,ignore
490/// let html = r#"
491/// <!DOCTYPE html>
492/// <html>
493/// <head>
494///     <meta charset="UTF-8">
495///     <style>
496///         body {
497///             font-family: 'Arial', sans-serif;
498///             margin: 40px;
499///             color: #333;
500///         }
501///         h1 {
502///             color: #0066cc;
503///             border-bottom: 2px solid #0066cc;
504///             padding-bottom: 10px;
505///         }
506///         table {
507///             width: 100%;
508///             border-collapse: collapse;
509///             margin-top: 20px;
510///         }
511///         th, td {
512///             border: 1px solid #ddd;
513///             padding: 12px;
514///             text-align: left;
515///         }
516///         th {
517///             background-color: #f5f5f5;
518///         }
519///     </style>
520/// </head>
521/// <body>
522///     <h1>Monthly Report</h1>
523///     <p>Generated on: 2024-01-15</p>
524///     <table>
525///         <tr><th>Metric</th><th>Value</th></tr>
526///         <tr><td>Revenue</td><td>$50,000</td></tr>
527///         <tr><td>Users</td><td>1,234</td></tr>
528///     </table>
529/// </body>
530/// </html>
531/// "#;
532///
533/// let request = PdfFromHtmlRequest {
534///     html: html.to_string(),
535///     filename: Some("monthly-report.pdf".to_string()),
536///     print_background: Some(true), // Include styled backgrounds
537///     ..Default::default()
538/// };
539///
540/// let response = generate_pdf_from_html(&pool, &request)?;
541/// ```
542///
543/// ## With Embedded Images
544///
545/// ```rust,ignore
546/// // Base64 encode an image
547/// let image_base64 = base64::encode(std::fs::read("logo.png")?);
548///
549/// let html = format!(r#"
550/// <!DOCTYPE html>
551/// <html>
552/// <body>
553///     <img src="data:image/png;base64,{}" alt="Logo">
554///     <h1>Company Report</h1>
555/// </body>
556/// </html>
557/// "#, image_base64);
558///
559/// let request = PdfFromHtmlRequest {
560///     html,
561///     ..Default::default()
562/// };
563///
564/// let response = generate_pdf_from_html(&pool, &request)?;
565/// ```
566pub fn generate_pdf_from_html(
567    pool: &BrowserPool,
568    request: &PdfFromHtmlRequest,
569) -> Result<PdfResponse, PdfServiceError> {
570    // Validate HTML content
571    if request.html.trim().is_empty() {
572        log::warn!("Empty HTML content provided");
573        return Err(PdfServiceError::EmptyHtml);
574    }
575
576    log::debug!(
577        "Generating PDF from HTML ({} bytes, landscape={}, wait={}s)",
578        request.html.len(),
579        request.is_landscape(),
580        request.wait_duration().as_secs()
581    );
582
583    // Acquire browser from pool
584    let browser = acquire_browser(pool)?;
585
586    // Convert HTML to data URL
587    // Using percent-encoding to handle special characters
588    let data_url = format!(
589        "data:text/html;charset=utf-8,{}",
590        urlencoding::encode(&request.html)
591    );
592
593    log::trace!("Data URL length: {} bytes", data_url.len());
594
595    // Generate PDF
596    let pdf_data = generate_pdf_internal(
597        &browser,
598        &data_url,
599        request.wait_duration(),
600        request.is_landscape(),
601        request.print_background(),
602    )?;
603
604    log::info!(
605        "✅ PDF generated successfully from HTML ({} bytes input → {} bytes output)",
606        request.html.len(),
607        pdf_data.len()
608    );
609
610    Ok(PdfResponse::new(
611        pdf_data,
612        request.filename_or_default(),
613        request.is_download(),
614    ))
615}
616
617/// Get current browser pool statistics.
618///
619/// Returns real-time metrics about the browser pool state including
620/// available browsers, active browsers, and total count.
621///
622/// # Thread Safety
623///
624/// This function briefly acquires the pool lock to read statistics.
625/// It's safe to call frequently for monitoring purposes.
626///
627/// # Blocking Behavior
628///
629/// This function is fast (< 1ms) as it reads from the pool's internal
630/// state. Safe to call frequently from health check endpoints.
631///
632/// # Arguments
633///
634/// * `pool` - Reference to the browser pool
635///
636/// # Returns
637///
638/// * `Ok(PoolStatsResponse)` - Current pool statistics
639///
640/// # Examples
641///
642/// ## Basic Usage
643///
644/// ```rust,ignore
645/// use html2pdf_api::service::get_pool_stats;
646///
647/// let stats = get_pool_stats(&pool)?;
648/// println!("Available: {}", stats.available);
649/// println!("Active: {}", stats.active);
650/// println!("Total: {}", stats.total);
651/// ```
652///
653/// ## Monitoring Integration
654///
655/// ```rust,ignore
656/// use prometheus::{Gauge, register_gauge};
657///
658/// lazy_static! {
659///     static ref POOL_AVAILABLE: Gauge = register_gauge!(
660///         "browser_pool_available",
661///         "Number of available browsers in pool"
662///     ).unwrap();
663///     static ref POOL_ACTIVE: Gauge = register_gauge!(
664///         "browser_pool_active",
665///         "Number of active browsers in pool"
666///     ).unwrap();
667/// }
668///
669/// fn update_metrics(pool: &Mutex<BrowserPool>) {
670///     if let Ok(stats) = get_pool_stats(pool) {
671///         POOL_AVAILABLE.set(stats.available as f64);
672///         POOL_ACTIVE.set(stats.active as f64);
673///     }
674/// }
675/// ```
676///
677/// ## Capacity Check
678///
679/// ```rust,ignore
680/// let stats = get_pool_stats(&pool)?;
681///
682/// if stats.available == 0 {
683///     log::warn!("No browsers available, requests may be delayed");
684/// }
685///
686/// let utilization = stats.active as f64 / stats.total.max(1) as f64;
687/// if utilization > 0.8 {
688///     log::warn!("Pool utilization at {:.0}%, consider scaling", utilization * 100.0);
689/// }
690/// ```
691pub fn get_pool_stats(pool: &BrowserPool) -> Result<PoolStatsResponse, PdfServiceError> {
692    let stats = pool.stats();
693
694    Ok(PoolStatsResponse {
695        available: stats.available,
696        active: stats.active,
697        total: stats.total,
698    })
699}
700
701/// Check if the browser pool is ready to handle requests.
702///
703/// Returns `true` if the pool has available browsers or capacity to create
704/// new ones. This is useful for readiness probes in container orchestration.
705///
706/// # Readiness Criteria
707///
708/// The pool is considered "ready" if either:
709/// - There are idle browsers available (`available > 0`), OR
710/// - There is capacity to create new browsers (`active < max_pool_size`)
711///
712/// The pool is "not ready" only when:
713/// - All browsers are in use AND the pool is at maximum capacity
714///
715/// # Arguments
716///
717/// * `pool` - Reference to the browser pool
718///
719/// # Returns
720///
721/// * `Ok(true)` - Pool can accept new requests
722/// * `Ok(false)` - Pool is at capacity, requests will queue
723///
724/// # Use Cases
725///
726/// ## Kubernetes Readiness Probe
727///
728/// ```yaml
729/// readinessProbe:
730///   httpGet:
731///     path: /ready
732///     port: 8080
733///   initialDelaySeconds: 5
734///   periodSeconds: 10
735/// ```
736///
737/// ## Load Balancer Health Check
738///
739/// When `is_pool_ready` returns `false`, the endpoint should return
740/// HTTP 503 Service Unavailable to remove the instance from rotation.
741///
742/// # Examples
743///
744/// ## Basic Check
745///
746/// ```rust,ignore
747/// use html2pdf_api::service::is_pool_ready;
748///
749/// if is_pool_ready(&pool)? {
750///     println!("Pool is ready to accept requests");
751/// } else {
752///     println!("Pool is at capacity");
753/// }
754/// ```
755///
756/// ## Request Gating
757///
758/// ```rust,ignore
759/// async fn handle_request(pool: &Mutex<BrowserPool>, request: PdfFromUrlRequest) -> Result<PdfResponse, Error> {
760///     // Quick capacity check before expensive operation
761///     if !is_pool_ready(pool)? {
762///         return Err(Error::ServiceUnavailable("Pool at capacity, try again later"));
763///     }
764///     
765///     // Proceed with PDF generation
766///     generate_pdf_from_url(pool, &request)
767/// }
768/// ```
769pub fn is_pool_ready(pool: &BrowserPool) -> Result<bool, PdfServiceError> {
770    let stats = pool.stats();
771    let config = pool.config();
772
773    // Ready if we have available browsers OR we can create more
774    let is_ready = stats.available > 0 || stats.active < config.max_pool_size;
775
776    log::trace!(
777        "Pool readiness check: available={}, active={}, max={}, ready={}",
778        stats.available,
779        stats.active,
780        config.max_pool_size,
781        is_ready
782    );
783
784    Ok(is_ready)
785}
786
787// ============================================================================
788// Internal Helper Functions
789// ============================================================================
790
791/// Validate and normalize a URL string.
792///
793/// Parses the URL using the `url` crate and returns the normalized form.
794/// This catches malformed URLs early, before acquiring a browser.
795///
796/// # Validation Rules
797///
798/// - URL must not be empty
799/// - URL must be parseable by the `url` crate
800/// - Scheme must be present (http/https/file/data)
801///
802/// # Arguments
803///
804/// * `url` - The URL string to validate
805///
806/// # Returns
807///
808/// * `Ok(String)` - The normalized URL
809/// * `Err(PdfServiceError::InvalidUrl)` - If validation fails
810///
811/// # Examples
812///
813/// ```rust,ignore
814/// assert!(validate_url("https://example.com").is_ok());
815/// assert!(validate_url("").is_err());
816/// assert!(validate_url("not-a-url").is_err());
817/// ```
818fn validate_url(url: &str) -> Result<String, PdfServiceError> {
819    // Check for empty URL first (better error message)
820    if url.trim().is_empty() {
821        log::debug!("URL validation failed: empty URL");
822        return Err(PdfServiceError::InvalidUrl("URL is required".to_string()));
823    }
824
825    // Parse and normalize the URL
826    match url::Url::parse(url) {
827        Ok(parsed) => {
828            let scheme = parsed.scheme();
829            if scheme != "http" && scheme != "https" && scheme != "data" {
830                log::debug!("URL validation failed: unsupported scheme '{}'", scheme);
831                return Err(PdfServiceError::InvalidUrl(format!(
832                    "Unsupported URL scheme '{}'. Only http, https, and data are allowed",
833                    scheme
834                )));
835            }
836            log::trace!("URL validated successfully: {}", parsed);
837            Ok(parsed.to_string())
838        }
839        Err(e) => {
840            log::debug!("URL validation failed for '{}': {}", url, e);
841            Err(PdfServiceError::InvalidUrl(e.to_string()))
842        }
843    }
844}
845
846/// Acquire a browser from the pool.
847///
848/// Locks the pool mutex, retrieves a browser, and returns it. The lock is
849/// released immediately after checkout, not held during PDF generation.
850///
851/// # Browser Lifecycle
852///
853/// The returned `BrowserHandle` uses RAII to automatically return the
854/// browser to the pool when dropped:
855///
856/// ```text
857/// ┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
858/// │  acquire_browser │ ──▶ │  BrowserHandle  │ ──▶ │  PDF Generation │
859/// │  (lock, get)     │     │  (RAII guard)   │     │  (uses browser) │
860/// └─────────────────┘     └─────────────────┘     └────────┬────────┘
861///                                                          │
862///                                                          ▼
863///                         ┌─────────────────┐     ┌─────────────────┐
864///                         │  Back to Pool   │ ◀── │  Drop Handle    │
865///                         │  (automatic)    │     │  (RAII cleanup) │
866///                         └─────────────────┘     └─────────────────┘
867/// ```
868///
869/// # Arguments
870///
871/// * `pool` - Reference to the mutex-wrapped browser pool
872///
873/// # Returns
874///
875/// * `Ok(BrowserHandle)` - A browser ready for use
876/// * `Err(PdfServiceError)` - If pool lock or browser acquisition fails
877fn acquire_browser(pool: &BrowserPool) -> Result<BrowserHandle, PdfServiceError> {
878    // Get a browser from the pool (no outer lock needed — pool uses internal locks)
879    let browser = pool.get().map_err(|e| {
880        log::error!("❌ Failed to get browser from pool: {}", e);
881        PdfServiceError::BrowserUnavailable(e.to_string())
882    })?;
883
884    log::debug!("Acquired browser {} from pool", browser.id());
885
886    Ok(browser)
887}
888
889/// Core PDF generation logic.
890///
891/// This function performs the actual work of:
892/// 1. Creating a new browser tab
893/// 2. Navigating to the URL
894/// 3. Waiting for JavaScript completion
895/// 4. Generating the PDF
896/// 5. Cleaning up the tab
897///
898/// # Arguments
899///
900/// * `browser` - Browser handle from the pool
901/// * `url` - URL to navigate to (can be http/https or data: URL)
902/// * `wait_duration` - How long to wait for JavaScript
903/// * `landscape` - Whether to use landscape orientation
904/// * `print_background` - Whether to include background graphics
905///
906/// # Returns
907///
908/// * `Ok(Vec<u8>)` - The raw PDF binary data
909/// * `Err(PdfServiceError)` - If any step fails
910///
911/// # Tab Lifecycle
912///
913/// A new tab is created for each PDF generation and closed afterward.
914/// This ensures clean state and prevents memory leaks from accumulating
915/// page resources.
916///
917/// ```text
918/// Browser Instance
919/// ├── Tab 1 (new) ◀── Created for this request
920/// │   ├── Navigate to URL
921/// │   ├── Wait for JS
922/// │   ├── Generate PDF
923/// │   └── Close tab ◀── Cleanup
924/// └── (available for next request)
925/// ```
926fn generate_pdf_internal(
927    browser: &BrowserHandle,
928    url: &str,
929    wait_duration: Duration,
930    landscape: bool,
931    print_background: bool,
932) -> Result<Vec<u8>, PdfServiceError> {
933    let start_time = Instant::now();
934
935    // Create new tab
936    log::trace!("Creating new browser tab");
937    let tab = browser.new_tab().map_err(|e| {
938        log::error!("❌ Failed to create tab: {}", e);
939        PdfServiceError::TabCreationFailed(e.to_string())
940    })?;
941
942    // Configure PDF options
943    let print_options = build_print_options(landscape, print_background);
944
945    // Navigate to URL
946    log::trace!("Navigating to URL: {}", truncate_url(url, 100));
947    let nav_start = Instant::now();
948
949    let page = tab
950        .navigate_to(url)
951        .map_err(|e| {
952            log::error!("❌ Failed to navigate to URL: {}", e);
953            PdfServiceError::NavigationFailed(e.to_string())
954        })?
955        .wait_until_navigated()
956        .map_err(|e| {
957            log::error!("❌ Navigation timeout: {}", e);
958            PdfServiceError::NavigationTimeout(e.to_string())
959        })?;
960
961    log::debug!("Navigation completed in {:?}", nav_start.elapsed());
962
963    // Wait for JavaScript execution
964    wait_for_page_ready(&tab, wait_duration);
965
966    // Generate PDF
967    log::trace!("Generating PDF");
968    let pdf_start = Instant::now();
969
970    let pdf_data = page.print_to_pdf(print_options).map_err(|e| {
971        log::error!("❌ Failed to generate PDF: {}", e);
972        PdfServiceError::PdfGenerationFailed(e.to_string())
973    })?;
974
975    log::debug!(
976        "PDF generated in {:?} ({} bytes)",
977        pdf_start.elapsed(),
978        pdf_data.len()
979    );
980
981    // Close tab (best effort - don't fail if this doesn't work)
982    close_tab_safely(&tab);
983
984    log::debug!("Total PDF generation time: {:?}", start_time.elapsed());
985
986    Ok(pdf_data)
987}
988
989/// Build PDF print options.
990///
991/// Creates the `PrintToPdfOptions` struct with the specified settings
992/// and sensible defaults for margins and other options.
993///
994/// # Default Settings
995///
996/// - **Margins**: All set to 0 (full page)
997/// - **Header/Footer**: Disabled
998/// - **Background**: Configurable (default: true)
999/// - **Scale**: 1.0 (100%)
1000fn build_print_options(landscape: bool, print_background: bool) -> Option<PrintToPdfOptions> {
1001    Some(PrintToPdfOptions {
1002        landscape: Some(landscape),
1003        display_header_footer: Some(false),
1004        print_background: Some(print_background),
1005        // Zero margins for full-page output
1006        margin_top: Some(0.0),
1007        margin_bottom: Some(0.0),
1008        margin_left: Some(0.0),
1009        margin_right: Some(0.0),
1010        // Use defaults for everything else
1011        ..Default::default()
1012    })
1013}
1014
1015/// Wait for the page to signal it's ready for PDF generation.
1016///
1017/// This function implements a polling loop that checks for `window.isPageDone === true`.
1018/// This allows JavaScript-heavy pages to signal when they've finished rendering,
1019/// enabling early PDF generation without waiting the full timeout.
1020///
1021/// # Behavior Summary
1022///
1023/// | Page State | Result |
1024/// |------------|--------|
1025/// | `window.isPageDone = true` | Returns **immediately** (early exit) |
1026/// | `window.isPageDone = false` | Waits **full duration** |
1027/// | `window.isPageDone` not defined | Waits **full duration** |
1028/// | JavaScript error during check | Waits **full duration** |
1029///
1030/// # Default Behavior (No Flag Set)
1031///
1032/// **Important:** If the page does not set `window.isPageDone = true`, this function
1033/// waits the **full `max_wait` duration** before returning. This is intentional -
1034/// it gives JavaScript-heavy pages time to render even without explicit signaling.
1035///
1036/// For example, with the default `waitsecs = 5`:
1037/// - A page **with** the flag set immediately: ~0ms wait
1038/// - A page **without** the flag: full 5000ms wait
1039///
1040/// # How It Works
1041///
1042/// ```text
1043/// ┌─────────────────────────────────────────────────────────────────┐
1044/// │                    wait_for_page_ready                          │
1045/// │                                                                 │
1046/// │   ┌─────────┐     ┌──────────────┐     ┌─────────────────────┐  │
1047/// │   │  Start  │────▶│ Check flag   │────▶│ window.isPageDone?  │  │
1048/// │   └─────────┘     └──────────────┘     └──────────┬──────────┘  │
1049/// │                                                   │             │
1050/// │                          ┌────────────────────────┼─────────┐   │
1051/// │                          │                        │         │   │
1052/// │                          ▼                        ▼         │   │
1053/// │                   ┌────────────┐           ┌───────────┐    │   │
1054/// │                   │   true     │           │  false /  │    │   │
1055/// │                   │ (ready!)   │           │ undefined │    │   │
1056/// │                   └─────┬──────┘           └─────┬─────┘    │   │
1057/// │                         │                        │          │   │
1058/// │                         ▼                        ▼          │   │
1059/// │                   ┌───────────┐           ┌───────────┐     │   │
1060/// │                   │  Return   │           │ Sleep     │     │   │
1061/// │                   │  early    │           │ 200ms     │─────┘   │
1062/// │                   └───────────┘           └───────────┘         │
1063/// │                                                  │              │
1064/// │                                                  ▼              │
1065/// │                                           ┌───────────┐         │
1066/// │                                           │ Timeout?  │         │
1067/// │                                           └─────┬─────┘         │
1068/// │                                                 │               │
1069/// │                                    ┌────────────┴────────────┐  │
1070/// │                                    ▼                         ▼  │
1071/// │                             ┌───────────┐              ┌──────┐ │
1072/// │                             │   Yes     │              │  No  │ │
1073/// │                             │ (proceed) │              │(loop)│ │
1074/// │                             └───────────┘              └──────┘ │
1075/// └─────────────────────────────────────────────────────────────────┘
1076/// ```
1077///
1078/// # Polling Timeline
1079///
1080/// The function polls every 200ms (see `JS_POLL_INTERVAL_MS`):
1081///
1082/// ```text
1083/// Time:   0ms    200ms   400ms   600ms   800ms  ...  5000ms
1084///          │       │       │       │       │           │
1085///          ▼       ▼       ▼       ▼       ▼           ▼
1086///        Poll    Poll    Poll    Poll    Poll  ...   Timeout
1087///          │       │       │       │       │           │
1088///          └───────┴───────┴───────┴───────┴───────────┤
1089///                                                      ▼
1090///                                              Proceed to PDF
1091///
1092/// If window.isPageDone = true at any poll → Exit immediately
1093/// ```
1094///
1095/// Each poll executes this JavaScript:
1096///
1097/// ```javascript
1098/// window.isPageDone === true  // Returns true, false, or undefined
1099/// ```
1100///
1101/// - `true` → Function returns immediately
1102/// - `false` / `undefined` / error → Continue polling until timeout
1103///
1104/// # Page-Side Implementation (Optional)
1105///
1106/// To enable early completion and avoid unnecessary waiting, add this to your
1107/// page's JavaScript **after** all content is rendered:
1108///
1109/// ```javascript
1110/// // Signal that the page is ready for PDF generation
1111/// window.isPageDone = true;
1112/// ```
1113///
1114/// ## Framework Examples
1115///
1116/// **React:**
1117/// ```javascript
1118/// useEffect(() => {
1119///     fetchData().then((result) => {
1120///         setData(result);
1121///         // Signal ready after state update and re-render
1122///         setTimeout(() => { window.isPageDone = true; }, 0);
1123///     });
1124/// }, []);
1125/// ```
1126///
1127/// **Vue:**
1128/// ```javascript
1129/// mounted() {
1130///     this.loadData().then(() => {
1131///         this.$nextTick(() => {
1132///             window.isPageDone = true;
1133///         });
1134///     });
1135/// }
1136/// ```
1137///
1138/// **Vanilla JavaScript:**
1139/// ```javascript
1140/// document.addEventListener('DOMContentLoaded', async () => {
1141///     await loadDynamicContent();
1142///     await renderCharts();
1143///     window.isPageDone = true;  // All done!
1144/// });
1145/// ```
1146///
1147/// # When to Increase `waitsecs`
1148///
1149/// If you cannot modify the target page to set `window.isPageDone`, increase
1150/// `waitsecs` based on the page complexity:
1151///
1152/// | Page Type | Recommended `waitsecs` |
1153/// |-----------|------------------------|
1154/// | Static HTML (no JS) | 1 |
1155/// | Light JS (form validation, simple DOM) | 2-3 |
1156/// | Moderate JS (API calls, dynamic content) | 5 (default) |
1157/// | Heavy SPA (React, Vue, Angular) | 5-10 |
1158/// | Complex visualizations (D3, charts, maps) | 10-15 |
1159/// | Pages loading external resources | 10-20 |
1160///
1161/// # Performance Optimization
1162///
1163/// For high-throughput scenarios, implementing `window.isPageDone` on your
1164/// pages can significantly improve performance:
1165///
1166/// ```text
1167/// Without flag (5s default wait):
1168///     Request 1: ████████████████████ 5.2s
1169///     Request 2: ████████████████████ 5.1s
1170///     Request 3: ████████████████████ 5.3s
1171///     Average: 5.2s per PDF
1172///
1173/// With flag (page ready in 800ms):
1174///     Request 1: ████ 0.9s
1175///     Request 2: ████ 0.8s
1176///     Request 3: ████ 0.9s
1177///     Average: 0.87s per PDF (6x faster!)
1178/// ```
1179///
1180/// # Arguments
1181///
1182/// * `tab` - The browser tab to check. Must have completed navigation.
1183/// * `max_wait` - Maximum time to wait before proceeding with PDF generation.
1184///   This is the upper bound; the function may return earlier if the page
1185///   signals readiness.
1186///
1187/// # Returns
1188///
1189/// This function returns `()` (unit). It either:
1190/// - Returns early when `window.isPageDone === true` is detected
1191/// - Returns after `max_wait` duration has elapsed (timeout)
1192///
1193/// In both cases, PDF generation proceeds afterward. This function never fails -
1194/// timeout is a normal completion path, not an error.
1195///
1196/// # Thread Blocking
1197///
1198/// This function blocks the calling thread with `std::thread::sleep()`.
1199/// Always call from within a blocking context (e.g., `spawn_blocking`).
1200///
1201/// # Example
1202///
1203/// ```rust,ignore
1204/// // Navigate to page first
1205/// let page = tab.navigate_to(url)?.wait_until_navigated()?;
1206///
1207/// // Wait up to 10 seconds for JavaScript
1208/// wait_for_page_ready(&tab, Duration::from_secs(10));
1209///
1210/// // Now generate PDF - page is either ready or we've waited long enough
1211/// let pdf_data = page.print_to_pdf(options)?;
1212/// ```
1213fn wait_for_page_ready(tab: &headless_chrome::Tab, max_wait: Duration) {
1214    let start = Instant::now();
1215    let poll_interval = Duration::from_millis(JS_POLL_INTERVAL_MS);
1216
1217    log::trace!(
1218        "Waiting up to {:?} for page to be ready (polling every {:?})",
1219        max_wait,
1220        poll_interval
1221    );
1222
1223    while start.elapsed() < max_wait {
1224        // Check if page signals completion
1225        let is_done = tab
1226            .evaluate("window.isPageDone === true", false)
1227            .map(|result| result.value.and_then(|v| v.as_bool()).unwrap_or(false))
1228            .unwrap_or(false);
1229
1230        if is_done {
1231            log::debug!("Page signaled ready after {:?}", start.elapsed());
1232            return;
1233        }
1234
1235        // Sleep before next poll
1236        std::thread::sleep(poll_interval);
1237    }
1238
1239    log::debug!(
1240        "Page wait completed after {:?} (timeout, proceeding anyway)",
1241        start.elapsed()
1242    );
1243}
1244
1245/// Safely close a browser tab, ignoring errors.
1246///
1247/// Tab cleanup is best-effort. If it fails, we log a warning but don't
1248/// propagate the error since the PDF generation already succeeded.
1249///
1250/// # Why Best-Effort?
1251///
1252/// - The PDF data is already captured
1253/// - Tab resources will be cleaned up when the browser is recycled
1254/// - Failing here would discard a valid PDF
1255/// - Some errors (e.g., browser already closed) are expected
1256///
1257/// # Arguments
1258///
1259/// * `tab` - The browser tab to close
1260fn close_tab_safely(tab: &headless_chrome::Tab) {
1261    log::trace!("Closing browser tab");
1262
1263    if let Err(e) = tab.close(true) {
1264        // Log but don't fail - PDF generation already succeeded
1265        log::warn!(
1266            "Failed to close tab (continuing anyway, resources will be cleaned up): {}",
1267            e
1268        );
1269    } else {
1270        log::trace!("Tab closed successfully");
1271    }
1272}
1273
1274/// Truncate a URL for logging purposes.
1275///
1276/// Data URLs can be extremely long (containing entire HTML documents).
1277/// This function truncates them for readable log output.
1278///
1279/// # Arguments
1280///
1281/// * `url` - The URL to truncate
1282/// * `max_len` - Maximum length before truncation
1283///
1284/// # Returns
1285///
1286/// The URL, truncated with "..." if longer than `max_len`.
1287fn truncate_url(url: &str, max_len: usize) -> String {
1288    if url.len() <= max_len {
1289        url.to_string()
1290    } else {
1291        format!("{}...", &url[..max_len])
1292    }
1293}
1294
1295// ============================================================================
1296// Unit Tests
1297// ============================================================================
1298
1299#[cfg(test)]
1300mod tests {
1301    use super::*;
1302
1303    // -------------------------------------------------------------------------
1304    // URL Validation Tests
1305    // -------------------------------------------------------------------------
1306
1307    #[test]
1308    fn test_validate_url_valid_https() {
1309        let result = validate_url("https://example.com");
1310        assert!(result.is_ok());
1311        assert_eq!(result.unwrap(), "https://example.com/");
1312    }
1313
1314    #[test]
1315    fn test_validate_url_valid_http() {
1316        let result = validate_url("http://example.com/path?query=value");
1317        assert!(result.is_ok());
1318    }
1319
1320    #[test]
1321    fn test_validate_url_valid_with_port() {
1322        let result = validate_url("http://localhost:3000/api");
1323        assert!(result.is_ok());
1324    }
1325
1326    #[test]
1327    fn test_validate_url_empty() {
1328        let result = validate_url("");
1329        assert!(matches!(result, Err(PdfServiceError::InvalidUrl(_))));
1330    }
1331
1332    #[test]
1333    fn test_validate_url_whitespace_only() {
1334        let result = validate_url("   ");
1335        assert!(matches!(result, Err(PdfServiceError::InvalidUrl(_))));
1336    }
1337
1338    #[test]
1339    fn test_validate_url_no_scheme() {
1340        let result = validate_url("example.com");
1341        assert!(matches!(result, Err(PdfServiceError::InvalidUrl(_))));
1342    }
1343
1344    #[test]
1345    fn test_validate_url_relative() {
1346        let result = validate_url("/path/to/page");
1347        assert!(matches!(result, Err(PdfServiceError::InvalidUrl(_))));
1348    }
1349
1350    #[test]
1351    fn test_validate_url_data_url() {
1352        let result = validate_url("data:text/html,<h1>Hello</h1>");
1353        assert!(result.is_ok());
1354    }
1355
1356    #[test]
1357    fn test_validate_url_file_url() {
1358        let result = validate_url("file:///etc/passwd");
1359        assert!(matches!(result, Err(PdfServiceError::InvalidUrl(_))));
1360    }
1361
1362    // -------------------------------------------------------------------------
1363    // Helper Function Tests
1364    // -------------------------------------------------------------------------
1365
1366    #[test]
1367    fn test_truncate_url_short() {
1368        let url = "https://example.com";
1369        assert_eq!(truncate_url(url, 50), url);
1370    }
1371
1372    #[test]
1373    fn test_truncate_url_long() {
1374        let url = "https://example.com/very/long/path/that/exceeds/the/maximum/length";
1375        let truncated = truncate_url(url, 30);
1376        assert_eq!(truncated.len(), 33); // 30 + "..."
1377        assert!(truncated.ends_with("..."));
1378    }
1379
1380    #[test]
1381    fn test_truncate_url_exact_length() {
1382        let url = "https://example.com";
1383        assert_eq!(truncate_url(url, url.len()), url);
1384    }
1385
1386    #[test]
1387    fn test_build_print_options_landscape() {
1388        let options = build_print_options(true, true).unwrap();
1389        assert_eq!(options.landscape, Some(true));
1390        assert_eq!(options.print_background, Some(true));
1391    }
1392
1393    #[test]
1394    fn test_build_print_options_portrait() {
1395        let options = build_print_options(false, false).unwrap();
1396        assert_eq!(options.landscape, Some(false));
1397        assert_eq!(options.print_background, Some(false));
1398    }
1399
1400    #[test]
1401    fn test_build_print_options_zero_margins() {
1402        let options = build_print_options(false, true).unwrap();
1403        assert_eq!(options.margin_top, Some(0.0));
1404        assert_eq!(options.margin_bottom, Some(0.0));
1405        assert_eq!(options.margin_left, Some(0.0));
1406        assert_eq!(options.margin_right, Some(0.0));
1407    }
1408
1409    #[test]
1410    fn test_build_print_options_no_header_footer() {
1411        let options = build_print_options(false, true).unwrap();
1412        assert_eq!(options.display_header_footer, Some(false));
1413    }
1414
1415    // -------------------------------------------------------------------------
1416    // Constants Tests
1417    // -------------------------------------------------------------------------
1418
1419    // -------------------------------------------------------------------------
1420
1421    #[test]
1422    #[allow(clippy::assertions_on_constants)]
1423    fn test_default_timeout_reasonable() {
1424        // Timeout should be at least 30 seconds for complex pages
1425        assert!(DEFAULT_TIMEOUT_SECS >= 30);
1426        // But not more than 5 minutes (would be too long)
1427        assert!(DEFAULT_TIMEOUT_SECS <= 300);
1428    }
1429
1430    #[test]
1431    #[allow(clippy::assertions_on_constants)]
1432    fn test_default_wait_reasonable() {
1433        // Wait should be at least 1 second for any JS
1434        assert!(DEFAULT_WAIT_SECS >= 1);
1435        // But not more than 30 seconds by default
1436        assert!(DEFAULT_WAIT_SECS <= 30);
1437    }
1438
1439    #[test]
1440    #[allow(clippy::assertions_on_constants)]
1441    fn test_poll_interval_reasonable() {
1442        // Poll interval should be at least 100ms (not too aggressive)
1443        assert!(JS_POLL_INTERVAL_MS >= 100);
1444        // But not more than 1 second (responsive enough)
1445        assert!(JS_POLL_INTERVAL_MS <= 1000);
1446    }
1447}