//! Path resolution utilities: fuzzy matching, sigil expansion, and unified path parsing.

use ignore::WalkBuilder;
use nucleo_matcher::pattern::{CaseMatching, Normalization, Pattern};
use nucleo_matcher::{Config, Matcher};
use std::path::Path;

/// Whether a resolved path points to a file or a directory.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PathMatchKind {
    File,
    Directory,
}

/// A resolved path match with fuzzy-match score and kind.
#[derive(Debug, Clone)]
pub struct PathMatch {
    pub path: String,
    pub kind: PathMatchKind,
    pub score: u32,
}

/// A single entry returned by [`PathSource::all_files`] and [`PathSource::find_like`].
pub struct PathEntry {
    pub path: String,
    pub kind: PathMatchKind,
}

/// Result of expanding a sigil like `@todo` or `@config`.
#[derive(Debug, Clone)]
pub struct SigilExpansion {
    /// Expanded file paths (may be multiple, e.g., ["TODO.md", "TASKS.md"])
    pub paths: Vec<String>,
    /// Remaining path after the sigil (e.g., "Section/Item" from "@todo/Section/Item")
    pub suffix: String,
}

/// Source of indexed file paths (e.g., from a database index).
pub trait PathSource {
    /// Return paths matching `query` using a fast prefix/substring filter.
    /// Returns `None` if the source cannot answer this query (caller falls back to `all_files`).
    fn find_like(&self, query: &str) -> Option<Vec<PathEntry>>;

    /// Return all known file/directory entries.
    /// Returns `None` if the source is unavailable; caller falls back to a filesystem walk.
    fn all_files(&self) -> Option<Vec<PathEntry>>;
}

/// Expand an alias query like `@todo` or `@config/section`.
/// Returns None if the query doesn't start with @ or the alias is unknown.
///
/// `alias_lookup` resolves an alias name to its target paths.
pub fn expand_sigil(
    query: &str,
    alias_lookup: &dyn Fn(&str) -> Option<Vec<String>>,
) -> Option<SigilExpansion> {
    if !query.starts_with('@') {
        return None;
    }

    let rest = &query[1..]; // Strip @

    // Alias name: alphanumeric, underscore, hyphen
    let alias_end = rest
        .find(|c: char| !c.is_alphanumeric() && c != '_' && c != '-')
        .unwrap_or(rest.len());

    let alias_name = &rest[..alias_end];
    let after_alias = &rest[alias_end..];

    // Strip the separator to get suffix (supports /, :, ::, #)
    let suffix = after_alias
        .strip_prefix("::")
        .or_else(|| after_alias.strip_prefix('/'))
        .or_else(|| after_alias.strip_prefix(':'))
        .or_else(|| after_alias.strip_prefix('#'))
        .unwrap_or(after_alias);

    let targets = alias_lookup(alias_name)?;

    Some(SigilExpansion {
        paths: targets,
        suffix: suffix.to_string(),
    })
}

/// Result of resolving a unified path like `src/main.py/Foo/bar`
#[derive(Debug, Clone)]
pub struct UnifiedPath {
    /// The file path portion (e.g., "src/main.py")
    pub file_path: String,
    /// The symbol path within the file (e.g., "Foo/bar"), empty if pointing to file itself
    pub symbol_path: Vec<String>,
    /// Whether the path resolved to a directory (no symbol path possible)
    pub is_directory: bool,
}

/// Normalize a unified path query, converting various separator styles to `/`.
/// Supports: `::` (Rust-style), `#` (URL fragment), `:` (compact)
fn normalize_separators(query: &str) -> String {
    query
        .replace("::", "/")
        .replace('#', "/")
        // Only replace single : if it looks like file:symbol (has file extension before it)
        .split(':')
        .enumerate()
        .map(|(i, part)| {
            if i == 0 {
                part.to_string()
            } else {
                format!("/{}", part)
            }
        })
        .collect::<String>()
}

/// Resolve a unified path like `src/main.py/Foo/bar` to file + symbol components.
///
/// Uses filesystem as source of truth: walks segments left-to-right, checking
/// at each step whether the path exists as file or directory. Once we hit a file,
/// remaining segments are the symbol path.
///
/// Strategy:
/// 1. Walk path segments, checking each accumulated path against filesystem
/// 2. When we hit a file, everything after is symbol path
/// 3. If exact path doesn't exist, try fuzzy matching for the file portion
pub fn resolve_unified(
    query: &str,
    root: &Path,
    alias_lookup: &dyn Fn(&str) -> Option<Vec<String>>,
    path_source: Option<&dyn PathSource>,
) -> Option<UnifiedPath> {
    resolve_unified_depth(query, root, alias_lookup, path_source, 0)
}

fn resolve_unified_depth(
    query: &str,
    root: &Path,
    alias_lookup: &dyn Fn(&str) -> Option<Vec<String>>,
    path_source: Option<&dyn PathSource>,
    depth: u8,
) -> Option<UnifiedPath> {
    // Handle sigil expansion (@todo, @config, etc.)
    if query.starts_with('@')
        && let Some(expansion) = expand_sigil(query, alias_lookup)
    {
        // Guard against alias cycles (a→@b→@a→...) by limiting recursion depth.
        if depth >= 32 {
            return None;
        }
        // Try each target path until one exists
        for target in &expansion.paths {
            let full_query = if expansion.suffix.is_empty() {
                target.clone()
            } else {
                format!("{}/{}", target, expansion.suffix)
            };
            if let Some(result) =
                resolve_unified_depth(&full_query, root, alias_lookup, None, depth + 1)
            {
                return Some(result);
            }
        }
        return None;
    }
    // Unknown sigil - fall through to normal resolution (will likely fail)

    let normalized = normalize_separators(query);

    // Handle absolute paths (start with /) - use filesystem root instead of project root
    let (segments, base_path): (Vec<&str>, std::path::PathBuf) = if normalized.starts_with('/') {
        let segs: Vec<&str> = normalized.split('/').filter(|s| !s.is_empty()).collect();
        (segs, std::path::PathBuf::from("/"))
    } else {
        let segs: Vec<&str> = normalized.split('/').filter(|s| !s.is_empty()).collect();
        (segs, root.to_path_buf())
    };
    let is_absolute = normalized.starts_with('/');

    if segments.is_empty() {
        return None;
    }

    // Strategy 1: Walk exact path segments
    let mut current_path = base_path.clone();
    for (idx, segment) in segments.iter().enumerate() {
        let test_path = current_path.join(segment);

        if test_path.is_file() {
            // Found a file - this is the boundary
            // For absolute paths, keep full path; for relative, strip root prefix
            let file_path = if is_absolute {
                test_path.to_string_lossy().to_string()
            } else {
                test_path
                    .strip_prefix(root)
                    .unwrap_or(&test_path)
                    .to_string_lossy()
                    .to_string()
            };
            return Some(UnifiedPath {
                file_path,
                symbol_path: segments[idx + 1..].iter().map(|s| s.to_string()).collect(),
                is_directory: false,
            });
        } else if test_path.is_dir() {
            current_path = test_path;
        } else {
            // Path doesn't exist - try fuzzy resolution (only for relative paths)
            break;
        }
    }

    // Check if we ended at a directory
    if current_path != base_path && current_path.is_dir() {
        let dir_path = if is_absolute {
            current_path.to_string_lossy().to_string()
        } else {
            current_path
                .strip_prefix(root)
                .unwrap_or(&current_path)
                .to_string_lossy()
                .to_string()
        };
        let matched_segments = dir_path.matches('/').count() + 1;
        if matched_segments >= segments.len() {
            return Some(UnifiedPath {
                file_path: dir_path,
                symbol_path: vec![],
                is_directory: true,
            });
        }
    }

    // Strategy 2: Try fuzzy matching (only for relative paths within project)
    if !is_absolute {
        // Pre-load all paths once to avoid repeated mutable borrows in the loop
        let all_paths = get_paths_for_query(root, "", path_source);
        for split_point in (1..=segments.len()).rev() {
            let file_query = segments[..split_point].join("/");
            let matches = resolve_from_paths(&file_query, &all_paths);

            if let Some(m) = matches.first() {
                if m.kind == PathMatchKind::File {
                    return Some(UnifiedPath {
                        file_path: m.path.clone(),
                        symbol_path: segments[split_point..]
                            .iter()
                            .map(|s| s.to_string())
                            .collect(),
                        is_directory: false,
                    });
                } else if m.kind == PathMatchKind::Directory && split_point == segments.len() {
                    // Only return directory if it's the full query
                    return Some(UnifiedPath {
                        file_path: m.path.clone(),
                        symbol_path: vec![],
                        is_directory: true,
                    });
                }
            }
        }
    }

    None
}

/// Resolve a query to ALL matching unified paths (for ambiguous queries).
/// Returns empty vec if no matches, single-element vec if unambiguous,
/// or multiple elements if query matches multiple files.
pub fn resolve_unified_all(
    query: &str,
    root: &Path,
    alias_lookup: &dyn Fn(&str) -> Option<Vec<String>>,
    path_source: Option<&dyn PathSource>,
) -> Vec<UnifiedPath> {
    resolve_unified_all_depth(query, root, alias_lookup, path_source, 0)
}

fn resolve_unified_all_depth(
    query: &str,
    root: &Path,
    alias_lookup: &dyn Fn(&str) -> Option<Vec<String>>,
    path_source: Option<&dyn PathSource>,
    depth: u8,
) -> Vec<UnifiedPath> {
    // Handle sigil expansion (@todo, @config, etc.)
    if query.starts_with('@')
        && let Some(expansion) = expand_sigil(query, alias_lookup)
    {
        // Guard against alias cycles by limiting recursion depth.
        if depth >= 32 {
            return vec![];
        }
        let mut results = Vec::new();
        for target in &expansion.paths {
            let full_query = if expansion.suffix.is_empty() {
                target.clone()
            } else {
                format!("{}/{}", target, expansion.suffix)
            };
            results.extend(resolve_unified_all_depth(
                &full_query,
                root,
                alias_lookup,
                None,
                depth + 1,
            ));
        }
        return results;
    }

    let normalized = normalize_separators(query);

    // Trailing slash means "directory only" for fuzzy matching
    let dir_only = normalized.ends_with('/');

    // Absolute paths: single result or none
    if normalized.starts_with('/') {
        return resolve_unified_depth(query, root, alias_lookup, None, depth)
            .into_iter()
            .collect();
    }

    let segments: Vec<&str> = normalized.split('/').filter(|s| !s.is_empty()).collect();
    if segments.is_empty() {
        return vec![];
    }

    // Try exact path first
    let mut current_path = root.to_path_buf();
    for (idx, segment) in segments.iter().enumerate() {
        let test_path = current_path.join(segment);
        if test_path.is_file() {
            // Exact match - return single result
            let file_path = test_path
                .strip_prefix(root)
                .unwrap_or(&test_path)
                .to_string_lossy()
                .to_string();
            return vec![UnifiedPath {
                file_path,
                symbol_path: segments[idx + 1..].iter().map(|s| s.to_string()).collect(),
                is_directory: false,
            }];
        } else if test_path.is_dir() {
            current_path = test_path;
        } else {
            break;
        }
    }

    // Check if we ended at a directory (exact match)
    if current_path != root.to_path_buf() && current_path.is_dir() {
        let dir_path = current_path
            .strip_prefix(root)
            .unwrap_or(&current_path)
            .to_string_lossy()
            .to_string();
        return vec![UnifiedPath {
            file_path: dir_path,
            symbol_path: vec![],
            is_directory: true,
        }];
    }

    // Fuzzy matching - return ALL matches
    // Pre-load all paths once to avoid repeated mutable borrows in the loop
    let all_paths = get_paths_for_query(root, "", path_source);
    for split_point in (1..=segments.len()).rev() {
        let file_query = segments[..split_point].join("/");
        let matches = resolve_from_paths(&file_query, &all_paths);

        if !matches.is_empty() {
            // Filter to directories only if query ended with /
            let filtered: Vec<_> = if dir_only {
                matches
                    .into_iter()
                    .filter(|m| m.kind == PathMatchKind::Directory)
                    .collect()
            } else {
                matches
            };

            if !filtered.is_empty() {
                return filtered
                    .into_iter()
                    .map(|m| UnifiedPath {
                        file_path: m.path,
                        symbol_path: segments[split_point..]
                            .iter()
                            .map(|s| s.to_string())
                            .collect(),
                        is_directory: m.kind == PathMatchKind::Directory,
                    })
                    .collect();
            }
        }
    }

    vec![]
}

/// Get all files in the repository (uses path source if available, else walks filesystem)
pub fn all_files(root: &Path, path_source: Option<&dyn PathSource>) -> Vec<PathMatch> {
    get_paths_for_query(root, "", path_source)
        .into_iter()
        .map(|(path, is_dir)| PathMatch {
            path,
            kind: if is_dir {
                PathMatchKind::Directory
            } else {
                PathMatchKind::File
            },
            score: 0,
        })
        .collect()
}

/// Resolve a fuzzy query to matching paths.
///
/// Handles:
/// - Absolute paths: /tmp/foo.py (if file exists)
/// - Extension patterns: .rs, .py (returns all matching files)
/// - Exact paths: src/myapp/dwim.py
/// - Partial filenames: dwim.py, dwim
/// - Directory names: myapp, src
///
/// **Note:** colon-paths like `src/main.py:MyClass` are silently truncated — only the
/// file component before `:` is resolved. Symbol resolution is left to the caller
/// (use [`resolve_unified`] if you need both file and symbol).
pub fn resolve(query: &str, root: &Path, path_source: Option<&dyn PathSource>) -> Vec<PathMatch> {
    // Handle absolute paths first - check if file exists directly
    if query.starts_with('/') {
        let abs_path = std::path::Path::new(query);
        if abs_path.is_file() {
            return vec![PathMatch {
                path: query.to_string(),
                kind: PathMatchKind::File,
                score: u32::MAX,
            }];
        } else if abs_path.is_dir() {
            return vec![PathMatch {
                path: query.to_string(),
                kind: PathMatchKind::Directory,
                score: u32::MAX,
            }];
        }
        // Absolute path doesn't exist - return empty
        return vec![];
    }

    // Handle file:symbol syntax (defer symbol resolution to Python for now)
    if query.contains(':') {
        // normalize-syntax-allow: rust/unwrap-in-impl - split(':') always yields at least one element
        let file_part = query.split(':').next().unwrap();
        return resolve(file_part, root, path_source);
    }

    // Handle extension patterns (e.g., ".rs", ".py") - return all matches directly
    if query.starts_with('.') && !query.contains('/') {
        if let Some(src) = path_source.as_ref()
            && let Some(files) = src.find_like(query)
        {
            return files
                .into_iter()
                .map(|e| PathMatch {
                    path: e.path,
                    kind: e.kind,
                    score: u32::MAX,
                })
                .collect();
        }
        // Fallback: walk filesystem for extension matches
        let walker = WalkBuilder::new(root)
            .hidden(false)
            .git_ignore(true)
            .git_global(true)
            .git_exclude(true)
            .filter_entry(|e| e.file_name() != ".git")
            .build();
        return walker
            .flatten()
            .filter_map(|entry| {
                let path = entry.path();
                if path.is_file() {
                    let path_str = path.to_string_lossy();
                    if path_str.ends_with(query)
                        && let Ok(rel) = path.strip_prefix(root)
                    {
                        return Some(PathMatch {
                            path: rel.to_string_lossy().to_string(),
                            kind: PathMatchKind::File,
                            score: u32::MAX,
                        });
                    }
                }
                None
            })
            .collect();
    }

    // Get candidate paths (uses LIKE for fast filtering when possible)
    let all_paths = get_paths_for_query(root, query, path_source);

    resolve_from_paths(query, &all_paths)
}

/// Get paths matching query using PathSource, fallback to all files
fn get_paths_for_query(
    root: &Path,
    query: &str,
    path_source: Option<&dyn PathSource>,
) -> Vec<(String, bool)> {
    if let Some(src) = path_source {
        // Try LIKE first for faster queries
        if !query.is_empty()
            && let Some(files) = src.find_like(query)
            && !files.is_empty()
        {
            return files
                .into_iter()
                .map(|e| (e.path, e.kind == PathMatchKind::Directory))
                .collect();
        }
        // Fall back to all files for empty query or no LIKE matches
        if let Some(files) = src.all_files() {
            return files
                .into_iter()
                .map(|e| (e.path, e.kind == PathMatchKind::Directory))
                .collect();
        }
    }
    // Fall back to filesystem walk
    let mut all_paths: Vec<(String, bool)> = Vec::new();
    let walker = WalkBuilder::new(root)
        .hidden(false)
        .git_ignore(true)
        .git_global(true)
        .git_exclude(true)
        .build();

    for entry in walker.flatten() {
        let path = entry.path();
        if let Ok(rel) = path.strip_prefix(root) {
            let rel_str = rel.to_string_lossy().to_string();
            // Skip empty paths and .git directory
            if rel_str.is_empty() || rel_str == ".git" || rel_str.starts_with(".git/") {
                continue;
            }
            let is_dir = path.is_dir();
            all_paths.push((rel_str, is_dir));
        }
    }

    all_paths
}

/// Normalize a char for comparison
#[inline]
fn normalize_char(c: char) -> char {
    match c {
        '-' | '.' | '_' => ' ',
        c => c.to_ascii_lowercase(),
    }
}

/// Compare two strings with normalization (no allocation)
fn eq_normalized(a: &str, b: &str) -> bool {
    let mut a_chars = a.chars().map(normalize_char);
    let mut b_chars = b.chars().map(normalize_char);
    loop {
        match (a_chars.next(), b_chars.next()) {
            (Some(ac), Some(bc)) if ac == bc => continue,
            (None, None) => return true,
            _ => return false,
        }
    }
}

/// Normalize string for comparison (used for filename matching)
fn normalize_for_match(s: &str) -> String {
    s.chars().map(normalize_char).collect()
}

/// Resolve from a pre-loaded list of paths
fn resolve_from_paths(query: &str, all_paths: &[(String, bool)]) -> Vec<PathMatch> {
    // Handle glob patterns (* and **)
    if query.contains('*') {
        let pattern = glob::Pattern::new(query).ok();
        if let Some(ref pat) = pattern {
            let mut glob_matches: Vec<PathMatch> = Vec::new();
            for (path, is_dir) in all_paths {
                if pat.matches(path) || pat.matches(&path.replace('\\', "/")) {
                    glob_matches.push(PathMatch {
                        path: path.clone(),
                        kind: if *is_dir {
                            PathMatchKind::Directory
                        } else {
                            PathMatchKind::File
                        },
                        score: u32::MAX,
                    });
                }
            }
            if !glob_matches.is_empty() {
                return glob_matches;
            }
        }
    }

    let query_lower = query.to_lowercase();
    let query_normalized = normalize_for_match(query);

    // Try normalized path match (handles exact match too, no allocation)
    for (path, is_dir) in all_paths {
        if eq_normalized(path, query) {
            return vec![PathMatch {
                path: path.clone(),
                kind: if *is_dir {
                    PathMatchKind::Directory
                } else {
                    PathMatchKind::File
                },
                score: u32::MAX,
            }];
        }
    }

    // Try exact filename/dirname match (case-insensitive, _ and - equivalent)
    let mut exact_matches: Vec<PathMatch> = Vec::new();
    for (path, is_dir) in all_paths {
        let name = Path::new(path)
            .file_name()
            .map(|n| n.to_string_lossy().to_lowercase())
            .unwrap_or_default();
        let stem = Path::new(path)
            .file_stem()
            .map(|n| n.to_string_lossy().to_lowercase())
            .unwrap_or_default();
        let name_normalized = normalize_for_match(&name);
        let stem_normalized = normalize_for_match(&stem);

        if name == query_lower
            || stem == query_lower
            || name_normalized == query_normalized
            || stem_normalized == query_normalized
        {
            exact_matches.push(PathMatch {
                path: path.clone(),
                kind: if *is_dir {
                    PathMatchKind::Directory
                } else {
                    PathMatchKind::File
                },
                score: u32::MAX - 1,
            });
        }
    }

    if !exact_matches.is_empty() {
        return exact_matches;
    }

    // Try suffix match (query is end of path)
    // e.g., "analyze/report.rs" matches "crates/myapp/src/commands/analyze/report.rs"
    if query.contains('/') || query.contains('\\') {
        let query_suffix = query.replace('\\', "/");
        let mut suffix_matches: Vec<PathMatch> = Vec::new();
        for (path, is_dir) in all_paths {
            let path_normalized = path.replace('\\', "/");
            if path_normalized.ends_with(&query_suffix)
                || path_normalized.ends_with(&format!("/{}", query_suffix))
            {
                suffix_matches.push(PathMatch {
                    path: path.clone(),
                    kind: if *is_dir {
                        PathMatchKind::Directory
                    } else {
                        PathMatchKind::File
                    },
                    score: u32::MAX - 2,
                });
            }
        }
        if !suffix_matches.is_empty() {
            return suffix_matches;
        }
    }

    // Fuzzy match using nucleo
    let mut matcher = Matcher::new(Config::DEFAULT);
    let pattern = Pattern::parse(query, CaseMatching::Ignore, Normalization::Smart);

    let mut fuzzy_matches: Vec<PathMatch> = Vec::new();

    for (path, is_dir) in all_paths {
        let mut buf = Vec::new();
        if let Some(score) =
            pattern.score(nucleo_matcher::Utf32Str::new(path, &mut buf), &mut matcher)
        {
            fuzzy_matches.push(PathMatch {
                path: path.clone(),
                kind: if *is_dir {
                    PathMatchKind::Directory
                } else {
                    PathMatchKind::File
                },
                score,
            });
        }
    }

    // Sort by score descending, take top 10
    fuzzy_matches.sort_by(|a, b| b.score.cmp(&a.score));
    fuzzy_matches.truncate(10);

    fuzzy_matches
}

/// Check if a pattern contains glob characters (* ? [)
pub fn is_glob_pattern(pattern: &str) -> bool {
    pattern.contains('*') || pattern.contains('?') || pattern.contains('[')
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::tempdir;

    fn no_aliases(_name: &str) -> Option<Vec<String>> {
        None
    }

    #[test]
    fn test_exact_match() {
        let dir = tempdir().unwrap();
        fs::create_dir_all(dir.path().join("src/myapp")).unwrap();
        fs::write(dir.path().join("src/myapp/cli.py"), "").unwrap();

        let matches = resolve("src/myapp/cli.py", dir.path(), None);
        assert_eq!(matches.len(), 1);
        assert_eq!(matches[0].path, "src/myapp/cli.py");
    }

    #[test]
    fn test_filename_match() {
        let dir = tempdir().unwrap();
        fs::create_dir_all(dir.path().join("src/myapp")).unwrap();
        fs::write(dir.path().join("src/myapp/dwim.py"), "").unwrap();

        let matches = resolve("dwim.py", dir.path(), None);
        assert_eq!(matches.len(), 1);
        assert_eq!(matches[0].path, "src/myapp/dwim.py");
    }

    #[test]
    fn test_stem_match() {
        let dir = tempdir().unwrap();
        fs::create_dir_all(dir.path().join("src/myapp")).unwrap();
        fs::write(dir.path().join("src/myapp/dwim.py"), "").unwrap();

        let matches = resolve("dwim", dir.path(), None);
        assert_eq!(matches.len(), 1);
        assert_eq!(matches[0].path, "src/myapp/dwim.py");
    }

    #[test]
    fn test_underscore_hyphen_equivalence() {
        let dir = tempdir().unwrap();
        fs::create_dir_all(dir.path().join("docs")).unwrap();
        fs::write(dir.path().join("docs/prior-art.md"), "").unwrap();

        // underscore query should match hyphen filename
        let matches = resolve("prior_art", dir.path(), None);
        assert_eq!(matches.len(), 1);
        assert_eq!(matches[0].path, "docs/prior-art.md");

        // hyphen query should also work
        let matches = resolve("prior-art", dir.path(), None);
        assert_eq!(matches.len(), 1);
        assert_eq!(matches[0].path, "docs/prior-art.md");

        // full path with underscores should match hyphenated path
        let matches = resolve("docs/prior_art.md", dir.path(), None);
        assert_eq!(matches.len(), 1);
        assert_eq!(matches[0].path, "docs/prior-art.md");
    }

    #[test]
    fn test_unified_path_file_only() {
        let dir = tempdir().unwrap();
        fs::create_dir_all(dir.path().join("src/myapp")).unwrap();
        fs::write(dir.path().join("src/myapp/cli.py"), "").unwrap();

        let result = resolve_unified("src/myapp/cli.py", dir.path(), &no_aliases, None);
        assert!(result.is_some());
        let u = result.unwrap();
        assert_eq!(u.file_path, "src/myapp/cli.py");
        assert!(u.symbol_path.is_empty());
        assert!(!u.is_directory);
    }

    #[test]
    fn test_unified_path_with_symbol() {
        let dir = tempdir().unwrap();
        fs::create_dir_all(dir.path().join("src/myapp")).unwrap();
        fs::write(dir.path().join("src/myapp/cli.py"), "").unwrap();

        // File with symbol path
        let result = resolve_unified("src/myapp/cli.py/Foo/bar", dir.path(), &no_aliases, None);
        assert!(result.is_some());
        let u = result.unwrap();
        assert_eq!(u.file_path, "src/myapp/cli.py");
        assert_eq!(u.symbol_path, vec!["Foo", "bar"]);
        assert!(!u.is_directory);
    }

    #[test]
    fn test_unified_path_directory() {
        let dir = tempdir().unwrap();
        fs::create_dir_all(dir.path().join("src/myapp")).unwrap();
        fs::write(dir.path().join("src/myapp/cli.py"), "").unwrap();

        let result = resolve_unified("src/myapp", dir.path(), &no_aliases, None);
        assert!(result.is_some());
        let u = result.unwrap();
        assert_eq!(u.file_path, "src/myapp");
        assert!(u.symbol_path.is_empty());
        assert!(u.is_directory);
    }

    #[test]
    fn test_unified_path_rust_style_separator() {
        let dir = tempdir().unwrap();
        fs::create_dir_all(dir.path().join("src/myapp")).unwrap();
        fs::write(dir.path().join("src/myapp/cli.py"), "").unwrap();

        // Rust-style :: separator
        let result = resolve_unified("src/myapp/cli.py::Foo::bar", dir.path(), &no_aliases, None);
        assert!(result.is_some());
        let u = result.unwrap();
        assert_eq!(u.file_path, "src/myapp/cli.py");
        assert_eq!(u.symbol_path, vec!["Foo", "bar"]);
    }

    #[test]
    fn test_unified_path_hash_separator() {
        let dir = tempdir().unwrap();
        fs::create_dir_all(dir.path().join("src/myapp")).unwrap();
        fs::write(dir.path().join("src/myapp/cli.py"), "").unwrap();

        // URL fragment-style # separator
        let result = resolve_unified("src/myapp/cli.py#Foo", dir.path(), &no_aliases, None);
        assert!(result.is_some());
        let u = result.unwrap();
        assert_eq!(u.file_path, "src/myapp/cli.py");
        assert_eq!(u.symbol_path, vec!["Foo"]);
    }

    #[test]
    fn test_unified_path_colon_separator() {
        let dir = tempdir().unwrap();
        fs::create_dir_all(dir.path().join("src/myapp")).unwrap();
        fs::write(dir.path().join("src/myapp/cli.py"), "").unwrap();

        // Compact : separator
        let result = resolve_unified("src/myapp/cli.py:Foo:bar", dir.path(), &no_aliases, None);
        assert!(result.is_some());
        let u = result.unwrap();
        assert_eq!(u.file_path, "src/myapp/cli.py");
        assert_eq!(u.symbol_path, vec!["Foo", "bar"]);
    }

    #[test]
    fn test_unified_path_fuzzy_file() {
        let dir = tempdir().unwrap();
        fs::create_dir_all(dir.path().join("src/myapp")).unwrap();
        fs::write(dir.path().join("src/myapp/cli.py"), "").unwrap();

        // Fuzzy file match with symbol
        let result = resolve_unified("cli.py/Foo", dir.path(), &no_aliases, None);
        assert!(result.is_some());
        let u = result.unwrap();
        assert_eq!(u.file_path, "src/myapp/cli.py");
        assert_eq!(u.symbol_path, vec!["Foo"]);
    }

    #[test]
    fn test_unified_path_absolute() {
        let dir = tempdir().unwrap();
        let abs_path = dir.path().join("test.py");
        fs::write(&abs_path, "def foo(): pass").unwrap();

        // Absolute path should resolve directly
        let abs_str = abs_path.to_string_lossy().to_string();
        let result = resolve_unified(&abs_str, Path::new("/some/other/root"), &no_aliases, None);
        assert!(result.is_some());
        let u = result.unwrap();
        assert_eq!(u.file_path, abs_str);
        assert!(u.symbol_path.is_empty());
        assert!(!u.is_directory);
    }

    #[test]
    fn test_unified_path_absolute_with_symbol() {
        let dir = tempdir().unwrap();
        let abs_path = dir.path().join("test.py");
        fs::write(&abs_path, "def foo(): pass").unwrap();

        // Absolute path with symbol
        let query = format!("{}/foo", abs_path.to_string_lossy());
        let result = resolve_unified(&query, Path::new("/some/other/root"), &no_aliases, None);
        assert!(result.is_some());
        let u = result.unwrap();
        assert_eq!(u.file_path, abs_path.to_string_lossy().to_string());
        assert_eq!(u.symbol_path, vec!["foo"]);
    }

    #[test]
    fn test_unified_path_unicode() {
        let dir = tempdir().unwrap();
        let unicode_dir = dir.path().join("日本語");
        fs::create_dir_all(&unicode_dir).unwrap();
        let unicode_file = unicode_dir.join("テスト.py");
        fs::write(&unicode_file, "def hello(): pass").unwrap();

        // Absolute unicode path
        let abs_str = unicode_file.to_string_lossy().to_string();
        let result = resolve_unified(&abs_str, Path::new("/some/other/root"), &no_aliases, None);
        assert!(result.is_some());
        let u = result.unwrap();
        assert_eq!(u.file_path, abs_str);
        assert!(!u.is_directory);
    }
}
