use clap::{ArgGroup, Parser, ValueEnum};

#[derive(Parser, Debug)]
#[command(about, author, long_about = None, name = "riperf3", version, disable_version_flag = true)]
#[command(group(
    ArgGroup::new("mode")
        .required(true)
        .args(&["server", "client"])
))]
#[command(group(
    ArgGroup::new("meta")
        .required(false)
        .args(&["help", "version"])
))]
pub struct Cli {
    // -----------------------------------------------------------------------
    // Common arguments
    // -----------------------------------------------------------------------
    /// Run in server mode
    #[arg(short, long, group = "mode")]
    pub server: bool,

    /// Run in client mode, connecting to <host>
    #[arg(short, long, group = "mode", value_name = "host")]
    pub client: Option<String>,

    /// Server port to listen on/connect to
    #[arg(short, long)]
    pub port: Option<u16>,

    /// Format to report: Kbits, Mbits, Gbits, Tbits
    #[arg(
        short,
        long,
        ignore_case = true,
        value_enum,
        value_name = "format",
        default_value = "m"
    )]
    pub format: Format,

    /// Seconds between periodic throughput reports
    #[arg(short, long, value_name = "interval")]
    pub interval: Option<f64>,

    /// Enable verbose output
    #[arg(short = 'V', long)]
    pub verbose: bool,

    /// Output in JSON format
    #[arg(short = 'J', long)]
    pub json: bool,

    /// Debug level 1-4 (default 4)
    #[arg(short, long, value_name = "level", num_args = 0..=1,
          value_parser = clap::value_parser!(u8).range(1..=4),
          default_missing_value = "4")]
    pub debug: Option<u8>,

    /// Print version
    #[arg(short = 'v', long, group = "meta", action = clap::ArgAction::Version)]
    pub version: Option<bool>,

    // -----------------------------------------------------------------------
    // Server-specific arguments
    // -----------------------------------------------------------------------
    /// Write PID to file
    #[arg(short = 'I', long, value_name = "file")]
    pub pidfile: Option<String>,

    /// Send output to a log file
    #[arg(long, value_name = "file")]
    pub logfile: Option<String>,

    /// Force flushing output at every interval
    #[arg(long)]
    pub forceflush: bool,

    /// Emit a timestamp at the start of each output line
    #[arg(long, value_name = "format", num_args = 0..=1, default_missing_value = "%c ")]
    pub timestamps: Option<String>,

    // -----------------------------------------------------------------------
    // Server-specific arguments
    // -----------------------------------------------------------------------
    /// Handle one client connection then exit
    #[arg(short = '1', long)]
    pub one_off: bool,

    /// Run the server as a daemon
    #[arg(short = 'D', long)]
    pub daemon: bool,

    /// Restart idle server after # seconds
    #[arg(long, value_name = "secs")]
    pub idle_timeout: Option<u32>,

    /// Server's total bit rate limit
    #[arg(long = "server-bitrate-limit", value_name = "rate")]
    pub server_bitrate_limit: Option<String>,

    /// Max time a test can run on the server
    #[arg(long = "server-max-duration", value_name = "secs")]
    pub server_max_duration: Option<u32>,

    // -----------------------------------------------------------------------
    // Client-specific arguments
    // -----------------------------------------------------------------------
    /// Use UDP rather than TCP
    #[arg(short = 'u', long)]
    pub udp: bool,

    /// Time in seconds to transmit for (default 10 secs)
    #[arg(short = 't', long, value_name = "secs")]
    pub time: Option<u32>,

    /// Number of bytes to transmit (instead of -t)
    #[arg(short = 'n', long, value_name = "bytes")]
    pub bytes: Option<String>,

    /// Number of blocks (packets) to transmit (instead of -t or -n)
    #[arg(short = 'k', long, value_name = "count")]
    pub blockcount: Option<String>,

    /// Length of buffer to read or write (default 128 KB for TCP; UDP tracks the connection MSS, else 1460)
    #[arg(short = 'l', long, value_name = "size")]
    pub length: Option<String>,

    /// Number of parallel client streams to run
    #[arg(short = 'P', long, value_name = "num")]
    pub parallel: Option<u32>,

    /// Reverse mode (server sends, client receives)
    #[arg(short = 'R', long)]
    pub reverse: bool,

    /// Bidirectional mode: client and server send and receive
    #[arg(long)]
    pub bidir: bool,

    /// Set socket buffer sizes (indirectly sets TCP window size)
    #[arg(short = 'w', long, value_name = "size")]
    pub window: Option<String>,

    /// Set TCP congestion control algorithm
    #[arg(short = 'C', long, value_name = "algo")]
    pub congestion: Option<String>,

    /// Set TCP/SCTP maximum segment size (MTU - 40 bytes)
    #[arg(short = 'M', long = "set-mss", value_name = "mss")]
    pub mss: Option<i32>,

    /// Disable Nagle's algorithm (set TCP_NODELAY)
    #[arg(short = 'N', long = "no-delay")]
    pub no_delay: bool,

    /// Target bitrate in bits/sec (0 = unlimited; default: unlimited TCP, 1M UDP)
    #[arg(short = 'b', long, value_name = "rate[/burst]")]
    pub bitrate: Option<String>,

    /// Set the IP type of service (0-255)
    #[arg(short = 'S', long, value_name = "tos")]
    pub tos: Option<i32>,

    /// Omit the first N seconds of the test
    #[arg(short = 'O', long, value_name = "secs")]
    pub omit: Option<u32>,

    /// Prefix every output line with this string
    #[arg(short = 'T', long, value_name = "title")]
    pub title: Option<String>,

    /// Extra data string to include in JSON output
    #[arg(long, value_name = "str")]
    pub extra_data: Option<String>,

    /// Timeout for control connection setup (ms)
    #[arg(long, value_name = "ms")]
    pub connect_timeout: Option<u64>,

    /// Get results from server
    #[arg(long)]
    pub get_server_output: bool,

    /// Use 64-bit counters in UDP test packets
    #[arg(long)]
    pub udp_counters_64bit: bool,

    /// Use repeating pattern in payload instead of zeros
    #[arg(long)]
    pub repeating_payload: bool,

    /// Set IPv4 Don't Fragment flag
    #[arg(long)]
    pub dont_fragment: bool,

    /// Bind to a specific client port
    #[arg(long, value_name = "port")]
    pub cport: Option<u16>,

    /// Set the server timing for pacing in microseconds (deprecated)
    #[arg(long, value_name = "usec")]
    pub pacing_timer: Option<u32>,

    /// Only use IPv4
    #[arg(short = '4', long, conflicts_with = "version6")]
    pub version4: bool,

    /// Only use IPv6
    #[arg(short = '6', long)]
    pub version6: bool,

    /// Bind to a local source address (device binding is `--bind-dev`)
    #[arg(short = 'B', long, value_name = "host")]
    pub bind: Option<String>,

    /// Bind to the network interface with SO_BINDTODEVICE
    #[arg(long, value_name = "dev")]
    pub bind_dev: Option<String>,

    /// Enable fair-queuing based socket pacing (bits/sec, Linux only)
    #[arg(long, value_name = "rate")]
    pub fq_rate: Option<String>,

    /// Set the IPv6 flow label (Linux only)
    #[arg(short = 'L', long, value_name = "N")]
    pub flowlabel: Option<i32>,

    /// Set the IP DSCP value (0-63 or symbolic)
    #[arg(long, value_name = "val")]
    pub dscp: Option<String>,

    /// Use MPTCP rather than plain TCP
    #[arg(short = 'm', long)]
    pub mptcp: bool,

    /// Use zero copy method of sending data
    #[arg(short = 'Z', long)]
    pub zerocopy: bool,

    /// Ignore received messages using MSG_TRUNC
    #[arg(long)]
    pub skip_rx_copy: bool,

    /// Idle timeout for receiving data (ms)
    #[arg(long, value_name = "ms")]
    pub rcv_timeout: Option<u64>,

    /// Timeout for unacknowledged TCP data (ms)
    #[arg(long, value_name = "ms")]
    pub snd_timeout: Option<u64>,

    /// Transmit/receive the specified file
    #[arg(short = 'F', long, value_name = "name")]
    pub file: Option<String>,

    /// Set CPU affinity
    #[arg(short = 'A', long, value_name = "n[,m]")]
    pub affinity: Option<String>,

    /// Output in line-delimited JSON format
    #[arg(long)]
    pub json_stream: bool,

    /// Enable UDP GSO/GRO
    #[arg(long)]
    pub gsro: bool,

    /// Use sendmmsg for batched UDP sends (experimental, Linux/FreeBSD/NetBSD)
    #[arg(long)]
    pub sendmmsg: bool,

    /// Use control connection TCP keepalive
    #[arg(long, value_name = "idle/intv/cnt")]
    pub cntl_ka: Option<String>,

    /// Username for authentication
    #[arg(long)]
    pub username: Option<String>,

    /// Path to RSA public key for authentication
    #[arg(long, value_name = "file")]
    pub rsa_public_key_path: Option<String>,

    /// Path to RSA private key for authentication (server)
    #[arg(long, value_name = "file")]
    pub rsa_private_key_path: Option<String>,

    /// Path to authorized users file (server)
    #[arg(long, value_name = "file")]
    pub authorized_users_path: Option<String>,

    /// Time skew threshold for authentication (seconds)
    #[arg(long, value_name = "secs")]
    pub time_skew_threshold: Option<u32>,

    /// Use PKCS#1 padding for authentication
    #[arg(long)]
    pub use_pkcs1_padding: bool,
}

impl Cli {
    /// Reject client-only options when running as a server (`-s`), matching
    /// iperf3 (#65). iperf3 sets an internal `client_flag` for each of these in
    /// `iperf_parse_arguments` and errors with IECLIENTONLY ("some option you are
    /// trying to set is client only") when `role == 's'`. riperf3 silently
    /// accepted and ignored them, which is a drop-in divergence.
    ///
    /// The set is adjudicated against iperf3's `iperf_api.c` (3.20) so riperf3
    /// rejects exactly what iperf3 rejects, no more:
    /// - Every option iperf3 marks `client_flag = 1`.
    /// - `--gsro`: iperf3 tracks this with a separate `gsro_flag` but applies the
    ///   same `role == 's'` -> IECLIENTONLY rejection, so it belongs here.
    /// - `--sendmmsg`: riperf3-only client transmit option with no iperf3
    ///   equivalent and no server meaning; rejecting it carries no drop-in risk.
    ///
    /// Deliberately NOT rejected, because iperf3 accepts them on a server:
    /// - `-m/--mptcp` and `--cport`: iperf3's `'m'` and `OPT_CLIENT_PORT` cases do
    ///   not set `client_flag`.
    /// - A bare `-A n`: iperf3 only sets `client_flag` for the two-arg `-A n,m`
    ///   form (where the first value is the *client* affinity); `-A n` alone is
    ///   the server's own CPU affinity. riperf3's server does not yet honor it,
    ///   but faithfulness means matching iperf3's accept/reject decision, not
    ///   erroring on a legal invocation.
    ///
    /// Returns the name of the first offending flag if one was given.
    pub fn first_client_only_violation(&self) -> Option<&'static str> {
        // iperf3 rejects `-A n,m` (the comma form carries a client affinity) but
        // accepts a bare `-A n` (server affinity). Mirror that distinction.
        let affinity_client_only = self
            .affinity
            .as_deref()
            .is_some_and(|spec| spec.contains(','));
        // (was-it-set, canonical flag name) — order is the report priority.
        let checks: [(bool, &'static str); 33] = [
            (self.udp, "-u/--udp"),
            (self.time.is_some(), "-t/--time"),
            (self.bytes.is_some(), "-n/--bytes"),
            (self.blockcount.is_some(), "-k/--blockcount"),
            (self.length.is_some(), "-l/--length"),
            (self.parallel.is_some(), "-P/--parallel"),
            (self.reverse, "-R/--reverse"),
            (self.bidir, "--bidir"),
            (self.window.is_some(), "-w/--window"),
            (self.congestion.is_some(), "-C/--congestion"),
            (self.mss.is_some(), "-M/--set-mss"),
            (self.no_delay, "-N/--no-delay"),
            (self.bitrate.is_some(), "-b/--bitrate"),
            (self.tos.is_some(), "-S/--tos"),
            (self.omit.is_some(), "-O/--omit"),
            (self.title.is_some(), "-T/--title"),
            (self.extra_data.is_some(), "--extra-data"),
            (self.connect_timeout.is_some(), "--connect-timeout"),
            (self.get_server_output, "--get-server-output"),
            (self.udp_counters_64bit, "--udp-counters-64bit"),
            (self.repeating_payload, "--repeating-payload"),
            (self.dont_fragment, "--dont-fragment"),
            (self.pacing_timer.is_some(), "--pacing-timer"),
            (self.zerocopy, "-Z/--zerocopy"),
            (self.skip_rx_copy, "--skip-rx-copy"),
            (self.fq_rate.is_some(), "--fq-rate"),
            (self.flowlabel.is_some(), "-L/--flowlabel"),
            (self.dscp.is_some(), "--dscp"),
            (affinity_client_only, "-A/--affinity"),
            (self.username.is_some(), "--username"),
            (self.rsa_public_key_path.is_some(), "--rsa-public-key-path"),
            (self.gsro, "--gsro"),
            (self.sendmmsg, "--sendmmsg"),
        ];
        checks.iter().find(|(set, _)| *set).map(|(_, name)| *name)
    }
}

#[derive(Debug, Clone, PartialEq, ValueEnum)]
pub enum Format {
    K,
    M,
    G,
    T,
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod cli_tests {
    use super::*;

    mod common_arg_tests {
        use super::*;

        #[test]
        fn test_common_defaults() {
            let cli = Cli::parse_from(["riperf3", "--server"]);
            assert!(cli.server);
            assert!(cli.client.is_none());
            assert_eq!(cli.port, None);
            assert_eq!(cli.format, Format::M);
            assert_eq!(cli.interval, None);
            assert!(!cli.verbose);
            assert_eq!(cli.debug, None);
            assert_eq!(cli.version, None);

            let cli = Cli::parse_from(["riperf3", "--client", "localhost"]);
            assert!(!cli.server);
            assert_eq!(cli.client, Some("localhost".to_string()));
            assert_eq!(cli.port, None);
            assert_eq!(cli.format, Format::M);
            assert_eq!(cli.interval, None);
            assert!(!cli.verbose);
            assert_eq!(cli.debug, None);
            assert_eq!(cli.version, None);
        }

        #[test]
        fn test_common_port() {
            let cli = Cli::parse_from(["riperf3", "--server", "--port", "1234"]);
            assert_eq!(cli.port, Some(1234));

            let cli = Cli::parse_from(["riperf3", "--client", "localhost", "--port", "1234"]);
            assert_eq!(cli.port, Some(1234));
        }

        #[test]
        fn test_common_format() {
            let cli = Cli::parse_from(["riperf3", "--server", "--format", "k"]);
            assert_eq!(cli.format, Format::K);

            let cli = Cli::parse_from(["riperf3", "--client", "localhost", "--format", "g"]);
            assert_eq!(cli.format, Format::G);

            let cli = Cli::parse_from(["riperf3", "--server", "--format", "t"]);
            assert_eq!(cli.format, Format::T);

            let cli = Cli::parse_from(["riperf3", "--client", "localhost", "--format", "m"]);
            assert_eq!(cli.format, Format::M);

            let cli = Cli::parse_from(["riperf3", "--server", "--format", "M"]);
            assert_eq!(cli.format, Format::M);

            let cli = Cli::parse_from(["riperf3", "--client", "localhost", "--format", "T"]);
            assert_eq!(cli.format, Format::T);

            let cli = Cli::parse_from(["riperf3", "--server", "--format", "G"]);
            assert_eq!(cli.format, Format::G);

            let cli = Cli::parse_from(["riperf3", "--client", "localhost", "--format", "K"]);
            assert_eq!(cli.format, Format::K);
        }
    }

    mod client_arg_tests {
        use super::*;

        #[test]
        fn test_client_flags() {
            let cli = Cli::parse_from([
                "riperf3", "-c", "10.0.0.1", "-u", "-t", "30", "-P", "4", "-R", "--bidir", "-N",
                "-l", "1460", "-b", "100M",
            ]);
            assert!(cli.udp);
            assert_eq!(cli.time, Some(30));
            assert_eq!(cli.parallel, Some(4));
            assert!(cli.reverse);
            assert!(cli.bidir);
            assert!(cli.no_delay);
            assert_eq!(cli.length, Some("1460".to_string()));
            assert_eq!(cli.bitrate, Some("100M".to_string()));
        }

        #[test]
        fn test_client_bytes_and_blocks() {
            let cli = Cli::parse_from(["riperf3", "-c", "host", "-n", "1G"]);
            assert_eq!(cli.bytes, Some("1G".to_string()));

            let cli = Cli::parse_from(["riperf3", "-c", "host", "-k", "100K"]);
            assert_eq!(cli.blockcount, Some("100K".to_string()));
        }

        #[test]
        fn test_client_window_mss_congestion() {
            let cli = Cli::parse_from([
                "riperf3", "-c", "host", "-w", "512K", "-M", "1400", "-C", "bbr",
            ]);
            assert_eq!(cli.window, Some("512K".to_string()));
            assert_eq!(cli.mss, Some(1400));
            assert_eq!(cli.congestion, Some("bbr".to_string()));
        }
    }

    mod server_arg_tests {
        use super::*;

        #[test]
        fn test_one_off() {
            let cli = Cli::parse_from(["riperf3", "-s", "-1"]);
            assert!(cli.one_off);
        }

        // #65: the server rejects client-only options, like iperf3.
        #[test]
        fn server_flags_client_only_violation() {
            // A bare server, and a server with only server/common-valid options,
            // are clean.
            assert_eq!(
                Cli::parse_from(["riperf3", "-s"]).first_client_only_violation(),
                None
            );
            let clean = Cli::parse_from([
                "riperf3",
                "-s",
                "-1",
                "-D",
                "-p",
                "5201",
                "-B",
                "0.0.0.0",
                "-i",
                "1",
                "-J",
                "-V",
                "--idle-timeout",
                "30",
                "--server-max-duration",
                "60",
                "--forceflush",
            ]);
            assert_eq!(clean.first_client_only_violation(), None);

            // Options iperf3 ACCEPTS on a server must not be flagged: a bare
            // `-A n` (server's own affinity), `--cport`, and `-m/--mptcp` all map
            // to iperf3 cases that do not set `client_flag`.
            for args in [
                vec!["-A", "3"],
                vec!["--affinity", "3"],
                vec!["--cport", "12345"],
                vec!["-m"],
                vec!["--mptcp"],
            ] {
                let mut argv = vec!["riperf3", "-s"];
                argv.extend(args.iter().copied());
                let cli = Cli::parse_from(&argv);
                assert_eq!(
                    cli.first_client_only_violation(),
                    None,
                    "expected {args:?} accepted on the server (iperf3 parity)"
                );
            }

            // Each client-only option is flagged on the server. `-A n,m` is
            // client-only (the comma form carries a client affinity); `--gsro`
            // maps to iperf3's `gsro_flag` reject; `--sendmmsg` is riperf3-only.
            for (args, want) in [
                (vec!["-t", "5"], "-t/--time"),
                (vec!["-u"], "-u/--udp"),
                (vec!["-R"], "-R/--reverse"),
                (vec!["--bidir"], "--bidir"),
                (vec!["-P", "4"], "-P/--parallel"),
                (vec!["-b", "100M"], "-b/--bitrate"),
                (vec!["--extra-data", "x"], "--extra-data"),
                (vec!["-w", "1M"], "-w/--window"),
                (vec!["--get-server-output"], "--get-server-output"),
                (vec!["-Z"], "-Z/--zerocopy"),
                (vec!["-A", "3,4"], "-A/--affinity"),
                (vec!["--gsro"], "--gsro"),
                (vec!["--sendmmsg"], "--sendmmsg"),
            ] {
                let mut argv = vec!["riperf3", "-s"];
                argv.extend(args.iter().copied());
                let cli = Cli::parse_from(&argv);
                assert_eq!(
                    cli.first_client_only_violation(),
                    Some(want),
                    "expected {want} flagged for args {args:?}"
                );
            }
        }
    }

    /// Tests that CLI flags are correctly wired through to Client/Server
    /// fields, matching the logic in main.rs. This is the test class that
    /// would have caught the -J flag not being wired up.
    mod cli_wiring_tests {
        use super::*;
        use riperf3::TransportProtocol;

        /// Simulate the main.rs client builder wiring for a parsed CLI.
        fn build_client_from_cli(cli: &Cli) -> riperf3::Client {
            let host = cli.client.as_ref().unwrap();
            let mut b = riperf3::ClientBuilder::new(host);
            if let Some(port) = cli.port {
                b = b.port(Some(port));
            }
            if cli.udp {
                b = b.protocol(TransportProtocol::Udp);
            }
            if let Some(t) = cli.time {
                b = b.duration(t);
            }
            if let Some(ref s) = cli.bytes {
                b = b.bytes_str(s).unwrap();
            }
            if let Some(ref s) = cli.blockcount {
                b = b.blocks_str(s).unwrap();
            }
            if let Some(ref s) = cli.length {
                b = b.blksize_str(s).unwrap();
            }
            if let Some(n) = cli.parallel {
                b = b.num_streams(n);
            }
            if cli.reverse {
                b = b.reverse(true);
            }
            if cli.bidir {
                b = b.bidir(true);
            }
            if let Some(ref s) = cli.window {
                b = b.window_str(s).unwrap();
            }
            if let Some(ref algo) = cli.congestion {
                b = b.congestion(algo);
            }
            if let Some(mss) = cli.mss {
                b = b.mss(mss);
            }
            if cli.no_delay {
                b = b.no_delay(true);
            }
            if let Some(ref s) = cli.bitrate {
                b = b.bandwidth_str(s).unwrap();
            }
            if let Some(tos) = cli.tos {
                b = b.tos(tos);
            }
            if let Some(o) = cli.omit {
                b = b.omit(o);
            }
            if let Some(i) = cli.interval {
                b = b.interval(i);
            }
            if let Some(ref t) = cli.title {
                b = b.title(t);
            }
            if let Some(ref d) = cli.extra_data {
                b = b.extra_data(d);
            }
            if let Some(ms) = cli.connect_timeout {
                b = b.connect_timeout(std::time::Duration::from_millis(ms));
            }
            if cli.verbose {
                b = b.verbose(true);
            }
            if cli.json {
                b = b.json_output(true);
            }
            if cli.json_stream {
                b = b.json_stream(true);
            }
            if cli.udp_counters_64bit {
                b = b.udp_counters_64bit(true);
            }
            if cli.repeating_payload {
                b = b.repeating_payload(true);
            }
            if cli.zerocopy {
                b = b.zerocopy(true);
            }
            if cli.gsro {
                b = b.gsro(true);
            }
            if cli.sendmmsg {
                b = b.sendmmsg(true);
            }
            if cli.dont_fragment {
                b = b.dont_fragment(true);
            }
            if let Some(port) = cli.cport {
                b = b.cport(port);
            }
            if cli.get_server_output {
                b = b.get_server_output(true);
            }
            if cli.forceflush {
                b = b.forceflush(true);
            }
            if let Some(ref fmt) = cli.timestamps {
                b = b.timestamps(fmt);
            }
            if let Some(ref addr) = cli.bind {
                b = b.bind_address(addr);
            }
            if let Some(ref dev) = cli.bind_dev {
                b = b.bind_dev(dev);
            }
            if let Some(ref s) = cli.fq_rate {
                b = b.fq_rate_str(s).unwrap();
            }
            if let Some(label) = cli.flowlabel {
                b = b.flowlabel(label);
            }
            if cli.version4 {
                b = b.ip_version(4);
            }
            if cli.version6 {
                b = b.ip_version(6);
            }
            if cli.mptcp {
                b = b.mptcp(true);
            }
            if cli.skip_rx_copy {
                b = b.skip_rx_copy(true);
            }
            if let Some(ms) = cli.rcv_timeout {
                b = b.rcv_timeout(ms);
            }
            if let Some(ms) = cli.snd_timeout {
                b = b.snd_timeout(ms);
            }
            if let Some(ref path) = cli.file {
                b = b.file(path);
            }
            if let Some(ref spec) = cli.cntl_ka {
                b = b.cntl_ka(spec);
            }
            if let Some(ref val) = cli.dscp {
                b = b.dscp(val);
            }
            b.build().unwrap()
        }

        #[test]
        fn json_flag_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "host", "-J"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("host")
                    .json_output(true)
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn udp_flag_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "host", "-u"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("host")
                    .protocol(TransportProtocol::Udp)
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn duration_flag_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "host", "-t", "30"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("host")
                    .duration(30)
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn bytes_flag_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "host", "-n", "1M"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("host")
                    .bytes_str("1M")
                    .unwrap()
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn blockcount_flag_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "host", "-k", "100"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("host")
                    .blocks_str("100")
                    .unwrap()
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn length_flag_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "host", "-l", "64K"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("host")
                    .blksize_str("64K")
                    .unwrap()
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn parallel_flag_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "host", "-P", "8"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("host")
                    .num_streams(8)
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn reverse_flag_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "host", "-R"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("host")
                    .reverse(true)
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn bidir_flag_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "host", "--bidir"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("host")
                    .bidir(true)
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn window_flag_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "host", "-w", "512K"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("host")
                    .window_str("512K")
                    .unwrap()
                    .build()
                    .unwrap()
            );
        }

        // build() rejects -C/--congestion (TCP congestion control) on non-Unix
        // (cfg(not(unix)) → Unsupported), so gate the wiring test to match.
        #[cfg(unix)]
        #[test]
        fn congestion_flag_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "host", "-C", "bbr"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("host")
                    .congestion("bbr")
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn mss_flag_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "host", "-M", "1400"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("host")
                    .mss(1400)
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn no_delay_flag_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "host", "-N"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("host")
                    .no_delay(true)
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn bitrate_flag_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "host", "-b", "100M"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("host")
                    .bandwidth_str("100M")
                    .unwrap()
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn tos_flag_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "host", "-S", "16"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("host").tos(16).build().unwrap()
            );
        }

        #[test]
        fn omit_flag_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "host", "-O", "3"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("host").omit(3).build().unwrap()
            );
        }

        #[test]
        fn title_flag_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "host", "-T", "my test"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("host")
                    .title("my test")
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn extra_data_flag_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "host", "--extra-data", "abc"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("host")
                    .extra_data("abc")
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn connect_timeout_flag_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "host", "--connect-timeout", "500"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("host")
                    .connect_timeout(std::time::Duration::from_millis(500))
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn verbose_flag_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "host", "-V"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("host")
                    .verbose(true)
                    .build()
                    .unwrap()
            );
        }

        // Combines the Unix-only flags above (congestion/bind-dev/affinity),
        // which build() rejects on non-Unix, so gate to match.
        #[cfg(unix)]
        #[test]
        fn all_flags_combined() {
            let cli = Cli::parse_from([
                "riperf3",
                "-c",
                "host",
                "-u",
                "-t",
                "30",
                "-P",
                "4",
                "-R",
                "--bidir",
                "-N",
                "-l",
                "1460",
                "-b",
                "100M",
                "-J",
                "-V",
                "-w",
                "512K",
                "-M",
                "1400",
                "-C",
                "bbr",
                "-S",
                "16",
                "-O",
                "2",
                "-T",
                "test",
                "--extra-data",
                "x",
                "--connect-timeout",
                "500",
            ]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("host")
                    .protocol(TransportProtocol::Udp)
                    .duration(30)
                    .num_streams(4)
                    .reverse(true)
                    .bidir(true)
                    .no_delay(true)
                    .blksize_str("1460")
                    .unwrap()
                    .bandwidth_str("100M")
                    .unwrap()
                    .json_output(true)
                    .verbose(true)
                    .window_str("512K")
                    .unwrap()
                    .mss(1400)
                    .congestion("bbr")
                    .tos(16)
                    .omit(2)
                    .title("test")
                    .extra_data("x")
                    .connect_timeout(std::time::Duration::from_millis(500))
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn server_one_off_wired() {
            let cli = Cli::parse_from(["riperf3", "-s", "-1"]);
            let mut b = riperf3::ServerBuilder::new();
            if cli.one_off {
                b = b.one_off(true);
            }
            let s = b.build().unwrap();
            assert_eq!(
                s,
                riperf3::ServerBuilder::new().one_off(true).build().unwrap()
            );
        }

        // -- New flag wiring tests --

        #[test]
        fn udp_counters_64bit_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "h", "--udp-counters-64bit"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("h")
                    .udp_counters_64bit(true)
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn repeating_payload_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "h", "--repeating-payload"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("h")
                    .repeating_payload(true)
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn dont_fragment_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "h", "--dont-fragment"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("h")
                    .dont_fragment(true)
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn cport_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "h", "--cport", "12345"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("h")
                    .cport(12345)
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn get_server_output_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "h", "--get-server-output"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("h")
                    .get_server_output(true)
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn forceflush_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "h", "--forceflush"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("h")
                    .forceflush(true)
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn timestamps_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "h", "--timestamps", "%H:%M"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("h")
                    .timestamps("%H:%M")
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn version4_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "h", "-4"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("h")
                    .ip_version(4)
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn version6_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "h", "-6"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("h")
                    .ip_version(6)
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn version4_and_version6_conflict() {
            // -4 and -6 are mutually exclusive (matches iperf3).
            let err = Cli::try_parse_from(["riperf3", "-c", "h", "-4", "-6"]);
            assert!(err.is_err(), "-4 -6 together should be rejected");
        }

        #[test]
        fn bind_address_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "h", "-B", "10.0.0.1"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("h")
                    .bind_address("10.0.0.1")
                    .build()
                    .unwrap()
            );
        }

        // build() rejects --bind-dev (SO_BINDTODEVICE/IP_BOUND_IF) on non-Unix,
        // so gate the wiring test to match.
        #[cfg(unix)]
        #[test]
        fn bind_dev_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "h", "--bind-dev", "eth0"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("h")
                    .bind_dev("eth0")
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn fq_rate_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "h", "--fq-rate", "1G"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("h")
                    .fq_rate_str("1G")
                    .unwrap()
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn flowlabel_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "h", "-L", "42"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("h")
                    .flowlabel(42)
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn dscp_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "h", "--dscp", "46"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("h").dscp("46").build().unwrap()
            );
        }

        #[test]
        fn mptcp_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "h", "-m"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("h")
                    .mptcp(true)
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn skip_rx_copy_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "h", "--skip-rx-copy"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("h")
                    .skip_rx_copy(true)
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn rcv_timeout_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "h", "--rcv-timeout", "5000"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("h")
                    .rcv_timeout(5000)
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn snd_timeout_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "h", "--snd-timeout", "3000"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("h")
                    .snd_timeout(3000)
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn file_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "h", "-F", "/tmp/data"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("h")
                    .file("/tmp/data")
                    .build()
                    .unwrap()
            );
        }

        // affinity/pidfile/logfile builder setters were pruned (#122) — the CLI
        // realizes those at the process level, not via the builder.

        #[test]
        fn json_stream_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "h", "--json-stream"]);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("h")
                    .json_stream(true)
                    .build()
                    .unwrap()
            );
        }

        // sendmmsg(2) is Linux/FreeBSD/NetBSD-only (stream.rs); build() rejects
        // --sendmmsg on other platforms, so gate the wiring test to match (#76).
        #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))]
        #[test]
        fn sendmmsg_flag_wired() {
            let cli = Cli::parse_from(["riperf3", "-c", "h", "-u", "--sendmmsg"]);
            assert!(cli.sendmmsg);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("h")
                    .protocol(TransportProtocol::Udp)
                    .sendmmsg(true)
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn sendmmsg_default_false() {
            let cli = Cli::parse_from(["riperf3", "-c", "h", "-u"]);
            assert!(!cli.sendmmsg);
            let c = build_client_from_cli(&cli);
            assert_eq!(
                c,
                riperf3::ClientBuilder::new("h")
                    .protocol(TransportProtocol::Udp)
                    .build()
                    .unwrap()
            );
        }

        // Server new flags
        #[test]
        fn server_daemon_flag_parses() {
            // `-D`/`--daemon` is consumed by the binary (it daemonizes before the
            // tokio runtime is built — see main.rs / #81), not by ServerBuilder,
            // so there's nothing to wire through the builder; just assert parse.
            let cli = Cli::parse_from(["riperf3", "-s", "-D"]);
            assert!(cli.daemon);
            let long = Cli::parse_from(["riperf3", "-s", "--daemon"]);
            assert!(long.daemon);
        }

        #[test]
        fn server_idle_timeout_wired() {
            let cli = Cli::parse_from(["riperf3", "-s", "--idle-timeout", "30"]);
            let mut b = riperf3::ServerBuilder::new();
            if let Some(secs) = cli.idle_timeout {
                b = b.idle_timeout(secs);
            }
            let s = b.build().unwrap();
            assert_eq!(
                s,
                riperf3::ServerBuilder::new()
                    .idle_timeout(30)
                    .build()
                    .unwrap()
            );
        }

        #[test]
        fn server_max_duration_wired() {
            let cli = Cli::parse_from(["riperf3", "-s", "--server-max-duration", "60"]);
            let mut b = riperf3::ServerBuilder::new();
            if let Some(secs) = cli.server_max_duration {
                b = b.server_max_duration(secs);
            }
            let s = b.build().unwrap();
            assert_eq!(
                s,
                riperf3::ServerBuilder::new()
                    .server_max_duration(60)
                    .build()
                    .unwrap()
            );
        }

        /// Verify every short flag alias parses identically to its long form.
        /// Clap guarantees this, but this test catches typos in the arg declarations.
        #[test]
        fn short_and_long_flags_equivalent() {
            // (short_args, long_args, description)
            let pairs: Vec<(&[&str], &[&str], &str)> = vec![
                // Boolean client flags
                (&["-c", "h", "-u"], &["-c", "h", "--udp"], "udp"),
                (&["-c", "h", "-R"], &["-c", "h", "--reverse"], "reverse"),
                (&["-c", "h", "-N"], &["-c", "h", "--no-delay"], "no-delay"),
                (&["-c", "h", "-Z"], &["-c", "h", "--zerocopy"], "zerocopy"),
                (&["-c", "h", "-m"], &["-c", "h", "--mptcp"], "mptcp"),
                (&["-c", "h", "-J"], &["-c", "h", "--json"], "json"),
                (&["-c", "h", "-V"], &["-c", "h", "--verbose"], "verbose"),
                (&["-c", "h", "-4"], &["-c", "h", "--version4"], "version4"),
                (&["-c", "h", "-6"], &["-c", "h", "--version6"], "version6"),
                // Value client flags
                (&["-c", "h", "-t", "5"], &["-c", "h", "--time", "5"], "time"),
                (
                    &["-c", "h", "-n", "1M"],
                    &["-c", "h", "--bytes", "1M"],
                    "bytes",
                ),
                (
                    &["-c", "h", "-k", "10"],
                    &["-c", "h", "--blockcount", "10"],
                    "blockcount",
                ),
                (
                    &["-c", "h", "-l", "8K"],
                    &["-c", "h", "--length", "8K"],
                    "length",
                ),
                (
                    &["-c", "h", "-P", "4"],
                    &["-c", "h", "--parallel", "4"],
                    "parallel",
                ),
                (
                    &["-c", "h", "-w", "1M"],
                    &["-c", "h", "--window", "1M"],
                    "window",
                ),
                (
                    &["-c", "h", "-C", "bbr"],
                    &["-c", "h", "--congestion", "bbr"],
                    "congestion",
                ),
                (
                    &["-c", "h", "-M", "1400"],
                    &["-c", "h", "--set-mss", "1400"],
                    "set-mss",
                ),
                (
                    &["-c", "h", "-b", "1G"],
                    &["-c", "h", "--bitrate", "1G"],
                    "bitrate",
                ),
                (&["-c", "h", "-S", "16"], &["-c", "h", "--tos", "16"], "tos"),
                (&["-c", "h", "-O", "3"], &["-c", "h", "--omit", "3"], "omit"),
                (
                    &["-c", "h", "-T", "hi"],
                    &["-c", "h", "--title", "hi"],
                    "title",
                ),
                (
                    &["-c", "h", "-B", "lo"],
                    &["-c", "h", "--bind", "lo"],
                    "bind",
                ),
                (
                    &["-c", "h", "-L", "42"],
                    &["-c", "h", "--flowlabel", "42"],
                    "flowlabel",
                ),
                (
                    &["-c", "h", "-F", "/tmp/f"],
                    &["-c", "h", "--file", "/tmp/f"],
                    "file",
                ),
                (
                    &["-c", "h", "-A", "0"],
                    &["-c", "h", "--affinity", "0"],
                    "affinity",
                ),
                (
                    &["-c", "h", "-I", "/tmp/p"],
                    &["-c", "h", "--pidfile", "/tmp/p"],
                    "pidfile",
                ),
                // Server flags
                (&["-s", "-1"], &["-s", "--one-off"], "one-off"),
                (&["-s", "-D"], &["-s", "--daemon"], "daemon"),
            ];

            for (short_args, long_args, desc) in &pairs {
                let mut s: Vec<&str> = vec!["riperf3"];
                s.extend_from_slice(short_args);
                let mut l: Vec<&str> = vec!["riperf3"];
                l.extend_from_slice(long_args);

                let short = Cli::parse_from(&s);
                let long = Cli::parse_from(&l);

                // Compare the fields that matter for each flag
                assert_eq!(
                    format!("{:?}", short),
                    format!("{:?}", long),
                    "short vs long mismatch for '{desc}': {short_args:?} vs {long_args:?}"
                );
            }
        }
    }
}
