// Copyright 2015-2019 Benjamin Fry <benjaminfry@me.com>
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// https://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.

//! Structs for creating and using a AsyncResolver
use std::fmt;
use std::net::IpAddr;
use std::sync::Arc;

use proto::error::ProtoResult;
use proto::op::Query;
use proto::rr::domain::usage::ONION;
use proto::rr::domain::TryParseIp;
use proto::rr::{IntoName, Name, Record, RecordType};
use proto::xfer::{DnsRequestOptions, RetryDnsHandle};
use tracing::{debug, trace};

use crate::caching_client::CachingClient;
use crate::config::{ResolverConfig, ResolverOpts};
use crate::dns_lru::{self, DnsLru};
use crate::error::*;
use crate::lookup::{self, Lookup, LookupEither, LookupFuture};
use crate::lookup_ip::{LookupIp, LookupIpFuture};
#[cfg(feature = "tokio-runtime")]
use crate::name_server::TokioConnectionProvider;
use crate::name_server::{ConnectionProvider, NameServerPool};

use crate::Hosts;

/// An asynchronous resolver for DNS generic over async Runtimes.
///
/// Creating a `AsyncResolver` returns a new handle and a future that should
/// be spawned on an executor to drive the background work. The lookup methods
/// on `AsyncResolver` request lookups from the background task.
///
/// The futures returned by a `AsyncResolver` and the corresponding background
/// task need not be spawned on the same executor, or be in the same thread.
///  Additionally, one background task may have any number of handles; calling
/// `clone()` on a handle will create a new handle linked to the same
/// background task.
///
/// *NOTE* If lookup futures returned by a `AsyncResolver` and the background
/// future are spawned on two separate `CurrentThread` executors, one thread
/// cannot run both executors simultaneously, so the `run` or `block_on`
/// functions will cause the thread to deadlock. If both the background work
/// and the lookup futures are intended to be run on the same thread, they
/// should be spawned on the same executor.
///
/// The background task manages the name server pool and other state used
/// to drive lookups. When this future is spawned on an executor, it will
/// first construct and configure the necessary client state, before checking
/// for any incoming lookup requests, handling them, and yielding. It will
/// continue to do so as long as there are still any [`AsyncResolver`] handle
/// linked to it. When all of its [`AsyncResolver`]s have been dropped, the
/// background future will finish.
#[derive(Clone)]
pub struct AsyncResolver<P: ConnectionProvider> {
    config: ResolverConfig,
    options: ResolverOpts,
    client_cache: CachingClient<LookupEither<P>, ResolveError>,
    hosts: Option<Arc<Hosts>>,
}

/// An AsyncResolver used with Tokio
#[cfg(feature = "tokio-runtime")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio-runtime")))]
pub type TokioAsyncResolver = AsyncResolver<TokioConnectionProvider>;

macro_rules! lookup_fn {
    ($p:ident, $l:ty, $r:path) => {
        /// Performs a lookup for the associated type.
        ///
        /// *hint* queries that end with a '.' are fully qualified names and are cheaper lookups
        ///
        /// # Arguments
        ///
        /// * `query` - a string which parses to a domain name, failure to parse will return an error
        pub async fn $p<N: IntoName>(&self, query: N) -> Result<$l, ResolveError> {
            let name = match query.into_name() {
                Ok(name) => name,
                Err(err) => {
                    return Err(err.into());
                }
            };

            self.inner_lookup(name, $r, self.request_options()).await
        }
    };
    ($p:ident, $l:ty, $r:path, $t:ty) => {
        /// Performs a lookup for the associated type.
        ///
        /// # Arguments
        ///
        /// * `query` - a type which can be converted to `Name` via `From`.
        pub async fn $p(&self, query: $t) -> Result<$l, ResolveError> {
            let name = Name::from(query);
            self.inner_lookup(name, $r, self.request_options()).await
        }
    };
}

#[cfg(feature = "tokio-runtime")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio-runtime")))]
impl TokioAsyncResolver {
    /// Construct a new Tokio based `AsyncResolver` with the provided configuration.
    ///
    /// # Arguments
    ///
    /// * `config` - configuration, name_servers, etc. for the Resolver
    /// * `options` - basic lookup options for the resolver
    ///
    /// # Returns
    ///
    /// A tuple containing the new `AsyncResolver` and a future that drives the
    /// background task that runs resolutions for the `AsyncResolver`. See the
    /// documentation for `AsyncResolver` for more information on how to use
    /// the background future.
    pub fn tokio(config: ResolverConfig, options: ResolverOpts) -> Self {
        Self::new(config, options, TokioConnectionProvider::default())
    }

    /// Constructs a new Tokio based Resolver with the system configuration.
    ///
    /// This will use `/etc/resolv.conf` on Unix OSes and the registry on Windows.
    #[cfg(any(unix, target_os = "windows"))]
    #[cfg(feature = "system-config")]
    #[cfg_attr(
        docsrs,
        doc(cfg(all(feature = "system-config", any(unix, target_os = "windows"))))
    )]
    pub fn tokio_from_system_conf() -> Result<Self, ResolveError> {
        Self::from_system_conf(TokioConnectionProvider::default())
    }
}

impl<R: ConnectionProvider> AsyncResolver<R> {
    /// Construct a new generic `AsyncResolver` with the provided configuration.
    ///
    /// see [TokioAsyncResolver::tokio(..)] instead.
    ///
    /// # Arguments
    ///
    /// * `config` - configuration, name_servers, etc. for the Resolver
    /// * `options` - basic lookup options for the resolver
    ///
    /// # Returns
    ///
    /// A tuple containing the new `AsyncResolver` and a future that drives the
    /// background task that runs resolutions for the `AsyncResolver`. See the
    /// documentation for `AsyncResolver` for more information on how to use
    /// the background future.
    pub fn new(config: ResolverConfig, options: ResolverOpts, provider: R) -> Self {
        Self::new_with_conn(config, options, provider)
    }

    /// Constructs a new Resolver with the system configuration.
    ///
    /// see [TokioAsyncResolver::tokio_from_system_conf(..)] instead.
    ///
    /// This will use `/etc/resolv.conf` on Unix OSes and the registry on Windows.
    #[cfg(any(unix, target_os = "windows"))]
    #[cfg(feature = "system-config")]
    #[cfg_attr(
        docsrs,
        doc(cfg(all(feature = "system-config", any(unix, target_os = "windows"))))
    )]
    pub fn from_system_conf(runtime: R) -> Result<Self, ResolveError> {
        Self::from_system_conf_with_provider(runtime)
    }

    /// Flushes/Removes all entries from the cache
    pub fn clear_cache(&self) {
        self.client_cache.clear_cache();
    }
}

impl<P: ConnectionProvider> AsyncResolver<P> {
    /// Construct a new `AsyncResolver` with the provided configuration.
    ///
    /// # Arguments
    ///
    /// * `config` - configuration, name_servers, etc. for the Resolver
    /// * `options` - basic lookup options for the resolver
    ///
    /// # Returns
    ///
    /// A tuple containing the new `AsyncResolver` and a future that drives the
    /// background task that runs resolutions for the `AsyncResolver`. See the
    /// documentation for `AsyncResolver` for more information on how to use
    /// the background future.
    pub fn new_with_conn(config: ResolverConfig, options: ResolverOpts, conn_provider: P) -> Self {
        let pool =
            NameServerPool::from_config_with_provider(&config, options.clone(), conn_provider);
        let either;
        let client = RetryDnsHandle::new(pool, options.attempts);
        if options.validate {
            #[cfg(feature = "dnssec")]
            {
                use proto::xfer::DnssecDnsHandle;
                either = LookupEither::Secure(DnssecDnsHandle::new(client));
            }

            #[cfg(not(feature = "dnssec"))]
            {
                // TODO: should this just be a panic, or a pinned error?
                tracing::warn!("validate option is only available with 'dnssec' feature");
                either = LookupEither::Retry(client);
            }
        } else {
            either = LookupEither::Retry(client);
        }

        let hosts = if options.use_hosts_file {
            Some(Arc::new(Hosts::new()))
        } else {
            None
        };

        trace!("handle passed back");
        let lru = DnsLru::new(options.cache_size, dns_lru::TtlConfig::from_opts(&options));
        Self {
            config,
            client_cache: CachingClient::with_cache(lru, either, options.preserve_intermediates),
            options,
            hosts,
        }
    }

    /// Constructs a new Resolver with the system configuration.
    ///
    /// This will use `/etc/resolv.conf` on Unix OSes and the registry on Windows.
    #[cfg(any(unix, target_os = "windows"))]
    #[cfg(feature = "system-config")]
    #[cfg_attr(
        docsrs,
        doc(cfg(all(feature = "system-config", any(unix, target_os = "windows"))))
    )]
    pub fn from_system_conf_with_provider(conn_provider: P) -> Result<Self, ResolveError> {
        let (config, options) = super::system_conf::read_system_conf()?;
        Ok(Self::new_with_conn(config, options, conn_provider))
    }

    /// Per request options based on the ResolverOpts
    pub(crate) fn request_options(&self) -> DnsRequestOptions {
        let mut request_opts = DnsRequestOptions::default();
        request_opts.recursion_desired = self.options.recursion_desired;
        request_opts.use_edns = self.options.edns0;

        request_opts
    }

    /// Generic lookup for any RecordType
    ///
    /// *WARNING* this interface may change in the future, see if one of the specializations would be better.
    ///
    /// # Arguments
    ///
    /// * `name` - name of the record to lookup, if name is not a valid domain name, an error will be returned
    /// * `record_type` - type of record to lookup, all RecordData responses will be filtered to this type
    ///
    /// # Returns
    ///
    //  A future for the returned Lookup RData
    pub async fn lookup<N: IntoName>(
        &self,
        name: N,
        record_type: RecordType,
    ) -> Result<Lookup, ResolveError> {
        let name = match name.into_name() {
            Ok(name) => name,
            Err(err) => return Err(err.into()),
        };

        self.inner_lookup(name, record_type, self.request_options())
            .await
    }

    fn push_name(name: Name, names: &mut Vec<Name>) {
        if !names.contains(&name) {
            names.push(name);
        }
    }

    fn build_names(&self, name: Name) -> Vec<Name> {
        // if it's fully qualified, we can short circuit the lookup logic
        if name.is_fqdn()
            || ONION.zone_of(&name)
                && name
                    .trim_to(2)
                    .iter()
                    .next()
                    .map(|name| name.len() == 56) // size of onion v3 address
                    .unwrap_or(false)
        {
            // if already fully qualified, or if onion address, don't assume it might be a
            // sub-domain
            vec![name]
        } else {
            // Otherwise we have to build the search list
            // Note: the vec is built in reverse order of precedence, for stack semantics
            let mut names =
                Vec::<Name>::with_capacity(1 /*FQDN*/ + 1 /*DOMAIN*/ + self.config.search().len());

            // if not meeting ndots, we always do the raw name in the final lookup, or it's a localhost...
            let raw_name_first: bool =
                name.num_labels() as usize > self.options.ndots || name.is_localhost();

            // if not meeting ndots, we always do the raw name in the final lookup
            if !raw_name_first {
                names.push(name.clone());
            }

            for search in self.config.search().iter().rev() {
                let name_search = name.clone().append_domain(search);

                match name_search {
                    Ok(name_search) => Self::push_name(name_search, &mut names),
                    Err(e) => debug!(
                        "Not adding {} to {} for search due to error: {}",
                        search, name, e
                    ),
                }
            }

            if let Some(domain) = self.config.domain() {
                let name_search = name.clone().append_domain(domain);

                match name_search {
                    Ok(name_search) => Self::push_name(name_search, &mut names),
                    Err(e) => debug!(
                        "Not adding {} to {} for search due to error: {}",
                        domain, name, e
                    ),
                }
            }

            // this is the direct name lookup
            if raw_name_first {
                // adding the name as though it's an FQDN for lookup
                names.push(name);
            }

            names
        }
    }

    pub(crate) async fn inner_lookup<L>(
        &self,
        name: Name,
        record_type: RecordType,
        options: DnsRequestOptions,
    ) -> Result<L, ResolveError>
    where
        L: From<Lookup> + Send + 'static,
    {
        let names = self.build_names(name);
        LookupFuture::lookup(names, record_type, options, self.client_cache.clone())
            .await
            .map(L::from)
    }

    /// Performs a dual-stack DNS lookup for the IP for the given hostname.
    ///
    /// See the configuration and options parameters for controlling the way in which A(Ipv4) and AAAA(Ipv6) lookups will be performed. For the least expensive query a fully-qualified-domain-name, FQDN, which ends in a final `.`, e.g. `www.example.com.`, will only issue one query. Anything else will always incur the cost of querying the `ResolverConfig::domain` and `ResolverConfig::search`.
    ///
    /// # Arguments
    /// * `host` - string hostname, if this is an invalid hostname, an error will be returned.
    pub async fn lookup_ip<N: IntoName + TryParseIp>(
        &self,
        host: N,
    ) -> Result<LookupIp, ResolveError> {
        let mut finally_ip_addr: Option<Record> = None;
        let maybe_ip = host.try_parse_ip();
        let maybe_name: ProtoResult<Name> = host.into_name();

        // if host is a ip address, return directly.
        if let Some(ip_addr) = maybe_ip {
            let name = maybe_name.clone().unwrap_or_default();
            let record = Record::from_rdata(name.clone(), dns_lru::MAX_TTL, ip_addr.clone());

            // if ndots are greater than 4, then we can't assume the name is an IpAddr
            //   this accepts IPv6 as well, b/c IPv6 can take the form: 2001:db8::198.51.100.35
            //   but `:` is not a valid DNS character, so technically this will fail parsing.
            //   TODO: should we always do search before returning this?
            if self.options.ndots > 4 {
                finally_ip_addr = Some(record);
            } else {
                let query = Query::query(name, ip_addr.record_type());
                let lookup = Lookup::new_with_max_ttl(query, Arc::from([record]));
                return Ok(lookup.into());
            }
        }

        let name = match (maybe_name, finally_ip_addr.as_ref()) {
            (Ok(name), _) => name,
            (Err(_), Some(ip_addr)) => {
                // it was a valid IP, return that...
                let query = Query::query(ip_addr.name().clone(), ip_addr.record_type());
                let lookup = Lookup::new_with_max_ttl(query, Arc::from([ip_addr.clone()]));
                return Ok(lookup.into());
            }
            (Err(err), None) => {
                return Err(err.into());
            }
        };

        let names = self.build_names(name);
        let hosts = self.hosts.as_ref().cloned();

        LookupIpFuture::lookup(
            names,
            self.options.ip_strategy,
            self.client_cache.clone(),
            self.request_options(),
            hosts,
            finally_ip_addr.and_then(Record::into_data),
        )
        .await
    }

    /// Customizes the static hosts used in this resolver.
    pub fn set_hosts(&mut self, hosts: Option<Hosts>) {
        self.hosts = hosts.map(Arc::new);
    }

    lookup_fn!(
        reverse_lookup,
        lookup::ReverseLookup,
        RecordType::PTR,
        IpAddr
    );
    lookup_fn!(ipv4_lookup, lookup::Ipv4Lookup, RecordType::A);
    lookup_fn!(ipv6_lookup, lookup::Ipv6Lookup, RecordType::AAAA);
    lookup_fn!(mx_lookup, lookup::MxLookup, RecordType::MX);
    lookup_fn!(ns_lookup, lookup::NsLookup, RecordType::NS);
    lookup_fn!(soa_lookup, lookup::SoaLookup, RecordType::SOA);
    lookup_fn!(srv_lookup, lookup::SrvLookup, RecordType::SRV);
    lookup_fn!(tlsa_lookup, lookup::TlsaLookup, RecordType::TLSA);
    lookup_fn!(txt_lookup, lookup::TxtLookup, RecordType::TXT);
}

impl<P: ConnectionProvider> fmt::Debug for AsyncResolver<P> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("AsyncResolver")
            .field("request_tx", &"...")
            .finish()
    }
}

/// Unit tests compatible with different runtime.
#[cfg(any(test, feature = "testing"))]
#[cfg_attr(docsrs, doc(cfg(feature = "testing")))]
#[allow(dead_code, unreachable_pub)]
pub mod testing {
    use std::{net::*, str::FromStr};

    use crate::config::{LookupIpStrategy, NameServerConfig, ResolverConfig, ResolverOpts};
    use crate::name_server::ConnectionProvider;
    use crate::AsyncResolver;
    use proto::{rr::Name, Executor};

    /// Test IP lookup from URLs.
    pub fn lookup_test<E: Executor, R: ConnectionProvider>(
        config: ResolverConfig,
        mut exec: E,
        handle: R,
    ) {
        let resolver = AsyncResolver::<R>::new(config, ResolverOpts::default(), handle);

        let response = exec
            .block_on(resolver.lookup_ip("www.example.com."))
            .expect("failed to run lookup");

        assert_ne!(response.iter().count(), 0);
    }

    /// Test IP lookup from IP literals.
    pub fn ip_lookup_test<E: Executor, R: ConnectionProvider>(mut exec: E, handle: R) {
        let resolver =
            AsyncResolver::<R>::new(ResolverConfig::default(), ResolverOpts::default(), handle);

        let response = exec
            .block_on(resolver.lookup_ip("10.1.0.2"))
            .expect("failed to run lookup");

        assert_eq!(
            Some(IpAddr::V4(Ipv4Addr::new(10, 1, 0, 2))),
            response.iter().next()
        );

        let response = exec
            .block_on(resolver.lookup_ip("2606:2800:21f:cb07:6820:80da:af6b:8b2c"))
            .expect("failed to run lookup");

        assert_eq!(
            Some(IpAddr::V6(Ipv6Addr::new(
                0x2606, 0x2800, 0x21f, 0xcb07, 0x6820, 0x80da, 0xaf6b, 0x8b2c,
            ))),
            response.iter().next()
        );
    }

    /// Test IP lookup from IP literals across threads.
    pub fn ip_lookup_across_threads_test<E: Executor + Send + 'static, R: ConnectionProvider>(
        handle: R,
    ) {
        // Test ensuring that running the background task on a separate
        // executor in a separate thread from the futures returned by the
        // AsyncResolver works correctly.
        use std::thread;
        let resolver =
            AsyncResolver::<R>::new(ResolverConfig::default(), ResolverOpts::default(), handle);

        let resolver_one = resolver.clone();
        let resolver_two = resolver;

        let test_fn = |resolver: AsyncResolver<R>| {
            let mut exec = E::new();

            let response = exec
                .block_on(resolver.lookup_ip("10.1.0.2"))
                .expect("failed to run lookup");

            assert_eq!(
                Some(IpAddr::V4(Ipv4Addr::new(10, 1, 0, 2))),
                response.iter().next()
            );

            let response = exec
                .block_on(resolver.lookup_ip("2606:2800:21f:cb07:6820:80da:af6b:8b2c"))
                .expect("failed to run lookup");

            assert_eq!(
                Some(IpAddr::V6(Ipv6Addr::new(
                    0x2606, 0x2800, 0x21f, 0xcb07, 0x6820, 0x80da, 0xaf6b, 0x8b2c,
                ))),
                response.iter().next()
            );
        };

        let thread_one = thread::spawn(move || {
            test_fn(resolver_one);
        });

        let thread_two = thread::spawn(move || {
            test_fn(resolver_two);
        });

        thread_one.join().expect("thread_one failed");
        thread_two.join().expect("thread_two failed");
    }

    /// Test IP lookup from URLs with DNSSEC validation.
    #[cfg(feature = "dnssec")]
    #[cfg_attr(docsrs, doc(cfg(feature = "dnssec")))]
    pub fn sec_lookup_test<E: Executor + Send + 'static, R: ConnectionProvider>(
        mut exec: E,
        handle: R,
    ) {
        //env_logger::try_init().ok();

        let resolver = AsyncResolver::new(
            ResolverConfig::default(),
            ResolverOpts {
                validate: true,
                try_tcp_on_error: true,
                ..ResolverOpts::default()
            },
            handle,
        );

        let response = exec
            .block_on(resolver.lookup_ip("www.internetsociety.org."))
            .expect("failed to run lookup");

        assert_ne!(response.iter().count(), 0);
    }

    /// Test IP lookup from domains that exist but unsigned with DNSSEC validation.
    #[allow(deprecated)]
    #[cfg(feature = "dnssec")]
    #[cfg_attr(docsrs, doc(cfg(feature = "dnssec")))]
    pub fn sec_lookup_fails_test<E: Executor + Send + 'static, R: ConnectionProvider>(
        mut exec: E,
        handle: R,
    ) {
        use crate::error::*;
        use proto::rr::RecordType;
        let resolver = AsyncResolver::new(
            ResolverConfig::default(),
            ResolverOpts {
                validate: true,
                ip_strategy: LookupIpStrategy::Ipv4Only,
                ..ResolverOpts::default()
            },
            handle,
        );

        // needs to be a domain that exists, but is not signed (eventually this will be)
        let response = exec.block_on(resolver.lookup_ip("hickory-dns.org."));

        assert!(response.is_err());
        let error = response.unwrap_err();

        use proto::error::{ProtoError, ProtoErrorKind};

        let error_str = format!("{error}");
        let name = Name::from_str("hickory-dns.org.").unwrap();
        let expected_str = format!(
            "{}",
            ResolveError::from(ProtoError::from(ProtoErrorKind::RrsigsNotPresent {
                name,
                record_type: RecordType::A
            }))
        );
        assert_eq!(error_str, expected_str);
        if let ResolveErrorKind::Proto(_) = *error.kind() {
        } else {
            panic!("wrong error")
        }
    }

    /// Test AsyncResolver created from system configuration with IP lookup.
    #[cfg(feature = "system-config")]
    #[cfg_attr(docsrs, doc(cfg(feature = "system-config")))]
    pub fn system_lookup_test<E: Executor + Send + 'static, R: ConnectionProvider>(
        mut exec: E,
        handle: R,
    ) {
        let resolver =
            AsyncResolver::<R>::from_system_conf(handle).expect("failed to create resolver");

        let response = exec
            .block_on(resolver.lookup_ip("www.example.com."))
            .expect("failed to run lookup");

        assert_ne!(response.iter().count(), 0);
    }

    /// Test AsyncResolver created from system configuration with host lookups.
    #[cfg(feature = "system-config")]
    #[cfg_attr(docsrs, doc(cfg(feature = "system-config")))]
    pub fn hosts_lookup_test<E: Executor + Send + 'static, R: ConnectionProvider>(
        mut exec: E,
        handle: R,
    ) {
        let resolver =
            AsyncResolver::<R>::from_system_conf(handle).expect("failed to create resolver");

        let response = exec
            .block_on(resolver.lookup_ip("a.com"))
            .expect("failed to run lookup");

        assert_eq!(response.iter().count(), 1);
        for address in response.iter() {
            if address.is_ipv4() {
                assert_eq!(address, IpAddr::V4(Ipv4Addr::new(10, 1, 0, 104)));
            } else {
                panic!("failed to run lookup");
            }
        }
    }

    /// Test fqdn.
    pub fn fqdn_test<E: Executor + Send + 'static, R: ConnectionProvider>(mut exec: E, handle: R) {
        let domain = Name::from_str("incorrect.example.com.").unwrap();
        let search = vec![
            Name::from_str("bad.example.com.").unwrap(),
            Name::from_str("wrong.example.com.").unwrap(),
        ];
        let name_servers: Vec<NameServerConfig> =
            ResolverConfig::default().name_servers().to_owned();

        let resolver = AsyncResolver::<R>::new(
            ResolverConfig::from_parts(Some(domain), search, name_servers),
            ResolverOpts {
                ip_strategy: LookupIpStrategy::Ipv4Only,
                ..ResolverOpts::default()
            },
            handle,
        );

        let response = exec
            .block_on(resolver.lookup_ip("www.example.com."))
            .expect("failed to run lookup");

        assert_ne!(response.iter().count(), 0);
    }

    /// Test ndots with non-fqdn.
    pub fn ndots_test<E: Executor + Send + 'static, R: ConnectionProvider>(mut exec: E, handle: R) {
        let domain = Name::from_str("incorrect.example.com.").unwrap();
        let search = vec![
            Name::from_str("bad.example.com.").unwrap(),
            Name::from_str("wrong.example.com.").unwrap(),
        ];
        let name_servers: Vec<NameServerConfig> =
            ResolverConfig::default().name_servers().to_owned();

        let resolver = AsyncResolver::<R>::new(
            ResolverConfig::from_parts(Some(domain), search, name_servers),
            ResolverOpts {
                // our name does have 2, the default should be fine, let's just narrow the test criteria a bit.
                ndots: 2,
                ip_strategy: LookupIpStrategy::Ipv4Only,
                ..ResolverOpts::default()
            },
            handle,
        );

        // notice this is not a FQDN, no trailing dot.
        let response = exec
            .block_on(resolver.lookup_ip("www.example.com"))
            .expect("failed to run lookup");

        assert_ne!(response.iter().count(), 0);
    }

    /// Test large ndots with non-fqdn.
    pub fn large_ndots_test<E: Executor + Send + 'static, R: ConnectionProvider>(
        mut exec: E,
        handle: R,
    ) {
        let domain = Name::from_str("incorrect.example.com.").unwrap();
        let search = vec![
            Name::from_str("bad.example.com.").unwrap(),
            Name::from_str("wrong.example.com.").unwrap(),
        ];
        let name_servers: Vec<NameServerConfig> =
            ResolverConfig::default().name_servers().to_owned();

        let resolver = AsyncResolver::<R>::new(
            ResolverConfig::from_parts(Some(domain), search, name_servers),
            ResolverOpts {
                // matches kubernetes default
                ndots: 5,
                ip_strategy: LookupIpStrategy::Ipv4Only,
                ..ResolverOpts::default()
            },
            handle,
        );

        // notice this is not a FQDN, no trailing dot.
        let response = exec
            .block_on(resolver.lookup_ip("www.example.com"))
            .expect("failed to run lookup");

        assert_ne!(response.iter().count(), 0);
    }

    /// Test domain search.
    pub fn domain_search_test<E: Executor + Send + 'static, R: ConnectionProvider>(
        mut exec: E,
        handle: R,
    ) {
        //env_logger::try_init().ok();

        // domain is good now, should be combined with the name to form www.example.com
        let domain = Name::from_str("example.com.").unwrap();
        let search = vec![
            Name::from_str("bad.example.com.").unwrap(),
            Name::from_str("wrong.example.com.").unwrap(),
        ];
        let name_servers: Vec<NameServerConfig> =
            ResolverConfig::default().name_servers().to_owned();

        let resolver = AsyncResolver::<R>::new(
            ResolverConfig::from_parts(Some(domain), search, name_servers),
            ResolverOpts {
                ip_strategy: LookupIpStrategy::Ipv4Only,
                ..ResolverOpts::default()
            },
            handle,
        );

        // notice no dots, should not trigger ndots rule
        let response = exec
            .block_on(resolver.lookup_ip("www"))
            .expect("failed to run lookup");

        assert_ne!(response.iter().count(), 0);
    }

    /// Test search lists.
    pub fn search_list_test<E: Executor + Send + 'static, R: ConnectionProvider>(
        mut exec: E,
        handle: R,
    ) {
        let domain = Name::from_str("incorrect.example.com.").unwrap();
        let search = vec![
            // let's skip one search domain to test the loop...
            Name::from_str("bad.example.com.").unwrap(),
            // this should combine with the search name to form www.example.com
            Name::from_str("example.com.").unwrap(),
        ];
        let name_servers: Vec<NameServerConfig> =
            ResolverConfig::default().name_servers().to_owned();

        let resolver = AsyncResolver::<R>::new(
            ResolverConfig::from_parts(Some(domain), search, name_servers),
            ResolverOpts {
                ip_strategy: LookupIpStrategy::Ipv4Only,
                ..ResolverOpts::default()
            },
            handle,
        );

        // notice no dots, should not trigger ndots rule
        let response = exec
            .block_on(resolver.lookup_ip("www"))
            .expect("failed to run lookup");

        assert_ne!(response.iter().count(), 0);
    }

    /// Test idna.
    pub fn idna_test<E: Executor + Send + 'static, R: ConnectionProvider>(mut exec: E, handle: R) {
        let resolver =
            AsyncResolver::<R>::new(ResolverConfig::default(), ResolverOpts::default(), handle);

        let response = exec
            .block_on(resolver.lookup_ip("中国.icom.museum."))
            .expect("failed to run lookup");

        // we just care that the request succeeded, not about the actual content
        //   it's not certain that the ip won't change.
        assert!(response.iter().next().is_some());
    }

    /// Test ipv4 localhost.
    pub fn localhost_ipv4_test<E: Executor + Send + 'static, R: ConnectionProvider>(
        mut exec: E,
        handle: R,
    ) {
        let resolver = AsyncResolver::<R>::new(
            ResolverConfig::default(),
            ResolverOpts {
                ip_strategy: LookupIpStrategy::Ipv4thenIpv6,
                ..ResolverOpts::default()
            },
            handle,
        );

        let response = exec
            .block_on(resolver.lookup_ip("localhost"))
            .expect("failed to run lookup");

        let mut iter = response.iter();
        assert_eq!(
            iter.next().expect("no A"),
            IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))
        );
    }

    /// Test ipv6 localhost.
    pub fn localhost_ipv6_test<E: Executor + Send + 'static, R: ConnectionProvider>(
        mut exec: E,
        handle: R,
    ) {
        let resolver = AsyncResolver::<R>::new(
            ResolverConfig::default(),
            ResolverOpts {
                ip_strategy: LookupIpStrategy::Ipv6thenIpv4,
                ..ResolverOpts::default()
            },
            handle,
        );

        let response = exec
            .block_on(resolver.lookup_ip("localhost"))
            .expect("failed to run lookup");

        let mut iter = response.iter();
        assert_eq!(
            iter.next().expect("no AAAA"),
            IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1,))
        );
    }

    /// Test ipv4 search with large ndots.
    pub fn search_ipv4_large_ndots_test<E: Executor + Send + 'static, R: ConnectionProvider>(
        mut exec: E,
        handle: R,
    ) {
        let mut config = ResolverConfig::default();
        config.add_search(Name::from_str("example.com").unwrap());

        let resolver = AsyncResolver::<R>::new(
            config,
            ResolverOpts {
                ip_strategy: LookupIpStrategy::Ipv4Only,
                ndots: 5,
                ..ResolverOpts::default()
            },
            handle,
        );

        let response = exec
            .block_on(resolver.lookup_ip("198.51.100.35"))
            .expect("failed to run lookup");

        let mut iter = response.iter();
        assert_eq!(
            iter.next().expect("no rdatas"),
            IpAddr::V4(Ipv4Addr::new(198, 51, 100, 35))
        );
    }

    /// Test ipv6 search with large ndots.
    pub fn search_ipv6_large_ndots_test<E: Executor + Send + 'static, R: ConnectionProvider>(
        mut exec: E,
        handle: R,
    ) {
        let mut config = ResolverConfig::default();
        config.add_search(Name::from_str("example.com").unwrap());

        let resolver = AsyncResolver::<R>::new(
            config,
            ResolverOpts {
                ip_strategy: LookupIpStrategy::Ipv4Only,
                ndots: 5,
                ..ResolverOpts::default()
            },
            handle,
        );

        let response = exec
            .block_on(resolver.lookup_ip("2001:db8::c633:6423"))
            .expect("failed to run lookup");

        let mut iter = response.iter();
        assert_eq!(
            iter.next().expect("no rdatas"),
            IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0xc633, 0x6423))
        );
    }

    /// Test ipv6 name parse fails.
    pub fn search_ipv6_name_parse_fails_test<
        E: Executor + Send + 'static,
        R: ConnectionProvider,
    >(
        mut exec: E,
        handle: R,
    ) {
        let mut config = ResolverConfig::default();
        config.add_search(Name::from_str("example.com").unwrap());

        let resolver = AsyncResolver::<R>::new(
            config,
            ResolverOpts {
                ip_strategy: LookupIpStrategy::Ipv4Only,
                ndots: 5,
                ..ResolverOpts::default()
            },
            handle,
        );

        let response = exec
            .block_on(resolver.lookup_ip("2001:db8::198.51.100.35"))
            .expect("failed to run lookup");

        let mut iter = response.iter();
        assert_eq!(
            iter.next().expect("no rdatas"),
            IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0xc633, 0x6423))
        );
    }
}
#[cfg(test)]
#[cfg(feature = "tokio-runtime")]
#[allow(clippy::extra_unused_type_parameters)]
mod tests {
    use proto::xfer::DnsRequest;
    use tokio::runtime::Runtime;

    use crate::config::{ResolverConfig, ResolverOpts};
    use crate::name_server::GenericConnection;

    use super::*;

    fn is_send_t<T: Send>() -> bool {
        true
    }

    fn is_sync_t<T: Sync>() -> bool {
        true
    }

    #[test]
    fn test_send_sync() {
        assert!(is_send_t::<ResolverConfig>());
        assert!(is_sync_t::<ResolverConfig>());
        assert!(is_send_t::<ResolverOpts>());
        assert!(is_sync_t::<ResolverOpts>());

        assert!(is_send_t::<AsyncResolver<TokioConnectionProvider>>());
        assert!(is_sync_t::<AsyncResolver<TokioConnectionProvider>>());

        assert!(is_send_t::<DnsRequest>());
        assert!(is_send_t::<LookupIpFuture<GenericConnection, ResolveError>>());
        assert!(is_send_t::<LookupFuture<GenericConnection, ResolveError>>());
    }

    #[test]
    fn test_lookup_google() {
        use super::testing::lookup_test;
        let io_loop = Runtime::new().expect("failed to create tokio runtime");
        let handle = TokioConnectionProvider::default();
        lookup_test::<Runtime, TokioConnectionProvider>(ResolverConfig::google(), io_loop, handle)
    }

    #[test]
    fn test_lookup_cloudflare() {
        use super::testing::lookup_test;
        let io_loop = Runtime::new().expect("failed to create tokio runtime");
        let handle = TokioConnectionProvider::default();
        lookup_test::<Runtime, TokioConnectionProvider>(
            ResolverConfig::cloudflare(),
            io_loop,
            handle,
        )
    }

    #[test]
    fn test_lookup_quad9() {
        use super::testing::lookup_test;
        let io_loop = Runtime::new().expect("failed to create tokio runtime");
        let handle = TokioConnectionProvider::default();
        lookup_test::<Runtime, TokioConnectionProvider>(ResolverConfig::quad9(), io_loop, handle)
    }

    #[test]
    fn test_ip_lookup() {
        use super::testing::ip_lookup_test;
        let io_loop = Runtime::new().expect("failed to create tokio runtime");
        let handle = TokioConnectionProvider::default();
        ip_lookup_test::<Runtime, TokioConnectionProvider>(io_loop, handle)
    }

    #[test]
    fn test_ip_lookup_across_threads() {
        use super::testing::ip_lookup_across_threads_test;
        let _io_loop = Runtime::new().expect("failed to create tokio runtime io_loop");
        let handle = TokioConnectionProvider::default();
        ip_lookup_across_threads_test::<Runtime, TokioConnectionProvider>(handle)
    }

    #[test]
    #[cfg(feature = "dnssec")]
    fn test_sec_lookup() {
        use super::testing::sec_lookup_test;
        let io_loop = Runtime::new().expect("failed to create tokio runtime io_loop");
        let handle = TokioConnectionProvider::default();
        sec_lookup_test::<Runtime, TokioConnectionProvider>(io_loop, handle);
    }

    #[test]
    #[cfg(feature = "dnssec")]
    fn test_sec_lookup_fails() {
        use super::testing::sec_lookup_fails_test;
        let io_loop = Runtime::new().expect("failed to create tokio runtime io_loop");
        let handle = TokioConnectionProvider::default();
        sec_lookup_fails_test::<Runtime, TokioConnectionProvider>(io_loop, handle);
    }

    #[test]
    #[ignore]
    #[cfg(any(unix, target_os = "windows"))]
    #[cfg(feature = "system-config")]
    fn test_system_lookup() {
        use super::testing::system_lookup_test;
        let io_loop = Runtime::new().expect("failed to create tokio runtime io_loop");
        let handle = TokioConnectionProvider::default();
        system_lookup_test::<Runtime, TokioConnectionProvider>(io_loop, handle);
    }

    #[test]
    #[ignore]
    // these appear to not work on CI, test on macos with `10.1.0.104  a.com`
    #[cfg(unix)]
    #[cfg(feature = "system-config")]
    fn test_hosts_lookup() {
        use super::testing::hosts_lookup_test;
        let io_loop = Runtime::new().expect("failed to create tokio runtime io_loop");
        let handle = TokioConnectionProvider::default();
        hosts_lookup_test::<Runtime, TokioConnectionProvider>(io_loop, handle);
    }

    #[test]
    fn test_fqdn() {
        use super::testing::fqdn_test;
        let io_loop = Runtime::new().expect("failed to create tokio runtime io_loop");
        let handle = TokioConnectionProvider::default();
        fqdn_test::<Runtime, TokioConnectionProvider>(io_loop, handle);
    }

    #[test]
    fn test_ndots() {
        use super::testing::ndots_test;
        let io_loop = Runtime::new().expect("failed to create tokio runtime io_loop");
        let handle = TokioConnectionProvider::default();
        ndots_test::<Runtime, TokioConnectionProvider>(io_loop, handle);
    }

    #[test]
    fn test_large_ndots() {
        use super::testing::large_ndots_test;
        let io_loop = Runtime::new().expect("failed to create tokio runtime io_loop");
        let handle = TokioConnectionProvider::default();
        large_ndots_test::<Runtime, TokioConnectionProvider>(io_loop, handle);
    }

    #[test]
    fn test_domain_search() {
        use super::testing::domain_search_test;
        let io_loop = Runtime::new().expect("failed to create tokio runtime io_loop");
        let handle = TokioConnectionProvider::default();
        domain_search_test::<Runtime, TokioConnectionProvider>(io_loop, handle);
    }

    #[test]
    fn test_search_list() {
        use super::testing::search_list_test;
        let io_loop = Runtime::new().expect("failed to create tokio runtime io_loop");
        let handle = TokioConnectionProvider::default();
        search_list_test::<Runtime, TokioConnectionProvider>(io_loop, handle);
    }

    #[test]
    fn test_idna() {
        use super::testing::idna_test;
        let io_loop = Runtime::new().expect("failed to create tokio runtime io_loop");
        let handle = TokioConnectionProvider::default();
        idna_test::<Runtime, TokioConnectionProvider>(io_loop, handle);
    }

    #[test]
    fn test_localhost_ipv4() {
        use super::testing::localhost_ipv4_test;
        let io_loop = Runtime::new().expect("failed to create tokio runtime io_loop");
        let handle = TokioConnectionProvider::default();
        localhost_ipv4_test::<Runtime, TokioConnectionProvider>(io_loop, handle);
    }

    #[test]
    fn test_localhost_ipv6() {
        use super::testing::localhost_ipv6_test;
        let io_loop = Runtime::new().expect("failed to create tokio runtime io_loop");
        let handle = TokioConnectionProvider::default();
        localhost_ipv6_test::<Runtime, TokioConnectionProvider>(io_loop, handle);
    }

    #[test]
    fn test_search_ipv4_large_ndots() {
        use super::testing::search_ipv4_large_ndots_test;
        let io_loop = Runtime::new().expect("failed to create tokio runtime io_loop");
        let handle = TokioConnectionProvider::default();
        search_ipv4_large_ndots_test::<Runtime, TokioConnectionProvider>(io_loop, handle);
    }

    #[test]
    fn test_search_ipv6_large_ndots() {
        use super::testing::search_ipv6_large_ndots_test;
        let io_loop = Runtime::new().expect("failed to create tokio runtime io_loop");
        let handle = TokioConnectionProvider::default();
        search_ipv6_large_ndots_test::<Runtime, TokioConnectionProvider>(io_loop, handle);
    }

    #[test]
    fn test_search_ipv6_name_parse_fails() {
        use super::testing::search_ipv6_name_parse_fails_test;
        let io_loop = Runtime::new().expect("failed to create tokio runtime io_loop");
        let handle = TokioConnectionProvider::default();
        search_ipv6_name_parse_fails_test::<Runtime, TokioConnectionProvider>(io_loop, handle);
    }

    #[test]
    fn test_build_names_onion() {
        let handle = TokioConnectionProvider::default();
        let mut config = ResolverConfig::default();
        config.add_search(Name::from_ascii("example.com.").unwrap());
        let resolver =
            AsyncResolver::<TokioConnectionProvider>::new(config, ResolverOpts::default(), handle);
        let tor_address = [
            Name::from_ascii("2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion")
                .unwrap(),
            Name::from_ascii("www.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion")
                .unwrap(), // subdomain are allowed too
        ];
        let not_tor_address = [
            Name::from_ascii("onion").unwrap(),
            Name::from_ascii("www.onion").unwrap(),
            Name::from_ascii("2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.www.onion")
                .unwrap(), // www before key
            Name::from_ascii("2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion.to")
                .unwrap(), // Tor2web
        ];
        for name in &tor_address {
            assert_eq!(resolver.build_names(name.clone()).len(), 1);
        }
        for name in &not_tor_address {
            assert_eq!(resolver.build_names(name.clone()).len(), 2);
        }
    }
}
