//! Stress test demonstrating WHOIS cache disk space exhaustion.
#![allow(
    clippy::cast_precision_loss,
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss,
    clippy::too_many_lines,
    clippy::manual_flatten
)]
//!
//! **VULNERABILITY FOUND**: No quota or LRU eviction for WHOIS cache.
//!
//! **ROOT CAUSE**:
//! - src/whois/cache.rs:36-51 saves WHOIS results to disk as JSON files
//! - One file per domain: {domain}_com.json (~5KB per file)
//! - Cache has 7-day TTL (`CACHE_TTL_SECS`)
//! - Expired files deleted on access (lazy cleanup)
//! - NO cache quota limit
//! - NO LRU (Least Recently Used) eviction
//! - NO max cache size enforcement
//!
//! **Attack Vector**:
//! - Adversary submits list of 10M unique domains
//! - Scanner performs WHOIS lookup for each domain
//! - Results cached to disk: 10M files × 5KB = 50GB
//! - Cache fills disk space over 7-day window
//! - Disk exhaustion causes database write failures
//! - Circuit breaker opens → system unavailability
//!
//! **Real-World Scenario**:
//! - Organization scans 100K unique domains daily
//! - 100K domains/day × 7 days = 700K cached files
//! - 700K files × 5KB = 3.5GB disk space
//! - Over 1 year (continuous operation): 36.5M files = 182GB
//! - Cache cleanup only happens on access (lazy)
//! - Domains never accessed again = permanent disk usage
//!
//! **Impact**: Disk exhaustion, system failure, degraded I/O performance
//!
//! **Recommended Fix**:
//! - Add `WHOIS_CACHE_MAX_SIZE_GB` = 10 to config
//! - Implement LRU eviction when quota exceeded
//! - Add background cleanup job (not just lazy cleanup)
//! - Monitor cache size and alert when approaching quota

use std::path::Path;
use tempfile::TempDir;

/// Simulates WHOIS cache growth with realistic data.
///
/// Creates cache files matching the structure used by `save_to_cache()`,
/// measuring disk usage as cache grows.
#[tokio::test]
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
#[ignore] // Run with: cargo test --test stress_whois_cache_explosion -- --ignored --nocapture
async fn test_whois_cache_growth_simulation() {
    let temp_dir = TempDir::new().expect("Failed to create temp directory");
    let cache_path = temp_dir.path();

    println!("=== WHOIS Cache Growth Simulation ===\n");
    println!("Simulating WHOIS cache growth for 10,000 domains");
    println!("(represents 10M domains at 1000:1 scale)\n");

    // Simulate cache entries similar to real WHOIS data
    let sample_whois_json = create_sample_whois_cache_entry("example.com");
    let bytes_per_entry = sample_whois_json.len();

    println!(
        "Sample cache entry size: {} bytes ({:.1} KB)",
        bytes_per_entry,
        bytes_per_entry as f64 / 1024.0
    );
    println!();

    let domain_count = 10_000;
    let mut total_size = 0u64;

    println!("Creating cache files...");

    for i in 0..domain_count {
        let domain = format!("domain-{:08}.com", i);
        let cache_file = cache_path.join(format!("{}.json", domain.replace('.', "_")));
        let content = create_sample_whois_cache_entry(&domain);

        std::fs::write(&cache_file, &content).expect("Failed to write cache file");
        total_size += content.len() as u64;

        if (i + 1) % 1000 == 0 {
            let current_mb = total_size as f64 / 1_048_576.0;
            println!("  {:5} domains cached | {:.2} MB", i + 1, current_mb);
        }
    }

    // Measure actual disk usage
    let disk_usage = calculate_directory_size(cache_path).expect("Failed to calculate size");

    println!("\n=== Results ===");
    println!("Domains cached: {}", domain_count);
    println!(
        "Total cache size: {:.2} MB",
        disk_usage as f64 / 1_048_576.0
    );
    println!(
        "Average per domain: {:.1} KB",
        disk_usage as f64 / f64::from(domain_count) / 1024.0
    );
    println!();

    // Extrapolate to production scale
    let kb_per_domain = disk_usage as f64 / f64::from(domain_count) / 1024.0;

    println!("=== Production Scale Extrapolation ===");
    let scenarios = vec![
        (100_000, "100K domains (1 day of scanning at 100K/day)"),
        (700_000, "700K domains (7-day TTL at 100K/day)"),
        (1_000_000, "1M domains (10 days of scanning)"),
        (10_000_000, "10M domains (100 days or enterprise scale)"),
        (36_500_000, "36.5M domains (1 year at 100K/day)"),
    ];

    for (scale, description) in scenarios {
        let projected_gb = (kb_per_domain * f64::from(scale)) / 1_048_576.0;
        println!("{}: {:.1} GB", description, projected_gb);
    }

    println!();
    println!("VULNERABILITY CONFIRMED: No cache quota or LRU eviction");
    println!("Cache can grow unbounded until disk exhaustion");
    println!("At enterprise scale (10M domains): ~50GB disk space");
    println!("At 1 year continuous operation: ~180GB disk space");
}

/// Demonstrates cache growth rate over simulated time periods.
#[tokio::test]
#[ignore]
async fn test_whois_cache_growth_rate_analysis() {
    let temp_dir = TempDir::new().expect("Failed to create temp directory");
    let _cache_path = temp_dir.path();

    println!("=== WHOIS Cache Growth Rate Analysis ===\n");
    println!("Simulating cache growth patterns over time\n");

    // Simulate different growth patterns
    let daily_scan_rates = vec![
        (10_000, "Small deployment (10K domains/day)"),
        (50_000, "Medium deployment (50K domains/day)"),
        (100_000, "Large deployment (100K domains/day)"),
        (500_000, "Enterprise scale (500K domains/day)"),
    ];

    let days_in_cache = 7; // 7-day TTL
    let sample_entry_kb = 5.0; // Approximate size per WHOIS cache entry

    println!("Assuming 7-day cache TTL (CACHE_TTL_SECS):");
    println!("Cache size = domains/day × 7 days × 5KB/domain\n");

    for (daily_rate, description) in daily_scan_rates {
        let cache_entries = daily_rate * days_in_cache;
        let cache_size_gb = (cache_entries as f64 * sample_entry_kb) / 1_048_576.0;

        println!("{}:", description);
        println!("  Daily scan rate: {} domains", format_number(daily_rate));
        println!(
            "  Cache entries (7-day window): {} files",
            format_number(cache_entries)
        );
        println!("  Cache size: {:.2} GB", cache_size_gb);

        // Show what happens without cleanup
        let yearly_size_gb = (daily_rate as f64 * 365.0 * sample_entry_kb) / 1_048_576.0;
        println!("  Without cleanup (1 year): {:.1} GB", yearly_size_gb);
        println!();
    }

    println!("FINDING: Cache can grow to hundreds of GB without quota");
    println!("Lazy cleanup (on-access) insufficient for infrequently accessed domains");
}

/// Demonstrates I/O performance degradation with large cache directories.
#[tokio::test]
#[ignore]
async fn test_whois_cache_io_performance() {
    let temp_dir = TempDir::new().expect("Failed to create temp directory");
    let cache_path = temp_dir.path();

    println!("=== WHOIS Cache I/O Performance Test ===\n");
    println!("Testing directory performance with large file counts\n");

    let file_counts = vec![1000, 5000, 10_000];
    let last_count = *file_counts.last().unwrap();

    for file_count in &file_counts {
        let file_count = *file_count;
        println!("Creating {} cache files...", file_count);

        let create_start = std::time::Instant::now();

        for i in 0..file_count {
            let domain = format!("test-{:08}.com", i);
            let cache_file = cache_path.join(format!("{}.json", domain.replace('.', "_")));
            let content = create_sample_whois_cache_entry(&domain);
            std::fs::write(&cache_file, content).expect("Failed to write file");
        }

        let create_elapsed = create_start.elapsed();

        // Measure directory listing performance
        let list_start = std::time::Instant::now();
        let _entries = std::fs::read_dir(cache_path)
            .expect("Failed to read dir")
            .count();
        let list_elapsed = list_start.elapsed();

        // Measure random access performance
        let access_start = std::time::Instant::now();
        let test_domain = "test-00005000.com";
        let test_file = cache_path.join(format!("{}.json", test_domain.replace('.', "_")));
        let _ = std::fs::read_to_string(&test_file);
        let access_elapsed = access_start.elapsed();

        println!("  File count: {}", file_count);
        println!("  Create time: {:.2}s", create_elapsed.as_secs_f64());
        println!("  Directory listing: {}ms", list_elapsed.as_millis());
        println!("  Random access: {}μs", access_elapsed.as_micros());
        println!();

        // Clean up for next iteration (except last)
        if file_count != last_count {
            std::fs::remove_dir_all(cache_path).expect("Failed to clean up");
            std::fs::create_dir_all(cache_path).expect("Failed to recreate dir");
        }
    }

    println!("FINDING: Directory operations slow down with large file counts");
    println!("At 100K+ files, directory listing becomes expensive");
    println!("Consider subdirectory sharding: cache/ab/cd/domain.json");
}

/// Demonstrates disk exhaustion scenario.
#[tokio::test]
#[ignore]
async fn test_whois_cache_disk_exhaustion_scenario() {
    println!("=== WHOIS Cache Disk Exhaustion Scenario ===\n");
    println!("Scenario: Attacker submits 10M unique domains\n");

    let kb_per_domain = 5.0;
    let domain_count = 10_000_000u64;

    let cache_size_gb = (domain_count as f64 * kb_per_domain) / 1_048_576.0;

    println!("Attack parameters:");
    println!(
        "  Domains submitted: {}",
        format_number(domain_count as usize)
    );
    println!("  WHOIS cache growth: {:.1} GB", cache_size_gb);
    println!();

    println!("Timeline:");
    println!("  Day 0: Attacker submits 10M domain list");
    println!("  Day 0-7: Scanner processes domains, caches WHOIS results");
    println!("  Day 7: Cache reaches {:.1} GB", cache_size_gb);
    println!(
        "  Day 7+: Cache remains at {:.1} GB (7-day TTL)",
        cache_size_gb
    );
    println!("  Day 14: Oldest entries start expiring (lazy cleanup)");
    println!();

    println!("Impact analysis:");
    println!("  Disk space consumed: {:.1} GB", cache_size_gb);
    println!(
        "  If disk has 100GB free: {:.1}% consumed",
        (cache_size_gb / 100.0) * 100.0
    );
    println!(
        "  If disk has 50GB free: {:.1}% consumed",
        (cache_size_gb / 50.0) * 100.0
    );
    println!();

    println!("Cascade failures:");
    println!("  1. Cache fills {:.1} GB of disk", cache_size_gb);
    println!("  2. Database unable to grow (shared disk)");
    println!("  3. Database writes fail");
    println!("  4. Circuit breaker opens (too many failures)");
    println!("  5. System becomes unavailable");
    println!();

    println!("CRITICAL VULNERABILITY: No cache quota enforcement");
    println!("Recommendation: MAX_WHOIS_CACHE_SIZE = 10GB with LRU eviction");
}

/// Creates a sample WHOIS cache entry matching the real format.
fn create_sample_whois_cache_entry(domain: &str) -> String {
    let now = std::time::SystemTime::now();
    let duration = now
        .duration_since(std::time::UNIX_EPOCH)
        .expect("Time went backwards");

    // Match the structure from WhoisCacheEntry
    format!(
        r#"{{
  "result": {{
    "creation_date": "2020-01-01T00:00:00Z",
    "expiration_date": "2025-01-01T00:00:00Z",
    "updated_date": "2024-01-01T00:00:00Z",
    "registrar": "Example Registrar, Inc.",
    "registrant_country": "US",
    "registrant_org": "Example Organization",
    "status": [
      "clientTransferProhibited",
      "clientUpdateProhibited"
    ],
    "nameservers": [
      "ns1.{domain}",
      "ns2.{domain}"
    ],
    "raw_text": "Domain Name: {domain}\nRegistrar: Example Registrar, Inc.\nRegistrant Organization: Example Organization\nRegistrant Country: US\nCreation Date: 2020-01-01T00:00:00Z\nExpiration Date: 2025-01-01T00:00:00Z\nUpdated Date: 2024-01-01T00:00:00Z\nStatus: clientTransferProhibited\nStatus: clientUpdateProhibited\nName Server: ns1.{domain}\nName Server: ns2.{domain}\n"
  }},
  "cached_at": {{
    "secs_since_epoch": {},
    "nanos_since_epoch": {}
  }},
  "domain": "{}"
}}"#,
        duration.as_secs(),
        duration.subsec_nanos(),
        domain,
        domain = domain
    )
}

/// Calculates total size of all files in a directory.
fn calculate_directory_size(path: &Path) -> std::io::Result<u64> {
    let mut total = 0u64;

    for entry in std::fs::read_dir(path)? {
        let entry = entry?;
        let metadata = entry.metadata()?;

        if metadata.is_file() {
            total += metadata.len();
        } else if metadata.is_dir() {
            total += calculate_directory_size(&entry.path())?;
        }
    }

    Ok(total)
}

/// Formats a number with thousand separators.
fn format_number(n: usize) -> String {
    let s = n.to_string();
    let chars: Vec<char> = s.chars().collect();
    let mut result = String::new();

    for (i, c) in chars.iter().rev().enumerate() {
        if i > 0 && i % 3 == 0 {
            result.push(',');
        }
        result.push(*c);
    }

    result.chars().rev().collect()
}
