//! Directory listing builtins - ls, find, rmdir

// Uses unwrap() for validated single-char strings (e.g., "f".chars().next())
#![allow(clippy::unwrap_used)]

use async_trait::async_trait;
use std::path::Path;

use super::{Builtin, Context, ExecutionPlan, SubCommand, resolve_path};
use crate::error::Result;
use crate::fs::FileType;
use crate::interpreter::{ControlFlow, ExecResult};

/// Options for ls command
struct LsOptions {
    long: bool,
    all: bool,
    human: bool,
    one_per_line: bool,
    recursive: bool,
    sort_by_time: bool,
    classify: bool,
    columns: bool,
}

/// The ls builtin - list directory contents.
///
/// Usage: ls [-l] [-a] [-h] [-1] [-R] [-t] [-F] [-C] [PATH...]
///
/// Options:
///   -l   Use long listing format
///   -a   Show hidden files (starting with .)
///   -h   Human-readable sizes (with -l)
///   -1   One entry per line
///   -R   List subdirectories recursively
///   -t   Sort by modification time, newest first
///   -F   Append indicator (/ for dirs, * for executables, @ for symlinks, | for FIFOs)
///   -C   List entries in columns (multi-column output)
pub struct Ls;

#[async_trait]
impl Builtin for Ls {
    async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
        if let Some(r) = super::check_help_version(
            ctx.args,
            "Usage: ls [OPTION]... [FILE]...\nList directory contents.\n\n  -l\t\tuse a long listing format\n  -a\t\tdo not ignore entries starting with .\n  -h\t\thuman-readable sizes (with -l)\n  -1\t\tlist one file per line\n  -R\t\tlist subdirectories recursively\n  -t\t\tsort by modification time, newest first\n  -F, --classify\tappend indicator (/ for dirs, * for executables, @ for symlinks, | for FIFOs)\n  -C\t\tlist entries in columns\n      --help\tdisplay this help and exit\n      --version\toutput version information and exit\n",
            Some("ls (bashkit) 0.1"),
        ) {
            return Ok(r);
        }

        let mut opts = LsOptions {
            long: false,
            all: false,
            human: false,
            one_per_line: false,
            recursive: false,
            sort_by_time: false,
            classify: false,
            columns: false,
        };

        // Parse flags
        let mut paths: Vec<&str> = Vec::new();
        for arg in ctx.args {
            if arg == "--classify" {
                opts.classify = true;
            } else if arg.starts_with('-') && arg.len() > 1 && !arg.starts_with("--") {
                for c in arg[1..].chars() {
                    match c {
                        'l' => opts.long = true,
                        'a' => opts.all = true,
                        'h' => opts.human = true,
                        '1' => opts.one_per_line = true,
                        'R' => opts.recursive = true,
                        't' => opts.sort_by_time = true,
                        'F' => opts.classify = true,
                        'C' => opts.columns = true,
                        _ => {
                            return Ok(ExecResult::err(
                                format!("ls: invalid option -- '{}'\n", c),
                                2,
                            ));
                        }
                    }
                }
            } else {
                paths.push(arg);
            }
        }

        // Default to current directory
        if paths.is_empty() {
            paths.push(".");
        }

        let mut output = String::new();
        let multiple_paths = paths.len() > 1 || opts.recursive;

        // Separate file and directory arguments (like real ls)
        let mut file_args: Vec<(&str, crate::fs::Metadata)> = Vec::new();
        let mut dir_args: Vec<(usize, &str, std::path::PathBuf)> = Vec::new();

        for (i, path_str) in paths.iter().enumerate() {
            let path = resolve_path(ctx.cwd, path_str);

            // Check if path exists
            if !ctx.fs.exists(&path).await.unwrap_or(false) {
                return Ok(ExecResult::err(
                    format!(
                        "ls: cannot access '{}': No such file or directory\n",
                        path_str
                    ),
                    2,
                ));
            }

            let metadata = ctx.fs.stat(&path).await?;

            if metadata.file_type.is_file() {
                file_args.push((path_str, metadata));
            } else {
                dir_args.push((i, path_str, path));
            }
        }

        // Sort file arguments by time if -t, preserving original paths
        if opts.sort_by_time {
            file_args.sort_by_key(|entry| std::cmp::Reverse(entry.1.modified));
        }

        // Output file arguments first (preserving path as given by user)
        if opts.long {
            for (path_str, metadata) in &file_args {
                let mut entry = format_long_entry(path_str, metadata, opts.human);
                if opts.classify {
                    // Insert suffix before the trailing newline
                    let suffix = classify_suffix(metadata);
                    if !suffix.is_empty() {
                        entry.insert_str(entry.len() - 1, suffix);
                    }
                }
                output.push_str(&entry);
            }
        } else if !file_args.is_empty() {
            let names: Vec<String> = file_args
                .iter()
                .map(|(path_str, metadata)| {
                    let mut name = (*path_str).to_string();
                    if opts.classify {
                        name.push_str(classify_suffix(metadata));
                    }
                    name
                })
                .collect();
            if opts.columns && !opts.one_per_line {
                output.push_str(&format_columns(&names, 80));
            } else {
                for name in &names {
                    output.push_str(name);
                    output.push('\n');
                }
            }
        }

        // Then output directory listings
        for (i, path_str, path) in &dir_args {
            if let Err(e) = list_directory(
                &ctx,
                path,
                path_str,
                &mut output,
                &opts,
                multiple_paths,
                *i > 0 || !file_args.is_empty(),
            )
            .await
            {
                return Ok(ExecResult::err(format!("ls: {}\n", e), 2));
            }
        }

        Ok(ExecResult::ok(output))
    }
}

async fn list_directory(
    ctx: &Context<'_>,
    path: &Path,
    display_path: &str,
    output: &mut String,
    opts: &LsOptions,
    show_header: bool,
    add_newline: bool,
) -> std::result::Result<(), String> {
    if add_newline {
        output.push('\n');
    }

    if show_header {
        output.push_str(&format!("{}:\n", display_path));
    }

    let entries = ctx
        .fs
        .read_dir(path)
        .await
        .map_err(|e| format!("cannot open directory '{}': {}", display_path, e))?;

    // Sort entries
    let mut sorted_entries = entries;
    if opts.sort_by_time {
        // Sort by modification time, newest first
        sorted_entries.sort_by_key(|entry| std::cmp::Reverse(entry.metadata.modified));
    } else {
        // Sort alphabetically
        sorted_entries.sort_by(|a, b| a.name.cmp(&b.name));
    }

    // Filter hidden files unless -a
    let filtered: Vec<_> = sorted_entries
        .iter()
        .filter(|e| opts.all || !e.name.starts_with('.'))
        .collect();

    // Collect subdirectories for recursive listing
    let mut subdirs: Vec<(std::path::PathBuf, String)> = Vec::new();

    if opts.long {
        for entry in &filtered {
            let mut line = format_long_entry(&entry.name, &entry.metadata, opts.human);
            if opts.classify {
                let suffix = classify_suffix(&entry.metadata);
                if !suffix.is_empty() {
                    line.insert_str(line.len() - 1, suffix);
                }
            }
            output.push_str(&line);
            if opts.recursive && entry.metadata.file_type.is_dir() {
                subdirs.push((
                    path.join(&entry.name),
                    format!("{}/{}", display_path, entry.name),
                ));
            }
        }
    } else {
        // Collect entry names for potential column formatting
        let mut names: Vec<String> = Vec::new();
        for entry in &filtered {
            let mut name = entry.name.clone();
            if opts.classify {
                name.push_str(classify_suffix(&entry.metadata));
            }
            names.push(name);
            if opts.recursive && entry.metadata.file_type.is_dir() {
                subdirs.push((
                    path.join(&entry.name),
                    format!("{}/{}", display_path, entry.name),
                ));
            }
        }

        // Precedence: -l > -1 > -C > default (one-per-line)
        if opts.columns && !opts.one_per_line {
            output.push_str(&format_columns(&names, 80));
        } else {
            for name in &names {
                output.push_str(name);
                output.push('\n');
            }
        }
    }

    // Recursive listing
    if opts.recursive {
        for (subpath, display) in subdirs {
            // Box the future to avoid infinite recursion type size
            Box::pin(list_directory(
                ctx, &subpath, &display, output, opts, true, true,
            ))
            .await?;
        }
    }

    Ok(())
}

/// Return the classify indicator character for a file type.
/// `/` for directories, `*` for executables, `@` for symlinks, `|` for FIFOs.
fn classify_suffix(metadata: &crate::fs::Metadata) -> &'static str {
    match metadata.file_type {
        FileType::Directory => "/",
        FileType::Symlink => "@",
        FileType::Fifo => "|",
        FileType::File => {
            // Executable if any execute bit is set
            if metadata.mode & 0o111 != 0 { "*" } else { "" }
        }
    }
}

/// Format entries in column-major order, like `ls -C`.
/// Uses a fixed terminal width (80) since VFS has no real terminal.
/// Per-column widths match GNU coreutils behavior.
fn format_columns(entries: &[String], terminal_width: usize) -> String {
    if entries.is_empty() {
        return String::new();
    }

    // Try fitting as many columns as possible, starting from the maximum
    let max_width = entries.iter().map(|e| e.len()).max().unwrap_or(0);
    let max_possible_cols = (terminal_width / (max_width.min(1) + 2)).max(1);

    let mut num_cols = 1;
    let mut col_widths: Vec<usize> = vec![0];
    let mut num_rows = entries.len();

    // Try increasing column counts to find the best fit
    for try_cols in 2..=max_possible_cols.min(entries.len()) {
        let try_rows = entries.len().div_ceil(try_cols);
        // Calculate per-column widths (max entry width in each column)
        let mut widths = vec![0usize; try_cols];
        for (i, entry) in entries.iter().enumerate() {
            let col = i / try_rows;
            if col < try_cols {
                widths[col] = widths[col].max(entry.len());
            }
        }
        // Total width: each column except last gets 2-space padding
        let total: usize = widths.iter().sum::<usize>() + (try_cols - 1) * 2;
        if total <= terminal_width {
            num_cols = try_cols;
            col_widths = widths;
            num_rows = try_rows;
        }
    }

    let mut output = String::new();
    for row in 0..num_rows {
        for (col, col_w) in col_widths.iter().enumerate() {
            // Column-major order: fill columns top-to-bottom, left-to-right
            let idx = col * num_rows + row;
            if idx < entries.len() {
                let is_last = col == num_cols - 1 || idx + num_rows >= entries.len();
                if is_last {
                    output.push_str(&entries[idx]);
                } else {
                    let width = col_w + 2; // entry width + 2 spaces
                    output.push_str(&format!("{:<width$}", entries[idx], width = width));
                }
            }
        }
        output.push('\n');
    }
    output
}

fn format_long_entry(name: &str, metadata: &crate::fs::Metadata, human: bool) -> String {
    let file_type = match metadata.file_type {
        FileType::Directory => 'd',
        FileType::Symlink => 'l',
        FileType::Fifo => 'p',
        FileType::File => '-',
    };

    let mode = metadata.mode;
    let perms = format!(
        "{}{}{}{}{}{}{}{}{}",
        if mode & 0o400 != 0 { 'r' } else { '-' },
        if mode & 0o200 != 0 { 'w' } else { '-' },
        if mode & 0o100 != 0 { 'x' } else { '-' },
        if mode & 0o040 != 0 { 'r' } else { '-' },
        if mode & 0o020 != 0 { 'w' } else { '-' },
        if mode & 0o010 != 0 { 'x' } else { '-' },
        if mode & 0o004 != 0 { 'r' } else { '-' },
        if mode & 0o002 != 0 { 'w' } else { '-' },
        if mode & 0o001 != 0 { 'x' } else { '-' },
    );

    let size = if human {
        human_readable_size(metadata.size)
    } else {
        format!("{:>8}", metadata.size)
    };

    // Format modified time
    let modified = metadata
        .modified
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| {
            let secs = d.as_secs();
            // Simple date formatting: YYYY-MM-DD HH:MM
            let days = secs / 86400;
            let hours = (secs % 86400) / 3600;
            let mins = (secs % 3600) / 60;
            // Approximate date calculation
            let years = 1970 + (days / 365);
            let remaining_days = days % 365;
            let month = remaining_days / 30 + 1;
            let day = remaining_days % 30 + 1;
            format!(
                "{:04}-{:02}-{:02} {:02}:{:02}",
                years, month, day, hours, mins
            )
        })
        .unwrap_or_else(|_| "????-??-?? ??:??".to_string());

    format!("{}{} {} {} {}\n", file_type, perms, size, modified, name)
}

fn human_readable_size(size: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = 1024 * KB;
    const GB: u64 = 1024 * MB;

    if size >= GB {
        format!("{:>5.1}G", size as f64 / GB as f64)
    } else if size >= MB {
        format!("{:>5.1}M", size as f64 / MB as f64)
    } else if size >= KB {
        format!("{:>5.1}K", size as f64 / KB as f64)
    } else {
        format!("{:>6}", size)
    }
}

/// Options for find command
struct FindOptions {
    name_pattern: Option<String>,
    /// -path pattern: match against the full display path
    path_pattern: Option<String>,
    type_filter: Option<char>,
    max_depth: Option<usize>,
    min_depth: Option<usize>,
    printf_format: Option<String>,
    /// -exec/-execdir command template (args before \; or +)
    exec_args: Vec<String>,
    /// true if -exec uses + (batch mode), false for \; (per-file mode)
    exec_batch: bool,
    /// Negate the -name predicate
    negate_name: bool,
    /// Negate the -path predicate
    negate_path: bool,
}

/// The find builtin - search for files.
///
/// Usage: find [PATH...] [-name PATTERN] [-type TYPE] [-maxdepth N] [-mindepth N] [-printf FMT] [-exec CMD {} \;]
///
/// Options:
///   -name PATTERN      Match filename against PATTERN (supports * and ?)
///   -type TYPE         Match file type: f (file), d (directory), l (link)
///   -maxdepth N        Descend at most N levels
///   -mindepth N        Do not apply tests at levels less than N
///   -print             Print matching paths (default)
///   -printf FMT        Print using format string (%f %p %P %s %m %M %y %d %T@)
///   -exec CMD {} \;    Execute CMD for each match ({} = path)
///   -exec CMD {} +     Execute CMD once with all matches
pub struct Find;

/// Parse find arguments into search paths and options.
/// Returns (paths, opts) or an error ExecResult.
#[allow(clippy::result_large_err)]
fn parse_find_args(args: &[String]) -> std::result::Result<(Vec<String>, FindOptions), ExecResult> {
    let mut paths: Vec<String> = Vec::new();
    let mut opts = FindOptions {
        name_pattern: None,
        path_pattern: None,
        type_filter: None,
        max_depth: None,
        min_depth: None,
        printf_format: None,
        exec_args: Vec::new(),
        exec_batch: false,
        negate_name: false,
        negate_path: false,
    };
    let mut negate_next = false;

    let mut i = 0;
    while i < args.len() {
        let arg = &args[i];
        match arg.as_str() {
            "-name" => {
                i += 1;
                if i >= args.len() {
                    return Err(ExecResult::err(
                        "find: missing argument to '-name'\n".to_string(),
                        1,
                    ));
                }
                opts.name_pattern = Some(args[i].clone());
                if negate_next {
                    opts.negate_name = true;
                    negate_next = false;
                }
            }
            "-path" => {
                i += 1;
                if i >= args.len() {
                    return Err(ExecResult::err(
                        "find: missing argument to '-path'\n".to_string(),
                        1,
                    ));
                }
                opts.path_pattern = Some(args[i].clone());
                if negate_next {
                    opts.negate_path = true;
                    negate_next = false;
                }
            }
            "-type" => {
                i += 1;
                if i >= args.len() {
                    return Err(ExecResult::err(
                        "find: missing argument to '-type'\n".to_string(),
                        1,
                    ));
                }
                let t = &args[i];
                match t.as_str() {
                    "f" | "d" | "l" => opts.type_filter = Some(t.chars().next().unwrap()),
                    _ => {
                        return Err(ExecResult::err(format!("find: unknown type '{}'\n", t), 1));
                    }
                }
            }
            "-maxdepth" => {
                i += 1;
                if i >= args.len() {
                    return Err(ExecResult::err(
                        "find: missing argument to '-maxdepth'\n".to_string(),
                        1,
                    ));
                }
                match args[i].parse::<usize>() {
                    Ok(n) => opts.max_depth = Some(n),
                    Err(_) => {
                        return Err(ExecResult::err(
                            format!("find: invalid maxdepth value '{}'\n", args[i]),
                            1,
                        ));
                    }
                }
            }
            "-mindepth" => {
                i += 1;
                if i >= args.len() {
                    return Err(ExecResult::err(
                        "find: missing argument to '-mindepth'\n".to_string(),
                        1,
                    ));
                }
                match args[i].parse::<usize>() {
                    Ok(n) => opts.min_depth = Some(n),
                    Err(_) => {
                        return Err(ExecResult::err(
                            format!("find: invalid mindepth value '{}'\n", args[i]),
                            1,
                        ));
                    }
                }
            }
            "-print" | "-print0" => {
                // Default action, ignore
            }
            "-printf" => {
                i += 1;
                if i >= args.len() {
                    return Err(ExecResult::err(
                        "find: missing argument to '-printf'\n".to_string(),
                        1,
                    ));
                }
                opts.printf_format = Some(args[i].clone());
            }
            "-exec" | "-execdir" => {
                i += 1;
                while i < args.len() {
                    let a = &args[i];
                    if a == ";" || a == "\\;" {
                        break;
                    }
                    if a == "+" {
                        opts.exec_batch = true;
                        break;
                    }
                    opts.exec_args.push(a.clone());
                    i += 1;
                }
            }
            "-not" | "!" => {
                negate_next = true;
            }
            s if s.starts_with('-') => {
                return Err(ExecResult::err(
                    format!("find: unknown predicate '{}'\n", s),
                    1,
                ));
            }
            _ => {
                paths.push(arg.clone());
            }
        }
        i += 1;
    }

    if paths.is_empty() {
        paths.push(".".to_string());
    }

    Ok((paths, opts))
}

struct FindPlanData {
    matched_paths: Vec<String>,
    errors: String,
    had_error: bool,
}

/// Collect matched paths and path/traversal errors for find execution planning.
async fn collect_find_plan_data(
    ctx: &Context<'_>,
    search_paths: &[String],
    opts: &FindOptions,
) -> Result<FindPlanData> {
    let mut matched: Vec<String> = Vec::new();
    let mut errors = String::new();
    let mut had_error = false;
    // Reuse find_recursive but with a temporary output buffer
    let temp_opts = FindOptions {
        name_pattern: opts.name_pattern.clone(),
        path_pattern: opts.path_pattern.clone(),
        type_filter: opts.type_filter,
        max_depth: opts.max_depth,
        min_depth: opts.min_depth,
        printf_format: None, // Don't format, just collect paths
        exec_args: Vec::new(),
        exec_batch: false,
        negate_name: opts.negate_name,
        negate_path: opts.negate_path,
    };
    let mut output = String::new();
    for path_str in search_paths {
        let path = resolve_path(ctx.cwd, path_str);
        if !ctx.fs.exists(&path).await.unwrap_or(false) {
            errors.push_str(&format!(
                "find: '{}': No such file or directory\n",
                path_str
            ));
            had_error = true;
            continue;
        }
        if let Err(e) = find_recursive(ctx, &path, path_str, &temp_opts, 0, &mut output).await {
            errors.push_str(&format!("find: '{}': {}\n", path_str, e));
            had_error = true;
        }
    }
    // Parse the output back into paths (each line is a path)
    for line in output.lines() {
        if !line.is_empty() {
            matched.push(line.to_string());
        }
    }
    Ok(FindPlanData {
        matched_paths: matched,
        errors,
        had_error,
    })
}

/// Build exec sub-commands from matched paths and exec_args template.
fn build_find_exec_commands(
    exec_args: &[String],
    matched_paths: &[String],
    batch: bool,
) -> Vec<SubCommand> {
    if exec_args.is_empty() || matched_paths.is_empty() {
        return Vec::new();
    }

    if batch {
        // Batch mode: -exec cmd {} +
        // Replace {} with all paths at once
        let cmd_args: Vec<String> = exec_args
            .iter()
            .flat_map(|arg| {
                if arg == "{}" {
                    matched_paths.to_vec()
                } else {
                    vec![arg.clone()]
                }
            })
            .collect();

        if cmd_args.is_empty() {
            return Vec::new();
        }

        vec![SubCommand {
            name: cmd_args[0].clone(),
            args: cmd_args[1..].to_vec(),
            stdin: None,
        }]
    } else {
        // Per-file mode: -exec cmd {} \;
        matched_paths
            .iter()
            .map(|found_path| {
                let cmd_args: Vec<String> = exec_args
                    .iter()
                    .map(|arg| arg.replace("{}", found_path))
                    .collect();

                SubCommand {
                    name: cmd_args[0].clone(),
                    args: cmd_args[1..].to_vec(),
                    stdin: None,
                }
            })
            .collect()
    }
}

#[async_trait]
impl Builtin for Find {
    async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
        if let Some(r) = super::check_help_version(
            ctx.args,
            "Usage: find [PATH...] [EXPRESSION]\nSearch for files in a directory hierarchy.\n\n  -name PATTERN\tmatch filename against PATTERN (supports * and ?)\n  -path PATTERN\tmatch full path against PATTERN\n  -type TYPE\tmatch file type: f (file), d (directory), l (link)\n  -maxdepth N\tdescend at most N levels\n  -mindepth N\tdo not apply tests at levels less than N\n  -print\t\tprint matching paths (default)\n  -printf FMT\tprint using format string (%f %p %P %s %m %M %y %d %T@)\n  -exec CMD {} \\;\texecute CMD for each match ({} = path)\n  -exec CMD {} +\texecute CMD once with all matches\n  -not, !\t\tnegate the next predicate\n      --help\tdisplay this help and exit\n      --version\toutput version information and exit\n",
            Some("find (bashkit) 0.1"),
        ) {
            return Ok(r);
        }

        let (search_paths, opts) = match parse_find_args(ctx.args) {
            Ok(v) => v,
            Err(e) => return Ok(e),
        };

        let mut output = String::new();
        let mut errors = String::new();
        let mut had_error = false;

        for path_str in &search_paths {
            let path = resolve_path(ctx.cwd, path_str);
            if !ctx.fs.exists(&path).await.unwrap_or(false) {
                errors.push_str(&format!(
                    "find: '{}': No such file or directory\n",
                    path_str
                ));
                had_error = true;
                continue;
            }

            if let Err(e) = find_recursive(&ctx, &path, path_str, &opts, 0, &mut output).await {
                errors.push_str(&format!("find: '{}': {}\n", path_str, e));
                had_error = true;
            }
        }

        Ok(ExecResult {
            stdout: output,
            stderr: errors,
            exit_code: if had_error { 1 } else { 0 },
            control_flow: ControlFlow::None,
            ..Default::default()
        })
    }

    async fn execution_plan(&self, ctx: &Context<'_>) -> Result<Option<ExecutionPlan>> {
        let (search_paths, opts) = match parse_find_args(ctx.args) {
            Ok(v) => v,
            Err(_) => return Ok(None), // Let execute() handle errors
        };

        // Only return a plan when -exec is present
        if opts.exec_args.is_empty() {
            return Ok(None);
        }

        // Collect matched paths plus any path/traversal errors.
        let plan_data = collect_find_plan_data(ctx, &search_paths, &opts).await?;
        if plan_data.matched_paths.is_empty() && !plan_data.had_error {
            return Ok(None);
        }

        let commands =
            build_find_exec_commands(&opts.exec_args, &plan_data.matched_paths, opts.exec_batch);
        if commands.is_empty() && !plan_data.had_error {
            return Ok(None);
        }

        if plan_data.had_error {
            return Ok(Some(ExecutionPlan::BatchWithStatus {
                commands,
                stderr_prefix: plan_data.errors,
                force_error_exit: true,
            }));
        }

        Ok(Some(ExecutionPlan::Batch { commands }))
    }
}

fn find_recursive<'a>(
    ctx: &'a Context<'_>,
    path: &'a Path,
    display_path: &'a str,
    opts: &'a FindOptions,
    current_depth: usize,
    output: &'a mut String,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
    Box::pin(async move {
        // Check if this entry matches
        let metadata = ctx.fs.stat(path).await?;
        let entry_name = Path::new(display_path)
            .file_name()
            .map(|s| s.to_string_lossy().to_string())
            .unwrap_or_else(|| display_path.to_string());

        // Check type filter
        let type_matches = match opts.type_filter {
            Some('f') => metadata.file_type.is_file(),
            Some('d') => metadata.file_type.is_dir(),
            Some('l') => metadata.file_type.is_symlink(),
            _ => true,
        };

        // Check name pattern
        let name_matches = match &opts.name_pattern {
            Some(pattern) => {
                let m = glob_match(&entry_name, pattern);
                if opts.negate_name { !m } else { m }
            }
            None => true,
        };

        // Check path pattern
        let path_matches = match &opts.path_pattern {
            Some(pattern) => {
                let m = glob_match(display_path, pattern);
                if opts.negate_path { !m } else { m }
            }
            None => true,
        };

        // Check min depth before outputting
        let above_min_depth = match opts.min_depth {
            Some(min) => current_depth >= min,
            None => true,
        };

        // Output if matches (or if no filters, show everything)
        if type_matches && name_matches && path_matches && above_min_depth {
            if let Some(ref fmt) = opts.printf_format {
                output.push_str(&find_printf_format(fmt, display_path, &metadata));
            } else {
                output.push_str(display_path);
                output.push('\n');
            }
        }

        // Recurse into directories
        if metadata.file_type.is_dir() {
            // Check max depth
            if let Some(max) = opts.max_depth
                && current_depth >= max
            {
                return Ok(());
            }

            let entries = ctx.fs.read_dir(path).await?;
            let mut sorted_entries = entries;
            sorted_entries.sort_by(|a, b| a.name.cmp(&b.name));

            for entry in sorted_entries {
                let child_path = path.join(&entry.name);
                let child_display = if display_path == "." {
                    format!("./{}", entry.name)
                } else {
                    format!("{}/{}", display_path, entry.name)
                };

                find_recursive(
                    ctx,
                    &child_path,
                    &child_display,
                    opts,
                    current_depth + 1,
                    output,
                )
                .await?;
            }
        }

        Ok(())
    })
}

/// Format a path using find's -printf format string.
fn find_printf_format(fmt: &str, display_path: &str, metadata: &crate::fs::Metadata) -> String {
    let mut out = String::new();
    let chars: Vec<char> = fmt.chars().collect();
    let mut i = 0;
    while i < chars.len() {
        match chars[i] {
            '\\' => {
                i += 1;
                if i < chars.len() {
                    match chars[i] {
                        'n' => out.push('\n'),
                        't' => out.push('\t'),
                        '0' => out.push('\0'),
                        '\\' => out.push('\\'),
                        c => {
                            out.push('\\');
                            out.push(c);
                        }
                    }
                }
            }
            '%' => {
                i += 1;
                if i >= chars.len() {
                    out.push('%');
                    continue;
                }
                match chars[i] {
                    'f' => {
                        let name = std::path::Path::new(display_path)
                            .file_name()
                            .map(|s| s.to_string_lossy().to_string())
                            .unwrap_or_else(|| display_path.to_string());
                        out.push_str(&name);
                    }
                    'p' => out.push_str(display_path),
                    'P' => {
                        // In builtin context, display_path is already relative
                        let rel = display_path.strip_prefix("./").unwrap_or(display_path);
                        out.push_str(rel);
                    }
                    's' => out.push_str(&metadata.size.to_string()),
                    'm' => out.push_str(&format!("{:o}", metadata.mode & 0o7777)),
                    'M' => {
                        let type_ch = if metadata.file_type.is_dir() {
                            'd'
                        } else if metadata.file_type.is_symlink() {
                            'l'
                        } else {
                            '-'
                        };
                        out.push(type_ch);
                        for shift in [6, 3, 0] {
                            let bits = (metadata.mode >> shift) & 7;
                            out.push(if bits & 4 != 0 { 'r' } else { '-' });
                            out.push(if bits & 2 != 0 { 'w' } else { '-' });
                            out.push(if bits & 1 != 0 { 'x' } else { '-' });
                        }
                    }
                    'y' => {
                        let ch = if metadata.file_type.is_dir() {
                            'd'
                        } else if metadata.file_type.is_symlink() {
                            'l'
                        } else {
                            'f'
                        };
                        out.push(ch);
                    }
                    'd' => {
                        // Approximate depth from display_path
                        let base = display_path.strip_prefix("./").unwrap_or(display_path);
                        let depth = if base == "." || base.is_empty() {
                            0
                        } else {
                            base.matches('/').count() + 1
                        };
                        out.push_str(&depth.to_string());
                    }
                    'T' => {
                        i += 1;
                        if i < chars.len() && chars[i] == '@' {
                            let secs = metadata
                                .modified
                                .duration_since(std::time::UNIX_EPOCH)
                                .ok()
                                .map(|d| d.as_secs())
                                .unwrap_or(0);
                            out.push_str(&secs.to_string());
                        } else {
                            out.push_str("%T");
                            continue;
                        }
                    }
                    '%' => out.push('%'),
                    c => {
                        out.push('%');
                        out.push(c);
                    }
                }
            }
            c => out.push(c),
        }
        i += 1;
    }
    out
}

/// Simple glob pattern matching for find -name
pub(crate) fn glob_match(value: &str, pattern: &str) -> bool {
    let mut value_chars = value.chars().peekable();
    let mut pattern_chars = pattern.chars().peekable();

    loop {
        match (pattern_chars.peek(), value_chars.peek()) {
            (None, None) => return true,
            (None, Some(_)) => return false,
            (Some('*'), _) => {
                pattern_chars.next();
                if pattern_chars.peek().is_none() {
                    return true;
                }
                while value_chars.peek().is_some() {
                    let remaining_value: String = value_chars.clone().collect();
                    let remaining_pattern: String = pattern_chars.clone().collect();
                    if glob_match(&remaining_value, &remaining_pattern) {
                        return true;
                    }
                    value_chars.next();
                }
                let remaining_pattern: String = pattern_chars.collect();
                return glob_match("", &remaining_pattern);
            }
            (Some('?'), Some(_)) => {
                pattern_chars.next();
                value_chars.next();
            }
            (Some('?'), None) => return false,
            (Some(p), Some(v)) => {
                if *p == *v {
                    pattern_chars.next();
                    value_chars.next();
                } else {
                    return false;
                }
            }
            (Some(_), None) => return false,
        }
    }
}

/// The rmdir builtin - remove empty directories.
///
/// Usage: rmdir [-p] DIRECTORY...
///
/// Options:
///   -p   Remove parent directories as well if they become empty
pub struct Rmdir;

#[async_trait]
impl Builtin for Rmdir {
    async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
        if let Some(r) = super::check_help_version(
            ctx.args,
            "Usage: rmdir [OPTION]... DIRECTORY...\nRemove empty directories.\n\n  -p\t\tremove DIRECTORY and its ancestors\n      --help\tdisplay this help and exit\n      --version\toutput version information and exit\n",
            Some("rmdir (bashkit) 0.1"),
        ) {
            return Ok(r);
        }

        if ctx.args.is_empty() {
            return Ok(ExecResult::err("rmdir: missing operand\n".to_string(), 1));
        }

        let parents = ctx.args.iter().any(|a| a == "-p");
        let dirs: Vec<_> = ctx.args.iter().filter(|a| !a.starts_with('-')).collect();

        if dirs.is_empty() {
            return Ok(ExecResult::err("rmdir: missing operand\n".to_string(), 1));
        }

        for dir in dirs {
            let path = resolve_path(ctx.cwd, dir);

            // Check if exists
            if !ctx.fs.exists(&path).await.unwrap_or(false) {
                return Ok(ExecResult::err(
                    format!(
                        "rmdir: failed to remove '{}': No such file or directory\n",
                        dir
                    ),
                    1,
                ));
            }

            // Check if it's a directory
            let metadata = ctx.fs.stat(&path).await?;
            if !metadata.file_type.is_dir() {
                return Ok(ExecResult::err(
                    format!("rmdir: failed to remove '{}': Not a directory\n", dir),
                    1,
                ));
            }

            // Check if directory is empty
            let entries = ctx.fs.read_dir(&path).await?;
            if !entries.is_empty() {
                return Ok(ExecResult::err(
                    format!("rmdir: failed to remove '{}': Directory not empty\n", dir),
                    1,
                ));
            }

            // Remove the directory
            if let Err(e) = ctx.fs.remove(&path, false).await {
                return Ok(ExecResult::err(
                    format!("rmdir: failed to remove '{}': {}\n", dir, e),
                    1,
                ));
            }

            // If -p, try to remove parent directories
            if parents {
                let mut current = path.parent();
                while let Some(parent) = current {
                    // Don't remove root or cwd
                    if parent.as_os_str().is_empty() || parent == ctx.cwd.as_path() {
                        break;
                    }

                    // Check if parent is empty
                    if let Ok(entries) = ctx.fs.read_dir(parent).await {
                        if entries.is_empty() {
                            if ctx.fs.remove(parent, false).await.is_err() {
                                break;
                            }
                        } else {
                            break;
                        }
                    } else {
                        break;
                    }

                    current = parent.parent();
                }
            }
        }

        Ok(ExecResult::ok(String::new()))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashMap;
    use std::path::PathBuf;
    use std::sync::Arc;

    use crate::fs::{FileSystem, InMemoryFs};

    async fn create_test_ctx() -> (Arc<InMemoryFs>, PathBuf, HashMap<String, String>) {
        let fs = Arc::new(InMemoryFs::new());
        let cwd = PathBuf::from("/home/user");
        let variables = HashMap::new();

        fs.mkdir(&cwd, true).await.unwrap();

        (fs, cwd, variables)
    }

    // ==================== ls tests ====================

    #[tokio::test]
    async fn test_ls_empty_dir() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        let args: Vec<String> = vec![];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);
        assert_eq!(result.stdout, "");
    }

    #[tokio::test]
    async fn test_ls_with_files() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        // Create some files
        fs.write_file(&cwd.join("file1.txt"), b"content1")
            .await
            .unwrap();
        fs.write_file(&cwd.join("file2.txt"), b"content2")
            .await
            .unwrap();

        let args: Vec<String> = vec![];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);
        assert!(result.stdout.contains("file1.txt"));
        assert!(result.stdout.contains("file2.txt"));
    }

    #[tokio::test]
    async fn test_ls_hidden_files() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        fs.write_file(&cwd.join(".hidden"), b"hidden")
            .await
            .unwrap();
        fs.write_file(&cwd.join("visible"), b"visible")
            .await
            .unwrap();

        // Without -a
        let args: Vec<String> = vec![];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);
        assert!(!result.stdout.contains(".hidden"));
        assert!(result.stdout.contains("visible"));

        // With -a
        let args = vec!["-a".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);
        assert!(result.stdout.contains(".hidden"));
        assert!(result.stdout.contains("visible"));
    }

    #[tokio::test]
    async fn test_ls_long_format() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        fs.write_file(&cwd.join("test.txt"), b"content")
            .await
            .unwrap();

        let args = vec!["-l".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);
        // Long format should include permissions
        assert!(result.stdout.contains("rw"));
        assert!(result.stdout.contains("test.txt"));
    }

    #[tokio::test]
    async fn test_ls_nonexistent() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        let args = vec!["nonexistent".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 2);
        assert!(result.stderr.contains("No such file or directory"));
    }

    #[tokio::test]
    async fn test_ls_invalid_option() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        let args = vec!["-z".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 2);
        assert!(result.stderr.contains("invalid option"));
    }

    #[tokio::test]
    async fn test_ls_sort_by_time() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        // Create files with different modification times
        fs.write_file(&cwd.join("older.txt"), b"older")
            .await
            .unwrap();
        fs.write_file(&cwd.join("newer.txt"), b"newer")
            .await
            .unwrap();

        let args = vec!["-t".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);
        // -t should be accepted (not cause an error)
        assert!(result.stdout.contains("older.txt"));
        assert!(result.stdout.contains("newer.txt"));
    }

    #[tokio::test]
    async fn test_ls_file() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        fs.write_file(&cwd.join("test.txt"), b"content")
            .await
            .unwrap();

        let args = vec!["test.txt".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);
        assert!(result.stdout.contains("test.txt"));
    }

    #[tokio::test]
    async fn test_ls_recursive() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        fs.mkdir(&cwd.join("subdir"), false).await.unwrap();
        fs.write_file(&cwd.join("file.txt"), b"content")
            .await
            .unwrap();
        fs.write_file(&cwd.join("subdir/nested.txt"), b"nested")
            .await
            .unwrap();

        let args = vec!["-R".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);
        assert!(result.stdout.contains("file.txt"));
        assert!(result.stdout.contains("subdir"));
        assert!(result.stdout.contains("nested.txt"));
    }

    // ==================== find tests ====================

    #[tokio::test]
    async fn test_find_current_dir() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        fs.write_file(&cwd.join("file.txt"), b"content")
            .await
            .unwrap();

        let args: Vec<String> = vec![];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Find.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);
        assert!(result.stdout.contains("."));
        assert!(result.stdout.contains("file.txt"));
    }

    #[tokio::test]
    async fn test_find_name_pattern() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        fs.write_file(&cwd.join("file.txt"), b"content")
            .await
            .unwrap();
        fs.write_file(&cwd.join("other.md"), b"content")
            .await
            .unwrap();

        let args = vec!["-name".to_string(), "*.txt".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Find.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);
        assert!(result.stdout.contains("file.txt"));
        assert!(!result.stdout.contains("other.md"));
    }

    #[tokio::test]
    async fn test_find_type_file() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        fs.write_file(&cwd.join("file.txt"), b"content")
            .await
            .unwrap();
        fs.mkdir(&cwd.join("subdir"), false).await.unwrap();

        let args = vec!["-type".to_string(), "f".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Find.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);
        assert!(result.stdout.contains("file.txt"));
        assert!(!result.stdout.contains("subdir"));
    }

    #[tokio::test]
    async fn test_find_type_directory() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        fs.write_file(&cwd.join("file.txt"), b"content")
            .await
            .unwrap();
        fs.mkdir(&cwd.join("subdir"), false).await.unwrap();

        let args = vec!["-type".to_string(), "d".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Find.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);
        assert!(!result.stdout.contains("file.txt"));
        // Should contain the directory
        let lines: Vec<&str> = result.stdout.lines().collect();
        assert!(lines.iter().any(|l| l.contains("subdir") || *l == "."));
    }

    #[tokio::test]
    async fn test_find_maxdepth() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        fs.mkdir(&cwd.join("a"), false).await.unwrap();
        fs.mkdir(&cwd.join("a/b"), false).await.unwrap();
        fs.write_file(&cwd.join("a/b/deep.txt"), b"deep")
            .await
            .unwrap();

        let args = vec!["-maxdepth".to_string(), "1".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Find.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);
        assert!(result.stdout.contains("./a"));
        assert!(!result.stdout.contains("deep.txt"));
    }

    #[tokio::test]
    async fn test_find_nonexistent() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        let args = vec!["nonexistent".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Find.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 1);
        assert!(result.stderr.contains("No such file or directory"));
    }

    #[tokio::test]
    async fn test_find_missing_name_arg() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        let args = vec!["-name".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Find.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 1);
        assert!(result.stderr.contains("missing argument"));
    }

    #[tokio::test]
    async fn test_find_unknown_type() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        let args = vec!["-type".to_string(), "x".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Find.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 1);
        assert!(result.stderr.contains("unknown type"));
    }

    #[tokio::test]
    async fn test_find_deep_recursion() {
        // Test that find without maxdepth descends into all subdirectory levels
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        // Create a deep directory structure: a/b/c/d/deep.txt
        fs.mkdir(&cwd.join("a"), false).await.unwrap();
        fs.mkdir(&cwd.join("a/b"), false).await.unwrap();
        fs.mkdir(&cwd.join("a/b/c"), false).await.unwrap();
        fs.mkdir(&cwd.join("a/b/c/d"), false).await.unwrap();
        fs.write_file(&cwd.join("a/b/c/d/deep.txt"), b"deep content")
            .await
            .unwrap();

        // Also add files at each level
        fs.write_file(&cwd.join("a/file1.txt"), b"level1")
            .await
            .unwrap();
        fs.write_file(&cwd.join("a/b/file2.txt"), b"level2")
            .await
            .unwrap();
        fs.write_file(&cwd.join("a/b/c/file3.txt"), b"level3")
            .await
            .unwrap();

        let args: Vec<String> = vec![];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Find.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);

        // Should find the root
        assert!(result.stdout.contains("."), "Should contain current dir");

        // Should find all directories at all levels
        assert!(result.stdout.contains("./a"), "Should contain ./a");
        assert!(result.stdout.contains("./a/b"), "Should contain ./a/b");
        assert!(result.stdout.contains("./a/b/c"), "Should contain ./a/b/c");
        assert!(
            result.stdout.contains("./a/b/c/d"),
            "Should contain ./a/b/c/d"
        );

        // Should find all files at all levels
        assert!(
            result.stdout.contains("file1.txt"),
            "Should contain file1.txt at level 1"
        );
        assert!(
            result.stdout.contains("file2.txt"),
            "Should contain file2.txt at level 2"
        );
        assert!(
            result.stdout.contains("file3.txt"),
            "Should contain file3.txt at level 3"
        );
        assert!(
            result.stdout.contains("deep.txt"),
            "Should contain deep.txt at level 4"
        );
    }

    #[tokio::test]
    async fn test_ls_recursive_deep() {
        // Test that ls -R descends into all subdirectory levels
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        // Create a deep directory structure: a/b/c/deep.txt
        fs.mkdir(&cwd.join("a"), false).await.unwrap();
        fs.mkdir(&cwd.join("a/b"), false).await.unwrap();
        fs.mkdir(&cwd.join("a/b/c"), false).await.unwrap();
        fs.write_file(&cwd.join("a/b/c/deep.txt"), b"deep content")
            .await
            .unwrap();

        let args = vec!["-R".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);

        // Should list all directories and their contents
        assert!(result.stdout.contains("a"), "Should list dir a");
        assert!(result.stdout.contains("b"), "Should list dir b under a");
        assert!(result.stdout.contains("c"), "Should list dir c under a/b");
        assert!(
            result.stdout.contains("deep.txt"),
            "Should list deep.txt under a/b/c"
        );
    }

    #[tokio::test]
    async fn test_find_very_deep_nesting() {
        // Test 10 levels of nesting to ensure no recursion limits
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        // Create 10 levels deep
        let mut path = cwd.clone();
        for i in 0..10 {
            path = path.join(format!("level{}", i));
            fs.mkdir(&path, false).await.unwrap();
            fs.write_file(
                &path.join(format!("file{}.txt", i)),
                format!("content{}", i).as_bytes(),
            )
            .await
            .unwrap();
        }

        let args: Vec<String> = vec![];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Find.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);

        // Verify all 10 levels are found
        for i in 0..10 {
            assert!(
                result.stdout.contains(&format!("level{}", i)),
                "Should find level{} directory",
                i
            );
            assert!(
                result.stdout.contains(&format!("file{}.txt", i)),
                "Should find file{}.txt",
                i
            );
        }
    }

    #[tokio::test]
    async fn test_find_and_ls_consistency() {
        // Ensure find and ls -R find the same nested structure
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        // Create test structure
        fs.mkdir(&cwd.join("top"), false).await.unwrap();
        fs.mkdir(&cwd.join("top/middle"), false).await.unwrap();
        fs.mkdir(&cwd.join("top/middle/bottom"), false)
            .await
            .unwrap();
        fs.write_file(&cwd.join("top/a.txt"), b"a").await.unwrap();
        fs.write_file(&cwd.join("top/middle/b.txt"), b"b")
            .await
            .unwrap();
        fs.write_file(&cwd.join("top/middle/bottom/c.txt"), b"c")
            .await
            .unwrap();

        // Run find
        let args_find: Vec<String> = vec![];
        let ctx_find = Context {
            args: &args_find,
            env: &env,
            variables: &mut variables.clone(),
            cwd: &mut cwd.clone(),
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let find_result = Find.execute(ctx_find).await.unwrap();

        // Run ls -R
        let args_ls = vec!["-R".to_string()];
        let ctx_ls = Context {
            args: &args_ls,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let ls_result = Ls.execute(ctx_ls).await.unwrap();

        // Both should find all the nested content
        assert!(find_result.stdout.contains("top"));
        assert!(find_result.stdout.contains("middle"));
        assert!(find_result.stdout.contains("bottom"));
        assert!(find_result.stdout.contains("a.txt"));
        assert!(find_result.stdout.contains("b.txt"));
        assert!(find_result.stdout.contains("c.txt"));

        assert!(ls_result.stdout.contains("top"));
        assert!(ls_result.stdout.contains("middle"));
        assert!(ls_result.stdout.contains("bottom"));
        assert!(ls_result.stdout.contains("a.txt"));
        assert!(ls_result.stdout.contains("b.txt"));
        assert!(ls_result.stdout.contains("c.txt"));
    }

    #[tokio::test]
    async fn test_find_with_empty_subdirs() {
        // Ensure empty subdirectories are still traversed
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        // Create structure with some empty dirs in the path
        fs.mkdir(&cwd.join("a"), false).await.unwrap();
        fs.mkdir(&cwd.join("a/empty1"), false).await.unwrap();
        fs.mkdir(&cwd.join("a/empty2"), false).await.unwrap();
        fs.mkdir(&cwd.join("a/empty1/deep"), false).await.unwrap();
        fs.write_file(&cwd.join("a/empty1/deep/file.txt"), b"found")
            .await
            .unwrap();

        let args: Vec<String> = vec![];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Find.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);

        // Should find the file through the empty directories
        assert!(result.stdout.contains("file.txt"));
        assert!(result.stdout.contains("empty1"));
        assert!(result.stdout.contains("empty2"));
        assert!(result.stdout.contains("deep"));
    }

    #[tokio::test]
    async fn test_find_from_specific_path() {
        // Test finding from a specific starting path (not cwd)
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        // Create nested structure
        fs.mkdir(&cwd.join("start"), false).await.unwrap();
        fs.mkdir(&cwd.join("start/sub1"), false).await.unwrap();
        fs.mkdir(&cwd.join("start/sub1/sub2"), false).await.unwrap();
        fs.write_file(&cwd.join("start/sub1/sub2/target.txt"), b"target")
            .await
            .unwrap();

        // Find from a specific starting path
        let args = vec!["start".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Find.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);

        assert!(
            result.stdout.contains("start"),
            "Should contain starting path"
        );
        assert!(result.stdout.contains("sub1"), "Should descend into sub1");
        assert!(result.stdout.contains("sub2"), "Should descend into sub2");
        assert!(
            result.stdout.contains("target.txt"),
            "Should find target.txt"
        );
    }

    #[tokio::test]
    async fn test_find_mindepth() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        fs.mkdir(&cwd.join("a"), false).await.unwrap();
        fs.mkdir(&cwd.join("a/b"), false).await.unwrap();
        fs.write_file(&cwd.join("a/file1.txt"), b"f1")
            .await
            .unwrap();
        fs.write_file(&cwd.join("a/b/file2.txt"), b"f2")
            .await
            .unwrap();

        // mindepth 1 should exclude the starting directory "."
        let args = vec!["-mindepth".to_string(), "1".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Find.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);
        // Should NOT contain "." as the starting point (depth 0)
        let lines: Vec<&str> = result.stdout.lines().collect();
        assert!(!lines.contains(&"."), "mindepth 1 should exclude '.'");
        // Should contain everything at depth >= 1
        assert!(result.stdout.contains("./a"));
        assert!(result.stdout.contains("file1.txt"));
        assert!(result.stdout.contains("file2.txt"));
    }

    #[tokio::test]
    async fn test_find_mindepth_with_type() {
        // Reproduces the reported issue: find . -mindepth 1 -type f | wc -l
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        fs.mkdir(&cwd.join("a"), false).await.unwrap();
        fs.mkdir(&cwd.join("a/b"), false).await.unwrap();
        fs.write_file(&cwd.join("a/file1.txt"), b"f1")
            .await
            .unwrap();
        fs.write_file(&cwd.join("a/b/file2.txt"), b"f2")
            .await
            .unwrap();

        // mindepth 1 + type f
        let args = vec![
            "-mindepth".to_string(),
            "1".to_string(),
            "-type".to_string(),
            "f".to_string(),
        ];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Find.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);
        let lines: Vec<&str> = result.stdout.lines().filter(|l| !l.is_empty()).collect();
        assert_eq!(lines.len(), 2, "Should find 2 files: {:?}", lines); // debug-ok: assert-failure message

        // mindepth 1 + type d
        let args2 = vec![
            "-mindepth".to_string(),
            "1".to_string(),
            "-type".to_string(),
            "d".to_string(),
        ];
        let ctx2 = Context {
            args: &args2,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result2 = Find.execute(ctx2).await.unwrap();
        assert_eq!(result2.exit_code, 0);
        let lines2: Vec<&str> = result2.stdout.lines().filter(|l| !l.is_empty()).collect();
        assert_eq!(lines2.len(), 2, "Should find 2 dirs: {:?}", lines2); // debug-ok: assert-failure message
    }

    #[tokio::test]
    async fn test_find_mindepth_2() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        fs.mkdir(&cwd.join("a"), false).await.unwrap();
        fs.mkdir(&cwd.join("a/b"), false).await.unwrap();
        fs.write_file(&cwd.join("top.txt"), b"top").await.unwrap();
        fs.write_file(&cwd.join("a/mid.txt"), b"mid").await.unwrap();
        fs.write_file(&cwd.join("a/b/deep.txt"), b"deep")
            .await
            .unwrap();

        // mindepth 2 should exclude depth 0 and depth 1
        let args = vec!["-mindepth".to_string(), "2".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Find.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);
        let lines: Vec<&str> = result.stdout.lines().collect();
        // depth 0: "." - excluded
        assert!(!lines.contains(&"."));
        // depth 1: "./a", "./top.txt" - excluded
        assert!(!lines.contains(&"./a"));
        assert!(!lines.contains(&"./top.txt"));
        // depth 2: "./a/b", "./a/mid.txt" - included
        assert!(lines.contains(&"./a/b"));
        assert!(lines.contains(&"./a/mid.txt"));
        // depth 3: "./a/b/deep.txt" - included
        assert!(lines.contains(&"./a/b/deep.txt"));
    }

    #[tokio::test]
    async fn test_find_mindepth_missing_arg() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        let args = vec!["-mindepth".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Find.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 1);
        assert!(result.stderr.contains("missing argument"));
    }

    #[tokio::test]
    async fn test_find_mindepth_invalid_value() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        let args = vec!["-mindepth".to_string(), "abc".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Find.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 1);
        assert!(result.stderr.contains("invalid mindepth"));
    }

    // ==================== rmdir tests ====================

    #[tokio::test]
    async fn test_rmdir_empty() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        fs.mkdir(&cwd.join("emptydir"), false).await.unwrap();

        let args = vec!["emptydir".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Rmdir.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);
        assert!(!fs.exists(&cwd.join("emptydir")).await.unwrap());
    }

    #[tokio::test]
    async fn test_rmdir_not_empty() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        fs.mkdir(&cwd.join("notempty"), false).await.unwrap();
        fs.write_file(&cwd.join("notempty/file.txt"), b"content")
            .await
            .unwrap();

        let args = vec!["notempty".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Rmdir.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 1);
        assert!(result.stderr.contains("not empty"));
    }

    #[tokio::test]
    async fn test_rmdir_nonexistent() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        let args = vec!["nonexistent".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Rmdir.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 1);
        assert!(result.stderr.contains("No such file or directory"));
    }

    #[tokio::test]
    async fn test_rmdir_not_directory() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        fs.write_file(&cwd.join("file.txt"), b"content")
            .await
            .unwrap();

        let args = vec!["file.txt".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Rmdir.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 1);
        assert!(result.stderr.contains("Not a directory"));
    }

    #[tokio::test]
    async fn test_rmdir_parents() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        fs.mkdir(&cwd.join("a/b/c"), true).await.unwrap();

        let args = vec!["-p".to_string(), "a/b/c".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Rmdir.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);
        assert!(!fs.exists(&cwd.join("a/b/c")).await.unwrap());
        assert!(!fs.exists(&cwd.join("a/b")).await.unwrap());
        assert!(!fs.exists(&cwd.join("a")).await.unwrap());
    }

    #[tokio::test]
    async fn test_rmdir_missing_operand() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        let args: Vec<String> = vec![];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Rmdir.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 1);
        assert!(result.stderr.contains("missing operand"));
    }

    // ==================== Custom filesystem tests ====================

    #[tokio::test]
    async fn test_find_with_overlay_fs() {
        use crate::fs::OverlayFs;

        // Create base filesystem with nested structure
        let base = Arc::new(InMemoryFs::new());
        base.mkdir(Path::new("/home/user"), true).await.unwrap();
        base.mkdir(Path::new("/home/user/base"), false)
            .await
            .unwrap();
        base.mkdir(Path::new("/home/user/base/sub1"), false)
            .await
            .unwrap();
        base.mkdir(Path::new("/home/user/base/sub1/sub2"), false)
            .await
            .unwrap();
        base.write_file(Path::new("/home/user/base/file1.txt"), b"base1")
            .await
            .unwrap();
        base.write_file(Path::new("/home/user/base/sub1/file2.txt"), b"base2")
            .await
            .unwrap();
        base.write_file(Path::new("/home/user/base/sub1/sub2/file3.txt"), b"base3")
            .await
            .unwrap();

        // Create overlay
        let overlay: Arc<dyn FileSystem> = Arc::new(OverlayFs::new(base));

        // Add a file in the overlay layer (use recursive to ensure parent exists in upper)
        overlay
            .mkdir(Path::new("/home/user/base/overlay_dir"), true)
            .await
            .unwrap();
        overlay
            .write_file(
                Path::new("/home/user/base/overlay_dir/overlay_file.txt"),
                b"overlay",
            )
            .await
            .unwrap();

        let mut cwd = PathBuf::from("/home/user");
        let mut variables = HashMap::new();
        let env = HashMap::new();

        // Run find on the overlay filesystem
        let args = vec!["base".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: overlay.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Find.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);

        // Should find files from base layer
        assert!(
            result.stdout.contains("file1.txt"),
            "Should find file1.txt from base"
        );
        assert!(
            result.stdout.contains("file2.txt"),
            "Should find file2.txt from base/sub1"
        );
        assert!(
            result.stdout.contains("file3.txt"),
            "Should find file3.txt from base/sub1/sub2"
        );

        // Should find files from overlay layer
        assert!(
            result.stdout.contains("overlay_dir"),
            "Should find overlay_dir"
        );
        assert!(
            result.stdout.contains("overlay_file.txt"),
            "Should find overlay_file.txt"
        );

        // Should descend into all subdirectories
        assert!(result.stdout.contains("sub1"), "Should find sub1");
        assert!(result.stdout.contains("sub2"), "Should find sub2");
    }

    #[tokio::test]
    async fn test_find_with_mountable_fs() {
        use crate::fs::MountableFs;

        // Create root filesystem
        let root = Arc::new(InMemoryFs::new());
        root.mkdir(Path::new("/home/user"), true).await.unwrap();
        root.write_file(Path::new("/home/user/root_file.txt"), b"root")
            .await
            .unwrap();

        // Create a nested filesystem to mount
        let nested = Arc::new(InMemoryFs::new());
        nested.mkdir(Path::new("/level1"), false).await.unwrap();
        nested
            .mkdir(Path::new("/level1/level2"), false)
            .await
            .unwrap();
        nested
            .mkdir(Path::new("/level1/level2/level3"), false)
            .await
            .unwrap();
        nested
            .write_file(Path::new("/level1/nested1.txt"), b"n1")
            .await
            .unwrap();
        nested
            .write_file(Path::new("/level1/level2/nested2.txt"), b"n2")
            .await
            .unwrap();
        nested
            .write_file(Path::new("/level1/level2/level3/nested3.txt"), b"n3")
            .await
            .unwrap();

        // Create mountable filesystem and mount nested at /home/user/mounted
        let mountable = MountableFs::new(root.clone());
        mountable
            .mount("/home/user/mounted", nested.clone())
            .unwrap();

        let fs: Arc<dyn FileSystem> = Arc::new(mountable);
        let mut cwd = PathBuf::from("/home/user");
        let mut variables = HashMap::new();
        let env = HashMap::new();

        // Run find from cwd - should find both root files and mounted files
        let args: Vec<String> = vec![];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Find.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);

        // Should find root file
        assert!(
            result.stdout.contains("root_file.txt"),
            "Should find root_file.txt"
        );

        // Should find mount point
        assert!(
            result.stdout.contains("mounted"),
            "Should find mounted directory"
        );

        // Should descend into mounted filesystem
        assert!(
            result.stdout.contains("level1"),
            "Should find level1 in mounted fs"
        );
        assert!(
            result.stdout.contains("level2"),
            "Should find level2 in mounted fs"
        );
        assert!(
            result.stdout.contains("level3"),
            "Should find level3 in mounted fs"
        );

        // Should find files deep in mounted filesystem
        assert!(
            result.stdout.contains("nested1.txt"),
            "Should find nested1.txt"
        );
        assert!(
            result.stdout.contains("nested2.txt"),
            "Should find nested2.txt"
        );
        assert!(
            result.stdout.contains("nested3.txt"),
            "Should find nested3.txt"
        );
    }

    #[tokio::test]
    async fn test_ls_recursive_with_overlay_fs() {
        use crate::fs::OverlayFs;

        // Create base filesystem with nested structure
        let base = Arc::new(InMemoryFs::new());
        base.mkdir(Path::new("/home/user"), true).await.unwrap();
        base.mkdir(Path::new("/home/user/dir"), false)
            .await
            .unwrap();
        base.mkdir(Path::new("/home/user/dir/subdir"), false)
            .await
            .unwrap();
        base.write_file(Path::new("/home/user/dir/base.txt"), b"base")
            .await
            .unwrap();
        base.write_file(Path::new("/home/user/dir/subdir/deep.txt"), b"deep")
            .await
            .unwrap();

        let overlay: Arc<dyn FileSystem> = Arc::new(OverlayFs::new(base));

        let mut cwd = PathBuf::from("/home/user");
        let mut variables = HashMap::new();
        let env = HashMap::new();

        let args = vec!["-R".to_string(), "dir".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: overlay,
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);

        assert!(result.stdout.contains("base.txt"), "Should list base.txt");
        assert!(result.stdout.contains("subdir"), "Should list subdir");
        assert!(result.stdout.contains("deep.txt"), "Should list deep.txt");
    }

    #[tokio::test]
    async fn test_ls_recursive_with_mountable_fs() {
        use crate::fs::MountableFs;

        let root = Arc::new(InMemoryFs::new());
        root.mkdir(Path::new("/home/user"), true).await.unwrap();

        let mounted = Arc::new(InMemoryFs::new());
        mounted.mkdir(Path::new("/a"), false).await.unwrap();
        mounted.mkdir(Path::new("/a/b"), false).await.unwrap();
        mounted
            .write_file(Path::new("/a/file_a.txt"), b"a")
            .await
            .unwrap();
        mounted
            .write_file(Path::new("/a/b/file_b.txt"), b"b")
            .await
            .unwrap();

        let mountable = MountableFs::new(root);
        mountable.mount("/home/user/mnt", mounted.clone()).unwrap();

        let fs: Arc<dyn FileSystem> = Arc::new(mountable);
        let mut cwd = PathBuf::from("/home/user");
        let mut variables = HashMap::new();
        let env = HashMap::new();

        let args = vec!["-R".to_string(), "mnt".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);

        assert!(result.stdout.contains("a"), "Should list directory a");
        assert!(result.stdout.contains("b"), "Should list directory b");
        assert!(
            result.stdout.contains("file_a.txt"),
            "Should list file_a.txt"
        );
        assert!(
            result.stdout.contains("file_b.txt"),
            "Should list file_b.txt"
        );
    }

    // ==================== root directory tests ====================

    #[tokio::test]
    async fn test_ls_root_directory() {
        // Test listing the root directory directly
        let fs = Arc::new(InMemoryFs::new());
        let mut cwd = PathBuf::from("/home/user");
        let mut variables = HashMap::new();
        let env = HashMap::new();

        // ls / should work
        let args = vec!["/".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(
            result.exit_code, 0,
            "ls / should succeed: {}",
            result.stderr
        );
        // Root should contain at least tmp, home, dev
        assert!(result.stdout.contains("tmp"), "Root should contain tmp");
        assert!(result.stdout.contains("home"), "Root should contain home");
        assert!(result.stdout.contains("dev"), "Root should contain dev");
    }

    #[tokio::test]
    async fn test_ls_dot_from_root() {
        // Test: when cwd is /, ls . should list root contents
        let fs = Arc::new(InMemoryFs::new());
        let mut cwd = PathBuf::from("/");
        let mut variables = HashMap::new();
        let env = HashMap::new();

        // ls . with cwd=/ should work
        let args = vec![".".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(
            result.exit_code, 0,
            "ls . from / should succeed: {}",
            result.stderr
        );
        assert!(result.stdout.contains("tmp"), "Root should contain tmp");
        assert!(result.stdout.contains("home"), "Root should contain home");
    }

    #[tokio::test]
    async fn test_ls_default_from_root() {
        // Test: when cwd is /, ls (no args) should list root contents
        let fs = Arc::new(InMemoryFs::new());
        let mut cwd = PathBuf::from("/");
        let mut variables = HashMap::new();
        let env = HashMap::new();

        // ls with no args and cwd=/ should work
        let args: Vec<String> = vec![];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(
            result.exit_code, 0,
            "ls from / should succeed: {}",
            result.stderr
        );
        assert!(result.stdout.contains("tmp"), "Root should contain tmp");
        assert!(result.stdout.contains("home"), "Root should contain home");
    }

    #[tokio::test]
    async fn test_ls_root_with_overlay_fs() {
        // Test: ls / with OverlayFs should work
        use crate::fs::OverlayFs;

        let base = Arc::new(InMemoryFs::new());
        let overlay: Arc<dyn FileSystem> = Arc::new(OverlayFs::new(base));

        let mut cwd = PathBuf::from("/");
        let mut variables = HashMap::new();
        let env = HashMap::new();

        // ls / should work with overlay
        let args = vec!["/".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: overlay.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(
            result.exit_code, 0,
            "ls / with overlay should succeed: {}",
            result.stderr
        );
        assert!(result.stdout.contains("tmp"), "Root should contain tmp");
        assert!(result.stdout.contains("home"), "Root should contain home");
    }

    #[tokio::test]
    async fn test_ls_dot_from_root_with_overlay_fs() {
        // Test: cd / && ls . with OverlayFs should work
        use crate::fs::OverlayFs;

        let base = Arc::new(InMemoryFs::new());
        let overlay: Arc<dyn FileSystem> = Arc::new(OverlayFs::new(base));

        let mut cwd = PathBuf::from("/");
        let mut variables = HashMap::new();
        let env = HashMap::new();

        // ls . from / with overlay
        let args = vec![".".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: overlay.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(
            result.exit_code, 0,
            "ls . from / with overlay should succeed: {}",
            result.stderr
        );
        assert!(result.stdout.contains("tmp"), "Root should contain tmp");
        assert!(result.stdout.contains("home"), "Root should contain home");
    }

    #[tokio::test]
    async fn test_resolve_path_slash_dot_normalized() {
        // Verify that "/." path (from /join(".")) resolves correctly to root
        let fs = Arc::new(InMemoryFs::new());

        // The path "/." should normalize to "/" and exist
        assert!(fs.exists(Path::new("/.")).await.unwrap(), "/. should exist");
        assert!(fs.exists(Path::new("/")).await.unwrap(), "/ should exist");

        // Both should return the same stat info
        let stat_root = fs.stat(Path::new("/")).await.unwrap();
        let stat_dot = fs.stat(Path::new("/.")).await.unwrap();
        assert!(stat_root.file_type.is_dir());
        assert!(stat_dot.file_type.is_dir());
    }

    // ==================== negative tests ====================

    #[tokio::test]
    async fn test_ls_nonexistent_path() {
        // Negative test: ls on path that doesn't exist should fail
        let fs = Arc::new(InMemoryFs::new());
        let mut cwd = PathBuf::from("/home/user");
        let mut variables = HashMap::new();
        let env = HashMap::new();

        let args = vec!["/nonexistent/path".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 2, "ls on nonexistent path should fail");
        assert!(
            result.stderr.contains("No such file or directory"),
            "Should report file not found"
        );
    }

    #[tokio::test]
    async fn test_ls_path_traversal_normalized() {
        // Positive test: path traversal with .. should be normalized and work
        let fs = Arc::new(InMemoryFs::new());
        let mut cwd = PathBuf::from("/home/user");
        let mut variables = HashMap::new();
        let env = HashMap::new();

        // /home/user/../user should normalize to /home/user
        let args = vec!["../user".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        // /home/user is an empty directory by default, so it should succeed with empty output
        assert_eq!(
            result.exit_code, 0,
            "ls with .. should succeed after normalization: {}",
            result.stderr
        );
    }

    #[tokio::test]
    async fn test_ls_excessive_dotdot_stays_at_root() {
        // Positive test: excessive .. should stay at root
        let fs = Arc::new(InMemoryFs::new());
        let mut cwd = PathBuf::from("/home/user");
        let mut variables = HashMap::new();
        let env = HashMap::new();

        // ../../../../.. from /home/user should normalize to /
        let args = vec!["../../../../..".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(
            result.exit_code, 0,
            "Excessive .. should normalize to root: {}",
            result.stderr
        );
        // Should list root contents
        assert!(result.stdout.contains("tmp"), "Should list root (tmp)");
        assert!(result.stdout.contains("home"), "Should list root (home)");
    }

    #[tokio::test]
    async fn test_ls_dot_in_middle_of_path() {
        // Positive test: . in middle of path should be normalized
        let fs = Arc::new(InMemoryFs::new());
        let mut cwd = PathBuf::from("/");
        let mut variables = HashMap::new();
        let env = HashMap::new();

        // /./home/./user/. should normalize to /home/user
        let args = vec!["./home/./user/.".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(
            result.exit_code, 0,
            "Path with . components should work: {}",
            result.stderr
        );
    }

    // ==================== glob_match tests ====================

    // ==================== file size reporting tests ====================

    #[tokio::test]
    async fn test_ls_long_format_shows_correct_file_size() {
        // Positive test: file with known content shows correct size
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        // Create file with exactly 13 bytes: "hello world\n" (11 chars + newline from echo)
        let content = b"hello world\n";
        fs.write_file(&cwd.join("test.txt"), content).await.unwrap();

        let args = vec!["-l".to_string(), "test.txt".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);
        // File size should be 12 bytes (content.len())
        assert!(
            result.stdout.contains("12"),
            "Expected size 12 in output, got: {}",
            result.stdout
        );
    }

    #[tokio::test]
    async fn test_ls_long_format_empty_file_shows_zero_size() {
        // Negative test: empty file shows size 0
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        fs.write_file(&cwd.join("empty.txt"), b"").await.unwrap();

        let args = vec!["-l".to_string(), "empty.txt".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);
        // Empty file should show size 0
        // Format is: -rw-r--r--        0 YYYY-MM-DD HH:MM empty.txt
        assert!(
            result.stdout.contains("       0"),
            "Expected size 0 in output, got: {}",
            result.stdout
        );
    }

    #[tokio::test]
    async fn test_ls_long_format_directory_shows_zero_size() {
        // Negative test: directory shows size 0
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        fs.mkdir(&cwd.join("subdir"), false).await.unwrap();

        let args = vec!["-l".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);
        // Directory line should contain size 0
        // Format is: drwxr-xr-x        0 YYYY-MM-DD HH:MM subdir
        let lines: Vec<&str> = result.stdout.lines().collect();
        let subdir_line = lines.iter().find(|l| l.contains("subdir")).unwrap();
        assert!(
            subdir_line.contains("       0"),
            "Expected directory size 0, got: {}",
            subdir_line
        );
    }

    #[tokio::test]
    async fn test_ls_long_format_multiple_files_correct_sizes() {
        // Positive test: multiple files show their respective sizes
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        // Create files with different sizes
        fs.write_file(&cwd.join("small.txt"), b"hi").await.unwrap(); // 2 bytes
        fs.write_file(&cwd.join("medium.txt"), b"hello world")
            .await
            .unwrap(); // 11 bytes
        fs.write_file(
            &cwd.join("large.txt"),
            b"this is a longer content string for testing",
        )
        .await
        .unwrap(); // 43 bytes

        let args = vec!["-l".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let result = Ls.execute(ctx).await.unwrap();
        assert_eq!(result.exit_code, 0);

        let lines: Vec<&str> = result.stdout.lines().collect();

        // Check small.txt has size 2
        let small_line = lines.iter().find(|l| l.contains("small.txt")).unwrap();
        assert!(
            small_line.contains("       2"),
            "Expected small.txt size 2, got: {}",
            small_line
        );

        // Check medium.txt has size 11
        let medium_line = lines.iter().find(|l| l.contains("medium.txt")).unwrap();
        assert!(
            medium_line.contains("      11"),
            "Expected medium.txt size 11, got: {}",
            medium_line
        );

        // Check large.txt has size 43
        let large_line = lines.iter().find(|l| l.contains("large.txt")).unwrap();
        assert!(
            large_line.contains("      43"),
            "Expected large.txt size 43, got: {}",
            large_line
        );
    }

    #[test]
    fn test_glob_match_star() {
        assert!(glob_match("file.txt", "*.txt"));
        assert!(glob_match("test.txt", "*.txt"));
        assert!(!glob_match("file.md", "*.txt"));
    }

    #[test]
    fn test_glob_match_question() {
        assert!(glob_match("ab", "a?"));
        assert!(glob_match("ac", "a?"));
        assert!(!glob_match("abc", "a?"));
    }

    #[test]
    fn test_glob_match_exact() {
        assert!(glob_match("test", "test"));
        assert!(!glob_match("test", "other"));
    }

    #[test]
    fn test_glob_match_star_middle() {
        assert!(glob_match("test.backup.txt", "test*.txt"));
        assert!(glob_match("test.txt", "test*.txt"));
    }

    // ==================== parse_find_args tests ====================

    #[test]
    fn test_parse_find_args_defaults_to_dot() {
        let args: Vec<String> = vec![];
        let (paths, opts) = parse_find_args(&args).unwrap();
        assert_eq!(paths, vec!["."]);
        assert!(opts.exec_args.is_empty());
        assert!(!opts.exec_batch);
    }

    #[test]
    fn test_parse_find_args_exec_per_file() {
        let args: Vec<String> = vec![
            ".".into(),
            "-name".into(),
            "*.txt".into(),
            "-exec".into(),
            "cat".into(),
            "{}".into(),
            ";".into(),
        ];
        let (paths, opts) = parse_find_args(&args).unwrap();
        assert_eq!(paths, vec!["."]);
        assert_eq!(opts.name_pattern.as_deref(), Some("*.txt"));
        assert_eq!(opts.exec_args, vec!["cat", "{}"]);
        assert!(!opts.exec_batch);
    }

    #[test]
    fn test_parse_find_args_exec_batch() {
        let args: Vec<String> = vec!["-exec".into(), "rm".into(), "{}".into(), "+".into()];
        let (_paths, opts) = parse_find_args(&args).unwrap();
        assert_eq!(opts.exec_args, vec!["rm", "{}"]);
        assert!(opts.exec_batch);
    }

    #[test]
    fn test_parse_find_args_error_missing_name() {
        let args: Vec<String> = vec!["-name".into()];
        assert!(parse_find_args(&args).is_err());
    }

    #[test]
    fn test_parse_find_args_error_unknown_predicate() {
        let args: Vec<String> = vec!["-bogus".into()];
        assert!(parse_find_args(&args).is_err());
    }

    // ==================== build_find_exec_commands tests ====================

    #[test]
    fn test_build_find_exec_commands_per_file() {
        let exec_args = vec!["echo".to_string(), "{}".to_string()];
        let paths = vec!["a.txt".to_string(), "b.txt".to_string()];
        let cmds = build_find_exec_commands(&exec_args, &paths, false);
        assert_eq!(cmds.len(), 2);
        assert_eq!(cmds[0].name, "echo");
        assert_eq!(cmds[0].args, vec!["a.txt"]);
        assert_eq!(cmds[1].name, "echo");
        assert_eq!(cmds[1].args, vec!["b.txt"]);
    }

    #[test]
    fn test_build_find_exec_commands_batch() {
        let exec_args = vec!["rm".to_string(), "{}".to_string()];
        let paths = vec!["a.txt".to_string(), "b.txt".to_string()];
        let cmds = build_find_exec_commands(&exec_args, &paths, true);
        assert_eq!(cmds.len(), 1);
        assert_eq!(cmds[0].name, "rm");
        assert_eq!(cmds[0].args, vec!["a.txt", "b.txt"]);
    }

    #[test]
    fn test_build_find_exec_commands_empty_paths() {
        let exec_args = vec!["echo".to_string(), "{}".to_string()];
        let cmds = build_find_exec_commands(&exec_args, &[], false);
        assert!(cmds.is_empty());
    }

    #[test]
    fn test_build_find_exec_commands_empty_exec() {
        let paths = vec!["a.txt".to_string()];
        let cmds = build_find_exec_commands(&[], &paths, false);
        assert!(cmds.is_empty());
    }

    #[test]
    fn test_build_find_exec_commands_multiple_placeholders() {
        let exec_args = vec!["cp".to_string(), "{}".to_string(), "{}.bak".to_string()];
        let paths = vec!["a.txt".to_string()];
        let cmds = build_find_exec_commands(&exec_args, &paths, false);
        assert_eq!(cmds.len(), 1);
        assert_eq!(cmds[0].name, "cp");
        assert_eq!(cmds[0].args, vec!["a.txt", "a.txt.bak"]);
    }

    // ==================== find execution_plan tests ====================

    #[tokio::test]
    async fn test_find_plan_no_exec_returns_none() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        let args = vec!["-name".to_string(), "*.txt".to_string()];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let plan = Find.execution_plan(&ctx).await.unwrap();
        assert!(plan.is_none());
    }

    #[tokio::test]
    async fn test_find_plan_exec_with_matches() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        // Create test files
        fs.write_file(&PathBuf::from("/home/user/a.txt"), b"hello")
            .await
            .unwrap();
        fs.write_file(&PathBuf::from("/home/user/b.txt"), b"world")
            .await
            .unwrap();

        let args = vec![
            ".".to_string(),
            "-name".to_string(),
            "*.txt".to_string(),
            "-exec".to_string(),
            "cat".to_string(),
            "{}".to_string(),
            ";".to_string(),
        ];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let plan = Find.execution_plan(&ctx).await.unwrap();
        match plan {
            Some(ExecutionPlan::Batch { commands }) => {
                assert_eq!(commands.len(), 2);
                assert_eq!(commands[0].name, "cat");
                // Each command should have a single arg (the found path)
                assert_eq!(commands[0].args.len(), 1);
                assert_eq!(commands[1].args.len(), 1);
            }
            _ => panic!("expected Batch plan"),
        }
    }

    #[tokio::test]
    async fn test_find_plan_exec_no_matches_returns_none() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        let args = vec![
            ".".to_string(),
            "-name".to_string(),
            "*.xyz".to_string(),
            "-exec".to_string(),
            "echo".to_string(),
            "{}".to_string(),
            ";".to_string(),
        ];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let plan = Find.execution_plan(&ctx).await.unwrap();
        assert!(plan.is_none());
    }

    #[tokio::test]
    async fn test_find_plan_exec_with_missing_path_returns_status_plan() {
        let (fs, mut cwd, mut variables) = create_test_ctx().await;
        let env = HashMap::new();

        fs.write_file(&PathBuf::from("/home/user/a.txt"), b"hello")
            .await
            .unwrap();

        let args = vec![
            "/home/user".to_string(),
            "/home/missing".to_string(),
            "-name".to_string(),
            "*.txt".to_string(),
            "-exec".to_string(),
            "echo".to_string(),
            "{}".to_string(),
            ";".to_string(),
        ];
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs: fs.clone(),
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        let plan = Find.execution_plan(&ctx).await.unwrap();
        match plan {
            Some(ExecutionPlan::BatchWithStatus {
                commands,
                stderr_prefix,
                force_error_exit,
            }) => {
                assert_eq!(commands.len(), 1);
                assert!(stderr_prefix.contains("No such file or directory"));
                assert!(force_error_exit);
            }
            _ => panic!("expected BatchWithStatus plan"),
        }
    }
}
