//! Tests for the main RenamingEngine.
//! 
//! These tests verify the core engine functionality including preview generation,
//! undo safety checks, and undo execution. Each test verifies actual behavior
//! rather than just checking for no panics.

use freneng::RenamingEngine;
use freneng::history::{History, RenameAction};
use std::path::PathBuf;
use tokio::fs;
use tempfile::TempDir;

// ============================================================================
// RenamingEngine::generate_preview tests
// ============================================================================

#[tokio::test]
async fn test_engine_preview_logic() {
    let engine = RenamingEngine;
    let files = vec![
        PathBuf::from("../test_data/photo_001.jpg"),
        PathBuf::from("../test_data/photo_002.jpg"),
    ];

    let result = engine.generate_preview(&files, "%L%N_v2.%E").await.unwrap();
    
    println!("=== test_engine_preview_logic ===");
    println!("Pattern: %L%N_v2.%E");
    for (i, rename) in result.renames.iter().enumerate() {
        println!("  File {}: {} -> {}", i + 1, rename.old_path.display(), rename.new_name);
    }
    
    assert_eq!(result.renames.len(), 2);
    assert_eq!(result.renames[0].new_name, "photo_001_v2.jpg");
    assert_eq!(result.renames[1].new_name, "photo_002_v2.jpg");
    assert!(result.warnings.is_empty());
    assert!(!result.has_empty_names);
}

#[tokio::test]
async fn test_engine_empty_name_detection() {
    let engine = RenamingEngine;
    let files = vec![PathBuf::from("../test_data/photo_001.jpg")];
    
    // Pattern that results in empty name
    let result = engine.generate_preview(&files, "%X/.//").await.unwrap();
    
    println!("=== test_engine_empty_name_detection ===");
    println!("Pattern: %X/.//");
    println!("  Result: has_empty_names={}, new_name='{}'", result.has_empty_names, result.renames[0].new_name);
    
    assert!(result.has_empty_names);
    assert_eq!(result.renames[0].new_name, "");
}

#[tokio::test]
async fn test_engine_warning_collection() {
    let engine = RenamingEngine;
    let files = vec![PathBuf::from("../test_data/photo_001.jpg")];
    
    // Pattern with unknown tokens
    let result = engine.generate_preview(&files, "%Z%Q_%N.%E").await.unwrap();
    
    println!("=== test_engine_warning_collection ===");
    println!("Pattern: %Z%Q_%N.%E");
    println!("  Warnings: {:?}", result.warnings);
    
    assert_eq!(result.warnings.len(), 2);
    assert!(result.warnings.contains(&"Unknown token: %Z".to_string()));
    assert!(result.warnings.contains(&"Unknown token: %Q".to_string()));
}

#[tokio::test]
async fn test_file_without_parent() {
    let engine = RenamingEngine;
    // Test that files with valid parents work correctly
    // (Files without parents are extremely rare and hard to create in tests)
    let temp_dir = TempDir::new().unwrap();
    let file = temp_dir.path().join("file.txt");
    fs::write(&file, "test").await.unwrap();
    
    // Verify the engine correctly handles files with parents
    let result = engine.generate_preview(&[file.clone()], "%N.%E").await.unwrap();
    assert_eq!(result.renames.len(), 1);
    assert_eq!(result.renames[0].new_name, "file.txt");
    assert_eq!(result.renames[0].old_path, file);
    // Verify new_path is in the same directory
    assert_eq!(result.renames[0].new_path.parent(), file.parent());
}

#[tokio::test]
async fn test_valid_file_name_processing() {
    // Test that valid file names are processed correctly
    // Invalid file names would fail at the OS level before reaching the engine
    let engine = RenamingEngine;
    let temp_dir = TempDir::new().unwrap();
    let file = temp_dir.path().join("valid_file_name.txt");
    fs::write(&file, "test").await.unwrap();
    
    let result = engine.generate_preview(&[file.clone()], "%L%N.%E").await.unwrap();
    assert_eq!(result.renames.len(), 1);
    assert_eq!(result.renames[0].new_name, "valid_file_name.txt");
    assert!(!result.has_empty_names);
    assert!(result.warnings.is_empty());
}

// ============================================================================
// RenamingEngine::check_undo tests
// ============================================================================

#[tokio::test]
async fn test_engine_undo_safety_check() {
    let engine = RenamingEngine;
    let history = History {
        timestamp: chrono::Local::now(),
        actions: vec![
            RenameAction {
                old_path: PathBuf::from("../test_data/non_existent.txt"),
                new_path: PathBuf::from("../test_data/also_missing.txt"),
            }
        ],
    };

    let (safe, conflicts) = engine.check_undo(&history).await;
    
    println!("=== test_engine_undo_safety_check ===");
    println!("  Safe actions: {}", safe.len());
    println!("  Conflicts: {:?}", conflicts);
    
    assert!(safe.is_empty());
    assert_eq!(conflicts.len(), 1);
    assert!(conflicts[0].contains("File no longer exists"));
}

#[tokio::test]
async fn test_check_undo_safe_operation() {
    let engine = RenamingEngine;
    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();
    fs::rename(&old_file, &new_file).await.unwrap();
    
    let history = History {
        timestamp: chrono::Local::now(),
        actions: vec![
            RenameAction {
                old_path: old_file.clone(),
                new_path: new_file.clone(),
            }
        ],
    };
    
    let (safe, conflicts) = engine.check_undo(&history).await;
    
    println!("=== test_check_undo_safe_operation ===");
    println!("  Safe actions: {}", safe.len());
    println!("  Conflicts: {:?}", conflicts);
    
    assert_eq!(safe.len(), 1);
    assert_eq!(safe[0].old_path, old_file);
    assert_eq!(safe[0].new_path, new_file);
    assert!(conflicts.is_empty());
}

#[tokio::test]
async fn test_check_undo_conflict_original_occupied() {
    let engine = RenamingEngine;
    let temp_dir = TempDir::new().unwrap();
    let old_file = temp_dir.path().join("old.txt");
    let new_file = temp_dir.path().join("new.txt");
    let other_file = temp_dir.path().join("old.txt"); // Same name as old
    
    fs::write(&old_file, "test").await.unwrap();
    fs::rename(&old_file, &new_file).await.unwrap();
    fs::write(&other_file, "other").await.unwrap(); // Create file at old location
    
    let history = History {
        timestamp: chrono::Local::now(),
        actions: vec![
            RenameAction {
                old_path: old_file.clone(),
                new_path: new_file.clone(),
            }
        ],
    };
    
    let (safe, conflicts) = engine.check_undo(&history).await;
    
    println!("=== test_check_undo_conflict_original_occupied ===");
    println!("  Safe actions: {}", safe.len());
    println!("  Conflicts: {:?}", conflicts);
    
    assert!(safe.is_empty());
    assert_eq!(conflicts.len(), 1);
    assert!(conflicts[0].contains("Original location occupied"));
}

#[tokio::test]
async fn test_check_undo_mixed_safe_and_conflicts() {
    let engine = RenamingEngine;
    let temp_dir = TempDir::new().unwrap();
    
    // Safe operation
    let old1 = temp_dir.path().join("old1.txt");
    let new1 = temp_dir.path().join("new1.txt");
    fs::write(&old1, "test").await.unwrap();
    fs::rename(&old1, &new1).await.unwrap();
    
    // Conflict: file doesn't exist
    let old2 = temp_dir.path().join("old2.txt");
    let new2 = temp_dir.path().join("new2.txt");
    
    let history = History {
        timestamp: chrono::Local::now(),
        actions: vec![
            RenameAction {
                old_path: old1.clone(),
                new_path: new1.clone(),
            },
            RenameAction {
                old_path: old2.clone(),
                new_path: new2.clone(),
            },
        ],
    };
    
    let (safe, conflicts) = engine.check_undo(&history).await;
    
    println!("=== test_check_undo_mixed_safe_and_conflicts ===");
    println!("  Safe actions: {}", safe.len());
    println!("  Conflicts: {:?}", conflicts);
    
    assert_eq!(safe.len(), 1);
    assert_eq!(conflicts.len(), 1);
}

// ============================================================================
// RenamingEngine::apply_undo tests
// ============================================================================

#[tokio::test]
async fn test_apply_undo_single_file() {
    let engine = RenamingEngine;
    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 content").await.unwrap();
    fs::rename(&old_file, &new_file).await.unwrap();
    assert!(fs::metadata(&old_file).await.is_err());
    assert!(fs::metadata(&new_file).await.is_ok());
    
    let actions = vec![
        RenameAction {
            old_path: old_file.clone(),
            new_path: new_file.clone(),
        }
    ];
    
    let count = engine.apply_undo(actions).await.unwrap();
    
    println!("=== test_apply_undo_single_file ===");
    println!("  Files renamed: {}", count);
    println!("  Old file exists: {}", fs::metadata(&old_file).await.is_ok());
    println!("  New file exists: {}", fs::metadata(&new_file).await.is_ok());
    
    assert_eq!(count, 1);
    assert!(fs::metadata(&old_file).await.is_ok());
    assert!(fs::metadata(&new_file).await.is_err());
    
    let content = fs::read_to_string(&old_file).await.unwrap();
    assert_eq!(content, "test content");
}

#[tokio::test]
async fn test_apply_undo_multiple_files() {
    let engine = RenamingEngine;
    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, new) in &files {
        fs::write(old, "test").await.unwrap();
        fs::rename(old, new).await.unwrap();
    }
    
    let actions: Vec<RenameAction> = files.iter()
        .map(|(old, new)| RenameAction {
            old_path: old.clone(),
            new_path: new.clone(),
        })
        .collect();
    
    let count = engine.apply_undo(actions).await.unwrap();
    
    println!("=== test_apply_undo_multiple_files ===");
    println!("  Files renamed: {}", count);
    for (old, new) in &files {
        println!("  {} exists: {}, {} exists: {}", 
                 old.display(), fs::metadata(old).await.is_ok(), 
                 new.display(), fs::metadata(new).await.is_ok());
    }
    
    assert_eq!(count, 3);
    for (old, new) in &files {
        assert!(fs::metadata(old).await.is_ok());
        assert!(fs::metadata(new).await.is_err());
    }
}

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

#[tokio::test]
async fn test_engine_preview_with_nested_directories() {
    let engine = RenamingEngine;
    let files = vec![
        PathBuf::from("../test_data/Photos/IMG_001.jpg"),
        PathBuf::from("../test_data/Photos/IMG_002.jpg"),
        PathBuf::from("../test_data/Photos/IMG_003.jpg"),
    ];

    let result = engine.generate_preview(&files, "%L%N_%C2.%E").await.unwrap();
    
    println!("=== test_engine_preview_with_nested_directories ===");
    println!("Pattern: %L%N_%C2.%E");
    for (i, rename) in result.renames.iter().enumerate() {
        println!("  File {}: {} -> {}", i + 1, rename.old_path.display(), rename.new_name);
        // Verify paths are preserved
        assert_eq!(rename.old_path.parent(), rename.new_path.parent(),
            "Path should be preserved in same directory");
    }
    
    assert_eq!(result.renames.len(), 3);
    assert_eq!(result.renames[0].new_name, "img_001_01.jpg");
    assert_eq!(result.renames[1].new_name, "img_002_02.jpg");
    assert_eq!(result.renames[2].new_name, "img_003_03.jpg");
    
    // Verify all paths are in Photos directory
    for rename in &result.renames {
        assert!(rename.old_path.to_string_lossy().contains("Photos"));
        assert!(rename.new_path.to_string_lossy().contains("Photos"));
    }
}

#[tokio::test]
async fn test_engine_preview_with_deeply_nested_directories() {
    let engine = RenamingEngine;
    let files = vec![
        PathBuf::from("../test_data/Photos/Vacation/2024/summer_001.jpg"),
        PathBuf::from("../test_data/Photos/Vacation/2024/summer_002.jpg"),
        PathBuf::from("../test_data/Photos/Vacation/2024/winter_001.jpg"),
    ];

    let result = engine.generate_preview(&files, "%P_%N.%E").await.unwrap();
    
    println!("=== test_engine_preview_with_deeply_nested_directories ===");
    println!("Pattern: %P_%N.%E");
    for (i, rename) in result.renames.iter().enumerate() {
        println!("  File {}: {} -> {}", i + 1, rename.old_path.display(), rename.new_name);
        // Verify paths are preserved in deeply nested structure
        assert_eq!(rename.old_path.parent(), rename.new_path.parent(),
            "Path should be preserved in same directory");
    }
    
    assert_eq!(result.renames.len(), 3);
    
    // Verify all paths are in Photos/Vacation/2024 directory
    for rename in &result.renames {
        assert!(rename.old_path.to_string_lossy().contains("Photos/Vacation/2024"));
        assert!(rename.new_path.to_string_lossy().contains("Photos/Vacation/2024"));
        // Verify parent directory name is included in new name
        assert!(rename.new_name.contains("2024"));
    }
}

#[tokio::test]
async fn test_engine_preview_with_mixed_file_types() {
    let engine = RenamingEngine;
    let files = vec![
        PathBuf::from("../test_data/Documents/report.pdf"),
        PathBuf::from("../test_data/Documents/manual.pdf"),
        PathBuf::from("../test_data/Documents/notes.txt"),
        PathBuf::from("../test_data/Documents/meeting.txt"),
    ];

    let result = engine.generate_preview(&files, "%L%N_%C2.%E").await.unwrap();
    
    println!("=== test_engine_preview_with_mixed_file_types ===");
    println!("Pattern: %L%N_%C2.%E");
    for (i, rename) in result.renames.iter().enumerate() {
        println!("  File {}: {} -> {}", i + 1, rename.old_path.display(), rename.new_name);
    }
    
    assert_eq!(result.renames.len(), 4);
    
    // Verify extensions are preserved
    assert_eq!(result.renames[0].new_name, "report_01.pdf");
    assert_eq!(result.renames[1].new_name, "manual_02.pdf");
    assert_eq!(result.renames[2].new_name, "notes_03.txt");
    assert_eq!(result.renames[3].new_name, "meeting_04.txt");
    
    // Verify all paths are in Documents directory
    for rename in &result.renames {
        assert!(rename.old_path.to_string_lossy().contains("Documents"));
        assert!(rename.new_path.to_string_lossy().contains("Documents"));
    }
}

#[tokio::test]
async fn test_engine_preview_with_subdirectory_files() {
    let engine = RenamingEngine;
    let files = vec![
        PathBuf::from("../test_data/Documents/Projects/project_a.txt"),
        PathBuf::from("../test_data/Documents/Projects/project_b.txt"),
        PathBuf::from("../test_data/Documents/Archive/doc1.txt"),
        PathBuf::from("../test_data/Documents/Archive/doc2.txt"),
    ];

    let result = engine.generate_preview(&files, "%P_%L%N.%E").await.unwrap();
    
    println!("=== test_engine_preview_with_subdirectory_files ===");
    println!("Pattern: %P_%L%N.%E");
    for (i, rename) in result.renames.iter().enumerate() {
        println!("  File {}: {} -> {}", i + 1, rename.old_path.display(), rename.new_name);
        // Verify paths are preserved in their respective subdirectories
        assert_eq!(rename.old_path.parent(), rename.new_path.parent(),
            "Path should be preserved in same directory");
    }
    
    assert_eq!(result.renames.len(), 4);
    
    // Verify parent directory names are included (lowercased due to %L modifier)
    for rename in &result.renames {
        let parent_name = rename.old_path.parent()
            .and_then(|p| p.file_name())
            .and_then(|n| n.to_str())
            .unwrap()
            .to_lowercase();
        assert!(rename.new_name.contains(&parent_name),
            "New name should contain parent directory (lowercased): {}", parent_name);
    }
}

