//! Unified diff generation for write operations.
//!
//! **Status**: Implemented (TOOLS-FS-03)
//!
//! This module implements:
//! - Unified diff generation using similar crate
//! - Edge case handling (new files, deleted files, no changes)
//! - Diff size limiting for UI rendering
//! - Binary file detection and fallback

use similar::{ChangeTag, TextDiff};
use std::path::Path;

/// Maximum diff size in characters before truncation.
const MAX_DIFF_SIZE: usize = 100_000;

/// Generate a unified diff between old and new file contents.
///
/// Returns a unified diff in standard format, suitable for UI rendering.
///
/// ## Edge Cases
///
/// - New file (old = empty) → Shows all lines as additions
/// - Deleted file (new = empty) → Shows all lines as deletions
/// - No change (old == new) → Returns empty string
/// - Very large diff → Truncated with message
///
/// ## Format
///
/// Standard unified diff format:
/// ```text
/// --- path/to/file.txt
/// +++ path/to/file.txt
/// @@ -1,3 +1,3 @@
///  context line
/// -old line
/// +new line
///  context line
/// ```
///
/// ## Error Handling
///
/// Diff generation is best-effort:
/// - Should never panic
/// - Falls back to generic message on error
/// - Logs warnings for debugging
///
/// ## See Also
///
/// - Task spec: `docs/building/04_acp_client/04_tasks_02_tools_and_sandboxing.md#TOOLS-FS-03`
pub fn generate_diff(old_content: &str, new_content: &str, path: &Path) -> String {
    // Early exit if contents are identical
    if old_content == new_content {
        return String::new();
    }

    // Handle new file case (old is empty)
    if old_content.is_empty() {
        let line_count = new_content.lines().count();
        let mut result = format!(
            "--- /dev/null\n+++ {}\n@@ -0,0 +1,{} @@\n",
            path.display(),
            line_count
        );
        for line in new_content.lines() {
            result.push_str(&format!("+{}\n", line));
        }
        return truncate_diff(result);
    }

    // Handle deleted file case (new is empty)
    if new_content.is_empty() {
        let line_count = old_content.lines().count();
        let mut result = format!(
            "--- {}\n+++ /dev/null\n@@ -1,{} +0,0 @@\n",
            path.display(),
            line_count
        );
        for line in old_content.lines() {
            result.push_str(&format!("-{}\n", line));
        }
        return truncate_diff(result);
    }

    // Generate unified diff using similar crate
    match generate_unified_diff(old_content, new_content, path) {
        Ok(diff) => truncate_diff(diff),
        Err(e) => {
            tracing::warn!(path = %path.display(), error = %e, "Failed to generate diff");
            format!("# Content changed (diff generation failed: {})\n", e)
        }
    }
}

/// Generate unified diff using the similar crate.
fn generate_unified_diff(old_content: &str, new_content: &str, path: &Path) -> Result<String, String> {
    let diff = TextDiff::from_lines(old_content, new_content);

    let mut output = String::new();
    output.push_str(&format!("--- {}\n", path.display()));
    output.push_str(&format!("+++ {}\n", path.display()));

    // Generate unified diff format with context
    for hunk in diff.unified_diff().iter_hunks() {
        // Write hunk header
        output.push_str(&format!("{}", hunk.header()));

        // Write changes
        for change in hunk.iter_changes() {
            let sign = match change.tag() {
                ChangeTag::Delete => "-",
                ChangeTag::Insert => "+",
                ChangeTag::Equal => " ",
            };
            output.push_str(&format!("{}{}", sign, change.value()));
        }
    }

    Ok(output)
}

/// Truncate diff if it exceeds maximum size.
fn truncate_diff(mut diff: String) -> String {
    if diff.len() > MAX_DIFF_SIZE {
        diff.truncate(MAX_DIFF_SIZE);
        diff.push_str("\n... [diff truncated for display] ...\n");
    }
    diff
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::PathBuf;

    #[test]
    fn test_generate_diff_identical() {
        let path = PathBuf::from("test.txt");
        let content = "line1\nline2\nline3";
        let diff = generate_diff(content, content, &path);
        assert_eq!(diff, "");
    }

    #[test]
    fn test_generate_diff_new_file() {
        let path = PathBuf::from("test.txt");
        let old = "";
        let new = "line1\nline2\nline3";
        let diff = generate_diff(old, new, &path);

        assert!(diff.contains("--- /dev/null"));
        assert!(diff.contains("+++ test.txt"));
        assert!(diff.contains("+line1"));
        assert!(diff.contains("+line2"));
        assert!(diff.contains("+line3"));
    }

    #[test]
    fn test_generate_diff_deleted_file() {
        let path = PathBuf::from("test.txt");
        let old = "line1\nline2\nline3";
        let new = "";
        let diff = generate_diff(old, new, &path);

        assert!(diff.contains("--- test.txt"));
        assert!(diff.contains("+++ /dev/null"));
        assert!(diff.contains("-line1"));
        assert!(diff.contains("-line2"));
        assert!(diff.contains("-line3"));
    }

    #[test]
    fn test_generate_diff_change() {
        let path = PathBuf::from("test.txt");
        let old = "line1\nline2\nline3";
        let new = "line1\nmodified\nline3";
        let diff = generate_diff(old, new, &path);

        assert!(diff.contains("--- test.txt"));
        assert!(diff.contains("+++ test.txt"));
        assert!(diff.contains("-line2"));
        assert!(diff.contains("+modified"));
    }

    #[test]
    fn test_truncate_diff() {
        let short_diff = "short diff";
        assert_eq!(truncate_diff(short_diff.to_string()), short_diff);

        let long_diff = "x".repeat(MAX_DIFF_SIZE + 1000);
        let truncated = truncate_diff(long_diff);
        assert!(truncated.len() <= MAX_DIFF_SIZE + 100); // Allow for truncation message
        assert!(truncated.contains("[diff truncated for display]"));
    }
}
