use dircpy::copy_dir;
use glob::glob;
use std::{
    collections::VecDeque,
    env::current_dir,
    fs::{copy, create_dir_all, remove_file, rename, symlink_metadata},
    io::{Error as IoError, ErrorKind as IoErrorKind},
    os::unix::fs::symlink,
    path::{Path, PathBuf},
    time::SystemTime,
};
use text_io::read;
use uuid::Uuid;

use crate::{
    errors::{AppError, AppInfo, AppWarning, FileError, FileWarning},
    models::{
        CollisionResolution, CollisionResolutionChoice, EntryType, Metadata, Operation,
        PasteContent, RecordEntry, RecordType,
    },
    records::{read_clipboard, read_history, write_clipboard, write_history},
};

pub fn handle_transfer<P: AsRef<Path>>(
    paths: Vec<P>,
    operation: Operation,
) -> Result<(Vec<AppInfo>, Vec<AppWarning>), AppError> {
    let mut clipboard_entries = VecDeque::from(read_clipboard()?.unwrap_or(Vec::new()));
    let (expanded_paths, warnings) = expand_paths(paths)?;
    let mut infos = Vec::new();

    for path in &expanded_paths {
        let Metadata {
            size,
            entry_type,
            absolute_path,
            modified: _,
        } = get_metadata(path)?;

        clipboard_entries.push_front(RecordEntry {
            operation: operation.clone(),
            size,
            entry_type,
            path: absolute_path,
            timestamp: SystemTime::now(),
            id: Uuid::new_v4(),
        });
    }
    let clipboard_entries: Vec<RecordEntry> = clipboard_entries.into();
    write_clipboard(&clipboard_entries)?;
    for path in expanded_paths {
        infos.push(match operation {
            Operation::Copy => AppInfo::Copy { path },
            Operation::Cut => AppInfo::Cut { path },
            Operation::Link => AppInfo::Link { path },
        });
    }
    Ok((infos, warnings))
}

pub fn handle_paste<P: AsRef<Path>>(
    destination_path: P,
    paste_content: Option<PasteContent>,
) -> Result<(Vec<AppInfo>, Vec<AppWarning>), AppError> {
    handle_paste_with_prompt(
        destination_path,
        paste_content,
        get_collision_resolution_choice,
    )
}

fn handle_paste_with_prompt<P: AsRef<Path>>(
    destination_path: P,
    paste_content: Option<PasteContent>,
    get_collision_resolution_choice: fn(path: &Path) -> CollisionResolutionChoice,
) -> Result<(Vec<AppInfo>, Vec<AppWarning>), AppError> {
    let destination_path = get_absolute_path(&destination_path)?;
    let mut infos = Vec::new();
    let mut warnings = Vec::new();

    let (mut entries_to_paste, mut clipboard_entries, mut history_entries) = match &paste_content {
        None => (
            read_clipboard()?.unwrap_or(Vec::new()),
            Some(read_clipboard()?.unwrap_or(Vec::new())),
            Some(VecDeque::from(read_history()?.unwrap_or(Vec::new()))),
        ),
        Some(content) => match content.source {
            RecordType::Clipboard => (
                content.entries.clone(),
                Some(read_clipboard()?.unwrap_or(Vec::new())),
                Some(VecDeque::from(read_history()?.unwrap_or(Vec::new()))),
            ),
            RecordType::History => (content.entries.clone(), None, None),
        },
    };
    entries_to_paste.retain(|entry| {
        let validity = check_validity(entry);
        match validity {
            Err(_) => false,
            Ok(Some(warning)) => {
                warnings.push(AppWarning::File(warning));
                true
            }
            Ok(_) => true,
        }
    });
    if !destination_path.is_dir() && entries_to_paste.len() > 1 {
        return Err(AppError::File(FileError::FileNameCollision {
            num_files: entries_to_paste.len(),
            to_path: destination_path,
        }));
    }

    let mut resolve_all: Option<CollisionResolution> = None;
    for mut entry in entries_to_paste {
        let mut collision_resolution = resolve_all;

        let mut quit = false;
        let file_name = entry.path.file_name().ok_or_else(|| FileError::FileName {
            path: entry.path.clone(),
        })?;
        let prospective_path = if destination_path.is_dir() {
            destination_path.join(file_name)
        } else {
            destination_path.clone()
        };

        if collision_resolution.is_none() && prospective_path.exists() {
            let overwrite_choice = get_collision_resolution_choice(&prospective_path);
            match overwrite_choice {
                CollisionResolutionChoice::Yes => {
                    collision_resolution = Some(CollisionResolution::Overwrite)
                }
                CollisionResolutionChoice::No => {
                    collision_resolution = Some(CollisionResolution::Skip)
                }
                CollisionResolutionChoice::OverwriteAll => {
                    resolve_all = Some(CollisionResolution::Overwrite);
                    collision_resolution = Some(CollisionResolution::Overwrite);
                }
                CollisionResolutionChoice::SkipAll => {
                    resolve_all = Some(CollisionResolution::Skip);
                    collision_resolution = Some(CollisionResolution::Skip);
                }
                CollisionResolutionChoice::Quit => quit = true,
            }
        }
        if quit {
            break;
        }
        ensure_dir(&prospective_path).map_err(|_| FileError::CreateDir {
            path: prospective_path.to_path_buf(),
        })?;

        let operation_result: Result<bool, FileError> = match entry.operation {
            Operation::Copy => copy_operation(&entry.path, &prospective_path, collision_resolution),
            Operation::Cut => move_operation(&entry.path, &prospective_path, collision_resolution),
            Operation::Link => {
                if let Some(resolution) = collision_resolution
                    && resolution == CollisionResolution::Overwrite
                {
                    let _ = remove_file(&prospective_path);
                }
                match symlink(&entry.path, &prospective_path) {
                    Ok(_) => Ok(true),
                    Err(error) => {
                        if let Some(resolution) = collision_resolution
                            && resolution == CollisionResolution::Skip
                            && error.kind() == IoErrorKind::AlreadyExists
                        {
                            Ok(false)
                        } else {
                            Err(FileError::Link {
                                from_path: entry.path.clone(),
                                to_path: prospective_path.clone(),
                                source: error,
                            })
                        }
                    }
                }
            }
        };

        match operation_result {
            Ok(pasted) => {
                if pasted {
                    entry.path = prospective_path;
                    if let Some(clipboard_entries) = clipboard_entries.as_mut() {
                        clipboard_entries.retain(|clipboard_entry| clipboard_entry.id != entry.id);
                    }
                    if let Some(history_entries) = history_entries.as_mut() {
                        history_entries.push_front(entry.clone());
                    }
                    infos.push(AppInfo::Paste {
                        path: entry.path.clone(),
                    });
                }
            }
            Err(error) => return Err(AppError::File(error)),
        }
    }

    if let Some(clipboard_entries) = clipboard_entries {
        write_clipboard(&clipboard_entries)?
    }
    if let Some(history_entries) = history_entries {
        let history_entries: Vec<RecordEntry> = history_entries.into();
        write_history(&history_entries)?;
    }
    Ok((infos, warnings))
}

pub fn get_metadata<P: AsRef<Path>>(path: P) -> Result<Metadata, FileError> {
    let path = path.as_ref();

    let absolute_path = if path.is_relative() {
        current_dir()
            .map_err(|source| FileError::Cwd { source })?
            .join(path)
    } else {
        path.to_path_buf()
    };

    let metadata = symlink_metadata(&absolute_path).map_err(|source| {
        if source.kind() == IoErrorKind::NotFound {
            FileError::PathNotFound {
                path: absolute_path.clone(),
            }
        } else {
            FileError::Metadata {
                path: absolute_path.clone(),
                source,
            }
        }
    })?;

    let canonical_path =
        absolute_path
            .canonicalize()
            .map_err(|source| FileError::AbsolutePath {
                path: absolute_path,
                source,
            })?;

    let modified = metadata
        .modified()
        .map_err(|source| FileError::ModifiedAccess {
            path: canonical_path.clone(),
            source,
        })?;

    let file_type = metadata.file_type();

    let entry_type = match () {
        () if file_type.is_symlink() => EntryType::Symlink,
        () if file_type.is_dir() => EntryType::Directory,
        () if file_type.is_file() => EntryType::File,
        _ => {
            return Err(FileError::UnsupportedType {
                path: canonical_path,
            });
        }
    };

    let size = if entry_type == EntryType::Directory {
        None
    } else {
        Some(metadata.len())
    };

    Ok(Metadata {
        modified,
        size,
        entry_type,
        absolute_path: canonical_path,
    })
}

pub fn get_absolute_path<P: AsRef<Path>>(path: P) -> Result<PathBuf, FileError> {
    let path = path.as_ref();
    let absolute_path = if path.is_relative() {
        let cwd = current_dir().map_err(|source| FileError::Cwd { source })?;
        cwd.join(path)
    } else {
        path.to_path_buf()
    };
    Ok(absolute_path)
}

fn expand_paths<P: AsRef<Path>>(
    paths: Vec<P>,
) -> Result<(Vec<PathBuf>, Vec<AppWarning>), FileError> {
    let mut expanded = Vec::new();
    let mut warnings = Vec::new();

    for path in paths {
        let path_str = path.as_ref().to_string_lossy();

        if path_str.contains('*') || path_str.contains('?') || path_str.contains('[') {
            match glob(&path_str) {
                Ok(entries) => {
                    let mut matched_paths = entries
                        .map(|entry| {
                            entry.map_err(|source| FileError::GlobUnreadable {
                                path: path.as_ref().to_path_buf(),
                                source,
                            })
                        })
                        .collect::<Result<Vec<PathBuf>, FileError>>()?;

                    if matched_paths.is_empty() {
                        warnings.push(
                            FileWarning::GlobUnmatched {
                                path: path.as_ref().to_path_buf(),
                            }
                            .into(),
                        );
                    } else {
                        matched_paths.sort();
                        expanded.extend(matched_paths);
                    }
                }
                Err(source) => {
                    return Err(FileError::GlobInvalidPattern {
                        path: path.as_ref().to_path_buf(),
                        source,
                    });
                }
            }
        } else {
            expanded.push(path.as_ref().to_path_buf());
        }
    }

    Ok((expanded, warnings))
}

fn check_validity(entry: &RecordEntry) -> Result<Option<FileWarning>, FileError> {
    let Metadata {
        modified,
        size,
        entry_type,
        absolute_path,
    } = get_metadata(&entry.path)?;

    if entry_type != entry.entry_type {
        return Ok(Some(FileWarning::TypeMismatch {
            path: absolute_path,
            old_type: entry.entry_type.to_string(),
            new_type: entry_type.to_string(),
        }));
    }

    if let (Some(expected_size), Some(self_size)) = (size, entry.size)
        && self_size != expected_size
    {
        return Ok(Some(FileWarning::SizeMismatch {
            path: absolute_path,
            old_size: self_size,
            new_size: expected_size,
        }));
    }

    if modified
        .duration_since(SystemTime::UNIX_EPOCH)
        .unwrap()
        .as_secs()
        > entry
            .timestamp
            .duration_since(SystemTime::UNIX_EPOCH)
            .unwrap()
            .as_secs()
    {
        return Ok(Some(FileWarning::ModifiedMismatch {
            path: absolute_path,
        }));
    }

    Ok(None)
}

fn get_collision_resolution_choice(path: &Path) -> CollisionResolutionChoice {
    loop {
        println!("[Warning]: Destination path already exists at: ");
        println!("{}", path.to_string_lossy());
        println!(
            "Overwrite?\nY: yes; N: no; A: overwrite all remaining; S: skip all remaining; Q: quit"
        );
        let choice: String = read!();
        if let Some(user_choice) = CollisionResolutionChoice::from_str(&choice) {
            return user_choice;
        }
        println!("Invalid input. Please try again.");
    }
}

fn copy_operation(
    from: &PathBuf,
    to: &PathBuf,
    collision_resolution: Option<CollisionResolution>,
) -> Result<bool, FileError> {
    if to.exists() {
        match collision_resolution {
            Some(CollisionResolution::Overwrite) => (),
            Some(CollisionResolution::Skip) => return Ok(false),
            None => unreachable!(),
        }
    }
    if from.is_dir() {
        copy_dir(from, to).map_err(|source| FileError::Copy {
            from_path: from.clone(),
            to_path: to.clone(),
            source,
        })?;
        Ok(true)
    } else {
        copy(from, to).map_err(|source| FileError::Copy {
            from_path: from.clone(),
            to_path: to.clone(),
            source,
        })?;
        Ok(true)
    }
}

fn move_operation(
    from: &PathBuf,
    to: &PathBuf,
    collision_resolution: Option<CollisionResolution>,
) -> Result<bool, FileError> {
    if to.exists() {
        match collision_resolution {
            Some(CollisionResolution::Overwrite) => (),
            Some(CollisionResolution::Skip) => return Ok(false),
            None => unreachable!(),
        }
    }
    rename(from, to).map_err(|source| FileError::Move {
        from_path: from.clone(),
        to_path: to.clone(),
        source,
    })?;
    Ok(true)
}

fn ensure_dir(path: &Path) -> Result<(), IoError> {
    if path.exists() {
        if path.is_dir() {
            return Ok(());
        }
        if let Some(parent) = path.parent()
            && !parent.exists()
        {
            create_dir_all(parent)?;
        }
    } else if path.extension().is_some() || path.to_string_lossy().contains('.') {
        if let Some(parent) = path.parent() {
            create_dir_all(parent)?;
        }
    } else {
        create_dir_all(path)?;
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        models::Operation,
        test_helpers::{
            create_file_and_get_metadata, create_mock_record_entry, create_test_file,
            get_test_entry, setup_test_env,
        },
    };
    use serial_test::serial;
    use std::{
        fs::{File, OpenOptions, canonicalize, symlink_metadata},
        io::Write,
        os::unix::fs::symlink,
        thread::sleep,
        time::Duration,
    };
    use tempfile::tempdir;

    fn mock_collision_resolution_choice_yes(_: &Path) -> CollisionResolutionChoice {
        CollisionResolutionChoice::Yes
    }

    fn mock_collision_resolution_choice_no(_: &Path) -> CollisionResolutionChoice {
        CollisionResolutionChoice::No
    }

    fn mock_collision_resolution_choice_quit(_: &Path) -> CollisionResolutionChoice {
        CollisionResolutionChoice::Quit
    }

    fn mock_collision_resolution_choice_overwrite_all(_: &Path) -> CollisionResolutionChoice {
        CollisionResolutionChoice::OverwriteAll
    }

    fn mock_collision_resolution_choice_skip_all(_: &Path) -> CollisionResolutionChoice {
        CollisionResolutionChoice::SkipAll
    }

    #[test]
    #[serial]
    fn test_handle_transfer_copy() {
        let env = setup_test_env();
        let file_path = env.source_dir.join("a.txt");
        create_test_file(&file_path, "a");

        let (infos, warnings) = handle_transfer(vec![&file_path], Operation::Copy).unwrap();

        assert_eq!(infos.len(), 1);
        assert!(warnings.is_empty());

        let clipboard = read_clipboard().unwrap().unwrap();
        assert_eq!(clipboard.len(), 1);
        assert_eq!(clipboard[0].operation, Operation::Copy);
        assert_eq!(clipboard[0].path, get_absolute_path(&file_path).unwrap());
    }

    #[test]
    #[serial]
    fn test_handle_paste_copy() {
        let env = setup_test_env();
        let file_path = env.source_dir.join("a.txt");
        create_test_file(&file_path, "a");
        let entry = get_test_entry(&file_path, Operation::Copy);
        write_clipboard(&[entry]).unwrap();

        let (infos, warnings) =
            handle_paste_with_prompt(&env.dest_dir, None, mock_collision_resolution_choice_yes)
                .unwrap();

        assert_eq!(infos.len(), 1);
        assert!(warnings.is_empty());
        assert!(env.dest_dir.join("a.txt").exists());
        assert!(file_path.exists());

        let history = read_history().unwrap().unwrap();
        assert_eq!(history.len(), 1);
    }

    #[test]
    #[serial]
    fn test_handle_paste_cut() {
        let env = setup_test_env();
        let file_path = env.source_dir.join("a.txt");
        create_test_file(&file_path, "a");
        let entry = get_test_entry(&file_path, Operation::Cut);
        write_clipboard(&[entry]).unwrap();

        let (infos, warnings) =
            handle_paste_with_prompt(&env.dest_dir, None, mock_collision_resolution_choice_yes)
                .unwrap();

        assert_eq!(infos.len(), 1);
        assert!(warnings.is_empty());
        let dest_file_path = env.dest_dir.join("a.txt");
        assert!(dest_file_path.exists());
        assert!(!file_path.exists());

        let history = read_history().unwrap().unwrap();
        assert_eq!(history[0].path, get_absolute_path(&dest_file_path).unwrap());
    }

    #[test]
    #[serial]
    #[cfg(unix)]
    fn test_handle_paste_link() {
        let env = setup_test_env();
        let file_path = env.source_dir.join("a.txt");
        create_test_file(&file_path, "a");
        let entry = get_test_entry(&file_path, Operation::Link);
        write_clipboard(&[entry]).unwrap();

        handle_paste_with_prompt(&env.dest_dir, None, mock_collision_resolution_choice_yes)
            .unwrap();

        let dest_link_path = env.dest_dir.join("a.txt");
        assert!(dest_link_path.exists());
        assert!(
            symlink_metadata(&dest_link_path)
                .unwrap()
                .file_type()
                .is_symlink()
        );
    }

    #[test]
    #[serial]
    fn test_handle_paste_with_invalid_entry() {
        let env = setup_test_env();
        let non_existent_path = env.source_dir.join("a.txt");
        let entry = create_mock_record_entry(
            Some(non_existent_path.clone()),
            Some(Operation::Copy),
            None,
            None,
            None,
        );
        write_clipboard(std::slice::from_ref(&entry)).unwrap();

        let (infos, warnings) =
            handle_paste_with_prompt(&env.dest_dir, None, mock_collision_resolution_choice_yes)
                .unwrap();

        assert!(infos.is_empty());
        assert!(warnings.is_empty());

        let clipboard = read_clipboard().unwrap().unwrap();
        assert_eq!(clipboard.len(), 1);
        assert_eq!(clipboard[0].id, entry.id);
    }

    #[test]
    #[serial]
    fn test_handle_paste_with_existing_entry() {
        let env = setup_test_env();
        let file_path = env.source_dir.join("a.txt");
        create_test_file(&file_path, "a");
        let entry = get_test_entry(&file_path, Operation::Copy);
        write_clipboard(std::slice::from_ref(&entry)).unwrap();
        let destination_file_path = env.dest_dir.join("a.txt");
        create_test_file(&destination_file_path, "a");

        let (infos, warnings) =
            handle_paste_with_prompt(&env.dest_dir, None, mock_collision_resolution_choice_quit)
                .unwrap();

        assert!(infos.is_empty());
        assert!(warnings.is_empty());

        let clipboard = read_clipboard().unwrap().unwrap();
        assert_eq!(clipboard.len(), 1);
        assert_eq!(clipboard[0].id, entry.id);
    }

    #[test]
    #[serial]
    fn test_handle_paste_overwrite_choice_no() {
        let env = setup_test_env();
        let file_path = env.source_dir.join("a.txt");
        create_test_file(&file_path, "a");
        let entry = get_test_entry(&file_path, Operation::Copy);
        write_clipboard(std::slice::from_ref(&entry)).unwrap();
        let destination_file_path = env.dest_dir.join("a.txt");
        create_test_file(&destination_file_path, "destination content");

        let (infos, warnings) =
            handle_paste_with_prompt(&env.dest_dir, None, mock_collision_resolution_choice_no)
                .unwrap();

        assert!(infos.is_empty());
        assert!(warnings.is_empty());

        let content = std::fs::read_to_string(destination_file_path).unwrap();
        assert_eq!(content, "destination content");

        let clipboard = read_clipboard().unwrap().unwrap();
        assert_eq!(clipboard.len(), 1);
        assert_eq!(clipboard[0].id, entry.id);
    }

    #[test]
    #[serial]
    fn test_handle_paste_overwrite_choice_skip_all() {
        let env = setup_test_env();
        // file 'a.txt' will exist in destination
        let file_a_path = env.source_dir.join("a.txt");
        create_test_file(&file_a_path, "source a");
        let entry_a = get_test_entry(&file_a_path, Operation::Copy);
        let dest_a_path = env.dest_dir.join("a.txt");
        create_test_file(&dest_a_path, "destination a");

        // file 'b.txt' will not exist in destination
        let file_b_path = env.source_dir.join("b.txt");
        create_test_file(&file_b_path, "source b");
        let entry_b = get_test_entry(&file_b_path, Operation::Copy);

        write_clipboard(&[entry_a.clone(), entry_b.clone()]).unwrap();

        let (infos, warnings) = handle_paste_with_prompt(
            &env.dest_dir,
            None,
            mock_collision_resolution_choice_skip_all,
        )
        .unwrap();

        assert_eq!(infos.len(), 1);
        assert!(warnings.is_empty());
        assert!(env.dest_dir.join("b.txt").exists());

        let clipboard = read_clipboard().unwrap().unwrap();
        assert_eq!(clipboard.len(), 1);
        assert_eq!(clipboard[0].id, entry_a.id);

        let content_a = std::fs::read_to_string(dest_a_path).unwrap();
        assert_eq!(content_a, "destination a");
    }

    #[test]
    #[serial]
    fn test_expand_paths() {
        let env = setup_test_env();
        let file_a_path = env.source_dir.join("a.txt");
        let file_b_path = env.source_dir.join("b.txt");
        let file_c_path = env.source_dir.join("c.log");
        create_test_file(&file_a_path, "a");
        create_test_file(&file_b_path, "b");
        create_test_file(&file_c_path, "c");

        let glob_path = env.source_dir.join("*.txt");
        let (expanded, warnings) = expand_paths(vec![glob_path.clone()]).unwrap();
        assert_eq!(expanded.len(), 2);
        assert!(expanded.contains(&file_a_path));
        assert!(expanded.contains(&file_b_path));
        assert!(warnings.is_empty());

        let unmatched_glob_path = env.source_dir.join("*.md");
        let (expanded, warnings) = expand_paths(vec![unmatched_glob_path.clone()]).unwrap();
        assert!(expanded.is_empty());
        assert!(!warnings.is_empty());
        assert!(matches!(
            warnings[0],
            AppWarning::File(FileWarning::GlobUnmatched { .. })
        ));

        let (expanded, warnings) = expand_paths(vec![file_c_path.clone()]).unwrap();
        assert_eq!(expanded.len(), 1);
        assert_eq!(expanded[0], file_c_path);
        assert!(warnings.is_empty());
    }

    #[test]
    fn test_check_validity_happy_path() {
        let dir = tempdir().expect("Failed to create temp dir");
        let metadata = create_file_and_get_metadata(&dir, "valid.txt", "hello");

        let entry = create_mock_record_entry(
            Some(metadata.absolute_path),
            Some(Operation::Copy),
            Some(metadata.entry_type),
            Some(metadata.modified),
            metadata.size,
        );

        let result = check_validity(&entry).expect("check_validity failed");
        assert!(result.is_none());
    }

    #[test]
    fn test_check_validity_type_mismatch() {
        let dir = tempdir().expect("Failed to create temp dir");
        let metadata = create_file_and_get_metadata(&dir, "type_mismatch.txt", "hello");

        let entry = create_mock_record_entry(
            Some(metadata.absolute_path),
            Some(Operation::Copy),
            Some(EntryType::Directory), // Intentionally wrong type
            Some(metadata.modified),
            metadata.size,
        );

        let warning = check_validity(&entry)
            .expect("check_validity failed")
            .expect("Expected a warning");

        matches!(warning, FileWarning::TypeMismatch { .. });
    }

    #[test]
    fn test_check_validity_size_mismatch() {
        let dir = tempdir().expect("Failed to create temp dir");
        let metadata = create_file_and_get_metadata(&dir, "size_mismatch.txt", "hello");

        let entry = create_mock_record_entry(
            Some(metadata.absolute_path),
            Some(Operation::Copy),
            Some(metadata.entry_type),
            Some(metadata.modified),
            Some(0), // Intentionally wrong size
        );

        let warning = check_validity(&entry)
            .expect("check_validity failed")
            .expect("Expected a warning");

        matches!(warning, FileWarning::SizeMismatch { .. });
    }

    #[test]
    fn test_check_validity_modified_mismatch() {
        let dir = tempdir().expect("Failed to create temp dir");
        let metadata = create_file_and_get_metadata(&dir, "modified.txt", "first");

        let entry = create_mock_record_entry(
            Some(metadata.absolute_path.clone()),
            Some(Operation::Copy),
            Some(metadata.entry_type),
            Some(metadata.modified),
            metadata.size,
        );

        // Sleep to ensure the modification time will be different
        sleep(Duration::from_secs(1));

        let mut file = OpenOptions::new()
            .write(true)
            .open(&metadata.absolute_path)
            .unwrap();
        writeln!(file, "second").unwrap();
        file.sync_all().unwrap();

        let warning = check_validity(&entry)
            .expect("check_validity failed")
            .expect("Expected a warning");

        matches!(warning, FileWarning::ModifiedMismatch { .. });
    }

    #[test]
    fn test_check_validity_path_not_found() {
        let entry = create_mock_record_entry(
            Some(PathBuf::from("/this/path/should/not/exist/ever.txt")),
            Some(Operation::Copy),
            Some(EntryType::File),
            None,
            None,
        );

        let result = check_validity(&entry);
        assert!(result.is_err());
        matches!(result.unwrap_err(), FileError::PathNotFound { .. });
    }

    #[test]
    fn test_get_absolute_path() {
        let dir = tempdir().unwrap();
        let file_path = dir.path().join("test_file.txt");
        File::create(&file_path).unwrap();

        let absolute_path = get_absolute_path(&file_path).unwrap();
        assert!(absolute_path.is_absolute());
        assert!(absolute_path.exists());
        assert_eq!(absolute_path, canonicalize(&file_path).unwrap());
    }

    #[test]
    fn test_get_metadata_for_file() {
        let dir = tempdir().unwrap();
        let file_path = dir.path().join("test_file.txt");
        let mut file = File::create(&file_path).unwrap();
        writeln!(file, "hello").unwrap();

        let metadata = get_metadata(&file_path).unwrap();

        assert_eq!(metadata.entry_type, EntryType::File);
        assert_eq!(metadata.size, Some(6)); // "hello\n"
        assert!(metadata.absolute_path.ends_with("test_file.txt"));
    }

    #[test]
    fn test_get_metadata_for_directory() {
        let dir = tempdir().unwrap();
        let dir_path = dir.path();

        let metadata = get_metadata(dir_path).unwrap();

        assert_eq!(metadata.entry_type, EntryType::Directory);
        assert_eq!(metadata.size, None);
        assert!(metadata.absolute_path.exists());
    }

    #[test]
    #[cfg(unix)]
    fn test_get_metadata_for_symlink() {
        let dir = tempdir().unwrap();
        let file_path = dir.path().join("target.txt");
        File::create(&file_path).unwrap();

        let symlink_path = dir.path().join("link.txt");
        symlink(&file_path, &symlink_path).unwrap();

        let metadata = get_metadata(&symlink_path).unwrap();

        assert_eq!(metadata.entry_type, EntryType::Symlink);
    }

    #[test]
    fn test_get_metadata_for_non_existent_path() {
        let dir = tempdir().unwrap();
        let file_path = dir.path().join("non_existent.txt");

        let result = get_metadata(&file_path);
        assert!(result.is_err());

        match result.unwrap_err() {
            FileError::PathNotFound { .. } => (),
            other_error => panic!("Expected PathNotFound error, got {:?}", other_error),
        }
    }

    #[test]
    #[serial]
    fn test_handle_paste_duplicate_entries() {
        let env = setup_test_env();
        let file_path = env.source_dir.join("duplicate.txt");
        create_test_file(&file_path, "content");

        let entry1 = get_test_entry(&file_path, Operation::Copy);
        let entry2 = get_test_entry(&file_path, Operation::Copy);

        write_clipboard(&[entry1, entry2]).unwrap();

        let (infos, warnings) = handle_paste_with_prompt(
            &env.dest_dir,
            None,
            mock_collision_resolution_choice_overwrite_all,
        )
        .unwrap();

        assert_eq!(infos.len(), 2);
        assert!(warnings.is_empty());

        assert!(env.dest_dir.join("duplicate.txt").exists());
        let clipboard = read_clipboard().unwrap().unwrap();
        assert!(clipboard.is_empty());
    }

    #[test]
    #[serial]
    fn test_handle_paste_copy_rename_file() {
        let env = setup_test_env();
        let file_path = env.source_dir.join("a.txt");
        create_test_file(&file_path, "a");
        let entry = get_test_entry(&file_path, Operation::Copy);
        write_clipboard(&[entry]).unwrap();

        let dest_path = env.dest_dir.join("b.txt");

        let (infos, warnings) =
            handle_paste_with_prompt(&dest_path, None, mock_collision_resolution_choice_yes)
                .unwrap();

        assert_eq!(infos.len(), 1);
        assert!(warnings.is_empty());
        assert!(dest_path.exists());
        assert!(file_path.exists());

        let history = read_history().unwrap().unwrap();
        assert_eq!(history.len(), 1);
        assert_eq!(history[0].path, dest_path);
    }

    #[test]
    #[serial]
    fn test_handle_paste_copy_directory_to_directory() {
        let env = setup_test_env();
        let source_dir = env.source_dir.join("dir_a");
        create_dir_all(&source_dir).unwrap();
        create_test_file(&source_dir.join("a.txt"), "a");

        let entry = get_test_entry(&source_dir, Operation::Copy);
        write_clipboard(&[entry]).unwrap();

        let (infos, warnings) =
            handle_paste_with_prompt(&env.dest_dir, None, mock_collision_resolution_choice_yes)
                .unwrap();

        assert_eq!(infos.len(), 1);
        assert!(warnings.is_empty());

        let dest_dir = env.dest_dir.join("dir_a");
        assert!(dest_dir.exists());
        assert!(dest_dir.is_dir());
        assert!(dest_dir.join("a.txt").exists());
        assert!(source_dir.exists());
    }

    #[test]
    #[serial]
    fn test_handle_paste_move_directory() {
        let env = setup_test_env();
        let source_dir = env.source_dir.join("dir_a");
        create_dir_all(&source_dir).unwrap();
        create_test_file(&source_dir.join("a.txt"), "a");

        let entry = get_test_entry(&source_dir, Operation::Cut);
        write_clipboard(&[entry]).unwrap();

        let (infos, warnings) =
            handle_paste_with_prompt(&env.dest_dir, None, mock_collision_resolution_choice_yes)
                .unwrap();

        assert_eq!(infos.len(), 1);
        assert!(warnings.is_empty());

        let dest_dir = env.dest_dir.join("dir_a");
        assert!(dest_dir.exists());
        assert!(dest_dir.is_dir());
        assert!(dest_dir.join("a.txt").exists());
        assert!(!source_dir.exists());
    }
}
