use crate::{svn_result, with_tmp_pool, Error};

/// Options for diff operations
#[derive(Debug, Clone, Copy, Default)]
pub struct DiffOptions {
    /// Ignore changes in whitespace
    pub ignore_whitespace: bool,
    /// Ignore changes in end-of-line style  
    pub ignore_eol_style: bool,
    /// Show context around changes
    pub show_c_function: bool,
}

impl DiffOptions {
    /// Creates new diff options with default values.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets whether to ignore whitespace changes.
    pub fn with_ignore_whitespace(mut self, ignore: bool) -> Self {
        self.ignore_whitespace = ignore;
        self
    }

    /// Sets whether to ignore end-of-line style differences.
    pub fn with_ignore_eol_style(mut self, ignore: bool) -> Self {
        self.ignore_eol_style = ignore;
        self
    }

    /// Sets whether to show C function names in context.
    pub fn with_show_c_function(mut self, show: bool) -> Self {
        self.show_c_function = show;
        self
    }
}

/// A diff hunk showing differences between files.
///
/// Obtained from [`Patch::hunks`] when parsing a unified diff / patch file.
/// Borrows from the [`PatchFile`] that produced it.
pub struct DiffHunk<'a> {
    ptr: *mut subversion_sys::svn_diff_hunk_t,
    _phantom: std::marker::PhantomData<&'a PatchFile>,
}

impl DiffHunk<'_> {
    unsafe fn from_raw(ptr: *mut subversion_sys::svn_diff_hunk_t) -> Self {
        Self {
            ptr,
            _phantom: std::marker::PhantomData,
        }
    }

    /// Get the starting line number in the original file
    pub fn original_start(&self) -> u64 {
        unsafe { subversion_sys::svn_diff_hunk_get_original_start(self.ptr).into() }
    }

    /// Get the number of lines in the original file
    pub fn original_length(&self) -> u64 {
        unsafe { subversion_sys::svn_diff_hunk_get_original_length(self.ptr).into() }
    }

    /// Get the starting line number in the modified file
    pub fn modified_start(&self) -> u64 {
        unsafe { subversion_sys::svn_diff_hunk_get_modified_start(self.ptr).into() }
    }

    /// Get the number of lines in the modified file  
    pub fn modified_length(&self) -> u64 {
        unsafe { subversion_sys::svn_diff_hunk_get_modified_length(self.ptr).into() }
    }

    /// Get the leading context lines
    pub fn leading_context(&self) -> u64 {
        unsafe { subversion_sys::svn_diff_hunk_get_leading_context(self.ptr).into() }
    }

    /// Get the trailing context lines
    pub fn trailing_context(&self) -> u64 {
        unsafe { subversion_sys::svn_diff_hunk_get_trailing_context(self.ptr).into() }
    }
}

/// The operation a patch performs on a file.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PatchOperation {
    /// The file is unchanged.
    Unchanged,
    /// The file is added.
    Added,
    /// The file is deleted.
    Deleted,
    /// The file is copied.
    Copied,
    /// The file is moved.
    Moved,
}

impl From<subversion_sys::svn_diff_operation_kind_t> for PatchOperation {
    fn from(kind: subversion_sys::svn_diff_operation_kind_t) -> Self {
        match kind {
            subversion_sys::svn_diff_operation_kind_e_svn_diff_op_unchanged => {
                PatchOperation::Unchanged
            }
            subversion_sys::svn_diff_operation_kind_e_svn_diff_op_added => PatchOperation::Added,
            subversion_sys::svn_diff_operation_kind_e_svn_diff_op_deleted => {
                PatchOperation::Deleted
            }
            subversion_sys::svn_diff_operation_kind_e_svn_diff_op_copied => PatchOperation::Copied,
            subversion_sys::svn_diff_operation_kind_e_svn_diff_op_moved => PatchOperation::Moved,
            _ => unreachable!("unknown svn_diff_operation_kind_t value: {}", kind),
        }
    }
}

/// A single patch (one file's worth of changes) parsed from a patch file.
///
/// Borrows from the [`PatchFile`] that produced it.
pub struct Patch<'a> {
    ptr: *mut subversion_sys::svn_patch_t,
    _phantom: std::marker::PhantomData<&'a PatchFile>,
}

impl Patch<'_> {
    /// The old file name as recorded in the patch.
    pub fn old_filename(&self) -> Option<&str> {
        unsafe {
            let p = (*self.ptr).old_filename;
            if p.is_null() {
                None
            } else {
                std::ffi::CStr::from_ptr(p).to_str().ok()
            }
        }
    }

    /// The new file name as recorded in the patch.
    pub fn new_filename(&self) -> Option<&str> {
        unsafe {
            let p = (*self.ptr).new_filename;
            if p.is_null() {
                None
            } else {
                std::ffi::CStr::from_ptr(p).to_str().ok()
            }
        }
    }

    /// The operation this patch performs on the file.
    pub fn operation(&self) -> PatchOperation {
        unsafe { (*self.ptr).operation.into() }
    }

    /// The hunks that make up this patch.
    pub fn hunks(&self) -> Vec<DiffHunk<'_>> {
        unsafe {
            let array = (*self.ptr).hunks;
            if array.is_null() {
                return Vec::new();
            }
            let arr = &*array;
            let elts = arr.elts as *const *mut subversion_sys::svn_diff_hunk_t;
            (0..arr.nelts as isize)
                .map(|i| DiffHunk::from_raw(*elts.offset(i)))
                .collect()
        }
    }
}

/// A patch file opened for parsing.
///
/// Use [`PatchFile::open`] to open a unified-diff/patch file, then
/// [`PatchFile::next_patch`] to iterate over the patches it contains.
pub struct PatchFile {
    ptr: *mut subversion_sys::svn_patch_file_t,
    pool: apr::Pool<'static>,
}

impl PatchFile {
    /// Open a patch file at the given path for parsing.
    pub fn open(path: &std::path::Path) -> Result<Self, Error<'static>> {
        let pool = apr::Pool::new();
        let abspath = crate::dirent::to_absolute_cstring(path.to_string_lossy().as_ref())?;
        let mut ptr = std::ptr::null_mut();
        let err = unsafe {
            subversion_sys::svn_diff_open_patch_file(&mut ptr, abspath.as_ptr(), pool.as_mut_ptr())
        };
        svn_result(err)?;
        Ok(Self { ptr, pool })
    }

    /// Parse and return the next patch in the file, or `None` at end of file.
    ///
    /// If `reverse` is true, the patch is interpreted as a reverse patch. If
    /// `ignore_whitespace` is true, whitespace differences are ignored when
    /// matching hunks.
    pub fn next_patch(
        &mut self,
        reverse: bool,
        ignore_whitespace: bool,
    ) -> Result<Option<Patch<'_>>, Error<'static>> {
        let mut patch_ptr = std::ptr::null_mut();
        let err = unsafe {
            subversion_sys::svn_diff_parse_next_patch(
                &mut patch_ptr,
                self.ptr,
                reverse as subversion_sys::svn_boolean_t,
                ignore_whitespace as subversion_sys::svn_boolean_t,
                self.pool.as_mut_ptr(),
                self.pool.as_mut_ptr(),
            )
        };
        svn_result(err)?;
        if patch_ptr.is_null() {
            Ok(None)
        } else {
            Ok(Some(Patch {
                ptr: patch_ptr,
                _phantom: std::marker::PhantomData,
            }))
        }
    }
}

impl Drop for PatchFile {
    fn drop(&mut self) {
        unsafe {
            // Errors closing the patch file are not actionable during drop.
            let err = subversion_sys::svn_diff_close_patch_file(self.ptr, self.pool.as_mut_ptr());
            if !err.is_null() {
                subversion_sys::svn_error_clear(err);
            }
        }
    }
}

/// A diff between two files
pub struct Diff {
    ptr: *mut subversion_sys::svn_diff_t,
    _pool: apr::Pool<'static>,
}

impl Diff {
    unsafe fn from_raw(ptr: *mut subversion_sys::svn_diff_t, pool: apr::Pool<'static>) -> Self {
        Self { ptr, _pool: pool }
    }

    /// Check if the diff contains any changes
    pub fn contains_changes(&self) -> bool {
        unsafe { subversion_sys::svn_diff_contains_diffs(self.ptr) != 0 }
    }

    /// Check if the diff contains conflicts  
    pub fn contains_conflicts(&self) -> bool {
        unsafe { subversion_sys::svn_diff_contains_conflicts(self.ptr) != 0 }
    }

    /// Get the raw pointer for use with other SVN functions
    pub fn as_ptr(&self) -> *mut subversion_sys::svn_diff_t {
        self.ptr
    }
}

/// File options for diff operations
#[derive(Debug, Clone, Copy, Default)]
pub struct FileOptions {
    /// Ignore whitespace changes
    pub ignore_space: IgnoreSpace,
    /// Ignore end-of-line differences
    pub ignore_eol_style: bool,
    /// Show function context
    pub show_c_function: bool,
}

impl FileOptions {
    /// Creates new file options with default values.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets whether to ignore whitespace changes.
    pub fn with_ignore_whitespace(mut self, ignore: bool) -> Self {
        self.ignore_space = if ignore {
            IgnoreSpace::Change
        } else {
            IgnoreSpace::None
        };
        self
    }

    /// Sets whether to ignore end-of-line style differences.
    pub fn with_ignore_eol_style(mut self, ignore: bool) -> Self {
        self.ignore_eol_style = ignore;
        self
    }

    /// Sets whether to show C function names in context.
    pub fn with_show_c_function(mut self, show: bool) -> Self {
        self.show_c_function = show;
        self
    }
}

/// Types of whitespace ignoring
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum IgnoreSpace {
    #[default]
    /// Don't ignore any whitespace
    None,
    /// Ignore changes in whitespace
    Change,
    /// Ignore all whitespace
    All,
}

impl From<IgnoreSpace> for subversion_sys::svn_diff_file_ignore_space_t {
    fn from(ignore: IgnoreSpace) -> Self {
        match ignore {
            IgnoreSpace::None => {
                subversion_sys::svn_diff_file_ignore_space_t_svn_diff_file_ignore_space_none
            }
            IgnoreSpace::Change => {
                subversion_sys::svn_diff_file_ignore_space_t_svn_diff_file_ignore_space_change
            }
            IgnoreSpace::All => {
                subversion_sys::svn_diff_file_ignore_space_t_svn_diff_file_ignore_space_all
            }
        }
    }
}

/// Diff two files
pub fn file_diff(
    original: &std::path::Path,
    modified: &std::path::Path,
    options: FileOptions,
) -> Result<Diff, Error<'static>> {
    let original_cstr = std::ffi::CString::new(original.to_string_lossy().as_ref())?;
    let modified_cstr = std::ffi::CString::new(modified.to_string_lossy().as_ref())?;

    let pool = apr::Pool::new();
    let mut diff_ptr = std::ptr::null_mut();

    // Create diff options
    let diff_options = unsafe { subversion_sys::svn_diff_file_options_create(pool.as_mut_ptr()) };
    unsafe {
        (*diff_options).ignore_space = options.ignore_space.into();
        (*diff_options).ignore_eol_style = if options.ignore_eol_style { 1 } else { 0 };
        (*diff_options).show_c_function = if options.show_c_function { 1 } else { 0 };
    }

    let err = unsafe {
        subversion_sys::svn_diff_file_diff_2(
            &mut diff_ptr,
            original_cstr.as_ptr(),
            modified_cstr.as_ptr(),
            diff_options,
            pool.as_mut_ptr(),
        )
    };

    svn_result(err)?;
    Ok(unsafe { Diff::from_raw(diff_ptr, pool) })
}

/// Diff three files (three-way comparison)
pub fn file_diff3(
    original: &std::path::Path,
    modified: &std::path::Path,
    latest: &std::path::Path,
    options: FileOptions,
) -> Result<Diff, Error<'static>> {
    let original_cstr = std::ffi::CString::new(original.to_string_lossy().as_ref())?;
    let modified_cstr = std::ffi::CString::new(modified.to_string_lossy().as_ref())?;
    let latest_cstr = std::ffi::CString::new(latest.to_string_lossy().as_ref())?;

    let pool = apr::Pool::new();
    let mut diff_ptr = std::ptr::null_mut();

    // Create diff options
    let diff_options = unsafe { subversion_sys::svn_diff_file_options_create(pool.as_mut_ptr()) };
    unsafe {
        (*diff_options).ignore_space = options.ignore_space.into();
        (*diff_options).ignore_eol_style = if options.ignore_eol_style { 1 } else { 0 };
        (*diff_options).show_c_function = if options.show_c_function { 1 } else { 0 };
    }

    let err = unsafe {
        subversion_sys::svn_diff_file_diff3_2(
            &mut diff_ptr,
            original_cstr.as_ptr(),
            modified_cstr.as_ptr(),
            latest_cstr.as_ptr(),
            diff_options,
            pool.as_mut_ptr(),
        )
    };

    svn_result(err)?;
    Ok(unsafe { Diff::from_raw(diff_ptr, pool) })
}

/// Diff four files (four-way comparison with ancestor)
pub fn file_diff4(
    original: &std::path::Path,
    modified: &std::path::Path,
    latest: &std::path::Path,
    ancestor: &std::path::Path,
    options: FileOptions,
) -> Result<Diff, Error<'static>> {
    let original_cstr = std::ffi::CString::new(original.to_string_lossy().as_ref())?;
    let modified_cstr = std::ffi::CString::new(modified.to_string_lossy().as_ref())?;
    let latest_cstr = std::ffi::CString::new(latest.to_string_lossy().as_ref())?;
    let ancestor_cstr = std::ffi::CString::new(ancestor.to_string_lossy().as_ref())?;

    let pool = apr::Pool::new();
    let mut diff_ptr = std::ptr::null_mut();

    // Create diff options
    let diff_options = unsafe { subversion_sys::svn_diff_file_options_create(pool.as_mut_ptr()) };
    unsafe {
        (*diff_options).ignore_space = options.ignore_space.into();
        (*diff_options).ignore_eol_style = if options.ignore_eol_style { 1 } else { 0 };
        (*diff_options).show_c_function = if options.show_c_function { 1 } else { 0 };
    }

    let err = unsafe {
        subversion_sys::svn_diff_file_diff4_2(
            &mut diff_ptr,
            original_cstr.as_ptr(),
            modified_cstr.as_ptr(),
            latest_cstr.as_ptr(),
            ancestor_cstr.as_ptr(),
            diff_options,
            pool.as_mut_ptr(),
        )
    };

    svn_result(err)?;
    Ok(unsafe { Diff::from_raw(diff_ptr, pool) })
}

/// Output unified diff format
pub fn file_output_unified(
    output_stream: &mut crate::io::Stream,
    diff: &Diff,
    original_path: &std::path::Path,
    modified_path: &std::path::Path,
    original_header: Option<&str>,
    modified_header: Option<&str>,
    header_encoding: &str,
    context_size: i32,
) -> Result<(), Error<'static>> {
    let original_path_cstr = std::ffi::CString::new(original_path.to_string_lossy().as_ref())?;
    let modified_path_cstr = std::ffi::CString::new(modified_path.to_string_lossy().as_ref())?;
    let header_encoding_cstr = std::ffi::CString::new(header_encoding)?;

    let original_header_cstr = original_header.map(std::ffi::CString::new).transpose()?;
    let modified_header_cstr = modified_header.map(std::ffi::CString::new).transpose()?;

    with_tmp_pool(|scratch_pool| {
        let err = unsafe {
            subversion_sys::svn_diff_file_output_unified4(
                output_stream.as_mut_ptr(),
                diff.as_ptr(),
                original_path_cstr.as_ptr(),
                modified_path_cstr.as_ptr(),
                original_header_cstr
                    .as_ref()
                    .map_or(std::ptr::null(), |c| c.as_ptr()),
                modified_header_cstr
                    .as_ref()
                    .map_or(std::ptr::null(), |c| c.as_ptr()),
                header_encoding_cstr.as_ptr(),
                std::ptr::null(), // relative_to_dir
                1,                // show_c_function
                context_size,
                None,                 // cancel_func
                std::ptr::null_mut(), // cancel_baton
                scratch_pool.as_mut_ptr(),
            )
        };

        svn_result(err)
    })
}

/// Diff memory strings
pub fn mem_string_diff(
    original: &str,
    modified: &str,
    options: FileOptions,
) -> Result<Diff, Error<'static>> {
    let pool = apr::Pool::new();
    let mut diff_ptr = std::ptr::null_mut();

    // Create svn_string_t structures
    let original_svn_str = subversion_sys::svn_string_t {
        data: original.as_ptr() as *const std::os::raw::c_char,
        len: original.len(),
    };

    let modified_svn_str = subversion_sys::svn_string_t {
        data: modified.as_ptr() as *const std::os::raw::c_char,
        len: modified.len(),
    };

    // Create diff options
    let diff_options = unsafe { subversion_sys::svn_diff_file_options_create(pool.as_mut_ptr()) };
    unsafe {
        (*diff_options).ignore_space = options.ignore_space.into();
        (*diff_options).ignore_eol_style = if options.ignore_eol_style { 1 } else { 0 };
        (*diff_options).show_c_function = if options.show_c_function { 1 } else { 0 };
    }

    let err = unsafe {
        subversion_sys::svn_diff_mem_string_diff(
            &mut diff_ptr,
            &original_svn_str,
            &modified_svn_str,
            diff_options,
            pool.as_mut_ptr(),
        )
    };

    svn_result(err)?;
    Ok(unsafe { Diff::from_raw(diff_ptr, pool) })
}

/// Conflict display style for merge output
#[derive(Debug, Clone, Copy)]
pub enum ConflictDisplayStyle {
    /// Show modified and latest
    ModifiedLatest,
    /// Show resolved conflicts only
    ResolvedModifiedLatest,
    /// Show modified, original, and latest
    ModifiedOriginalLatest,
    /// Show only modified
    OnlyConflicts,
}

impl From<ConflictDisplayStyle> for subversion_sys::svn_diff_conflict_display_style_t {
    fn from(style: ConflictDisplayStyle) -> Self {
        match style {
            ConflictDisplayStyle::ModifiedLatest => {
                subversion_sys::svn_diff_conflict_display_style_t_svn_diff_conflict_display_modified_latest
            }
            ConflictDisplayStyle::ResolvedModifiedLatest => {
                subversion_sys::svn_diff_conflict_display_style_t_svn_diff_conflict_display_resolved_modified_latest
            }
            ConflictDisplayStyle::ModifiedOriginalLatest => {
                subversion_sys::svn_diff_conflict_display_style_t_svn_diff_conflict_display_modified_original_latest
            }
            ConflictDisplayStyle::OnlyConflicts => {
                subversion_sys::svn_diff_conflict_display_style_t_svn_diff_conflict_display_only_conflicts
            }
        }
    }
}

/// Output merge result with conflict markers
pub fn file_output_merge(
    output_stream: &mut crate::io::Stream,
    diff: &Diff,
    original_path: &std::path::Path,
    modified_path: &std::path::Path,
    latest_path: &std::path::Path,
    conflict_original: Option<&str>,
    conflict_modified: Option<&str>,
    conflict_latest: Option<&str>,
    conflict_separator: Option<&str>,
    conflict_style: ConflictDisplayStyle,
) -> Result<(), Error<'static>> {
    let original_path_cstr = std::ffi::CString::new(original_path.to_string_lossy().as_ref())?;
    let modified_path_cstr = std::ffi::CString::new(modified_path.to_string_lossy().as_ref())?;
    let latest_path_cstr = std::ffi::CString::new(latest_path.to_string_lossy().as_ref())?;

    let conflict_original_cstr = conflict_original.map(std::ffi::CString::new).transpose()?;
    let conflict_modified_cstr = conflict_modified.map(std::ffi::CString::new).transpose()?;
    let conflict_latest_cstr = conflict_latest.map(std::ffi::CString::new).transpose()?;
    let conflict_separator_cstr = conflict_separator.map(std::ffi::CString::new).transpose()?;

    with_tmp_pool(|scratch_pool| {
        let err = unsafe {
            subversion_sys::svn_diff_file_output_merge3(
                output_stream.as_mut_ptr(),
                diff.as_ptr(),
                original_path_cstr.as_ptr(),
                modified_path_cstr.as_ptr(),
                latest_path_cstr.as_ptr(),
                conflict_original_cstr
                    .as_ref()
                    .map_or(std::ptr::null(), |c| c.as_ptr()),
                conflict_modified_cstr
                    .as_ref()
                    .map_or(std::ptr::null(), |c| c.as_ptr()),
                conflict_latest_cstr
                    .as_ref()
                    .map_or(std::ptr::null(), |c| c.as_ptr()),
                conflict_separator_cstr
                    .as_ref()
                    .map_or(std::ptr::null(), |c| c.as_ptr()),
                conflict_style.into(),
                None,                 // cancel_func
                std::ptr::null_mut(), // cancel_baton
                scratch_pool.as_mut_ptr(),
            )
        };

        svn_result(err)
    })
}

/// Generic diff output using callback functions
///
/// # Safety
///
/// The caller must ensure that `output_baton` and `output_fns` are valid and compatible.
/// The function pointers in `output_fns` must be safe to call with the provided baton.
pub unsafe fn output(
    diff: &Diff,
    output_baton: *mut std::ffi::c_void,
    output_fns: &subversion_sys::svn_diff_output_fns_t,
) -> Result<(), Error<'static>> {
    let err = unsafe {
        subversion_sys::svn_diff_output2(
            diff.as_ptr(),
            output_baton,
            output_fns,
            None,                 // cancel_func
            std::ptr::null_mut(), // cancel_baton
        )
    };

    svn_result(err)
}

/// Output unified diff with more options
pub fn file_output_unified_with_options(
    output_stream: &mut crate::io::Stream,
    diff: &Diff,
    original_path: &std::path::Path,
    modified_path: &std::path::Path,
    original_header: Option<&str>,
    modified_header: Option<&str>,
    header_encoding: &str,
    relative_to_dir: Option<&std::path::Path>,
    show_c_function: bool,
    context_size: i32,
) -> Result<(), Error<'static>> {
    let original_path_cstr = std::ffi::CString::new(original_path.to_string_lossy().as_ref())?;
    let modified_path_cstr = std::ffi::CString::new(modified_path.to_string_lossy().as_ref())?;
    let header_encoding_cstr = std::ffi::CString::new(header_encoding)?;

    let original_header_cstr = original_header.map(std::ffi::CString::new).transpose()?;
    let modified_header_cstr = modified_header.map(std::ffi::CString::new).transpose()?;
    let relative_to_dir_cstr = relative_to_dir
        .map(|p| std::ffi::CString::new(p.to_string_lossy().as_ref()))
        .transpose()?;

    with_tmp_pool(|scratch_pool| {
        let err = unsafe {
            subversion_sys::svn_diff_file_output_unified4(
                output_stream.as_mut_ptr(),
                diff.as_ptr(),
                original_path_cstr.as_ptr(),
                modified_path_cstr.as_ptr(),
                original_header_cstr
                    .as_ref()
                    .map_or(std::ptr::null(), |c| c.as_ptr()),
                modified_header_cstr
                    .as_ref()
                    .map_or(std::ptr::null(), |c| c.as_ptr()),
                header_encoding_cstr.as_ptr(),
                relative_to_dir_cstr
                    .as_ref()
                    .map_or(std::ptr::null(), |c| c.as_ptr()),
                if show_c_function { 1 } else { 0 },
                context_size,
                None,                 // cancel_func
                std::ptr::null_mut(), // cancel_baton
                scratch_pool.as_mut_ptr(),
            )
        };

        svn_result(err)
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::tempdir;

    #[test]
    fn test_file_options() {
        let options = FileOptions::default()
            .with_ignore_whitespace(true)
            .with_ignore_eol_style(true)
            .with_show_c_function(false);

        assert_eq!(options.ignore_space, IgnoreSpace::Change);
        assert!(options.ignore_eol_style);
        assert!(!options.show_c_function);
    }

    #[test]
    fn test_diff_options_builder() {
        let options = DiffOptions::new()
            .with_ignore_whitespace(true)
            .with_ignore_eol_style(true)
            .with_show_c_function(true);
        assert!(options.ignore_whitespace);
        assert!(options.ignore_eol_style);
        assert!(options.show_c_function);

        // Each setter only flips its own field.
        let only_ws = DiffOptions::new().with_ignore_whitespace(true);
        assert!(only_ws.ignore_whitespace);
        assert!(!only_ws.ignore_eol_style);
        assert!(!only_ws.show_c_function);

        let only_eol = DiffOptions::new().with_ignore_eol_style(true);
        assert!(!only_eol.ignore_whitespace);
        assert!(only_eol.ignore_eol_style);
        assert!(!only_eol.show_c_function);

        let only_cfn = DiffOptions::new().with_show_c_function(true);
        assert!(!only_cfn.ignore_whitespace);
        assert!(!only_cfn.ignore_eol_style);
        assert!(only_cfn.show_c_function);
    }

    #[test]
    fn test_ignore_space_into_raw() {
        use subversion_sys::*;
        assert_eq!(
            svn_diff_file_ignore_space_t::from(IgnoreSpace::None),
            svn_diff_file_ignore_space_t_svn_diff_file_ignore_space_none
        );
        assert_eq!(
            svn_diff_file_ignore_space_t::from(IgnoreSpace::Change),
            svn_diff_file_ignore_space_t_svn_diff_file_ignore_space_change
        );
        assert_eq!(
            svn_diff_file_ignore_space_t::from(IgnoreSpace::All),
            svn_diff_file_ignore_space_t_svn_diff_file_ignore_space_all
        );
    }

    #[test]
    fn test_file_diff3_conflict() -> Result<(), Box<dyn std::error::Error>> {
        let temp_dir = tempdir()?;
        let original = temp_dir.path().join("orig.txt");
        let modified = temp_dir.path().join("mine.txt");
        let latest = temp_dir.path().join("theirs.txt");

        // Both sides change the same line differently -> a conflict.
        std::fs::write(&original, b"line 1\nline 2\nline 3\n")?;
        std::fs::write(&modified, b"line 1\nMINE\nline 3\n")?;
        std::fs::write(&latest, b"line 1\nTHEIRS\nline 3\n")?;

        let diff = file_diff3(&original, &modified, &latest, FileOptions::default())?;
        assert!(diff.contains_changes());
        assert!(diff.contains_conflicts());

        Ok(())
    }

    #[test]
    fn test_mem_string_diff() {
        let original = "line 1\nline 2\nline 3\n";
        let modified = "line 1\nline 2 modified\nline 3\n";

        let options = FileOptions::default();
        let diff = mem_string_diff(original, modified, options).unwrap();
        assert!(diff.contains_changes());
    }

    #[test]
    fn test_file_diff() -> Result<(), Box<dyn std::error::Error>> {
        let temp_dir = tempdir()?;

        // Create test files
        let original_path = temp_dir.path().join("original.txt");
        let modified_path = temp_dir.path().join("modified.txt");

        let mut original_file = std::fs::File::create(&original_path)?;
        original_file.write_all(b"line 1\nline 2\nline 3\n")?;

        let mut modified_file = std::fs::File::create(&modified_path)?;
        modified_file.write_all(b"line 1\nline 2 modified\nline 3\n")?;

        let options = FileOptions::default();
        let diff = file_diff(&original_path, &modified_path, options)?;

        assert!(diff.contains_changes());
        assert!(!diff.contains_conflicts());

        Ok(())
    }

    #[test]
    fn test_diff_identical_files() -> Result<(), Box<dyn std::error::Error>> {
        let temp_dir = tempdir()?;

        // Create identical test files
        let original_path = temp_dir.path().join("original.txt");
        let modified_path = temp_dir.path().join("modified.txt");

        let content = b"line 1\nline 2\nline 3\n";
        std::fs::write(&original_path, content)?;
        std::fs::write(&modified_path, content)?;

        let options = FileOptions::default();
        let diff = file_diff(&original_path, &modified_path, options)?;

        assert!(!diff.contains_changes());

        Ok(())
    }

    #[test]
    fn test_file_output_unified() -> Result<(), Box<dyn std::error::Error>> {
        let temp_dir = tempdir()?;

        // Create test files
        let original_path = temp_dir.path().join("original.txt");
        let modified_path = temp_dir.path().join("modified.txt");

        std::fs::write(&original_path, b"line 1\nline 2\nline 3\n")?;
        std::fs::write(&modified_path, b"line 1\nline 2 modified\nline 3\nline 4\n")?;

        let options = FileOptions::default();
        let diff = file_diff(&original_path, &modified_path, options)?;

        // Create output stream using a string buffer
        let mut stringbuf = crate::io::StringBuf::new();
        let mut stream = crate::io::Stream::from_stringbuf(&mut stringbuf);

        // Generate unified diff output
        file_output_unified(
            &mut stream,
            &diff,
            &original_path,
            &modified_path,
            Some("Original File"),
            Some("Modified File"),
            "UTF-8",
            3,
        )?;

        // Verify output contains unified diff markers
        let output = stringbuf.to_string();
        assert!(output.contains("---"));
        assert!(output.contains("+++"));
        assert!(output.contains("@@"));

        Ok(())
    }

    #[test]
    fn test_file_output_merge() -> Result<(), Box<dyn std::error::Error>> {
        let temp_dir = tempdir()?;

        // Create test files for 3-way merge
        let original_path = temp_dir.path().join("original.txt");
        let modified_path = temp_dir.path().join("modified.txt");
        let latest_path = temp_dir.path().join("latest.txt");

        std::fs::write(&original_path, b"line 1\nline 2\nline 3\n")?;
        std::fs::write(&modified_path, b"line 1\nline 2 modified\nline 3\n")?;
        std::fs::write(&latest_path, b"line 1\nline 2 latest\nline 3\n")?;

        let options = FileOptions::default();
        let diff = file_diff3(&modified_path, &original_path, &latest_path, options)?;

        // Create output stream using a string buffer
        let mut stringbuf = crate::io::StringBuf::new();
        let mut stream = crate::io::Stream::from_stringbuf(&mut stringbuf);

        // Generate merge output with default conflict markers
        file_output_merge(
            &mut stream,
            &diff,
            &original_path,
            &modified_path,
            &latest_path,
            None,
            None,
            None,
            None,
            ConflictDisplayStyle::ModifiedLatest,
        )?;

        // Verify output was generated
        let output = stringbuf.to_string();
        assert!(!output.is_empty());

        Ok(())
    }

    #[test]
    fn test_file_output_merge_with_diff3_style() -> Result<(), Box<dyn std::error::Error>> {
        let temp_dir = tempdir()?;

        // Create test files with conflicting changes
        let original_path = temp_dir.path().join("original.txt");
        let modified_path = temp_dir.path().join("modified.txt");
        let latest_path = temp_dir.path().join("latest.txt");

        std::fs::write(&original_path, b"common line\noriginal line\ncommon end\n")?;
        std::fs::write(&modified_path, b"common line\nmodified line\ncommon end\n")?;
        std::fs::write(&latest_path, b"common line\nlatest line\ncommon end\n")?;

        let options = FileOptions::default();
        let diff = file_diff3(&modified_path, &original_path, &latest_path, options)?;

        // Create output stream using a string buffer
        let mut stringbuf = crate::io::StringBuf::new();
        let mut stream = crate::io::Stream::from_stringbuf(&mut stringbuf);

        // Generate merge output with diff3-style conflict markers
        file_output_merge(
            &mut stream,
            &diff,
            &original_path,
            &modified_path,
            &latest_path,
            Some("Modified"),
            Some("Original"),
            Some("Latest"),
            Some("======="),
            ConflictDisplayStyle::ModifiedOriginalLatest,
        )?;

        // Verify output was generated
        let output = stringbuf.to_string();
        assert!(!output.is_empty());

        Ok(())
    }

    #[test]
    fn test_file_output_unified_with_options() -> Result<(), Box<dyn std::error::Error>> {
        let temp_dir = tempdir()?;

        // Create test files
        let original_path = temp_dir.path().join("src/original.c");
        let modified_path = temp_dir.path().join("src/modified.c");

        // Create parent directory
        std::fs::create_dir_all(original_path.parent().unwrap())?;

        let original_content = b"void foo() {\n    int x = 1;\n}\n\nvoid bar() {\n    return;\n}\n";
        let modified_content = b"void foo() {\n    int x = 2;\n}\n\nvoid bar() {\n    return;\n}\n";

        std::fs::write(&original_path, original_content)?;
        std::fs::write(&modified_path, modified_content)?;

        let options = FileOptions::default();
        let diff = file_diff(&original_path, &modified_path, options)?;

        // Create output stream using a string buffer
        let mut stringbuf = crate::io::StringBuf::new();
        let mut stream = crate::io::Stream::from_stringbuf(&mut stringbuf);

        // Generate unified diff with options
        file_output_unified_with_options(
            &mut stream,
            &diff,
            &original_path,
            &modified_path,
            Some("Original Version"),
            Some("Modified Version"),
            "UTF-8",
            Some(&temp_dir.path()),
            true, // show_c_function
            5,    // context_size
        )?;

        // Verify output was generated
        let output = stringbuf.to_string();
        assert!(output.contains("---"));
        assert!(output.contains("+++"));

        Ok(())
    }

    #[test]
    fn test_conflict_display_style() {
        // Test the ConflictDisplayStyle enum conversions
        let style: subversion_sys::svn_diff_conflict_display_style_t =
            ConflictDisplayStyle::ModifiedLatest.into();
        assert_eq!(style, subversion_sys::svn_diff_conflict_display_style_t_svn_diff_conflict_display_modified_latest);

        let style: subversion_sys::svn_diff_conflict_display_style_t =
            ConflictDisplayStyle::ModifiedOriginalLatest.into();
        assert_eq!(style, subversion_sys::svn_diff_conflict_display_style_t_svn_diff_conflict_display_modified_original_latest);

        let style: subversion_sys::svn_diff_conflict_display_style_t =
            ConflictDisplayStyle::ResolvedModifiedLatest.into();
        assert_eq!(style, subversion_sys::svn_diff_conflict_display_style_t_svn_diff_conflict_display_resolved_modified_latest);

        let style: subversion_sys::svn_diff_conflict_display_style_t =
            ConflictDisplayStyle::OnlyConflicts.into();
        assert_eq!(style, subversion_sys::svn_diff_conflict_display_style_t_svn_diff_conflict_display_only_conflicts);
    }

    #[test]
    fn test_file_diff4() -> Result<(), Box<dyn std::error::Error>> {
        let temp_dir = tempdir()?;

        // Create test files for 4-way diff
        let original_path = temp_dir.path().join("original.txt");
        let modified_path = temp_dir.path().join("modified.txt");
        let latest_path = temp_dir.path().join("latest.txt");
        let ancestor_path = temp_dir.path().join("ancestor.txt");

        std::fs::write(&original_path, b"line 1\nline 2\nline 3\n")?;
        std::fs::write(&modified_path, b"line 1 modified\nline 2\nline 3\n")?;
        std::fs::write(&latest_path, b"line 1\nline 2 latest\nline 3\n")?;
        std::fs::write(&ancestor_path, b"line 1\nline 2\nline 3\n")?;

        let options = FileOptions::default();
        let diff = file_diff4(
            &original_path,
            &modified_path,
            &latest_path,
            &ancestor_path,
            options,
        )?;

        // Should have changes since modified and latest differ from original
        assert!(diff.contains_changes());

        Ok(())
    }

    #[test]
    fn test_parse_patch_file_and_hunks() -> Result<(), Box<dyn std::error::Error>> {
        let temp_dir = tempdir()?;
        let patch_path = temp_dir.path().join("change.patch");
        // One file, one hunk whose every coordinate is distinct and not 0/1, so
        // the per-accessor constant-replacement mutants are all caught. Leading
        // spaces on context lines are significant, so avoid line continuations.
        let patch = "--- a/file.txt\n+++ b/file.txt\n@@ -5,4 +8,5 @@\n c1\n c2\n-old\n+new1\n+new2\n c3\n c4\n";
        std::fs::write(&patch_path, patch)?;

        let mut patch_file = PatchFile::open(&patch_path)?;

        let patch = patch_file
            .next_patch(false, false)?
            .expect("expected one patch");
        assert_eq!(patch.old_filename(), Some("a/file.txt"));
        assert_eq!(patch.new_filename(), Some("b/file.txt"));
        assert_eq!(patch.operation(), PatchOperation::Unchanged);

        let hunks = patch.hunks();
        assert_eq!(hunks.len(), 1);
        let hunk = &hunks[0];
        // Original side starts at line 5, modified side at line 8; two context
        // lines lead and two trail the single changed line.
        assert_eq!(hunk.original_start(), 5);
        assert_eq!(hunk.original_length(), 5);
        assert_eq!(hunk.modified_start(), 8);
        assert_eq!(hunk.modified_length(), 6);
        assert_eq!(hunk.leading_context(), 2);
        assert_eq!(hunk.trailing_context(), 2);

        // The file contains only one patch.
        assert!(patch_file.next_patch(false, false)?.is_none());

        Ok(())
    }

    #[test]
    fn test_patch_operation_from() {
        use subversion_sys::*;
        assert_eq!(
            PatchOperation::from(svn_diff_operation_kind_e_svn_diff_op_unchanged),
            PatchOperation::Unchanged
        );
        assert_eq!(
            PatchOperation::from(svn_diff_operation_kind_e_svn_diff_op_added),
            PatchOperation::Added
        );
        assert_eq!(
            PatchOperation::from(svn_diff_operation_kind_e_svn_diff_op_deleted),
            PatchOperation::Deleted
        );
        assert_eq!(
            PatchOperation::from(svn_diff_operation_kind_e_svn_diff_op_copied),
            PatchOperation::Copied
        );
        assert_eq!(
            PatchOperation::from(svn_diff_operation_kind_e_svn_diff_op_moved),
            PatchOperation::Moved
        );
    }
}
