//! Integration tests for file operations (read, write, diff, edit).
//!
//! These tests use real filesystem operations with temporary directories
//! to verify end-to-end functionality including sandboxing and error handling.

use dirigent_tools::config::{EolPolicy, PermissionConfig, SandboxConfig};
use dirigent_tools::error::ToolError;
use dirigent_tools::fs::diff::generate_diff;
use dirigent_tools::fs::edit::{edit_file, EditFileRequest, EditOperation};
use dirigent_tools::fs::read::{read_text_file, ReadTextFileRequest};
use dirigent_tools::fs::write::{write_text_file, WriteTextFileRequest};
use dirigent_tools::permission::check::PermissionContext;
use dirigent_tools::permission::whitelist::CompiledWhitelist;
use std::path::PathBuf;
use tempfile::TempDir;

/// Create a test configuration with a single allowed root.
fn test_config(allowed_root: PathBuf) -> SandboxConfig {
    let mut config = SandboxConfig::default();
    // Ensure the root is canonical
    let canonical_root = dunce::canonicalize(&allowed_root)
        .unwrap_or_else(|_| panic!("Failed to canonicalize test root: {:?}", allowed_root));
    config.allowed_roots = vec![canonical_root];
    config.write_enabled = true;
    config.read_enabled = true;
    config
}

/// Create a test permission config with YOLO mode (no prompts).
fn test_permission_config() -> PermissionConfig {
    PermissionConfig::default()
}

/// Create a test permission context for a test connector.
fn test_permission_context() -> PermissionContext {
    let whitelist = CompiledWhitelist::compile(&Default::default()).unwrap();
    PermissionContext::new(
        "test-connector".to_string(),
        Some("test-session".to_string()),
        whitelist,
    )
}

// =============================================================================
// Read Operation Tests
// =============================================================================

#[tokio::test]
async fn test_read_entire_file() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("test.txt");

    // Create test file
    std::fs::write(&file_path, "line1\nline2\nline3").unwrap();

    let config = test_config(temp_dir.path().to_path_buf());
    let request = ReadTextFileRequest {
        path: file_path.to_string_lossy().to_string(),
        line: None,
        limit: None,
    };

    let response = read_text_file(request, &config).await.unwrap();
    assert_eq!(response.content, "line1\nline2\nline3");
}

#[tokio::test]
async fn test_read_with_line_limit() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("test.txt");

    // Create test file
    std::fs::write(&file_path, "line1\nline2\nline3\nline4\nline5").unwrap();

    let config = test_config(temp_dir.path().to_path_buf());

    // Read from line 2, limit 2 lines
    let request = ReadTextFileRequest {
        path: file_path.to_string_lossy().to_string(),
        line: Some(2),
        limit: Some(2),
    };

    let response = read_text_file(request, &config).await.unwrap();
    assert_eq!(response.content, "line2\nline3");
}

#[tokio::test]
async fn test_read_only_limit() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("test.txt");

    std::fs::write(&file_path, "line1\nline2\nline3\nline4").unwrap();

    let config = test_config(temp_dir.path().to_path_buf());
    let request = ReadTextFileRequest {
        path: file_path.to_string_lossy().to_string(),
        line: None,
        limit: Some(2),
    };

    let response = read_text_file(request, &config).await.unwrap();
    assert_eq!(response.content, "line1\nline2");
}

#[tokio::test]
async fn test_read_file_not_found() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("nonexistent.txt");

    let config = test_config(temp_dir.path().to_path_buf());
    let request = ReadTextFileRequest {
        path: file_path.to_string_lossy().to_string(),
        line: None,
        limit: None,
    };

    let result = read_text_file(request, &config).await;
    assert!(matches!(result, Err(ToolError::NotFound { .. })));
}

#[tokio::test]
async fn test_read_non_utf8() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("binary.bin");

    // Write invalid UTF-8
    std::fs::write(&file_path, &[0xFF, 0xFE, 0xFD]).unwrap();

    let config = test_config(temp_dir.path().to_path_buf());
    let request = ReadTextFileRequest {
        path: file_path.to_string_lossy().to_string(),
        line: None,
        limit: None,
    };

    let result = read_text_file(request, &config).await;
    assert!(matches!(result, Err(ToolError::EncodingUnsupported { .. })));
}

#[tokio::test]
async fn test_read_outside_sandbox() {
    let temp_dir = TempDir::new().unwrap();
    let other_temp_dir = TempDir::new().unwrap();
    let file_path = other_temp_dir.path().join("test.txt");

    std::fs::write(&file_path, "content").unwrap();

    // Config only allows temp_dir, not other_temp_dir
    let config = test_config(temp_dir.path().to_path_buf());
    let request = ReadTextFileRequest {
        path: file_path.to_string_lossy().to_string(),
        line: None,
        limit: None,
    };

    let result = read_text_file(request, &config).await;
    assert!(matches!(result, Err(ToolError::SandboxViolation { .. })));
}

#[tokio::test]
async fn test_read_disabled() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("test.txt");
    std::fs::write(&file_path, "content").unwrap();

    let mut config = test_config(temp_dir.path().to_path_buf());
    config.read_enabled = false;

    let request = ReadTextFileRequest {
        path: file_path.to_string_lossy().to_string(),
        line: None,
        limit: None,
    };

    let result = read_text_file(request, &config).await;
    assert!(matches!(result, Err(ToolError::PermissionDenied { .. })));
}

// =============================================================================
// Write Operation Tests
// =============================================================================

#[tokio::test]
async fn test_write_new_file() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("new.txt");

    let config = test_config(temp_dir.path().to_path_buf());
    let request = WriteTextFileRequest {
        path: file_path.to_string_lossy().to_string(),
        content: "hello world".to_string(),
    };

    write_text_file(request, &config, &test_permission_config(), &test_permission_context()).await.unwrap();

    // Verify file was written
    let content = std::fs::read_to_string(&file_path).unwrap();
    assert_eq!(content, "hello world");
}

#[tokio::test]
async fn test_write_overwrite_existing() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("existing.txt");

    std::fs::write(&file_path, "old content").unwrap();

    let config = test_config(temp_dir.path().to_path_buf());
    let request = WriteTextFileRequest {
        path: file_path.to_string_lossy().to_string(),
        content: "new content".to_string(),
    };

    write_text_file(request, &config, &test_permission_config(), &test_permission_context()).await.unwrap();

    let content = std::fs::read_to_string(&file_path).unwrap();
    assert_eq!(content, "new content");
}

#[tokio::test]
async fn test_write_create_parent_directories() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("subdir").join("nested").join("file.txt");

    let config = test_config(temp_dir.path().to_path_buf());
    let request = WriteTextFileRequest {
        path: file_path.to_string_lossy().to_string(),
        content: "nested content".to_string(),
    };

    write_text_file(request, &config, &test_permission_config(), &test_permission_context()).await.unwrap();

    let content = std::fs::read_to_string(&file_path).unwrap();
    assert_eq!(content, "nested content");
}

#[tokio::test]
async fn test_write_eol_normalization_lf() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("eol_lf.txt");

    let mut config = test_config(temp_dir.path().to_path_buf());
    config.eol_policy = EolPolicy::Lf;

    let request = WriteTextFileRequest {
        path: file_path.to_string_lossy().to_string(),
        content: "line1\r\nline2\rline3\n".to_string(),
    };

    write_text_file(request, &config, &test_permission_config(), &test_permission_context()).await.unwrap();

    let content = std::fs::read_to_string(&file_path).unwrap();
    assert_eq!(content, "line1\nline2\nline3\n");
}

#[tokio::test]
async fn test_write_eol_normalization_crlf() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("eol_crlf.txt");

    let mut config = test_config(temp_dir.path().to_path_buf());
    config.eol_policy = EolPolicy::Crlf;

    let request = WriteTextFileRequest {
        path: file_path.to_string_lossy().to_string(),
        content: "line1\nline2\rline3\r\n".to_string(),
    };

    write_text_file(request, &config, &test_permission_config(), &test_permission_context()).await.unwrap();

    let content = std::fs::read_to_string(&file_path).unwrap();
    assert_eq!(content, "line1\r\nline2\r\nline3\r\n");
}

#[tokio::test]
async fn test_write_size_limit_exceeded() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("large.txt");

    let mut config = test_config(temp_dir.path().to_path_buf());
    config.max_write_bytes = 10;

    let request = WriteTextFileRequest {
        path: file_path.to_string_lossy().to_string(),
        content: "this is way too much content".to_string(),
    };

    let result = write_text_file(request, &config, &test_permission_config(), &test_permission_context()).await;
    assert!(matches!(result, Err(ToolError::FileTooLarge { .. })));
}

#[tokio::test]
async fn test_write_outside_sandbox() {
    let temp_dir = TempDir::new().unwrap();
    let other_temp_dir = TempDir::new().unwrap();
    let file_path = other_temp_dir.path().join("test.txt");

    let config = test_config(temp_dir.path().to_path_buf());
    let request = WriteTextFileRequest {
        path: file_path.to_string_lossy().to_string(),
        content: "content".to_string(),
    };

    let result = write_text_file(request, &config, &test_permission_config(), &test_permission_context()).await;
    assert!(matches!(result, Err(ToolError::SandboxViolation { .. })));
}

#[tokio::test]
async fn test_write_disabled() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("test.txt");

    let mut config = test_config(temp_dir.path().to_path_buf());
    config.write_enabled = false;

    let request = WriteTextFileRequest {
        path: file_path.to_string_lossy().to_string(),
        content: "content".to_string(),
    };

    let result = write_text_file(request, &config, &test_permission_config(), &test_permission_context()).await;
    assert!(matches!(result, Err(ToolError::PermissionDenied { .. })));
}

// =============================================================================
// Diff Generation Tests
// =============================================================================

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

    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_diff_deleted_file() {
    let old = "line1\nline2\nline3";
    let new = "";
    let path = PathBuf::from("test.txt");

    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_diff_modification() {
    let old = "line1\nline2\nline3";
    let new = "line1\nmodified\nline3";
    let path = PathBuf::from("test.txt");

    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"));
}

// =============================================================================
// Edit Operation Tests
// =============================================================================

#[tokio::test]
async fn test_edit_replace_once() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("edit.txt");

    std::fs::write(&file_path, "foo bar foo baz").unwrap();

    let config = test_config(temp_dir.path().to_path_buf());
    let request = EditFileRequest {
        path: file_path.to_string_lossy().to_string(),
        edits: vec![EditOperation::Replace {
            old_text: "foo".to_string(),
            new_text: "qux".to_string(),
            replace_all: false,
        }],
    };

    let response = edit_file(request, &config, &test_permission_config(), &test_permission_context()).await.unwrap();

    // Verify file was edited
    let content = std::fs::read_to_string(&file_path).unwrap();
    assert_eq!(content, "qux bar foo baz");

    // Verify diff was generated
    assert!(response.diff.contains("-foo"));
    assert!(response.diff.contains("+qux"));
}

#[tokio::test]
async fn test_edit_replace_all() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("edit.txt");

    std::fs::write(&file_path, "foo bar foo baz foo").unwrap();

    let config = test_config(temp_dir.path().to_path_buf());
    let request = EditFileRequest {
        path: file_path.to_string_lossy().to_string(),
        edits: vec![EditOperation::Replace {
            old_text: "foo".to_string(),
            new_text: "qux".to_string(),
            replace_all: true,
        }],
    };

    edit_file(request, &config, &test_permission_config(), &test_permission_context()).await.unwrap();

    let content = std::fs::read_to_string(&file_path).unwrap();
    assert_eq!(content, "qux bar qux baz qux");
}

#[tokio::test]
async fn test_edit_multiple_operations() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("edit.txt");

    std::fs::write(&file_path, "hello world\nhello rust").unwrap();

    let config = test_config(temp_dir.path().to_path_buf());
    let request = EditFileRequest {
        path: file_path.to_string_lossy().to_string(),
        edits: vec![
            EditOperation::Replace {
                old_text: "hello".to_string(),
                new_text: "goodbye".to_string(),
                replace_all: true,
            },
            EditOperation::Replace {
                old_text: "world".to_string(),
                new_text: "universe".to_string(),
                replace_all: false,
            },
        ],
    };

    edit_file(request, &config, &test_permission_config(), &test_permission_context()).await.unwrap();

    let content = std::fs::read_to_string(&file_path).unwrap();
    assert_eq!(content, "goodbye universe\ngoodbye rust");
}

#[tokio::test]
async fn test_edit_nonexistent_file() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("nonexistent.txt");

    let config = test_config(temp_dir.path().to_path_buf());
    let request = EditFileRequest {
        path: file_path.to_string_lossy().to_string(),
        edits: vec![EditOperation::Replace {
            old_text: "foo".to_string(),
            new_text: "bar".to_string(),
            replace_all: false,
        }],
    };

    let result = edit_file(request, &config, &test_permission_config(), &test_permission_context()).await;
    assert!(matches!(result, Err(ToolError::NotFound { .. })));
}

#[tokio::test]
async fn test_edit_outside_sandbox() {
    let temp_dir = TempDir::new().unwrap();
    let other_temp_dir = TempDir::new().unwrap();
    let file_path = other_temp_dir.path().join("test.txt");

    std::fs::write(&file_path, "content").unwrap();

    let config = test_config(temp_dir.path().to_path_buf());
    let request = EditFileRequest {
        path: file_path.to_string_lossy().to_string(),
        edits: vec![EditOperation::Replace {
            old_text: "content".to_string(),
            new_text: "modified".to_string(),
            replace_all: false,
        }],
    };

    let result = edit_file(request, &config, &test_permission_config(), &test_permission_context()).await;
    assert!(matches!(result, Err(ToolError::SandboxViolation { .. })));
}

// =============================================================================
// Windows-Specific Tests
// =============================================================================

#[cfg(windows)]
#[tokio::test]
async fn test_read_write_windows_line_endings() {
    let temp_dir = TempDir::new().unwrap();
    let file_path = temp_dir.path().join("windows.txt");

    // Write file with CRLF
    std::fs::write(&file_path, "line1\r\nline2\r\nline3").unwrap();

    let config = test_config(temp_dir.path().to_path_buf());

    // Read file
    let read_request = ReadTextFileRequest {
        path: file_path.to_string_lossy().to_string(),
        line: None,
        limit: None,
    };

    let response = read_text_file(read_request, &config).await.unwrap();
    // Content should preserve CRLF when read
    assert!(response.content.contains("\r\n"));

    // Write with LF normalization
    let mut write_config = config.clone();
    write_config.eol_policy = EolPolicy::Lf;

    let write_request = WriteTextFileRequest {
        path: file_path.to_string_lossy().to_string(),
        content: response.content,
    };

    write_text_file(write_request, &write_config, &test_permission_config(), &test_permission_context()).await.unwrap();

    // Verify normalization happened
    let final_content = std::fs::read_to_string(&file_path).unwrap();
    assert!(!final_content.contains("\r\n"));
    assert!(final_content.contains("\n"));
}
