//! Tests for file system operations module.
//! 
//! These tests verify file finding and renaming functionality, including
//! recursive searches, pattern matching, and error handling.
//! All tests are async to match the async API of the file operations.

use freneng::{find_matching_files, find_matching_files_recursive, perform_renames, FileRename};
use tokio::fs;
use tempfile::TempDir;
use std::path::PathBuf;

// ============================================================================
// fs_ops::find_matching_files tests
// ============================================================================

#[tokio::test]
async fn test_find_matching_files_non_recursive_wrapper() {
    let temp_dir = TempDir::new().unwrap();
    let files = vec![
        temp_dir.path().join("file1.txt"),
        temp_dir.path().join("file2.txt"),
        temp_dir.path().join("file3.jpg"),
    ];
    
    for file in &files {
        fs::write(file, "test").await.unwrap();
    }
    
    // Test the wrapper function (non-recursive)
    let pattern = temp_dir.path().join("*.txt").to_str().unwrap().to_string();
    let result = find_matching_files(&pattern).await.unwrap();
    
    println!("=== test_find_matching_files_non_recursive_wrapper ===");
    println!("  Pattern: {}", pattern);
    println!("  Files found: {}", result.len());
    for file in &result {
        println!("    {}", file.display());
    }
    
    assert_eq!(result.len(), 2);
    assert!(result.iter().any(|f| f.file_name().unwrap() == "file1.txt"));
    assert!(result.iter().any(|f| f.file_name().unwrap() == "file2.txt"));
    assert!(!result.iter().any(|f| f.file_name().unwrap() == "file3.jpg"));
}

// ============================================================================
// fs_ops::find_matching_files_recursive tests
// ============================================================================

#[tokio::test]
async fn test_find_matching_files_simple() {
    let temp_dir = TempDir::new().unwrap();
    let files = vec![
        temp_dir.path().join("file1.txt"),
        temp_dir.path().join("file2.txt"),
        temp_dir.path().join("file3.jpg"),
    ];
    
    for file in &files {
        fs::write(file, "test").await.unwrap();
    }
    
    let pattern = temp_dir.path().join("*.txt").to_str().unwrap().to_string();
    let result = find_matching_files_recursive(&pattern, false).await.unwrap();
    
    println!("=== test_find_matching_files_simple ===");
    println!("  Pattern: {}", pattern);
    println!("  Files found: {}", result.len());
    for file in &result {
        println!("    {}", file.display());
    }
    
    assert_eq!(result.len(), 2);
    assert!(result.iter().any(|f| f.file_name().unwrap() == "file1.txt"));
    assert!(result.iter().any(|f| f.file_name().unwrap() == "file2.txt"));
}

#[tokio::test]
async fn test_find_matching_files_recursive() {
    let temp_dir = TempDir::new().unwrap();
    let subdir = temp_dir.path().join("subdir");
    let subsubdir = subdir.join("subsubdir");
    fs::create_dir_all(&subsubdir).await.unwrap();
    
    let files = vec![
        temp_dir.path().join("file1.txt"),
        temp_dir.path().join("file2.txt"),
        subdir.join("file3.txt"),
        subdir.join("file4.jpg"),
        subsubdir.join("file5.txt"),
        subsubdir.join("file6.txt"),
    ];
    
    for file in &files {
        fs::write(file, "test").await.unwrap();
    }
    
    // Use absolute path pattern to avoid directory changes (better for parallel tests)
    let temp_path = temp_dir.path().canonicalize().unwrap();
    let pattern = format!("{}/**/*.txt", temp_path.display());
    let result = find_matching_files_recursive(&pattern, true).await.unwrap();
    let _keep_alive = &temp_dir;
    
    println!("=== test_find_matching_files_recursive ===");
    println!("  Pattern: {} (recursive)", pattern);
    println!("  Files found: {}", result.len());
    for file in &result {
        println!("    {}", file.display());
    }
    
    // Should find files at root, subdir, and subsubdir levels
    assert_eq!(result.len(), 5);
    assert!(result.iter().any(|f| f.file_name().unwrap() == "file1.txt"));
    assert!(result.iter().any(|f| f.file_name().unwrap() == "file2.txt"));
    assert!(result.iter().any(|f| f.file_name().unwrap() == "file3.txt"));
    assert!(result.iter().any(|f| f.file_name().unwrap() == "file5.txt"));
    assert!(result.iter().any(|f| f.file_name().unwrap() == "file6.txt"));
}

#[tokio::test]
async fn test_find_matching_files_recursive_flag_without_star_star() {
    let temp_dir = TempDir::new().unwrap();
    let subdir = temp_dir.path().join("subdir");
    let subsubdir = subdir.join("subsubdir");
    fs::create_dir_all(&subsubdir).await.unwrap();
    
    let files = vec![
        temp_dir.path().join("file1.txt"),
        temp_dir.path().join("file2.txt"),
        subdir.join("file3.txt"),
        subsubdir.join("file4.txt"),
    ];
    
    for file in &files {
        fs::write(file, "test").await.unwrap();
    }
    
    // Pattern without **, but recursive=true should auto-convert it
    // Use absolute path to avoid directory changes (better for parallel tests)
    let temp_path = temp_dir.path().canonicalize().unwrap();
    let pattern = format!("{}/*.txt", temp_path.display());
    let result = find_matching_files_recursive(&pattern, true).await.unwrap();
    let _keep_alive = &temp_dir;
    
    println!("=== test_find_matching_files_recursive_flag_without_star_star ===");
    println!("  Pattern: {} (recursive=true, no **)", pattern);
    println!("  Files found: {}", result.len());
    for file in &result {
        println!("    {}", file.display());
    }
    
    // Should find files at all levels when recursive=true
    assert_eq!(result.len(), 4);
    assert!(result.iter().any(|f| f.file_name().unwrap() == "file1.txt"));
    assert!(result.iter().any(|f| f.file_name().unwrap() == "file2.txt"));
    assert!(result.iter().any(|f| f.file_name().unwrap() == "file3.txt"));
    assert!(result.iter().any(|f| f.file_name().unwrap() == "file4.txt"));
}

#[tokio::test]
async fn test_find_matching_files_non_recursive() {
    let temp_dir = TempDir::new().unwrap();
    let subdir = temp_dir.path().join("subdir");
    let subsubdir = subdir.join("subsubdir");
    fs::create_dir_all(&subsubdir).await.unwrap();
    
    let files = vec![
        temp_dir.path().join("file1.txt"),
        temp_dir.path().join("file2.txt"),
        subdir.join("file3.txt"),
        subsubdir.join("file4.txt"),
    ];
    
    for file in &files {
        fs::write(file, "test").await.unwrap();
    }
    
    // Pattern without **, recursive=false should only find root level
    // Use relative pattern from temp_dir
    let temp_path = temp_dir.path().canonicalize().unwrap();
    let old_dir = std::env::current_dir().unwrap();
    
    // Keep temp_dir alive
    let _keep_alive = &temp_dir;
    std::env::set_current_dir(&temp_path).unwrap();
    let pattern = "*.txt".to_string();
    let result = find_matching_files_recursive(&pattern, false).await.unwrap();
    let _ = std::env::set_current_dir(&old_dir);
    
    println!("=== test_find_matching_files_non_recursive ===");
    println!("  Pattern: {} (recursive=false)", pattern);
    println!("  Files found: {}", result.len());
    for file in &result {
        println!("    {}", file.display());
    }
    
    // Should only find files at root level when recursive=false
    assert_eq!(result.len(), 2);
    assert!(result.iter().any(|f| f.file_name().unwrap() == "file1.txt"));
    assert!(result.iter().any(|f| f.file_name().unwrap() == "file2.txt"));
    assert!(!result.iter().any(|f| f.file_name().unwrap() == "file3.txt"));
    assert!(!result.iter().any(|f| f.file_name().unwrap() == "file4.txt"));
}

#[tokio::test]
async fn test_find_matching_files_with_glob_star_star() {
    let temp_dir = TempDir::new().unwrap();
    let subdir = temp_dir.path().join("subdir");
    fs::create_dir_all(&subdir).await.unwrap();
    
    let files = vec![
        temp_dir.path().join("file1.txt"),
        subdir.join("file2.txt"),
    ];
    
    for file in &files {
        fs::write(file, "test").await.unwrap();
    }
    
    // Use absolute path pattern to avoid directory changes (better for parallel tests)
    let temp_path = temp_dir.path().canonicalize().unwrap();
    let pattern = format!("{}/**/*.txt", temp_path.display());
    let result = find_matching_files_recursive(&pattern, false).await.unwrap();
    let _keep_alive = &temp_dir;
    
    println!("=== test_find_matching_files_with_glob_star_star ===");
    println!("  Pattern: {}", pattern);
    println!("  Files found: {}", result.len());
    for file in &result {
        println!("    {}", file.display());
    }
    
    assert_eq!(result.len(), 2);
}

#[tokio::test]
async fn test_find_matching_files_no_matches() {
    let temp_dir = TempDir::new().unwrap();
    fs::write(temp_dir.path().join("file.jpg"), "test").await.unwrap();
    
    let pattern = temp_dir.path().join("*.txt").to_str().unwrap().to_string();
    let result = find_matching_files_recursive(&pattern, false).await.unwrap();
    
    println!("=== test_find_matching_files_no_matches ===");
    println!("  Pattern: {}", pattern);
    println!("  Files found: {}", result.len());
    
    assert_eq!(result.len(), 0);
}

#[tokio::test]
async fn test_find_matching_files_invalid_pattern() {
    let result = find_matching_files_recursive("[invalid", false).await;
    
    println!("=== test_find_matching_files_invalid_pattern ===");
    println!("  Pattern: [invalid");
    println!("  Result: {:?}", result);
    
    assert!(result.is_err());
}

// ============================================================================
// fs_ops::perform_renames tests
// ============================================================================

#[tokio::test]
async fn test_perform_renames_success() {
    let temp_dir = TempDir::new().unwrap();
    let old_file = temp_dir.path().join("old.txt");
    let new_file = temp_dir.path().join("new.txt");
    
    fs::write(&old_file, "test").await.unwrap();
    
    let renames = vec![
        FileRename {
            old_path: old_file.clone(),
            new_path: new_file.clone(),
            new_name: "new.txt".to_string(),
        }
    ];
    
    let result = perform_renames(&renames, false).await.unwrap();
    
    println!("=== test_perform_renames_success ===");
    println!("  Successful: {}", result.successful.len());
    println!("  Skipped: {}", result.skipped.len());
    println!("  Errors: {}", result.errors.len());
    
    assert_eq!(result.successful.len(), 1);
    assert!(result.skipped.is_empty());
    assert!(result.errors.is_empty());
    assert!(!fs::metadata(&old_file).await.is_ok());
    assert!(fs::metadata(&new_file).await.is_ok());
}

#[tokio::test]
async fn test_perform_renames_skip_identical() {
    let temp_dir = TempDir::new().unwrap();
    let file = temp_dir.path().join("file.txt");
    
    fs::write(&file, "test").await.unwrap();
    
    let renames = vec![
        FileRename {
            old_path: file.clone(),
            new_path: file.clone(),
            new_name: "file.txt".to_string(),
        }
    ];
    
    let result = perform_renames(&renames, false).await.unwrap();
    
    println!("=== test_perform_renames_skip_identical ===");
    println!("  Successful: {}", result.successful.len());
    println!("  Skipped: {}", result.skipped.len());
    println!("  Errors: {}", result.errors.len());
    
    assert!(result.successful.is_empty());
    assert_eq!(result.skipped.len(), 1);
    assert!(result.skipped[0].1.contains("identical"));
}

#[tokio::test]
async fn test_perform_renames_skip_existing() {
    let temp_dir = TempDir::new().unwrap();
    let old_file = temp_dir.path().join("old.txt");
    let new_file = temp_dir.path().join("new.txt");
    
    fs::write(&old_file, "old content").await.unwrap();
    fs::write(&new_file, "existing content").await.unwrap();
    
    let renames = vec![
        FileRename {
            old_path: old_file.clone(),
            new_path: new_file.clone(),
            new_name: "new.txt".to_string(),
        }
    ];
    
    let result = perform_renames(&renames, false).await.unwrap();
    
    println!("=== test_perform_renames_skip_existing ===");
    println!("  Successful: {}", result.successful.len());
    println!("  Skipped: {}", result.skipped.len());
    println!("  Errors: {}", result.errors.len());
    
    assert!(result.successful.is_empty());
    assert_eq!(result.skipped.len(), 1);
    assert!(result.skipped[0].1.contains("already exists"));
    assert!(fs::metadata(&old_file).await.is_ok());
    assert!(fs::metadata(&new_file).await.is_ok());
}

#[tokio::test]
async fn test_perform_renames_overwrite() {
    let temp_dir = TempDir::new().unwrap();
    let old_file = temp_dir.path().join("old.txt");
    let new_file = temp_dir.path().join("new.txt");
    
    fs::write(&old_file, "old content").await.unwrap();
    fs::write(&new_file, "existing content").await.unwrap();
    
    let renames = vec![
        FileRename {
            old_path: old_file.clone(),
            new_path: new_file.clone(),
            new_name: "new.txt".to_string(),
        }
    ];
    
    let result = perform_renames(&renames, true).await.unwrap();
    
    println!("=== test_perform_renames_overwrite ===");
    println!("  Successful: {}", result.successful.len());
    println!("  Skipped: {}", result.skipped.len());
    println!("  Errors: {}", result.errors.len());
    
    assert_eq!(result.successful.len(), 1);
    assert!(result.skipped.is_empty());
    assert!(!fs::metadata(&old_file).await.is_ok());
    assert!(fs::metadata(&new_file).await.is_ok());
    
    let content = fs::read_to_string(&new_file).await.unwrap();
    assert_eq!(content, "old content");
}

#[tokio::test]
async fn test_perform_renames_multiple() {
    let temp_dir = TempDir::new().unwrap();
    
    let files = vec![
        (temp_dir.path().join("old1.txt"), temp_dir.path().join("new1.txt")),
        (temp_dir.path().join("old2.txt"), temp_dir.path().join("new2.txt")),
        (temp_dir.path().join("old3.txt"), temp_dir.path().join("new3.txt")),
    ];
    
    for (old, _) in &files {
        fs::write(old, "test").await.unwrap();
    }
    
    let renames: Vec<FileRename> = files.iter()
        .map(|(old, new)| FileRename {
            old_path: old.clone(),
            new_path: new.clone(),
            new_name: new.file_name().unwrap().to_str().unwrap().to_string(),
        })
        .collect();
    
    let result = perform_renames(&renames, false).await.unwrap();
    
    println!("=== test_perform_renames_multiple ===");
    println!("  Successful: {}", result.successful.len());
    println!("  Skipped: {}", result.skipped.len());
    println!("  Errors: {}", result.errors.len());
    
    assert_eq!(result.successful.len(), 3);
    assert!(result.skipped.is_empty());
    assert!(result.errors.is_empty());
    
    for (old, new) in &files {
        assert!(!fs::metadata(&old).await.is_ok());
        assert!(fs::metadata(&new).await.is_ok());
    }
}

#[tokio::test]
async fn test_perform_renames_error_nonexistent() {
    let temp_dir = TempDir::new().unwrap();
    let old_file = temp_dir.path().join("nonexistent.txt");
    let new_file = temp_dir.path().join("new.txt");
    
    let renames = vec![
        FileRename {
            old_path: old_file.clone(),
            new_path: new_file.clone(),
            new_name: "new.txt".to_string(),
        }
    ];
    
    let result = perform_renames(&renames, false).await.unwrap();
    
    println!("=== test_perform_renames_error_nonexistent ===");
    println!("  Successful: {}", result.successful.len());
    println!("  Skipped: {}", result.skipped.len());
    println!("  Errors: {}", result.errors.len());
    
    assert!(result.successful.is_empty());
    assert!(result.skipped.is_empty());
    assert_eq!(result.errors.len(), 1);
}

// ============================================================================
// fs_ops::find_matching_files_recursive - literal file path tests
// ============================================================================

#[tokio::test]
async fn test_find_matching_files_literal_file_path() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("test_file.txt");
    fs::write(&file_path, "test").await.unwrap();
    
    let pattern = file_path.to_str().unwrap();
    let result = find_matching_files_recursive(pattern, false).await.unwrap();
    
    println!("=== test_find_matching_files_literal_file_path ===");
    println!("  Pattern: {}", pattern);
    println!("  Files found: {}", result.len());
    for file in &result {
        println!("    {}", file.display());
    }
    
    assert_eq!(result.len(), 1);
    assert_eq!(result[0], file_path);
}

#[tokio::test]
async fn test_find_matching_files_literal_file_path_recursive() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("test_file.txt");
    fs::write(&file_path, "test").await.unwrap();
    
    let pattern = file_path.to_str().unwrap();
    let result = find_matching_files_recursive(pattern, true).await.unwrap();
    
    println!("=== test_find_matching_files_literal_file_path_recursive ===");
    println!("  Pattern: {}", pattern);
    println!("  Files found: {}", result.len());
    for file in &result {
        println!("    {}", file.display());
    }
    
    assert_eq!(result.len(), 1);
    assert_eq!(result[0], file_path);
}

#[tokio::test]
async fn test_find_matching_files_literal_directory_non_recursive() {
    let temp_dir = TempDir::new().unwrap();
    let subdir = temp_dir.path().join("subdir");
    fs::create_dir_all(&subdir).await.unwrap();
    let file_path = subdir.join("test_file.txt");
    fs::write(&file_path, "test").await.unwrap();
    
    let pattern = subdir.to_str().unwrap();
    let result = find_matching_files_recursive(pattern, false).await.unwrap();
    
    println!("=== test_find_matching_files_literal_directory_non_recursive ===");
    println!("  Pattern: {}", pattern);
    println!("  Files found: {}", result.len());
    
    // Directory without recursive should return empty (directories aren't files)
    assert_eq!(result.len(), 0);
}

#[tokio::test]
async fn test_find_matching_files_literal_directory_recursive() {
    let temp_dir = TempDir::new().unwrap();
    let subdir = temp_dir.path().join("subdir");
    fs::create_dir_all(&subdir).await.unwrap();
    let file1 = subdir.join("file1.txt");
    let file2 = subdir.join("file2.txt");
    fs::write(&file1, "test").await.unwrap();
    fs::write(&file2, "test").await.unwrap();
    
    let pattern = subdir.to_str().unwrap();
    let result = find_matching_files_recursive(pattern, true).await.unwrap();
    
    println!("=== test_find_matching_files_literal_directory_recursive ===");
    println!("  Pattern: {}", pattern);
    println!("  Files found: {}", result.len());
    for file in &result {
        println!("    {}", file.display());
    }
    
    // Should find all files in the directory recursively
    assert_eq!(result.len(), 2);
    assert!(result.iter().any(|f| f.file_name().unwrap() == "file1.txt"));
    assert!(result.iter().any(|f| f.file_name().unwrap() == "file2.txt"));
}

#[tokio::test]
async fn test_find_matching_files_literal_nonexistent_path() {
    let temp_dir = TempDir::new().unwrap();
    let nonexistent_path = temp_dir.path().join("nonexistent_file.txt");
    let pattern = nonexistent_path.to_str().unwrap();
    
    // Should not error, but return empty (falls through to glob matching which finds nothing)
    let result = find_matching_files_recursive(pattern, false).await.unwrap();
    
    println!("=== test_find_matching_files_literal_nonexistent_path ===");
    println!("  Pattern: {}", pattern);
    println!("  Files found: {}", result.len());
    
    assert_eq!(result.len(), 0);
}

#[tokio::test]
async fn test_find_matching_files_multiple_literal_paths_via_cli() {
    // Simulates what happens when shell expands globs: multiple literal paths
    let temp_dir = TempDir::new().unwrap();
    let file1 = temp_dir.path().join("file1.txt");
    let file2 = temp_dir.path().join("file2.txt");
    let file3 = temp_dir.path().join("file3.jpg");
    fs::write(&file1, "test").await.unwrap();
    fs::write(&file2, "test").await.unwrap();
    fs::write(&file3, "test").await.unwrap();
    
    // Test each literal path individually (CLI would call this for each)
    let pattern1 = file1.to_str().unwrap();
    let result1 = find_matching_files_recursive(pattern1, false).await.unwrap();
    
    let pattern2 = file2.to_str().unwrap();
    let result2 = find_matching_files_recursive(pattern2, false).await.unwrap();
    
    let pattern3 = file3.to_str().unwrap();
    let result3 = find_matching_files_recursive(pattern3, false).await.unwrap();
    
    println!("=== test_find_matching_files_multiple_literal_paths_via_cli ===");
    println!("  Pattern1: {} -> {} files", pattern1, result1.len());
    println!("  Pattern2: {} -> {} files", pattern2, result2.len());
    println!("  Pattern3: {} -> {} files", pattern3, result3.len());
    
    assert_eq!(result1.len(), 1);
    assert_eq!(result1[0], file1);
    assert_eq!(result2.len(), 1);
    assert_eq!(result2[0], file2);
    assert_eq!(result3.len(), 1);
    assert_eq!(result3[0], file3);
}

// ============================================================================
// Tests using test_data structure
// ============================================================================

#[tokio::test]
async fn test_find_matching_files_in_test_data_structure() {
    // Test finding files in the nested test_data structure
    // Skip if test_data doesn't exist (it's generated by generate_test_data.sh)
    let test_data_path = PathBuf::from("../test_data");
    if !test_data_path.exists() {
        println!("=== test_find_matching_files_in_test_data_structure ===");
        println!("  Skipping: test_data directory not found. Run generate_test_data.sh first.");
        return;
    }
    
    let test_data_path = test_data_path.canonicalize().unwrap();
    let pattern = format!("{}/**/*.jpg", test_data_path.display());
    
    let result = find_matching_files_recursive(&pattern, true).await.unwrap();
    
    println!("=== test_find_matching_files_in_test_data_structure ===");
    println!("  Pattern: {}", pattern);
    println!("  Files found: {}", result.len());
    for file in &result {
        println!("    {}", file.display());
    }
    
    // Should find multiple JPG files in nested structure
    assert!(result.len() >= 2, "Should find at least 2 JPG files");
    
    // Verify files are from test_data
    for file in &result {
        assert!(file.to_string_lossy().contains("test_data"),
            "File should be in test_data: {}", file.display());
    }
}

#[tokio::test]
async fn test_find_matching_files_recursive_in_test_data() {
    // Test recursive finding in test_data Photos directory
    // Skip if test_data doesn't exist (it's generated by generate_test_data.sh)
    let test_data_photos = PathBuf::from("../test_data/Photos");
    if !test_data_photos.exists() {
        println!("=== test_find_matching_files_recursive_in_test_data ===");
        println!("  Skipping: test_data/Photos directory not found. Run generate_test_data.sh first.");
        return;
    }
    
    let test_data_path = test_data_photos.canonicalize().unwrap();
    let pattern = format!("{}/**/*.jpg", test_data_path.display());
    
    let result = find_matching_files_recursive(&pattern, true).await.unwrap();
    
    println!("=== test_find_matching_files_recursive_in_test_data ===");
    println!("  Pattern: {} (recursive)", pattern);
    println!("  Files found: {}", result.len());
    for file in &result {
        println!("    {}", file.display());
    }
    
    // Should find files in Photos and subdirectories
    assert!(result.len() >= 3, "Should find multiple JPG files in Photos directory");
    
    // Verify all files are in Photos directory or subdirectories
    for file in &result {
        assert!(file.to_string_lossy().contains("Photos"),
            "File should be in Photos directory: {}", file.display());
    }
    
    // Should find files in nested directories like Photos/Vacation/2024
    let has_nested = result.iter().any(|f| f.to_string_lossy().contains("Vacation"));
    assert!(has_nested, "Should find files in nested Vacation directory");
}

