//! L2 integration tests for [`FsMcpToolPermissionStore`] against a real temp project.

use std::path::PathBuf;
use std::sync::Arc;

use domain::ids::{AgentId, ProjectId};
use domain::ports::{FileSystem, McpToolPermissionStore, StoreError};
use domain::project::{Project, ProjectPath};
use domain::remote::RemoteRef;
use domain::{
    AgentMcpToolPolicyOverride, McpToolPolicy, ProjectMcpToolPermissions,
    MCP_TOOL_PERMISSIONS_VERSION,
};
use infrastructure::orchestrator::mcp::tools::{classified_tool_names, READ_ONLY_TOOLS};
use infrastructure::{FsMcpToolPermissionStore, LocalFileSystem};
use uuid::Uuid;

/// A unique scratch project directory under the OS temp dir, cleaned up on drop.
struct TempDir(PathBuf);

impl TempDir {
    fn new() -> Self {
        let p =
            std::env::temp_dir().join(format!("idea-l2-mcp-tool-permissions-{}", Uuid::new_v4()));
        std::fs::create_dir_all(&p).unwrap();
        Self(p)
    }

    fn project_root(&self) -> String {
        self.0.to_string_lossy().into_owned()
    }
}

impl Drop for TempDir {
    fn drop(&mut self) {
        let _ = std::fs::remove_dir_all(&self.0);
    }
}

fn store() -> FsMcpToolPermissionStore {
    let fs: Arc<dyn FileSystem> = Arc::new(LocalFileSystem::new());
    FsMcpToolPermissionStore::new(fs)
}

fn project(tmp: &TempDir) -> Project {
    Project::new(
        ProjectId::new_random(),
        "mcp-tool-permissions",
        ProjectPath::new(tmp.project_root()).unwrap(),
        RemoteRef::local(),
        1_700_000_000_000,
    )
    .unwrap()
}

fn policy(tools: &[&str]) -> McpToolPolicy {
    McpToolPolicy::new(
        tools.iter().map(|tool| (*tool).to_owned()).collect(),
        &classified_tool_names(),
    )
    .unwrap()
}

#[tokio::test]
async fn missing_mcp_tool_permissions_file_returns_default_document() {
    let tmp = TempDir::new();
    let project = project(&tmp);

    let loaded = store().load_mcp_tool_permissions(&project).await.unwrap();

    assert_eq!(loaded, ProjectMcpToolPermissions::default());
    assert_eq!(loaded.version, MCP_TOOL_PERMISSIONS_VERSION);

    let effective = loaded
        .effective_policy(
            AgentId::new_random(),
            READ_ONLY_TOOLS,
            &classified_tool_names(),
        )
        .unwrap();
    assert!(effective.permits("idea_context_read"));
    assert!(!effective.permits("idea_memory_write"));
}

#[tokio::test]
async fn save_then_load_roundtrips_project_default_and_agent_override() {
    let tmp = TempDir::new();
    let project = project(&tmp);
    let agent = AgentId::new_random();
    let doc = ProjectMcpToolPermissions::new(
        Some(policy(&["idea_context_read", "idea_memory_read"])),
        vec![AgentMcpToolPolicyOverride::new(
            agent,
            policy(&["idea_ticket_update"]),
        )],
        &classified_tool_names(),
    )
    .unwrap();

    let store = store();
    store
        .save_mcp_tool_permissions(&project, &doc)
        .await
        .unwrap();

    let loaded = store.load_mcp_tool_permissions(&project).await.unwrap();
    assert_eq!(loaded, doc);

    let effective = loaded
        .effective_policy(agent, READ_ONLY_TOOLS, &classified_tool_names())
        .unwrap();
    assert!(effective.permits("idea_ticket_update"));
    assert!(effective.permits("idea_context_read"));
    assert!(effective.permits("idea_memory_read"));

    let path = tmp.0.join(".ideai").join("mcp-tool-permissions.json");
    assert!(
        path.exists(),
        "store writes under .ideai/mcp-tool-permissions.json"
    );
}

#[tokio::test]
async fn load_rejects_unknown_tool_permissions() {
    let tmp = TempDir::new();
    let project = project(&tmp);
    let path = tmp.0.join(".ideai").join("mcp-tool-permissions.json");
    std::fs::create_dir_all(path.parent().unwrap()).unwrap();
    std::fs::write(
        path,
        r#"{
  "version": 1,
  "projectDefault": { "allowedTools": ["idea_unknown"] },
  "agents": []
}
"#,
    )
    .unwrap();

    let err = store()
        .load_mcp_tool_permissions(&project)
        .await
        .unwrap_err();

    assert!(
        matches!(err, StoreError::Serialization(ref message) if message.contains("unknown MCP tool: idea_unknown")),
        "unknown tools must not become latent permissions, got {err:?}"
    );
}

#[tokio::test]
async fn save_rejects_invalid_allowlist_before_writing() {
    let tmp = TempDir::new();
    let project = project(&tmp);
    let invalid = ProjectMcpToolPermissions {
        version: MCP_TOOL_PERMISSIONS_VERSION,
        project_default: Some(McpToolPolicy {
            allowed_tools: vec!["idea_unknown".to_owned()],
        }),
        agents: Vec::new(),
    };

    let err = store()
        .save_mcp_tool_permissions(&project, &invalid)
        .await
        .unwrap_err();

    assert!(
        matches!(err, StoreError::Serialization(ref message) if message.contains("unknown MCP tool: idea_unknown")),
        "invalid allowlists must be rejected before persistence, got {err:?}"
    );
    assert!(!tmp
        .0
        .join(".ideai")
        .join("mcp-tool-permissions.json")
        .exists());
}
