//! Integration tests for the permission system (TOOLS-PERM-01 through TOOLS-PERM-04).
//!
//! This test suite validates:
//! - Permission modes (ask, whitelist, yolo)
//! - Decision caching with TTL and scope
//! - Whitelist pattern matching
//! - ACP integration (stubbed for now)
//! - Integration with file operations

use dirigent_tools::config::{
    DecisionScope, PermissionConfig, PermissionMode, SandboxConfig, WhitelistConfig,
};
use dirigent_tools::error::ToolError;
use dirigent_tools::fs::write::{write_text_file, WriteTextFileRequest};
use dirigent_tools::permission::check::{check_permission, PermissionContext};
use dirigent_tools::permission::cache::{CacheKey, PermissionDecision};
use dirigent_tools::permission::whitelist::{CompiledWhitelist, PermissionOperation};
use std::time::Duration;
use tempfile::TempDir;

/// Create a test sandbox configuration.
fn create_test_sandbox(temp_dir: &TempDir) -> SandboxConfig {
    let mut config = SandboxConfig {
        allowed_roots: vec![temp_dir.path().to_path_buf()],
        blocked_paths: vec![],
        allow_symlink_escape: false,
        follow_symlinks_within_roots: true,
        read_enabled: true,
        write_enabled: true,
        max_read_bytes: 1_000_000,
        max_write_bytes: 1_000_000,
        eol_policy: dirigent_tools::config::EolPolicy::Preserve,
        encoding: "utf-8".to_string(),
    };
    config.normalize_roots();
    config
}

#[tokio::test]
async fn test_yolo_mode_allows_everything() {
    let temp_dir = TempDir::new().unwrap();
    let config = PermissionConfig {
        mode: PermissionMode::Yolo,
        remember_decisions: true,
        remember_ttl_secs: 300,
        scope: DecisionScope::PerConnector,
        whitelist: WhitelistConfig::default(),
    };

    let whitelist = CompiledWhitelist::compile(&config.whitelist).unwrap();
    let context = PermissionContext::new(
        "test-connector".to_string(),
        Some("test-session".to_string()),
        whitelist,
    );

    // All operations should be allowed in yolo mode
    let operations = vec![
        PermissionOperation::Read {
            path: temp_dir.path().join("file.txt").display().to_string(),
        },
        PermissionOperation::Write {
            path: temp_dir.path().join("file.txt").display().to_string(),
        },
        PermissionOperation::Execute {
            command: "dangerous_command".to_string(),
            cwd: temp_dir.path().display().to_string(),
        },
    ];

    for operation in operations {
        let decision = check_permission(&operation, &context, &config).await.unwrap();
        assert_eq!(decision, PermissionDecision::Allowed);
    }
}

#[tokio::test]
async fn test_whitelist_mode_auto_approves_matching() {
    let temp_dir = TempDir::new().unwrap();
    let temp_path = temp_dir.path().display().to_string();

    let config = PermissionConfig {
        mode: PermissionMode::Whitelist,
        remember_decisions: true,
        remember_ttl_secs: 300,
        scope: DecisionScope::PerConnector,
        whitelist: WhitelistConfig {
            write_paths: vec![format!("{}/**", temp_path)],
            execute_commands: vec!["cargo".to_string(), "npm".to_string()],
        },
    };

    let whitelist = CompiledWhitelist::compile(&config.whitelist).unwrap();
    let context = PermissionContext::new(
        "test-connector".to_string(),
        None,
        whitelist,
    );

    // Read should always be allowed
    let read_op = PermissionOperation::Read {
        path: "/any/path/file.txt".to_string(),
    };
    let decision = check_permission(&read_op, &context, &config).await.unwrap();
    assert_eq!(decision, PermissionDecision::Allowed);

    // Write within whitelist should be allowed
    let write_ok = PermissionOperation::Write {
        path: temp_dir.path().join("file.txt").display().to_string(),
    };
    let decision = check_permission(&write_ok, &context, &config).await.unwrap();
    assert_eq!(decision, PermissionDecision::Allowed);

    // Execute whitelisted command should be allowed
    let exec_ok = PermissionOperation::Execute {
        command: "cargo".to_string(),
        cwd: temp_dir.path().display().to_string(),
    };
    let decision = check_permission(&exec_ok, &context, &config).await.unwrap();
    assert_eq!(decision, PermissionDecision::Allowed);
}

#[tokio::test]
async fn test_decision_cache_per_connector() {
    // Note: This test manually adds cache entries to verify cache separation.
    // In production, cache entries are only added when users select "always" options in ACP prompts.

    let config = PermissionConfig {
        mode: PermissionMode::Ask,
        remember_decisions: true,
        remember_ttl_secs: 300,
        scope: DecisionScope::PerConnector,
        whitelist: WhitelistConfig::default(),
    };

    let whitelist = CompiledWhitelist::compile(&config.whitelist).unwrap();
    let context1 = PermissionContext::new(
        "connector-1".to_string(),
        Some("session-1".to_string()),
        whitelist.clone(),
    );
    let context2 = PermissionContext::new(
        "connector-2".to_string(),
        Some("session-2".to_string()),
        whitelist,
    );

    // Manually add cache entries to test separation
    {
        let mut cache1 = context1.cache.lock().unwrap();
        let key = CacheKey::write("/test/path", "connector-1", Some("session-1"), DecisionScope::PerConnector);
        cache1.insert(key, PermissionDecision::Allowed, Duration::from_secs(300));
    }

    assert_eq!(context1.cache_size(), 1);

    // Second connector should have separate cache
    assert_eq!(context2.cache_size(), 0);

    {
        let mut cache2 = context2.cache.lock().unwrap();
        let key = CacheKey::write("/test/path", "connector-2", Some("session-2"), DecisionScope::PerConnector);
        cache2.insert(key, PermissionDecision::Allowed, Duration::from_secs(300));
    }

    assert_eq!(context2.cache_size(), 1);
}

#[tokio::test]
async fn test_decision_cache_per_session() {
    // Note: This test manually adds cache entries to verify per-session scoping.
    // In production, cache entries are only added when users select "always" options in ACP prompts.

    let config = PermissionConfig {
        mode: PermissionMode::Ask,
        remember_decisions: true,
        remember_ttl_secs: 300,
        scope: DecisionScope::PerSession,
        whitelist: WhitelistConfig::default(),
    };

    let whitelist = CompiledWhitelist::compile(&config.whitelist).unwrap();

    // Same connector, different sessions - they share the same cache object but have different keys
    let context1 = PermissionContext::new(
        "connector-1".to_string(),
        Some("session-1".to_string()),
        whitelist.clone(),
    );
    let context2 = PermissionContext::new(
        "connector-1".to_string(),
        Some("session-2".to_string()),
        whitelist,
    );

    // Manually add entries for each session with per-session scope
    {
        let mut cache1 = context1.cache.lock().unwrap();
        let key1 = CacheKey::write("/test/path", "connector-1", Some("session-1"), DecisionScope::PerSession);
        cache1.insert(key1, PermissionDecision::Allowed, Duration::from_secs(300));
    }

    {
        let mut cache2 = context2.cache.lock().unwrap();
        let key2 = CacheKey::write("/test/path", "connector-1", Some("session-2"), DecisionScope::PerSession);
        cache2.insert(key2, PermissionDecision::Allowed, Duration::from_secs(300));
    }

    // Each context has its own cache object, so they each have 1 entry
    assert_eq!(context1.cache_size(), 1);
    assert_eq!(context2.cache_size(), 1);
}

#[tokio::test]
async fn test_cache_ttl_expiration() {
    // Note: This test manually adds a cache entry with short TTL to test expiration.

    let whitelist = CompiledWhitelist::compile(&WhitelistConfig::default()).unwrap();
    let context = PermissionContext::new(
        "test-connector".to_string(),
        None,
        whitelist,
    );

    // Manually add entry with very short TTL
    {
        let mut cache = context.cache.lock().unwrap();
        let key = CacheKey::write("/test/path", "test-connector", None, DecisionScope::PerConnector);
        cache.insert(key.clone(), PermissionDecision::Allowed, Duration::from_millis(1));
    }

    // Cache entry should be present
    let initial_size = context.cache_size();
    assert_eq!(initial_size, 1);

    // Wait for expiration
    tokio::time::sleep(Duration::from_millis(10)).await;

    // Try to get the entry (should be expired and removed)
    {
        let mut cache = context.cache.lock().unwrap();
        let key = CacheKey::write("/test/path", "test-connector", None, DecisionScope::PerConnector);
        let result = cache.get(&key);
        assert_eq!(result, None); // Should be expired
    }

    // Cache should now be empty
    assert_eq!(context.cache_size(), 0);
}

#[tokio::test]
async fn test_write_file_with_permission_yolo() {
    let temp_dir = TempDir::new().unwrap();
    let sandbox_config = create_test_sandbox(&temp_dir);

    let permission_config = PermissionConfig {
        mode: PermissionMode::Yolo,
        remember_decisions: true,
        remember_ttl_secs: 300,
        scope: DecisionScope::PerConnector,
        whitelist: WhitelistConfig::default(),
    };

    let whitelist = CompiledWhitelist::compile(&permission_config.whitelist).unwrap();
    let permission_context = PermissionContext::new(
        "test-connector".to_string(),
        None,
        whitelist,
    );

    let file_path = temp_dir.path().join("test_file.txt");
    let request = WriteTextFileRequest {
        path: file_path.display().to_string(),
        content: "Hello, world!".to_string(),
    };

    // Should succeed in yolo mode
    let result = write_text_file(
        request,
        &sandbox_config,
        &permission_config,
        &permission_context,
    )
    .await;

    assert!(result.is_ok());
    assert!(file_path.exists());

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

#[tokio::test]
async fn test_write_file_with_permission_whitelist() {
    let temp_dir = TempDir::new().unwrap();
    let sandbox_config = create_test_sandbox(&temp_dir);
    let temp_path = temp_dir.path().display().to_string();

    let permission_config = PermissionConfig {
        mode: PermissionMode::Whitelist,
        remember_decisions: true,
        remember_ttl_secs: 300,
        scope: DecisionScope::PerConnector,
        whitelist: WhitelistConfig {
            write_paths: vec![format!("{}/**", temp_path)],
            execute_commands: vec![],
        },
    };

    let whitelist = CompiledWhitelist::compile(&permission_config.whitelist).unwrap();
    let permission_context = PermissionContext::new(
        "test-connector".to_string(),
        None,
        whitelist,
    );

    let file_path = temp_dir.path().join("test_file.txt");
    let request = WriteTextFileRequest {
        path: file_path.display().to_string(),
        content: "Whitelisted write".to_string(),
    };

    // Should succeed because path matches whitelist
    let result = write_text_file(
        request,
        &sandbox_config,
        &permission_config,
        &permission_context,
    )
    .await;

    assert!(result.is_ok());
    assert!(file_path.exists());

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

#[tokio::test]
async fn test_cache_key_equality() {
    // Test that cache keys are correctly generated for different scopes
    let key1 = CacheKey::write(
        "/path/to/file",
        "conn-1",
        Some("session-1"),
        DecisionScope::PerConnector,
    );
    let key2 = CacheKey::write(
        "/path/to/file",
        "conn-1",
        Some("session-1"),
        DecisionScope::PerConnector,
    );
    let key3 = CacheKey::write(
        "/path/to/file",
        "conn-1",
        Some("session-2"),
        DecisionScope::PerConnector,
    );

    // Same connector and path should produce equal keys for per-connector scope
    assert_eq!(key1, key2);
    // Different sessions should still be equal for per-connector scope
    assert_eq!(key1, key3);

    // Different scope should produce different keys
    let key4 = CacheKey::write(
        "/path/to/file",
        "conn-1",
        Some("session-1"),
        DecisionScope::PerSession,
    );
    assert_ne!(key1, key4);
}

#[test]
fn test_whitelist_compilation() {
    let config = WhitelistConfig {
        write_paths: vec![
            "C:/work/**".to_string(),
            "/home/user/**".to_string(),
        ],
        execute_commands: vec![
            "cargo".to_string(),
            "npm*".to_string(),
        ],
    };

    let whitelist = CompiledWhitelist::compile(&config);
    assert!(whitelist.is_ok());

    let whitelist = whitelist.unwrap();
    assert!(whitelist.has_write_patterns());
    assert!(whitelist.has_execute_patterns());
}

#[test]
fn test_invalid_whitelist_pattern() {
    let config = WhitelistConfig {
        write_paths: vec!["[invalid".to_string()], // Unclosed bracket
        execute_commands: vec![],
    };

    let result = CompiledWhitelist::compile(&config);
    assert!(result.is_err());

    match result {
        Err(ToolError::InvalidConfig(_)) => {
            // Expected error
        }
        _ => panic!("Expected InvalidConfig error"),
    }
}

#[tokio::test]
async fn test_permission_context_sharing() {
    let whitelist = CompiledWhitelist::compile(&WhitelistConfig::default()).unwrap();
    let context = PermissionContext::new(
        "test-connector".to_string(),
        None,
        whitelist,
    );

    // Clone should share the same cache
    let context_clone = context.clone();

    // Add entry via original context
    {
        let mut cache = context.cache.lock().unwrap();
        let key = CacheKey::write("/path", "test", None, DecisionScope::PerConnector);
        cache.insert(key, PermissionDecision::Allowed, Duration::from_secs(300));
    }

    // Clone should see the same entry
    assert_eq!(context.cache_size(), 1);
    assert_eq!(context_clone.cache_size(), 1);
}
