//! DNS resolver implementation using tokio's DNS resolution.
//!
//! This module provides the [`TokioDnsResolver`] type which implements the
//! [`DnsResolver`] trait using tokio's asynchronous DNS resolution.

use secir::plugin::DnsResolver;
use std::net::IpAddr;

/// A DNS resolver implementation using tokio's built-in DNS resolution.
///
/// This resolver uses tokio's `lookup_host` function to resolve hostnames
/// to IP addresses. It supports both IPv4 and IPv6 addresses.
///
/// # Example
///
/// ```
/// use scanclient::dns_resolver::TokioDnsResolver;
/// use secir::plugin::DnsResolver;
///
/// # async fn example() -> secir::Result<()> {
/// let resolver = TokioDnsResolver::new();
/// let ips = resolver.resolve("example.com").await?;
/// println!("Resolved to: {:?}", ips);
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Default)]
pub struct TokioDnsResolver;

impl TokioDnsResolver {
    /// Creates a new `TokioDnsResolver`.
    #[must_use]
    pub fn new() -> Self {
        Self
    }
}

#[async_trait::async_trait]
impl DnsResolver for TokioDnsResolver {
    fn name(&self) -> &'static str {
        "tokio-dns"
    }

    async fn resolve(&self, hostname: &str) -> secir::Result<Vec<IpAddr>> {
        // Handle empty or invalid hostnames
        if hostname.is_empty() {
            return Ok(Vec::new());
        }

        // Try to parse as an IP address first (if it's already an IP, just return it)
        if let Ok(ip) = hostname.parse::<IpAddr>() {
            return Ok(vec![ip]);
        }

        // Use tokio's DNS resolution
        let addrs: Vec<std::net::SocketAddr> = tokio::net::lookup_host(format!("{hostname}:0"))
            .await
            .map_err(|error| secir::Error::Transport {
                url: hostname.to_string(),
                message: format!("failed to resolve hostname: {error}"),
                source: Some(Box::new(error)),
            })?
            .collect();

        // Extract unique IP addresses
        let mut ips: Vec<IpAddr> = addrs
            .iter()
            .map(|addr: &std::net::SocketAddr| addr.ip())
            .collect();
        ips.sort();
        ips.dedup();

        Ok(ips)
    }
}

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

    #[tokio::test]
    async fn resolver_returns_name() {
        let resolver = TokioDnsResolver::new();
        assert_eq!(resolver.name(), "tokio-dns");
    }

    #[tokio::test]
    async fn resolver_returns_ip_for_ip_input() {
        let resolver = TokioDnsResolver::new();

        // Should return the same IP if given an IP address
        let ips = resolver
            .resolve("127.0.0.1")
            .await
            .expect("operation should succeed");
        assert_eq!(ips.len(), 1);
        assert_eq!(ips[0], IpAddr::V4(std::net::Ipv4Addr::LOCALHOST));

        // IPv6
        let ips = resolver
            .resolve("::1")
            .await
            .expect("operation should succeed");
        assert_eq!(ips.len(), 1);
        assert_eq!(ips[0], IpAddr::V6(std::net::Ipv6Addr::LOCALHOST));
    }

    #[tokio::test]
    async fn resolver_handles_empty_hostname() {
        let resolver = TokioDnsResolver::new();
        let ips = resolver
            .resolve("")
            .await
            .expect("operation should succeed");
        assert_eq!(
            ips.len(),
            0,
            "empty hostname should resolve to no IP addresses"
        );
    }

    #[tokio::test]
    async fn resolver_resolves_localhost() {
        let resolver = TokioDnsResolver::new();

        // localhost should resolve to at least 127.0.0.1
        let ips = resolver
            .resolve("localhost")
            .await
            .expect("operation should succeed");
        assert!(
            !ips.is_empty(),
            "localhost should resolve to at least one IP"
        );

        // Should contain 127.0.0.1
        let localhost_v4 = IpAddr::V4(std::net::Ipv4Addr::LOCALHOST);
        assert!(
            ips.contains(&localhost_v4),
            "localhost should resolve to 127.0.0.1"
        );
    }

    #[tokio::test]
    async fn resolver_deduplicates_ips() {
        let resolver = TokioDnsResolver::new();

        // localhost might resolve to both IPv4 and IPv6, but we shouldn't have duplicates
        let ips = resolver
            .resolve("localhost")
            .await
            .expect("operation should succeed");

        // Check no duplicates by comparing length with a HashSet
        let unique: std::collections::HashSet<_> = ips.iter().collect();
        assert_eq!(ips.len(), unique.len(), "Should not have duplicate IPs");
    }
}
