//! Test harness for VPN integration tests.
//!
//! This module provides Docker-based test infrastructure for WireGuard tests.
//! In CI environments, it uses pre-configured service containers.
//! In local development, it spawns Docker containers on demand.
//!
//! Note: These utilities are available for tests that need Docker containers,
//! but may not be used in all test configurations.
#![allow(dead_code)]

use std::process::Command;
use std::time::Duration;
use tokio::time::sleep;

const WIREGUARD_IMAGE: &str = "linuxserver/wireguard:latest";
const WG_CONTAINER: &str = "donut-wg-test";

/// Check if running in CI environment
pub fn is_ci() -> bool {
  std::env::var("CI").is_ok() || std::env::var("GITHUB_ACTIONS").is_ok()
}

fn has_external_wireguard_service() -> bool {
  std::env::var("VPN_TEST_WG_HOST").is_ok()
}

/// Check if Docker can run the Linux fixtures this harness needs
pub fn is_docker_available() -> bool {
  crate::common::docker_supports_linux_containers()
}

/// Start a WireGuard test server and return client config
pub async fn start_wireguard_server() -> Result<WireGuardTestConfig, String> {
  if has_external_wireguard_service() {
    let host = std::env::var("VPN_TEST_WG_HOST").unwrap_or_else(|_| "localhost".into());
    let port = std::env::var("VPN_TEST_WG_PORT").unwrap_or_else(|_| "51820".into());

    return get_ci_wireguard_config(&host, &port);
  }

  if !is_docker_available() {
    return Err("Docker is not available for local testing".to_string());
  }

  // Stop any existing container
  let _ = Command::new("docker")
    .args(["rm", "-f", WG_CONTAINER])
    .output();

  // Start WireGuard container
  let output = Command::new("docker")
    .args([
      "run",
      "-d",
      "--name",
      WG_CONTAINER,
      "--cap-add=NET_ADMIN",
      "-p",
      "51820:51820/udp",
      "-e",
      "PEERS=1",
      "-e",
      "SERVERURL=127.0.0.1",
      "-e",
      "SERVERPORT=51820",
      "-e",
      "PEERDNS=auto",
      "-e",
      "INTERNAL_SUBNET=10.64.0.0",
      WIREGUARD_IMAGE,
    ])
    .output()
    .map_err(|e| format!("Failed to start WireGuard container: {e}"))?;

  if !output.status.success() {
    return Err(format!(
      "Docker run failed: {}",
      String::from_utf8_lossy(&output.stderr)
    ));
  }

  // Wait for container to generate configs and bring up the WireGuard interface.
  // A fixed sleep is flaky — on busy machines the interface takes longer. Instead
  // we poll `wg show` inside the container until it reports an active interface,
  // with a generous upper bound.
  let wg_ready_deadline = tokio::time::Instant::now() + Duration::from_secs(45);
  loop {
    sleep(Duration::from_secs(2)).await;

    // Check if peer config file has been generated
    let config_check = Command::new("docker")
      .args(["exec", WG_CONTAINER, "cat", "/config/peer1/peer1.conf"])
      .output();
    let config_exists = config_check
      .as_ref()
      .map(|o| o.status.success())
      .unwrap_or(false);

    // Check if WireGuard interface is actually up and listening
    let wg_check = Command::new("docker")
      .args(["exec", WG_CONTAINER, "wg", "show"])
      .output();
    let wg_up = wg_check
      .as_ref()
      .map(|o| o.status.success() && String::from_utf8_lossy(&o.stdout).contains("listening port"))
      .unwrap_or(false);

    if config_exists && wg_up {
      break;
    }

    if tokio::time::Instant::now() >= wg_ready_deadline {
      return Err("WireGuard container did not become ready within 45s".to_string());
    }
  }

  // Extract client config from container
  let config_output = Command::new("docker")
    .args(["exec", WG_CONTAINER, "cat", "/config/peer1/peer1.conf"])
    .output()
    .map_err(|e| format!("Failed to get client config: {e}"))?;

  if !config_output.status.success() {
    return Err(format!(
      "Failed to read config: {}",
      String::from_utf8_lossy(&config_output.stderr)
    ));
  }

  let config_str = String::from_utf8_lossy(&config_output.stdout).to_string();
  let mut config = parse_wireguard_test_config(&config_str)?;

  // Start a lightweight HTTP server inside the container on the WireGuard
  // interface so tests can verify traffic flows through the tunnel without
  // depending on internet access (Docker Desktop for Mac can't reliably NAT
  // WireGuard tunnel traffic to the internet). The linuxserver/wireguard
  // image doesn't have python3 or busybox httpd, but it has nc (netcat).
  let _ = Command::new("docker")
    .args([
      "exec",
      "-d",
      WG_CONTAINER,
      "sh",
      "-c",
      r#"while true; do printf "HTTP/1.1 200 OK\r\nContent-Length: 13\r\nConnection: close\r\n\r\nWG-TUNNEL-OK\n" | nc -l -p 8080 2>/dev/null; done"#,
    ])
    .output();
  // Give the nc loop a moment to start accepting
  sleep(Duration::from_millis(500)).await;

  // Extract the server's tunnel IP (first octet group from INTERNAL_SUBNET + .1)
  config.server_tunnel_ip = "10.64.0.1".to_string();

  Ok(config)
}

/// Stop all VPN test servers
pub async fn stop_vpn_servers() {
  let _ = Command::new("docker")
    .args(["rm", "-f", WG_CONTAINER])
    .output();
}

/// WireGuard test configuration
pub struct WireGuardTestConfig {
  pub private_key: String,
  pub address: String,
  pub dns: Option<String>,
  pub peer_public_key: String,
  pub peer_endpoint: String,
  pub allowed_ips: Vec<String>,
  pub preshared_key: Option<String>,
  /// IP of the WireGuard server on the tunnel interface (e.g. 10.64.0.1).
  /// Tests use this to reach an HTTP server inside the container without
  /// needing internet access from Docker.
  pub server_tunnel_ip: String,
}

/// Parse WireGuard test config from INI content
fn parse_wireguard_test_config(content: &str) -> Result<WireGuardTestConfig, String> {
  let mut private_key = String::new();
  let mut address = String::new();
  let mut dns = None;
  let mut peer_public_key = String::new();
  let mut peer_endpoint = String::new();
  let mut allowed_ips = vec!["0.0.0.0/0".to_string()];
  let mut preshared_key = None;
  let mut current_section = "";

  for line in content.lines() {
    let line = line.trim();
    if line.is_empty() || line.starts_with('#') {
      continue;
    }

    if line == "[Interface]" {
      current_section = "interface";
      continue;
    }
    if line == "[Peer]" {
      current_section = "peer";
      continue;
    }

    if let Some((key, value)) = line.split_once('=') {
      let key = key.trim();
      let value = value.trim();

      match (current_section, key) {
        ("interface", "PrivateKey") => private_key = value.to_string(),
        ("interface", "Address") => address = value.to_string(),
        ("interface", "DNS") => dns = Some(value.to_string()),
        ("peer", "PublicKey") => peer_public_key = value.to_string(),
        ("peer", "Endpoint") => peer_endpoint = value.to_string(),
        ("peer", "PresharedKey") => preshared_key = Some(value.to_string()),
        ("peer", "AllowedIPs") => {
          allowed_ips = value.split(',').map(|s| s.trim().to_string()).collect();
        }
        _ => {}
      }
    }
  }

  if private_key.is_empty() || address.is_empty() || peer_public_key.is_empty() {
    return Err("Invalid WireGuard config: missing required fields".to_string());
  }

  // Replace Endpoint with localhost for local testing
  if peer_endpoint.contains("10.") || peer_endpoint.contains("172.") {
    let port = peer_endpoint.split(':').next_back().unwrap_or("51820");
    peer_endpoint = format!("127.0.0.1:{port}");
  }

  Ok(WireGuardTestConfig {
    private_key,
    address,
    dns,
    peer_public_key,
    peer_endpoint,
    allowed_ips,
    preshared_key,
    server_tunnel_ip: String::new(), // filled in by caller
  })
}

/// Get WireGuard config from CI environment
fn get_ci_wireguard_config(host: &str, port: &str) -> Result<WireGuardTestConfig, String> {
  if std::env::var("VPN_TEST_WG_PRIVATE_KEY").is_err()
    || std::env::var("VPN_TEST_WG_PUBLIC_KEY").is_err()
  {
    return Err(
      "External WireGuard test service is configured, but VPN_TEST_WG_PRIVATE_KEY and VPN_TEST_WG_PUBLIC_KEY are missing"
        .to_string(),
    );
  }

  let private_key =
    std::env::var("VPN_TEST_WG_PRIVATE_KEY").unwrap_or_else(|_| "test-private-key".to_string());
  let public_key =
    std::env::var("VPN_TEST_WG_PUBLIC_KEY").unwrap_or_else(|_| "test-public-key".to_string());

  Ok(WireGuardTestConfig {
    private_key,
    address: std::env::var("VPN_TEST_WG_ADDRESS").unwrap_or_else(|_| "10.0.0.2/24".to_string()),
    dns: Some("1.1.1.1".to_string()),
    peer_public_key: public_key,
    peer_endpoint: format!("{host}:{port}"),
    allowed_ips: vec!["0.0.0.0/0".to_string()],
    preshared_key: std::env::var("VPN_TEST_WG_PRESHARED_KEY").ok(),
    server_tunnel_ip: std::env::var("VPN_TEST_WG_SERVER_IP")
      .unwrap_or_else(|_| "10.0.0.1".to_string()),
  })
}
