//! Integration tests for search operations (ls, glob, grep).
//!
//! These tests verify:
//! - Directory listing with filtering
//! - Glob pattern matching
//! - Regex content search with context
//! - Result limits enforcement
//! - Binary file detection
//! - Windows path support

use dirigent_tools::config::SearchConfig;
use dirigent_tools::search::{
    glob_search, grep_search, ls, FileKind, GlobRequest, GrepRequest, LsRequest,
};
use std::fs;
use std::io::Write;
use tempfile::TempDir;

/// Helper to create test directory structure
fn create_test_structure() -> TempDir {
    let temp_dir = TempDir::new().unwrap();
    let base = temp_dir.path();

    // Create directory structure
    fs::create_dir(base.join("src")).unwrap();
    fs::create_dir(base.join("tests")).unwrap();
    fs::create_dir(base.join("target")).unwrap(); // Should be excluded by default
    fs::create_dir(base.join(".git")).unwrap(); // Should be excluded by default

    // Create some files
    fs::write(base.join("README.md"), "# Test Project\n").unwrap();
    fs::write(base.join("Cargo.toml"), "[package]\nname = \"test\"\n").unwrap();
    fs::write(
        base.join("src/main.rs"),
        "fn main() {\n    println!(\"Hello, world!\");\n}\n",
    )
    .unwrap();
    fs::write(
        base.join("src/lib.rs"),
        "pub fn add(a: i32, b: i32) -> i32 {\n    a + b\n}\n",
    )
    .unwrap();
    fs::write(
        base.join("tests/integration.rs"),
        "#[test]\nfn test_add() {\n    assert_eq!(add(2, 2), 4);\n}\n",
    )
    .unwrap();

    // Create a file in excluded directory (should not appear in searches)
    fs::write(base.join("target/debug.log"), "debug info\n").unwrap();

    temp_dir
}

#[tokio::test]
async fn test_ls_basic() {
    let temp_dir = create_test_structure();
    let base = temp_dir.path();

    let config = SearchConfig::default();
    let request = LsRequest {
        path: base.to_string_lossy().to_string(),
    };

    let response = ls(request, &config).await.unwrap();

    // Should have files and directories, but not .git or target (excluded by default)
    assert!(response.entries.len() >= 4); // README, Cargo.toml, src, tests

    // Check we have both files and directories
    let has_file = response.entries.iter().any(|e| e.kind == FileKind::File);
    let has_dir = response.entries.iter().any(|e| e.kind == FileKind::Dir);
    assert!(has_file);
    assert!(has_dir);

    // Check that excluded directories are not present
    let has_target = response
        .entries
        .iter()
        .any(|e| e.path.file_name().unwrap() == "target");
    let has_git = response
        .entries
        .iter()
        .any(|e| e.path.file_name().unwrap() == ".git");
    assert!(!has_target, "target/ should be excluded by default");
    assert!(!has_git, ".git/ should be excluded by default");
}

#[tokio::test]
async fn test_ls_file_sizes() {
    let temp_dir = create_test_structure();
    let base = temp_dir.path();

    let config = SearchConfig::default();
    let request = LsRequest {
        path: base.to_string_lossy().to_string(),
    };

    let response = ls(request, &config).await.unwrap();

    // Files should have sizes, directories should not
    for entry in &response.entries {
        match entry.kind {
            FileKind::File => assert!(entry.size.is_some(), "Files should have sizes"),
            FileKind::Dir => assert!(entry.size.is_none(), "Directories should not have sizes"),
            FileKind::Symlink => {} // Symlinks may or may not have sizes
        }
    }
}

#[tokio::test]
async fn test_glob_basic() {
    let temp_dir = create_test_structure();
    let base = temp_dir.path();

    let config = SearchConfig::default();

    // Search for all Rust files
    let request = GlobRequest {
        path: base.to_string_lossy().to_string(),
        pattern: "**/*.rs".to_string(),
        exclude: None,
        max_results: None,
    };

    let response = glob_search(request, &config).await.unwrap();

    // Should find main.rs, lib.rs, and integration.rs (3 files)
    assert_eq!(response.matches.len(), 3);
    assert!(!response.truncated);

    // Verify all matches end with .rs
    for path in &response.matches {
        assert!(path.extension().unwrap() == "rs");
    }
}

#[tokio::test]
async fn test_glob_pattern_variations() {
    let temp_dir = create_test_structure();
    let base = temp_dir.path();

    let config = SearchConfig::default();

    // Test single-level wildcard
    let request = GlobRequest {
        path: base.to_string_lossy().to_string(),
        pattern: "*.md".to_string(),
        exclude: None,
        max_results: None,
    };

    let response = glob_search(request, &config).await.unwrap();
    assert_eq!(response.matches.len(), 1); // README.md
    assert!(!response.truncated);

    // Test specific directory
    let request = GlobRequest {
        path: base.to_string_lossy().to_string(),
        pattern: "src/*.rs".to_string(),
        exclude: None,
        max_results: None,
    };

    let response = glob_search(request, &config).await.unwrap();
    assert_eq!(response.matches.len(), 2); // main.rs, lib.rs
    assert!(!response.truncated);
}

#[tokio::test]
async fn test_glob_max_results() {
    let temp_dir = create_test_structure();
    let base = temp_dir.path();

    let config = SearchConfig::default();

    // Limit to 2 results
    let request = GlobRequest {
        path: base.to_string_lossy().to_string(),
        pattern: "**/*.rs".to_string(),
        exclude: None,
        max_results: Some(2),
    };

    let response = glob_search(request, &config).await.unwrap();
    assert_eq!(response.matches.len(), 2);
    assert!(response.truncated); // Should be truncated since there are 3 .rs files
}

#[tokio::test]
async fn test_glob_exclude_patterns() {
    let temp_dir = create_test_structure();
    let base = temp_dir.path();

    let config = SearchConfig::default();

    // Search for all .rs files but exclude tests
    let request = GlobRequest {
        path: base.to_string_lossy().to_string(),
        pattern: "**/*.rs".to_string(),
        exclude: Some(vec!["**/tests/**".to_string()]),
        max_results: None,
    };

    let response = glob_search(request, &config).await.unwrap();
    assert_eq!(response.matches.len(), 2); // Only main.rs and lib.rs, not integration.rs
    assert!(!response.truncated);
}

#[tokio::test]
async fn test_grep_basic() {
    let temp_dir = create_test_structure();
    let base = temp_dir.path();

    let config = SearchConfig::default();

    // Search for "main" in all files
    let request = GrepRequest {
        path: base.to_string_lossy().to_string(),
        pattern: "main".to_string(),
        file_pattern: None,
        ignore_case: false,
        context_before: 0,
        context_after: 0,
        max_results: None,
    };

    let response = grep_search(request, &config).await.unwrap();

    // Should find "main" in src/main.rs
    assert!(response.matches.len() >= 1);
    assert!(!response.truncated);

    // Verify line numbers are 1-indexed
    for m in &response.matches {
        assert!(m.line_number > 0);
    }
}

#[tokio::test]
async fn test_grep_case_insensitive() {
    let temp_dir = create_test_structure();
    let base = temp_dir.path();

    let config = SearchConfig::default();

    // Search for "HELLO" (case insensitive)
    let request = GrepRequest {
        path: base.to_string_lossy().to_string(),
        pattern: "HELLO".to_string(),
        file_pattern: Some("**/*.rs".to_string()),
        ignore_case: true,
        context_before: 0,
        context_after: 0,
        max_results: None,
    };

    let response = grep_search(request, &config).await.unwrap();

    // Should find "Hello" in main.rs
    assert!(response.matches.len() >= 1);
}

#[tokio::test]
async fn test_grep_context_lines() {
    let temp_dir = create_test_structure();
    let base = temp_dir.path();

    // Create a file with more content
    fs::write(
        base.join("src/context_test.rs"),
        "line 1\nline 2\nMATCH HERE\nline 4\nline 5\n",
    )
    .unwrap();

    let config = SearchConfig::default();

    // Search with context
    let request = GrepRequest {
        path: base.to_string_lossy().to_string(),
        pattern: "MATCH".to_string(),
        file_pattern: Some("**/context_test.rs".to_string()),
        ignore_case: false,
        context_before: 2,
        context_after: 2,
        max_results: None,
    };

    let response = grep_search(request, &config).await.unwrap();

    assert_eq!(response.matches.len(), 1);
    let m = &response.matches[0];

    // Should have 2 lines before and 2 lines after
    assert_eq!(m.context_before.len(), 2);
    assert_eq!(m.context_after.len(), 2);
    assert_eq!(m.context_before[0], "line 1");
    assert_eq!(m.context_before[1], "line 2");
    assert_eq!(m.context_after[0], "line 4");
    assert_eq!(m.context_after[1], "line 5");
}

#[tokio::test]
async fn test_grep_max_results() {
    let temp_dir = create_test_structure();
    let base = temp_dir.path();

    // Create a file with multiple matches
    fs::write(
        base.join("many_matches.txt"),
        "match\nmatch\nmatch\nmatch\nmatch\n",
    )
    .unwrap();

    let config = SearchConfig::default();

    // Limit to 3 results
    let request = GrepRequest {
        path: base.to_string_lossy().to_string(),
        pattern: "match".to_string(),
        file_pattern: None,
        ignore_case: false,
        context_before: 0,
        context_after: 0,
        max_results: Some(3),
    };

    let response = grep_search(request, &config).await.unwrap();

    assert_eq!(response.matches.len(), 3);
    assert!(response.truncated);
}

#[tokio::test]
async fn test_grep_binary_file_skip() {
    let temp_dir = TempDir::new().unwrap();
    let base = temp_dir.path();

    // Create a binary file with null bytes
    let binary_path = base.join("binary.bin");
    let mut file = fs::File::create(&binary_path).unwrap();
    file.write_all(&[0x00, 0x01, 0x02, 0x03]).unwrap();

    // Create a text file
    fs::write(base.join("text.txt"), "some text\n").unwrap();

    let config = SearchConfig::default();

    // Search for any character
    let request = GrepRequest {
        path: base.to_string_lossy().to_string(),
        pattern: ".".to_string(),
        file_pattern: None,
        ignore_case: false,
        context_before: 0,
        context_after: 0,
        max_results: None,
    };

    let response = grep_search(request, &config).await.unwrap();

    // Should only find matches in text.txt, not binary.bin
    assert!(response.matches.len() > 0);
    for m in &response.matches {
        assert!(
            m.path.to_string_lossy().contains("text.txt"),
            "Should not match binary files"
        );
    }
}

#[tokio::test]
async fn test_grep_regex_patterns() {
    let temp_dir = create_test_structure();
    let base = temp_dir.path();

    let config = SearchConfig::default();

    // Test regex with character classes
    let request = GrepRequest {
        path: base.to_string_lossy().to_string(),
        pattern: r"\d+".to_string(), // Match numbers
        file_pattern: None,
        ignore_case: false,
        context_before: 0,
        context_after: 0,
        max_results: None,
    };

    let response = grep_search(request, &config).await.unwrap();

    // Should find numbers in test files (e.g., "2, 2" in assert)
    assert!(response.matches.len() > 0);
}

#[cfg(windows)]
#[tokio::test]
async fn test_windows_paths() {
    let temp_dir = create_test_structure();
    let base = temp_dir.path();

    let config = SearchConfig::default();

    // Test with Windows path separators
    let path_str = base.to_string_lossy().to_string();

    // Test ls
    let request = LsRequest { path: path_str.clone() };
    let response = ls(request, &config).await.unwrap();
    assert!(response.entries.len() > 0);

    // Test glob
    let request = GlobRequest {
        path: path_str.clone(),
        pattern: "**\\*.rs".to_string(), // Windows-style pattern
        exclude: None,
        max_results: None,
    };
    let response = glob_search(request, &config).await.unwrap();
    assert!(response.matches.len() > 0);
}
