#![allow(clippy::unwrap_used, clippy::expect_used)]

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::process::Output;
/// NAT Docker Integration Tests
///
/// Integration tests that use the Docker NAT testing environment
/// to validate NAT traversal under realistic network conditions
use std::time::Duration;
use tokio::process::Command;
use tokio::time::{sleep, timeout};

const DOCKER_COMMAND_TIMEOUT: Duration = Duration::from_secs(30);
const DOCKER_COMPOSE_PATH: &str = "docker/nat-emulation/docker-compose.yml";
const NAT_EMULATION_EXTERNAL_IFACE: &str = "eth1";
const NAT_EMULATION_CONTAINERS: &[&str] = &[
    "nat-fullcone",
    "nat-restricted",
    "nat-portrestricted",
    "nat-symmetric",
    "nat-cgnat",
    "nat-doublenat-outer",
    "nat-doublenat-inner",
    "nat-hairpin",
];
const ANT_QUIC_LISTEN_ADDR: &str = "0.0.0.0:9000";
const LISTENER_PID_FILE: &str = "/tmp/ant-quic-listener.pid";
const LISTENER_LOG_FILE: &str = "/tmp/ant-quic-listener.log";
const STOP_LISTENER_SCRIPT: &str = r#"if [ -f /tmp/ant-quic-listener.pid ]; then
    pid=$(cat /tmp/ant-quic-listener.pid)
    kill "$pid" 2>/dev/null || true
    for _ in 1 2 3 4 5; do
        kill -0 "$pid" 2>/dev/null || {
            rm -f /tmp/ant-quic-listener.pid
            exit 0
        }
        sleep 1
    done
    kill -s KILL "$pid" 2>/dev/null || true
    kill -0 "$pid" 2>/dev/null && exit 1
fi
rm -f /tmp/ant-quic-listener.pid
exit 0"#;
const VERIFY_LISTENER_SCRIPT: &str = r#"test -f /tmp/ant-quic-listener.pid && pid=$(cat /tmp/ant-quic-listener.pid) && kill -0 "$pid" 2>/dev/null"#;

async fn command_output_with_timeout(
    mut command: Command,
    description: &str,
    duration: Duration,
) -> Result<Output> {
    command.kill_on_drop(true);

    match timeout(duration, command.output()).await {
        Ok(output) => output.with_context(|| format!("Failed to execute {description}")),
        Err(_) => anyhow::bail!("{description} timed out after {duration:?}"),
    }
}

fn ensure_command_success(output: &Output, description: &str) -> Result<()> {
    if !output.status.success() {
        anyhow::bail!(
            "{description} failed with status {}. stdout: {}; stderr: {}",
            output.status,
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );
    }

    Ok(())
}

/// Docker-based NAT test configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DockerNatTest {
    pub name: String,
    pub description: String,
    pub client1_nat: String,
    pub client2_nat: String,
    pub network_profile: String,
    pub expected_result: ExpectedResult,
    pub timeout_seconds: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ExpectedResult {
    Success,
    FailWithRelay,
    FailCompletely,
}

/// Test execution result
#[derive(Debug)]
pub struct TestExecutionResult {
    pub test_name: String,
    pub success: bool,
    pub connection_time_ms: Option<u64>,
    pub relay_used: bool,
    pub error_message: Option<String>,
    pub logs: Vec<String>,
}

/// Docker NAT test orchestrator
pub struct DockerNatTestRunner {
    docker_compose_path: String,
    test_results: Vec<TestExecutionResult>,
    #[allow(dead_code)]
    container_logs: HashMap<String, Vec<String>>,
}

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

impl DockerNatTestRunner {
    pub fn new() -> Self {
        Self {
            docker_compose_path: DOCKER_COMPOSE_PATH.to_string(),
            test_results: Vec::new(),
            container_logs: HashMap::new(),
        }
    }

    /// Run all Docker-based NAT tests
    pub async fn run_all_tests(&mut self) -> Result<()> {
        println!("Starting Docker NAT integration tests...");

        self.validate_assets()?;

        // Start Docker environment
        self.start_docker_environment().await?;

        // Wait for containers to be ready
        sleep(Duration::from_secs(5)).await;

        // Run test scenarios
        let test_scenarios = self.create_test_scenarios();

        for scenario in test_scenarios {
            println!("\n=== Running: {} ===", scenario.name);
            println!("Description: {}", scenario.description);

            let result = self.run_test_scenario(&scenario).await;
            self.test_results.push(result);
        }

        // Generate report
        self.generate_test_report();

        // Cleanup
        self.cleanup_docker_environment().await?;

        Ok(())
    }

    fn validate_assets(&self) -> Result<()> {
        if !std::path::Path::new(self.docker_compose_path.as_str()).is_file() {
            anyhow::bail!(
                "Docker NAT test compose file is missing: {}",
                self.docker_compose_path
            );
        }

        Ok(())
    }

    /// Create comprehensive test scenarios
    fn create_test_scenarios(&self) -> Vec<DockerNatTest> {
        vec![
            // Basic NAT type combinations
            DockerNatTest {
                name: "full_cone_connectivity".to_string(),
                description: "Test connectivity between two full cone NATs".to_string(),
                client1_nat: "nat1".to_string(), // Full Cone
                client2_nat: "nat1".to_string(),
                network_profile: "normal".to_string(),
                expected_result: ExpectedResult::Success,
                timeout_seconds: 30,
            },
            DockerNatTest {
                name: "symmetric_challenge".to_string(),
                description: "Test hardest case: symmetric to symmetric NAT".to_string(),
                client1_nat: "nat2".to_string(), // Symmetric
                client2_nat: "nat2".to_string(),
                network_profile: "normal".to_string(),
                expected_result: ExpectedResult::FailWithRelay,
                timeout_seconds: 60,
            },
            DockerNatTest {
                name: "port_restricted_mixed".to_string(),
                description: "Test port restricted NAT with full cone".to_string(),
                client1_nat: "nat3".to_string(), // Port Restricted
                client2_nat: "nat1".to_string(), // Full Cone
                network_profile: "normal".to_string(),
                expected_result: ExpectedResult::Success,
                timeout_seconds: 45,
            },
            DockerNatTest {
                name: "cgnat_challenge".to_string(),
                description: "Test CGNAT (carrier grade) connectivity".to_string(),
                client1_nat: "nat4".to_string(), // CGNAT
                client2_nat: "nat1".to_string(), // Full Cone
                network_profile: "normal".to_string(),
                expected_result: ExpectedResult::FailWithRelay,
                timeout_seconds: 60,
            },
            // Network condition tests
            DockerNatTest {
                name: "high_latency_nat".to_string(),
                description: "Test NAT traversal with high latency (satellite)".to_string(),
                client1_nat: "nat1".to_string(),
                client2_nat: "nat3".to_string(),
                network_profile: "satellite".to_string(),
                expected_result: ExpectedResult::Success,
                timeout_seconds: 120,
            },
            DockerNatTest {
                name: "lossy_network_nat".to_string(),
                description: "Test NAT traversal with 5% packet loss".to_string(),
                client1_nat: "nat2".to_string(),
                client2_nat: "nat1".to_string(),
                network_profile: "lossy_wifi".to_string(),
                expected_result: ExpectedResult::Success,
                timeout_seconds: 90,
            },
            DockerNatTest {
                name: "congested_network_nat".to_string(),
                description: "Test NAT traversal on congested network".to_string(),
                client1_nat: "nat3".to_string(),
                client2_nat: "nat3".to_string(),
                network_profile: "congested".to_string(),
                expected_result: ExpectedResult::Success,
                timeout_seconds: 120,
            },
            // Mobile network scenarios
            DockerNatTest {
                name: "mobile_3g_nat".to_string(),
                description: "Test NAT traversal on 3G mobile network".to_string(),
                client1_nat: "nat2".to_string(),
                client2_nat: "nat1".to_string(),
                network_profile: "3g".to_string(),
                expected_result: ExpectedResult::Success,
                timeout_seconds: 90,
            },
            DockerNatTest {
                name: "mobile_4g_nat".to_string(),
                description: "Test NAT traversal on 4G LTE network".to_string(),
                client1_nat: "nat3".to_string(),
                client2_nat: "nat1".to_string(),
                network_profile: "4g".to_string(),
                expected_result: ExpectedResult::Success,
                timeout_seconds: 60,
            },
        ]
    }

    /// Run a single test scenario
    async fn run_test_scenario(&mut self, scenario: &DockerNatTest) -> TestExecutionResult {
        let start_time = std::time::Instant::now();

        // Apply network profile
        if let Err(e) = self.apply_network_profile(&scenario.network_profile).await {
            return TestExecutionResult {
                test_name: scenario.name.clone(),
                success: false,
                connection_time_ms: None,
                relay_used: false,
                error_message: Some(format!("Failed to apply network profile: {e}")),
                logs: vec![],
            };
        }

        // Get container names
        let client1_container = format!(
            "ant-quic-client{}",
            scenario.client1_nat.chars().last().unwrap_or('1')
        );
        let client2_container = format!(
            "ant-quic-client{}",
            scenario.client2_nat.chars().last().unwrap_or('2')
        );

        let test_outcome = timeout(
            Duration::from_secs(scenario.timeout_seconds),
            self.execute_nat_test(
                &client1_container,
                &client2_container,
                Duration::from_secs(scenario.timeout_seconds),
            ),
        )
        .await;

        let cleanup_error = self
            .stop_listener(&client2_container, DOCKER_COMMAND_TIMEOUT)
            .await
            .err()
            .map(|e| e.to_string());

        let logs = self
            .collect_container_logs(&[&client1_container, &client2_container])
            .await;

        match test_outcome {
            Ok(Ok((success, relay_used))) => {
                let elapsed = start_time.elapsed();

                TestExecutionResult {
                    test_name: scenario.name.clone(),
                    success: success && cleanup_error.is_none(),
                    connection_time_ms: Some(elapsed.as_millis() as u64),
                    relay_used,
                    error_message: cleanup_error
                        .map(|error| format!("Listener cleanup failed: {error}")),
                    logs,
                }
            }
            Ok(Err(e)) => {
                let mut error_message = e.to_string();
                if let Some(error) = cleanup_error {
                    error_message.push_str(&format!("; listener cleanup failed: {error}"));
                }

                TestExecutionResult {
                    test_name: scenario.name.clone(),
                    success: false,
                    connection_time_ms: None,
                    relay_used: false,
                    error_message: Some(error_message),
                    logs,
                }
            }
            Err(_) => {
                let mut error_message = "Test timeout".to_string();
                if let Some(error) = cleanup_error {
                    error_message.push_str(&format!("; listener cleanup failed: {error}"));
                }

                TestExecutionResult {
                    test_name: scenario.name.clone(),
                    success: false,
                    connection_time_ms: None,
                    relay_used: false,
                    error_message: Some(error_message),
                    logs,
                }
            }
        }
    }

    /// Execute NAT traversal test between two containers
    async fn execute_nat_test(
        &self,
        client1: &str,
        client2: &str,
        command_timeout: Duration,
    ) -> Result<(bool, bool)> {
        self.stop_listener(client2, command_timeout).await?;

        // Start ant-quic in listening mode on client2
        let start_listener_script = format!(
            "echo $$ >{LISTENER_PID_FILE}; exec ant-quic --listen {ANT_QUIC_LISTEN_ADDR} >{LISTENER_LOG_FILE} 2>&1"
        );
        let mut listen_cmd = Command::new("docker");
        listen_cmd.args(["exec", "-d", client2, "sh", "-c", &start_listener_script]);

        let output =
            command_output_with_timeout(listen_cmd, "start listener", command_timeout).await?;
        ensure_command_success(&output, "start listener")?;

        // Give listener time to start
        sleep(Duration::from_secs(2)).await;
        self.verify_listener_running(client2, command_timeout)
            .await?;

        // Get client2's peer ID (would be from actual implementation)
        let peer_id = "test_peer_id"; // Placeholder

        // Connect from client1 to client2
        let connect_cmd = format!("ant-quic --connect {peer_id} --bootstrap bootstrap:9000");
        let mut command = Command::new("docker");
        command.args([
            "exec",
            client1,
            "ant-quic",
            "--connect",
            peer_id,
            "--bootstrap",
            "bootstrap:9000",
        ]);

        let output = command_output_with_timeout(command, &connect_cmd, command_timeout).await?;

        let stdout = String::from_utf8_lossy(&output.stdout);
        let stderr = String::from_utf8_lossy(&output.stderr);

        // Check if connection succeeded
        let success = output.status.success()
            && (stdout.contains("Connection established")
                || stdout.contains("Connected successfully"));

        // Check if relay was used
        let relay_used =
            stdout.contains("Using relay") || stdout.contains("Relay connection established");

        if !success {
            println!("Connection failed. Stdout: {stdout}");
            println!("Stderr: {stderr}");
        }

        Ok((success, relay_used))
    }

    async fn stop_listener(&self, container: &str, command_timeout: Duration) -> Result<()> {
        let mut command = Command::new("docker");
        command.args(["exec", container, "sh", "-c", STOP_LISTENER_SCRIPT]);

        let output = command_output_with_timeout(command, "stop listener", command_timeout).await?;
        ensure_command_success(&output, "stop listener")
    }

    async fn verify_listener_running(
        &self,
        container: &str,
        command_timeout: Duration,
    ) -> Result<()> {
        let mut command = Command::new("docker");
        command.args(["exec", container, "sh", "-c", VERIFY_LISTENER_SCRIPT]);

        let output =
            command_output_with_timeout(command, "verify listener startup", command_timeout)
                .await?;
        ensure_command_success(&output, "verify listener startup")
    }

    /// Apply network profile to containers
    async fn apply_network_profile(&self, profile: &str) -> Result<()> {
        let profile_args = network_profile_tc_args(profile)?;
        let mut applied = 0;

        for &container in NAT_EMULATION_CONTAINERS {
            let inspect_cmd = format!("docker inspect {container}");
            let mut inspect = Command::new("docker");
            inspect.args(["inspect", "-f", "{{.State.Running}}", container]);

            let inspect_output =
                command_output_with_timeout(inspect, &inspect_cmd, DOCKER_COMMAND_TIMEOUT).await?;
            if !inspect_output.status.success()
                || String::from_utf8_lossy(&inspect_output.stdout).trim() != "true"
            {
                continue;
            }

            let clear_cmd = format!(
                "docker exec {container} tc qdisc del dev {NAT_EMULATION_EXTERNAL_IFACE} root"
            );
            let mut clear = Command::new("docker");
            clear.args([
                "exec",
                container,
                "tc",
                "qdisc",
                "del",
                "dev",
                NAT_EMULATION_EXTERNAL_IFACE,
                "root",
            ]);
            let _ = command_output_with_timeout(clear, &clear_cmd, DOCKER_COMMAND_TIMEOUT).await;

            if !profile_args.is_empty() {
                let mut args = vec![
                    "exec",
                    container,
                    "tc",
                    "qdisc",
                    "add",
                    "dev",
                    NAT_EMULATION_EXTERNAL_IFACE,
                    "root",
                    "netem",
                ];
                args.extend(profile_args);

                let apply_cmd = format!(
                    "docker exec {container} tc qdisc add dev {NAT_EMULATION_EXTERNAL_IFACE} root netem {}",
                    profile_args.join(" ")
                );
                let mut apply = Command::new("docker");
                apply.args(args);

                let output = command_output_with_timeout(apply, &apply_cmd, DOCKER_COMMAND_TIMEOUT)
                    .await
                    .context("Failed to apply network profile")?;

                if !output.status.success() {
                    anyhow::bail!(
                        "Failed to apply network profile {profile} to {container}: {}",
                        String::from_utf8_lossy(&output.stderr)
                    );
                }
            }

            applied += 1;
        }

        if applied == 0 {
            anyhow::bail!("No running NAT emulation containers found for profile: {profile}");
        }

        Ok(())
    }

    /// Collect logs from containers
    async fn collect_container_logs(&self, containers: &[&str]) -> Vec<String> {
        let mut logs = Vec::new();

        for container in containers {
            let cmd = format!("docker logs {container} --tail 100");
            let mut command = Command::new("docker");
            command.args(["logs", container, "--tail", "100"]);

            if let Ok(output) =
                command_output_with_timeout(command, &cmd, DOCKER_COMMAND_TIMEOUT).await
            {
                let container_logs = String::from_utf8_lossy(&output.stdout);
                logs.push(format!("=== {container} logs ===\n{container_logs}"));
            }
        }

        logs
    }

    /// Start Docker test environment
    async fn start_docker_environment(&self) -> Result<()> {
        println!("Starting Docker NAT test environment...");

        let mut command = Command::new("docker-compose");
        command
            .arg("-f")
            .arg(&self.docker_compose_path)
            .arg("up")
            .arg("-d");
        let output =
            command_output_with_timeout(command, "docker-compose up", DOCKER_COMMAND_TIMEOUT)
                .await
                .context("Failed to start Docker environment")?;

        if !output.status.success() {
            anyhow::bail!(
                "Failed to start Docker environment: {}",
                String::from_utf8_lossy(&output.stderr)
            );
        }

        Ok(())
    }

    /// Cleanup Docker environment
    async fn cleanup_docker_environment(&self) -> Result<()> {
        println!("Cleaning up Docker environment...");

        let mut command = Command::new("docker-compose");
        command.arg("-f").arg(&self.docker_compose_path).arg("down");
        let output =
            command_output_with_timeout(command, "docker-compose down", DOCKER_COMMAND_TIMEOUT)
                .await
                .context("Failed to stop Docker environment")?;

        if !output.status.success() {
            eprintln!(
                "Warning: Failed to cleanup Docker environment: {}",
                String::from_utf8_lossy(&output.stderr)
            );
        }

        Ok(())
    }

    /// Generate test report
    fn generate_test_report(&self) {
        println!("\n\n=== NAT Docker Integration Test Report ===\n");

        let total = self.test_results.len();
        let passed = self.test_results.iter().filter(|r| r.success).count();
        let failed = total - passed;

        println!("Total Tests: {total}");
        println!(
            "Passed: {} ({:.1}%)",
            passed,
            (passed as f64 / total as f64) * 100.0
        );
        println!("Failed: {failed}");
        println!();

        println!("Detailed Results:");
        println!("{:-<80}", "");
        println!(
            "{:<30} {:<15} {:<15} {:<20}",
            "Test Name", "Result", "Time (ms)", "Relay Used"
        );
        println!("{:-<80}", "");

        for result in &self.test_results {
            let status = if result.success {
                "✓ PASS"
            } else {
                "✗ FAIL"
            };
            let time = result
                .connection_time_ms
                .map(|t| t.to_string())
                .unwrap_or_else(|| "N/A".to_string());
            let relay = if result.relay_used { "Yes" } else { "No" };

            println!(
                "{:<30} {:<15} {:<15} {:<20}",
                result.test_name, status, time, relay
            );

            if let Some(ref error) = result.error_message {
                println!("  Error: {error}");
            }
        }

        println!("{:-<80}", "");

        // Summary by NAT type
        println!("\nNAT Type Success Rates:");
        let mut nat_stats: HashMap<String, (usize, usize)> = HashMap::new();

        for result in &self.test_results {
            let nat_types = result.test_name.split('_').collect::<Vec<_>>();
            if nat_types.len() >= 2 {
                let entry = nat_stats.entry(nat_types[0].to_string()).or_insert((0, 0));
                entry.1 += 1;
                if result.success {
                    entry.0 += 1;
                }
            }
        }

        for (nat_type, (passed, total)) in nat_stats {
            let rate = (passed as f64 / total as f64) * 100.0;
            println!("  {nat_type}: {passed}/{total} ({rate:.1}%)");
        }
    }
}

fn network_profile_tc_args(profile: &str) -> Result<&'static [&'static str]> {
    match profile {
        "normal" => Ok(&[]),
        "satellite" => Ok(&["delay", "600ms", "50ms", "loss", "0.1%"]),
        "lossy_wifi" => Ok(&["delay", "50ms", "20ms", "loss", "5%"]),
        "congested" => Ok(&["delay", "120ms", "30ms", "loss", "1%", "rate", "1mbit"]),
        "3g" => Ok(&["delay", "150ms", "40ms", "loss", "1%", "rate", "1.5mbit"]),
        "4g" => Ok(&["delay", "60ms", "15ms", "loss", "0.2%", "rate", "10mbit"]),
        _ => anyhow::bail!("Unknown network profile: {profile}"),
    }
}

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

    #[tokio::test]
    #[ignore] // Requires Docker environment
    async fn test_docker_nat_integration() {
        let mut runner = DockerNatTestRunner::new();
        runner
            .run_all_tests()
            .await
            .expect("Docker NAT tests failed");
    }

    #[test]
    fn test_scenario_creation() {
        let runner = DockerNatTestRunner::new();
        let scenarios = runner.create_test_scenarios();

        assert!(!scenarios.is_empty());
        assert!(scenarios.iter().any(|s| s.name.contains("symmetric")));
        assert!(scenarios.iter().any(|s| s.name.contains("cgnat")));
        assert!(scenarios.iter().any(|s| s.network_profile != "normal"));
    }

    #[test]
    fn configured_docker_nat_assets_exist() {
        let runner = DockerNatTestRunner::new();

        assert!(
            std::path::Path::new(runner.docker_compose_path.as_str()).is_file(),
            "missing Docker compose file: {}",
            runner.docker_compose_path
        );
        runner
            .validate_assets()
            .expect("configured Docker NAT assets should exist");
    }

    #[test]
    fn scenario_network_profiles_are_supported() {
        let runner = DockerNatTestRunner::new();

        for scenario in runner.create_test_scenarios() {
            network_profile_tc_args(&scenario.network_profile)
                .expect("scenario uses a supported network profile");
        }

        assert!(network_profile_tc_args("unknown").is_err());
    }

    #[cfg(unix)]
    #[test]
    fn command_status_failure_is_reported() {
        use std::os::unix::process::ExitStatusExt;

        let output = Output {
            status: std::process::ExitStatus::from_raw(1 << 8),
            stdout: b"listener stdout".to_vec(),
            stderr: b"listener stderr".to_vec(),
        };

        let error = ensure_command_success(&output, "start listener")
            .expect_err("failed status should be rejected")
            .to_string();

        assert!(error.contains("start listener failed"));
        assert!(error.contains("listener stdout"));
        assert!(error.contains("listener stderr"));
    }

    #[tokio::test]
    async fn command_output_timeout_returns_without_blocking_runtime() {
        if std::env::var_os("ANT_QUIC_NAT_DOCKER_SLEEP_CHILD").is_some() {
            std::thread::sleep(Duration::from_secs(5));
            return;
        }

        let mut command = Command::new(std::env::current_exe().expect("test binary path"));
        command
            .arg("tests::command_output_timeout_returns_without_blocking_runtime")
            .arg("--exact")
            .env("ANT_QUIC_NAT_DOCKER_SLEEP_CHILD", "1");

        let started = std::time::Instant::now();
        let result =
            command_output_with_timeout(command, "test sleep child", Duration::from_millis(50))
                .await;

        assert!(result.is_err());
        assert!(started.elapsed() < Duration::from_secs(1));
        assert!(
            result
                .expect_err("sleep child should time out")
                .to_string()
                .contains("timed out")
        );
    }
}
