//! POSIX-style argument parser.
//!
//! Handles: flag bundling (`-abc`), stuck values (`-oval`),
//! separated values (`-o val`), `--` terminator, and `+` polarity.

use crate::error::Error;
use crate::polarity::Polarity;
use crate::style::Style;

// ── Public types ────────────────────────────────────────────────

/// How to handle unrecognized flag characters.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[expect(clippy::exhaustive_enums, reason = "two-state behavioral switch")]
pub enum OnUnknown {
    /// Return an error (standard POSIX behavior).
    #[default]
    Reject,
    /// Treat the entire arg as an operand (echo-style).
    PassThrough,
}

/// Classification of a declared flag for the parser.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum FlagKind {
    /// Boolean toggle — presence means true.
    Bool,
    /// Takes a value (next arg or inline remainder).
    Value,
    /// Polarity-aware — tracks `-x` vs `+x`.
    Polar,
    /// Polarity-aware and takes a value.
    PolarValue,
    /// Accepted but ignored (no-op).
    Noop,
}

/// A declared flag that the parser recognizes.
#[derive(Debug, Clone)]
#[expect(clippy::exhaustive_structs, reason = "static config generated by derive")]
pub struct FlagDef {
    // TODO(ecmd-5.4): long-only options require `ch` to become optional.
    /// The single-character flag (e.g., `'v'` for `-v`).
    pub ch: char,
    /// The GNU long name without dashes (e.g., "verbose"); empty if short-only.
    pub long: &'static str,
    /// What kind of flag this is.
    pub kind: FlagKind,
    /// Flags cleared when this one fires (last-wins mutex).
    pub clears: &'static [char],
    /// Human-readable description (from doc comment).
    pub desc: &'static str,
    /// Value placeholder (empty for bool flags, e.g. "PATH" for valued).
    pub value_name: &'static str,
}

/// Result of parsing a single flag occurrence.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Parsed {
    /// A boolean flag was set.
    Bool(char),
    /// A valued flag with its argument.
    Value(char, String),
    /// A polarity flag was set.
    Polar(char, Polarity),
    /// A polarity flag with a value.
    PolarValue(char, Polarity, String),
}

/// Output of a successful `scan` call.
#[derive(Debug, Clone, PartialEq, Eq)]
#[expect(clippy::exhaustive_structs, reason = "result of scan")]
pub struct ScanResult {
    /// Parsed flag occurrences in order.
    pub flags: Vec<Parsed>,
    /// Remaining positional operands.
    pub operands: Vec<String>,
}

// ── Main entry point ────────────────────────────────────────────

/// Parse arguments according to declared flags.
///
/// # Errors
///
/// Returns an error if an unknown flag is encountered (when
/// `on_unknown` is `Reject`) or a valued flag is missing its argument.
pub fn scan(
    args: &[&str],
    flags: &[FlagDef],
    on_unknown: OnUnknown,
    style: Style,
) -> Result<ScanResult, Error> {
    let mut result = ScanResult { flags: Vec::new(), operands: Vec::new() };
    let has_polarity = flags.iter().any(|f| is_polar(&f.kind));
    let gnu = style == Style::Gnu;
    let mut cursor = Cursor::new(args);

    while let Some(arg) = cursor.peek() {
        if arg == "--" {
            cursor.advance();
            break;
        }
        if gnu && is_long(arg) {
            let spec = arg.strip_prefix("--").unwrap_or_default();
            process_long(spec, &mut cursor, flags, &mut result)?;
            continue;
        }
        match classify(arg, has_polarity) {
            ArgClass::Flags(polarity, chars) => {
                process_cluster(
                    chars, polarity, &mut cursor, flags, on_unknown,
                    &mut result,
                )?;
            }
            ArgClass::Operand if gnu => {
                result.operands.push(arg.to_owned());
                cursor.advance();
            }
            ArgClass::Operand => break,
        }
    }

    result.operands.extend(cursor.rest().iter().map(|s| (*s).to_owned()));
    Ok(result)
}

/// A `--name` or `--name=value` token (but not the bare `--` terminator).
fn is_long(arg: &str) -> bool {
    arg.len() > 2 && arg.starts_with("--")
}

// ── Cursor ──────────────────────────────────────────────────────

struct Cursor<'a> {
    args: &'a [&'a str],
    pos: usize,
}

impl<'a> Cursor<'a> {
    const fn new(args: &'a [&'a str]) -> Self {
        Self { args, pos: 0 }
    }

    fn peek(&self) -> Option<&'a str> {
        self.args.get(self.pos).copied()
    }

    const fn advance(&mut self) {
        self.pos = self.pos.saturating_add(1);
    }

    fn next_value(&mut self, flag_ch: char) -> Result<String, Error> {
        self.advance();
        self.take_next(&format!("-{flag_ch}"))
    }

    fn take_next(&mut self, opt: &str) -> Result<String, Error> {
        let val = self.peek()
            .map(ToOwned::to_owned)
            .ok_or_else(|| Error::MissingValue(opt.to_owned()))?;
        self.advance();
        Ok(val)
    }

    fn rest(&self) -> &'a [&'a str] {
        self.args.get(self.pos..).unwrap_or_default()
    }
}

// ── Classification ──────────────────────────────────────────────

const fn is_polar(kind: &FlagKind) -> bool {
    matches!(kind, FlagKind::Polar | FlagKind::PolarValue)
}

enum ArgClass<'a> {
    Flags(Polarity, &'a str),
    Operand,
}

fn classify(arg: &str, has_polarity: bool) -> ArgClass<'_> {
    let Some((&first, rest)) = arg.as_bytes().split_first() else {
        return ArgClass::Operand;
    };
    if rest.is_empty() {
        return ArgClass::Operand;
    }
    let chars = arg.get(1..).unwrap_or_default();
    match first {
        b'-' => ArgClass::Flags(Polarity::On, chars),
        b'+' if has_polarity => ArgClass::Flags(Polarity::Off, chars),
        _ => ArgClass::Operand,
    }
}

fn find_flag(ch: char, flags: &[FlagDef]) -> Option<&FlagDef> {
    flags.iter().find(|f| f.ch == ch)
}

// ── Cluster processing ──────────────────────────────────────────

fn process_cluster(
    chars: &str,
    polarity: Polarity,
    cursor: &mut Cursor<'_>,
    flags: &[FlagDef],
    on_unknown: OnUnknown,
    result: &mut ScanResult,
) -> Result<(), Error> {
    if try_passthrough(chars, cursor, flags, on_unknown, &mut result.operands) {
        return Ok(());
    }
    parse_known_cluster(chars, polarity, cursor, flags, result)
}

fn try_passthrough(
    chars: &str,
    cursor: &mut Cursor<'_>,
    flags: &[FlagDef],
    on_unknown: OnUnknown,
    operands: &mut Vec<String>,
) -> bool {
    if on_unknown != OnUnknown::PassThrough {
        return false;
    }
    if !has_unknown_flag(chars, flags) {
        return false;
    }
    if let Some(full_arg) = cursor.peek() {
        operands.push(full_arg.to_owned());
    }
    cursor.advance();
    true
}

fn has_unknown_flag(chars: &str, flags: &[FlagDef]) -> bool {
    for &b in chars.as_bytes() {
        match find_flag(char::from(b), flags) {
            None => return true,
            Some(def) if matches!(def.kind, FlagKind::Value | FlagKind::PolarValue) => {
                return false;
            }
            _ => {}
        }
    }
    false
}

fn parse_known_cluster(
    chars: &str,
    polarity: Polarity,
    cursor: &mut Cursor<'_>,
    flags: &[FlagDef],
    result: &mut ScanResult,
) -> Result<(), Error> {
    for (bi, &byte) in chars.as_bytes().iter().enumerate() {
        let ch = char::from(byte);
        let def = find_flag(ch, flags)
            .ok_or_else(|| Error::UnknownFlag(format!("-{ch}")))?;

        match def.kind {
            FlagKind::Bool => result.flags.push(Parsed::Bool(ch)),
            FlagKind::Noop => {}
            FlagKind::Polar => result.flags.push(Parsed::Polar(ch, polarity)),
            FlagKind::Value | FlagKind::PolarValue => {
                let value = extract_value(chars, bi, cursor, ch)?;
                let parsed = match def.kind {
                    FlagKind::Value => Parsed::Value(ch, value),
                    _ => Parsed::PolarValue(ch, polarity, value),
                };
                result.flags.push(parsed);
                return Ok(());
            }
        }
    }
    cursor.advance();
    Ok(())
}

fn extract_value(
    chars: &str,
    byte_pos: usize,
    cursor: &mut Cursor<'_>,
    flag_ch: char,
) -> Result<String, Error> {
    let after = byte_pos.saturating_add(1);
    let remainder = chars.get(after..).unwrap_or_default();
    if !remainder.is_empty() {
        cursor.advance();
        return Ok(remainder.to_owned());
    }
    cursor.next_value(flag_ch)
}

// ── Long-option processing (GNU style) ──────────────────────────

/// Outcome of resolving a long-option name against the declared flags.
enum LongMatch<'a> {
    Flag(&'a FlagDef),
    Help,
    Version,
    Ambiguous,
    Unknown,
}

/// Parse one `--name` / `--name=value` token. Cursor is on that token.
fn process_long(
    spec: &str,
    cursor: &mut Cursor<'_>,
    flags: &[FlagDef],
    result: &mut ScanResult,
) -> Result<(), Error> {
    let (name, inline) = spec.split_once('=').map_or((spec, None), |(n, v)| (n, Some(v)));
    cursor.advance();
    match resolve_long(name, flags) {
        LongMatch::Flag(def) => apply_long(def, name, inline, cursor, result),
        LongMatch::Help => Err(Error::HelpRequested),
        LongMatch::Version => Err(Error::VersionRequested),
        LongMatch::Ambiguous => Err(Error::AmbiguousOption(format!("--{name}"))),
        LongMatch::Unknown => Err(Error::UnknownFlag(format!("--{name}"))),
    }
}

/// Record a resolved long flag, pulling a value from `=inline` or the next arg.
fn apply_long(
    def: &FlagDef,
    name: &str,
    inline: Option<&str>,
    cursor: &mut Cursor<'_>,
    result: &mut ScanResult,
) -> Result<(), Error> {
    match def.kind {
        FlagKind::Noop => Ok(()),
        FlagKind::Bool | FlagKind::Polar => apply_long_flag(def, name, inline, result),
        FlagKind::Value | FlagKind::PolarValue => apply_long_value(def, name, inline, cursor, result),
    }
}

/// A long bool/polar flag: presence sets it; an inline `=value` is an error.
fn apply_long_flag(
    def: &FlagDef,
    name: &str,
    inline: Option<&str>,
    result: &mut ScanResult,
) -> Result<(), Error> {
    if inline.is_some() {
        return Err(Error::UnexpectedValue(format!("--{name}")));
    }
    result.flags.push(if matches!(def.kind, FlagKind::Bool) {
        Parsed::Bool(def.ch)
    } else {
        Parsed::Polar(def.ch, Polarity::On)
    });
    Ok(())
}

/// A long value flag: value comes from `=inline` or the next token.
fn apply_long_value(
    def: &FlagDef,
    name: &str,
    inline: Option<&str>,
    cursor: &mut Cursor<'_>,
    result: &mut ScanResult,
) -> Result<(), Error> {
    // GNU getopt_long parity: a value flag consumes the next token even when it looks like an option.
    let value = match inline {
        Some(v) => v.to_owned(),
        None => cursor.take_next(&format!("--{name}"))?,
    };
    result.flags.push(if matches!(def.kind, FlagKind::Value) {
        Parsed::Value(def.ch, value)
    } else {
        Parsed::PolarValue(def.ch, Polarity::On, value)
    });
    Ok(())
}

/// Exact match wins; otherwise fall back to unambiguous-prefix inference.
fn resolve_long<'a>(name: &str, flags: &'a [FlagDef]) -> LongMatch<'a> {
    if !name.is_empty()
        && let Some(def) = flags.iter().find(|f| f.long == name)
    {
        return LongMatch::Flag(def);
    }
    match name {
        "help" => LongMatch::Help,
        "version" => LongMatch::Version,
        _ => infer_long(name, flags),
    }
}

/// Collect every declared long (plus reserved help/version) prefixed by `name`.
fn infer_long<'a>(name: &str, flags: &'a [FlagDef]) -> LongMatch<'a> {
    if name.is_empty() {
        return LongMatch::Unknown;
    }
    let mut found = LongMatch::Unknown;
    let mut count = 0_u32;
    for def in flags.iter().filter(|f| !f.long.is_empty() && f.long.starts_with(name)) {
        found = LongMatch::Flag(def);
        count = count.saturating_add(1);
    }
    for (reserved, hit) in [("help", LongMatch::Help), ("version", LongMatch::Version)] {
        // A flag that owns this exact long name shadows the reserved option.
        if reserved.starts_with(name) && !flags.iter().any(|f| f.long == reserved) {
            found = hit;
            count = count.saturating_add(1);
        }
    }
    match count {
        1 => found,
        0 => LongMatch::Unknown,
        _ => LongMatch::Ambiguous,
    }
}

#[cfg(test)]
#[expect(clippy::unwrap_used, reason = "tests verify success paths")]
mod tests {
    use pretty_assertions::assert_eq;

    use super::{FlagDef, FlagKind, OnUnknown, Parsed, scan};
    use crate::error::Error;
    use crate::polarity::Polarity;
    use crate::style::Style;

    fn bool_flag(ch: char) -> FlagDef {
        FlagDef { ch, kind: FlagKind::Bool, long: "", clears: &[], desc: "", value_name: "" }
    }

    fn value_flag(ch: char) -> FlagDef {
        FlagDef { ch, kind: FlagKind::Value, long: "", clears: &[], desc: "", value_name: "" }
    }

    fn polar_flag(ch: char) -> FlagDef {
        FlagDef { ch, kind: FlagKind::Polar, long: "", clears: &[], desc: "", value_name: "" }
    }

    fn noop_flag(ch: char) -> FlagDef {
        FlagDef { ch, kind: FlagKind::Noop, long: "", clears: &[], desc: "", value_name: "" }
    }

    #[test]
    fn empty_args_returns_empty() {
        let r = scan(&[], &[], OnUnknown::Reject, Style::Posix).unwrap();
        assert!(r.flags.is_empty());
        assert!(r.operands.is_empty());
    }

    #[test]
    fn single_bool_flag() {
        let flags = [bool_flag('v')];
        let r = scan(&["-v"], &flags, OnUnknown::Reject, Style::Posix).unwrap();
        assert_eq!(r.flags, [Parsed::Bool('v')]);
        assert!(r.operands.is_empty());
    }

    #[test]
    fn bundled_bool_flags() {
        let flags = [bool_flag('a'), bool_flag('b'), bool_flag('c')];
        let r = scan(&["-abc"], &flags, OnUnknown::Reject, Style::Posix).unwrap();
        assert_eq!(r.flags, [
            Parsed::Bool('a'),
            Parsed::Bool('b'),
            Parsed::Bool('c'),
        ]);
    }

    #[test]
    fn double_dash_terminates_flags() {
        let flags = [bool_flag('v')];
        let r = scan(&["--", "-v"], &flags, OnUnknown::Reject, Style::Posix).unwrap();
        assert!(r.flags.is_empty());
        assert_eq!(r.operands, ["-v"]);
    }

    #[test]
    fn operands_after_flags() {
        let flags = [bool_flag('v')];
        let r = scan(&["-v", "file.txt"], &flags, OnUnknown::Reject, Style::Posix).unwrap();
        assert_eq!(r.flags, [Parsed::Bool('v')]);
        assert_eq!(r.operands, ["file.txt"]);
    }

    #[test]
    fn valued_flag_separate() {
        let flags = [value_flag('o')];
        let r = scan(&["-o", "file"], &flags, OnUnknown::Reject, Style::Posix).unwrap();
        assert_eq!(r.flags, [Parsed::Value('o', "file".into())]);
        assert!(r.operands.is_empty());
    }

    #[test]
    fn valued_flag_separate_with_trailing() {
        let flags = [value_flag('o')];
        let r = scan(&["-o", "file", "rest"], &flags, OnUnknown::Reject, Style::Posix).unwrap();
        assert_eq!(r.flags, [Parsed::Value('o', "file".into())]);
        assert_eq!(r.operands, ["rest"]);
    }

    #[test]
    fn valued_flag_stuck() {
        let flags = [value_flag('o')];
        let r = scan(&["-ofile"], &flags, OnUnknown::Reject, Style::Posix).unwrap();
        assert_eq!(r.flags, [Parsed::Value('o', "file".into())]);
        assert!(r.operands.is_empty());
    }

    #[test]
    fn valued_flag_missing_arg_errors() {
        let flags = [value_flag('o')];
        let r = scan(&["-o"], &flags, OnUnknown::Reject, Style::Posix);
        assert!(matches!(r, Err(Error::MissingValue(_))));
    }

    #[test]
    fn bundled_bool_then_valued() {
        let flags = [bool_flag('v'), value_flag('o')];
        let r = scan(&["-vo", "file"], &flags, OnUnknown::Reject, Style::Posix).unwrap();
        assert_eq!(r.flags, [
            Parsed::Bool('v'),
            Parsed::Value('o', "file".into()),
        ]);
        assert!(r.operands.is_empty());
    }

    #[test]
    fn bundled_bool_then_valued_stuck() {
        let flags = [bool_flag('v'), value_flag('o')];
        let r = scan(&["-vofile"], &flags, OnUnknown::Reject, Style::Posix).unwrap();
        assert_eq!(r.flags, [
            Parsed::Bool('v'),
            Parsed::Value('o', "file".into()),
        ]);
        assert!(r.operands.is_empty());
    }

    #[test]
    fn multiple_valued_flags_in_sequence() {
        let flags = [value_flag('o'), value_flag('d')];
        let r = scan(&["-o", "out", "-d", "dir"], &flags, OnUnknown::Reject, Style::Posix).unwrap();
        assert_eq!(r.flags, [
            Parsed::Value('o', "out".into()),
            Parsed::Value('d', "dir".into()),
        ]);
        assert!(r.operands.is_empty());
    }

    #[test]
    fn unknown_flag_errors() {
        let flags = [bool_flag('v')];
        let r = scan(&["-x"], &flags, OnUnknown::Reject, Style::Posix);
        assert!(matches!(r, Err(Error::UnknownFlag(_))));
    }

    #[test]
    fn passthrough_unknown_becomes_operand() {
        let flags = [bool_flag('n')];
        let r = scan(&["-nea"], &flags, OnUnknown::PassThrough, Style::Posix).unwrap();
        assert!(r.flags.is_empty());
        assert_eq!(r.operands, ["-nea"]);
    }

    #[test]
    fn passthrough_all_known_still_parses() {
        let flags = [bool_flag('n'), bool_flag('e')];
        let r = scan(&["-ne", "hello"], &flags, OnUnknown::PassThrough, Style::Posix).unwrap();
        assert_eq!(r.flags, [Parsed::Bool('n'), Parsed::Bool('e')]);
        assert_eq!(r.operands, ["hello"]);
    }

    #[test]
    fn passthrough_first_char_unknown() {
        let flags = [bool_flag('n')];
        let r = scan(&["-xyz", "rest"], &flags, OnUnknown::PassThrough, Style::Posix).unwrap();
        assert!(r.flags.is_empty());
        assert_eq!(r.operands, ["-xyz", "rest"]);
    }

    #[test]
    fn passthrough_value_flag_stuck_still_parses() {
        let flags = [value_flag('o')];
        let r = scan(&["-ofile"], &flags, OnUnknown::PassThrough, Style::Posix).unwrap();
        assert_eq!(r.flags, [Parsed::Value('o', "file".into())]);
        assert!(r.operands.is_empty());
    }

    #[test]
    fn polarity_on() {
        let flags = [polar_flag('x')];
        let r = scan(&["-x"], &flags, OnUnknown::Reject, Style::Posix).unwrap();
        assert_eq!(r.flags, [Parsed::Polar('x', Polarity::On)]);
    }

    #[test]
    fn polarity_off() {
        let flags = [polar_flag('x')];
        let r = scan(&["+x"], &flags, OnUnknown::Reject, Style::Posix).unwrap();
        assert_eq!(r.flags, [Parsed::Polar('x', Polarity::Off)]);
    }

    #[test]
    fn plus_prefix_only_when_polarity_flags_exist() {
        let flags = [bool_flag('x')];
        let r = scan(&["+x"], &flags, OnUnknown::Reject, Style::Posix).unwrap();
        assert_eq!(r.operands, ["+x"]);
    }

    #[test]
    fn noop_flag_accepted_silently() {
        let flags = [bool_flag('r'), noop_flag('e')];
        let r = scan(&["-re"], &flags, OnUnknown::Reject, Style::Posix).unwrap();
        assert_eq!(r.flags, [Parsed::Bool('r')]);
    }

    #[test]
    fn bare_dash_is_operand() {
        let flags = [bool_flag('v')];
        let r = scan(&["-"], &flags, OnUnknown::Reject, Style::Posix).unwrap();
        assert_eq!(r.operands, ["-"]);
    }

    #[test]
    fn multiple_flag_groups() {
        let flags = [bool_flag('a'), bool_flag('b')];
        let r = scan(&["-a", "-b", "file"], &flags, OnUnknown::Reject, Style::Posix).unwrap();
        assert_eq!(r.flags, [Parsed::Bool('a'), Parsed::Bool('b')]);
        assert_eq!(r.operands, ["file"]);
    }

    #[test]
    fn polar_value_on() {
        let flags = [FlagDef { ch: 'o', kind: FlagKind::PolarValue, long: "", clears: &[], desc: "", value_name: "" }];
        let r = scan(&["-o", "errexit"], &flags, OnUnknown::Reject, Style::Posix).unwrap();
        assert_eq!(r.flags, [Parsed::PolarValue('o', Polarity::On, "errexit".into())]);
        assert!(r.operands.is_empty());
    }

    #[test]
    fn polar_value_off() {
        let flags = [FlagDef { ch: 'o', kind: FlagKind::PolarValue, long: "", clears: &[], desc: "", value_name: "" }];
        let r = scan(&["+o", "verbose"], &flags, OnUnknown::Reject, Style::Posix).unwrap();
        assert_eq!(r.flags, [Parsed::PolarValue('o', Polarity::Off, "verbose".into())]);
        assert!(r.operands.is_empty());
    }

    #[test]
    fn polar_value_stuck() {
        let flags = [FlagDef { ch: 'o', kind: FlagKind::PolarValue, long: "", clears: &[], desc: "", value_name: "" }];
        let r = scan(&["-oerrexit"], &flags, OnUnknown::Reject, Style::Posix).unwrap();
        assert_eq!(r.flags, [Parsed::PolarValue('o', Polarity::On, "errexit".into())]);
        assert!(r.operands.is_empty());
    }

    // ── GNU long options (Style::Gnu) ───────────────────────────

    fn long_bool(ch: char, long: &'static str) -> FlagDef {
        FlagDef { ch, long, kind: FlagKind::Bool, clears: &[], desc: "", value_name: "" }
    }

    fn long_value(ch: char, long: &'static str) -> FlagDef {
        FlagDef { ch, long, kind: FlagKind::Value, clears: &[], desc: "", value_name: "" }
    }

    #[test]
    fn gnu_exact_long_bool() {
        let flags = [long_bool('a', "multiple")];
        let r = scan(&["--multiple"], &flags, OnUnknown::Reject, Style::Gnu).unwrap();
        assert_eq!(r.flags, [Parsed::Bool('a')]);
        assert!(r.operands.is_empty());
    }

    #[test]
    fn gnu_long_value_inline() {
        let flags = [long_value('s', "suffix")];
        let r = scan(&["--suffix=.txt"], &flags, OnUnknown::Reject, Style::Gnu).unwrap();
        assert_eq!(r.flags, [Parsed::Value('s', ".txt".into())]);
    }

    #[test]
    fn gnu_long_value_separated() {
        let flags = [long_value('s', "suffix")];
        let r = scan(&["--suffix", ".txt"], &flags, OnUnknown::Reject, Style::Gnu).unwrap();
        assert_eq!(r.flags, [Parsed::Value('s', ".txt".into())]);
    }

    #[test]
    fn gnu_empty_inline_value_is_allowed() {
        let flags = [long_value('s', "suffix")];
        let r = scan(&["--suffix="], &flags, OnUnknown::Reject, Style::Gnu).unwrap();
        assert_eq!(r.flags, [Parsed::Value('s', String::new())]);
    }

    #[test]
    fn gnu_prefix_inference_resolves_unambiguous() {
        let flags = [long_bool('a', "multiple")];
        let r = scan(&["--mult"], &flags, OnUnknown::Reject, Style::Gnu).unwrap();
        assert_eq!(r.flags, [Parsed::Bool('a')]);
    }

    #[test]
    fn gnu_prefix_ambiguous_is_error() {
        let flags = [long_bool('v', "verbose")];
        // "--ver" matches both "verbose" and the reserved "version".
        let e = scan(&["--ver"], &flags, OnUnknown::Reject, Style::Gnu).unwrap_err();
        assert_eq!(e, Error::AmbiguousOption("--ver".into()));
    }

    #[test]
    fn gnu_exact_match_beats_prefix() {
        let flags = [long_bool('a', "no"), long_bool('b', "nonsense")];
        let r = scan(&["--no"], &flags, OnUnknown::Reject, Style::Gnu).unwrap();
        assert_eq!(r.flags, [Parsed::Bool('a')]);
    }

    #[test]
    fn gnu_bool_rejects_inline_value() {
        let flags = [long_bool('a', "multiple")];
        let e = scan(&["--multiple=x"], &flags, OnUnknown::Reject, Style::Gnu).unwrap_err();
        assert_eq!(e, Error::UnexpectedValue("--multiple".into()));
    }

    #[test]
    fn gnu_permutes_flags_after_operands() {
        let flags = [long_bool('a', "multiple"), long_bool('z', "zero")];
        let r = scan(&["one", "--multiple", "two", "--zero"], &flags, OnUnknown::Reject, Style::Gnu).unwrap();
        assert_eq!(r.flags, [Parsed::Bool('a'), Parsed::Bool('z')]);
        assert_eq!(r.operands, ["one", "two"]);
    }

    #[test]
    fn gnu_short_flags_still_bundle() {
        let flags = [long_bool('a', "multiple"), long_bool('z', "zero")];
        let r = scan(&["-az"], &flags, OnUnknown::Reject, Style::Gnu).unwrap();
        assert_eq!(r.flags, [Parsed::Bool('a'), Parsed::Bool('z')]);
    }

    #[test]
    fn gnu_short_value_separated_still_works() {
        let flags = [long_value('s', "suffix")];
        let r = scan(&["-s", ".bak"], &flags, OnUnknown::Reject, Style::Gnu).unwrap();
        assert_eq!(r.flags, [Parsed::Value('s', ".bak".into())]);
    }

    #[test]
    fn gnu_help_is_signalled() {
        let e = scan(&["--help"], &[], OnUnknown::Reject, Style::Gnu).unwrap_err();
        assert_eq!(e, Error::HelpRequested);
    }

    #[test]
    fn gnu_version_is_signalled() {
        let e = scan(&["--version"], &[], OnUnknown::Reject, Style::Gnu).unwrap_err();
        assert_eq!(e, Error::VersionRequested);
    }

    #[test]
    fn gnu_help_prefix_infers() {
        let e = scan(&["--hel"], &[], OnUnknown::Reject, Style::Gnu).unwrap_err();
        assert_eq!(e, Error::HelpRequested);
    }

    #[test]
    fn gnu_unknown_long_is_error() {
        let e = scan(&["--bogus"], &[], OnUnknown::Reject, Style::Gnu).unwrap_err();
        assert_eq!(e, Error::UnknownFlag("--bogus".into()));
    }

    #[test]
    fn gnu_long_value_missing_errors() {
        let flags = [long_value('s', "suffix")];
        let e = scan(&["--suffix"], &flags, OnUnknown::Reject, Style::Gnu).unwrap_err();
        assert_eq!(e, Error::MissingValue("--suffix".into()));
    }

    #[test]
    fn gnu_empty_long_name_is_unknown() {
        let e = scan(&["--=x"], &[], OnUnknown::Reject, Style::Gnu).unwrap_err();
        assert_eq!(e, Error::UnknownFlag("--".into()));
    }

    #[test]
    fn gnu_value_flag_swallows_optionlike_next() {
        // getopt_long parity: --suffix consumes the following --zero as its value.
        let flags = [long_value('s', "suffix"), long_bool('z', "zero")];
        let r = scan(&["--suffix", "--zero"], &flags, OnUnknown::Reject, Style::Gnu).unwrap();
        assert_eq!(r.flags, [Parsed::Value('s', "--zero".into())]);
        assert!(r.operands.is_empty());
    }

    #[test]
    fn gnu_real_help_flag_shadows_reserved() {
        let flags = [long_bool('h', "help")];
        let r = scan(&["--help"], &flags, OnUnknown::Reject, Style::Gnu).unwrap();
        assert_eq!(r.flags, [Parsed::Bool('h')]);
        // `--hel` is not ambiguous: the declared "help" flag shadows the reserved signal.
        let r2 = scan(&["--hel"], &flags, OnUnknown::Reject, Style::Gnu).unwrap();
        assert_eq!(r2.flags, [Parsed::Bool('h')]);
    }

    #[test]
    fn gnu_exact_valued_beats_prefix() {
        let flags = [long_value('a', "max"), long_value('b', "maximum")];
        let r = scan(&["--max=5"], &flags, OnUnknown::Reject, Style::Gnu).unwrap();
        assert_eq!(r.flags, [Parsed::Value('a', "5".into())]);
    }

    #[test]
    fn gnu_double_dash_still_terminates() {
        let flags = [long_bool('a', "multiple")];
        let r = scan(&["--", "--multiple"], &flags, OnUnknown::Reject, Style::Gnu).unwrap();
        assert!(r.flags.is_empty());
        assert_eq!(r.operands, ["--multiple"]);
    }

    #[test]
    fn posix_never_parses_double_dash_word_as_long() {
        // Bash/POSIX mode must not gain GNU long-option behavior.
        let flags = [long_bool('a', "multiple")];
        let e = scan(&["--multiple"], &flags, OnUnknown::Reject, Style::Posix).unwrap_err();
        assert_eq!(e, Error::UnknownFlag("--".into()));
    }
}
