#![warn(rust_2018_idioms)]
#![cfg(all(feature = "full", not(target_os = "wasi"), not(miri)))] // Wasi doesn't support bind
                                                                   // No `socket` on miri.

use std::time::Duration;
use tokio::net::TcpSocket;
use tokio_test::assert_ok;

#[tokio::test]
async fn basic_usage_v4() {
    // Create server
    let addr = assert_ok!("127.0.0.1:0".parse());
    let srv = assert_ok!(TcpSocket::new_v4());
    assert_ok!(srv.bind(addr));

    let srv = assert_ok!(srv.listen(128));

    // Create client & connect
    let addr = srv.local_addr().unwrap();
    let cli = assert_ok!(TcpSocket::new_v4());
    let _cli = assert_ok!(cli.connect(addr).await);

    // Accept
    let _ = assert_ok!(srv.accept().await);
}

#[tokio::test]
async fn basic_usage_v6() {
    // Create server
    let addr = assert_ok!("[::1]:0".parse());
    let srv = assert_ok!(TcpSocket::new_v6());
    assert_ok!(srv.bind(addr));

    let srv = assert_ok!(srv.listen(128));

    // Create client & connect
    let addr = srv.local_addr().unwrap();
    let cli = assert_ok!(TcpSocket::new_v6());
    let _cli = assert_ok!(cli.connect(addr).await);

    // Accept
    let _ = assert_ok!(srv.accept().await);
}

#[tokio::test]
async fn bind_before_connect() {
    // Create server
    let any_addr = assert_ok!("127.0.0.1:0".parse());
    let srv = assert_ok!(TcpSocket::new_v4());
    assert_ok!(srv.bind(any_addr));

    let srv = assert_ok!(srv.listen(128));

    // Create client & connect
    let addr = srv.local_addr().unwrap();
    let cli = assert_ok!(TcpSocket::new_v4());
    assert_ok!(cli.bind(any_addr));
    let _cli = assert_ok!(cli.connect(addr).await);

    // Accept
    let _ = assert_ok!(srv.accept().await);
}

#[tokio::test]
async fn basic_linger() {
    // Create server
    let addr = assert_ok!("127.0.0.1:0".parse());
    let srv = assert_ok!(TcpSocket::new_v4());
    assert_ok!(srv.bind(addr));

    assert!(srv.linger().unwrap().is_none());

    srv.set_zero_linger().unwrap();
    assert_eq!(srv.linger().unwrap(), Some(Duration::new(0, 0)));
}

/// Macro to create a simple test to set and get a socket option.
macro_rules! test {
    // Test using the `arg`ument as expected return value.
    ($( #[ $attr: meta ] )* $get_fn: ident, $set_fn: ident ( $arg: expr ) ) => {
        test!($( #[$attr] )* $get_fn, $set_fn($arg), $arg);
    };
    ($( #[ $attr: meta ] )* $get_fn: ident, $set_fn: ident ( $arg: expr ), $expected: expr ) => {
        #[test]
        $( #[$attr] )*
        fn $get_fn() {
            test!(__ new_v4, $get_fn, $set_fn($arg), $expected);
            #[cfg(not(target_os = "vita"))]
            test!(__ new_v6, $get_fn, $set_fn($arg), $expected);
        }
    };
    // Only test using a IPv4 socket.
    (IPv4 $get_fn: ident, $set_fn: ident ( $arg: expr ) ) => {
        #[test]
        fn $get_fn() {
            test!(__ new_v4, $get_fn, $set_fn($arg), $arg);
        }
    };
    // Only test using a IPv6 socket.
    (IPv6 $get_fn: ident, $set_fn: ident ( $arg: expr ) ) => {
        #[test]
        fn $get_fn() {
            test!(__ new_v6, $get_fn, $set_fn($arg), $arg);
        }
    };

    // Internal to this macro.
    (__ $constructor: ident, $get_fn: ident, $set_fn: ident ( $arg: expr ), $expected: expr ) => {
        let socket = TcpSocket::$constructor().expect("failed to create `TcpSocket`");

        let initial = socket.$get_fn().expect("failed to get initial value");
        let arg = $arg;
        assert_ne!(initial, arg, "initial value and argument are the same");

        socket.$set_fn(arg).expect("failed to set option");
        let got = socket.$get_fn().expect("failed to get value");
        let expected = $expected;
        assert_eq!(got, expected, "set and get values differ");
    };
}

const SET_BUF_SIZE: u32 = 4096;
// Linux doubles the buffer size for kernel usage, and exposes that when
// retrieving the buffer size.

#[cfg(not(any(target_os = "android", target_os = "linux")))]
const GET_BUF_SIZE: u32 = SET_BUF_SIZE;

#[cfg(any(target_os = "android", target_os = "linux"))]
const GET_BUF_SIZE: u32 = 2 * SET_BUF_SIZE;

test!(keepalive, set_keepalive(true));

test!(reuseaddr, set_reuseaddr(true));

#[cfg(all(
    unix,
    not(target_os = "solaris"),
    not(target_os = "illumos"),
    not(target_os = "cygwin"),
))]
test!(reuseport, set_reuseport(true));

test!(
    send_buffer_size,
    set_send_buffer_size(SET_BUF_SIZE),
    GET_BUF_SIZE
);

test!(
    recv_buffer_size,
    set_recv_buffer_size(SET_BUF_SIZE),
    GET_BUF_SIZE
);

test!(
    #[expect(deprecated, reason = "set_linger is deprecated")]
    linger,
    set_linger(Some(Duration::from_secs(10)))
);

test!(nodelay, set_nodelay(true));

#[cfg(any(
    target_os = "android",
    target_os = "dragonfly",
    target_os = "freebsd",
    target_os = "fuchsia",
    target_os = "linux",
    target_os = "macos",
    target_os = "netbsd",
    target_os = "openbsd",
    target_os = "cygwin",
))]
test!(IPv6 tclass_v6, set_tclass_v6(96));

#[cfg(not(any(
    target_os = "fuchsia",
    target_os = "redox",
    target_os = "solaris",
    target_os = "illumos",
    target_os = "haiku"
)))]
test!(IPv4 tos_v4, set_tos_v4(96));
