//! Command Line Interface (CLI) Module for Rona
//!
//! This module handles all command-line interface functionality for Rona, including
//! - Command parsing and execution
//! - Subcommand implementations
//! - CLI argument handling
//!
//! # Commands
//!
//! The CLI supports several commands:
//! - `add-with-exclude`: Add files to git while excluding specified patterns
//! - `commit`: Commit changes using the commit message from `commit_message.md`
//! - `config`: Create or manage local/global configuration files
//! - `generate`: Generate a new commit message file
//! - `init`: Initialize Rona configuration
//! - `list-status`: List git status files (for shell completion)
//! - `push`: Push changes to remote repository
//! - `set-editor`: Configure the editor for commit messages
//!
//! # Features
//!
//! - Supports verbose mode for detailed operation logging
//! - Supports dry-run mode for previewing changes
//! - Integrates with git commands
//! - Provides shell completion capabilities
//! - Handles configuration management
//!

use clap::{Command as ClapCommand, CommandFactory, Parser, Subcommand, ValueEnum, ValueHint};
use clap_complete::{Shell, generate};
use glob::Pattern;
use inquire::ui::{Attributes, Color, RenderConfig, StyleSheet, Styled};
use inquire::{Confirm, Select, Text};
use std::{collections::HashMap, fs::read_to_string, io, process::Command};

use crate::{
    config::{Config, find_config_sources},
    errors::{Result, RonaError},
    extra_fields::{ExtraField, prompt_extra_field},
    git::{
        COMMIT_MESSAGE_FILE_PATH, COMMIT_TYPES, create_needed_files, format_branch_name,
        generate_commit_message, get_current_branch, get_current_commit_nb, get_status_files,
        get_top_level_path, git_add_with_exclude_patterns, git_commit, git_push,
    },
    template::{TemplateVariables, process_template, validate_template},
};

/// Configuration scope for config command
#[derive(Clone, Copy, Debug, ValueEnum)]
pub(crate) enum ConfigScope {
    /// Local project configuration (.rona.toml)
    Local,
    /// Global configuration (~/.config/rona.toml)
    Global,
}

/// CLI's commands
#[derive(Subcommand)]
pub(crate) enum CliCommand {
    /// Add all files to the `git add` command and exclude the patterns passed as positional arguments.
    #[command(short_flag = 'a', name = "add-with-exclude")]
    AddWithExclude {
        /// Patterns of files to exclude (supports glob patterns like `"node_modules/*"`)
        #[arg(value_name = "PATTERNS", value_hint = ValueHint::AnyPath)]
        to_exclude: Vec<String>,

        /// Show what would be added without actually adding files
        #[arg(long, default_value_t = false)]
        dry_run: bool,
    },

    /// Directly commit the file with the text in `commit_message.md`.
    #[command(short_flag = 'c')]
    Commit {
        /// Whether to push the commit after committing
        #[arg(short = 'p', long = "push", default_value_t = false)]
        push: bool,

        /// Show what would be committed without actually committing
        #[arg(short = 'd', long, default_value_t = false)]
        dry_run: bool,

        /// Create unsigned commit (default is to auto-detect GPG availability and sign if possible)
        #[arg(short = 'u', long = "unsigned", default_value_t = false)]
        unsigned: bool,

        /// Skip confirmation prompt and commit directly
        #[arg(short = 'y', long = "yes", default_value_t = false)]
        yes: bool,

        /// Copy commit message to clipboard instead of committing
        #[arg(long = "copy", default_value_t = false)]
        copy: bool,

        /// Additional arguments to pass to the commit command
        #[arg(allow_hyphen_values = true)]
        args: Vec<String>,
    },

    /// Generate shell completions for your shell
    #[command(name = "completion")]
    Completion {
        /// The shell to generate completions for
        #[arg(value_enum)]
        shell: Shell,
    },

    /// Manage configuration files (create or edit local or global config)
    #[command(name = "config")]
    Config {
        /// Scope of the configuration (local project or global)
        #[arg(value_enum)]
        scope: ConfigScope,

        /// Show what would be created without actually creating the config file
        #[arg(long, default_value_t = false)]
        dry_run: bool,
    },

    /// Directly generate the `commit_message.md` file.
    #[command(short_flag = 'g')]
    Generate {
        /// Show what would be generated without creating files
        #[arg(long, default_value_t = false)]
        dry_run: bool,

        /// Interactive mode - input the commit message directly in the terminal
        #[arg(short = 'i', long = "interactive", default_value_t = false)]
        interactive: bool,

        /// No commit number
        #[arg(short = 'n', long = "no-commit-number", default_value_t = false)]
        no_commit_number: bool,
    },

    /// Initialize the rona configuration file.
    #[command(short_flag = 'i', name = "init")]
    Initialize {
        /// Editor to use for the commit message.
        #[arg(default_value_t = String::from("nano"))]
        editor: String,

        /// Show what would be initialized without creating files
        #[arg(long, default_value_t = false)]
        dry_run: bool,
    },

    /// List files from git status (for shell completion on the -a)
    #[command(short_flag = 'l')]
    ListStatus,

    /// Push to a git repository.
    #[command(short_flag = 'p')]
    Push {
        /// Show what would be pushed without actually pushing
        #[arg(long, default_value_t = false)]
        dry_run: bool,

        /// Additional arguments to pass to the push command
        #[arg(allow_hyphen_values = true)]
        args: Vec<String>,
    },

    /// Set the editor to use for editing the commit message.
    #[command(short_flag = 's', name = "set-editor")]
    Set {
        /// The editor to use for the commit message
        #[arg(value_name = "EDITOR")]
        editor: String,

        /// Show what would be changed without modifying config
        #[arg(long, default_value_t = false)]
        dry_run: bool,
    },

    /// Sync current branch with main (or another branch) by pulling and merging/rebasing.
    #[command(name = "sync")]
    Sync {
        /// Branch to sync from (default: main)
        #[arg(short = 'b', long = "branch", default_value = "main")]
        source_branch: String,

        /// Use rebase instead of merge
        #[arg(short = 'r', long = "rebase", default_value_t = false)]
        rebase: bool,

        /// Create a new branch before syncing
        #[arg(short = 'n', long = "new-branch")]
        new_branch: Option<String>,

        /// Show what would be done without actually doing it
        #[arg(long, default_value_t = false)]
        dry_run: bool,
    },

    /// Show which configuration files would be used from a directory.
    /// Similar to 'git config --show-origin' - displays all config sources and their priority.
    #[command(short_flag = 'w', name = "which-config", visible_alias = "find-config")]
    WhichConfig {
        /// Directory to check from (defaults to current directory)
        #[arg(value_name = "PATH", value_hint = ValueHint::DirPath)]
        path: Option<String>,

        /// Show the effective (merged) configuration values
        #[arg(short = 'e', long = "effective", default_value_t = false)]
        show_effective: bool,
    },
}

#[derive(Parser)]
#[command(about = "Simple program that can:\n\
\t- Commit with the current 'commit_message.md' file text.\n\
\t- Generate the 'commit_message.md' file.\n\
\t- Push to git repository.\n\
\t- Add files with pattern exclusion.\n\
\nAll commands support --dry-run to preview changes.")]
#[command(author = "Tom Planche <tomplanche@proton.me>")]
#[command(help_template = "{about}\nMade by: {author}\n\nUSAGE:\n{usage}\n\n{all-args}\n")]
#[command(name = "rona")]
#[command(version)]
pub(crate) struct Cli {
    /// Commands
    #[command(subcommand)]
    pub(crate) command: CliCommand,

    /// Verbose output - show detailed information about operations
    #[arg(short, long, default_value = "false")]
    verbose: bool,

    /// Use the custom config file path instead of default
    #[arg(long, value_name = "PATH")]
    config: Option<String>,
}

/// Build the CLI command structure for generating completions
#[doc(hidden)]
fn build_cli() -> ClapCommand {
    Cli::command()
}

fn get_render_config() -> RenderConfig<'static> {
    let mut render_config = RenderConfig::default();

    // Prefix/icons
    render_config.prompt_prefix = Styled::new("$").with_fg(Color::LightRed);
    render_config.answered_prompt_prefix = Styled::new("✔").with_fg(Color::LightGreen);
    render_config.highlighted_option_prefix = Styled::new("➠").with_fg(Color::LightBlue);
    render_config.selected_checkbox = Styled::new("☑").with_fg(Color::LightGreen);
    render_config.unselected_checkbox = Styled::new("☐").with_fg(Color::Black);
    render_config.scroll_up_prefix = Styled::new("⇞").with_fg(Color::Black);
    render_config.scroll_down_prefix = Styled::new("⇟").with_fg(Color::Black);

    // Input prompt label
    render_config.prompt = StyleSheet::new()
        .with_fg(Color::LightCyan)
        .with_attr(Attributes::BOLD);

    // Help under the input
    render_config.help_message = StyleSheet::new()
        .with_fg(Color::DarkYellow)
        .with_attr(Attributes::ITALIC);

    // Validation error
    render_config.error_message = render_config
        .error_message
        .with_prefix(Styled::new("❌").with_fg(Color::LightRed));

    // Shown after submit (echoed answer)
    render_config.answer = StyleSheet::new()
        .with_fg(Color::LightMagenta)
        .with_attr(Attributes::BOLD);

    // Optional: default/placeholder styles
    render_config.default_value = StyleSheet::new().with_fg(Color::LightBlue);
    render_config.placeholder = StyleSheet::new().with_fg(Color::Black);

    render_config
}

/// Print custom fish shell completions that enhance the auto-generated ones
#[doc(hidden)]
fn print_fish_custom_completions() {
    println!();
    println!("# === CUSTOM RONA COMPLETIONS ===");
    println!("# Helper function to get git status files");
    println!("function __rona_status_files");
    println!("    rona -l");
    println!("end");
    println!();
    println!("# Command-specific completions");
    println!("# add-with-exclude: Complete with git status files");
    println!(
        "complete -c rona -n '__fish_seen_subcommand_from add-with-exclude -a' -xa '(__rona_status_files)'"
    );
}

/// Handle the `AddWithExclude` command which adds files to git while excluding specified patterns.
///
/// # Arguments
/// * `exclude` - List of glob patterns for files to exclude from git add
/// * `config` - Global configuration including verbose and dry-run settings
///
/// # Errors
/// * If any glob pattern is invalid
/// * If git add operation fails
/// * If reading git status fails
fn handle_add_with_exclude(exclude: &[String], config: &Config) -> Result<()> {
    let patterns: Vec<Pattern> = exclude
        .iter()
        .map(|p| {
            Pattern::new(p)
                .map_err(|e| RonaError::InvalidInput(format!("Invalid glob pattern '{p}': {e}")))
        })
        .collect::<Result<Vec<Pattern>>>()?;

    git_add_with_exclude_patterns(&patterns, config.verbose, config.dry_run)?;
    Ok(())
}

/// Handle the Commit command which commits changes using the message from `commit_message.md`.
///
/// # Arguments
/// * `args` - Additional arguments to pass to git commit
/// * `push` - Whether to push changes after committing
/// * `unsigned` - Whether to create an unsigned commit (skips -S flag)
/// * `yes` - Whether to skip the confirmation prompt
/// * `copy` - Whether to copy the commit message to clipboard instead of committing
/// * `config` - Global configuration including verbose and dry-run settings
///
/// # Errors
/// * If git commit operation fails
/// * If push is true and git push operation fails
/// * If commit message file doesn't exist or cannot be read
/// * If user cancels the commit confirmation
/// * If clipboard operation fails
#[allow(clippy::fn_params_excessive_bools)]
fn handle_commit(
    args: &[String],
    push: bool,
    unsigned: bool,
    yes: bool,
    copy: bool,
    config: &Config,
) -> Result<()> {
    // Read the commit message file
    let project_root = get_top_level_path()?;
    let commit_file_path = project_root.join(COMMIT_MESSAGE_FILE_PATH);

    if !commit_file_path.exists() {
        return Err(crate::errors::RonaError::Git(
            crate::errors::GitError::CommitMessageNotFound,
        ));
    }

    let commit_message = read_to_string(&commit_file_path)?;

    // If copy flag is set, copy to clipboard and exit
    if copy {
        use arboard::Clipboard;
        let mut clipboard = Clipboard::new().map_err(|e| {
            crate::errors::RonaError::Io(std::io::Error::other(format!(
                "Failed to access clipboard: {e}"
            )))
        })?;

        clipboard.set_text(&commit_message).map_err(|e| {
            crate::errors::RonaError::Io(std::io::Error::other(format!(
                "Failed to copy to clipboard: {e}"
            )))
        })?;

        println!("Commit message copied to clipboard");
        return Ok(());
    }

    // Show confirmation prompt unless --yes flag is set or in dry-run mode
    if !yes && !config.dry_run {
        // Show confirmation prompt
        let confirmation_message = format!("Commit with message:\n{}", commit_message.trim());
        let confirm = Confirm::new(&confirmation_message)
            .with_default(true)
            .prompt()
            .unwrap_or(false);

        if !confirm {
            println!("Commit cancelled.");
            return Ok(());
        }
    }

    git_commit(args, unsigned, config.dry_run)?;

    if push {
        git_push(args, config.verbose, config.dry_run)?;
    }
    Ok(())
}

/// Handle the Completion command
#[doc(hidden)]
fn handle_completion(shell: Shell) {
    let mut cmd = build_cli();
    generate(shell, &mut cmd, "rona", &mut io::stdout());

    // Add custom completions for fish shell
    if matches!(shell, Shell::Fish) {
        print_fish_custom_completions();
    }
}

/// Prompt the commit message and any configured extra fields in the order defined by
/// `field_order`.
///
/// The reserved name `"message"` positions the built-in message prompt among the extra
/// fields. Extra fields not listed in `field_order` are appended after all listed items.
/// When `field_order` is empty the default order is: extra fields first, then message.
///
/// # Errors
/// Returns an error if any prompt is cancelled or a validation regex is invalid.
fn prompt_interactive_fields(
    extra_fields: &[ExtraField],
    field_order: &[String],
) -> Result<(String, HashMap<String, String>)> {
    const MESSAGE_KEY: &str = "message";

    let ordered: Vec<String> = if field_order.is_empty() {
        let mut v: Vec<String> = extra_fields.iter().map(|f| f.name.clone()).collect();
        v.push(MESSAGE_KEY.to_string());
        v
    } else {
        let mut v: Vec<String> = field_order.to_vec();
        // Append extra fields not explicitly listed
        for f in extra_fields {
            if !v.iter().any(|s| s == &f.name) {
                v.push(f.name.clone());
            }
        }
        // Guarantee message is always prompted
        if !v.iter().any(|s| s == MESSAGE_KEY) {
            v.push(MESSAGE_KEY.to_string());
        }
        v
    };

    let mut message: Option<String> = None;
    let mut extra_values: HashMap<String, String> = HashMap::new();

    for name in &ordered {
        if name == MESSAGE_KEY {
            message = Some(
                Text::new("Message")
                    .prompt()
                    .map_err(|_| RonaError::UserCancelled)?,
            );
        } else if let Some(field) = extra_fields.iter().find(|f| f.name == *name)
            && let Some(value) = prompt_extra_field(field)?
        {
            extra_values.insert(field.name.clone(), value);
        }
    }

    let message = message
        .ok_or_else(|| RonaError::InvalidInput("message prompt was not executed".to_string()))?;

    Ok((message, extra_values))
}

/// Handle the Generate command which creates a new commit message file.
///
/// # Arguments
/// * `interactive` - Whether to prompt for commit message in terminal
/// * `no_commit_number` - Whether to include commit number in message
/// * `config` - Global configuration including verbose and dry-run settings
///
/// # Errors
/// * If creating needed files fails
/// * If generating commit message fails
/// * If writing commit message fails
/// * If launching editor fails (in non-interactive mode)
fn handle_generate(interactive: bool, no_commit_number: bool, config: &Config) -> Result<()> {
    if config.dry_run {
        println!("Would create files: commit_message.md, .commitignore");
        println!("Would add files to .git/info/exclude");
        return Ok(());
    }

    create_needed_files()?;

    let commit_type = {
        let commit_types_vec = config.project_config.commit_types.as_ref().map_or_else(
            || COMMIT_TYPES.to_vec(),
            |v| v.iter().map(String::as_str).collect::<Vec<&str>>(),
        );

        Select::new("Select commit type", commit_types_vec)
            .with_starting_cursor(0)
            .prompt()
            .map_err(|_| RonaError::UserCancelled)?
    };

    if interactive {
        // In interactive mode, prompt all fields (including message) in configured order
        let (message, extra_values) = prompt_interactive_fields(
            &config.project_config.extra_fields,
            &config.project_config.field_order,
        )?;
        handle_interactive_mode(
            commit_type,
            no_commit_number,
            &message,
            &extra_values,
            config,
        )?;
    } else {
        // In editor mode, generate the template file first, then open editor
        generate_commit_message(commit_type, no_commit_number)?;
        handle_editor_mode(config)?;
    }
    Ok(())
}

/// Handle interactive mode for generate command
fn handle_interactive_mode(
    commit_type: &str,
    no_commit_number: bool,
    message: &str,
    extra_values: &HashMap<String, String>,
    config: &Config,
) -> Result<()> {
    use std::fs;

    let project_root = get_top_level_path()?;
    let commit_file_path = project_root.join(COMMIT_MESSAGE_FILE_PATH);

    if message.trim().is_empty() {
        println!("⚠️  Empty message provided. Exiting.");
        return Ok(());
    }

    let branch_name = format_branch_name(&COMMIT_TYPES, &get_current_branch()?);
    let commit_number = if no_commit_number {
        None
    } else {
        Some(get_current_commit_nb()? + 1)
    };

    // Get template from config or use default with conditional syntax
    // The conditional block {?commit_number}...{/commit_number} is only included if commit_number has a value
    let default_template = "{?commit_number}[{commit_number}] {/commit_number}({commit_type} on {branch_name}) {message}";

    let template = config
        .project_config
        .template
        .as_deref()
        .unwrap_or(default_template);

    // Warn about extra fields that are configured but not referenced in the template.
    // A field is considered "used" if {name} or {?name} appears anywhere in the template.
    for field in &config.project_config.extra_fields {
        let referenced = template.contains(&format!("{{{}}}", field.name))
            || template.contains(&format!("{{?{}}}", field.name));
        if !referenced {
            println!(
                "[WARNING] Extra field '{}' is configured but not referenced in the template.",
                field.name
            );
        }
    }

    // Validate template (including any extra field variable names)
    let extra_names: Vec<&str> = extra_values.keys().map(String::as_str).collect();
    if let Err(e) = validate_template(template, &extra_names) {
        println!("⚠️  Template validation error: {e}");
        println!("Using fallback format...");
        let formatted_message = if no_commit_number {
            format!("({} on {}) {}", commit_type, branch_name, message.trim())
        } else {
            format!(
                "[{}] ({} on {}) {}",
                commit_number.unwrap_or(0),
                commit_type,
                branch_name,
                message.trim()
            )
        };
        fs::write(&commit_file_path, &formatted_message)?;
        println!("\n✅ Commit message created!");
        println!("📄 Message: {formatted_message}");
        return Ok(());
    }

    // Create template variables
    let variables = TemplateVariables::new(
        commit_number,
        commit_type.to_string(),
        branch_name,
        message.trim().to_string(),
    )?;

    // Process template (extra_values are substituted alongside built-in variables)
    let formatted_message = process_template(template, &variables, extra_values)?;

    // Write the formatted message to commit_message.md
    fs::write(&commit_file_path, &formatted_message)?;

    println!("\n✅ Commit message created!");
    println!("📄 Message: {formatted_message}");
    Ok(())
}

/// Handle editor mode for generate command
fn handle_editor_mode(config: &Config) -> Result<()> {
    let editor = config.get_editor()?;
    let project_root = get_top_level_path()?;
    let commit_file_path = project_root.join(COMMIT_MESSAGE_FILE_PATH);

    Command::new(&editor)
        .arg(&commit_file_path)
        .spawn()
        .map_err(|e| RonaError::CommandFailed {
            command: format!("Failed to spawn editor '{editor}': {e}"),
        })?
        .wait()
        .map_err(|e| RonaError::CommandFailed {
            command: format!("Failed to wait for editor '{editor}': {e}"),
        })?;
    Ok(())
}

/// Handle the Initialize command which creates the initial configuration file.
///
/// # Arguments
/// * `editor` - The editor command to configure
/// * `config` - Global configuration including verbose and dry-run settings
///
/// # Errors
/// * If creating configuration file fails
fn handle_initialize(editor: &str, config: &Config) -> Result<()> {
    if config.dry_run {
        println!("Would create config file with editor: {editor}");
        return Ok(());
    }
    config.create_config_file(editor)?;
    Ok(())
}

/// Handle the `ListStatus` command
fn handle_list_status() -> Result<()> {
    let files = get_status_files()?;
    // Print each file on a new line for fish shell completion
    for file in files {
        println!("{file}");
    }
    Ok(())
}

/// Handle the Push command which pushes changes to the remote repository.
///
/// # Arguments
/// * `args` - Additional arguments to pass to git push
/// * `config` - Global configuration including verbose and dry-run settings
///
/// # Errors
/// * If git push operation fails
fn handle_push(args: &[String], config: &Config) -> Result<()> {
    git_push(args, config.verbose, config.dry_run)?;
    Ok(())
}

/// Handle the Set command which updates the editor in the configuration.
///
/// # Arguments
/// * `editor` - The editor command to set
/// * `config` - Global configuration including verbose and dry-run settings
///
/// # Errors
/// * If updating configuration file fails
fn handle_set(editor: &str, config: &Config) -> Result<()> {
    if config.dry_run {
        println!("Would set editor to: {editor}");
        return Ok(());
    }
    config.set_editor(editor)?;
    Ok(())
}

/// Handle the Sync command which syncs the current branch with another branch.
///
/// # Arguments
/// * `source_branch` - The branch to sync from (e.g., "main")
/// * `rebase` - Whether to use rebase instead of merge
/// * `new_branch` - Optional name for a new branch to create before syncing
/// * `config` - Global configuration including verbose and dry-run settings
///
/// # Errors
/// * If git operations fail
/// * If the source branch doesn't exist
/// * If there are uncommitted changes that would be lost
fn handle_sync(
    source_branch: &str,
    rebase: bool,
    new_branch: Option<&str>,
    config: &Config,
) -> Result<()> {
    use crate::git::{git_create_branch, git_merge, git_pull, git_rebase, git_switch};

    // Get current branch before any operations
    let original_branch = get_current_branch()?;

    if config.dry_run {
        if let Some(branch_name) = new_branch {
            println!("Would create new branch: {branch_name}");
        }
        println!("Would switch to: {source_branch}");
        println!("Would pull latest changes");
        println!(
            "Would switch back to: {}",
            new_branch.unwrap_or(&original_branch)
        );
        if rebase {
            println!("Would rebase with: {source_branch}");
        } else {
            println!("Would merge with: {source_branch}");
        }
        return Ok(());
    }

    // Create new branch if specified
    if let Some(branch_name) = new_branch {
        git_create_branch(branch_name)?;
        git_switch(branch_name)?;
    }

    let target_branch = new_branch.unwrap_or(&original_branch);

    // Switch to source branch and pull
    git_switch(source_branch)?;
    git_pull(config.verbose)?;

    // Switch back to target branch
    git_switch(target_branch)?;

    // Merge or rebase
    if rebase {
        git_rebase(source_branch, config.verbose)?;
    } else {
        git_merge(source_branch, config.verbose)?;
    }

    println!("\nSuccessfully synced '{target_branch}' with '{source_branch}'");
    Ok(())
}

/// Handle the `WhichConfig` command which shows which config files would be used.
///
/// # Arguments
/// * `path` - Optional directory to check from (defaults to current directory)
/// * `show_effective` - Whether to also show the effective merged configuration
///
/// # Errors
/// * If the directory does not exist
/// * If the home directory cannot be determined
fn handle_which_config(path: Option<&str>, show_effective: bool) -> Result<()> {
    use std::path::Path;

    let search_path = match path {
        Some(p) => {
            let path = Path::new(p);
            if !path.exists() {
                return Err(crate::errors::RonaError::Io(std::io::Error::new(
                    std::io::ErrorKind::NotFound,
                    format!("Directory not found: {p}"),
                )));
            }
            Some(path)
        }
        None => None,
    };

    let config_info = find_config_sources(search_path)?;

    println!("Searching from: {}", config_info.search_directory.display());
    println!();

    // Check if any config exists
    let active_sources: Vec<_> = config_info.sources.iter().filter(|s| s.exists).collect();

    if active_sources.is_empty() {
        println!("! No configuration files found.");
        println!();
        println!("Possible config locations (in loading order):");
        for source in &config_info.sources {
            println!(
                "  ○ [priority {}] {}",
                source.priority,
                source.path.display()
            );
            println!("    └─ {}", source.description);
        }
        println!();
        println!("Run 'rona init' or 'rona config local/global' to create a config file.");
        return Ok(());
    }

    println!("Configuration sources (in loading order, later overrides earlier):");
    println!();

    for source in &config_info.sources {
        let status = if source.exists { "✓" } else { "○" };
        let exists_text = if source.exists {
            "(active)"
        } else {
            "(not found)"
        };

        println!(
            "  {} [priority {}] {}",
            status,
            source.priority,
            source.path.display()
        );
        println!("    └─ {} {}", source.description, exists_text);
    }

    // Show which config takes precedence
    if let Some(highest) = active_sources.iter().max_by_key(|s| s.priority) {
        println!();
        println!("Effective config from: {}", highest.path.display());
    }

    // Show effective configuration values if requested
    if show_effective {
        println!();
        println!("Effective configuration values:");
        println!();

        if let Some(cfg) = &config_info.effective_config {
            if let Some(editor) = &cfg.editor {
                println!("- editor = \"{editor}\"");
            }
            if let Some(commit_types) = &cfg.commit_types {
                println!("- commit_types = {commit_types:?}");
            }
            if let Some(template) = &cfg.template {
                println!("- template = \"{template}\"");
            }
        } else {
            println!("  (using defaults)");
        }
    }

    Ok(())
}

/// Handle the Config command which creates or manages configuration files.
///
/// # Arguments
/// * `scope` - Whether to create local (.rona.toml) or global (~/.config/rona.toml) config
/// * `config` - Global configuration including verbose and dry-run settings
///
/// # Errors
/// * If creating configuration file fails
/// * If writing configuration content fails
fn handle_config_command(scope: ConfigScope, config: &Config) -> Result<()> {
    use std::io::Write;

    let config_path = {
        match scope {
            ConfigScope::Local => {
                let project_root = get_top_level_path()?;
                project_root.join(".rona.toml")
            }
            ConfigScope::Global => {
                let home = dirs::home_dir().ok_or(crate::errors::ConfigError::ConfigNotFound)?;
                home.join(".config/rona.toml")
            }
        }
    };

    if config.dry_run {
        println!(
            "Would create {} configuration file at: {}",
            match scope {
                ConfigScope::Local => "local",
                ConfigScope::Global => "global",
            },
            config_path.display()
        );
        return Ok(());
    }

    // Check if config already exists
    if config_path.exists() {
        println!(
            "Configuration file already exists at: {}",
            config_path.display()
        );
        println!("Use 'rona set-editor <editor>' to modify the editor setting.");
        return Ok(());
    }

    // Create parent directory if it doesn't exist (for global config)
    if let Some(parent) = config_path.parent()
        && !parent.exists()
    {
        std::fs::create_dir_all(parent)?;
    }

    // Get default config and serialize to TOML
    let default_config = crate::config::ProjectConfig::default();
    let toml_content = toml::to_string_pretty(&default_config)
        .map_err(|_| crate::errors::ConfigError::InvalidConfig)?;

    // Write the config file
    let mut file = std::fs::File::create(&config_path)?;
    file.write_all(toml_content.as_bytes())?;

    println!("Configuration file created at: {}", config_path.display());
    println!("You can now edit this file to customize your settings.");

    Ok(())
}

/// Runs the program by parsing command line arguments and executing the appropriate command.
///
/// # Errors
/// * If creating configuration fails
/// * If command execution fails
/// * If any operation fails based on command-specific errors
///
/// # Returns
/// * `Result<()>` - Ok if all operations succeed, Err with error details otherwise
pub fn run() -> Result<()> {
    // Apply global colors/styles for all inquire prompts
    inquire::set_global_render_config(get_render_config());

    let cli = Cli::parse();

    // Initialize structured logging. Respects RUST_LOG env var; falls back to
    // "debug" when --verbose is set, "warn" otherwise.
    let log_level = if cli.verbose { "debug" } else { "warn" };
    let filter = tracing_subscriber::EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(log_level));
    tracing_subscriber::fmt()
        .with_env_filter(filter)
        .with_target(false)
        .compact()
        .try_init()
        .ok();

    let mut config = if let Some(ref config_path) = cli.config {
        Config::new_with_config_file(std::path::Path::new(config_path))?
    } else {
        Config::new()?
    };

    // Set the global flags in the config
    config.set_verbose(cli.verbose);

    match cli.command {
        CliCommand::AddWithExclude {
            to_exclude: exclude,
            dry_run,
        } => {
            config.set_dry_run(dry_run);
            handle_add_with_exclude(&exclude, &config)
        }

        CliCommand::Commit {
            args,
            push,
            dry_run,
            unsigned,
            yes,
            copy,
        } => {
            config.set_dry_run(dry_run);
            handle_commit(&args, push, unsigned, yes, copy, &config)
        }

        CliCommand::Completion { shell } => {
            handle_completion(shell);
            Ok(())
        }

        CliCommand::Config { scope, dry_run } => {
            config.set_dry_run(dry_run);
            handle_config_command(scope, &config)
        }

        CliCommand::Generate {
            dry_run,
            interactive,
            no_commit_number,
        } => {
            config.set_dry_run(dry_run);
            handle_generate(interactive, no_commit_number, &config)
        }

        CliCommand::Initialize { editor, dry_run } => {
            config.set_dry_run(dry_run);
            handle_initialize(&editor, &config)
        }

        CliCommand::ListStatus => handle_list_status(),

        CliCommand::Push { args, dry_run } => {
            config.set_dry_run(dry_run);
            handle_push(&args, &config)
        }

        CliCommand::Set { editor, dry_run } => {
            config.set_dry_run(dry_run);
            handle_set(&editor, &config)
        }

        CliCommand::Sync {
            source_branch,
            rebase,
            new_branch,
            dry_run,
        } => {
            config.set_dry_run(dry_run);
            handle_sync(&source_branch, rebase, new_branch.as_deref(), &config)
        }

        CliCommand::WhichConfig {
            path,
            show_effective,
        } => handle_which_config(path.as_deref(), show_effective),
    }
}

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

    type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;

    // === ADD COMMAND TESTS ===

    #[test]
    fn test_add_basic() -> TestResult {
        let args = vec!["rona", "-a"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::AddWithExclude {
            to_exclude: exclude,
            dry_run,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(exclude.is_empty());
        assert!(!dry_run);
        Ok(())
    }

    #[test]
    fn test_add_single_pattern() -> TestResult {
        let args = vec!["rona", "-a", "*.txt"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::AddWithExclude {
            to_exclude: exclude,
            dry_run,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert_eq!(exclude, vec!["*.txt"]);
        assert!(!dry_run);
        Ok(())
    }

    #[test]
    fn test_add_multiple_patterns() -> TestResult {
        let args = vec!["rona", "-a", "*.txt", "*.log", "target/*"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::AddWithExclude {
            to_exclude: exclude,
            dry_run,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert_eq!(exclude, vec!["*.txt", "*.log", "target/*"]);
        assert!(!dry_run);
        Ok(())
    }

    #[test]
    fn test_add_with_long_name() -> TestResult {
        let args = vec!["rona", "add-with-exclude", "*.txt"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::AddWithExclude {
            to_exclude: exclude,
            dry_run,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert_eq!(exclude, vec!["*.txt"]);
        assert!(!dry_run);
        Ok(())
    }

    // === COMMIT COMMAND TESTS ===

    #[test]
    fn test_commit_basic() -> TestResult {
        let args = vec!["rona", "-c"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Commit {
            args,
            push,
            dry_run,
            unsigned,
            yes,
            copy,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(!push);
        assert!(args.is_empty());
        assert!(!dry_run);
        assert!(!unsigned);
        assert!(!yes);
        assert!(!copy);
        Ok(())
    }

    #[test]
    fn test_commit_with_push_flag() -> TestResult {
        let args = vec!["rona", "-c", "--push"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Commit {
            args,
            push,
            dry_run,
            unsigned,
            yes,
            copy,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(push);
        assert!(args.is_empty());
        assert!(!dry_run);
        assert!(!unsigned);
        assert!(!yes);
        assert!(!copy);
        Ok(())
    }

    #[test]
    fn test_commit_with_message() -> TestResult {
        let args = vec!["rona", "-c", "Regular commit message"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Commit {
            args,
            push,
            dry_run,
            unsigned,
            yes,
            copy,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(!push);
        assert_eq!(args, vec!["Regular commit message"]);
        assert!(!dry_run);
        assert!(!unsigned);
        assert!(!yes);
        assert!(!copy);
        Ok(())
    }

    #[test]
    fn test_commit_with_git_flag() -> TestResult {
        let args = vec!["rona", "-c", "--amend"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Commit {
            args,
            push,
            dry_run,
            unsigned,
            yes,
            copy,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(!push);
        assert_eq!(args, vec!["--amend"]);
        assert!(!dry_run);
        assert!(!unsigned);
        assert!(!yes);
        assert!(!copy);
        Ok(())
    }

    #[test]
    fn test_commit_with_multiple_git_flags() -> TestResult {
        let args = vec!["rona", "-c", "--amend", "--no-edit"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Commit {
            args,
            push,
            dry_run,
            unsigned,
            yes,
            copy,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(!push);
        assert_eq!(args, vec!["--amend", "--no-edit"]);
        assert!(!dry_run);
        assert!(!unsigned);
        assert!(!yes);
        assert!(!copy);
        Ok(())
    }

    #[test]
    fn test_commit_with_push_and_git_flags() -> TestResult {
        let args = vec!["rona", "-c", "--push", "--amend", "--no-edit"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Commit {
            args,
            push,
            dry_run,
            unsigned,
            yes,
            copy,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(push);
        assert_eq!(args, vec!["--amend", "--no-edit"]);
        assert!(!dry_run);
        assert!(!unsigned);
        assert!(!yes);
        assert!(!copy);
        Ok(())
    }

    #[test]
    fn test_commit_with_message_and_push() -> TestResult {
        let args = vec!["rona", "-c", "--push", "Commit message"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Commit {
            args,
            push,
            dry_run,
            unsigned,
            yes,
            copy,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(push);
        assert_eq!(args, vec!["Commit message"]);
        assert!(!dry_run);
        assert!(!unsigned);
        assert!(!yes);
        assert!(!copy);
        Ok(())
    }

    // === PUSH COMMAND TESTS ===

    #[test]
    fn test_push_basic() -> TestResult {
        let args = vec!["rona", "-p"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Push { args, dry_run } = cli.command else {
            return Err("Wrong command parsed".into());
        };
        assert!(args.is_empty());
        assert!(!dry_run);
        Ok(())
    }

    #[test]
    fn test_push_with_force() -> TestResult {
        let args = vec!["rona", "-p", "--force"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Push { args, dry_run } = cli.command else {
            return Err("Wrong command parsed".into());
        };
        assert_eq!(args, vec!["--force"]);
        assert!(!dry_run);
        Ok(())
    }

    #[test]
    fn test_push_with_multiple_args() -> TestResult {
        let args = vec!["rona", "-p", "--force", "--set-upstream", "origin", "main"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Push { args, dry_run } = cli.command else {
            return Err("Wrong command parsed".into());
        };
        assert_eq!(args, vec!["--force", "--set-upstream", "origin", "main"]);
        assert!(!dry_run);
        Ok(())
    }

    #[test]
    fn test_push_with_remote_and_branch() -> TestResult {
        let args = vec!["rona", "-p", "origin", "feature/branch"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Push { args, dry_run } = cli.command else {
            return Err("Wrong command parsed".into());
        };
        assert_eq!(args, vec!["origin", "feature/branch"]);
        assert!(!dry_run);
        Ok(())
    }

    #[test]
    fn test_push_with_upstream_tracking() -> TestResult {
        let args = vec!["rona", "-p", "-u", "origin", "main"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Push { args, dry_run } = cli.command else {
            return Err("Wrong command parsed".into());
        };
        assert_eq!(args, vec!["-u", "origin", "main"]);
        assert!(!dry_run);
        Ok(())
    }

    // === GENERATE COMMAND TESTS ===

    #[test]
    fn test_generate_command() -> TestResult {
        let args = vec!["rona", "-g"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Generate {
            dry_run,
            interactive,
            no_commit_number,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(!dry_run);
        assert!(!interactive);
        assert!(!no_commit_number);
        Ok(())
    }

    #[test]
    fn test_generate_interactive_command() -> TestResult {
        let args = vec!["rona", "-g", "-i"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Generate {
            dry_run,
            interactive,
            no_commit_number,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(!dry_run);
        assert!(interactive);
        assert!(!no_commit_number);
        Ok(())
    }

    #[test]
    fn test_generate_interactive_long_form() -> TestResult {
        let args = vec!["rona", "-g", "--interactive"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Generate {
            dry_run,
            interactive,
            no_commit_number,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(!dry_run);
        assert!(interactive);
        assert!(!no_commit_number);
        Ok(())
    }

    #[test]
    fn test_generate_no_commit_number() -> TestResult {
        let args = vec!["rona", "-g", "-n"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Generate {
            dry_run,
            interactive,
            no_commit_number,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(!dry_run);
        assert!(!interactive);
        assert!(no_commit_number);
        Ok(())
    }

    #[test]
    fn test_generate_no_commit_number_long_form() -> TestResult {
        let args = vec!["rona", "-g", "--no-commit-number"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Generate {
            dry_run,
            interactive,
            no_commit_number,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(!dry_run);
        assert!(!interactive);
        assert!(no_commit_number);
        Ok(())
    }

    #[test]
    fn test_generate_interactive_no_commit_number() -> TestResult {
        let args = vec!["rona", "-g", "-i", "-n"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Generate {
            dry_run,
            interactive,
            no_commit_number,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(!dry_run);
        assert!(interactive);
        assert!(no_commit_number);
        Ok(())
    }

    // === LIST STATUS COMMAND TESTS ===

    #[test]
    fn test_list_status_command() -> TestResult {
        let args = vec!["rona", "-l"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::ListStatus = cli.command else {
            return Err("Wrong command parsed".into());
        };
        Ok(())
    }

    // === INITIALIZE COMMAND TESTS ===

    #[test]
    fn test_init_default_editor() -> TestResult {
        let args = vec!["rona", "-i"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Initialize { editor, dry_run } = cli.command else {
            return Err("Wrong command parsed".into());
        };
        assert_eq!(editor, "nano");
        assert!(!dry_run);
        Ok(())
    }

    #[test]
    fn test_init_custom_editor() -> TestResult {
        let args = vec!["rona", "-i", "zed"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Initialize { editor, dry_run } = cli.command else {
            return Err("Wrong command parsed".into());
        };
        assert_eq!(editor, "zed");
        assert!(!dry_run);
        Ok(())
    }

    // === SET EDITOR COMMAND TESTS ===

    #[test]
    fn test_set_editor() -> TestResult {
        let args = vec!["rona", "-s", "vim"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Set { editor, dry_run } = cli.command else {
            return Err("Wrong command parsed".into());
        };
        assert_eq!(editor, "vim");
        assert!(!dry_run);
        Ok(())
    }

    #[test]
    fn test_set_editor_with_spaces() -> TestResult {
        let args = vec!["rona", "-s", "\"Visual Studio Code\""];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Set { editor, dry_run } = cli.command else {
            return Err("Wrong command parsed".into());
        };
        assert_eq!(editor, "\"Visual Studio Code\"");
        assert!(!dry_run);
        Ok(())
    }

    #[test]
    fn test_set_editor_with_path() -> TestResult {
        let args = vec!["rona", "-s", "/usr/bin/vim"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Set { editor, dry_run } = cli.command else {
            return Err("Wrong command parsed".into());
        };
        assert_eq!(editor, "/usr/bin/vim");
        assert!(!dry_run);
        Ok(())
    }

    // === VERBOSE FLAG TESTS ===

    #[test]
    fn test_verbose_with_commit() -> TestResult {
        let args = vec!["rona", "-v", "-c"];
        let cli = Cli::try_parse_from(args)?;
        assert!(cli.verbose);
        Ok(())
    }

    #[test]
    fn test_verbose_with_push() -> TestResult {
        let args = vec!["rona", "-v", "-p"];
        let cli = Cli::try_parse_from(args)?;
        assert!(cli.verbose);
        Ok(())
    }

    #[test]
    fn test_verbose_long_form() -> TestResult {
        let args = vec!["rona", "--verbose", "-c"];
        let cli = Cli::try_parse_from(args)?;
        assert!(cli.verbose);
        Ok(())
    }

    // === EDGE CASES AND ERROR TESTS ===

    #[test]
    fn test_commit_flag_order_sensitivity() -> TestResult {
        let args = vec!["rona", "-c", "--amend", "--push"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Commit {
            args,
            push,
            dry_run,
            unsigned,
            yes,
            copy,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(!push); // --push should be treated as git arg
        assert_eq!(args, vec!["--amend", "--push"]);
        assert!(!dry_run);
        assert!(!unsigned);
        assert!(!yes);
        assert!(!copy);
        Ok(())
    }

    #[test]
    fn test_commit_with_similar_looking_args() -> TestResult {
        let args = vec!["rona", "-c", "--push-to-upstream"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Commit {
            args,
            push,
            dry_run,
            unsigned,
            yes,
            copy,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(!push);
        assert_eq!(args, vec!["--push-to-upstream"]);
        assert!(!dry_run);
        assert!(!unsigned);
        assert!(!yes);
        assert!(!copy);
        Ok(())
    }

    #[test]
    fn test_invalid_command() {
        let args = vec!["rona", "--invalid"];
        assert!(Cli::try_parse_from(args).is_err());
    }

    #[test]
    fn test_missing_required_value() {
        let args = vec!["rona", "-s"]; // missing editor value
        assert!(Cli::try_parse_from(args).is_err());
    }

    #[test]
    fn test_complex_command_combination() -> TestResult {
        let args = vec!["rona", "-v", "-c", "--push", "--amend", "--no-edit"];
        let cli = Cli::try_parse_from(args)?;

        assert!(cli.verbose);
        let CliCommand::Commit {
            args,
            push,
            dry_run,
            unsigned,
            yes,
            copy,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(push);
        assert_eq!(args, vec!["--amend", "--no-edit"]);
        assert!(!dry_run);
        assert!(!unsigned);
        assert!(!yes);
        assert!(!copy);
        Ok(())
    }

    #[test]
    fn test_commit_unsigned_short_flag() -> TestResult {
        let args = vec!["rona", "-c", "-u"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Commit {
            args,
            push,
            dry_run,
            unsigned,
            yes,
            copy,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(!push);
        assert!(args.is_empty());
        assert!(!dry_run);
        assert!(unsigned);
        assert!(!yes);
        assert!(!copy);
        Ok(())
    }

    #[test]
    fn test_commit_unsigned_long_flag() -> TestResult {
        let args = vec!["rona", "-c", "--unsigned"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Commit {
            args,
            push,
            dry_run,
            unsigned,
            yes,
            copy,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(!push);
        assert!(args.is_empty());
        assert!(!dry_run);
        assert!(unsigned);
        assert!(!yes);
        assert!(!copy);
        Ok(())
    }

    #[test]
    fn test_commit_unsigned_with_push_and_args() -> TestResult {
        let args = vec!["rona", "-c", "-u", "--push", "--amend"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Commit {
            args,
            push,
            dry_run,
            unsigned,
            yes,
            copy,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(push);
        assert_eq!(args, vec!["--amend"]);
        assert!(!dry_run);
        assert!(unsigned);
        assert!(!yes);
        assert!(!copy);
        Ok(())
    }

    #[test]
    fn test_commit_dry_run_short_flag() -> TestResult {
        let args = vec!["rona", "-c", "-d"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Commit {
            args,
            push,
            dry_run,
            unsigned,
            yes,
            copy,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(!push);
        assert!(args.is_empty());
        assert!(dry_run);
        assert!(!unsigned);
        assert!(!yes);
        assert!(!copy);
        Ok(())
    }

    #[test]
    fn test_commit_dry_run_long_flag() -> TestResult {
        let args = vec!["rona", "-c", "--dry-run"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Commit {
            args,
            push,
            dry_run,
            unsigned,
            yes,
            copy,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(!push);
        assert!(args.is_empty());
        assert!(dry_run);
        assert!(!unsigned);
        assert!(!yes);
        assert!(!copy);
        Ok(())
    }

    #[test]
    fn test_commit_dry_run_with_push() -> TestResult {
        let args = vec!["rona", "-c", "-d", "--push"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Commit {
            args,
            push,
            dry_run,
            unsigned,
            yes,
            copy,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(push);
        assert!(args.is_empty());
        assert!(dry_run);
        assert!(!unsigned);
        assert!(!yes);
        assert!(!copy);
        Ok(())
    }

    // === COPY FLAG TESTS ===

    #[test]
    fn test_commit_with_copy_flag() -> TestResult {
        let args = vec!["rona", "-c", "--copy"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Commit {
            args,
            push,
            dry_run,
            unsigned,
            yes,
            copy,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(!push);
        assert!(args.is_empty());
        assert!(!dry_run);
        assert!(!unsigned);
        assert!(!yes);
        assert!(copy);
        Ok(())
    }

    #[test]
    fn test_commit_copy_flag_with_other_flags() -> TestResult {
        let args = vec!["rona", "-c", "--copy", "--dry-run"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Commit {
            args,
            push,
            dry_run,
            unsigned,
            yes,
            copy,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert!(!push);
        assert!(args.is_empty());
        assert!(dry_run);
        assert!(!unsigned);
        assert!(!yes);
        assert!(copy);
        Ok(())
    }

    // === CONFIG COMMAND TESTS ===

    #[test]
    fn test_config_local() -> TestResult {
        let args = vec!["rona", "config", "local"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Config { scope, dry_run } = cli.command else {
            return Err("Wrong command parsed".into());
        };
        assert!(matches!(scope, ConfigScope::Local));
        assert!(!dry_run);
        Ok(())
    }

    #[test]
    fn test_config_global() -> TestResult {
        let args = vec!["rona", "config", "global"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Config { scope, dry_run } = cli.command else {
            return Err("Wrong command parsed".into());
        };
        assert!(matches!(scope, ConfigScope::Global));
        assert!(!dry_run);
        Ok(())
    }

    #[test]
    fn test_config_local_dry_run() -> TestResult {
        let args = vec!["rona", "config", "local", "--dry-run"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Config { scope, dry_run } = cli.command else {
            return Err("Wrong command parsed".into());
        };
        assert!(matches!(scope, ConfigScope::Local));
        assert!(dry_run);
        Ok(())
    }

    #[test]
    fn test_config_global_dry_run() -> TestResult {
        let args = vec!["rona", "config", "global", "--dry-run"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Config { scope, dry_run } = cli.command else {
            return Err("Wrong command parsed".into());
        };
        assert!(matches!(scope, ConfigScope::Global));
        assert!(dry_run);
        Ok(())
    }

    #[test]
    fn test_config_missing_scope() {
        let args = vec!["rona", "config"];
        assert!(Cli::try_parse_from(args).is_err());
    }

    #[test]
    fn test_config_invalid_scope() {
        let args = vec!["rona", "config", "invalid"];
        assert!(Cli::try_parse_from(args).is_err());
    }

    // === TEMPLATE SELECTION TESTS (REGRESSION TESTS) ===
    // These tests would have caught the bug where `rona -g -i -n` produced empty brackets []

    /// REGRESSION TEST: Verify template selection logic for interactive mode with `no_commit_number`
    /// This test verifies that the new conditional block syntax properly handles None `commit_number`
    #[test]
    fn test_template_selection_with_no_commit_number() -> TestResult {
        use std::collections::HashMap;

        use crate::template::{TemplateVariables, process_template};

        let default_template = "{?commit_number}[{commit_number}] {/commit_number}({commit_type} on {branch_name}) {message}";

        let variables = TemplateVariables {
            commit_number: None,
            commit_type: "docs".to_string(),
            branch_name: "main".to_string(),
            message: "Update docs".to_string(),
            date: "2024-01-15".to_string(),
            time: "14:30:00".to_string(),
            author: "Test User".to_string(),
            email: "test@example.com".to_string(),
        };

        let result = process_template(default_template, &variables, &HashMap::new())?;

        assert!(
            !result.contains("[]"),
            "Output should not contain empty brackets: {result}"
        );
        assert_eq!(result, "(docs on main) Update docs");
        Ok(())
    }

    /// REGRESSION TEST: Verify template selection logic for interactive mode WITH `commit_number`
    #[test]
    fn test_template_selection_with_commit_number() -> TestResult {
        use std::collections::HashMap;

        use crate::template::{TemplateVariables, process_template};

        let default_template = "{?commit_number}[{commit_number}] {/commit_number}({commit_type} on {branch_name}) {message}";

        let variables = TemplateVariables {
            commit_number: Some(42),
            commit_type: "feat".to_string(),
            branch_name: "new-feature".to_string(),
            message: "Add feature".to_string(),
            date: "2024-01-15".to_string(),
            time: "14:30:00".to_string(),
            author: "Test User".to_string(),
            email: "test@example.com".to_string(),
        };

        let result = process_template(default_template, &variables, &HashMap::new())?;

        assert!(
            result.starts_with("[42]"),
            "Output should start with [42]: {result}"
        );
        assert_eq!(result, "[42] (feat on new-feature) Add feature");
        Ok(())
    }

    /// REGRESSION TEST: Verify that using wrong template produces the bug
    /// This documents the original bug and ensures our fix prevents it
    #[test]
    fn test_bug_using_wrong_template_with_no_commit_number() -> TestResult {
        use std::collections::HashMap;

        use crate::template::{TemplateVariables, process_template};

        let wrong_template = "[{commit_number}] ({commit_type} on {branch_name}) {message}";

        let variables = TemplateVariables {
            commit_number: None,
            commit_type: "docs".to_string(),
            branch_name: "main".to_string(),
            message: "Update docs".to_string(),
            date: "2024-01-15".to_string(),
            time: "14:30:00".to_string(),
            author: "Test User".to_string(),
            email: "test@example.com".to_string(),
        };

        let result = process_template(wrong_template, &variables, &HashMap::new())?;

        assert_eq!(result, "[] (docs on main) Update docs");
        assert!(result.contains("[]"), "This demonstrates the bug we fixed");
        Ok(())
    }

    /// REGRESSION TEST: Test fallback format in `handle_interactive_mode`
    /// Verify the fallback format also respects `no_commit_number` flag
    #[test]
    fn test_fallback_format_with_no_commit_number() {
        let no_commit_number = true;
        let commit_type = "fix";
        let branch_name = "bugfix";
        let message = "Fix issue";

        let formatted_message = if no_commit_number {
            format!("({commit_type} on {branch_name}) {message}")
        } else {
            format!("[42] ({commit_type} on {branch_name}) {message}")
        };

        assert_eq!(formatted_message, "(fix on bugfix) Fix issue");
        assert!(
            !formatted_message.contains("[]"),
            "Fallback should not produce empty brackets"
        );
    }

    /// REGRESSION TEST: Test fallback format with commit number
    #[test]
    fn test_fallback_format_with_commit_number() {
        let no_commit_number = false;
        let commit_number = 15u32;
        let commit_type = "feat";
        let branch_name = "feature";
        let message = "Add feature";

        let formatted_message = if no_commit_number {
            format!("({commit_type} on {branch_name}) {message}")
        } else {
            format!("[{commit_number}] ({commit_type} on {branch_name}) {message}")
        };

        assert_eq!(formatted_message, "[15] (feat on feature) Add feature");
        assert!(
            !formatted_message.contains("[]"),
            "Should not produce empty brackets"
        );
    }

    // === SYNC COMMAND TESTS ===

    #[test]
    fn test_sync_basic() -> TestResult {
        let args = vec!["rona", "sync"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Sync {
            source_branch,
            rebase,
            new_branch,
            dry_run,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert_eq!(source_branch, "main");
        assert!(!rebase);
        assert!(new_branch.is_none());
        assert!(!dry_run);
        Ok(())
    }

    #[test]
    fn test_sync_with_branch() -> TestResult {
        let args = vec!["rona", "sync", "--branch", "develop"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Sync {
            source_branch,
            rebase,
            new_branch,
            dry_run,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert_eq!(source_branch, "develop");
        assert!(!rebase);
        assert!(new_branch.is_none());
        assert!(!dry_run);
        Ok(())
    }

    #[test]
    fn test_sync_with_branch_short_flag() -> TestResult {
        let args = vec!["rona", "sync", "-b", "staging"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Sync {
            source_branch,
            rebase,
            new_branch,
            dry_run,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert_eq!(source_branch, "staging");
        assert!(!rebase);
        assert!(new_branch.is_none());
        assert!(!dry_run);
        Ok(())
    }

    #[test]
    fn test_sync_with_rebase() -> TestResult {
        let args = vec!["rona", "sync", "--rebase"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Sync {
            source_branch,
            rebase,
            new_branch,
            dry_run,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert_eq!(source_branch, "main");
        assert!(rebase);
        assert!(new_branch.is_none());
        assert!(!dry_run);
        Ok(())
    }

    #[test]
    fn test_sync_with_rebase_short_flag() -> TestResult {
        let args = vec!["rona", "sync", "-r"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Sync {
            source_branch,
            rebase,
            new_branch,
            dry_run,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert_eq!(source_branch, "main");
        assert!(rebase);
        assert!(new_branch.is_none());
        assert!(!dry_run);
        Ok(())
    }

    #[test]
    fn test_sync_with_new_branch() -> TestResult {
        let args = vec!["rona", "sync", "--new-branch", "feature/new-feature"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Sync {
            source_branch,
            rebase,
            new_branch,
            dry_run,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert_eq!(source_branch, "main");
        assert!(!rebase);
        assert_eq!(new_branch, Some("feature/new-feature".to_string()));
        assert!(!dry_run);
        Ok(())
    }

    #[test]
    fn test_sync_with_new_branch_short_flag() -> TestResult {
        let args = vec!["rona", "sync", "-n", "bugfix/issue-123"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Sync {
            source_branch,
            rebase,
            new_branch,
            dry_run,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert_eq!(source_branch, "main");
        assert!(!rebase);
        assert_eq!(new_branch, Some("bugfix/issue-123".to_string()));
        assert!(!dry_run);
        Ok(())
    }

    #[test]
    fn test_sync_with_dry_run() -> TestResult {
        let args = vec!["rona", "sync", "--dry-run"];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Sync {
            source_branch,
            rebase,
            new_branch,
            dry_run,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert_eq!(source_branch, "main");
        assert!(!rebase);
        assert!(new_branch.is_none());
        assert!(dry_run);
        Ok(())
    }

    #[test]
    fn test_sync_all_options() -> TestResult {
        let args = vec![
            "rona",
            "sync",
            "--branch",
            "develop",
            "--rebase",
            "--new-branch",
            "feature/test",
            "--dry-run",
        ];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Sync {
            source_branch,
            rebase,
            new_branch,
            dry_run,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert_eq!(source_branch, "develop");
        assert!(rebase);
        assert_eq!(new_branch, Some("feature/test".to_string()));
        assert!(dry_run);
        Ok(())
    }

    #[test]
    fn test_sync_short_flags_combination() -> TestResult {
        let args = vec![
            "rona",
            "sync",
            "-b",
            "staging",
            "-r",
            "-n",
            "hotfix/critical",
        ];
        let cli = Cli::try_parse_from(args)?;

        let CliCommand::Sync {
            source_branch,
            rebase,
            new_branch,
            dry_run,
        } = cli.command
        else {
            return Err("Wrong command parsed".into());
        };
        assert_eq!(source_branch, "staging");
        assert!(rebase);
        assert_eq!(new_branch, Some("hotfix/critical".to_string()));
        assert!(!dry_run);
        Ok(())
    }
}
