/*!
 * Local Folder Sync Service Unit Tests
 * 
 * Tests for local filesystem synchronization functionality including:
 * - Path validation and access checking
 * - Recursive directory traversal
 * - Symlink handling
 * - File change detection
 * - Permission handling
 * - Cross-platform path normalization
 */

use std::path::{Path, PathBuf};
use std::fs;
use tempfile::TempDir;
use uuid::Uuid;
use chrono::Utc;
use serde_json::json;

use readur::{
    models::{LocalFolderSourceConfig, SourceType},
    services::local_folder_service::LocalFolderService,
};

/// Create a test local folder configuration
fn create_test_local_config() -> LocalFolderSourceConfig {
    LocalFolderSourceConfig {
        watch_folders: vec!["/test/documents".to_string(), "/test/images".to_string()],
        recursive: true,
        follow_symlinks: false,
        auto_sync: true,
        sync_interval_minutes: 30,
        file_extensions: vec![".pdf".to_string(), ".txt".to_string(), ".jpg".to_string()],
    }
}

/// Create a test directory structure
fn create_test_directory_structure() -> Result<TempDir, std::io::Error> {
    let temp_dir = TempDir::new()?;
    let base_path = temp_dir.path();
    
    // Create directory structure
    fs::create_dir_all(base_path.join("documents"))?;
    fs::create_dir_all(base_path.join("documents/subfolder"))?;
    fs::create_dir_all(base_path.join("images"))?;
    fs::create_dir_all(base_path.join("restricted"))?;
    
    // Create test files
    fs::write(base_path.join("documents/test1.pdf"), b"PDF content")?;
    fs::write(base_path.join("documents/test2.txt"), b"Text content")?;
    fs::write(base_path.join("documents/subfolder/nested.pdf"), b"Nested PDF")?;
    fs::write(base_path.join("images/photo.jpg"), b"Image content")?;
    fs::write(base_path.join("documents/ignored.exe"), b"Executable")?;
    fs::write(base_path.join("restricted/secret.txt"), b"Secret content")?;
    
    Ok(temp_dir)
}

#[test]
fn test_local_folder_config_creation() {
    let config = create_test_local_config();
    
    assert_eq!(config.watch_folders.len(), 2);
    assert_eq!(config.watch_folders[0], "/test/documents");
    assert_eq!(config.watch_folders[1], "/test/images");
    assert!(config.recursive);
    assert!(!config.follow_symlinks);
    assert!(config.auto_sync);
    assert_eq!(config.sync_interval_minutes, 30);
    assert_eq!(config.file_extensions.len(), 3);
}

#[test]
fn test_local_folder_config_validation() {
    let config = create_test_local_config();
    
    // Test paths validation
    assert!(!config.watch_folders.is_empty(), "Should have at least one path");
    for path in &config.watch_folders {
        assert!(Path::new(path).is_absolute() || path.starts_with('.'), 
                "Path should be absolute or relative: {}", path);
    }
    
    // Test sync interval validation
    assert!(config.sync_interval_minutes > 0, "Sync interval should be positive");
    
    // Test file extensions validation
    assert!(!config.file_extensions.is_empty(), "Should have file extensions");
    for ext in &config.file_extensions {
        assert!(ext.starts_with('.'), "Extension should start with dot: {}", ext);
    }
}

#[test]
fn test_path_normalization() {
    let test_cases = vec![
        ("./documents", "./documents"),
        ("../documents", "../documents"),
        ("/home/user/documents", "/home/user/documents"),
        ("C:\\Users\\test\\Documents", "C:\\Users\\test\\Documents"),
        ("documents/", "documents"),
        ("documents//subfolder", "documents/subfolder"),
    ];
    
    for (input, expected) in test_cases {
        let normalized = normalize_path(input);
        // On different platforms, the exact normalization might vary
        // but we can test basic properties
        assert!(!normalized.is_empty(), "Normalized path should not be empty");
        assert!(!normalized.contains("//"), "Should not contain double slashes");
        assert!(!normalized.ends_with('/') || normalized == "/", "Should not end with slash unless root");
    }
}

fn normalize_path(path: &str) -> String {
    let path = path.trim_end_matches('/');
    path.replace("//", "/")
}

#[test]
fn test_file_extension_filtering() {
    let config = create_test_local_config();
    let allowed_extensions = &config.file_extensions;
    
    let test_files = vec![
        ("document.pdf", true),
        ("notes.txt", true),
        ("photo.jpg", true),
        ("archive.zip", false),
        ("program.exe", false),
        ("script.sh", false),
        ("Document.PDF", true), // Test case insensitivity
        ("README", false), // No extension
        (".hidden.txt", true), // Hidden file with allowed extension
    ];
    
    for (filename, should_be_allowed) in test_files {
        let extension = extract_extension(filename);
        let is_allowed = allowed_extensions.contains(&extension);
        
        assert_eq!(is_allowed, should_be_allowed, 
                   "File {} should be {}", filename, 
                   if should_be_allowed { "allowed" } else { "rejected" });
    }
}

fn extract_extension(filename: &str) -> String {
    if let Some(pos) = filename.rfind('.') {
        filename[pos..].to_lowercase()
    } else {
        String::new()
    }
}

#[test]
fn test_recursive_directory_traversal() {
    let temp_dir = create_test_directory_structure().unwrap();
    let base_path = temp_dir.path();
    
    // Test recursive traversal
    let mut files_found = Vec::new();
    collect_files_recursive(base_path, &mut files_found).unwrap();
    
    assert!(!files_found.is_empty(), "Should find files in directory structure");
    
    // Should find files in subdirectories when recursive is enabled
    let nested_files: Vec<_> = files_found.iter()
        .filter(|f| f.to_string_lossy().contains("subfolder"))
        .collect();
    assert!(!nested_files.is_empty(), "Should find files in subdirectories");
    
    // Test non-recursive traversal
    let mut files_flat = Vec::new();
    collect_files_flat(base_path, &mut files_flat).unwrap();
    
    // Should find fewer files when not recursive
    assert!(files_flat.len() <= files_found.len(), 
            "Non-recursive should find same or fewer files");
}

fn collect_files_recursive(dir: &Path, files: &mut Vec<PathBuf>) -> std::io::Result<()> {
    use walkdir::WalkDir;
    
    for entry in WalkDir::new(dir) {
        let entry = entry?;
        if entry.file_type().is_file() {
            files.push(entry.path().to_path_buf());
        }
    }
    Ok(())
}

fn collect_files_flat(dir: &Path, files: &mut Vec<PathBuf>) -> std::io::Result<()> {
    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        if entry.file_type()?.is_file() {
            files.push(entry.path());
        }
    }
    Ok(())
}

#[test]
fn test_symlink_handling() {
    let temp_dir = TempDir::new().unwrap();
    let base_path = temp_dir.path();
    
    // Create a file and a symlink to it
    let file_path = base_path.join("original.txt");
    fs::write(&file_path, b"Original content").unwrap();
    
    let symlink_path = base_path.join("link.txt");
    
    // Create symlink (this might fail on Windows without admin rights)
    #[cfg(unix)]
    {
        use std::os::unix::fs::symlink;
        if symlink(&file_path, &symlink_path).is_ok() {
            // Test with follow_symlinks = true
            let mut files_with_symlinks = Vec::new();
            collect_files_with_symlinks(base_path, true, &mut files_with_symlinks).unwrap();
            
            // Should find both original and symlinked file
            assert!(files_with_symlinks.len() >= 2, "Should find original and symlinked files");
            
            // Test with follow_symlinks = false
            let mut files_without_symlinks = Vec::new();
            collect_files_with_symlinks(base_path, false, &mut files_without_symlinks).unwrap();
            
            // Should find only original file
            assert!(files_without_symlinks.len() < files_with_symlinks.len(), 
                    "Should find fewer files when not following symlinks");
        }
    }
}

fn collect_files_with_symlinks(dir: &Path, follow_symlinks: bool, files: &mut Vec<PathBuf>) -> std::io::Result<()> {
    use walkdir::WalkDir;
    
    let walker = WalkDir::new(dir).follow_links(follow_symlinks);
    
    for entry in walker {
        let entry = entry?;
        if entry.file_type().is_file() {
            files.push(entry.path().to_path_buf());
        }
    }
    Ok(())
}

#[test]
fn test_file_metadata_extraction() {
    let temp_dir = create_test_directory_structure().unwrap();
    let base_path = temp_dir.path();
    let test_file = base_path.join("documents/test1.pdf");
    
    let metadata = fs::metadata(&test_file).unwrap();
    
    // Test basic metadata
    assert!(metadata.is_file());
    assert!(!metadata.is_dir());
    assert!(metadata.len() > 0);
    
    // Test modification time
    let modified = metadata.modified().unwrap();
    let now = std::time::SystemTime::now();
    assert!(modified <= now, "File modification time should be in the past");
    
    // Test file size
    let expected_size = "PDF content".len() as u64;
    assert_eq!(metadata.len(), expected_size);
}

#[test]
fn test_permission_checking() {
    let temp_dir = create_test_directory_structure().unwrap();
    let base_path = temp_dir.path();
    
    // Test readable file
    let readable_file = base_path.join("documents/test1.pdf");
    assert!(readable_file.exists());
    assert!(is_readable(&readable_file));
    
    // Test readable directory
    let readable_dir = base_path.join("documents");
    assert!(readable_dir.exists());
    assert!(readable_dir.is_dir());
    assert!(is_readable(&readable_dir));
    
    // Test non-existent path
    let non_existent = base_path.join("does_not_exist.txt");
    assert!(!non_existent.exists());
    assert!(!is_readable(&non_existent));
}

fn is_readable(path: &Path) -> bool {
    path.exists() && fs::metadata(path).is_ok()
}

#[test]
fn test_file_change_detection() {
    let temp_dir = TempDir::new().unwrap();
    let test_file = temp_dir.path().join("test.txt");
    
    // Create initial file
    fs::write(&test_file, b"Initial content").unwrap();
    let initial_metadata = fs::metadata(&test_file).unwrap();
    let initial_modified = initial_metadata.modified().unwrap();
    let initial_size = initial_metadata.len();
    
    // Wait a bit to ensure timestamp difference
    std::thread::sleep(std::time::Duration::from_millis(10));
    
    // Modify file
    fs::write(&test_file, b"Modified content").unwrap();
    let modified_metadata = fs::metadata(&test_file).unwrap();
    let modified_modified = modified_metadata.modified().unwrap();
    let modified_size = modified_metadata.len();
    
    // Test change detection
    assert_ne!(initial_size, modified_size, "File size should change");
    assert!(modified_modified >= initial_modified, "Modification time should advance");
}

#[test]
fn test_error_handling() {
    // Test various error scenarios
    
    // Non-existent path
    let non_existent_config = LocalFolderSourceConfig {
        watch_folders: vec!["/this/path/does/not/exist".to_string()],
        recursive: true,
        follow_symlinks: false,
        auto_sync: true,
        sync_interval_minutes: 30,
        file_extensions: vec![".txt".to_string()],
    };
    
    assert_eq!(non_existent_config.watch_folders[0], "/this/path/does/not/exist");
    
    // Empty paths
    let empty_paths_config = LocalFolderSourceConfig {
        watch_folders: Vec::new(),
        recursive: true,
        follow_symlinks: false,
        auto_sync: true,
        sync_interval_minutes: 30,
        file_extensions: vec![".txt".to_string()],
    };
    
    assert!(empty_paths_config.watch_folders.is_empty());
    
    // Invalid sync interval
    let invalid_interval_config = LocalFolderSourceConfig {
        watch_folders: vec!["/test".to_string()],
        recursive: true,
        follow_symlinks: false,
        auto_sync: true,
        sync_interval_minutes: 0, // Invalid
        file_extensions: vec![".txt".to_string()],
    };
    
    assert_eq!(invalid_interval_config.sync_interval_minutes, 0);
}

#[test]
fn test_cross_platform_paths() {
    let test_paths = vec![
        ("/home/user/documents", true),  // Unix absolute
        ("./documents", true),           // Relative
        ("../documents", true),          // Relative parent
        ("documents", true),             // Relative simple
        ("C:\\Users\\test", true),       // Windows absolute
        ("", false),                     // Empty path
    ];
    
    for (path, should_be_valid) in test_paths {
        let is_valid = !path.is_empty();
        assert_eq!(is_valid, should_be_valid, "Path validation failed for: {}", path);
        
        if is_valid {
            let path_obj = Path::new(path);
            // Test that we can create a Path object
            assert_eq!(path_obj.to_string_lossy(), path);
        }
    }
}

#[test]
fn test_file_filtering_performance() {
    // Create a larger set of test files to test filtering performance
    let temp_dir = TempDir::new().unwrap();
    let base_path = temp_dir.path();
    
    // Create many files
    for i in 0..1000 {
        let filename = format!("file_{}.txt", i);
        let filepath = base_path.join(&filename);
        fs::write(filepath, format!("Content {}", i)).unwrap();
    }
    
    // Create some files with different extensions
    for i in 0..100 {
        let filename = format!("doc_{}.pdf", i);
        let filepath = base_path.join(&filename);
        fs::write(filepath, format!("PDF {}", i)).unwrap();
    }
    
    let config = create_test_local_config();
    let start = std::time::Instant::now();
    
    // Simulate filtering
    let mut matching_files = 0;
    for entry in fs::read_dir(base_path).unwrap() {
        let entry = entry.unwrap();
        if entry.file_type().unwrap().is_file() {
            let filename = entry.file_name().to_string_lossy().to_string();
            let extension = extract_extension(&filename);
            if config.file_extensions.contains(&extension) {
                matching_files += 1;
            }
        }
    }
    
    let elapsed = start.elapsed();
    
    assert!(matching_files > 0, "Should find matching files");
    assert!(elapsed < std::time::Duration::from_secs(1), "Filtering should be fast");
}

#[test]
fn test_concurrent_access_safety() {
    use std::sync::{Arc, Mutex};
    use std::thread;
    
    let temp_dir = create_test_directory_structure().unwrap();
    let base_path = Arc::new(temp_dir.path().to_path_buf());
    let file_count = Arc::new(Mutex::new(0));
    
    // Verify the directory has files before testing concurrent access
    let initial_count = fs::read_dir(&*base_path).unwrap().count();
    assert!(initial_count > 0, "Test directory should contain files");
    
    let mut handles = vec![];
    
    // Spawn multiple threads to read the same directory
    for _ in 0..4 {
        let base_path = Arc::clone(&base_path);
        let file_count = Arc::clone(&file_count);
        
        let handle = thread::spawn(move || {
            let mut local_count = 0;
            
            // Recursively count files
            fn count_files_in_dir(path: &std::path::Path, count: &mut usize) {
                if let Ok(entries) = fs::read_dir(path) {
                    for entry in entries {
                        if let Ok(entry) = entry {
                            let path = entry.path();
                            if path.is_file() {
                                *count += 1;
                            } else if path.is_dir() {
                                count_files_in_dir(&path, count);
                            }
                        }
                    }
                }
            }
            
            count_files_in_dir(&*base_path, &mut local_count);
            
            let mut count = file_count.lock().unwrap();
            *count += local_count;
        });
        
        handles.push(handle);
    }
    
    // Wait for all threads to complete
    for handle in handles {
        handle.join().unwrap();
    }
    
    let final_count = *file_count.lock().unwrap();
    assert!(final_count > 0, "Should have counted files from multiple threads");
}

#[test]
fn test_hidden_file_handling() {
    let temp_dir = TempDir::new().unwrap();
    let base_path = temp_dir.path();
    
    // Create regular and hidden files
    fs::write(base_path.join("visible.txt"), b"Visible content").unwrap();
    fs::write(base_path.join(".hidden.txt"), b"Hidden content").unwrap();
    
    // Test file discovery
    let mut all_files = Vec::new();
    for entry in fs::read_dir(base_path).unwrap() {
        let entry = entry.unwrap();
        if entry.file_type().unwrap().is_file() {
            all_files.push(entry.file_name().to_string_lossy().to_string());
        }
    }
    
    assert!(all_files.contains(&"visible.txt".to_string()));
    
    // Hidden file visibility depends on the OS and settings
    let has_hidden = all_files.iter().any(|f| f.starts_with('.'));
    println!("Hidden files found: {}", has_hidden);
    
    // Filter hidden files if needed
    let visible_files: Vec<_> = all_files.iter()
        .filter(|f| !f.starts_with('.'))
        .collect();
    
    assert!(!visible_files.is_empty(), "Should find at least one visible file");
}

#[test]
fn test_large_file_handling() {
    let temp_dir = TempDir::new().unwrap();
    let large_file = temp_dir.path().join("large.txt");
    
    // Create a larger file (1MB)
    let content = "a".repeat(1024 * 1024);
    fs::write(&large_file, content.as_bytes()).unwrap();
    
    let metadata = fs::metadata(&large_file).unwrap();
    assert_eq!(metadata.len(), 1024 * 1024);
    
    // Test that we can handle large file metadata efficiently
    let start = std::time::Instant::now();
    let _metadata = fs::metadata(&large_file).unwrap();
    let elapsed = start.elapsed();
    
    assert!(elapsed < std::time::Duration::from_millis(100), 
            "Metadata reading should be fast even for large files");
}

#[test]
fn test_disk_space_estimation() {
    let temp_dir = create_test_directory_structure().unwrap();
    let base_path = temp_dir.path();
    
    let mut total_size = 0u64;
    let mut file_count = 0u32;
    
    for entry in walkdir::WalkDir::new(base_path) {
        let entry = entry.unwrap();
        if entry.file_type().is_file() {
            if let Ok(metadata) = entry.metadata() {
                total_size += metadata.len();
                file_count += 1;
            }
        }
    }
    
    assert!(file_count > 0, "Should count files");
    assert!(total_size > 0, "Should calculate total size");
    
    // Calculate average file size
    let avg_size = if file_count > 0 { total_size / file_count as u64 } else { 0 };
    assert!(avg_size > 0, "Should calculate average file size");
}