use serde::Serialize;
use strsim::jaro_winkler;

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

/// A single correction applied to one argument.
#[derive(Debug, Clone, Serialize)]
pub struct Correction {
    pub original: String,
    pub corrected: String,
    pub rule: CorrectionRule,
    pub confidence: f64,
}

/// Which rule triggered the correction.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CorrectionRule {
    SingleDashLongFlag,
    CaseNormalization,
    FuzzyFlag,
    SubcommandAlias,
    /// Fuzzy subcommand match: "issuess" → "issues"
    SubcommandFuzzy,
    /// Flag-style subcommand: "--robot-docs" → "robot-docs"
    FlagAsSubcommand,
    ValueNormalization,
    ValueFuzzy,
    FlagPrefix,
    NoColorExpansion,
}

/// Result of the correction pass over raw args.
#[derive(Debug, Clone)]
pub struct CorrectionResult {
    pub args: Vec<String>,
    pub corrections: Vec<Correction>,
}

// ---------------------------------------------------------------------------
// Flag registry
// ---------------------------------------------------------------------------

/// Global flags accepted by every command (from `Cli` struct).
const GLOBAL_FLAGS: &[&str] = &[
    "--config",
    "--robot",
    "--json",
    "--color",
    "--icons",
    "--quiet",
    "--no-quiet",
    "--verbose",
    "--no-verbose",
    "--log-format",
];

/// Per-subcommand flags. Each entry is `(command_name, &[flags])`.
/// Hidden `--no-*` variants are included so they can be fuzzy-matched too.
const COMMAND_FLAGS: &[(&str, &[&str])] = &[
    (
        "issues",
        &[
            "--limit",
            "--fields",
            "--state",
            "--project",
            "--author",
            "--assignee",
            "--label",
            "--milestone",
            "--status",
            "--since",
            "--due-before",
            "--has-due",
            "--no-has-due",
            "--sort",
            "--asc",
            "--no-asc",
            "--open",
            "--no-open",
        ],
    ),
    (
        "mrs",
        &[
            "--limit",
            "--fields",
            "--state",
            "--project",
            "--author",
            "--assignee",
            "--reviewer",
            "--label",
            "--since",
            "--draft",
            "--no-draft",
            "--target",
            "--source",
            "--sort",
            "--asc",
            "--no-asc",
            "--open",
            "--no-open",
        ],
    ),
    (
        "ingest",
        &[
            "--project",
            "--force",
            "--no-force",
            "--full",
            "--no-full",
            "--dry-run",
            "--no-dry-run",
        ],
    ),
    (
        "sync",
        &[
            "--full",
            "--no-full",
            "--force",
            "--no-force",
            "--no-embed",
            "--no-docs",
            "--no-events",
            "--no-file-changes",
            "--no-status",
            "--dry-run",
            "--no-dry-run",
            "--timings",
            "--lock",
            "--issue",
            "--mr",
            "--project",
            "--preflight-only",
        ],
    ),
    (
        "search",
        &[
            "--mode",
            "--type",
            "--author",
            "--project",
            "--label",
            "--path",
            "--since",
            "--updated-since",
            "--limit",
            "--fields",
            "--explain",
            "--no-explain",
            "--fts-mode",
        ],
    ),
    (
        "embed",
        &["--full", "--no-full", "--retry-failed", "--no-retry-failed"],
    ),
    (
        "stats",
        &[
            "--check",
            "--no-check",
            "--repair",
            "--dry-run",
            "--no-dry-run",
        ],
    ),
    ("count", &["--for"]),
    (
        "timeline",
        &[
            "--project",
            "--since",
            "--depth",
            "--no-mentions",
            "--limit",
            "--fields",
            "--max-seeds",
            "--max-entities",
            "--max-evidence",
        ],
    ),
    ("related", &["--limit", "--project"]),
    (
        "who",
        &[
            "--path",
            "--active",
            "--overlap",
            "--reviews",
            "--since",
            "--project",
            "--limit",
            "--fields",
            "--detail",
            "--no-detail",
            "--as-of",
            "--explain-score",
            "--include-bots",
            "--include-closed",
            "--all-history",
        ],
    ),
    ("drift", &["--threshold", "--project"]),
    (
        "explain",
        &[
            "--project",
            "--sections",
            "--no-timeline",
            "--max-decisions",
            "--since",
        ],
    ),
    (
        "notes",
        &[
            "--limit",
            "--fields",
            "--author",
            "--note-type",
            "--contains",
            "--note-id",
            "--gitlab-note-id",
            "--discussion-id",
            "--include-system",
            "--for-issue",
            "--for-mr",
            "--project",
            "--since",
            "--until",
            "--path",
            "--resolution",
            "--sort",
            "--asc",
            "--open",
        ],
    ),
    (
        "init",
        &[
            "--refresh",
            "--force",
            "--non-interactive",
            "--gitlab-url",
            "--token-env-var",
            "--projects",
            "--default-project",
        ],
    ),
    (
        "file-history",
        &[
            "--project",
            "--discussions",
            "--no-follow-renames",
            "--merged",
            "--limit",
        ],
    ),
    (
        "trace",
        &[
            "--project",
            "--discussions",
            "--no-follow-renames",
            "--limit",
        ],
    ),
    ("generate-docs", &["--full", "--project"]),
    ("completions", &[]),
    ("robot-docs", &["--brief"]),
    (
        "list",
        &[
            "--limit",
            "--project",
            "--state",
            "--author",
            "--assignee",
            "--label",
            "--milestone",
            "--since",
            "--due-before",
            "--has-due-date",
            "--sort",
            "--order",
            "--open",
            "--draft",
            "--no-draft",
            "--reviewer",
            "--target-branch",
            "--source-branch",
        ],
    ),
    ("reset", &["--yes"]),
    (
        "me",
        &[
            "--issues",
            "--mrs",
            "--activity",
            "--mentions",
            "--since",
            "--project",
            "--all",
            "--user",
            "--fields",
            "--reset-cursor",
        ],
    ),
];

/// Valid values for enum-like flags, used for post-clap error enhancement.
pub const ENUM_VALUES: &[(&str, &[&str])] = &[
    ("--state", &["opened", "closed", "merged", "locked", "all"]),
    ("--mode", &["lexical", "hybrid", "semantic"]),
    ("--sort", &["updated", "created", "iid"]),
    ("--type", &["issue", "mr", "discussion", "note"]),
    ("--fts-mode", &["safe", "raw"]),
    ("--color", &["auto", "always", "never"]),
    ("--log-format", &["text", "json"]),
    ("--for", &["issue", "mr"]),
];

// ---------------------------------------------------------------------------
// Subcommand alias map (for forms clap aliases can't express)
// ---------------------------------------------------------------------------

/// Subcommand aliases for non-standard forms (underscores, no separators).
/// Clap `visible_alias`/`alias` handles hyphenated forms (`merge-requests`);
/// this map catches the rest.
const SUBCOMMAND_ALIASES: &[(&str, &str)] = &[
    ("merge_requests", "mrs"),
    ("merge_request", "mrs"),
    ("mergerequests", "mrs"),
    ("mergerequest", "mrs"),
    ("generate_docs", "generate-docs"),
    ("generatedocs", "generate-docs"),
    ("gendocs", "generate-docs"),
    ("gen-docs", "generate-docs"),
    ("robot_docs", "robot-docs"),
    ("robotdocs", "robot-docs"),
    ("sync_status", "status"),
    ("syncstatus", "status"),
    ("auth_test", "auth"),
    ("authtest", "auth"),
    ("file_history", "file-history"),
    ("filehistory", "file-history"),
];

// ---------------------------------------------------------------------------
// Correction thresholds
// ---------------------------------------------------------------------------

const FUZZY_FLAG_THRESHOLD: f64 = 0.8;
/// Stricter threshold for robot mode — only high-confidence corrections to
/// avoid misleading agents. Still catches obvious typos like `--projct`.
const FUZZY_FLAG_THRESHOLD_STRICT: f64 = 0.9;

/// Fuzzy subcommand threshold — higher than flags because subcommand names
/// are shorter words where JW scores inflate more easily.
const FUZZY_SUBCMD_THRESHOLD: f64 = 0.85;

/// All canonical subcommand names for fuzzy matching and flag-as-subcommand
/// detection. Includes hidden commands so agents that know about them can
/// still benefit from typo correction.
const CANONICAL_SUBCOMMANDS: &[&str] = &[
    "issues",
    "mrs",
    "notes",
    "ingest",
    "count",
    "status",
    "auth",
    "doctor",
    "version",
    "init",
    "search",
    "stats",
    "generate-docs",
    "embed",
    "sync",
    "migrate",
    "health",
    "robot-docs",
    "completions",
    "timeline",
    "who",
    "me",
    "file-history",
    "trace",
    "drift",
    "explain",
    "related",
    "cron",
    "token",
    // Hidden but still valid
    "backup",
    "reset",
    "list",
    "auth-test",
    "sync-status",
];

// ---------------------------------------------------------------------------
// Core logic
// ---------------------------------------------------------------------------

/// Detect which subcommand is being invoked by finding the first positional
/// arg (not a flag, not a flag value).
fn detect_subcommand(args: &[String]) -> Option<&str> {
    // Skip args[0] (binary name). Walk forward looking for the first
    // arg that isn't a flag and isn't the value to a flag that takes one.
    let mut skip_next = false;
    for arg in args.iter().skip(1) {
        if skip_next {
            skip_next = false;
            continue;
        }
        if arg.starts_with('-') {
            // Flags that take a value: we know global ones; for simplicity
            // skip the next arg for any `--flag=value` form (handled inline)
            // or known value-taking global flags.
            if arg.contains('=') {
                continue;
            }
            if matches!(arg.as_str(), "--config" | "-c" | "--color" | "--log-format") {
                skip_next = true;
            }
            continue;
        }
        // First non-flag positional = subcommand
        return Some(arg.as_str());
    }
    None
}

/// Build the set of valid long flags for the detected subcommand.
fn valid_flags_for(subcommand: Option<&str>) -> Vec<&'static str> {
    let mut flags: Vec<&str> = GLOBAL_FLAGS.to_vec();
    if let Some(cmd) = subcommand {
        for (name, cmd_flags) in COMMAND_FLAGS {
            if *name == cmd {
                flags.extend_from_slice(cmd_flags);
                break;
            }
        }
    } else {
        // No subcommand detected — include all flags for maximum matching
        for (_, cmd_flags) in COMMAND_FLAGS {
            for flag in *cmd_flags {
                if !flags.contains(flag) {
                    flags.push(flag);
                }
            }
        }
    }
    flags
}

/// Run the pre-clap correction pass on raw args.
///
/// Three-phase pipeline:
/// - Phase A: Subcommand alias correction (case-insensitive alias map)
/// - Phase B: Per-arg flag corrections (single-dash, case, prefix, fuzzy)
/// - Phase C: Enum value normalization (case + fuzzy + prefix on known values)
///
/// When `strict` is true (robot mode), fuzzy matching uses a higher threshold
/// (0.9 vs 0.8) to avoid speculative corrections while still catching obvious
/// typos like `--projct` → `--project`.
///
/// Returns the (possibly modified) args and any corrections applied.
pub fn correct_args(raw: Vec<String>, strict: bool) -> CorrectionResult {
    let mut corrections = Vec::new();

    // Phase A: Subcommand alias correction
    let args = correct_subcommand(raw, &mut corrections);

    // Phase B: Per-arg flag corrections
    let valid = valid_flags_for(detect_subcommand(&args));

    let mut corrected = Vec::with_capacity(args.len());
    let mut past_terminator = false;

    for arg in args {
        // B1: Stop correcting after POSIX `--` option terminator
        if arg == "--" {
            past_terminator = true;
            corrected.push(arg);
            continue;
        }

        if past_terminator {
            corrected.push(arg);
            continue;
        }

        if let Some(fixed) = try_correct(&arg, &valid, strict) {
            if fixed.rule == CorrectionRule::NoColorExpansion {
                // Expand --no-color → --color never
                corrections.push(Correction {
                    original: fixed.original,
                    corrected: "--color never".to_string(),
                    rule: CorrectionRule::NoColorExpansion,
                    confidence: 1.0,
                });
                corrected.push("--color".to_string());
                corrected.push("never".to_string());
            } else {
                let s = fixed.corrected.clone();
                corrections.push(fixed);
                corrected.push(s);
            }
        } else {
            corrected.push(arg);
        }
    }

    // Phase C: Enum value normalization
    normalize_enum_values(&mut corrected, &mut corrections);

    CorrectionResult {
        args: corrected,
        corrections,
    }
}

/// Phase A: Replace subcommand aliases with their canonical names, fuzzy-match
/// typo'd subcommands, and detect flag-style subcommands (`--robot-docs`).
///
/// Three-step pipeline:
/// - A1: Exact alias match (underscore/no-separator forms)
/// - A2: Fuzzy subcommand match ("issuess" → "issues")
/// - A3: Flag-as-subcommand ("--robot-docs" → "robot-docs")
fn correct_subcommand(mut args: Vec<String>, corrections: &mut Vec<Correction>) -> Vec<String> {
    // Find the subcommand position index.
    let mut skip_next = false;
    let mut subcmd_idx = None;
    for (i, arg) in args.iter().enumerate().skip(1) {
        if skip_next {
            skip_next = false;
            continue;
        }
        if arg.starts_with('-') {
            if arg.contains('=') {
                continue;
            }
            if matches!(arg.as_str(), "--config" | "-c" | "--color" | "--log-format") {
                skip_next = true;
            }
            continue;
        }
        subcmd_idx = Some(i);
        break;
    }

    if let Some(i) = subcmd_idx {
        // A1: Exact alias match (existing logic)
        if let Some((_, canonical)) = SUBCOMMAND_ALIASES
            .iter()
            .find(|(alias, _)| alias.eq_ignore_ascii_case(&args[i]))
        {
            corrections.push(Correction {
                original: args[i].clone(),
                corrected: (*canonical).to_string(),
                rule: CorrectionRule::SubcommandAlias,
                confidence: 1.0,
            });
            args[i] = (*canonical).to_string();
        }
        // A2: Fuzzy subcommand match — only if not already a canonical name
        else {
            let lower = args[i].to_lowercase();
            if !CANONICAL_SUBCOMMANDS.contains(&lower.as_str()) {
                // Guard: don't fuzzy-match words that look like misplaced global flags
                // (e.g., "robot" should not match "robot-docs")
                let as_flag = format!("--{lower}");
                let is_flag_word = GLOBAL_FLAGS
                    .iter()
                    .any(|f| f.eq_ignore_ascii_case(&as_flag));

                // Guard: don't fuzzy-match if it's a valid prefix of a canonical command
                // (clap's infer_subcommands handles prefix resolution)
                let is_prefix = CANONICAL_SUBCOMMANDS
                    .iter()
                    .any(|cmd| cmd.starts_with(&*lower) && *cmd != lower);

                if !is_flag_word && !is_prefix {
                    let best = CANONICAL_SUBCOMMANDS
                        .iter()
                        .map(|cmd| (*cmd, jaro_winkler(&lower, cmd)))
                        .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));

                    if let Some((cmd, score)) = best
                        && score >= FUZZY_SUBCMD_THRESHOLD
                    {
                        corrections.push(Correction {
                            original: args[i].clone(),
                            corrected: cmd.to_string(),
                            rule: CorrectionRule::SubcommandFuzzy,
                            confidence: score,
                        });
                        args[i] = cmd.to_string();
                    }
                }
            }
        }
    } else {
        // A3: No subcommand detected — check for flag-style subcommands.
        // Agents sometimes type `--robot-docs` or `--generate-docs` as flags.
        let mut flag_as_subcmd: Option<(usize, String)> = None;
        let mut flag_skip = false;
        for (i, arg) in args.iter().enumerate().skip(1) {
            if flag_skip {
                flag_skip = false;
                continue;
            }
            if !arg.starts_with("--") || arg.contains('=') {
                continue;
            }

            let arg_lower = arg.to_lowercase();
            // Skip clap built-in flags (--help, --version)
            if CLAP_BUILTINS
                .iter()
                .any(|b| b.eq_ignore_ascii_case(&arg_lower))
            {
                continue;
            }
            // Skip known global flags
            if GLOBAL_FLAGS.iter().any(|f| f.to_lowercase() == arg_lower) {
                if matches!(arg_lower.as_str(), "--config" | "--color" | "--log-format") {
                    flag_skip = true;
                }
                continue;
            }

            let stripped = arg_lower[2..].to_string();
            if CANONICAL_SUBCOMMANDS.contains(&stripped.as_str()) {
                flag_as_subcmd = Some((i, stripped));
                break;
            }
        }

        if let Some((i, subcmd)) = flag_as_subcmd {
            corrections.push(Correction {
                original: args[i].clone(),
                corrected: subcmd.clone(),
                rule: CorrectionRule::FlagAsSubcommand,
                confidence: 1.0,
            });
            args[i] = subcmd;
        }
    }

    args
}

/// Phase C: Normalize enum values for flags with known valid values.
///
/// Handles both `--flag value` and `--flag=value` forms. Corrections are:
/// 1. Case normalization: `Opened` → `opened`
/// 2. Prefix expansion: `open` → `opened` (only if unambiguous)
/// 3. Fuzzy matching: `opend` → `opened`
fn normalize_enum_values(args: &mut [String], corrections: &mut Vec<Correction>) {
    let mut i = 0;
    while i < args.len() {
        // Respect POSIX `--` option terminator — don't normalize values after it
        if args[i] == "--" {
            break;
        }

        // Handle --flag=value form
        if let Some(eq_pos) = args[i].find('=') {
            let flag = args[i][..eq_pos].to_string();
            let value = args[i][eq_pos + 1..].to_string();
            if let Some(valid_vals) = lookup_enum_values(&flag)
                && let Some((corrected_val, is_case_only)) = normalize_value(&value, valid_vals)
            {
                let original = args[i].clone();
                let corrected = format!("{flag}={corrected_val}");
                args[i] = corrected.clone();
                corrections.push(Correction {
                    original,
                    corrected,
                    rule: if is_case_only {
                        CorrectionRule::ValueNormalization
                    } else {
                        CorrectionRule::ValueFuzzy
                    },
                    confidence: 0.95,
                });
            }
            i += 1;
            continue;
        }

        // Handle --flag value form
        if args[i].starts_with("--")
            && let Some(valid_vals) = lookup_enum_values(&args[i])
            && i + 1 < args.len()
            && !args[i + 1].starts_with('-')
        {
            let value = args[i + 1].clone();
            if let Some((corrected_val, is_case_only)) = normalize_value(&value, valid_vals) {
                let original = args[i + 1].clone();
                args[i + 1] = corrected_val.to_string();
                corrections.push(Correction {
                    original,
                    corrected: corrected_val.to_string(),
                    rule: if is_case_only {
                        CorrectionRule::ValueNormalization
                    } else {
                        CorrectionRule::ValueFuzzy
                    },
                    confidence: 0.95,
                });
            }
            i += 2;
            continue;
        }

        i += 1;
    }
}

/// Look up valid enum values for a flag (case-insensitive flag name match).
fn lookup_enum_values(flag: &str) -> Option<&'static [&'static str]> {
    let lower = flag.to_lowercase();
    ENUM_VALUES
        .iter()
        .find(|(f, _)| f.to_lowercase() == lower)
        .map(|(_, vals)| *vals)
}

/// Try to normalize a value against a set of valid values.
///
/// Returns `Some((corrected, is_case_only))` if a correction is needed:
/// - `is_case_only = true` for pure case normalization
/// - `is_case_only = false` for prefix/fuzzy corrections
///
/// Returns `None` if the value is already valid or no match is found.
fn normalize_value(input: &str, valid_values: &[&str]) -> Option<(String, bool)> {
    // Already valid (exact match)? No correction needed.
    if valid_values.contains(&input) {
        return None;
    }

    let lower = input.to_lowercase();

    // Case-insensitive exact match
    if let Some(&val) = valid_values.iter().find(|v| v.to_lowercase() == lower) {
        return Some((val.to_string(), true));
    }

    // Prefix match (e.g., "open" → "opened") — only if unambiguous
    let prefix_matches: Vec<&&str> = valid_values
        .iter()
        .filter(|v| v.starts_with(&*lower))
        .collect();
    if prefix_matches.len() == 1 {
        return Some(((*prefix_matches[0]).to_string(), false));
    }

    // Fuzzy match
    let best = valid_values
        .iter()
        .map(|v| (*v, jaro_winkler(&lower, v)))
        .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
    if let Some((val, score)) = best
        && score >= 0.8
    {
        return Some((val.to_string(), false));
    }

    None
}

/// Clap built-in flags that should never be corrected. These are handled by clap
/// directly and are not in our GLOBAL_FLAGS registry.
const CLAP_BUILTINS: &[&str] = &["--help", "--version"];

/// Try to correct a single arg. Returns `None` if no correction needed.
///
/// When `strict` is true, fuzzy matching is disabled — only deterministic
/// corrections (single-dash fix, case normalization) are applied.
///
/// Special case: `--no-color` is rewritten to `--color never` by returning
/// the `--color` correction and letting the caller handle arg insertion.
/// However, since we correct one arg at a time, we use `NoColorExpansion`
/// to signal that the next phase should insert `never` after this arg.
fn try_correct(arg: &str, valid_flags: &[&str], strict: bool) -> Option<Correction> {
    // Only attempt correction on flag-like args (starts with `-`)
    if !arg.starts_with('-') {
        return None;
    }

    // Special case: --no-color → --color never (common agent/user expectation)
    if arg.eq_ignore_ascii_case("--no-color") {
        return Some(Correction {
            original: arg.to_string(),
            corrected: "--no-color".to_string(), // sentinel; expanded in correct_args
            rule: CorrectionRule::NoColorExpansion,
            confidence: 1.0,
        });
    }

    // B2: Never correct clap built-in flags (--help, --version)
    let flag_part_for_builtin = if let Some(eq_pos) = arg.find('=') {
        &arg[..eq_pos]
    } else {
        arg
    };
    if CLAP_BUILTINS
        .iter()
        .any(|b| b.eq_ignore_ascii_case(flag_part_for_builtin))
    {
        return None;
    }

    // Skip short flags — they're unambiguous single chars (-p, -n, -v, -J)
    // Also skip stacked short flags (-vvv)
    if !arg.starts_with("--") {
        // Rule 1: Single-dash long flag — e.g. `-robot` (len > 2, not a valid short flag)
        // A short flag is `-` + single char, optionally stacked (-vvv).
        // If it's `-` + multiple chars and NOT all the same char, it's likely a single-dash long flag.
        let after_dash = &arg[1..];

        // Check if it's a stacked short flag like -vvv (all same char)
        let all_same_char = after_dash.len() > 1
            && after_dash
                .chars()
                .all(|c| c == after_dash.chars().next().unwrap_or('\0'));
        if all_same_char {
            return None;
        }

        // Single char = valid short flag, don't touch
        if after_dash.len() == 1 {
            return None;
        }

        // It looks like a single-dash long flag (e.g. `-robot`, `-state`)
        let candidate = format!("--{after_dash}");

        // Check exact match first (case-sensitive)
        if valid_flags.contains(&candidate.as_str()) {
            return Some(Correction {
                original: arg.to_string(),
                corrected: candidate,
                rule: CorrectionRule::SingleDashLongFlag,
                confidence: 0.95,
            });
        }

        // Check case-insensitive exact match
        let lower = candidate.to_lowercase();
        if let Some(&flag) = valid_flags.iter().find(|f| f.to_lowercase() == lower) {
            return Some(Correction {
                original: arg.to_string(),
                corrected: flag.to_string(),
                rule: CorrectionRule::SingleDashLongFlag,
                confidence: 0.95,
            });
        }

        // Try fuzzy on the single-dash candidate (skip in strict mode)
        if !strict
            && let Some((best_flag, score)) = best_fuzzy_match(&lower, valid_flags)
            && score >= FUZZY_FLAG_THRESHOLD
        {
            return Some(Correction {
                original: arg.to_string(),
                corrected: best_flag.to_string(),
                rule: CorrectionRule::SingleDashLongFlag,
                confidence: score * 0.95, // discount slightly for compound correction
            });
        }

        return None;
    }

    // For `--flag` or `--flag=value` forms: only correct the flag name
    let (flag_part, value_suffix) = if let Some(eq_pos) = arg.find('=') {
        (&arg[..eq_pos], Some(&arg[eq_pos..]))
    } else {
        (arg, None)
    };

    // Already valid? No correction needed.
    if valid_flags.contains(&flag_part) {
        return None;
    }

    // Rule 2: Case normalization — `--Robot` -> `--robot`
    let lower = flag_part.to_lowercase();
    if lower != flag_part
        && let Some(&flag) = valid_flags.iter().find(|f| f.to_lowercase() == lower)
    {
        let corrected = match value_suffix {
            Some(suffix) => format!("{flag}{suffix}"),
            None => flag.to_string(),
        };
        return Some(Correction {
            original: arg.to_string(),
            corrected,
            rule: CorrectionRule::CaseNormalization,
            confidence: 0.9,
        });
    }

    // Rule 3: Prefix match — `--proj` -> `--project` (only if unambiguous)
    let prefix_matches: Vec<&str> = valid_flags
        .iter()
        .filter(|f| f.starts_with(&*lower) && f.to_lowercase() != lower)
        .copied()
        .collect();
    if prefix_matches.len() == 1 {
        let matched = prefix_matches[0];
        let corrected = match value_suffix {
            Some(suffix) => format!("{matched}{suffix}"),
            None => matched.to_string(),
        };
        return Some(Correction {
            original: arg.to_string(),
            corrected,
            rule: CorrectionRule::FlagPrefix,
            confidence: 0.95,
        });
    }

    // Rule 4: Fuzzy flag match — higher threshold in strict/robot mode
    let threshold = if strict {
        FUZZY_FLAG_THRESHOLD_STRICT
    } else {
        FUZZY_FLAG_THRESHOLD
    };
    if let Some((best_flag, score)) = best_fuzzy_match(&lower, valid_flags)
        && score >= threshold
    {
        let corrected = match value_suffix {
            Some(suffix) => format!("{best_flag}{suffix}"),
            None => best_flag.to_string(),
        };
        return Some(Correction {
            original: arg.to_string(),
            corrected,
            rule: CorrectionRule::FuzzyFlag,
            confidence: score,
        });
    }

    None
}

/// Find the best fuzzy match among valid flags for a given (lowercased) input.
///
/// Applies a length guard to prevent short candidates (e.g. `--for`, 5 chars
/// including dashes) from inflating Jaro-Winkler scores against long inputs.
/// When the input is more than 40% longer than a candidate, that candidate is
/// excluded from fuzzy consideration (it can still match via prefix rule).
fn best_fuzzy_match<'a>(input: &str, valid_flags: &[&'a str]) -> Option<(&'a str, f64)> {
    valid_flags
        .iter()
        .filter(|&&flag| {
            // Guard: skip short candidates when input is much longer.
            // e.g. "--foobar" (8 chars) should not fuzzy-match "--for" (5 chars)
            // Ratio: input must be within 1.4x the candidate length.
            let max_input_len = (flag.len() as f64 * 1.4) as usize;
            input.len() <= max_input_len
        })
        .map(|&flag| (flag, jaro_winkler(input, flag)))
        .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
}

// ---------------------------------------------------------------------------
// Post-clap suggestion helpers
// ---------------------------------------------------------------------------

/// Given an unrecognized flag (from a clap error), suggest the most similar
/// valid flag for the detected subcommand.
pub fn suggest_similar_flag(invalid_flag: &str, raw_args: &[String]) -> Option<String> {
    let subcommand = detect_subcommand(raw_args);
    let valid = valid_flags_for(subcommand);
    let lower = invalid_flag.to_lowercase();

    let (best_flag, score) = best_fuzzy_match(&lower, &valid)?;
    if score >= 0.6 {
        Some(best_flag.to_string())
    } else {
        None
    }
}

/// Given a flag name, return its valid enum values (if known).
pub fn valid_values_for_flag(flag: &str) -> Option<&'static [&'static str]> {
    let lower = flag.to_lowercase();
    ENUM_VALUES
        .iter()
        .find(|(f, _)| f.to_lowercase() == lower)
        .map(|(_, vals)| *vals)
}

/// Format a human/robot teaching note for a correction.
pub fn format_teaching_note(correction: &Correction) -> String {
    match correction.rule {
        CorrectionRule::SingleDashLongFlag => {
            format!(
                "Use double-dash for long flags: {} (not {})",
                correction.corrected, correction.original
            )
        }
        CorrectionRule::CaseNormalization => {
            format!(
                "Flags are lowercase: {} (not {})",
                correction.corrected, correction.original
            )
        }
        CorrectionRule::FuzzyFlag => {
            format!(
                "Correct spelling: {} (not {})",
                correction.corrected, correction.original
            )
        }
        CorrectionRule::SubcommandAlias => {
            format!(
                "Use canonical command name: {} (not {})",
                correction.corrected, correction.original
            )
        }
        CorrectionRule::SubcommandFuzzy => {
            format!(
                "Correct command spelling: lore {} (not lore {})",
                correction.corrected, correction.original
            )
        }
        CorrectionRule::FlagAsSubcommand => {
            format!(
                "Commands are positional, not flags: lore {} (not lore --{})",
                correction.corrected, correction.corrected
            )
        }
        CorrectionRule::ValueNormalization => {
            format!(
                "Values are lowercase: {} (not {})",
                correction.corrected, correction.original
            )
        }
        CorrectionRule::ValueFuzzy => {
            format!(
                "Correct value spelling: {} (not {})",
                correction.corrected, correction.original
            )
        }
        CorrectionRule::FlagPrefix => {
            format!(
                "Use full flag name: {} (not {})",
                correction.corrected, correction.original
            )
        }
        CorrectionRule::NoColorExpansion => {
            "Use `--color never` instead of `--no-color`".to_string()
        }
    }
}

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

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

    fn args(s: &str) -> Vec<String> {
        s.split_whitespace().map(String::from).collect()
    }

    // ---- Single-dash long flag ----

    #[test]
    fn single_dash_robot() {
        let result = correct_args(args("lore -robot issues -n 5"), false);
        assert_eq!(result.corrections.len(), 1);
        assert_eq!(result.corrections[0].original, "-robot");
        assert_eq!(result.corrections[0].corrected, "--robot");
        assert_eq!(
            result.corrections[0].rule,
            CorrectionRule::SingleDashLongFlag
        );
        assert_eq!(result.args, args("lore --robot issues -n 5"));
    }

    #[test]
    fn single_dash_state() {
        let result = correct_args(args("lore --robot issues -state opened"), false);
        assert_eq!(result.corrections.len(), 1);
        assert_eq!(result.corrections[0].corrected, "--state");
    }

    // ---- Case normalization ----

    #[test]
    fn case_robot() {
        let result = correct_args(args("lore --Robot issues"), false);
        assert_eq!(result.corrections.len(), 1);
        assert_eq!(result.corrections[0].corrected, "--robot");
        assert_eq!(
            result.corrections[0].rule,
            CorrectionRule::CaseNormalization
        );
    }

    #[test]
    fn case_state_upper() {
        let result = correct_args(args("lore --robot issues --State opened"), false);
        assert_eq!(result.corrections.len(), 1);
        assert_eq!(result.corrections[0].corrected, "--state");
        assert_eq!(
            result.corrections[0].rule,
            CorrectionRule::CaseNormalization
        );
    }

    #[test]
    fn case_all_upper() {
        let result = correct_args(args("lore --ROBOT issues --STATE opened"), false);
        assert_eq!(result.corrections.len(), 2);
        assert_eq!(result.corrections[0].corrected, "--robot");
        assert_eq!(result.corrections[1].corrected, "--state");
    }

    // ---- Fuzzy flag match ----

    #[test]
    fn fuzzy_staate() {
        let result = correct_args(args("lore --robot issues --staate opened"), false);
        assert_eq!(result.corrections.len(), 1);
        assert_eq!(result.corrections[0].corrected, "--state");
        assert_eq!(result.corrections[0].rule, CorrectionRule::FuzzyFlag);
    }

    #[test]
    fn fuzzy_projct() {
        let result = correct_args(args("lore --robot issues --projct group/repo"), false);
        assert_eq!(result.corrections.len(), 1);
        assert_eq!(result.corrections[0].corrected, "--project");
        assert_eq!(result.corrections[0].rule, CorrectionRule::FuzzyFlag);
    }

    // ---- No corrections ----

    #[test]
    fn already_correct() {
        let original = args("lore --robot issues --state opened -n 10");
        let result = correct_args(original.clone(), false);
        assert!(result.corrections.is_empty());
        assert_eq!(result.args, original);
    }

    #[test]
    fn short_flags_untouched() {
        let original = args("lore -J issues -n 10 -s opened -p group/repo");
        let result = correct_args(original.clone(), false);
        assert!(result.corrections.is_empty());
    }

    #[test]
    fn stacked_short_flags_untouched() {
        let original = args("lore -vvv issues");
        let result = correct_args(original.clone(), false);
        assert!(result.corrections.is_empty());
    }

    #[test]
    fn positional_args_untouched() {
        let result = correct_args(args("lore --robot search authentication"), false);
        assert!(result.corrections.is_empty());
    }

    #[test]
    fn wildly_wrong_flag_not_corrected() {
        // `--xyzzy` shouldn't match anything above 0.8
        let result = correct_args(args("lore --robot issues --xyzzy foo"), false);
        assert!(result.corrections.is_empty());
    }

    // ---- Flag with = value ----

    #[test]
    fn flag_eq_value_case_correction() {
        let result = correct_args(args("lore --robot issues --State=opened"), false);
        assert_eq!(result.corrections.len(), 1);
        assert_eq!(result.corrections[0].corrected, "--state=opened");
    }

    // ---- Multiple corrections in one invocation ----

    #[test]
    fn multiple_corrections() {
        let result = correct_args(
            args("lore -robot issues --State opened --projct group/repo"),
            false,
        );
        assert_eq!(result.corrections.len(), 3);
        assert_eq!(result.args[1], "--robot");
        assert_eq!(result.args[3], "--state");
        assert_eq!(result.args[5], "--project");
    }

    // ---- B1: POSIX -- option terminator ----

    #[test]
    fn option_terminator_stops_corrections() {
        let result = correct_args(args("lore issues -- --staate --projct"), false);
        // Nothing after `--` should be corrected
        assert!(result.corrections.is_empty());
        assert_eq!(result.args[2], "--");
        assert_eq!(result.args[3], "--staate");
        assert_eq!(result.args[4], "--projct");
    }

    #[test]
    fn correction_before_terminator_still_works() {
        let result = correct_args(args("lore --Robot issues -- --staate"), false);
        assert_eq!(result.corrections.len(), 1);
        assert_eq!(result.corrections[0].corrected, "--robot");
        assert_eq!(result.args[4], "--staate"); // untouched after --
    }

    // ---- B2: Clap built-in flags not corrected ----

    #[test]
    fn version_flag_not_corrected() {
        let result = correct_args(args("lore --version"), false);
        assert!(result.corrections.is_empty());
        assert_eq!(result.args[1], "--version");
    }

    #[test]
    fn help_flag_not_corrected() {
        let result = correct_args(args("lore --help"), false);
        assert!(result.corrections.is_empty());
        assert_eq!(result.args[1], "--help");
    }

    // ---- Strict mode (robot) uses higher fuzzy threshold ----

    #[test]
    fn strict_mode_rejects_low_confidence_fuzzy() {
        // `--staate` vs `--state` — close but may be below strict threshold (0.9)
        // The exact score depends on Jaro-Winkler; this tests that the strict
        // threshold is higher than non-strict.
        let non_strict = correct_args(args("lore --robot issues --staate opened"), false);
        assert_eq!(non_strict.corrections.len(), 1);
        assert_eq!(non_strict.corrections[0].rule, CorrectionRule::FuzzyFlag);

        // In strict mode, same typo might or might not match depending on JW score.
        // We verify that at least wildly wrong flags are still rejected.
        let strict = correct_args(args("lore --robot issues --xyzzy foo"), true);
        assert!(strict.corrections.is_empty());
    }

    #[test]
    fn strict_mode_still_fixes_case() {
        let result = correct_args(args("lore --Robot issues --State opened"), true);
        assert_eq!(result.corrections.len(), 2);
        assert_eq!(result.corrections[0].corrected, "--robot");
        assert_eq!(result.corrections[1].corrected, "--state");
    }

    #[test]
    fn strict_mode_still_fixes_single_dash() {
        let result = correct_args(args("lore -robot issues"), true);
        assert_eq!(result.corrections.len(), 1);
        assert_eq!(result.corrections[0].corrected, "--robot");
    }

    // ---- Subcommand alias correction ----

    #[test]
    fn subcommand_alias_merge_requests_underscore() {
        let result = correct_args(args("lore --robot merge_requests -n 10"), false);
        assert!(
            result
                .corrections
                .iter()
                .any(|c| c.rule == CorrectionRule::SubcommandAlias && c.corrected == "mrs")
        );
        assert!(result.args.contains(&"mrs".to_string()));
    }

    #[test]
    fn subcommand_alias_mergerequests_no_sep() {
        let result = correct_args(args("lore --robot mergerequests"), false);
        assert!(result.corrections.iter().any(|c| c.corrected == "mrs"));
    }

    #[test]
    fn subcommand_alias_generate_docs_underscore() {
        let result = correct_args(args("lore generate_docs"), false);
        assert!(
            result
                .corrections
                .iter()
                .any(|c| c.corrected == "generate-docs")
        );
    }

    #[test]
    fn subcommand_alias_case_insensitive() {
        let result = correct_args(args("lore Merge_Requests"), false);
        assert!(result.corrections.iter().any(|c| c.corrected == "mrs"));
    }

    #[test]
    fn subcommand_alias_valid_command_untouched() {
        let result = correct_args(args("lore issues -n 10"), false);
        assert!(result.corrections.is_empty());
    }

    // ---- Enum value normalization ----

    #[test]
    fn value_case_normalization() {
        let result = correct_args(args("lore issues --state Opened"), false);
        assert!(
            result
                .corrections
                .iter()
                .any(|c| c.rule == CorrectionRule::ValueNormalization && c.corrected == "opened")
        );
        assert!(result.args.contains(&"opened".to_string()));
    }

    #[test]
    fn value_case_normalization_eq_form() {
        let result = correct_args(args("lore issues --state=Opened"), false);
        assert!(
            result
                .corrections
                .iter()
                .any(|c| c.corrected == "--state=opened")
        );
    }

    #[test]
    fn value_prefix_expansion() {
        // "open" is a unique prefix of "opened"
        let result = correct_args(args("lore issues --state open"), false);
        assert!(
            result
                .corrections
                .iter()
                .any(|c| c.corrected == "opened" && c.rule == CorrectionRule::ValueFuzzy)
        );
    }

    #[test]
    fn value_fuzzy_typo() {
        let result = correct_args(args("lore issues --state opend"), false);
        assert!(result.corrections.iter().any(|c| c.corrected == "opened"));
    }

    #[test]
    fn value_already_valid_untouched() {
        let result = correct_args(args("lore issues --state opened"), false);
        // No value corrections expected (flag corrections may still exist)
        assert!(!result.corrections.iter().any(|c| matches!(
            c.rule,
            CorrectionRule::ValueNormalization | CorrectionRule::ValueFuzzy
        )));
    }

    #[test]
    fn value_mode_case() {
        let result = correct_args(args("lore search --mode Hybrid query"), false);
        assert!(result.corrections.iter().any(|c| c.corrected == "hybrid"));
    }

    #[test]
    fn value_normalization_respects_option_terminator() {
        // Values after `--` are positional and must not be corrected
        let result = correct_args(args("lore search -- --state Opened"), false);
        assert!(!result.corrections.iter().any(|c| matches!(
            c.rule,
            CorrectionRule::ValueNormalization | CorrectionRule::ValueFuzzy
        )));
        assert_eq!(result.args[4], "Opened"); // preserved as-is
    }

    // ---- Flag prefix matching ----

    #[test]
    fn flag_prefix_project() {
        let result = correct_args(args("lore issues --proj group/repo"), false);
        assert!(
            result
                .corrections
                .iter()
                .any(|c| c.rule == CorrectionRule::FlagPrefix && c.corrected == "--project")
        );
    }

    #[test]
    fn flag_prefix_ambiguous_not_corrected() {
        // --s could be --state, --since, --sort, --status — ambiguous
        let result = correct_args(args("lore issues --s opened"), false);
        assert!(
            !result
                .corrections
                .iter()
                .any(|c| c.rule == CorrectionRule::FlagPrefix)
        );
    }

    #[test]
    fn flag_prefix_with_eq_value() {
        let result = correct_args(args("lore issues --proj=group/repo"), false);
        assert!(
            result
                .corrections
                .iter()
                .any(|c| c.corrected == "--project=group/repo")
        );
    }

    // ---- Teaching notes ----

    #[test]
    fn teaching_note_single_dash() {
        let c = Correction {
            original: "-robot".to_string(),
            corrected: "--robot".to_string(),
            rule: CorrectionRule::SingleDashLongFlag,
            confidence: 0.95,
        };
        let note = format_teaching_note(&c);
        assert!(note.contains("double-dash"));
        assert!(note.contains("--robot"));
    }

    #[test]
    fn teaching_note_case() {
        let c = Correction {
            original: "--State".to_string(),
            corrected: "--state".to_string(),
            rule: CorrectionRule::CaseNormalization,
            confidence: 0.9,
        };
        let note = format_teaching_note(&c);
        assert!(note.contains("lowercase"));
    }

    #[test]
    fn teaching_note_fuzzy() {
        let c = Correction {
            original: "--staate".to_string(),
            corrected: "--state".to_string(),
            rule: CorrectionRule::FuzzyFlag,
            confidence: 0.85,
        };
        let note = format_teaching_note(&c);
        assert!(note.contains("spelling"));
    }

    #[test]
    fn teaching_note_subcommand_alias() {
        let c = Correction {
            original: "merge_requests".to_string(),
            corrected: "mrs".to_string(),
            rule: CorrectionRule::SubcommandAlias,
            confidence: 1.0,
        };
        let note = format_teaching_note(&c);
        assert!(note.contains("canonical"));
        assert!(note.contains("mrs"));
    }

    #[test]
    fn teaching_note_value_normalization() {
        let c = Correction {
            original: "Opened".to_string(),
            corrected: "opened".to_string(),
            rule: CorrectionRule::ValueNormalization,
            confidence: 0.95,
        };
        let note = format_teaching_note(&c);
        assert!(note.contains("lowercase"));
    }

    #[test]
    fn teaching_note_flag_prefix() {
        let c = Correction {
            original: "--proj".to_string(),
            corrected: "--project".to_string(),
            rule: CorrectionRule::FlagPrefix,
            confidence: 0.95,
        };
        let note = format_teaching_note(&c);
        assert!(note.contains("full flag name"));
    }

    // ---- --no-color expansion ----

    #[test]
    fn no_color_expands_to_color_never() {
        let result = correct_args(args("lore --no-color health"), false);
        assert_eq!(result.corrections.len(), 1);
        assert_eq!(result.corrections[0].rule, CorrectionRule::NoColorExpansion);
        assert_eq!(result.args, args("lore --color never health"));
    }

    #[test]
    fn no_color_case_insensitive() {
        let result = correct_args(args("lore --No-Color issues"), false);
        assert_eq!(result.corrections.len(), 1);
        assert_eq!(result.args, args("lore --color never issues"));
    }

    #[test]
    fn no_color_with_robot_mode() {
        let result = correct_args(args("lore --robot --no-color health"), true);
        assert_eq!(result.corrections.len(), 1);
        assert_eq!(result.args, args("lore --robot --color never health"));
    }

    // ---- Fuzzy matching length guard ----

    #[test]
    fn foobar_does_not_match_for() {
        // --foobar (8 chars) should NOT fuzzy-match --for (5 chars)
        let result = correct_args(args("lore count --foobar issues"), false);
        assert!(
            !result.corrections.iter().any(|c| c.corrected == "--for"),
            "expected --foobar not to match --for"
        );
    }

    #[test]
    fn fro_still_matches_for() {
        // --fro (5 chars) is short enough to fuzzy-match --for (5 chars)
        // and also qualifies as a prefix match
        let result = correct_args(args("lore count --fro issues"), false);
        assert!(
            result.corrections.iter().any(|c| c.corrected == "--for"),
            "expected --fro to match --for"
        );
    }

    // ---- Post-clap suggestion helpers ----

    #[test]
    fn suggest_similar_flag_works() {
        let raw = args("lore --robot issues --xstat opened");
        let suggestion = suggest_similar_flag("--xstat", &raw);
        // Should suggest --state (close enough with lower threshold 0.6)
        assert!(suggestion.is_some());
    }

    #[test]
    fn valid_values_for_state() {
        let vals = valid_values_for_flag("--state");
        assert!(vals.is_some());
        let vals = vals.unwrap();
        assert!(vals.contains(&"opened"));
        assert!(vals.contains(&"closed"));
    }

    #[test]
    fn valid_values_unknown_flag() {
        assert!(valid_values_for_flag("--xyzzy").is_none());
    }

    // ---- Subcommand detection ----

    #[test]
    fn detect_subcommand_basic() {
        assert_eq!(
            detect_subcommand(&args("lore issues -n 10")),
            Some("issues")
        );
    }

    #[test]
    fn detect_subcommand_with_globals() {
        assert_eq!(
            detect_subcommand(&args("lore --robot --config /tmp/c.json mrs")),
            Some("mrs")
        );
    }

    #[test]
    fn detect_subcommand_with_color() {
        assert_eq!(
            detect_subcommand(&args("lore --color never issues")),
            Some("issues")
        );
    }

    #[test]
    fn detect_subcommand_none() {
        assert_eq!(detect_subcommand(&args("lore --robot")), None);
    }

    // ---- Fuzzy subcommand matching (A2) ----

    #[test]
    fn fuzzy_subcommand_issuess() {
        let result = correct_args(args("lore --robot issuess -n 10"), false);
        assert!(
            result
                .corrections
                .iter()
                .any(|c| c.rule == CorrectionRule::SubcommandFuzzy && c.corrected == "issues"),
            "expected 'issuess' to fuzzy-match 'issues'"
        );
        assert!(result.args.contains(&"issues".to_string()));
    }

    #[test]
    fn fuzzy_subcommand_timline() {
        let result = correct_args(args("lore timline \"auth\""), false);
        assert!(
            result.corrections.iter().any(|c| c.corrected == "timeline"),
            "expected 'timline' to fuzzy-match 'timeline'"
        );
    }

    #[test]
    fn fuzzy_subcommand_serach() {
        let result = correct_args(args("lore --robot serach \"auth bug\""), false);
        assert!(
            result.corrections.iter().any(|c| c.corrected == "search"),
            "expected 'serach' to fuzzy-match 'search'"
        );
    }

    #[test]
    fn fuzzy_subcommand_already_valid_untouched() {
        let result = correct_args(args("lore issues -n 10"), false);
        assert!(
            !result
                .corrections
                .iter()
                .any(|c| c.rule == CorrectionRule::SubcommandFuzzy)
        );
    }

    #[test]
    fn fuzzy_subcommand_robot_not_matched_to_robot_docs() {
        // "robot" looks like a misplaced --robot flag, not a typo for "robot-docs"
        let result = correct_args(args("lore robot issues"), false);
        assert!(
            !result
                .corrections
                .iter()
                .any(|c| c.rule == CorrectionRule::SubcommandFuzzy),
            "expected 'robot' NOT to fuzzy-match 'robot-docs' (it's a misplaced flag)"
        );
    }

    #[test]
    fn fuzzy_subcommand_prefix_deferred_to_clap() {
        // "iss" is a prefix of "issues" — clap's infer_subcommands handles this
        let result = correct_args(args("lore iss -n 10"), false);
        assert!(
            !result
                .corrections
                .iter()
                .any(|c| c.rule == CorrectionRule::SubcommandFuzzy),
            "expected prefix 'iss' NOT to be fuzzy-matched (clap handles it)"
        );
    }

    #[test]
    fn fuzzy_subcommand_wildly_wrong_not_matched() {
        let result = correct_args(args("lore xyzzyplugh"), false);
        assert!(
            !result
                .corrections
                .iter()
                .any(|c| c.rule == CorrectionRule::SubcommandFuzzy),
            "expected gibberish NOT to fuzzy-match any command"
        );
    }

    // ---- Flag-as-subcommand (A3) ----

    #[test]
    fn flag_as_subcommand_robot_docs() {
        let result = correct_args(args("lore --robot-docs"), false);
        assert!(
            result
                .corrections
                .iter()
                .any(|c| c.rule == CorrectionRule::FlagAsSubcommand && c.corrected == "robot-docs"),
            "expected '--robot-docs' to be corrected to 'robot-docs'"
        );
        assert!(result.args.contains(&"robot-docs".to_string()));
    }

    #[test]
    fn flag_as_subcommand_generate_docs() {
        let result = correct_args(args("lore --generate-docs"), false);
        assert!(
            result
                .corrections
                .iter()
                .any(|c| c.corrected == "generate-docs"),
            "expected '--generate-docs' to be corrected to 'generate-docs'"
        );
    }

    #[test]
    fn flag_as_subcommand_with_robot_flag() {
        // `lore --robot --robot-docs` — --robot is a valid global flag, --robot-docs is not
        let result = correct_args(args("lore --robot --robot-docs"), false);
        assert!(
            result
                .corrections
                .iter()
                .any(|c| c.corrected == "robot-docs"),
        );
        assert_eq!(result.args, args("lore --robot robot-docs"));
    }

    #[test]
    fn flag_as_subcommand_does_not_touch_real_flags() {
        // --robot is a real global flag, should NOT be rewritten to "robot"
        let result = correct_args(args("lore --robot issues"), false);
        assert!(
            !result
                .corrections
                .iter()
                .any(|c| c.rule == CorrectionRule::FlagAsSubcommand),
        );
    }

    #[test]
    fn flag_as_subcommand_not_triggered_when_subcommand_present() {
        // A subcommand IS detected, so A3 shouldn't activate
        let result = correct_args(args("lore issues --robot-docs"), false);
        assert!(
            !result
                .corrections
                .iter()
                .any(|c| c.rule == CorrectionRule::FlagAsSubcommand),
            "expected A3 not to trigger when subcommand is already present"
        );
    }

    // ---- Teaching notes for new rules ----

    #[test]
    fn teaching_note_subcommand_fuzzy() {
        let c = Correction {
            original: "issuess".to_string(),
            corrected: "issues".to_string(),
            rule: CorrectionRule::SubcommandFuzzy,
            confidence: 0.92,
        };
        let note = format_teaching_note(&c);
        assert!(note.contains("spelling"));
        assert!(note.contains("issues"));
    }

    #[test]
    fn teaching_note_flag_as_subcommand() {
        let c = Correction {
            original: "--robot-docs".to_string(),
            corrected: "robot-docs".to_string(),
            rule: CorrectionRule::FlagAsSubcommand,
            confidence: 1.0,
        };
        let note = format_teaching_note(&c);
        assert!(note.contains("positional"));
        assert!(note.contains("robot-docs"));
    }

    // ---- Canonical subcommands registry drift test ----

    #[test]
    fn canonical_subcommands_covers_clap() {
        use clap::CommandFactory;
        let cmd = crate::cli::Cli::command();

        for sub in cmd.get_subcommands() {
            let name = sub.get_name();
            assert!(
                CANONICAL_SUBCOMMANDS.contains(&name),
                "Clap subcommand '{name}' is missing from CANONICAL_SUBCOMMANDS. \
                 Add it to autocorrect.rs."
            );
        }
    }

    // ---- Registry drift test ----
    // This test uses clap introspection to verify our static registry covers
    // all long flags defined in the Cli struct.

    #[test]
    fn registry_covers_global_flags() {
        use clap::CommandFactory;
        let cmd = crate::cli::Cli::command();

        let clap_globals: Vec<String> = cmd
            .get_arguments()
            .filter_map(|a| a.get_long().map(|l| format!("--{l}")))
            .collect();

        for flag in &clap_globals {
            // Skip help/version — clap adds these automatically
            if flag == "--help" || flag == "--version" {
                continue;
            }
            assert!(
                GLOBAL_FLAGS.contains(&flag.as_str()),
                "Clap global flag {flag} is missing from GLOBAL_FLAGS registry. \
                 Add it to GLOBAL_FLAGS in autocorrect.rs."
            );
        }
    }

    #[test]
    fn registry_covers_command_flags() {
        use clap::CommandFactory;
        let cmd = crate::cli::Cli::command();

        for sub in cmd.get_subcommands() {
            let sub_name = sub.get_name().to_string();

            // Find our registry entry
            let registry_entry = COMMAND_FLAGS.iter().find(|(name, _)| *name == sub_name);

            // Not all subcommands need entries (e.g., version, auth, status
            // with no subcommand-specific flags)
            let clap_flags: Vec<String> = sub
                .get_arguments()
                .filter_map(|a| a.get_long().map(|l| format!("--{l}")))
                .filter(|f| !GLOBAL_FLAGS.contains(&f.as_str()))
                .filter(|f| f != "--help" && f != "--version")
                .collect();

            if clap_flags.is_empty() {
                continue;
            }

            let registry_flags = registry_entry.map(|(_, flags)| *flags);
            let registry_flags = registry_flags.unwrap_or_else(|| {
                panic!(
                    "Subcommand '{sub_name}' has clap flags {clap_flags:?} but no COMMAND_FLAGS \
                     registry entry. Add it to COMMAND_FLAGS in autocorrect.rs."
                )
            });

            for flag in &clap_flags {
                assert!(
                    registry_flags.contains(&flag.as_str()),
                    "Clap flag {flag} on subcommand '{sub_name}' is missing from \
                     COMMAND_FLAGS registry. Add it to the '{sub_name}' entry in autocorrect.rs."
                );
            }
        }
    }
}
