use async_trait::async_trait;

use crate::discovery::{
    Discovery, DiscoveryError, DiscoveryResult, InstanceEndpoint, ServiceInstance,
};

/// DNS-backed discovery for a single service and host.
#[derive(Debug, Clone)]
pub struct DnsDiscovery {
    service: String,
    host: String,
    port: u16,
}

impl DnsDiscovery {
    /// Creates a DNS discovery adapter.
    pub fn new(service: impl Into<String>, host: impl Into<String>, port: u16) -> Self {
        Self {
            service: service.into(),
            host: host.into(),
            port,
        }
    }
}

#[async_trait]
impl Discovery for DnsDiscovery {
    async fn discover(&self, service: &str) -> DiscoveryResult<Vec<ServiceInstance>> {
        if service != self.service {
            return Err(DiscoveryError::NoInstances {
                service: service.to_string(),
            });
        }

        let addresses = tokio::net::lookup_host((self.host.as_str(), self.port))
            .await
            .map_err(|error| DiscoveryError::Resolve {
                host: self.host.clone(),
                message: error.to_string(),
            })?
            .collect::<Vec<_>>();

        if addresses.is_empty() {
            return Err(DiscoveryError::NoInstances {
                service: service.to_string(),
            });
        }

        Ok(addresses
            .into_iter()
            .enumerate()
            .map(|(index, address)| {
                ServiceInstance::new(
                    service,
                    format!("{}-{index}", self.host),
                    InstanceEndpoint::from_socket_addr(address),
                )
            })
            .collect())
    }
}

#[cfg(test)]
mod tests {
    use super::{Discovery, DnsDiscovery};

    #[tokio::test]
    async fn dns_discovery_reports_unknown_service_as_empty() {
        let discovery = DnsDiscovery::new("api", "localhost", 80);
        assert!(discovery.discover("other").await.is_err());
    }

    #[tokio::test]
    async fn dns_discovery_resolves_localhost() {
        let discovery = DnsDiscovery::new("api", "localhost", 80);
        let instances = discovery.discover("api").await.expect("localhost");
        assert!(!instances.is_empty());
    }
}
