// Copyright 2015-2017 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.

mod server_harness;

use std::io::Write;
use std::net::*;
use std::str::FromStr;

use tokio::net::TcpStream as TokioTcpStream;
use tokio::net::UdpSocket as TokioUdpSocket;
use tokio::runtime::Runtime;

use hickory_client::client::*;
use hickory_client::op::ResponseCode;
use hickory_client::rr::*;
use hickory_client::tcp::TcpClientStream;
use hickory_client::udp::UdpClientStream;

// TODO: Needed for when TLS tests are added back
// #[cfg(feature = "dns-over-openssl")]
// use hickory_proto::openssl::TlsClientStreamBuilder;

use hickory_proto::iocompat::AsyncIoTokioAsStd;
use server_harness::{named_test_harness, query_a};

#[test]
fn test_example_toml_startup() {
    named_test_harness("example.toml", |_, tcp_port, _, _, _| {
        let mut io_loop = Runtime::new().unwrap();
        let addr: SocketAddr = SocketAddr::new(
            Ipv4Addr::new(127, 0, 0, 1).into(),
            tcp_port.expect("no tcp_port"),
        );
        let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
        let client = AsyncClient::new(Box::new(stream), sender, None);

        let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
        hickory_proto::spawn_bg(&io_loop, bg);

        query_a(&mut io_loop, &mut client);

        // just tests that multiple queries work
        let addr: SocketAddr = SocketAddr::new(
            Ipv4Addr::new(127, 0, 0, 1).into(),
            tcp_port.expect("no tcp_port"),
        );
        let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
        let client = AsyncClient::new(Box::new(stream), sender, None);

        let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
        hickory_proto::spawn_bg(&io_loop, bg);

        query_a(&mut io_loop, &mut client);
    })
}

#[test]
fn test_ipv4_only_toml_startup() {
    named_test_harness("ipv4_only.toml", |_, tcp_port, _, _, _| {
        let mut io_loop = Runtime::new().unwrap();
        let addr: SocketAddr = SocketAddr::new(
            Ipv4Addr::new(127, 0, 0, 1).into(),
            tcp_port.expect("no tcp_port"),
        );
        let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
        let client = AsyncClient::new(Box::new(stream), sender, None);

        let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
        hickory_proto::spawn_bg(&io_loop, bg);

        // ipv4 should succeed
        query_a(&mut io_loop, &mut client);

        let addr: SocketAddr = SocketAddr::new(
            Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1).into(),
            tcp_port.expect("no tcp_port"),
        );
        let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
        let client = AsyncClient::new(Box::new(stream), sender, None);

        assert!(io_loop.block_on(client).is_err());
        //let (client, bg) = io_loop.block_on(client).expect("client failed to connect");
        //hickory_proto::spawn_bg(&io_loop, bg);

        // ipv6 should fail
        // FIXME: probably need to send something for proper test... maybe use JoinHandle in tokio 0.2
        // assert!(io_loop.block_on(client).is_err());
    })
}

// TODO: this is commented out b/c at least on macOS, ipv4 will route properly to ipv6 only
//  listeners over the [::ffff:127.0.0.1] interface
//
// #[ignore]
// #[test]
// fn test_ipv6_only_toml_startup() {
//   named_test_harness("ipv6_only.toml", |port, _| {
//     let mut io_loop = Runtime::new().unwrap();
//     let addr: SocketAddr = ("127.0.0.1", port).to_socket_addrs().unwrap().next().unwrap();
//     let (stream, sender) = TcpClientStream::new(addr);
//     let client = AsyncClient::new(stream, sender, None);
//     let mut client = io_loop.block_on(client).unwrap();
//
//     // ipv4 should fail
//     assert!(!query(&mut io_loop, client));
//
//     let addr: SocketAddr = ("::1", port).to_socket_addrs().unwrap().next().unwrap();
//     let (stream, sender) = TcpClientStream::new(addr);
//     let client = AsyncClient::new(stream, sender, None);
//     let mut client = io_loop.block_on(client).unwrap();
//
//     // ipv6 should succeed
//     assert!(query(&mut io_loop, client));
//
//     assert!(true);
//   })
// }

#[ignore]
#[test]
fn test_ipv4_and_ipv6_toml_startup() {
    named_test_harness("ipv4_and_ipv6.toml", |_, tcp_port, _, _, _| {
        let mut io_loop = Runtime::new().unwrap();
        let addr: SocketAddr = SocketAddr::new(
            Ipv4Addr::new(127, 0, 0, 1).into(),
            tcp_port.expect("no tcp_port"),
        );
        let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
        let client = AsyncClient::new(Box::new(stream), sender, None);

        let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
        hickory_proto::spawn_bg(&io_loop, bg);
        // ipv4 should succeed
        query_a(&mut io_loop, &mut client);

        let addr: SocketAddr = SocketAddr::new(
            Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1).into(),
            tcp_port.expect("no tcp_port"),
        );
        let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
        let client = AsyncClient::new(Box::new(stream), sender, None);
        let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
        hickory_proto::spawn_bg(&io_loop, bg);

        // ipv6 should succeed
        query_a(&mut io_loop, &mut client);
    })
}

#[test]
fn test_nodata_where_name_exists() {
    named_test_harness("example.toml", |_, tcp_port, _, _, _| {
        let io_loop = Runtime::new().unwrap();
        let addr: SocketAddr = SocketAddr::new(
            Ipv4Addr::new(127, 0, 0, 1).into(),
            tcp_port.expect("no tcp_port"),
        );
        let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
        let client = AsyncClient::new(Box::new(stream), sender, None);
        let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
        hickory_proto::spawn_bg(&io_loop, bg);

        let msg = io_loop
            .block_on(client.query(
                Name::from_str("www.example.com.").unwrap(),
                DNSClass::IN,
                RecordType::SRV,
            ))
            .unwrap();
        assert_eq!(msg.response_code(), ResponseCode::NoError);
        assert!(msg.answers().is_empty());
    })
}

#[test]
fn test_nxdomain_where_no_name_exists() {
    named_test_harness("example.toml", |_, tcp_port, _, _, _| {
        let io_loop = Runtime::new().unwrap();
        let addr: SocketAddr = SocketAddr::new(
            Ipv4Addr::new(127, 0, 0, 1).into(),
            tcp_port.expect("no tcp_port"),
        );
        let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
        let client = AsyncClient::new(Box::new(stream), sender, None);
        let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
        hickory_proto::spawn_bg(&io_loop, bg);

        let msg = io_loop
            .block_on(client.query(
                Name::from_str("nxdomain.example.com.").unwrap(),
                DNSClass::IN,
                RecordType::SRV,
            ))
            .unwrap();
        assert_eq!(msg.response_code(), ResponseCode::NXDomain);
        assert!(msg.answers().is_empty());
    })
}

#[test]
fn test_server_continues_on_bad_data_udp() {
    named_test_harness("example.toml", |udp_port, _, _, _, _| {
        let mut io_loop = Runtime::new().unwrap();
        let addr: SocketAddr = SocketAddr::new(
            Ipv4Addr::new(127, 0, 0, 1).into(),
            udp_port.expect("no udp_port"),
        );
        let stream = UdpClientStream::<TokioUdpSocket>::new(addr);
        let client = AsyncClient::connect(stream);
        let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
        hickory_proto::spawn_bg(&io_loop, bg);

        query_a(&mut io_loop, &mut client);

        // Send a bad packet, this should get rejected by the server
        let raw_socket = UdpSocket::bind(SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0))
            .expect("couldn't bind raw");

        raw_socket
            .send_to(b"0xDEADBEEF", addr)
            .expect("raw send failed");

        // just tests that multiple queries work
        let addr: SocketAddr = SocketAddr::new(
            Ipv4Addr::new(127, 0, 0, 1).into(),
            udp_port.expect("no udp_port"),
        );
        let stream = UdpClientStream::<TokioUdpSocket>::new(addr);
        let client = AsyncClient::connect(stream);

        let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
        hickory_proto::spawn_bg(&io_loop, bg);

        query_a(&mut io_loop, &mut client);
    })
}

#[test]
fn test_server_continues_on_bad_data_tcp() {
    named_test_harness("example.toml", |_, tcp_port, _, _, _| {
        let mut io_loop = Runtime::new().unwrap();
        let addr: SocketAddr = SocketAddr::new(
            Ipv4Addr::new(127, 0, 0, 1).into(),
            tcp_port.expect("no tcp_port"),
        );
        let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
        let client = AsyncClient::new(Box::new(stream), sender, None);

        let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
        hickory_proto::spawn_bg(&io_loop, bg);

        query_a(&mut io_loop, &mut client);

        // Send a bad packet, this should get rejected by the server
        let mut raw_socket = TcpStream::connect(addr).expect("couldn't bind raw");

        raw_socket
            .write_all(b"0xDEADBEEF")
            .expect("raw send failed");

        // just tests that multiple queries work
        let addr: SocketAddr = SocketAddr::new(
            Ipv4Addr::new(127, 0, 0, 1).into(),
            tcp_port.expect("no tcp_port"),
        );
        let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
        let client = AsyncClient::new(Box::new(stream), sender, None);
        let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
        hickory_proto::spawn_bg(&io_loop, bg);

        query_a(&mut io_loop, &mut client);
    })
}

#[test]
#[cfg(feature = "resolver")]
fn test_forward() {
    use server_harness::query_message;

    //env_logger::init();

    named_test_harness("example_forwarder.toml", |_, tcp_port, _, _, _| {
        let mut io_loop = Runtime::new().unwrap();
        let addr: SocketAddr = SocketAddr::new(
            Ipv4Addr::new(127, 0, 0, 1).into(),
            tcp_port.expect("no tcp_port"),
        );
        let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
        let client = AsyncClient::new(Box::new(stream), sender, None);

        let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
        hickory_proto::spawn_bg(&io_loop, bg);

        let response = query_message(
            &mut io_loop,
            &mut client,
            Name::from_str("www.example.com").unwrap(),
            RecordType::A,
        );
        assert!(response
            .answers()
            .iter()
            .any(|record| record.data().unwrap().as_a().is_some()));

        // just tests that multiple queries work
        let addr: SocketAddr = SocketAddr::new(
            Ipv4Addr::new(127, 0, 0, 1).into(),
            tcp_port.expect("no tcp_port"),
        );
        let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
        let client = AsyncClient::new(Box::new(stream), sender, None);

        let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
        hickory_proto::spawn_bg(&io_loop, bg);

        let response = query_message(
            &mut io_loop,
            &mut client,
            Name::from_str("www.example.com").unwrap(),
            RecordType::A,
        );
        assert!(response
            .answers()
            .iter()
            .any(|record| record.data().unwrap().as_a().is_some()));
        assert!(!response.header().authoritative());
    })
}
