//! Core parsing coordination and project discovery functionality

use crate::cli;
use crate::debug::{log, log_debug, FeludaResult, LogLevel};
use crate::languages::{
    c::analyze_c_licenses, cpp::analyze_cpp_licenses, dotnet::analyze_dotnet_licenses,
    go::analyze_go_licenses, node::analyze_js_licenses_with_no_local,
    python::analyze_python_licenses, r::analyze_r_licenses,
    rust::analyze_rust_licenses_with_metadata,
};
use crate::languages::{Language, CPP_PATHS, C_PATHS, DOTNET_PATHS, PYTHON_PATHS, R_PATHS};
use crate::licenses::{
    detect_project_license, is_license_compatible, LicenseCompatibility, LicenseInfo,
};
use cargo_metadata::MetadataCommand;
use rayon::prelude::*;
use std::path::{Path, PathBuf};

/// Project root information
#[derive(Debug)]
struct ProjectRoot {
    pub path: PathBuf,
    pub project_type: Language,
}

/// Find project files only in the root directory (not recursive)
fn find_project_roots(root_path: impl AsRef<Path>) -> FeludaResult<Vec<ProjectRoot>> {
    let mut project_roots: Vec<ProjectRoot> = Vec::new();
    let root = root_path.as_ref();

    log(
        LogLevel::Info,
        &format!("Scanning for project files in: {}", root.display()),
    );

    // Only check files directly in the root directory, don't recurse into subdirectories
    if let Ok(entries) = std::fs::read_dir(root) {
        for entry in entries.filter_map(|e| e.ok()) {
            if let Ok(file_type) = entry.file_type() {
                if !file_type.is_file() {
                    continue;
                }
            } else {
                continue;
            }

            let path = entry.path();
            let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");

            if let Some(project_type) = Language::from_file_name(file_name) {
                log(
                    LogLevel::Info,
                    &format!(
                        "Found project file: {} ({:?})",
                        path.display(),
                        project_type
                    ),
                );

                // Several markers can map to the same language (e.g. go.mod and go.work). Keep
                // one entry per language per directory; analyzers handle workspace resolution.
                let already_seen = project_roots.iter().any(|existing| {
                    existing.path == root
                        && std::mem::discriminant(&existing.project_type)
                            == std::mem::discriminant(&project_type)
                });
                if already_seen {
                    log(
                        LogLevel::Info,
                        &format!("Skipping duplicate language entry for: {}", path.display()),
                    );
                    continue;
                }

                project_roots.push(ProjectRoot {
                    path: root.to_path_buf(),
                    project_type,
                });
            }
        }
    }

    log(
        LogLevel::Info,
        &format!("Found {} project roots", project_roots.len()),
    );
    log_debug("Project roots", &project_roots);

    Ok(project_roots)
}

/// Check which C project file exists in the given path
fn check_which_c_file_exists(project_path: impl AsRef<Path>) -> Option<String> {
    for &path in C_PATHS.iter() {
        let full_path = Path::new(project_path.as_ref()).join(path);
        if full_path.exists() {
            log(
                LogLevel::Info,
                &format!("Found C project file: {}", full_path.display()),
            );
            return Some(path.to_string());
        }
    }

    log(
        LogLevel::Warn,
        &format!(
            "No C project file found in: {}",
            project_path.as_ref().display()
        ),
    );
    None
}

/// Check which C++ project file exists in the given path
fn check_which_cpp_file_exists(project_path: impl AsRef<Path>) -> Option<String> {
    for &path in CPP_PATHS.iter() {
        let full_path = Path::new(project_path.as_ref()).join(path);
        if full_path.exists() {
            log(
                LogLevel::Info,
                &format!("Found C++ project file: {}", full_path.display()),
            );
            return Some(path.to_string());
        }
    }

    log(
        LogLevel::Warn,
        &format!(
            "No C++ project file found in: {}",
            project_path.as_ref().display()
        ),
    );
    None
}

/// Check which Python project file exists in the given path
fn check_which_python_file_exists(project_path: impl AsRef<Path>) -> Option<String> {
    for &path in PYTHON_PATHS.iter() {
        let full_path = Path::new(project_path.as_ref()).join(path);
        if full_path.exists() {
            log(
                LogLevel::Info,
                &format!("Found Python project file: {}", full_path.display()),
            );
            return Some(path.to_string());
        }
    }

    log(
        LogLevel::Warn,
        &format!(
            "No Python project file found in: {}",
            project_path.as_ref().display()
        ),
    );
    None
}

/// Check which R project file exists in the given path
fn check_which_r_file_exists(project_path: impl AsRef<Path>) -> Option<String> {
    for &path in R_PATHS.iter() {
        let full_path = Path::new(project_path.as_ref()).join(path);
        if full_path.exists() {
            log(
                LogLevel::Info,
                &format!("Found R project file: {}", full_path.display()),
            );
            return Some(path.to_string());
        }
    }

    log(
        LogLevel::Warn,
        &format!(
            "No R project file found in: {}",
            project_path.as_ref().display()
        ),
    );
    None
}

fn check_which_dotnet_file_exists(project_path: impl AsRef<Path>) -> Option<String> {
    for &path in DOTNET_PATHS.iter() {
        if path.starts_with('.') {
            if let Ok(entries) = std::fs::read_dir(project_path.as_ref()) {
                for entry in entries.filter_map(|e| e.ok()) {
                    if let Some(file_name) = entry.file_name().to_str() {
                        if file_name.ends_with(path) {
                            log(
                                LogLevel::Info,
                                &format!("Found .NET project file: {}", entry.path().display()),
                            );
                            return Some(file_name.to_string());
                        }
                    }
                }
            }
        } else {
            let full_path = Path::new(project_path.as_ref()).join(path);
            if full_path.exists() {
                log(
                    LogLevel::Info,
                    &format!("Found .NET project file: {}", full_path.display()),
                );
                return Some(path.to_string());
            }
        }
    }

    log(
        LogLevel::Warn,
        &format!(
            "No .NET project file found in: {}",
            project_path.as_ref().display()
        ),
    );
    None
}

/// Main entry point for parsing project dependencies
pub fn parse_root(
    root_path: impl AsRef<Path>,
    language: Option<&str>,
    strict: bool,
    no_local: bool,
) -> FeludaResult<Vec<LicenseInfo>> {
    let mut config = crate::config::load_config()?;
    config.strict = strict;
    parse_root_with_config(root_path, language, &config, no_local)
}

/// Main entry point for parsing project dependencies
pub fn parse_root_with_config(
    root_path: impl AsRef<Path>,
    language: Option<&str>,
    config: &crate::config::FeludaConfig,
    no_local: bool,
) -> FeludaResult<Vec<LicenseInfo>> {
    log(
        LogLevel::Info,
        &format!("Parsing root path: {}", root_path.as_ref().display()),
    );
    if let Some(lang) = language {
        log(LogLevel::Info, &format!("Filtering by language: {lang}"));
    }

    let project_roots = find_project_roots(&root_path)?;

    if project_roots.is_empty() {
        log(
            LogLevel::Warn,
            "No project files found in the specified path",
        );
        println!(
            "❌ No supported project files found.\n\
            Feluda supports: C, C++, .NET, Rust, Node.js, Go, Python, R"
        );
        return Ok(Vec::new());
    }

    let licenses: Vec<LicenseInfo> = project_roots
        .into_par_iter()
        .filter_map(|root| {
            if let Some(language) = language {
                if !matches_language(root.project_type, language) {
                    log(
                        LogLevel::Info,
                        &format!(
                            "Skipping {:?} project (language filter: {})",
                            root.project_type, language
                        ),
                    );
                    return None;
                }
            }

            match parse_dependencies(&root, config, no_local) {
                Ok(deps) => {
                    log(
                        LogLevel::Info,
                        &format!(
                            "Found {} dependencies in {}",
                            deps.len(),
                            root.path.display()
                        ),
                    );
                    Some(deps)
                }
                Err(err) => {
                    log(
                        LogLevel::Error,
                        &format!(
                            "Error parsing dependencies in {}: {}",
                            root.path.display(),
                            err
                        ),
                    );
                    None
                }
            }
        })
        .flatten()
        .collect();

    log(
        LogLevel::Info,
        &format!("Total dependencies found: {}", licenses.len()),
    );

    // Filter out ignored licenses
    let mut licenses = licenses;
    let ignored_count = licenses.len();
    licenses.retain(|license| !crate::licenses::is_license_ignored(license.license.as_deref()));
    let filtered_count = licenses.len();
    if ignored_count != filtered_count {
        log(
            LogLevel::Info,
            &format!(
                "Filtered out {} ignored licenses, {} remaining",
                ignored_count - filtered_count,
                filtered_count
            ),
        );
    }

    // Filter out ignored dependencies based on configuration
    let ignored_count = licenses.len();
    licenses.retain(|dep| {
        !config
            .dependencies
            .should_ignore_dependency(&dep.name, Some(&dep.version))
    });
    let filtered_count = licenses.len();
    if ignored_count != filtered_count {
        log(
            LogLevel::Info,
            &format!(
                "Filtered out {} ignored dependencies, {} remaining",
                ignored_count - filtered_count,
                filtered_count
            ),
        );
    }

    // Set license compatibility based on project license
    let project_license =
        detect_project_license(root_path.as_ref().to_str().unwrap_or("")).unwrap_or(None);

    set_license_compatibility(&mut licenses, &project_license);

    Ok(licenses)
}

/// Set license compatibility for all dependencies
fn set_license_compatibility(licenses: &mut [LicenseInfo], project_license: &Option<String>) {
    for license in licenses {
        license.compatibility = match (project_license, &license.license) {
            (Some(proj_license), Some(dep_license)) => {
                is_license_compatible(dep_license, proj_license, false)
            }
            _ => LicenseCompatibility::Unknown,
        };
    }
}

/// Check if a project type matches the given language filter
fn matches_language(project_type: Language, language: &str) -> bool {
    matches!(
        (project_type, language.to_lowercase().as_str()),
        (Language::C(_), "c")
            | (Language::Cpp(_), "cpp" | "c++")
            | (
                Language::DotNet(_),
                "dotnet" | ".net" | "csharp" | "c#" | "fsharp" | "f#"
            )
            | (Language::Rust(_), "rust")
            | (Language::Node(_), "node")
            | (Language::Go(_), "go")
            | (Language::Python(_), "python")
            | (Language::R(_), "r")
    )
}

/// Parse dependencies based on the project type
fn parse_dependencies(
    root: &ProjectRoot,
    config: &crate::config::FeludaConfig,
    no_local: bool,
) -> FeludaResult<Vec<LicenseInfo>> {
    let project_path = &root.path;
    let project_type = root.project_type;

    let licenses = cli::with_spinner(&format!("🔎: {}", project_path.display()), |indicator| {
        match project_type {
            Language::Rust(_) => {
                let project_path = Path::new(project_path).join("Cargo.toml");
                log(
                    LogLevel::Info,
                    &format!("Parsing Rust project: {}", project_path.display()),
                );

                indicator.update_progress("analyzing Cargo.toml");

                match MetadataCommand::new()
                    .manifest_path(Path::new(&project_path))
                    .exec()
                {
                    Ok(metadata) => {
                        log(
                            LogLevel::Info,
                            &format!("Found {} packages in Rust project", metadata.packages.len()),
                        );
                        let workspace_size = metadata.workspace_members.len();
                        indicator.update_progress(&format!(
                            "found {} packages ({} workspace member{})",
                            metadata.packages.len(),
                            workspace_size,
                            if workspace_size == 1 { "" } else { "s" }
                        ));

                        analyze_rust_licenses_with_metadata(metadata, config, no_local)
                    }
                    Err(err) => {
                        log(
                            LogLevel::Error,
                            &format!("Failed to fetch cargo metadata: {err}"),
                        );
                        Vec::new()
                    }
                }
            }
            Language::Node(_) => {
                let project_path = Path::new(project_path).join("package.json");
                log(
                    LogLevel::Info,
                    &format!("Parsing Node.js project: {}", project_path.display()),
                );

                indicator.update_progress("analyzing package.json");

                match project_path.to_str() {
                    Some(path_str) => {
                        let deps = analyze_js_licenses_with_no_local(path_str, no_local);
                        indicator.update_progress(&format!("found {} dependencies", deps.len()));
                        deps
                    }
                    None => {
                        log(LogLevel::Error, "Failed to convert Node.js path to string");
                        Vec::new()
                    }
                }
            }
            Language::Go(_) => {
                let go_work_path = Path::new(project_path).join("go.work");
                if go_work_path.exists() {
                    log(
                        LogLevel::Info,
                        &format!("Parsing Go workspace: {}", go_work_path.display()),
                    );
                    indicator.update_progress("analyzing go.work");

                    let deps =
                        crate::languages::go::analyze_go_workspace_licenses(project_path, config);
                    indicator.update_progress(&format!("found {} dependencies", deps.len()));
                    deps
                } else {
                    let project_path = Path::new(project_path).join("go.mod");
                    log(
                        LogLevel::Info,
                        &format!("Parsing Go project: {}", project_path.display()),
                    );

                    indicator.update_progress("analyzing go.mod");

                    match project_path.to_str() {
                        Some(path_str) => {
                            let deps = analyze_go_licenses(path_str, config);
                            indicator
                                .update_progress(&format!("found {} dependencies", deps.len()));
                            deps
                        }
                        None => {
                            log(LogLevel::Error, "Failed to convert Go path to string");
                            Vec::new()
                        }
                    }
                }
            }
            Language::Python(_) => match check_which_python_file_exists(project_path) {
                Some(python_package_file) => {
                    let project_path = Path::new(project_path).join(&python_package_file);
                    log(
                        LogLevel::Info,
                        &format!("Parsing Python project: {}", project_path.display()),
                    );

                    indicator.update_progress(&format!("analyzing {python_package_file}"));

                    match project_path.to_str() {
                        Some(path_str) => {
                            let deps = analyze_python_licenses(path_str, config);
                            indicator
                                .update_progress(&format!("found {} dependencies", deps.len()));
                            deps
                        }
                        None => {
                            log(LogLevel::Error, "Failed to convert Python path to string");
                            Vec::new()
                        }
                    }
                }
                None => {
                    log(LogLevel::Error, "Python package file not found");
                    Vec::new()
                }
            },
            Language::C(_) => match check_which_c_file_exists(project_path) {
                Some(c_build_file) => {
                    let project_path = Path::new(project_path).join(&c_build_file);
                    log(
                        LogLevel::Info,
                        &format!("Parsing C project: {}", project_path.display()),
                    );

                    indicator.update_progress(&format!("analyzing {c_build_file}"));

                    match project_path.to_str() {
                        Some(path_str) => {
                            let deps = analyze_c_licenses(path_str, config);
                            indicator
                                .update_progress(&format!("found {} dependencies", deps.len()));
                            deps
                        }
                        None => {
                            log(LogLevel::Error, "Failed to convert C path to string");
                            Vec::new()
                        }
                    }
                }
                None => {
                    log(LogLevel::Error, "C build file not found");
                    Vec::new()
                }
            },
            Language::Cpp(_) => match check_which_cpp_file_exists(project_path) {
                Some(cpp_build_file) => {
                    let project_path = Path::new(project_path).join(&cpp_build_file);
                    log(
                        LogLevel::Info,
                        &format!("Parsing C++ project: {}", project_path.display()),
                    );

                    indicator.update_progress(&format!("analyzing {cpp_build_file}"));

                    match project_path.to_str() {
                        Some(path_str) => {
                            let deps = analyze_cpp_licenses(path_str, config);
                            indicator
                                .update_progress(&format!("found {} dependencies", deps.len()));
                            deps
                        }
                        None => {
                            log(LogLevel::Error, "Failed to convert C++ path to string");
                            Vec::new()
                        }
                    }
                }
                None => {
                    log(LogLevel::Error, "C++ build file not found");
                    Vec::new()
                }
            },
            Language::DotNet(_) => match check_which_dotnet_file_exists(project_path) {
                Some(dotnet_project_file) => {
                    let project_path = Path::new(project_path).join(&dotnet_project_file);
                    log(
                        LogLevel::Info,
                        &format!("Parsing .NET project: {}", project_path.display()),
                    );

                    indicator.update_progress(&format!("analyzing {dotnet_project_file}"));

                    match project_path.to_str() {
                        Some(path_str) => {
                            let deps = analyze_dotnet_licenses(path_str, config);
                            indicator
                                .update_progress(&format!("found {} dependencies", deps.len()));
                            deps
                        }
                        None => {
                            log(LogLevel::Error, "Failed to convert .NET path to string");
                            Vec::new()
                        }
                    }
                }
                None => {
                    log(LogLevel::Error, ".NET project file not found");
                    Vec::new()
                }
            },
            Language::R(_) => match check_which_r_file_exists(project_path) {
                Some(r_package_file) => {
                    let project_path = Path::new(project_path).join(&r_package_file);
                    log(
                        LogLevel::Info,
                        &format!("Parsing R project: {}", project_path.display()),
                    );

                    indicator.update_progress(&format!("analyzing {r_package_file}"));

                    match project_path.to_str() {
                        Some(path_str) => {
                            let deps = analyze_r_licenses(path_str, config);
                            indicator
                                .update_progress(&format!("found {} dependencies", deps.len()));
                            deps
                        }
                        None => {
                            log(LogLevel::Error, "Failed to convert R path to string");
                            Vec::new()
                        }
                    }
                }
                None => {
                    log(LogLevel::Error, "R package file not found");
                    Vec::new()
                }
            },
        }
    });

    Ok(licenses)
}

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

    #[test]
    fn test_matches_language() {
        assert!(matches_language(Language::C(&C_PATHS), "c"));
        assert!(matches_language(Language::C(&C_PATHS), "C"));

        assert!(matches_language(Language::Cpp(&CPP_PATHS), "cpp"));
        assert!(matches_language(Language::Cpp(&CPP_PATHS), "c++"));
        assert!(matches_language(Language::Cpp(&CPP_PATHS), "CPP"));

        assert!(matches_language(Language::Rust("Cargo.toml"), "rust"));
        assert!(matches_language(Language::Rust("Cargo.toml"), "RUST"));
        assert!(matches_language(Language::Rust("Cargo.toml"), "Rust"));

        assert!(matches_language(Language::Node("package.json"), "node"));
        assert!(matches_language(Language::Node("package.json"), "NODE"));
        assert!(matches_language(Language::Node("package.json"), "Node"));

        assert!(matches_language(Language::Go("go.mod"), "go"));
        assert!(matches_language(Language::Go("go.mod"), "GO"));
        assert!(matches_language(Language::Go("go.mod"), "Go"));

        assert!(matches_language(Language::Python(&PYTHON_PATHS), "python"));
        assert!(matches_language(Language::Python(&PYTHON_PATHS), "PYTHON"));
        assert!(matches_language(Language::Python(&PYTHON_PATHS), "Python"));

        assert!(!matches_language(Language::Rust("Cargo.toml"), "node"));
        assert!(!matches_language(Language::Node("package.json"), "python"));
        assert!(!matches_language(Language::Go("go.mod"), "rust"));
        assert!(!matches_language(Language::Python(&PYTHON_PATHS), "go"));
        assert!(!matches_language(Language::C(&C_PATHS), "cpp"));
        assert!(!matches_language(Language::Cpp(&CPP_PATHS), "c"));

        assert!(!matches_language(Language::Rust("Cargo.toml"), "java"));
        assert!(!matches_language(Language::Node("package.json"), "java"));
    }

    #[test]
    fn test_check_which_python_file_exists() {
        let temp_dir = tempfile::TempDir::new().unwrap();

        // Test when no Python files exist
        let result = check_which_python_file_exists(temp_dir.path());
        assert_eq!(result, None);

        // Test when requirements.txt exists
        std::fs::write(temp_dir.path().join("requirements.txt"), "requests==2.28.1").unwrap();
        let result = check_which_python_file_exists(temp_dir.path());
        assert_eq!(result, Some("requirements.txt".to_string()));

        // Test when multiple Python files exist
        std::fs::write(
            temp_dir.path().join("pyproject.toml"),
            "[project]\nname = \"test\"",
        )
        .unwrap();
        std::fs::write(temp_dir.path().join("Pipfile.lock"), "{}").unwrap();
        let result = check_which_python_file_exists(temp_dir.path());
        assert_eq!(result, Some("requirements.txt".to_string()));
    }

    #[test]
    fn test_find_project_roots_empty_directory() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let result = find_project_roots(temp_dir.path().to_str().unwrap()).unwrap();
        assert!(result.is_empty());
    }

    #[test]
    fn test_find_project_roots_single_project() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let root_path = temp_dir.path();

        // Create a single Rust project
        std::fs::write(root_path.join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();

        let result = find_project_roots(root_path.to_str().unwrap()).unwrap();
        assert_eq!(result.len(), 1);
        assert_eq!(result[0].project_type, Language::Rust("Cargo.toml"));
        assert_eq!(result[0].path, root_path);
    }

    #[test]
    fn test_parse_root_with_language_filter() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let root_path = temp_dir.path();

        // Create multiple project types
        std::fs::write(root_path.join("package.json"), r#"{"name": "test"}"#).unwrap();
        std::fs::write(root_path.join("go.mod"), "module test").unwrap();
        std::fs::write(root_path.join("requirements.txt"), "# No dependencies").unwrap();

        // Test filtering by node
        let result = parse_root(root_path, Some("node"), false, false);
        assert!(result.is_ok());

        // Test filtering by go
        let result = parse_root(root_path, Some("go"), false, false);
        assert!(result.is_ok());

        // Test filtering by python
        let result = parse_root(root_path, Some("python"), false, false);
        assert!(result.is_ok());

        // Test filtering by non-existent language
        let result = parse_root(root_path, Some("java"), false, false);
        assert!(result.is_ok());
        let licenses = result.unwrap();
        assert!(licenses.is_empty());

        // Test case-insensitive filtering
        let result = parse_root(root_path, Some("NODE"), false, false);
        assert!(result.is_ok());

        let result = parse_root(root_path, Some("Python"), false, false);
        assert!(result.is_ok());
    }

    #[test]
    fn test_parse_root_no_projects() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let result = parse_root(temp_dir.path(), None, false, false).unwrap();
        assert!(result.is_empty());
    }

    #[test]
    fn test_parse_root_all_languages() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let root_path = temp_dir.path();

        // Create project files for all supported languages
        std::fs::write(
            root_path.join("Cargo.toml"),
            "[package]\nname = \"test\"\nversion = \"0.1.0\"",
        )
        .unwrap();
        std::fs::write(
            root_path.join("package.json"),
            r#"{"name": "test", "version": "1.0.0"}"#,
        )
        .unwrap();
        std::fs::write(root_path.join("go.mod"), "module test\n\ngo 1.19").unwrap();
        std::fs::write(root_path.join("requirements.txt"), "# No dependencies").unwrap();

        let result = parse_root(root_path, None, false, false);
        assert!(result.is_ok());
    }

    #[test]
    fn test_project_root_debug() {
        let project_root = ProjectRoot {
            path: std::path::PathBuf::from("/test/path"),
            project_type: Language::Rust("Cargo.toml"),
        };

        let debug_str = format!("{project_root:?}");
        assert!(debug_str.contains("/test/path"));
        assert!(debug_str.contains("Rust"));
        assert!(debug_str.contains("Cargo.toml"));
    }

    #[test]
    fn test_find_project_roots_nested_projects() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let root_path = temp_dir.path();

        // Create nested structure
        let rust_dir = root_path.join("rust_project");
        let node_dir = root_path.join("node_project").join("nested");
        std::fs::create_dir_all(&rust_dir).unwrap();
        std::fs::create_dir_all(&node_dir).unwrap();

        // Create project files
        std::fs::write(rust_dir.join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
        std::fs::write(node_dir.join("package.json"), "{}").unwrap();
        std::fs::write(root_path.join("go.mod"), "module test").unwrap();

        let result = find_project_roots(root_path.to_str().unwrap()).unwrap();
        // Only finds go.mod in root directory (non-recursive scanning)
        assert_eq!(result.len(), 1);

        let project_types: Vec<_> = result.iter().map(|r| r.project_type).collect();
        assert!(project_types.contains(&Language::Go("go.mod")));
    }

    #[test]
    fn test_parse_dependencies_error_handling() {
        let temp_dir = tempfile::TempDir::new().unwrap();

        // Test with invalid Rust project (missing lib.rs)
        let rust_project_root = ProjectRoot {
            path: temp_dir.path().to_path_buf(),
            project_type: Language::Rust("Cargo.toml"),
        };

        // Create Cargo.toml without lib.rs
        std::fs::write(
            temp_dir.path().join("Cargo.toml"),
            "[package]\nname = \"test\"\nversion = \"0.1.0\"\n[dependencies]\nserde = \"1.0\"",
        )
        .unwrap();

        let config = crate::config::FeludaConfig::default();
        let result = parse_dependencies(&rust_project_root, &config, false);
        assert!(result.is_ok());
        let licenses = result.unwrap();
        assert!(licenses.is_empty());
    }

    #[test]
    fn test_parse_dependencies_node_invalid_json() {
        let temp_dir = tempfile::TempDir::new().unwrap();

        let node_project_root = ProjectRoot {
            path: temp_dir.path().to_path_buf(),
            project_type: Language::Node("package.json"),
        };

        // Create invalid package.json
        std::fs::write(temp_dir.path().join("package.json"), "invalid json content").unwrap();

        let config = crate::config::FeludaConfig::default();
        let result = parse_dependencies(&node_project_root, &config, false);
        assert!(result.is_ok());
        let licenses = result.unwrap();
        assert!(licenses.is_empty());
    }

    #[test]
    fn test_parse_dependencies_python_no_dependencies() {
        let temp_dir = tempfile::TempDir::new().unwrap();

        let python_project_root = ProjectRoot {
            path: temp_dir.path().to_path_buf(),
            project_type: Language::Python(&PYTHON_PATHS),
        };

        // Create empty requirements.txt
        std::fs::write(temp_dir.path().join("requirements.txt"), "").unwrap();

        let config = crate::config::FeludaConfig::default();
        let result = parse_dependencies(&python_project_root, &config, false);
        assert!(result.is_ok());
        let licenses = result.unwrap();
        assert!(licenses.is_empty());
    }

    #[test]
    fn test_parse_root_invalid_path() {
        let result = parse_root("/definitely/nonexistent/path", None, false, false);
        assert!(result.is_ok());
        let licenses = result.unwrap();
        assert!(licenses.is_empty());
    }

    #[test]
    fn test_find_project_roots_dedupes_go_workspace_markers() {
        // Both go.mod and go.work in the same directory should produce a single Go entry,
        // not two — the analyzer decides which strategy to use.
        let temp = tempfile::TempDir::new().unwrap();
        let root = temp.path();
        std::fs::write(root.join("go.mod"), "module example/root\n").unwrap();
        std::fs::write(root.join("go.work"), "go 1.22\n\nuse ./svc\n").unwrap();

        let result = find_project_roots(root.to_str().unwrap()).unwrap();
        let go_count = result
            .iter()
            .filter(|r| {
                std::mem::discriminant(&r.project_type)
                    == std::mem::discriminant(&Language::Go("go.mod"))
            })
            .count();
        assert_eq!(go_count, 1, "expected exactly one Go project root entry");
    }

    #[test]
    fn test_find_project_roots_finds_go_work_alone() {
        // A go.work without a sibling go.mod should still register a Go project root.
        let temp = tempfile::TempDir::new().unwrap();
        std::fs::write(temp.path().join("go.work"), "go 1.22\n\nuse ./svc\n").unwrap();

        let result = find_project_roots(temp.path().to_str().unwrap()).unwrap();
        assert_eq!(result.len(), 1);
        assert_eq!(
            std::mem::discriminant(&result[0].project_type),
            std::mem::discriminant(&Language::Go("go.work"))
        );
    }
}
