// Copyright 2018 MaidSafe.net limited.
//
// This SAFE Network Software is licensed to you under the MIT license <LICENSE-MIT
// http://opensource.org/licenses/MIT> or the Modified BSD license <LICENSE-BSD
// https://opensource.org/licenses/BSD-3-Clause>, at your option. This file may not be copied,
// modified, or distributed except according to those terms. Please review the Licences for the
// specific language governing permissions and limitations relating to use of the SAFE Network
// Software.
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![cfg_attr(docsrs, feature(doc_cfg))]

#[cfg(not(windows))]
mod posix;
#[cfg(all(
    not(windows),
    not(all(
        target_vendor = "apple",
        any(
            target_os = "macos",
            target_os = "ios",
            target_os = "tvos",
            target_os = "watchos",
            target_os = "visionos"
        )
    )),
    not(target_os = "freebsd"),
    not(target_os = "netbsd"),
    not(target_os = "openbsd"),
    not(target_os = "illumos")
))]
mod posix_not_apple;
mod sockaddr;
#[cfg(windows)]
mod windows;

use std::io;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

/// The current operational state of the interface, as defined in RFC 2863 section 6.
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub enum IfOperStatus {
    /// The interface is up and running.
    Up = 1,

    /// The interface is down.
    Down = 2,

    /// The interface is testing.
    Testing = 3,

    /// The interface is unknown.
    Unknown = 4,

    /// The interface is in a "pending" state, waiting for some external event.
    Dormant = 5,

    /// A refinement on the down state which indicates that the relevant
    /// interface is down specifically because some component (typically,
    /// a hardware component) is not present in the managed system.
    NotPresent = 6,

    /// A refinement on the down state. This new state indicates
    /// that this interface runs on top of one or more other interfaces and
    /// that this interface is down specifically because one or more of these
    /// lower-layer interfaces are down.
    LowerLayerDown = 7,
}

impl From<i32> for IfOperStatus {
    fn from(value: i32) -> Self {
        match value {
            1 => Self::Up,
            2 => Self::Down,
            3 => Self::Testing,
            4 => Self::Unknown,
            5 => Self::Dormant,
            6 => Self::NotPresent,
            7 => Self::LowerLayerDown,
            _ => Self::Unknown,
        }
    }
}

/// Details about an interface on this host.
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct Interface {
    /// The name of the interface.
    pub name: String,
    /// The address details of the interface.
    pub addr: IfAddr,
    /// The index of the interface.
    pub index: Option<u32>,

    /// Whether the interface is operational up.
    pub oper_status: IfOperStatus,

    /// (Windows only) A permanent and unique identifier for the interface. It
    /// cannot be modified by the user. It is typically a GUID string of the
    /// form: "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}", but this is not
    /// guaranteed by the Windows API.
    #[cfg(windows)]
    pub adapter_name: String,
}

impl Interface {
    /// Check whether this is a loopback interface.
    #[must_use]
    pub const fn is_loopback(&self) -> bool {
        self.addr.is_loopback()
    }

    /// Check whether this is a link local interface.
    #[must_use]
    pub const fn is_link_local(&self) -> bool {
        self.addr.is_link_local()
    }

    /// Get the IP address of this interface.
    #[must_use]
    pub const fn ip(&self) -> IpAddr {
        self.addr.ip()
    }

    /// Check whether this interface is operationally up.
    #[must_use]
    pub fn is_oper_up(&self) -> bool {
        self.oper_status == IfOperStatus::Up
    }
}

/// Details about the address of an interface on this host.
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub enum IfAddr {
    /// This is an Ipv4 interface.
    V4(Ifv4Addr),
    /// This is an Ipv6 interface.
    V6(Ifv6Addr),
}

impl IfAddr {
    /// Check whether this is a loopback address.
    #[must_use]
    pub const fn is_loopback(&self) -> bool {
        match *self {
            IfAddr::V4(ref ifv4_addr) => ifv4_addr.is_loopback(),
            IfAddr::V6(ref ifv6_addr) => ifv6_addr.is_loopback(),
        }
    }

    /// Check whether this is a link local interface.
    #[must_use]
    pub const fn is_link_local(&self) -> bool {
        match *self {
            IfAddr::V4(ref ifv4_addr) => ifv4_addr.is_link_local(),
            IfAddr::V6(ref ifv6_addr) => ifv6_addr.is_link_local(),
        }
    }

    /// Get the IP address of this interface address.
    #[must_use]
    pub const fn ip(&self) -> IpAddr {
        match *self {
            IfAddr::V4(ref ifv4_addr) => IpAddr::V4(ifv4_addr.ip),
            IfAddr::V6(ref ifv6_addr) => IpAddr::V6(ifv6_addr.ip),
        }
    }
}

/// Details about the ipv4 address of an interface on this host.
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct Ifv4Addr {
    /// The IP address of the interface.
    pub ip: Ipv4Addr,
    /// The netmask of the interface.
    pub netmask: Ipv4Addr,
    /// The CIDR prefix of the interface.
    pub prefixlen: u8,
    /// The broadcast address of the interface.
    pub broadcast: Option<Ipv4Addr>,
}

impl Ifv4Addr {
    /// Check whether this is a loopback address.
    #[must_use]
    pub const fn is_loopback(&self) -> bool {
        self.ip.is_loopback()
    }

    /// Check whether this is a link local address.
    #[must_use]
    pub const fn is_link_local(&self) -> bool {
        self.ip.is_link_local()
    }
}

/// Details about the ipv6 address of an interface on this host.
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct Ifv6Addr {
    /// The IP address of the interface.
    pub ip: Ipv6Addr,
    /// The netmask of the interface.
    pub netmask: Ipv6Addr,
    /// The CIDR prefix of the interface.
    pub prefixlen: u8,
    /// The broadcast address of the interface.
    pub broadcast: Option<Ipv6Addr>,
}

impl Ifv6Addr {
    /// Check whether this is a loopback address.
    #[must_use]
    pub const fn is_loopback(&self) -> bool {
        self.ip.is_loopback()
    }

    /// Check whether this is a link local address.
    #[must_use]
    pub const fn is_link_local(&self) -> bool {
        let bytes = self.ip.octets();

        bytes[0] == 0xfe && bytes[1] == 0x80
    }
}

#[cfg(not(windows))]
mod getifaddrs_posix {
    use libc::if_nametoindex;

    use super::{IfAddr, Ifv4Addr, Ifv6Addr, Interface};
    use crate::posix::{self as ifaddrs, IfAddrs};
    use crate::sockaddr;
    use crate::IfOperStatus;
    use std::ffi::CStr;
    use std::io;
    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

    /// Defined in `<net/if.h>` on POSIX systems.
    /// https://github.com/torvalds/linux/blob/18531f4d1c8c47c4796289dbbc1ab657ffa063d2/include/uapi/linux/if.h#L85
    #[cfg(not(target_os = "illumos"))]
    const POSIX_IFF_RUNNING: u32 = 0x40; // 1<<6
    #[cfg(target_os = "illumos")]
    const POSIX_IFF_RUNNING: u64 = 0x40; // 1<<6

    /// Return a vector of IP details for all the valid interfaces on this host.
    #[allow(unsafe_code)]
    pub fn get_if_addrs() -> io::Result<Vec<Interface>> {
        let mut ret = Vec::<Interface>::new();
        let ifaddrs = IfAddrs::new()?;

        for ifaddr in ifaddrs.iter() {
            let addr = match sockaddr::to_ipaddr(ifaddr.ifa_addr) {
                None => continue,
                Some(IpAddr::V4(ipv4_addr)) => {
                    let netmask = match sockaddr::to_ipaddr(ifaddr.ifa_netmask) {
                        Some(IpAddr::V4(netmask)) => netmask,
                        _ => Ipv4Addr::new(0, 0, 0, 0),
                    };
                    let broadcast = if (ifaddr.ifa_flags & 2) != 0 {
                        match ifaddrs::do_broadcast(&ifaddr) {
                            Some(IpAddr::V4(broadcast)) => Some(broadcast),
                            _ => None,
                        }
                    } else {
                        None
                    };
                    let prefixlen = if cfg!(target_endian = "little") {
                        u32::from_le_bytes(netmask.octets()).count_ones() as u8
                    } else {
                        u32::from_be_bytes(netmask.octets()).count_ones() as u8
                    };
                    IfAddr::V4(Ifv4Addr {
                        ip: ipv4_addr,
                        netmask,
                        prefixlen,
                        broadcast,
                    })
                }
                Some(IpAddr::V6(ipv6_addr)) => {
                    let netmask = match sockaddr::to_ipaddr(ifaddr.ifa_netmask) {
                        Some(IpAddr::V6(netmask)) => netmask,
                        _ => Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0),
                    };
                    let broadcast = if (ifaddr.ifa_flags & 2) != 0 {
                        match ifaddrs::do_broadcast(&ifaddr) {
                            Some(IpAddr::V6(broadcast)) => Some(broadcast),
                            _ => None,
                        }
                    } else {
                        None
                    };
                    let prefixlen = if cfg!(target_endian = "little") {
                        u128::from_le_bytes(netmask.octets()).count_ones() as u8
                    } else {
                        u128::from_be_bytes(netmask.octets()).count_ones() as u8
                    };
                    IfAddr::V6(Ifv6Addr {
                        ip: ipv6_addr,
                        netmask,
                        prefixlen,
                        broadcast,
                    })
                }
            };

            let name = unsafe { CStr::from_ptr(ifaddr.ifa_name) }
                .to_string_lossy()
                .into_owned();
            let index = {
                let index = unsafe { if_nametoindex(ifaddr.ifa_name) };

                // From `man if_nametoindex 3`:
                // The if_nametoindex() function maps the interface name specified in ifname to its
                // corresponding index. If the specified interface does not exist, it returns 0.
                if index == 0 {
                    None
                } else {
                    Some(index)
                }
            };

            let oper_status = if ifaddr.ifa_flags & POSIX_IFF_RUNNING != 0 {
                IfOperStatus::Up
            } else {
                IfOperStatus::Unknown
            };

            ret.push(Interface {
                name,
                addr,
                index,
                oper_status,
            });
        }

        Ok(ret)
    }
}

/// Get a list of all the network interfaces on this machine along with their IP info.
#[cfg(not(windows))]
pub fn get_if_addrs() -> io::Result<Vec<Interface>> {
    getifaddrs_posix::get_if_addrs()
}

#[cfg(windows)]
mod getifaddrs_windows {
    use super::{IfAddr, Ifv4Addr, Ifv6Addr, Interface};
    use crate::sockaddr;
    use crate::windows::IfAddrs;
    use std::io;
    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
    use windows_sys::Win32::Networking::WinSock::IpDadStatePreferred;

    /// Return a vector of IP details for all the valid interfaces on this host.
    pub fn get_if_addrs() -> io::Result<Vec<Interface>> {
        let mut ret = Vec::<Interface>::new();
        let ifaddrs = IfAddrs::new()?;

        for ifaddr in ifaddrs.iter() {
            for addr in ifaddr.unicast_addresses() {
                if addr.DadState != IpDadStatePreferred {
                    continue;
                }
                let addr = match sockaddr::to_ipaddr(addr.Address.lpSockaddr) {
                    None => continue,
                    Some(IpAddr::V4(ipv4_addr)) => {
                        let mut item_netmask = Ipv4Addr::new(0, 0, 0, 0);
                        let mut item_broadcast = None;
                        let item_prefix = addr.OnLinkPrefixLength;

                        // Search prefixes for a prefix matching addr
                        'prefixloopv4: for prefix in ifaddr.prefixes() {
                            let ipprefix = sockaddr::to_ipaddr(prefix.Address.lpSockaddr);
                            match ipprefix {
                                Some(IpAddr::V4(ref a)) => {
                                    let mut netmask: [u8; 4] = [0; 4];
                                    for (n, netmask_elt) in netmask
                                        .iter_mut()
                                        .enumerate()
                                        .take((prefix.PrefixLength as usize + 7) / 8)
                                    {
                                        let x_byte = ipv4_addr.octets()[n];
                                        let y_byte = a.octets()[n];
                                        for m in 0..8 {
                                            if (n * 8) + m >= prefix.PrefixLength as usize {
                                                break;
                                            }
                                            let bit = 1 << (7 - m);
                                            if (x_byte & bit) == (y_byte & bit) {
                                                *netmask_elt |= bit;
                                            } else {
                                                continue 'prefixloopv4;
                                            }
                                        }
                                    }
                                    item_netmask = Ipv4Addr::new(
                                        netmask[0], netmask[1], netmask[2], netmask[3],
                                    );
                                    let mut broadcast: [u8; 4] = ipv4_addr.octets();
                                    for n in 0..4 {
                                        broadcast[n] |= !netmask[n];
                                    }
                                    item_broadcast = Some(Ipv4Addr::new(
                                        broadcast[0],
                                        broadcast[1],
                                        broadcast[2],
                                        broadcast[3],
                                    ));
                                    break 'prefixloopv4;
                                }
                                _ => continue,
                            };
                        }
                        IfAddr::V4(Ifv4Addr {
                            ip: ipv4_addr,
                            netmask: item_netmask,
                            prefixlen: item_prefix,
                            broadcast: item_broadcast,
                        })
                    }
                    Some(IpAddr::V6(ipv6_addr)) => {
                        let mut item_netmask = Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0);
                        let item_prefix = addr.OnLinkPrefixLength;
                        // Search prefixes for a prefix matching addr
                        'prefixloopv6: for prefix in ifaddr.prefixes() {
                            let ipprefix = sockaddr::to_ipaddr(prefix.Address.lpSockaddr);
                            match ipprefix {
                                Some(IpAddr::V6(ref a)) => {
                                    // Iterate the bits in the prefix, if they all match this prefix
                                    // is the right one, else try the next prefix
                                    let mut netmask: [u16; 8] = [0; 8];
                                    for (n, netmask_elt) in netmask
                                        .iter_mut()
                                        .enumerate()
                                        .take((prefix.PrefixLength as usize + 15) / 16)
                                    {
                                        let x_word = ipv6_addr.segments()[n];
                                        let y_word = a.segments()[n];
                                        for m in 0..16 {
                                            if (n * 16) + m >= prefix.PrefixLength as usize {
                                                break;
                                            }
                                            let bit = 1 << (15 - m);
                                            if (x_word & bit) == (y_word & bit) {
                                                *netmask_elt |= bit;
                                            } else {
                                                continue 'prefixloopv6;
                                            }
                                        }
                                    }
                                    item_netmask = Ipv6Addr::new(
                                        netmask[0], netmask[1], netmask[2], netmask[3], netmask[4],
                                        netmask[5], netmask[6], netmask[7],
                                    );
                                    break 'prefixloopv6;
                                }
                                _ => continue,
                            };
                        }
                        IfAddr::V6(Ifv6Addr {
                            ip: ipv6_addr,
                            netmask: item_netmask,
                            prefixlen: item_prefix,
                            broadcast: None,
                        })
                    }
                };

                let index = match addr {
                    IfAddr::V4(_) => ifaddr.ipv4_index(),
                    IfAddr::V6(_) => ifaddr.ipv6_index(),
                };
                let oper_status = ifaddr.oper_status();

                ret.push(Interface {
                    name: ifaddr.name(),
                    addr,
                    index,
                    oper_status,
                    adapter_name: ifaddr.adapter_name(),
                });
            }
        }

        Ok(ret)
    }
}

/// Get a list of all the network interfaces on this machine along with their IP info.
#[cfg(windows)]
pub fn get_if_addrs() -> io::Result<Vec<Interface>> {
    getifaddrs_windows::get_if_addrs()
}

#[cfg(not(any(
    all(
        target_vendor = "apple",
        any(
            target_os = "macos",
            target_os = "ios",
            target_os = "tvos",
            target_os = "watchos",
            target_os = "visionos"
        )
    ),
    target_os = "freebsd",
    target_os = "netbsd",
    target_os = "openbsd",
    target_os = "illumos"
)))]
#[cfg_attr(
    docsrs,
    doc(cfg(any(
        not(target_vendor = "apple"),
        not(target_os = "freebsd"),
        not(target_os = "netbsd"),
        not(target_os = "openbsd"),
        not(target_os = "illumos")
    )))
)]
mod if_change_notifier {
    use super::Interface;
    use std::collections::HashSet;
    use std::io;
    use std::time::{Duration, Instant};

    #[derive(Debug, PartialEq, Eq, Hash, Clone)]
    pub enum IfChangeType {
        Added(Interface),
        Removed(Interface),
    }

    #[cfg(windows)]
    type InternalIfChangeNotifier = crate::windows::WindowsIfChangeNotifier;
    #[cfg(not(windows))]
    type InternalIfChangeNotifier = crate::posix_not_apple::PosixIfChangeNotifier;

    /// (Not available on iOS/macOS) A utility to monitor for interface changes
    /// and report them, so you can handle events such as WiFi
    /// disconnection/flight mode/route changes
    pub struct IfChangeNotifier {
        inner: InternalIfChangeNotifier,
        last_ifs: HashSet<Interface>,
    }

    impl IfChangeNotifier {
        /// Create a new interface change notifier. Returns an OS specific error
        /// if the network notifier could not be set up.
        pub fn new() -> io::Result<Self> {
            Ok(Self {
                inner: InternalIfChangeNotifier::new()?,
                last_ifs: HashSet::from_iter(super::get_if_addrs()?),
            })
        }

        /// (Not available on iOS/macOS) Block until the OS reports that the
        /// network interface list has changed, or until an optional timeout.
        ///
        /// For example, if an ethernet connector is plugged/unplugged, or a
        /// WiFi network is connected to.
        ///
        /// The changed interfaces are returned. If an interface has both IPv4
        /// and IPv6 addresses, you can expect both of them to be returned from
        /// a single call to `wait`.
        ///
        /// Returns an [`io::ErrorKind::WouldBlock`] error on timeout, or
        /// another error if the network notifier could not be read from.
        pub fn wait(&mut self, timeout: Option<Duration>) -> io::Result<Vec<IfChangeType>> {
            let start = Instant::now();
            loop {
                self.inner
                    .wait(timeout.map(|t| t.saturating_sub(start.elapsed())))?;

                // something has changed - now we find out what (or whether it was spurious)
                let new_ifs = HashSet::from_iter(super::get_if_addrs()?);
                let mut changes: Vec<IfChangeType> = new_ifs
                    .difference(&self.last_ifs)
                    .cloned()
                    .map(IfChangeType::Added)
                    .collect();
                changes.extend(
                    self.last_ifs
                        .difference(&new_ifs)
                        .cloned()
                        .map(IfChangeType::Removed),
                );
                self.last_ifs = new_ifs;

                if !changes.is_empty() {
                    return Ok(changes);
                }
            }
        }
    }
}

#[cfg(not(any(
    all(
        target_vendor = "apple",
        any(
            target_os = "macos",
            target_os = "ios",
            target_os = "tvos",
            target_os = "watchos",
            target_os = "visionos"
        )
    ),
    target_os = "freebsd",
    target_os = "netbsd",
    target_os = "openbsd",
    target_os = "illumos"
)))]
#[cfg_attr(
    docsrs,
    doc(cfg(any(
        not(target_vendor = "apple"),
        not(target_os = "freebsd"),
        not(target_os = "netbsd"),
        not(target_os = "openbsd"),
        not(target_os = "illumos")
    )))
)]
pub use if_change_notifier::{IfChangeNotifier, IfChangeType};

#[cfg(test)]
mod tests {
    use super::{get_if_addrs, Interface};
    use std::io::Read;
    use std::net::{IpAddr, Ipv4Addr};
    use std::process::{Command, Stdio};
    use std::str::FromStr;
    use std::thread;
    use std::time::Duration;

    fn list_system_interfaces(cmd: &str, args: &[&str]) -> String {
        let start_cmd = if args.is_empty() {
            Command::new(cmd).stdout(Stdio::piped()).spawn()
        } else if args.len() == 1 {
            let arg1 = args[0];
            if arg1.is_empty() {
                Command::new(cmd).stdout(Stdio::piped()).spawn()
            } else {
                Command::new(cmd).arg(arg1).stdout(Stdio::piped()).spawn()
            }
        } else {
            Command::new(cmd).args(args).stdout(Stdio::piped()).spawn()
        };
        let mut process = match start_cmd {
            Err(why) => {
                println!("couldn't start cmd {} : {}", cmd, why);
                return String::new();
            }
            Ok(process) => process,
        };
        thread::sleep(Duration::from_millis(1000));
        let _ = process.kill();
        let result: Vec<u8> = process
            .stdout
            .unwrap()
            .bytes()
            .map(|x| x.unwrap())
            .collect();
        String::from_utf8(result).unwrap()
    }

    #[cfg(windows)]
    /// Returns (IP-addr-list, IPv4-interface-status-list)
    fn list_system_addrs() -> (Vec<IpAddr>, Vec<(String, bool)>) {
        use std::net::Ipv6Addr;
        let intf_list = list_system_interfaces("ipconfig", &[""]);
        let ipaddr_list = intf_list
            .lines()
            .filter_map(|line| {
                println!("{}", line);
                if line.contains("Address") && !line.contains("Link-local") {
                    let addr_s: Vec<&str> = line.split(" : ").collect();
                    if line.contains("IPv6") {
                        return Some(IpAddr::V6(Ipv6Addr::from_str(addr_s[1]).unwrap()));
                    } else if line.contains("IPv4") {
                        return Some(IpAddr::V4(Ipv4Addr::from_str(addr_s[1]).unwrap()));
                    }
                }
                None
            })
            .collect();

        /* An example on Windows:
           > netsh interface ipv4 show interfaces

           Idx     Met         MTU          State                Name
           ---  ----------  ----------  ------------  ---------------------------
           1          75  4294967295  connected     Loopback Pseudo-Interface 1
           17          35        1500  connected     Wi-Fi
           13          25        1500  disconnected  Local Area Connection* 1
           14          25        1500  disconnected  Local Area Connection* 2
           12          65        1500  disconnected  Bluetooth Network Connection
        */
        let netsh_list =
            list_system_interfaces("netsh", &["interface", "ipv4", "show", "interfaces"]);

        let ipv4_status_vec: Vec<_> = netsh_list
            .lines()
            .filter_map(|line| {
                if !line.contains("Idx") && !line.contains("---") && !line.is_empty() {
                    let columns: Vec<&str> = line.split_whitespace().collect();
                    let status = columns[3].trim().to_string();
                    let is_up = status == "connected";

                    // concat the strings in columns[4] and beyond
                    let intf_name = columns[4..].join(" ");
                    return Some((intf_name, is_up));
                }
                None
            })
            .collect();

        (ipaddr_list, ipv4_status_vec)
    }

    #[cfg(any(target_os = "linux", target_os = "android"))]
    /// Returns (IP-addr-list, interface-status-list)
    fn list_system_addrs() -> (Vec<IpAddr>, Vec<(String, bool)>) {
        let intf_list = list_system_interfaces("ip", &["addr"]);
        let ipaddr_list = intf_list
            .lines()
            .filter_map(|line| {
                println!("{}", line);
                if line.contains("inet ") {
                    let addr_s: Vec<&str> = line.split_whitespace().collect();
                    let addr: Vec<&str> = addr_s[1].split('/').collect();
                    return Some(IpAddr::V4(Ipv4Addr::from_str(addr[0]).unwrap()));
                }
                None
            })
            .collect();
        let mut intf_status_vec = Vec::new();
        for line in intf_list.lines() {
            if !line.starts_with(' ') && !line.is_empty() {
                let name_s: Vec<&str> = line.split(':').collect();
                let is_up = !line.contains("state DOWN");
                intf_status_vec.push((name_s[1].trim().to_string(), is_up));
            }
        }

        (ipaddr_list, intf_status_vec)
    }

    #[cfg(any(
        target_os = "freebsd",
        target_os = "netbsd",
        target_os = "openbsd",
        target_os = "illumos",
        all(
            target_vendor = "apple",
            any(
                target_os = "macos",
                target_os = "ios",
                target_os = "tvos",
                target_os = "watchos",
                target_os = "visionos"
            )
        )
    ))]
    /// Returns (IP-addr-list, interface-status-list)
    fn list_system_addrs() -> (Vec<IpAddr>, Vec<(String, bool)>) {
        let intf_list = list_system_interfaces("ifconfig", &[""]);
        let ipaddr_list = intf_list
            .lines()
            .filter_map(|line| {
                println!("{}", line);
                if line.contains("inet ") {
                    let addr_s: Vec<&str> = line.split_whitespace().collect();
                    return Some(IpAddr::V4(Ipv4Addr::from_str(addr_s[1]).unwrap()));
                }
                None
            })
            .collect();

        let mut intf_status_vec = Vec::new();
        for line in intf_list.lines() {
            // One example on macOS:
            /*
            en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
                options=6460<TSO4,TSO6,CHANNEL_IO,PARTIAL_CSUM,ZEROINVERT_CSUM>
                ether c6:0e:5e:f8:5d:f4
                inet6 fe80::c02:ea58:92f4:be76%en0 prefixlen 64 secured scopeid 0xb
                inet 192.168.0.112 netmask 0xffffff00 broadcast 192.168.0.255
                nd6 options=201<PERFORMNUD,DAD>
                media: autoselect
                status: active
             */
            if !line.starts_with('\t') && !line.is_empty() {
                let name_s: Vec<&str> = line.split(':').collect();
                let is_admin_up = line.contains("<UP");
                intf_status_vec.push((name_s[0].to_string(), is_admin_up));
            } else if line.contains("status: inactive") {
                if let Some(current_intf) = intf_status_vec.last_mut() {
                    current_intf.1 = false; // overwrite the admin up
                }
            }
        }

        (ipaddr_list, intf_status_vec)
    }

    #[test]
    fn test_get_if_addrs() {
        let ifaces = get_if_addrs().unwrap();
        println!("Local interfaces:");
        println!("{:#?}", ifaces);
        // at least one loop back address
        assert!(
            1 <= ifaces
                .iter()
                .filter(|interface| interface.is_loopback())
                .count()
        );
        // if index is set, it is non-zero
        for interface in &ifaces {
            if let Some(idx) = interface.index {
                assert!(idx > 0);
            }
        }

        // one address of IpV4(127.0.0.1)
        let is_loopback =
            |interface: &&Interface| interface.addr.ip() == IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
        assert_eq!(1, ifaces.iter().filter(is_loopback).count());

        // each system address shall be listed
        let (system_addrs, intf_status_list) = list_system_addrs();
        assert!(!system_addrs.is_empty());
        for addr in system_addrs {
            let mut listed = false;
            println!("\n checking whether {:?} has been properly listed \n", addr);
            for interface in &ifaces {
                if interface.addr.ip() == addr {
                    listed = true;
                }

                assert!(interface.index.is_some());
            }
            assert!(listed);
        }

        println!("Interface status list: {:#?}", intf_status_list);
        for (intf_name, is_up) in intf_status_list {
            for interface in &ifaces {
                if interface.name == intf_name {
                    if interface.is_oper_up() != is_up {
                        println!(
                            "Interface {} status mismatch: listed {}, detected {:?}",
                            intf_name, is_up, interface.oper_status
                        );
                    }
                    assert_eq!(interface.is_oper_up(), is_up);
                }
            }
        }
    }

    #[cfg(not(any(
        all(
            target_vendor = "apple",
            any(
                target_os = "macos",
                target_os = "ios",
                target_os = "tvos",
                target_os = "watchos",
                target_os = "visionos"
            )
        ),
        target_os = "freebsd",
        target_os = "netbsd",
        target_os = "openbsd",
        target_os = "illumos"
    )))]
    #[test]
    fn test_if_notifier() {
        // Check that the interface notifier can start up and time out. No easy
        // way to programmatically add/remove interfaces, so set a timeout of 0.
        // Will cover a potential case of inadequate setup leading to an
        // immediate change notification.
        //
        // There is a small race condition from creation -> check that an
        // interface change *actually* occurs, so this test may spuriously fail
        // extremely rarely.

        let notifier = crate::IfChangeNotifier::new();
        assert!(notifier.is_ok());
        let mut notifier = notifier.unwrap();

        assert!(notifier.wait(Some(Duration::ZERO)).is_err());
    }
}
