use anyhow::Context;
use anyhow::Result;
use reserve_port::ReservedPort;
use std::net::IpAddr;
use std::net::Ipv4Addr;
use std::net::SocketAddr;
use std::net::TcpListener as StdTcpListener;
use tokio::net::TcpListener as TokioTcpListener;

pub const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST);

#[derive(Debug)]
pub struct StartingTcpSetup {
    pub maybe_reserved_port: Option<ReservedPort>,
    pub socket_addr: SocketAddr,
    pub tcp_listener: TokioTcpListener,
}

impl StartingTcpSetup {
    pub fn new(maybe_ip: Option<IpAddr>, maybe_port: Option<u16>) -> Result<Self> {
        let ip = maybe_ip.unwrap_or(DEFAULT_IP_ADDRESS);

        maybe_port
            .map(|port| Self::new_with_port(ip, port))
            .unwrap_or_else(|| Self::new_without_port(ip))
    }

    fn new_with_port(ip: IpAddr, port: u16) -> Result<Self> {
        ReservedPort::reserve_port(port)?;
        let socket_addr = SocketAddr::new(ip, port);
        let std_tcp_listener = StdTcpListener::bind(socket_addr)
            .context("Failed to create TCPListener for TestServer")?;
        std_tcp_listener.set_nonblocking(true)?;
        let tokio_tcp_listener = TokioTcpListener::from_std(std_tcp_listener)?;

        Ok(Self {
            maybe_reserved_port: None,
            socket_addr,
            tcp_listener: tokio_tcp_listener,
        })
    }

    fn new_without_port(ip: IpAddr) -> Result<Self> {
        let (reserved_port, std_tcp_listener) = ReservedPort::random_with_tcp(ip)?;
        let socket_addr = SocketAddr::new(ip, reserved_port.port());
        std_tcp_listener.set_nonblocking(true)?;
        let tokio_tcp_listener = TokioTcpListener::from_std(std_tcp_listener)?;

        Ok(Self {
            maybe_reserved_port: Some(reserved_port),
            socket_addr,
            tcp_listener: tokio_tcp_listener,
        })
    }
}

#[cfg(test)]
mod test_new {
    use super::*;
    use regex::Regex;
    use std::net::Ipv4Addr;

    #[tokio::test]
    async fn it_should_create_default_ip_with_random_port_when_none() {
        let ip = None;
        let port = None;

        let setup = StartingTcpSetup::new(ip, port).unwrap();
        let addr = setup.socket_addr.to_string();

        let regex = Regex::new("^127\\.0\\.0\\.1:[0-9]+$").unwrap();
        let is_match = regex.is_match(&addr);
        assert!(is_match);
    }

    #[tokio::test]
    async fn it_should_create_ip_with_random_port_when_ip_given() {
        let ip = Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
        let port = None;

        let setup = StartingTcpSetup::new(ip, port).unwrap();
        let addr = setup.socket_addr.to_string();

        let regex = Regex::new("^127\\.0\\.0\\.1:[0-9]+$").unwrap();
        let is_match = regex.is_match(&addr);
        assert!(is_match);
    }

    #[tokio::test]
    async fn it_should_create_default_ip_with_port_when_port_given() {
        let ip = None;
        let port = Some(8123);

        let setup = StartingTcpSetup::new(ip, port).unwrap();
        let addr = setup.socket_addr.to_string();

        assert_eq!(addr, "127.0.0.1:8123");
    }

    #[tokio::test]
    async fn it_should_create_ip_port_given_when_both_given() {
        let ip = Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
        let port = Some(8124);

        let setup = StartingTcpSetup::new(ip, port).unwrap();
        let addr = setup.socket_addr.to_string();

        assert_eq!(addr, "127.0.0.1:8124");
    }
}
