//! Integration tests for path normalization and containment.
//!
//! These tests use temporary directories to test real filesystem operations.

use dirigent_tools::config::SandboxConfig;
use dirigent_tools::path::{canonicalize_path, check_containment, validate_path, SymlinkPolicy};
use dirigent_tools::ToolError;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;

#[test]
fn test_validate_path_with_real_filesystem() {
    // Create a temp directory structure
    let temp_dir = TempDir::new().unwrap();
    let project_dir = temp_dir.path().join("project");
    let src_dir = project_dir.join("src");
    fs::create_dir_all(&src_dir).unwrap();

    // Create a test file
    let test_file = src_dir.join("main.rs");
    fs::write(&test_file, "fn main() {}").unwrap();

    // Create config
    let mut config = SandboxConfig::default();
    config.allowed_roots = vec![project_dir.clone()];
    config.blocked_paths = vec!["**/.env".to_string()];

    // Test: Valid path within allowed root
    let result = validate_path(test_file.to_str().unwrap(), &config);
    assert!(result.is_ok());

    // Test: Path at the root level (should be allowed)
    let root_file = project_dir.join("README.md");
    fs::write(&root_file, "# Project").unwrap();
    let result = validate_path(root_file.to_str().unwrap(), &config);
    assert!(result.is_ok());

    // Test: Path outside allowed roots
    let outside_path = temp_dir.path().join("outside.txt");
    fs::write(&outside_path, "outside").unwrap();
    let result = validate_path(outside_path.to_str().unwrap(), &config);
    assert!(result.is_err());
    assert!(matches!(result, Err(ToolError::SandboxViolation { .. })));
}

#[test]
fn test_validate_path_blocked() {
    let temp_dir = TempDir::new().unwrap();
    let project_dir = temp_dir.path().join("project");
    fs::create_dir_all(&project_dir).unwrap();

    // Create a .env file
    let env_file = project_dir.join(".env");
    fs::write(&env_file, "SECRET=value").unwrap();

    // Create config with blocklist
    let mut config = SandboxConfig::default();
    config.allowed_roots = vec![project_dir.clone()];
    config.blocked_paths = vec!["**/.env".to_string()];

    // Test: Blocked path
    let result = validate_path(env_file.to_str().unwrap(), &config);
    assert!(result.is_err());
    assert!(matches!(result, Err(ToolError::BlockedPath { .. })));
}

#[test]
fn test_validate_path_non_existent_for_write() {
    let temp_dir = TempDir::new().unwrap();
    let project_dir = temp_dir.path().join("project");
    fs::create_dir_all(&project_dir).unwrap();

    // Non-existent file (for write operations)
    let new_file = project_dir.join("new_file.txt");

    let mut config = SandboxConfig::default();
    config.allowed_roots = vec![project_dir.clone()];

    // Test: Non-existent file within allowed root
    let result = validate_path(new_file.to_str().unwrap(), &config);
    assert!(result.is_ok());
}

#[test]
fn test_canonicalize_path_with_real_filesystem() {
    let temp_dir = TempDir::new().unwrap();
    let project_dir = temp_dir.path().join("project");
    fs::create_dir_all(&project_dir).unwrap();

    let test_file = project_dir.join("test.txt");
    fs::write(&test_file, "content").unwrap();

    let policy = SymlinkPolicy::default();

    // Test: Canonicalize existing file
    let canonical = canonicalize_path(&test_file, &policy).unwrap();
    assert!(canonical.is_absolute());
    assert!(canonical.exists());

    // Verify no ".." components
    assert!(!canonical.to_string_lossy().contains(".."));
}

#[test]
fn test_containment_with_real_filesystem() {
    let temp_dir = TempDir::new().unwrap();
    let root_dir = temp_dir.path().join("root");
    let subdir = root_dir.join("subdir");
    fs::create_dir_all(&subdir).unwrap();

    let file = subdir.join("file.txt");
    fs::write(&file, "content").unwrap();

    // Canonicalize paths
    let canonical_root = dunce::canonicalize(&root_dir).unwrap();
    let canonical_file = dunce::canonicalize(&file).unwrap();

    // Test: File is contained in root
    let result = check_containment(&canonical_file, &[canonical_root.clone()]);
    assert!(result.is_ok());

    // Test: Root is not strictly contained in itself
    let result = check_containment(&canonical_root, &[canonical_root.clone()]);
    assert!(result.is_err());
}

#[cfg(unix)]
#[test]
fn test_symlink_handling_unix() {
    use std::os::unix::fs::symlink;

    let temp_dir = TempDir::new().unwrap();
    let allowed_dir = temp_dir.path().join("allowed");
    let outside_dir = temp_dir.path().join("outside");
    fs::create_dir_all(&allowed_dir).unwrap();
    fs::create_dir_all(&outside_dir).unwrap();

    // Create a file outside the allowed root
    let outside_file = outside_dir.join("secret.txt");
    fs::write(&outside_file, "secret").unwrap();

    // Create a symlink inside allowed root pointing outside
    let symlink_path = allowed_dir.join("link_to_secret");
    symlink(&outside_file, &symlink_path).unwrap();

    let mut config = SandboxConfig::default();
    config.allowed_roots = vec![dunce::canonicalize(&allowed_dir).unwrap()];
    config.allow_symlink_escape = false; // Don't allow escapes

    // Test: Symlink escape should be blocked
    let result = validate_path(symlink_path.to_str().unwrap(), &config);
    assert!(result.is_err());
}

#[cfg(windows)]
#[test]
fn test_windows_reserved_device_names() {
    let policy = SymlinkPolicy::default();

    // Reserved device names should be rejected
    let reserved_names = vec!["CON", "PRN", "AUX", "NUL", "COM1", "COM2", "LPT1", "LPT2"];

    for name in reserved_names {
        let path = PathBuf::from(format!("C:\\{}", name));
        let result = canonicalize_path(&path, &policy);
        assert!(result.is_err(), "Should reject reserved name: {}", name);
    }
}

#[cfg(windows)]
#[test]
fn test_windows_path_forms() {
    // Test various Windows path forms with temp directory
    let temp_dir = TempDir::new().unwrap();
    let test_file = temp_dir.path().join("test.txt");
    fs::write(&test_file, "content").unwrap();

    let policy = SymlinkPolicy::default();

    // Test: Standard absolute path
    let canonical = canonicalize_path(&test_file, &policy).unwrap();
    assert!(canonical.is_absolute());

    // Test: Drive letter normalization (uppercase)
    let path_str = canonical.to_string_lossy();
    if path_str.len() >= 2 && path_str.chars().nth(1) == Some(':') {
        assert!(path_str.chars().nth(0).unwrap().is_ascii_uppercase());
    }
}
