use anyhow::Result;
use globset::Glob;
use std::path::PathBuf;

use super::PredicateEvaluator;
use crate::evaluator::{FileContext, MatchResult};
use crate::parser::PredicateKey;

pub(super) struct InPathEvaluator;

impl PredicateEvaluator for InPathEvaluator {
    fn evaluate(
        &self,
        context: &mut FileContext,
        _key: &PredicateKey,
        value: &str,
    ) -> Result<MatchResult> {
        // Check for glob metacharacters to switch between logic paths.
        if value.contains('*') || value.contains('?') || value.contains('[') || value.contains('{')
        {
            let glob = Glob::new(value)?.compile_matcher();
            let relative_path = context
                .root_relative_path()
                .unwrap_or(context.resolved_path());
            if glob.is_match(relative_path) {
                return Ok(MatchResult::Boolean(true));
            }

            if let Some(parent) = context.resolved_path().parent() {
                let relative_parent = parent.strip_prefix(context.root_path()).unwrap_or(parent);
                Ok(MatchResult::Boolean(glob.is_match(relative_parent)))
            } else {
                Ok(MatchResult::Boolean(false))
            }
        } else {
            // --- Non-recursive Exact-Path Logic ---
            let target_dir = PathBuf::from(value);
            let absolute_target_dir = if target_dir.is_absolute() {
                target_dir
            } else {
                context.root_path().join(target_dir)
            };

            let canonical_target = match context.normalized_path(&absolute_target_dir) {
                Ok(identity) => identity.resolved_path,
                Err(_) => return Ok(MatchResult::Boolean(false)),
            };
            let Ok(target_metadata) = context.backend().stat(&canonical_target) else {
                return Ok(MatchResult::Boolean(false));
            };
            if target_metadata.file_type != crate::backend::BackendFileType::Directory {
                return Ok(MatchResult::Boolean(false));
            }

            if let Some(file_parent) = context.resolved_path().parent() {
                let canonical_file_parent = match context.normalized_path(file_parent) {
                    Ok(identity) => identity.resolved_path,
                    Err(_) => return Ok(MatchResult::Boolean(false)),
                };
                Ok(MatchResult::Boolean(
                    canonical_file_parent == canonical_target,
                ))
            } else {
                Ok(MatchResult::Boolean(false))
            }
        }
    }
}

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

    #[test]
    fn test_in_path_evaluator_exact() -> Result<()> {
        let evaluator = InPathEvaluator;

        // Create a temporary directory structure
        let root_dir = tempdir()?;
        let root_path = root_dir.path();

        let project_dir = root_path.join("project");
        let src_dir = project_dir.join("src");
        let other_project_dir = root_path.join("other_project");
        fs::create_dir_all(&src_dir)?;
        fs::create_dir_all(&other_project_dir)?;

        let main_rs_path = src_dir.join("main.rs");
        fs::write(&main_rs_path, "fn main() {}")?;

        // --- Test Cases ---

        let mut context = FileContext::new(main_rs_path.clone(), root_path.to_path_buf());

        // 1. Absolute Path: Exact parent directory. Should match.
        assert!(evaluator
            .evaluate(&mut context, &PredicateKey::In, src_dir.to_str().unwrap())?
            .is_match());

        // 2. Absolute Path: Grandparent directory. Should NOT match due to non-recursive logic.
        assert!(!evaluator
            .evaluate(
                &mut context,
                &PredicateKey::In,
                project_dir.to_str().unwrap()
            )?
            .is_match());

        // 3. Absolute Path: Non-matching directory.
        assert!(!evaluator
            .evaluate(
                &mut context,
                &PredicateKey::In,
                other_project_dir.to_str().unwrap()
            )?
            .is_match());

        // 4. Relative Path: from the root. Should match.
        assert!(evaluator
            .evaluate(&mut context, &PredicateKey::In, "project/src")?
            .is_match());

        // 5. Relative Path: with dot-slash. Should match.
        assert!(evaluator
            .evaluate(&mut context, &PredicateKey::In, "./project/src")?
            .is_match());

        // 6. Relative Path: non-matching.
        assert!(!evaluator
            .evaluate(&mut context, &PredicateKey::In, "other_project")?
            .is_match());

        // 7. A file path is not a directory. Should not match.
        assert!(!evaluator
            .evaluate(
                &mut context,
                &PredicateKey::In,
                main_rs_path.to_str().unwrap()
            )?
            .is_match());

        // 8. Non-existent directory should not error, just return false.
        assert!(!evaluator
            .evaluate(&mut context, &PredicateKey::In, "non_existent_dir")?
            .is_match());

        Ok(())
    }

    #[test]
    fn test_in_path_evaluator_wildcard() -> Result<()> {
        let evaluator = InPathEvaluator;

        // Create a temporary directory structure
        let root_dir = tempdir()?;
        let root_path = root_dir.path();

        let project_a_src = root_path.join("project_a").join("src");
        let project_b_source = root_path.join("project_b").join("source");
        let other_src = root_path.join("other").join("src");
        fs::create_dir_all(&project_a_src)?;
        fs::create_dir_all(&project_b_source)?;
        fs::create_dir_all(&other_src)?;

        let file_a = project_a_src.join("main.rs");
        fs::write(&file_a, "")?;
        let file_b = project_b_source.join("lib.rs");
        fs::write(&file_b, "")?;
        let file_c = other_src.join("component.js");
        fs::write(&file_c, "")?;

        let mut context_a = FileContext::new(file_a, root_path.to_path_buf());
        let mut context_b = FileContext::new(file_b, root_path.to_path_buf());
        let mut context_c = FileContext::new(file_c, root_path.to_path_buf());

        // --- Test Cases ---

        // 1. `**/src` should match files in any `src` directory
        assert!(evaluator
            .evaluate(&mut context_a, &PredicateKey::In, "**/src")?
            .is_match());
        assert!(!evaluator
            .evaluate(&mut context_b, &PredicateKey::In, "**/src")?
            .is_match());
        assert!(evaluator
            .evaluate(&mut context_c, &PredicateKey::In, "**/src")?
            .is_match());

        // 2. `project_*/src` glob should match relative to the root.
        assert!(evaluator
            .evaluate(&mut context_a, &PredicateKey::In, "project_a/src")?
            .is_match());
        assert!(!evaluator
            .evaluate(&mut context_b, &PredicateKey::In, "project_*/src")?
            .is_match());
        assert!(!evaluator
            .evaluate(&mut context_c, &PredicateKey::In, "project_*/src")?
            .is_match());

        // 3. More specific glob `**/project_a/s?c`
        assert!(evaluator
            .evaluate(&mut context_a, &PredicateKey::In, "**/project_a/s?c")?
            .is_match());
        assert!(!evaluator
            .evaluate(&mut context_b, &PredicateKey::In, "**/project_a/s?c")?
            .is_match());

        // 4. Glob that should not match anything
        assert!(!evaluator
            .evaluate(&mut context_a, &PredicateKey::In, "**/test")?
            .is_match());

        // 5. Glob matching a different directory `**/so*ce`
        assert!(!evaluator
            .evaluate(&mut context_a, &PredicateKey::In, "**/so*ce")?
            .is_match());
        assert!(evaluator
            .evaluate(&mut context_b, &PredicateKey::In, "**/so*ce")?
            .is_match());
        assert!(!evaluator
            .evaluate(&mut context_c, &PredicateKey::In, "**/so*ce")?
            .is_match());

        Ok(())
    }

    #[test]
    fn test_in_path_evaluator_relative_wildcard() -> Result<()> {
        let evaluator = InPathEvaluator;

        // Create a temporary directory structure
        let root_dir = tempdir()?;
        let root_path = root_dir.path();

        let project_a_src = root_path.join("project_a").join("src");
        fs::create_dir_all(&project_a_src)?;

        let file_a = project_a_src.join("main.rs");
        fs::write(&file_a, "")?;

        // The context's root is the temporary directory we created.
        let mut context_a = FileContext::new(file_a, root_path.to_path_buf());

        // This glob is relative to the context's root.
        // It should match `.../project_a/src`
        assert!(evaluator
            .evaluate(&mut context_a, &PredicateKey::In, "project_a/*")?
            .is_match());

        // This glob should not match.
        assert!(!evaluator
            .evaluate(&mut context_a, &PredicateKey::In, "project_b/*")?
            .is_match());

        // This glob should also match.
        assert!(evaluator
            .evaluate(&mut context_a, &PredicateKey::In, "project_a/s?c")?
            .is_match());

        Ok(())
    }

    #[test]
    fn test_in_path_evaluator_advanced_wildcards() -> Result<()> {
        let evaluator = InPathEvaluator;
        let root_dir = tempdir()?;
        let root_path = root_dir.path();

        // Structure:
        //  /src/api/routes.js
        //  /src/db/connect.js
        //  /lib/auth/token.js
        //  /lib/deep/down/utils.js
        let src_api = root_path.join("src").join("api");
        let src_db = root_path.join("src").join("db");
        let lib_auth = root_path.join("lib").join("auth");
        let lib_deep = root_path.join("lib").join("deep").join("down");
        fs::create_dir_all(&src_api)?;
        fs::create_dir_all(&src_db)?;
        fs::create_dir_all(&lib_auth)?;
        fs::create_dir_all(&lib_deep)?;

        let file_in_src_api = src_api.join("routes.js");
        fs::write(&file_in_src_api, "")?;
        let file_in_src_db = src_db.join("connect.js");
        fs::write(&file_in_src_db, "")?;
        let file_in_lib_auth = lib_auth.join("token.js");
        fs::write(&file_in_lib_auth, "")?;
        let file_in_lib_deep = lib_deep.join("utils.js");
        fs::write(&file_in_lib_deep, "")?;

        let mut ctx_api = FileContext::new(file_in_src_api, root_path.to_path_buf());
        let mut ctx_db = FileContext::new(file_in_src_db, root_path.to_path_buf());
        let mut ctx_auth = FileContext::new(file_in_lib_auth, root_path.to_path_buf());
        let mut ctx_deep = FileContext::new(file_in_lib_deep, root_path.to_path_buf());

        // Test single-level wildcard `in:'src/*'`
        assert!(evaluator
            .evaluate(&mut ctx_api, &PredicateKey::In, "src/*")?
            .is_match());
        assert!(evaluator
            .evaluate(&mut ctx_db, &PredicateKey::In, "src/*")?
            .is_match());
        assert!(!evaluator
            .evaluate(&mut ctx_auth, &PredicateKey::In, "src/*")?
            .is_match());

        // Test recursive globstar `in:'**/*'`
        assert!(evaluator
            .evaluate(&mut ctx_deep, &PredicateKey::In, "lib/**")?
            .is_match());
        assert!(evaluator
            .evaluate(&mut ctx_deep, &PredicateKey::In, "lib/d*p/**")?
            .is_match());
        assert!(evaluator
            .evaluate(&mut ctx_auth, &PredicateKey::In, "lib/**")?
            .is_match());
        assert!(!evaluator
            .evaluate(&mut ctx_api, &PredicateKey::In, "lib/**")?
            .is_match());

        // Test non-matching wildcard
        assert!(!evaluator
            .evaluate(&mut ctx_api, &PredicateKey::In, "dist/*")?
            .is_match());

        Ok(())
    }

    #[test]
    fn test_in_path_evaluator_user_scenario() -> Result<()> {
        let evaluator = InPathEvaluator;
        let root_dir = tempdir()?;
        let root_path = root_dir.path();

        // Structure:
        //  /libs/feature/dependency-tracer/src/index.js
        //  /libs/feature/other/component.js
        let tracer_src = root_path
            .join("libs")
            .join("feature")
            .join("dependency-tracer")
            .join("src");
        let other_src = root_path.join("libs").join("feature").join("other");
        fs::create_dir_all(&tracer_src)?;
        fs::create_dir_all(&other_src)?;

        let file_in_tracer = tracer_src.join("index.js");
        fs::write(&file_in_tracer, "")?;
        let file_in_other = other_src.join("component.js");
        fs::write(&file_in_other, "")?;

        let mut ctx_tracer = FileContext::new(file_in_tracer, root_path.to_path_buf());
        let mut ctx_other = FileContext::new(file_in_other, root_path.to_path_buf());

        // This should match the file inside dependency-tracer/src
        assert!(evaluator
            .evaluate(
                &mut ctx_tracer,
                &PredicateKey::In,
                "libs/**/dependency-tracer/**"
            )?
            .is_match());

        // This should also match, as the file is within a directory that matches the glob.
        assert!(evaluator
            .evaluate(
                &mut ctx_tracer,
                &PredicateKey::In,
                "libs/**/dependency-tracer/*"
            )?
            .is_match());

        // This should not match the other file.
        assert!(!evaluator
            .evaluate(
                &mut ctx_other,
                &PredicateKey::In,
                "libs/**/dependency-tracer/**"
            )?
            .is_match());

        Ok(())
    }

    #[test]
    fn test_in_path_evaluator_root_file_no_parent() -> Result<()> {
        // This tests lines 34 and 63 - file at root with no real parent
        let evaluator = InPathEvaluator;

        let root_dir = tempdir()?;
        let root_path = root_dir.path();

        // Create a file directly in root
        let root_file = root_path.join("root_file.rs");
        fs::write(&root_file, "fn main() {}")?;

        let mut context = FileContext::new(root_file.clone(), root_path.to_path_buf());

        // Test with wildcard pattern - should check parent which is root
        let result = evaluator.evaluate(&mut context, &PredicateKey::In, "src/**")?;
        assert!(!result.is_match());

        // Test with exact path that doesn't exist
        let result = evaluator.evaluate(&mut context, &PredicateKey::In, "nonexistent")?;
        assert!(!result.is_match());

        Ok(())
    }

    #[test]
    fn test_in_path_evaluator_canonicalize_failure() -> Result<()> {
        // This tests lines 51 and 57 - canonicalize errors
        let evaluator = InPathEvaluator;

        let root_dir = tempdir()?;
        let root_path = root_dir.path();

        let src_dir = root_path.join("src");
        fs::create_dir_all(&src_dir)?;

        let file = src_dir.join("main.rs");
        fs::write(&file, "fn main() {}")?;

        let mut context = FileContext::new(file, root_path.to_path_buf());

        // Test with a path that exists but has issues (symlink loops, etc.)
        // Since we can't easily create canonicalize errors, test the normal path
        // which exercises the canonicalize success path
        let result = evaluator.evaluate(&mut context, &PredicateKey::In, "src")?;
        assert!(result.is_match());

        Ok(())
    }
}
