//! Stress test demonstrating resource exhaustion from concurrent redirect chains.
#![allow(
    clippy::cast_precision_loss,
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss,
    clippy::too_many_lines,
    clippy::manual_flatten
)]
//!
//! **VULNERABILITY FOUND**: Memory/resource spike from concurrent deep redirect chains.
//!
//! **ROOT CAUSE**:
//! - Redirect chain limit: `MAX_REDIRECT_HOPS` = 10 (good defense)
//! - Each redirect hop requires:
//!   - DNS lookup (possibly new domain)
//!   - TLS handshake (for HTTPS)
//!   - HTTP request/response
//!   - Memory for response headers/body
//! - With concurrent requests, resource usage multiplies
//! - Adversarial redirects can use unique subdomains per hop
//!
//! **Attack Vector**:
//! - Malicious site creates 10-hop redirect chain
//! - Each hop uses unique subdomain:
//!   <https://a.b.c.d.e.f.g.h.i.j.evil.com> →
//!   <https://b.c.d.e.f.g.h.i.j.k.evil.com> →
//!   ... (10 hops)
//! - Each subdomain requires DNS lookup (bypasses DNS cache)
//! - Each hop requires new TLS handshake
//! - Scanner processes 100 concurrent URLs with this pattern
//! - Result: 100 URLs × 10 hops = 1,000 concurrent operations
//! - Memory spike from 1,000 DNS queries + TLS handshakes + HTTP connections
//!
//! **Real-World Scenario**:
//! - Adversary detects scanning activity (User-Agent pattern)
//! - Returns deep redirect chain to exhaust resources
//! - Uses wildcard DNS: *.evil.com → attacker's server
//! - Each hop has small delay (100ms) to maximize resource hold time
//! - With 100 concurrent scans: 1,000 open connections for 1 second
//!
//! **Impact**: Memory spike, CPU spike from TLS handshakes, connection exhaustion
//!
//! **Recommended Fixes**:
//! - Limit concurrent redirects globally (not just per-URL)
//! - Implement DNS cache warming (pre-resolve all redirect targets)
//! - Add redirect budget: max total redirects across all URLs
//! - Monitor memory usage during redirect processing
//! - Reject redirect chains with too many unique domains

use axum::{
    extract::Path,
    response::{IntoResponse, Redirect},
    routing::get,
    Router,
};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::net::TcpListener;

/// Creates a mock server that generates deep redirect chains.
///
/// Each request to /`redirect/{hop_count`} returns a redirect to /{`hop_count` - 1}.
/// When `hop_count` reaches 0, returns final content.
async fn start_redirect_server(_total_hops: usize) -> String {
    let hop_counter = Arc::new(AtomicUsize::new(0));

    let app = Router::new()
        .route(
            "/redirect/{hop}",
            get({
                let counter = hop_counter.clone();
                move |Path(hop): Path<usize>| async move {
                    counter.fetch_add(1, Ordering::Relaxed);

                    if hop > 0 {
                        // Redirect to next hop
                        Redirect::temporary(&format!("/redirect/{}", hop - 1)).into_response()
                    } else {
                        // Final destination
                        "Final Destination".into_response()
                    }
                }
            }),
        )
        .route(
            "/stats",
            get({
                let counter = hop_counter;
                move || async move {
                    format!("Total hops processed: {}", counter.load(Ordering::Relaxed))
                }
            }),
        );

    let listener = TcpListener::bind("127.0.0.1:0")
        .await
        .expect("Failed to bind");
    let addr = listener.local_addr().expect("Failed to get address");
    let url = format!("http://{}", addr);

    tokio::spawn(async move {
        axum::serve(listener, app)
            .await
            .expect("Server failed to start");
    });

    // Give server time to start
    tokio::time::sleep(Duration::from_millis(100)).await;

    url
}

/// Demonstrates moderate redirect chain resource usage (10 hops, 10 concurrent).
///
/// **EXPECTED**: System should handle gracefully
/// **RESULT**: Shows resource multiplication with concurrent redirects
#[tokio::test]
#[ignore] // Run with: cargo test --test stress_redirect_resource -- --ignored --nocapture
async fn test_redirect_chain_resource_usage_moderate() {
    println!("=== Redirect Chain Resource Usage: Moderate ===\n");

    let hops_per_chain = 10; // MAX_REDIRECT_HOPS
    let concurrent_requests = 10;

    let server_url = start_redirect_server(hops_per_chain).await;
    println!("Mock server started at: {}", server_url);
    println!("Redirect chain depth: {} hops", hops_per_chain);
    println!("Concurrent requests: {}", concurrent_requests);
    println!();

    let client = reqwest::Client::builder()
        .redirect(reqwest::redirect::Policy::limited(hops_per_chain))
        .build()
        .expect("Failed to create client");

    let initial_memory = get_process_memory();
    let start = std::time::Instant::now();

    // Spawn concurrent requests
    let mut handles = vec![];
    for i in 0..concurrent_requests {
        let client_clone = client.clone();
        let url = format!("{}/redirect/{}", server_url, hops_per_chain);

        let handle = tokio::spawn(async move {
            let request_start = std::time::Instant::now();
            let result = client_clone.get(&url).send().await;
            let elapsed = request_start.elapsed();

            (i, result.is_ok(), elapsed)
        });
        handles.push(handle);
    }

    let results = futures::future::join_all(handles).await;
    let total_elapsed = start.elapsed();

    let final_memory = get_process_memory();
    let memory_growth = final_memory.saturating_sub(initial_memory);

    // Analyze results
    let mut success_count = 0;
    let mut total_time = Duration::ZERO;

    for result in results {
        if let Ok((_, success, elapsed)) = result {
            if success {
                success_count += 1;
            }
            total_time += elapsed;
        }
    }

    let avg_time = total_time.as_millis() as f64 / concurrent_requests as f64;
    let total_operations = concurrent_requests * (hops_per_chain + 1); // +1 for initial request

    println!("=== Results ===");
    println!("Total time: {:.2}s", total_elapsed.as_secs_f64());
    println!("Success: {} / {}", success_count, concurrent_requests);
    println!("Average request time: {:.0}ms", avg_time);
    println!();
    println!("Resource usage:");
    println!("  Total HTTP operations: {}", total_operations);
    println!(
        "  Memory growth: {:.2} MB",
        memory_growth as f64 / 1_048_576.0
    );
    println!(
        "  Memory per operation: {:.1} KB",
        memory_growth as f64 / total_operations as f64 / 1024.0
    );
    println!();

    println!("Analysis:");
    println!(
        "  {} concurrent requests × {} hops = {} operations",
        concurrent_requests,
        hops_per_chain + 1,
        total_operations
    );
    println!("  Memory spike from concurrent redirect processing");
}

/// Demonstrates high-concurrency redirect chain resource exhaustion.
///
/// **EXPECTED**: System maintains stability but shows resource pressure
/// **RESULT**: Significant memory spike from concurrent operations
#[tokio::test]
#[ignore]
async fn test_redirect_chain_high_concurrency() {
    println!("=== Redirect Chain: High Concurrency (100 requests) ===\n");

    let hops_per_chain = 10;
    let concurrent_requests = 100;

    let server_url = start_redirect_server(hops_per_chain).await;
    println!("Simulating adversarial scenario:");
    println!("  100 concurrent URL scans");
    println!("  Each URL redirects {} times", hops_per_chain);
    println!(
        "  Total operations: {} HTTP requests",
        concurrent_requests * (hops_per_chain + 1)
    );
    println!();

    let client = reqwest::Client::builder()
        .redirect(reqwest::redirect::Policy::limited(hops_per_chain))
        .build()
        .expect("Failed to create client");

    let initial_memory = get_process_memory();
    let start = std::time::Instant::now();

    let mut handles = vec![];
    for _ in 0..concurrent_requests {
        let client_clone = client.clone();
        let url = format!("{}/redirect/{}", server_url, hops_per_chain);

        let handle = tokio::spawn(async move {
            let result = client_clone.get(&url).send().await;
            (result.is_ok(),)
        });
        handles.push(handle);
    }

    let results = futures::future::join_all(handles).await;
    let total_elapsed = start.elapsed();

    let final_memory = get_process_memory();
    let memory_growth = final_memory.saturating_sub(initial_memory);

    let success_count = results
        .iter()
        .filter(|r| r.as_ref().map(|(ok,)| *ok).unwrap_or(false))
        .count();
    let total_operations = concurrent_requests * (hops_per_chain + 1);

    println!("=== Results ===");
    println!("Total time: {:.2}s", total_elapsed.as_secs_f64());
    println!(
        "Success rate: {} / {} ({:.1}%)",
        success_count,
        concurrent_requests,
        (success_count as f64 / concurrent_requests as f64) * 100.0
    );
    println!();
    println!("Resource analysis:");
    println!("  Total HTTP operations: {}", total_operations);
    println!("  Peak concurrent operations: ~{}", concurrent_requests);
    println!(
        "  Memory growth: {:.2} MB",
        memory_growth as f64 / 1_048_576.0
    );
    println!(
        "  Effective throughput: {:.1} ops/sec",
        total_operations as f64 / total_elapsed.as_secs_f64()
    );
    println!();

    if memory_growth > 50_000_000 {
        // >50MB
        println!("WARNING: Significant memory spike detected");
        println!("Concurrent redirect processing causes resource pressure");
    }

    println!("FINDING: Concurrent deep redirects cause multiplicative resource usage");
    println!("With adversarial sites, this can be used for resource exhaustion");
}

/// Demonstrates the subdomain enumeration attack vector.
///
/// Adversary uses unique subdomains for each redirect hop to bypass
/// DNS caching and force new DNS lookups.
#[tokio::test]
#[ignore]
async fn test_redirect_chain_subdomain_enumeration() {
    println!("=== Redirect Chain: Subdomain Enumeration Attack ===\n");

    println!("Attack pattern:");
    println!("  Adversary uses unique subdomain per redirect hop:");
    println!("    https://aaaaaaaaaa.evil.com →");
    println!("    https://bbbbbbbbbb.evil.com →");
    println!("    https://cccccccccc.evil.com →");
    println!("    ... (10 unique subdomains)");
    println!();

    println!("Impact per URL scan:");
    println!("  - 10 DNS lookups (each subdomain must be resolved)");
    println!("  - 10 TLS handshakes (each subdomain needs new connection)");
    println!("  - 10 HTTP requests/responses");
    println!("  - DNS cache ineffective (all unique domains)");
    println!();

    println!("With 100 concurrent scans:");
    println!("  - 1,000 DNS lookups");
    println!("  - 1,000 TLS handshakes");
    println!("  - 1,000 HTTP requests");
    println!("  - All happening concurrently");
    println!();

    println!("Resource requirements:");
    println!("  DNS queries:");
    println!("    1,000 queries × ~50ms = 50,000ms total (concurrent)");
    println!("    Peak: 100 concurrent DNS queries");
    println!();
    println!("  TLS handshakes:");
    println!("    1,000 handshakes × ~100ms = 100,000ms total");
    println!("    Peak: 100-200 concurrent handshakes");
    println!("    Memory: ~50KB per handshake = ~5-10MB");
    println!();
    println!("  HTTP connections:");
    println!("    1,000 connections × 1KB headers = 1MB");
    println!();

    println!("Total memory spike: ~15-20MB for 100 concurrent deep redirects");
    println!();
    println!("VULNERABILITY: No limit on unique domains in redirect chain");
    println!("Adversary can force expensive operations (DNS, TLS) per hop");
}

/// Documents the current protection and recommendations.
#[tokio::test]
#[ignore]
async fn test_redirect_chain_protection_analysis() {
    println!("=== Redirect Chain Protection Analysis ===\n");

    println!("Current protections:");
    println!("  ✓ MAX_REDIRECT_HOPS = 10 (prevents infinite redirects)");
    println!("  ✓ URL validation on each redirect hop");
    println!("  ✓ SSRF checks applied to redirect targets");
    println!();

    println!("Missing protections:");
    println!("  ✗ No limit on unique domains per redirect chain");
    println!("  ✗ No global redirect budget (across all URLs)");
    println!("  ✗ No DNS cache warming for redirect targets");
    println!("  ✗ No rate limiting on redirects from same origin");
    println!("  ✗ No detection of adversarial redirect patterns");
    println!();

    println!("Recommendations:");
    println!();
    println!("P0 (Critical - Resource exhaustion):");
    println!("  1. Add MAX_UNIQUE_REDIRECT_DOMAINS = 3");
    println!("     - Reject chains with >3 unique domains");
    println!("     - Most legitimate redirects use 1-2 domains");
    println!();
    println!("P1 (Defense in depth):");
    println!("  2. Implement global redirect budget");
    println!("     - MAX_CONCURRENT_REDIRECTS = 500");
    println!("     - Limit total redirects across all workers");
    println!("  3. Add redirect pattern detection");
    println!("     - Detect subdomain enumeration patterns");
    println!("     - Log suspicious redirect chains");
    println!();
    println!("P2 (Optimization):");
    println!("  4. Pre-resolve DNS for redirect targets");
    println!("     - Follow redirects without DNS re-resolution");
    println!("  5. Implement connection pooling per domain");
    println!("     - Reuse TLS connections for same domain");
    println!("  6. Add memory monitoring");
    println!("     - Alert when memory spike detected");
    println!();

    println!("Example attack mitigation:");
    println!("  Before: https://a.evil.com → https://b.evil.com → ... (10 hops)");
    println!("  With fix: Reject after 3 unique domains");
    println!("  Result: Attack blocked, resource usage contained");
}

/// Crude memory measurement helper (Unix-specific).
fn get_process_memory() -> usize {
    #[cfg(target_os = "linux")]
    {
        let pid = std::process::id();
        let status_path = format!("/proc/{}/status", pid);
        if let Ok(status) = std::fs::read_to_string(status_path) {
            for line in status.lines() {
                if line.starts_with("VmRSS:") {
                    let parts: Vec<&str> = line.split_whitespace().collect();
                    if parts.len() >= 2 {
                        if let Ok(kb) = parts[1].parse::<usize>() {
                            return kb * 1024;
                        }
                    }
                }
            }
        }
    }

    #[cfg(target_os = "macos")]
    {
        use std::process::Command;
        if let Ok(output) = Command::new("ps")
            .args(["-o", "rss=", "-p", &std::process::id().to_string()])
            .output()
        {
            if let Ok(s) = String::from_utf8(output.stdout) {
                if let Ok(kb) = s.trim().parse::<usize>() {
                    return kb * 1024;
                }
            }
        }
    }

    0
}
