mod arena;
mod boss;
mod messages;
mod character;
mod display;
mod events;
mod help;
mod journal;
mod loot;
mod sage;
mod state;
mod zones;

use character::{Class, Race};
use clap::{Parser, Subcommand};
use colored::*;
use std::io::{self, IsTerminal, Write};

#[derive(Parser)]
#[command(
    name = "sq",
    version,
    about = "A passive RPG that lives in your terminal",
    disable_help_subcommand = true
)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// View the in-game manual; pass an optional topic for a specific command guide
    Help {
        /// Topic name (e.g. arena, status, journal); omit for the index
        topic: Option<String>,
    },
    /// Create a new character
    Init,
    /// View your character sheet (includes inventory)
    #[clap(visible_alias = "stat")]
    Status,
    /// Check your inventory
    #[clap(visible_alias = "inv")]
    Inventory,
    /// View your adventure journal
    Journal,
    /// Process a terminal command (called by shell hook)
    Tick {
        /// The command that was run
        #[arg(long, default_value = "")]
        cmd: String,
        /// Current working directory
        #[arg(long, default_value = ".")]
        cwd: String,
        /// Exit code of the command
        #[arg(long, default_value_t = 0)]
        exit_code: i32,
        /// Force the update sage to appear (for testing)
        #[arg(long, hide = true)]
        test_sage: bool,
    },
    /// Print or install the shell hook
    Hook {
        /// Shell type: bash, zsh, or fish
        #[arg(long, default_value = "zsh")]
        shell: String,
        /// Install hook directly to a file (default: ~/.zshrc, ~/.bashrc, or fish config)
        #[arg(long)]
        install: bool,
        /// Custom file to install the hook to (implies --install)
        #[arg(long)]
        file: Option<String>,
    },
    /// Equip armor or ring from inventory
    #[clap(visible_alias = "wear")]
    Equip {
        /// Item name (or partial match)
        name: Vec<String>,
    },
    /// Wield a weapon from inventory
    Wield {
        /// Item name (or partial match)
        name: Vec<String>,
    },
    /// Remove equipped weapon, armor, or ring (puts it back in inventory)
    #[clap(visible_alias = "unequip")]
    Remove {
        /// Item name, partial match, or slot keyword (weapon/armor/ring)
        name: Vec<String>,
    },
    /// Drop an item from inventory permanently
    Drop {
        /// Item name (or partial match)
        name: Vec<String>,
    },
    /// Browse the shop (must be in home directory)
    Shop,
    /// Buy an item from the shop by number (see `sq shop` for numbered list)
    Buy {
        /// Item number from the shop list
        number: usize,
    },
    /// Sell an inventory item at the shop by number or name; `sq sell junk` sweeps all Common/Uncommon items
    Sell {
        /// Item number (1-indexed), partial name match, or the literal word `junk`
        item: Vec<String>,
    },
    /// Enchant an equipped item (+1 power, max +5). Wizard class can enchant anywhere; others must be in $HOME.
    Enchant {
        /// Equipped item name (partial match)
        name: Vec<String>,
    },
    /// Show full stats and modifiers for an inventory or equipped item
    #[clap(visible_alias = "id")]
    Identify {
        /// Item name (or partial match); append `.N` to pick the N-th match
        name: Vec<String>,
    },
    /// Drink a potion from inventory to restore HP
    Drink {
        /// Item name (or partial match)
        name: Vec<String>,
    },
    /// Prestige: reset to level 1 with a subclass and bonus stats
    Prestige,
    /// Reset your character (start over)
    Reset,
    /// Update sq to the latest version
    Update,
    /// Enter the Arena (interactive combat gauntlet)
    Arena,
    /// Enter the Terminal Gauntlet tournament (deprecated; use `arena`)
    Tournament,
}

fn main() {
    let cli = Cli::parse();

    match cli.command {
        Commands::Help { topic } => cmd_help(topic.as_deref()),
        Commands::Init => cmd_init(),
        Commands::Status => cmd_status(),
        Commands::Inventory => cmd_inventory(),
        Commands::Journal => cmd_journal(),
        Commands::Tick {
            cmd,
            cwd,
            exit_code,
            test_sage,
        } => cmd_tick(&cmd, &cwd, exit_code, test_sage),
        Commands::Hook { shell, install, file } => cmd_hook(&shell, install || file.is_some(), file),
        Commands::Shop => cmd_shop(),
        Commands::Buy { number } => cmd_buy(number),
        Commands::Sell { item } => cmd_sell(&item.join(" ")),
        Commands::Enchant { name } => cmd_enchant(&name.join(" ")),
        Commands::Identify { name } => cmd_identify(&name.join(" ")),
        Commands::Equip { name } => cmd_equip(&name.join(" ")),
        Commands::Wield { name } => cmd_wield(&name.join(" ")),
        Commands::Remove { name } => cmd_remove(&name.join(" ")),
        Commands::Drop { name } => cmd_drop_item(&name.join(" ")),
        Commands::Drink { name } => cmd_drink(&name.join(" ")),
        Commands::Prestige => cmd_prestige(),
        Commands::Reset => cmd_reset(),
        Commands::Update => cmd_update(),
        Commands::Arena => cmd_arena(false),
        Commands::Tournament => cmd_arena(true),
    }
}

fn prompt(msg: &str) -> String {
    print!("{}", msg);
    io::stdout().flush().unwrap();
    let mut input = String::new();
    io::stdin().read_line(&mut input).unwrap();
    input.trim().to_string()
}

fn cmd_init() {
    if state::save_path().exists() {
        let answer = prompt(&format!(
            "{} A character already exists! Overwrite? [y/N] ",
            "⚠️".yellow()
        ));
        if answer.to_lowercase() != "y" {
            println!("{}", "Cancelled.".dimmed());
            return;
        }
    }

    println!();
    println!(
        "{}",
        "⚔️  Welcome to sq — The Passive Terminal RPG ⚔️"
            .bold()
            .cyan()
    );
    println!(
        "{}",
        "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".dimmed()
    );
    println!();

    // Name
    let name = loop {
        let n = prompt(&format!("{} What is your name, adventurer? ", "📝".bold()));
        if !n.is_empty() {
            break n;
        }
        println!("{}", "  Please enter a name.".red());
    };

    println!();

    // Class
    println!("{}", "Choose your class:".bold().yellow());
    println!(
        "  {} {} — High INT, arcane power",
        "1.".dimmed(),
        "Wizard".blue().bold()
    );
    println!(
        "  {} {} — High STR, melee combat",
        "2.".dimmed(),
        "Warrior".red().bold()
    );
    println!(
        "  {} {} — High DEX, critical strikes",
        "3.".dimmed(),
        "Rogue".green().bold()
    );
    println!(
        "  {} {} — Balanced DEX/STR, versatile",
        "4.".dimmed(),
        "Ranger".yellow().bold()
    );
    println!(
        "  {} {} — Highest INT, dark arts",
        "5.".dimmed(),
        "Necromancer".magenta().bold()
    );

    let class = loop {
        let c = prompt(&format!("{} Choose [1-5]: ", "🎭".bold()));
        match c.as_str() {
            "1" => break Class::Wizard,
            "2" => break Class::Warrior,
            "3" => break Class::Rogue,
            "4" => break Class::Ranger,
            "5" => break Class::Necromancer,
            _ => println!("{}", "  Pick 1-5.".red()),
        }
    };

    println!();

    // Race
    println!("{}", "Choose your race:".bold().yellow());
    println!(
        "  {} {} — Balanced stats (+1/+1/+1)",
        "1.".dimmed(),
        "Human".white().bold()
    );
    println!(
        "  {} {} — Agile & wise (+0/+2/+2)",
        "2.".dimmed(),
        "Elf".cyan().bold()
    );
    println!(
        "  {} {} — Tough & sturdy (+3/+0/+1)",
        "3.".dimmed(),
        "Dwarf".yellow().bold()
    );
    println!(
        "  {} {} — Raw strength (+4/+1/-1)",
        "4.".dimmed(),
        "Orc".red().bold()
    );
    println!(
        "  {} {} — Quick & clever (-1/+3/+1)",
        "5.".dimmed(),
        "Goblin".green().bold()
    );

    let race = loop {
        let r = prompt(&format!("{} Choose [1-5]: ", "🧬".bold()));
        match r.as_str() {
            "1" => break Race::Human,
            "2" => break Race::Elf,
            "3" => break Race::Dwarf,
            "4" => break Race::Orc,
            "5" => break Race::Goblin,
            _ => println!("{}", "  Pick 1-5.".red()),
        }
    };

    let character = character::Character::new(name.clone(), class, race);
    let mut game_state = state::GameState::new(character);

    println!();
    println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".red().bold());
    println!("{} {}", "💀".bold(), "PERMADEATH MODE".red().bold());
    println!("  If you die, your character is gone forever. All is lost.");
    println!("  In standard mode, death resets your XP to 0 and costs 15% gold.");
    println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".red().bold());
    let pd_answer = prompt("Enable permadeath? [y/N] ");
    game_state.permadeath = pd_answer.trim().to_lowercase() == "y";

    if game_state.permadeath {
        println!(
            "{} {}",
            "☠".red().bold(),
            "Permadeath enabled. May the void be merciful."
                .red()
                .dimmed()
        );
    } else {
        println!(
            "{} {}",
            "✓".green(),
            "Standard mode. Death is a setback, not the end.".dimmed()
        );
    }

    match state::save(&game_state) {
        Ok(()) => {
            println!();
            println!(
                "{}",
                "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".dimmed()
            );
            println!(
                "{} {} has entered the terminal realm!",
                "🎉".bold(),
                name.bold().green()
            );
            println!();
            println!("  Run {} to install the shell hook.", "sq hook --shell zsh".cyan());
            println!(
                "  Run {} to see your character.",
                "sq status".cyan()
            );
            println!(
                "{}",
                "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".dimmed()
            );
            println!();
        }
        Err(e) => {
            eprintln!("{} Failed to save: {}", "❌".bold(), e.red());
        }
    }
}

fn cmd_status() {
    match state::load() {
        Ok(game) => {
            display::print_status(&game.character, game.permadeath);
            display::print_inventory(&game.character);
        }
        Err(e) => eprintln!("{} {}", "❌".bold(), e.red()),
    }
}

fn cmd_inventory() {
    match state::load() {
        Ok(game) => display::print_inventory(&game.character),
        Err(e) => eprintln!("{} {}", "❌".bold(), e.red()),
    }
}

fn cmd_journal() {
    match state::load() {
        Ok(game) => display::print_journal(&game.journal),
        Err(e) => eprintln!("{} {}", "❌".bold(), e.red()),
    }
}

fn format_help(topic: Option<&str>) -> String {
    match topic {
        None => help::render_index(),
        Some(name) => match help::lookup_topic(name) {
            help::LookupResult::Found(t) => help::render_topic(t),
            help::LookupResult::Suggestions(s) => help::render_no_match(name, &s),
            help::LookupResult::NoMatch => help::render_no_match(name, &[]),
        },
    }
}

fn cmd_help(topic: Option<&str>) {
    print!("{}", format_help(topic));
}

fn cmd_tick(cmd: &str, cwd: &str, exit_code: i32, test_sage: bool) {
    let mut game = match state::load() {
        Ok(g) => g,
        Err(_) => return, // Silently skip if no character
    };

    events::tick(&mut game, cmd, cwd, exit_code);
    if test_sage {
        sage::force_show_sage(&mut game);
    } else {
        sage::maybe_show_sage(&mut game);
    }
    game.last_tick = chrono::Utc::now();

    if let Err(e) = state::save(&game) {
        eprintln!("{} Failed to save: {}", "❌".bold(), e.red());
    }
}

fn hook_code(shell: &str) -> Option<String> {
    match shell {
        "bash" => Some(r#"
# shellquest (sq) — passive terminal RPG hook
__sq_hook() {
    local exit_code=$?
    local cmd=$(HISTTIMEFORMAT= history 1 | sed 's/^ *[0-9]* *//')
    local first=$(printf '%s' "$cmd" | awk '{print $1}')
    [ "$first" = "sq" ] && return
    sq tick --cmd "$cmd" --cwd "$PWD" --exit-code "$exit_code"
}
PROMPT_COMMAND="__sq_hook;$PROMPT_COMMAND"
"#.to_string()),
        "zsh" => Some(r#"
# shellquest (sq) — passive terminal RPG hook
__sq_hook() {
    local exit_code=$?
    local cmd=$(fc -ln -1)
    local first=${cmd[(w)1]}
    [[ "$first" == "sq" ]] && return
    sq tick --cmd "$cmd" --cwd "$PWD" --exit-code "$exit_code"
}
precmd_functions+=(__sq_hook)
"#.to_string()),
        "fish" => Some(r#"
# shellquest (sq) — passive terminal RPG hook
function __sq_hook --on-event fish_postexec
    set -l cmd $argv[1]
    set -l exit_code $status
    set -l first (string split -m1 ' ' $cmd)[1]
    [ "$first" = "sq" ]; and return
    sq tick --cmd "$cmd" --cwd "$PWD" --exit-code "$exit_code"
end
"#.to_string()),
        _ => None,
    }
}

fn default_rc_file(shell: &str) -> Option<String> {
    let home = dirs::home_dir()?;
    match shell {
        "bash" => Some(home.join(".bashrc").to_string_lossy().to_string()),
        "zsh" => Some(home.join(".zshrc").to_string_lossy().to_string()),
        "fish" => Some(home.join(".config/fish/config.fish").to_string_lossy().to_string()),
        _ => None,
    }
}

fn cmd_hook(shell: &str, install: bool, file: Option<String>) {
    let code = match hook_code(shell) {
        Some(c) => c,
        None => {
            eprintln!(
                "{} Unknown shell: {}. Supported: bash, zsh, fish",
                "❌".bold(),
                shell.red()
            );
            return;
        }
    };

    if !install {
        // Just print the hook code
        print!("{}", code);
        return;
    }

    // Install mode: write to file
    let target = file.or_else(|| default_rc_file(shell));
    let target = match target {
        Some(t) => t,
        None => {
            eprintln!("{} Could not determine rc file for shell: {}", "❌".bold(), shell.red());
            return;
        }
    };

    // Check if hook already installed
    if let Ok(contents) = std::fs::read_to_string(&target) {
        if contents.contains("__sq_hook") {
            println!(
                "{} Hook already installed in {}",
                "✓".green().bold(),
                target.cyan()
            );
            return;
        }
    }

    // Append hook
    use std::fs::OpenOptions;
    match OpenOptions::new().create(true).append(true).open(&target) {
        Ok(mut f) => {
            use std::io::Write;
            if let Err(e) = f.write_all(code.as_bytes()) {
                eprintln!("{} Failed to write hook: {}", "❌".bold(), e.to_string().red());
                return;
            }
            println!(
                "{} Hook installed to {}",
                "✓".green().bold(),
                target.cyan()
            );
            println!(
                "  Run {} or restart your terminal to activate.",
                format!("source {}", target).dimmed()
            );
        }
        Err(e) => {
            eprintln!("{} Failed to open {}: {}", "❌".bold(), target, e.to_string().red());
        }
    }
}

fn cmd_prestige() {
    let mut game = match state::load() {
        Ok(g) => g,
        Err(e) => {
            eprintln!("{} {}", "❌".bold(), e.red());
            return;
        }
    };

    if !game.character.can_prestige() {
        println!(
            "{} You must reach level {} to prestige. Current level: {}",
            "⚠️".yellow(),
            format!("{}", character::MAX_LEVEL).cyan().bold(),
            format!("{}", game.character.level).white().bold()
        );
        return;
    }

    println!();
    println!(
        "{}",
        "✨ PRESTIGE ✨"
            .yellow()
            .bold()
            .on_black()
    );
    println!(
        "{}",
        "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".yellow()
    );
    println!();
    println!(
        "  You will {} to level {} but gain:",
        "reset".red().bold(),
        "1".white().bold()
    );
    println!(
        "  {} {} to all stats per prestige tier",
        "•".yellow(),
        "+2".green().bold()
    );
    println!(
        "  {} A {} with unique stat bonuses",
        "•".yellow(),
        "subclass".magenta().bold()
    );
    println!(
        "  {} {} HP per prestige tier",
        "•".yellow(),
        "+10".green().bold()
    );
    println!(
        "  {} You {} your gold, gear, kills, and inventory",
        "•".yellow(),
        "keep".green().bold()
    );
    println!();

    let subclasses = character::Subclass::available_for(&game.character.class);
    println!("{}", "Choose your subclass:".bold().yellow());
    for (i, sub) in subclasses.iter().enumerate() {
        let (s, d, int) = sub.stat_bonus();
        println!(
            "  {} {} — STR:{} DEX:{} INT:{}",
            format!("{}.", i + 1).dimmed(),
            format!("{}", sub).magenta().bold(),
            format!("+{}", s).red(),
            format!("+{}", d).green(),
            format!("+{}", int).blue()
        );
    }

    let subclass = loop {
        let choice = prompt(&format!("{} Choose [1-{}]: ", "🎭".bold(), subclasses.len()));
        if let Ok(n) = choice.parse::<usize>() {
            if n >= 1 && n <= subclasses.len() {
                break subclasses[n - 1].clone();
            }
        }
        println!("{}", format!("  Pick 1-{}.", subclasses.len()).red());
    };

    let confirm = prompt(&format!(
        "{} Prestige as {}? This resets your level! [y/N] ",
        "⚠️".yellow(),
        format!("{}", subclass).magenta().bold()
    ));

    if confirm.to_lowercase() != "y" {
        println!("{}", "Cancelled.".dimmed());
        return;
    }

    let sub_name = format!("{}", subclass);
    game.character.prestige(subclass);

    match state::save(&game) {
        Ok(()) => {
            println!();
            println!(
                "{}",
                "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".yellow()
            );
            println!(
                "{} {} has ascended as a {} {}! Prestige tier: {}",
                "✨".bold(),
                game.character.name.bold().green(),
                sub_name.magenta().bold(),
                format!("{}", game.character.class).cyan(),
                format!("{}", game.character.prestige).yellow().bold()
            );
            println!(
                "{}",
                "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".yellow()
            );
            println!();
        }
        Err(e) => {
            eprintln!("{} Failed to save: {}", "❌".bold(), e.red());
        }
    }
}

fn refresh_shop_if_needed(game: &mut state::GameState) {
    use chrono::Utc;

    let now = Utc::now();
    let today_midnight = now.date_naive().and_hms_opt(0, 0, 0).unwrap();

    let needs_refresh = match game.shop_refreshed {
        None => true,
        Some(last) => last.date_naive() < today_midnight.date(),
    };

    if needs_refresh {
        game.shop_items.clear();
        for _ in 0..6 {
            game.shop_items.push(loot::roll_shop_loot());
        }
        game.shop_refreshed = Some(now);
    }
}

fn cmd_shop() {
    let cwd = std::env::current_dir()
        .map(|p| p.to_string_lossy().to_string())
        .unwrap_or_default();

    let home = dirs::home_dir()
        .map(|p| p.to_string_lossy().to_string())
        .unwrap_or_default();

    if cwd != home {
        println!(
            "{} The shop is only accessible from your {}. You are in {}",
            "🏠".bold(),
            "home directory".cyan().bold(),
            cwd.dimmed()
        );
        println!(
            "  Run {} to return home first.",
            "cd ~".cyan()
        );
        return;
    }

    let mut game = match state::load() {
        Ok(g) => g,
        Err(e) => {
            eprintln!("{} {}", "❌".bold(), e.red());
            return;
        }
    };

    refresh_shop_if_needed(&mut game);

    println!();
    println!(
        "{}",
        "🏪 The Terminal Bazaar".bold().yellow()
    );
    println!("{}", "─".repeat(50).dimmed());
    println!(
        "  {} {}",
        "Your gold:".bold(),
        format!("{}", game.character.gold).yellow().bold()
    );
    println!("{}", "─".repeat(50).dimmed());

    if game.shop_items.is_empty() {
        println!("{}", "  The shop is empty... come back tomorrow.".dimmed());
    } else {
        for (i, item) in game.shop_items.iter().enumerate() {
            let price = loot::item_price(item);
            let rarity_str = match item.rarity {
                character::Rarity::Common => format!("{}", "[Common]".dimmed()),
                character::Rarity::Uncommon => format!("{}", "[Uncommon]".dimmed().bold()),
                character::Rarity::Rare => format!("{}", "[Rare]".green().bold()),
                _ => format!("{}", item.rarity),
            };
            let affordable = if game.character.gold >= price {
                "".to_string()
            } else {
                format!(" {}", "(can't afford)".red().dimmed())
            };
            println!(
                "  {}. {} (+{} {}) {} — {} gold{}",
                format!("{}", i + 1).dimmed(),
                item.name.white().bold(),
                item.power,
                format!("{}", item.slot).dimmed(),
                rarity_str,
                format!("{}", price).yellow().bold(),
                affordable
            );
        }
    }

    println!("{}", "─".repeat(50).dimmed());
    println!(
        "  Use {} to purchase an item.",
        "sq buy <number>".cyan()
    );
    println!(
        "  Shop refreshes daily at {}.",
        "midnight UTC".dimmed()
    );
    println!();

    if let Err(e) = state::save(&game) {
        eprintln!("{} Failed to save: {}", "❌".bold(), e.red());
    }
}

fn cmd_buy(number: usize) {
    if number == 0 {
        eprintln!(
            "{} Usage: {} (see {} for numbered list)",
            "❌".bold(),
            "sq buy <number>".cyan(),
            "sq shop".cyan()
        );
        return;
    }

    let cwd = std::env::current_dir()
        .map(|p| p.to_string_lossy().to_string())
        .unwrap_or_default();

    let home = dirs::home_dir()
        .map(|p| p.to_string_lossy().to_string())
        .unwrap_or_default();

    if cwd != home {
        println!(
            "{} The shop is only accessible from your {}.",
            "🏠".bold(),
            "home directory".cyan().bold()
        );
        return;
    }

    let mut game = match state::load() {
        Ok(g) => g,
        Err(e) => {
            eprintln!("{} {}", "❌".bold(), e.red());
            return;
        }
    };

    refresh_shop_if_needed(&mut game);

    let idx = number - 1;
    if idx >= game.shop_items.len() {
        println!(
            "{} Invalid item number {}. The shop has {} items. Run {} to see the list.",
            "⚠️".yellow(),
            format!("{}", number).white().bold(),
            format!("{}", game.shop_items.len()).white().bold(),
            "sq shop".cyan()
        );
        return;
    }

    let price = loot::item_price(&game.shop_items[idx]);

    if game.character.gold < price {
        println!(
            "{} Not enough gold! {} costs {} gold, you have {}.",
            "⚠️".yellow(),
            game.shop_items[idx].name.white().bold(),
            format!("{}", price).yellow().bold(),
            format!("{}", game.character.gold).yellow()
        );
        return;
    }

    let item = game.shop_items.remove(idx);
    let item_name = item.name.clone();
    game.character.gold -= price;
    game.character.inventory.push(item);

    println!(
        "{} Purchased {} for {} gold! ({} gold remaining)",
        "💰".bold(),
        item_name.green().bold(),
        format!("{}", price).yellow().bold(),
        format!("{}", game.character.gold).yellow()
    );

    if let Err(e) = state::save(&game) {
        eprintln!("{} Failed to save: {}", "❌".bold(), e.red());
    }
}

fn cmd_sell(query: &str) {
    if query.is_empty() {
        eprintln!(
            "{} Usage: {} or {}",
            "❌".bold(),
            "sq sell <number>".cyan(),
            "sq sell <item name>".cyan()
        );
        return;
    }

    let cwd = std::env::current_dir()
        .map(|p| p.to_string_lossy().to_string())
        .unwrap_or_default();

    let home = dirs::home_dir()
        .map(|p| p.to_string_lossy().to_string())
        .unwrap_or_default();

    if cwd != home {
        println!(
            "{} The shop is only accessible from your {}.",
            "🏠".bold(),
            "home directory".cyan().bold()
        );
        return;
    }

    let mut game = match state::load() {
        Ok(g) => g,
        Err(e) => {
            eprintln!("{} {}", "❌".bold(), e.red());
            return;
        }
    };

    if game.character.inventory.is_empty() {
        println!(
            "{} Nothing to sell. Check {}.",
            "⚠️".yellow(),
            "sq inventory".cyan()
        );
        return;
    }

    if query.eq_ignore_ascii_case("junk") {
        cmd_sell_junk(&mut game);
        return;
    }

    let idx = if let Ok(n) = query.parse::<usize>() {
        if n == 0 || n > game.character.inventory.len() {
            println!(
                "{} No item at slot {}. You have {} item{}. Check {}.",
                "⚠️".yellow(),
                format!("{}", n).white().bold(),
                format!("{}", game.character.inventory.len()).white().bold(),
                if game.character.inventory.len() == 1 { "" } else { "s" },
                "sq inventory".cyan()
            );
            return;
        }
        n - 1
    } else {
        match find_inventory_item(&game, query) {
            Ok(Some(i)) => i,
            Ok(None) => {
                println!(
                    "{} No item matching {} in your inventory.",
                    "⚠️".yellow(),
                    format!("\"{}\"", query).white().bold()
                );
                return;
            }
            Err(msg) => {
                println!("{} {}", "⚠️".yellow(), msg);
                return;
            }
        }
    };

    let sell_price = loot::sell_price(&game.character.inventory[idx]);
    let item = game.character.inventory.remove(idx);
    let old_gold = game.character.gold;
    game.character.gold += sell_price;

    println!(
        "{} Sold {} (+{} {}) [{}] for {} gold.",
        "💰".bold(),
        item.name.white().bold(),
        item.power,
        format!("{}", item.slot).dimmed(),
        format!("{}", item.rarity).dimmed(),
        format!("{}", sell_price).yellow().bold(),
    );
    println!(
        "   Gold: {} → {}",
        format!("{}", old_gold).dimmed(),
        format!("{}", game.character.gold).yellow().bold(),
    );

    if let Err(e) = state::save(&game) {
        eprintln!("{} Failed to save: {}", "❌".bold(), e.red());
    }
}

fn cmd_sell_junk(game: &mut state::GameState) {
    let inv = std::mem::take(&mut game.character.inventory);
    let result = sweep_junk(inv);

    if result.sold_count == 0 {
        game.character.inventory = result.kept;
        println!(
            "{} No junk in your inventory. {} and up are kept.",
            "⚠️".yellow(),
            "Rare".green().bold()
        );
        return;
    }

    let old_gold = game.character.gold;
    game.character.inventory = result.kept;
    game.character.gold += result.total_price;

    println!(
        "{} Sold {} junk item{} for {} gold.",
        "💰".bold(),
        format!("{}", result.sold_count).white().bold(),
        if result.sold_count == 1 { "" } else { "s" },
        format!("{}", result.total_price).yellow().bold(),
    );
    println!(
        "   Gold: {} → {}",
        format!("{}", old_gold).dimmed(),
        format!("{}", game.character.gold).yellow().bold(),
    );

    if let Err(e) = state::save(game) {
        eprintln!("{} Failed to save: {}", "❌".bold(), e.red());
    }
}

struct SweepResult {
    kept: Vec<character::Item>,
    sold_count: usize,
    total_price: u32,
}

fn sweep_junk(items: Vec<character::Item>) -> SweepResult {
    let mut kept = Vec::new();
    let mut sold_count = 0;
    let mut total_price: u32 = 0;
    for item in items {
        if matches!(item.rarity, character::Rarity::Common | character::Rarity::Uncommon) {
            total_price += loot::sell_price(&item);
            sold_count += 1;
        } else {
            kept.push(item);
        }
    }
    SweepResult { kept, sold_count, total_price }
}

#[derive(Debug, PartialEq)]
enum EquippedSlot {
    Weapon,
    Armor,
    Ring,
}

#[derive(Debug, PartialEq)]
enum ItemLookup {
    Equipped(EquippedSlot),
    Inventory(usize),
}

fn find_equipped_slot_to_enchant(game: &state::GameState, query: &str) -> Result<EquippedSlot, &'static str> {
    let q = query.to_lowercase();
    let mut matches: Vec<EquippedSlot> = Vec::new();
    if let Some(w) = &game.character.weapon {
        if w.name.to_lowercase().contains(&q) || fuzzy_match_name(&w.name, query) {
            matches.push(EquippedSlot::Weapon);
        }
    }
    if let Some(a) = &game.character.armor {
        if a.name.to_lowercase().contains(&q) || fuzzy_match_name(&a.name, query) {
            matches.push(EquippedSlot::Armor);
        }
    }
    if let Some(r) = &game.character.ring {
        if r.name.to_lowercase().contains(&q) || fuzzy_match_name(&r.name, query) {
            matches.push(EquippedSlot::Ring);
        }
    }
    match matches.len() {
        0 => Err("no_match"),
        1 => Ok(matches.remove(0)),
        _ => Err("ambiguous"),
    }
}

fn cmd_enchant(query: &str) {
    if query.is_empty() {
        eprintln!(
            "{} Usage: {} (enchant an equipped item, +1 power per enchant, max +5)",
            "❌".bold(),
            "sq enchant <item name>".cyan()
        );
        return;
    }

    let mut game = match state::load() {
        Ok(g) => g,
        Err(e) => {
            eprintln!("{} {}", "❌".bold(), e.red());
            return;
        }
    };

    let is_wizard = matches!(game.character.class, character::Class::Wizard);
    if !is_wizard {
        let cwd = std::env::current_dir()
            .map(|p| p.to_string_lossy().to_string())
            .unwrap_or_default();
        let home = dirs::home_dir()
            .map(|p| p.to_string_lossy().to_string())
            .unwrap_or_default();
        if cwd != home {
            println!(
                "{} The enchanting workbench is only accessible from your {}. (Wizards can enchant anywhere.)",
                "🏠".bold(),
                "home directory".cyan().bold()
            );
            return;
        }
    }

    let slot = match find_equipped_slot_to_enchant(&game, query) {
        Ok(s) => s,
        Err("no_match") => {
            println!(
                "{} No equipped item matching {}. Equip the item first with {} / {}.",
                "⚠️".yellow(),
                format!("\"{}\"", query).white().bold(),
                "sq wield".cyan(),
                "sq equip".cyan()
            );
            return;
        }
        Err("ambiguous") => {
            println!(
                "{} Multiple equipped items match {}. Be more specific.",
                "⚠️".yellow(),
                format!("\"{}\"", query).white().bold()
            );
            return;
        }
        Err(_) => return,
    };

    let item_ref = match slot {
        EquippedSlot::Weapon => game.character.weapon.as_ref().unwrap(),
        EquippedSlot::Armor => game.character.armor.as_ref().unwrap(),
        EquippedSlot::Ring => game.character.ring.as_ref().unwrap(),
    };

    if !loot::is_enchantable(item_ref) {
        println!("{} That item cannot be enchanted.", "⚠️".yellow());
        return;
    }

    if !loot::can_enchant_further(item_ref) {
        println!(
            "{} {} is already at maximum enchantment ({}).",
            "⚠️".yellow(),
            item_ref.name.white().bold(),
            format!("+{}", loot::MAX_ENCHANT_LEVEL).yellow().bold()
        );
        return;
    }

    let cost = loot::enchant_cost(item_ref);
    if game.character.gold < cost {
        println!(
            "{} Not enough gold. Need {} gold (you have {}).",
            "⚠️".yellow(),
            format!("{}", cost).yellow().bold(),
            format!("{}", game.character.gold).yellow()
        );
        return;
    }

    let item_mut = match slot {
        EquippedSlot::Weapon => game.character.weapon.as_mut().unwrap(),
        EquippedSlot::Armor => game.character.armor.as_mut().unwrap(),
        EquippedSlot::Ring => game.character.ring.as_mut().unwrap(),
    };
    item_mut.enchant_level += 1;
    let new_level = item_mut.enchant_level;
    let item_name = item_mut.name.clone();

    let old_gold = game.character.gold;
    game.character.gold -= cost;

    println!(
        "{} {} is now {} (cost: {} gold).",
        "✨".bold(),
        item_name.white().bold(),
        display::enchant_tag(new_level).trim_start(),
        format!("{}", cost).yellow().bold()
    );
    println!(
        "   Gold: {} → {}",
        format!("{}", old_gold).dimmed(),
        format!("{}", game.character.gold).yellow().bold()
    );

    if let Err(e) = state::save(&game) {
        eprintln!("{} Failed to save: {}", "❌".bold(), e.red());
    }
}

fn cmd_identify(query: &str) {
    if query.is_empty() {
        eprintln!(
            "{} Usage: {} (alias: {})",
            "❌".bold(),
            "sq identify <item name>".cyan(),
            "sq id <item name>".cyan()
        );
        return;
    }

    let game = match state::load() {
        Ok(g) => g,
        Err(e) => {
            eprintln!("{} {}", "❌".bold(), e.red());
            return;
        }
    };

    let lookup = match find_inventory_or_equipped_item(&game, query) {
        Ok(Some(l)) => l,
        Ok(None) => {
            println!(
                "{} No item matching {} in your inventory or equipped slots.",
                "⚠️".yellow(),
                format!("\"{}\"", query).white().bold()
            );
            return;
        }
        Err(msg) => {
            println!("{} {}", "⚠️".yellow(), msg);
            return;
        }
    };

    let total_inv = game.character.inventory.len();
    let (item_ref, source) = match lookup {
        ItemLookup::Equipped(EquippedSlot::Weapon) => (
            game.character.weapon.as_ref().unwrap(),
            display::ItemSource::Equipped,
        ),
        ItemLookup::Equipped(EquippedSlot::Armor) => (
            game.character.armor.as_ref().unwrap(),
            display::ItemSource::Equipped,
        ),
        ItemLookup::Equipped(EquippedSlot::Ring) => (
            game.character.ring.as_ref().unwrap(),
            display::ItemSource::Equipped,
        ),
        ItemLookup::Inventory(idx) => (
            &game.character.inventory[idx],
            display::ItemSource::Inventory { index: idx + 1, total: total_inv },
        ),
    };

    display::print_item_detail(item_ref, source);
}

fn fuzzy_match_name(item_name: &str, query: &str) -> bool {
    let name_lower = item_name.to_lowercase();
    query
        .to_lowercase()
        .split_whitespace()
        .all(|token| name_lower.contains(token))
}

fn find_inventory_items(game: &state::GameState, query: &str) -> Vec<usize> {
    let query_lower = query.to_lowercase();
    let inv = &game.character.inventory;
    let mut matched: Vec<usize> = (0..inv.len())
        .filter(|&i| {
            let name_lower = inv[i].name.to_lowercase();
            name_lower == query_lower
                || name_lower.contains(&query_lower)
                || fuzzy_match_name(&inv[i].name, query)
        })
        .collect();
    matched.dedup();
    matched
}

fn find_inventory_item(game: &state::GameState, name: &str) -> Result<Option<usize>, String> {
    let (query, n) = if let Some(dot_pos) = name.rfind('.') {
        let suffix = &name[dot_pos + 1..];
        match suffix.parse::<usize>() {
            Ok(0) => {
                return Err("Item index must be 1 or higher (e.g. potion.1)".to_string());
            }
            Ok(n) => (&name[..dot_pos], n),
            Err(_) => (name, 1usize),
        }
    } else {
        (name, 1usize)
    };

    let matches = find_inventory_items(game, query);

    if matches.is_empty() {
        return Ok(None);
    }

    match matches.get(n - 1) {
        Some(&idx) => Ok(Some(idx)),
        None => Err(format!(
            "Only {} '{}' item(s) found — use {}.1 … {}.{}",
            matches.len(),
            query,
            query,
            query,
            matches.len()
        )),
    }
}

fn equipped_item_name_matches(item: &character::Item, query: &str) -> bool {
    let q = query.to_lowercase();
    let name_lower = item.name.to_lowercase();
    name_lower == q || name_lower.contains(&q) || fuzzy_match_name(&item.name, query)
}

fn find_inventory_or_equipped_item(
    game: &state::GameState,
    name: &str,
) -> Result<Option<ItemLookup>, String> {
    let (query, n) = if let Some(dot_pos) = name.rfind('.') {
        let suffix = &name[dot_pos + 1..];
        match suffix.parse::<usize>() {
            Ok(0) => {
                return Err("Item index must be 1 or higher (e.g. scythe.1)".to_string());
            }
            Ok(n) => (&name[..dot_pos], n),
            Err(_) => (name, 1usize),
        }
    } else {
        (name, 1usize)
    };

    let mut candidates: Vec<ItemLookup> = Vec::new();
    if let Some(w) = &game.character.weapon {
        if equipped_item_name_matches(w, query) {
            candidates.push(ItemLookup::Equipped(EquippedSlot::Weapon));
        }
    }
    if let Some(a) = &game.character.armor {
        if equipped_item_name_matches(a, query) {
            candidates.push(ItemLookup::Equipped(EquippedSlot::Armor));
        }
    }
    if let Some(r) = &game.character.ring {
        if equipped_item_name_matches(r, query) {
            candidates.push(ItemLookup::Equipped(EquippedSlot::Ring));
        }
    }
    for idx in find_inventory_items(game, query) {
        candidates.push(ItemLookup::Inventory(idx));
    }

    if candidates.is_empty() {
        return Ok(None);
    }

    match candidates.into_iter().nth(n - 1) {
        Some(c) => Ok(Some(c)),
        None => Err(format!(
            "Only matching items below '{}.{}' index — use a lower number.",
            query, n
        )),
    }
}

#[cfg(test)]
#[test]
fn registry_covers_all_canonical_topics() {
    let names: Vec<&str> = help::all_topics().iter().map(|t| t.name).collect();
    let expected: Vec<&str> = help::CANONICAL_TOPIC_ORDER.to_vec();
    assert_eq!(
        names, expected,
        "registry must contain exactly the canonical topics in canonical order"
    );
}

#[cfg(test)]
#[test]
fn registry_related_topics_are_known() {
    use std::collections::HashSet;
    let canonical: HashSet<&str> = help::CANONICAL_TOPIC_ORDER.iter().copied().collect();
    for topic in help::all_topics() {
        for related in topic.related {
            assert!(
                canonical.contains(*related),
                "topic '{}' references unknown related topic '{}'",
                topic.name,
                related
            );
        }
    }
}

#[cfg(test)]
#[test]
fn lookup_prefers_primary_then_alias() {
    use help::{lookup_topic, LookupResult};

    match lookup_topic("status") {
        LookupResult::Found(t) => assert_eq!(t.name, "status"),
        other => panic!("expected Found(status) for canonical name, got {:?}", other),
    }

    match lookup_topic("stat") {
        LookupResult::Found(t) => assert_eq!(
            t.name, "status",
            "alias 'stat' must resolve to canonical 'status'"
        ),
        other => panic!("expected Found(status) via alias, got {:?}", other),
    }
}

#[cfg(test)]
#[test]
fn lookup_typo_and_gibberish_paths() {
    use help::{lookup_topic, LookupResult};

    match lookup_topic("jounral") {
        LookupResult::Suggestions(s) => {
            assert!(!s.is_empty(), "expected at least one suggestion for 'jounral'");
            assert_eq!(
                s[0].name, "journal",
                "first suggestion for 'jounral' must be 'journal'"
            );
        }
        other => panic!("expected Suggestions for 'jounral', got {:?}", other),
    }

    match lookup_topic("xyzzy") {
        LookupResult::NoMatch => {}
        other => panic!("expected NoMatch for 'xyzzy', got {:?}", other),
    }
}

#[cfg(test)]
#[test]
fn render_index_lists_topics_in_order() {
    colored::control::set_override(false);

    let out = help::render_index();

    let mut search_start = 0usize;
    for name in help::CANONICAL_TOPIC_ORDER {
        let needle = format!("  {}", name);
        let rel_pos = out[search_start..].find(&needle).unwrap_or_else(|| {
            panic!(
                "topic row '{}' not found at or after offset {} in render_index() output:\n{}",
                name, search_start, out
            )
        });
        search_start += rel_pos + needle.len();
    }

    assert!(
        out.contains(help::INDEX_FOOTER),
        "render_index must include the exact footer '{}'; got:\n{}",
        help::INDEX_FOOTER,
        out
    );
}

#[cfg(test)]
#[test]
fn render_no_match_shows_suggestions() {
    use help::{lookup_topic, LookupResult};

    colored::control::set_override(false);

    let suggestions = match lookup_topic("jounral") {
        LookupResult::Suggestions(s) => s,
        other => panic!("expected Suggestions for 'jounral', got {:?}", other),
    };

    let out = help::render_no_match("jounral", &suggestions);

    assert!(
        out.contains("jounral"),
        "no-match output must echo the original query 'jounral':\n{}",
        out
    );
    assert!(
        out.contains("journal"),
        "no-match output must surface 'journal' as a close match:\n{}",
        out
    );
    assert!(
        out.to_lowercase().contains("did you mean"),
        "no-match output must clearly suggest close matches (looked for 'did you mean'):\n{}",
        out
    );
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::character::{Character, Class, Item, ItemSlot, Race, Rarity};

    fn make_state_with_items(items: Vec<Item>) -> state::GameState {
        let mut s = state::GameState::new(Character::new("T".to_string(), Class::Rogue, Race::Human));
        s.character.inventory = items;
        s
    }

    fn item(name: &str) -> Item {
        Item { name: name.to_string(), slot: ItemSlot::Weapon, power: 1, rarity: Rarity::Common, enchant_level: 0 }
    }

    #[test]
    fn fuzzy_match_two_tokens_both_present() {
        assert!(fuzzy_match_name("Big Sword of Awesome", "big of"));
    }

    #[test]
    fn fuzzy_match_partial_word_token() {
        assert!(fuzzy_match_name("Big Sword of Awesome", "big sw"));
    }

    #[test]
    fn fuzzy_match_case_insensitive() {
        assert!(fuzzy_match_name("Big Sword of Awesome", "BIG SWORD"));
    }

    #[test]
    fn fuzzy_match_single_token_prefix() {
        assert!(fuzzy_match_name("Big Sword of Awesome", "awe"));
    }

    #[test]
    fn fuzzy_match_full_name_exact() {
        assert!(fuzzy_match_name("Big Sword of Awesome", "Big Sword of Awesome"));
    }

    #[test]
    fn fuzzy_match_token_missing_returns_false() {
        assert!(!fuzzy_match_name("Big Sword of Awesome", "xyz"));
    }

    #[test]
    fn fuzzy_match_one_token_absent_returns_false() {
        assert!(!fuzzy_match_name("Big Sword of Awesome", "big xyz"));
    }

    #[test]
    fn fuzzy_match_empty_query_returns_true() {
        assert!(fuzzy_match_name("Big Sword of Awesome", ""));
    }

    #[test]
    fn find_inventory_item_exact_match() {
        let state = make_state_with_items(vec![item("Big Sword of Awesome")]);
        assert_eq!(find_inventory_item(&state, "Big Sword of Awesome"), Ok(Some(0)));
    }

    #[test]
    fn find_inventory_item_case_insensitive_exact() {
        let state = make_state_with_items(vec![item("Big Sword of Awesome")]);
        assert_eq!(find_inventory_item(&state, "big sword of awesome"), Ok(Some(0)));
    }

    #[test]
    fn find_inventory_item_substring_match() {
        let state = make_state_with_items(vec![item("Big Sword of Awesome")]);
        assert_eq!(find_inventory_item(&state, "big sw"), Ok(Some(0)));
    }

    #[test]
    fn find_inventory_item_fuzzy_non_contiguous_tokens() {
        let state = make_state_with_items(vec![item("Big Sword of Awesome")]);
        assert_eq!(find_inventory_item(&state, "big of"), Ok(Some(0)));
    }

    #[test]
    fn find_inventory_item_fuzzy_case_insensitive() {
        let state = make_state_with_items(vec![item("Big Sword of Awesome")]);
        assert_eq!(find_inventory_item(&state, "BIG OF"), Ok(Some(0)));
    }

    #[test]
    fn find_inventory_item_no_match_returns_none() {
        let state = make_state_with_items(vec![item("Big Sword of Awesome")]);
        assert_eq!(find_inventory_item(&state, "hammer"), Ok(None));
    }

    #[test]
    fn find_inventory_item_exact_wins_over_fuzzy() {
        let state = make_state_with_items(vec![
            item("Small Shield"),
            item("Big Sword of Awesome"),
        ]);
        assert_eq!(find_inventory_item(&state, "Big Sword of Awesome"), Ok(Some(1)));
    }

    #[test]
    fn find_inventory_item_fuzzy_picks_first_among_multiple() {
        let state = make_state_with_items(vec![
            item("Big Dagger of Doom"),
            item("Big Sword of Awesome"),
        ]);
        assert_eq!(find_inventory_item(&state, "big of"), Ok(Some(0)));
    }

    #[test]
    fn find_all_empty_inventory_returns_empty() {
        let state = make_state_with_items(vec![]);
        assert_eq!(find_inventory_items(&state, "potion"), Vec::<usize>::new());
    }

    #[test]
    fn find_all_single_word_matches_substring() {
        let state = make_state_with_items(vec![
            item("Potion of Coffee"),
            item("Rusty Pipe"),
            item("Potion of Sorrow"),
        ]);
        assert_eq!(find_inventory_items(&state, "potion"), vec![0, 2]);
    }

    #[test]
    fn find_all_multi_token_requires_all_tokens() {
        let state = make_state_with_items(vec![
            item("Big Sword of Awesome"),
            item("Small Dagger"),
            item("Big Shield"),
        ]);
        assert_eq!(find_inventory_items(&state, "big sword"), vec![0]);
    }

    #[test]
    fn find_all_case_insensitive() {
        let state = make_state_with_items(vec![item("Potion of Coffee"), item("Rusty Pipe")]);
        assert_eq!(find_inventory_items(&state, "POTION"), vec![0]);
    }

    #[test]
    fn find_all_no_match_returns_empty() {
        let state = make_state_with_items(vec![item("Rusty Pipe")]);
        assert_eq!(find_inventory_items(&state, "xyz"), Vec::<usize>::new());
    }

    #[test]
    fn find_all_exact_and_partial_both_included_in_order() {
        let state = make_state_with_items(vec![
            item("Rusty Pipe"),
            item("Pipewright Gauntlets"),
            item("Sword"),
        ]);
        let result = find_inventory_items(&state, "pipe");
        assert_eq!(result, vec![0, 1]);
    }

    #[test]
    fn find_all_returns_stable_inventory_order() {
        let state = make_state_with_items(vec![
            item("Potion of Sorrow"),
            item("Potion of Coffee"),
            item("Rusty Pipe"),
        ]);
        let result = find_inventory_items(&state, "potion");
        assert_eq!(result, vec![0, 1]);
    }

    #[test]
    fn selector_no_suffix_returns_first_match() {
        let state = make_state_with_items(vec![item("Potion of Coffee"), item("Potion of Sorrow")]);
        assert_eq!(find_inventory_item(&state, "potion"), Ok(Some(0)));
    }

    #[test]
    fn selector_explicit_dot_one_returns_first_match() {
        let state = make_state_with_items(vec![item("Potion of Coffee"), item("Potion of Sorrow")]);
        assert_eq!(find_inventory_item(&state, "potion.1"), Ok(Some(0)));
    }

    #[test]
    fn selector_dot_two_returns_second_match() {
        let state = make_state_with_items(vec![item("Potion of Coffee"), item("Potion of Sorrow")]);
        assert_eq!(find_inventory_item(&state, "potion.2"), Ok(Some(1)));
    }

    #[test]
    fn selector_dot_zero_returns_err() {
        let state = make_state_with_items(vec![item("Potion of Coffee")]);
        let result = find_inventory_item(&state, "potion.0");
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("1 or higher"));
    }

    #[test]
    fn selector_n_exceeds_match_count_returns_err() {
        let state = make_state_with_items(vec![item("Potion of Coffee"), item("Potion of Sorrow")]);
        let result = find_inventory_item(&state, "potion.5");
        assert!(result.is_err());
        let msg = result.unwrap_err();
        assert!(msg.contains("Only 2"), "expected 'Only 2' in: {msg}");
        assert!(msg.contains("potion"), "expected query name in: {msg}");
    }

    #[test]
    fn selector_non_numeric_suffix_treated_as_query() {
        let state = make_state_with_items(vec![item("Potion of Coffee")]);
        assert_eq!(find_inventory_item(&state, "Potion.of.Coffee"), Ok(None));
    }

    #[test]
    fn selector_no_match_no_suffix_returns_ok_none() {
        let state = make_state_with_items(vec![item("Rusty Pipe")]);
        assert_eq!(find_inventory_item(&state, "xyz"), Ok(None));
    }

    #[test]
    fn selector_no_match_with_valid_suffix_returns_ok_none() {
        let state = make_state_with_items(vec![item("Rusty Pipe")]);
        assert_eq!(find_inventory_item(&state, "xyz.2"), Ok(None));
    }

    #[test]
    fn selector_dot_n_on_exact_match_works() {
        let state = make_state_with_items(vec![item("Rusty Pipe"), item("Rusty Sword")]);
        assert_eq!(find_inventory_item(&state, "rusty.2"), Ok(Some(1)));
    }

    #[test]
    fn parser_help_no_topic_yields_help_variant() {
        let cli = Cli::try_parse_from(["sq", "help"]).expect("'sq help' must parse");
        match cli.command {
            Commands::Help { topic } => assert!(
                topic.is_none(),
                "'sq help' must parse with topic == None, got {:?}",
                topic
            ),
            _ => panic!("expected Commands::Help, got a different variant"),
        }
    }

    #[test]
    fn parser_help_with_arena_topic_yields_help_variant() {
        let cli = Cli::try_parse_from(["sq", "help", "arena"]).expect("'sq help arena' must parse");
        match cli.command {
            Commands::Help { topic } => assert_eq!(
                topic.as_deref(),
                Some("arena"),
                "'sq help arena' must carry topic Some(\"arena\")"
            ),
            _ => panic!("expected Commands::Help"),
        }
    }

    #[test]
    fn parser_help_help_topic_yields_help_variant() {
        let cli = Cli::try_parse_from(["sq", "help", "help"]).expect("'sq help help' must parse");
        match cli.command {
            Commands::Help { topic } => assert_eq!(
                topic.as_deref(),
                Some("help"),
                "'sq help help' must carry topic Some(\"help\"), proving we own the help name"
            ),
            _ => panic!("expected Commands::Help"),
        }
    }

    #[test]
    fn parser_identify_canonical_yields_identify_variant() {
        let cli = Cli::try_parse_from(["sq", "identify", "scythe"])
            .expect("'sq identify scythe' must parse");
        match cli.command {
            Commands::Identify { name } => {
                assert_eq!(name, vec!["scythe".to_string()]);
            }
            _ => panic!("expected Commands::Identify"),
        }
    }

    #[test]
    fn parser_id_alias_routes_to_identify_variant() {
        let cli = Cli::try_parse_from(["sq", "id", "ring", "of", "fortune"])
            .expect("'sq id ring of fortune' must parse via the `id` alias");
        match cli.command {
            Commands::Identify { name } => {
                assert_eq!(name, vec!["ring".to_string(), "of".to_string(), "fortune".to_string()]);
            }
            _ => panic!("expected Commands::Identify via alias"),
        }
    }

    #[test]
    fn help_topic_identify_resolves_canonical_and_id_alias() {
        use help::{lookup_topic, LookupResult};

        match lookup_topic("identify") {
            LookupResult::Found(t) => assert_eq!(t.name, "identify"),
            other => panic!("expected Found(identify), got {:?}", other),
        }
        match lookup_topic("id") {
            LookupResult::Found(t) => assert_eq!(
                t.name, "identify",
                "alias 'id' must resolve to canonical 'identify'"
            ),
            other => panic!("expected Found(identify) via 'id', got {:?}", other),
        }
    }

    #[test]
    fn parser_dash_dash_help_still_routes_to_clap() {
        let err = match Cli::try_parse_from(["sq", "--help"]) {
            Ok(_) => panic!("'sq --help' must short-circuit on clap's DisplayHelp path, not parse"),
            Err(e) => e,
        };
        assert_eq!(
            err.kind(),
            clap::error::ErrorKind::DisplayHelp,
            "--help must trigger clap's auto-generated help, not our custom Help variant"
        );
    }

    #[test]
    fn format_help_no_topic_emits_index() {
        colored::control::set_override(false);
        let out = format_help(None);
        assert!(
            out.contains("sq Manual"),
            "index header missing from no-topic output:\n{}",
            out
        );
        assert!(
            out.contains(help::INDEX_FOOTER),
            "index footer '{}' missing:\n{}",
            help::INDEX_FOOTER,
            out
        );
    }

    #[test]
    fn format_help_arena_emits_authored_topic() {
        colored::control::set_override(false);
        let out = format_help(Some("arena"));
        assert!(
            out.contains("sq arena"),
            "topic header missing from arena help:\n{}",
            out
        );
        assert!(
            out.contains("Usage:"),
            "Usage section missing from arena help:\n{}",
            out
        );
        assert!(
            out.contains("Examples:"),
            "Examples section missing from arena help:\n{}",
            out
        );
    }

    #[test]
    fn format_help_help_emits_authored_topic_not_clap_help() {
        colored::control::set_override(false);
        let out = format_help(Some("help"));
        assert!(
            out.contains("sq help"),
            "header 'sq help' missing — our Help variant must own the topic:\n{}",
            out
        );
        assert!(
            out.contains("Browse the in-game manual"),
            "authored summary missing — output looks like clap's auto-help instead of our topic:\n{}",
            out
        );
    }

    #[test]
    fn format_help_typo_emits_no_match_with_suggestion() {
        colored::control::set_override(false);
        let out = format_help(Some("jounral"));
        assert!(
            out.contains("jounral"),
            "no-match output must echo the misspelled query:\n{}",
            out
        );
        assert!(
            out.contains("journal"),
            "no-match output must surface 'journal' as a close suggestion:\n{}",
            out
        );
    }

    fn item_full(name: &str, power: i32, rarity: Rarity) -> Item {
        Item { name: name.to_string(), slot: ItemSlot::Weapon, power, rarity, enchant_level: 0 }
    }

    #[test]
    fn sweep_junk_empty_inventory_returns_empty_result() {
        let r = sweep_junk(vec![]);
        assert_eq!(r.sold_count, 0);
        assert_eq!(r.total_price, 0);
        assert!(r.kept.is_empty());
    }

    #[test]
    fn sweep_junk_preserves_order_of_kept_items_and_sums_prices() {
        let inv = vec![
            item_full("Common Stick", 2, Rarity::Common),
            item_full("Rare One", 10, Rarity::Rare),
            item_full("Uncommon Tonic", 5, Rarity::Uncommon),
            item_full("Epic Blade", 25, Rarity::Epic),
            item_full("Rare Two", 12, Rarity::Rare),
        ];
        let expected_price = loot::item_price(&inv[0]) / 2 + loot::item_price(&inv[2]) / 2;
        let r = sweep_junk(inv);
        assert_eq!(r.sold_count, 2);
        assert_eq!(r.total_price, expected_price);
        let kept_names: Vec<&str> = r.kept.iter().map(|i| i.name.as_str()).collect();
        assert_eq!(kept_names, vec!["Rare One", "Epic Blade", "Rare Two"]);
    }

    #[test]
    fn sweep_junk_never_sells_epic_or_legendary() {
        let epic = item_full("Doombringer", 20, Rarity::Epic);
        let legendary = item_full("Worldslayer", 50, Rarity::Legendary);
        let r = sweep_junk(vec![epic, legendary]);
        assert_eq!(r.sold_count, 0);
        assert_eq!(r.total_price, 0);
        assert_eq!(r.kept.len(), 2);
        let names: Vec<&str> = r.kept.iter().map(|i| i.name.as_str()).collect();
        assert!(names.contains(&"Doombringer"));
        assert!(names.contains(&"Worldslayer"));
    }

    #[test]
    fn sweep_junk_sells_an_uncommon_item_too() {
        let item = item_full("Decent Mace", 6, Rarity::Uncommon);
        let expected_price = loot::item_price(&item) / 2;
        let r = sweep_junk(vec![item]);
        assert_eq!(r.sold_count, 1);
        assert!(r.kept.is_empty());
        assert_eq!(r.total_price, expected_price);
    }

    #[test]
    fn sweep_junk_keeps_a_single_rare_item() {
        let rare = item_full("Rare Blade", 10, Rarity::Rare);
        let r = sweep_junk(vec![rare]);
        assert_eq!(r.sold_count, 0);
        assert_eq!(r.total_price, 0);
        assert_eq!(r.kept.len(), 1);
        assert_eq!(r.kept[0].name, "Rare Blade");
    }

    #[test]
    fn sweep_junk_sells_a_single_common_item() {
        let item = item_full("Rusty Spoon", 4, Rarity::Common);
        let expected_price = loot::item_price(&item) / 2;
        let r = sweep_junk(vec![item]);
        assert_eq!(r.sold_count, 1);
        assert!(r.kept.is_empty());
        assert_eq!(r.total_price, expected_price);
    }

    fn state_with_loadout(
        weapon: Option<Item>,
        armor: Option<Item>,
        ring: Option<Item>,
        inventory: Vec<Item>,
    ) -> state::GameState {
        let mut s = make_state_with_items(inventory);
        s.character.weapon = weapon;
        s.character.armor = armor;
        s.character.ring = ring;
        s
    }

    #[test]
    fn identify_lookup_matches_equipped_weapon_by_exact_name() {
        let s = state_with_loadout(
            Some(item("Scythe of Segfault")),
            None,
            None,
            vec![],
        );
        assert_eq!(
            find_inventory_or_equipped_item(&s, "Scythe of Segfault"),
            Ok(Some(ItemLookup::Equipped(EquippedSlot::Weapon))),
        );
    }

    #[test]
    fn identify_lookup_matches_equipped_armor_by_substring() {
        let s = state_with_loadout(
            None,
            Some(item("Leather Cuirass")),
            None,
            vec![],
        );
        assert_eq!(
            find_inventory_or_equipped_item(&s, "leather"),
            Ok(Some(ItemLookup::Equipped(EquippedSlot::Armor))),
        );
    }

    #[test]
    fn identify_lookup_matches_equipped_ring_via_fuzzy_tokens() {
        let s = state_with_loadout(
            None,
            None,
            Some(item("Ring of Fortune")),
            vec![],
        );
        assert_eq!(
            find_inventory_or_equipped_item(&s, "ring fortune"),
            Ok(Some(ItemLookup::Equipped(EquippedSlot::Ring))),
        );
    }

    #[test]
    fn identify_lookup_matches_inventory_item_by_substring() {
        let s = make_state_with_items(vec![item("Rusty Pipe")]);
        assert_eq!(
            find_inventory_or_equipped_item(&s, "rusty"),
            Ok(Some(ItemLookup::Inventory(0))),
        );
    }

    #[test]
    fn identify_lookup_matches_inventory_via_fuzzy_tokens() {
        let s = make_state_with_items(vec![item("Big Sword of Awesome")]);
        assert_eq!(
            find_inventory_or_equipped_item(&s, "big of"),
            Ok(Some(ItemLookup::Inventory(0))),
        );
    }

    #[test]
    fn identify_lookup_returns_none_on_no_match() {
        let s = state_with_loadout(
            Some(item("Pike of Ping")),
            None,
            None,
            vec![item("Rusty Pipe")],
        );
        assert_eq!(
            find_inventory_or_equipped_item(&s, "hammer"),
            Ok(None),
        );
    }

    #[test]
    fn identify_lookup_equipped_wins_over_inventory_by_default() {
        let s = state_with_loadout(
            Some(item("Scythe of Segfault")),
            None,
            None,
            vec![item("Scythe of Segfault")],
        );
        assert_eq!(
            find_inventory_or_equipped_item(&s, "scythe"),
            Ok(Some(ItemLookup::Equipped(EquippedSlot::Weapon))),
        );
    }

    #[test]
    fn identify_lookup_dot_two_picks_inventory_after_equipped() {
        let s = state_with_loadout(
            Some(item("Scythe of Segfault")),
            None,
            None,
            vec![item("Scythe of Segfault")],
        );
        assert_eq!(
            find_inventory_or_equipped_item(&s, "scythe.2"),
            Ok(Some(ItemLookup::Inventory(0))),
        );
    }

    #[test]
    fn identify_lookup_dot_three_with_only_two_matches_returns_err() {
        let s = state_with_loadout(
            Some(item("Pike of Ping")),
            None,
            None,
            vec![item("Pike of Ping")],
        );
        let result = find_inventory_or_equipped_item(&s, "pike.3");
        assert!(result.is_err());
    }

    #[test]
    fn identify_lookup_dot_zero_returns_err() {
        let s = make_state_with_items(vec![item("Rusty Pipe")]);
        let result = find_inventory_or_equipped_item(&s, "rusty.0");
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("1 or higher"));
    }

    #[test]
    fn identify_lookup_orders_equipped_before_inventory_across_slots() {
        let s = state_with_loadout(
            Some(item("Spike Mace")),
            Some(item("Spike Plate")),
            Some(item("Spike Ring")),
            vec![item("Spike Dagger")],
        );
        assert_eq!(
            find_inventory_or_equipped_item(&s, "spike"),
            Ok(Some(ItemLookup::Equipped(EquippedSlot::Weapon))),
        );
        assert_eq!(
            find_inventory_or_equipped_item(&s, "spike.2"),
            Ok(Some(ItemLookup::Equipped(EquippedSlot::Armor))),
        );
        assert_eq!(
            find_inventory_or_equipped_item(&s, "spike.3"),
            Ok(Some(ItemLookup::Equipped(EquippedSlot::Ring))),
        );
        assert_eq!(
            find_inventory_or_equipped_item(&s, "spike.4"),
            Ok(Some(ItemLookup::Inventory(0))),
        );
    }
}

fn cmd_equip(name: &str) {
    if name.is_empty() {
        eprintln!(
            "{} Usage: {} or {}",
            "❌".bold(),
            "sq equip <armor name>".cyan(),
            "sq equip <ring name>".cyan()
        );
        return;
    }

    let mut game = match state::load() {
        Ok(g) => g,
        Err(e) => {
            eprintln!("{} {}", "❌".bold(), e.red());
            return;
        }
    };

    let idx = match find_inventory_item(&game, name) {
        Ok(Some(i)) => i,
        Ok(None) => {
            println!(
                "{} No item matching {} in your inventory.",
                "⚠️".yellow(),
                format!("\"{}\"", name).white().bold()
            );
            return;
        }
        Err(msg) => {
            println!("{} {}", "⚠️".yellow(), msg);
            return;
        }
    };

    let item = &game.character.inventory[idx];
    match item.slot {
        character::ItemSlot::Weapon => {
            println!(
                "{} {} is a weapon. Use {} instead.",
                "⚠️".yellow(),
                item.name.cyan().bold(),
                "sq wield".cyan()
            );
            return;
        }
        character::ItemSlot::Potion => {
            println!(
                "{} {} is a potion and cannot be equipped.",
                "⚠️".yellow(),
                item.name.cyan().bold()
            );
            return;
        }
        character::ItemSlot::Armor | character::ItemSlot::Ring => {}
    }

    let item = game.character.inventory.remove(idx);
    let item_name = item.name.clone();
    let slot_name = format!("{}", item.slot);

    if let Some(old) = game.character.equip(item) {
        let old_name = old.name.clone();
        game.character.inventory.push(old);
        println!(
            "{} Equipped {}! (replaced {})",
            "🛡️".bold(),
            item_name.green().bold(),
            old_name.dimmed()
        );
    } else {
        println!(
            "{} Equipped {} in {} slot!",
            "🛡️".bold(),
            item_name.green().bold(),
            slot_name.cyan()
        );
    }

    if let Err(e) = state::save(&game) {
        eprintln!("{} Failed to save: {}", "❌".bold(), e.red());
    }
}

fn cmd_wield(name: &str) {
    if name.is_empty() {
        eprintln!(
            "{} Usage: {}",
            "❌".bold(),
            "sq wield <weapon name>".cyan()
        );
        return;
    }

    let mut game = match state::load() {
        Ok(g) => g,
        Err(e) => {
            eprintln!("{} {}", "❌".bold(), e.red());
            return;
        }
    };

    let idx = match find_inventory_item(&game, name) {
        Ok(Some(i)) => i,
        Ok(None) => {
            println!(
                "{} No item matching {} in your inventory.",
                "⚠️".yellow(),
                format!("\"{}\"", name).white().bold()
            );
            return;
        }
        Err(msg) => {
            println!("{} {}", "⚠️".yellow(), msg);
            return;
        }
    };

    let item = &game.character.inventory[idx];
    if item.slot != character::ItemSlot::Weapon {
        println!(
            "{} {} is not a weapon. Use {} to wear armor or rings.",
            "⚠️".yellow(),
            item.name.cyan().bold(),
            "sq equip".cyan()
        );
        return;
    }

    let item = game.character.inventory.remove(idx);
    let item_name = item.name.clone();

    if let Some(old) = game.character.equip(item) {
        let old_name = old.name.clone();
        game.character.inventory.push(old);
        println!(
            "{} Now wielding {}! (sheathed {})",
            "⚔️".bold(),
            item_name.green().bold(),
            old_name.dimmed()
        );
    } else {
        println!(
            "{} Now wielding {}!",
            "⚔️".bold(),
            item_name.green().bold()
        );
    }

    if let Err(e) = state::save(&game) {
        eprintln!("{} Failed to save: {}", "❌".bold(), e.red());
    }
}

fn cmd_remove(name: &str) {
    if name.is_empty() {
        eprintln!(
            "{} Usage: {} (or use slot keyword: weapon, armor, ring)",
            "❌".bold(),
            "sq remove <item name>".cyan()
        );
        return;
    }

    let mut game = match state::load() {
        Ok(g) => g,
        Err(e) => {
            eprintln!("{} {}", "❌".bold(), e.red());
            return;
        }
    };

    let query = name.to_lowercase();

    let slot = if fuzzy_match_name(
        game.character.weapon.as_ref().map_or("", |i| &i.name),
        name,
    ) || query == "weapon"
    {
        "weapon"
    } else if fuzzy_match_name(
        game.character.armor.as_ref().map_or("", |i| &i.name),
        name,
    ) || query == "armor"
        || query == "armour"
    {
        "armor"
    } else if fuzzy_match_name(
        game.character.ring.as_ref().map_or("", |i| &i.name),
        name,
    ) || query == "ring"
    {
        "ring"
    } else {
        let equipped: Vec<&str> = [
            game.character.weapon.as_ref().map(|i| i.name.as_str()),
            game.character.armor.as_ref().map(|i| i.name.as_str()),
            game.character.ring.as_ref().map(|i| i.name.as_str()),
        ]
        .iter()
        .filter_map(|x| *x)
        .collect();

        if equipped.is_empty() {
            println!("{} Nothing equipped. Use {} to see your gear.", "⚠️".yellow(), "sq status".cyan());
        } else {
            println!(
                "{} No equipped item matching {}. Equipped: {}",
                "⚠️".yellow(),
                format!("\"{}\"", name).white().bold(),
                equipped.join(", ").dimmed()
            );
        }
        return;
    };

    if game.character.inventory.len() >= 20 {
        println!(
            "{} Inventory full (20/20). Drop an item first with {}.",
            "⚠️".yellow(),
            "sq drop <name>".cyan()
        );
        return;
    }

    let item = match slot {
        "weapon" => game.character.weapon.take(),
        "armor" => game.character.armor.take(),
        "ring" => game.character.ring.take(),
        _ => None,
    };

    if let Some(item) = item {
        let item_name = item.name.clone();
        let slot_name = format!("{}", item.slot);
        game.character.inventory.push(item);
        println!(
            "{} Removed {} from {} slot → moved to inventory.",
            "📦".bold(),
            item_name.white().bold(),
            slot_name.dimmed()
        );
    }

    if let Err(e) = state::save(&game) {
        eprintln!("{} Failed to save: {}", "❌".bold(), e.red());
    }
}

fn cmd_drink(name: &str) {
    if name.is_empty() {
        eprintln!(
            "{} Usage: {}",
            "❌".bold(),
            "sq drink <potion name>".cyan()
        );
        return;
    }

    let mut game = match state::load() {
        Ok(g) => g,
        Err(e) => {
            eprintln!("{} {}", "❌".bold(), e.red());
            return;
        }
    };

    let idx = match find_inventory_item(&game, name) {
        Ok(Some(i)) => i,
        Ok(None) => {
            println!(
                "{} No item matching {} in your inventory.",
                "⚠️".yellow(),
                format!("\"{}\"", name).white().bold()
            );
            return;
        }
        Err(msg) => {
            println!("{} {}", "⚠️".yellow(), msg);
            return;
        }
    };

    let item = &game.character.inventory[idx];
    if item.slot != character::ItemSlot::Potion {
        println!(
            "{} {} is not drinkable.",
            "⚠️".yellow(),
            item.name.cyan().bold()
        );
        return;
    }

    let item = game.character.inventory.remove(idx);
    let heal = item.power;
    let item_name = item.name.clone();
    game.character.heal(heal);

    println!(
        "{} You drink the {}! Restored {} HP. HP: {}/{}",
        "🧪".bold(),
        item_name.green().bold(),
        format!("+{}", heal).green().bold(),
        format!("{}", game.character.hp).white().bold(),
        game.character.max_hp
    );

    if let Err(e) = state::save(&game) {
        eprintln!("{} Failed to save: {}", "❌".bold(), e.red());
    }
}

fn cmd_drop_item(name: &str) {
    if name.is_empty() {
        eprintln!(
            "{} Usage: {}",
            "❌".bold(),
            "sq drop <item name>".cyan()
        );
        return;
    }

    let mut game = match state::load() {
        Ok(g) => g,
        Err(e) => {
            eprintln!("{} {}", "❌".bold(), e.red());
            return;
        }
    };

    let idx = match find_inventory_item(&game, name) {
        Ok(Some(i)) => i,
        Ok(None) => {
            println!(
                "{} No item matching {} in your inventory.",
                "⚠️".yellow(),
                format!("\"{}\"", name).white().bold()
            );
            return;
        }
        Err(msg) => {
            println!("{} {}", "⚠️".yellow(), msg);
            return;
        }
    };

    let item = game.character.inventory.remove(idx);
    println!(
        "{} Dropped {} forever. It vanishes into the void.",
        "🗑️".bold(),
        item.name.red().bold()
    );

    if let Err(e) = state::save(&game) {
        eprintln!("{} Failed to save: {}", "❌".bold(), e.red());
    }
}

fn cmd_reset() {
    let answer = prompt(&format!(
        "{} This will delete your character permanently! Are you sure? [y/N] ",
        "💀".red().bold()
    ));
    if answer.to_lowercase() == "y" {
        let path = state::save_path();
        if path.exists() {
            match std::fs::remove_file(&path) {
                Ok(()) => println!(
                    "{} Character deleted. Run {} to start over.",
                    "🗑️".bold(),
                    "sq init".cyan()
                ),
                Err(e) => eprintln!("{} Failed to delete: {}", "❌".bold(), e.to_string().red()),
            }
        } else {
            println!("{}", "No character found.".dimmed());
        }
    } else {
        println!("{}", "Cancelled.".dimmed());
    }
}

fn cmd_update() {
    use std::process::Command;

    println!();
    println!(
        "{}",
        "⬆️  Updating shellquest...".bold().cyan()
    );
    println!(
        "{}",
        "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".dimmed()
    );

    // Try cargo install from crates.io first (simplest path)
    println!(
        "  {} Installing latest version from {}...",
        "📦".bold(),
        "crates.io".cyan()
    );

    let status = Command::new("cargo")
        .args(["install", "shellquest", "--force"])
        .status();

    match status {
        Ok(s) if s.success() => {
            println!();
            println!(
                "{}",
                "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".dimmed()
            );
            println!(
                "{} {} Restart your shell or run {} to use the new version.",
                "✅".bold(),
                "Update complete!".green().bold(),
                "sq status".cyan()
            );
            println!(
                "{}",
                "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".dimmed()
            );
            println!();
        }
        Ok(_) => {
            eprintln!(
                "{} {} Try manually: {}",
                "❌".bold(),
                "Update failed.".red(),
                "cargo install shellquest --force".dimmed()
            );
        }
        Err(e) => {
            eprintln!(
                "{} Failed to run cargo: {}",
                "❌".bold(),
                e.to_string().red()
            );
            eprintln!(
                "  Make sure {} is installed: {}",
                "cargo".bold(),
                "https://rustup.rs".dimmed()
            );
        }
    }
}

fn cmd_arena(from_deprecated: bool) {
    if !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() {
        eprintln!("Arena requires an interactive terminal.");
        std::process::exit(1);
    }

    if from_deprecated {
        println!(
            "{}",
            "⚠️  The `tournament` command is deprecated. Use `sq arena` instead."
                .yellow()
        );
    }

    let mut game = match state::load() {
        Ok(g) => g,
        Err(e) => {
            eprintln!("{} {}", "❌".bold(), e.red());
            return;
        }
    };

    let tier = match select_arena_tier(&game.character) {
        Some(t) => t,
        None => return,
    };

    if !tier.is_unlocked(&game.character) {
        let req = if tier.or_unlock {
            format!(
                "Requires level {} or prestige {}.",
                tier.min_level, tier.min_prestige
            )
        } else {
            format!(
                "Requires level {} and prestige {}.",
                tier.min_level, tier.min_prestige
            )
        };
        println!(
            "{} {} is locked. {}",
            "🔒".bold(),
            tier.name.yellow().bold(),
            req
        );
        return;
    }

    let entry = arena::ArenaEntrySnapshot::from_character(&game.character);
    let fee = tier.compute_fee(&entry);

    if game.character.gold < fee {
        println!(
            "{} Not enough gold! {} entry fee is {} gold, you have {}.",
            "⚠️".yellow(),
            tier.name,
            format!("{}", fee).yellow().bold(),
            format!("{}", game.character.gold).yellow()
        );
        return;
    }

    let confirm = prompt(&format!(
        "{} Enter {} for {} gold? [y/N] ",
        "🏟️".bold(),
        tier.name.yellow().bold(),
        format!("{}", fee).yellow().bold()
    ));
    if confirm.to_lowercase() != "y" {
        println!("{}", "Cancelled.".dimmed());
        return;
    }

    match arena::run_arena_session(&game.character, tier, fee) {
        Some(commit) => {
            // T7 transaction boundary: apply mutates state and returns deferred
            // reward output (level-up, overflow, inventory replacement). Nothing
            // user-visible is rendered until `state::save()` succeeds, so a save
            // failure leaves no stale prints behind.
            let deferred = arena::apply_arena_commit(&mut game, &commit);
            if let Err(e) = state::save(&game) {
                eprintln!("{} Failed to save arena results: {}", "❌".bold(), e.red());
                return;
            }
            arena::render_arena_deferred_output(&deferred);

            let (label, rounds) = match commit.outcome {
                arena::ArenaOutcome::Defeat { rounds_cleared } => ("Knocked out", rounds_cleared),
                arena::ArenaOutcome::CashOut { rounds_cleared } => ("Cashed out", rounds_cleared),
                arena::ArenaOutcome::Victory { rounds_cleared } => ("Victory", rounds_cleared),
            };
            println!(
                "{} {} after {} rounds.",
                "🏁".bold(),
                label,
                rounds
            );
        }
        None => {
            println!("{}", "Arena run cancelled before round 1.".dimmed());
        }
    }
}

fn select_arena_tier(character: &character::Character) -> Option<arena::ArenaTier> {
    println!();
    println!("{}", "🏟️  Arena Tiers".bold().yellow());
    println!("{}", "─".repeat(50).dimmed());

    for (i, tier) in arena::ARENA_TIERS.iter().enumerate() {
        let unlocked = tier.is_unlocked(character);
        let status = if unlocked {
            "✓".green().bold()
        } else {
            "🔒".dimmed()
        };
        let name = if unlocked {
            tier.name.white().bold()
        } else {
            tier.name.dimmed()
        };
        let req = if tier.or_unlock {
            format!("lvl {} or prestige {}", tier.min_level, tier.min_prestige)
        } else {
            format!("lvl {} & prestige {}", tier.min_level, tier.min_prestige)
        };
        println!(
            "  {}. {} {} — {} rounds — {}",
            format!("{}", i + 1).dimmed(),
            status,
            name,
            tier.max_rounds,
            req.dimmed()
        );
    }

    println!("{}", "─".repeat(50).dimmed());

    loop {
        let choice = prompt("   Select tier [1-5] (or press Enter to cancel): ");
        if choice.is_empty() {
            return None;
        }
        match choice.as_str() {
            "1" => return Some(arena::TIER_PIT),
            "2" => return Some(arena::TIER_GAUNTLET),
            "3" => return Some(arena::TIER_COLOSSEUM),
            "4" => return Some(arena::TIER_ABYSSAL),
            "5" => return Some(arena::TIER_GODSLAYER),
            _ => println!("   Invalid choice. Pick 1-5 or press Enter to cancel."),
        }
    }
}

