// vapora-agents: Agent coordinator - orchestrates agent workflows

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

use chrono::Utc;
use dashmap::DashMap;
use thiserror::Error;
use tokio::sync::mpsc;
use tracing::{debug, info, warn};
use uuid::Uuid;
use vapora_shared::validation::{SchemaRegistry, ValidationPipeline};

use crate::learning_profile::{ExecutionData, LearningProfile, TaskTypeExpertise};
use crate::messages::{AgentMessage, TaskAssignment};
use crate::registry::{AgentRegistry, RegistryError};
use crate::scoring::AgentScoringService;

#[derive(Debug, Error)]
pub enum CoordinatorError {
    #[error("No available agent for role: {0}")]
    NoAvailableAgent(String),

    #[error("Task not found: {0}")]
    TaskNotFound(String),

    #[error("Registry error: {0}")]
    RegistryError(#[from] RegistryError),

    #[error("NATS error: {0}")]
    NatsError(String),

    #[error("Invalid task state: {0}")]
    InvalidTaskState(String),

    #[error("Validation error: {0}")]
    ValidationError(String),
}

use vapora_llm_router::BudgetManager;
use vapora_swarm::coordinator::SwarmCoordinator;

use crate::config::AgentConfig;
use crate::profile_adapter::ProfileAdapter;

/// Agent coordinator orchestrates task assignment and execution
pub struct AgentCoordinator {
    registry: Arc<AgentRegistry>,
    nats_client: Option<Arc<async_nats::Client>>,
    #[allow(dead_code)]
    swarm_coordinator: Option<Arc<SwarmCoordinator>>,
    learning_profiles: Arc<std::sync::RwLock<HashMap<String, LearningProfile>>>,
    budget_manager: Option<Arc<BudgetManager>>,
    validation: Arc<ValidationPipeline>,
    /// In-process executor channels, keyed by agent ID.
    /// When a channel is registered for the selected agent, the task is
    /// dispatched directly to its executor rather than relying solely on NATS.
    executor_channels: Arc<DashMap<String, mpsc::Sender<TaskAssignment>>>,
}

impl AgentCoordinator {
    /// Create a new coordinator with config and registry
    pub async fn new(
        _config: AgentConfig,
        registry: Arc<AgentRegistry>,
    ) -> Result<Self, CoordinatorError> {
        // Optionally connect to NATS if configured
        let nats_url = std::env::var("NATS_URL").ok();
        let nats_client = if let Some(url) = nats_url {
            match async_nats::connect(&url).await {
                Ok(client) => {
                    info!("Connected to NATS at {}", url);
                    Some(Arc::new(client))
                }
                Err(e) => {
                    warn!("Failed to connect to NATS: {}", e);
                    None
                }
            }
        } else {
            None
        };

        // Initialize swarm coordinator (Phase 5.2)
        let swarm_coordinator = Arc::new(SwarmCoordinator::new());

        // Sync initial profiles from registry to swarm
        let agents = registry.list_all();
        let profiles = ProfileAdapter::batch_create_profiles(agents);
        for profile in &profiles {
            swarm_coordinator.register_agent(profile.clone()).ok();
        }

        // Spawn background profile sync task (every 30s)
        let registry_clone = Arc::clone(&registry);
        let swarm_clone = Arc::clone(&swarm_coordinator);
        tokio::spawn(async move {
            let mut interval = tokio::time::interval(std::time::Duration::from_secs(30));
            loop {
                interval.tick().await;

                let agents = registry_clone.list_all();
                let profiles = ProfileAdapter::batch_create_profiles(agents);
                let profile_count = profiles.len();
                for profile in &profiles {
                    swarm_clone.register_agent(profile.clone()).ok();
                }

                debug!("Synced {} agent profiles to swarm", profile_count);
            }
        });

        // Initialize validation pipeline
        let schema_path = resolve_schema_dir();
        let schema_registry = Arc::new(SchemaRegistry::new(schema_path.clone()));
        let validation = Arc::new(ValidationPipeline::new(schema_registry));

        info!(
            "Initialized validation pipeline with schema dir: {}",
            schema_path.display()
        );

        Ok(Self {
            registry,
            nats_client,
            swarm_coordinator: Some(swarm_coordinator),
            learning_profiles: Arc::new(std::sync::RwLock::new(HashMap::new())),
            budget_manager: None,
            validation,
            executor_channels: Arc::new(DashMap::new()),
        })
    }

    /// Create a simple coordinator with just registry (for testing)
    pub fn with_registry(registry: Arc<AgentRegistry>) -> Self {
        let swarm_coordinator = Arc::new(SwarmCoordinator::new());
        let agents = registry.list_all();
        let profiles = ProfileAdapter::batch_create_profiles(agents);
        for profile in &profiles {
            swarm_coordinator.register_agent(profile.clone()).ok();
        }

        // Initialize validation pipeline
        let schema_path = resolve_schema_dir();
        let schema_registry = Arc::new(SchemaRegistry::new(schema_path));
        let validation = Arc::new(ValidationPipeline::new(schema_registry));

        Self {
            registry,
            nats_client: None,
            swarm_coordinator: Some(swarm_coordinator),
            learning_profiles: Arc::new(std::sync::RwLock::new(HashMap::new())),
            budget_manager: None,
            validation,
            executor_channels: Arc::new(DashMap::new()),
        }
    }

    /// Register an in-process executor channel for an agent.
    ///
    /// When registered, [`Self::assign_task`] dispatches tasks directly to the
    /// executor via this channel in addition to (or instead of) NATS.
    pub fn register_executor_channel(&self, agent_id: String, tx: mpsc::Sender<TaskAssignment>) {
        self.executor_channels.insert(agent_id, tx);
    }

    /// Set NATS client for inter-agent communication
    pub fn with_nats(mut self, client: Arc<async_nats::Client>) -> Self {
        self.nats_client = Some(client);
        self
    }

    /// Set budget manager for cost enforcement
    pub fn with_budget_manager(mut self, budget_manager: Arc<BudgetManager>) -> Self {
        self.budget_manager = Some(budget_manager);
        self
    }

    /// Assign a task to an available agent using learning-based scoring
    pub async fn assign_task(
        &self,
        role: &str,
        title: String,
        description: String,
        context: String,
        priority: u32,
    ) -> Result<String, CoordinatorError> {
        // Validate inputs against schema
        let input = serde_json::json!({
            "role": role,
            "title": &title,
            "description": &description,
            "context": &context,
            "priority": priority,
        });

        let validation_result = self
            .validation
            .validate("agents/task_assignment", &input)
            .await
            .map_err(|e| CoordinatorError::ValidationError(e.to_string()))?;

        if !validation_result.valid {
            let error_messages: Vec<String> = validation_result
                .errors
                .iter()
                .map(|e| e.to_string())
                .collect();

            warn!(
                "Task assignment validation failed for role {}: {}",
                role,
                error_messages.join(", ")
            );

            return Err(CoordinatorError::ValidationError(format!(
                "Validation failed: {}",
                error_messages.join(", ")
            )));
        }

        info!("Task assignment validated successfully for role: {}", role);

        // Get all available candidates for role
        let all_agents = self.registry.get_agents_by_role(role);
        let candidates: Vec<_> = all_agents
            .into_iter()
            .filter(|a| a.can_accept_task())
            .collect();

        if candidates.is_empty() {
            return Err(CoordinatorError::NoAvailableAgent(role.to_string()));
        }

        // Extract task_type from title (priority) or use role as fallback
        // Simple heuristic: check if title/description contains known task types
        let task_type = extract_task_type(&title, &description, role);

        // Get learning profiles for all candidates, keyed by stable_id.
        let learning_profiles = {
            let profiles = self
                .learning_profiles
                .read()
                .unwrap_or_else(|e| e.into_inner());
            candidates
                .iter()
                .map(|a| {
                    let key = a.stable_id_or_role();
                    (a.id.clone(), profiles.get(key).cloned())
                })
                .collect::<Vec<_>>()
        };

        // Build learning profiles map for scoring
        let mut profiles_map = HashMap::new();
        for (agent_id, profile) in learning_profiles {
            if let Some(profile) = profile {
                profiles_map.insert(agent_id, profile);
            }
        }

        // Score candidates using learning profiles and SwarmCoordinator metrics
        let agent = if !profiles_map.is_empty() {
            // Use learning-based scoring
            let swarm_profiles = candidates
                .iter()
                .map(|a| vapora_swarm::messages::AgentProfile {
                    id: a.id.clone(),
                    roles: vec![a.role.clone()],
                    capabilities: a.capabilities.clone(),
                    current_load: a.current_tasks as f64 / a.max_concurrent_tasks as f64,
                    success_rate: 0.5, // Will be overridden by learning scores
                    availability: a.can_accept_task(),
                })
                .collect();

            let learning_profiles_vec = profiles_map
                .iter()
                .map(|(id, profile)| (id.clone(), profile.clone()))
                .collect::<Vec<_>>();

            let ranked = AgentScoringService::rank_agents(
                swarm_profiles,
                &task_type,
                &learning_profiles_vec,
            );

            // Get top-scored agent
            if let Some(top_score) = ranked.first() {
                candidates
                    .iter()
                    .find(|a| a.id == top_score.agent_id)
                    .cloned()
                    .ok_or_else(|| CoordinatorError::NoAvailableAgent(role.to_string()))?
            } else {
                return Err(CoordinatorError::NoAvailableAgent(role.to_string()));
            }
        } else {
            // Fall back to load-based selection (minimum current tasks)
            candidates
                .into_iter()
                .min_by_key(|a| a.current_tasks)
                .ok_or_else(|| CoordinatorError::NoAvailableAgent(role.to_string()))?
        };

        // Create task assignment
        let task_id = Uuid::new_v4().to_string();
        let assignment = TaskAssignment {
            id: task_id.clone(),
            agent_id: agent.id.clone(),
            required_role: role.to_string(),
            title,
            description,
            context,
            priority,
            deadline: None,
            assigned_at: Utc::now(),
        };

        // Update registry
        self.registry.assign_task(&agent.id)?;

        info!(
            "Assigned task {} to agent {} (role: {}, task_type: {})",
            task_id, agent.id, role, task_type
        );

        // Dispatch to in-process executor if one is registered for this agent.
        // Clone the sender out of the DashMap before awaiting to avoid holding
        // the shard lock across an await point.
        if let Some(tx) = self
            .executor_channels
            .get(&agent.id)
            .map(|r| r.value().clone())
        {
            if let Err(e) = tx.send(assignment.clone()).await {
                warn!(
                    "Failed to dispatch task {} to in-process executor for agent {}: {}",
                    task_id, agent.id, e
                );
            }
        }

        // Publish to NATS if available (external consumers / distributed mode)
        if let Some(nats) = &self.nats_client {
            self.publish_message(nats, AgentMessage::TaskAssigned(assignment))
                .await?;
        }

        Ok(task_id)
    }

    /// Complete a task
    pub async fn complete_task(
        &self,
        task_id: &str,
        agent_id: &str,
    ) -> Result<(), CoordinatorError> {
        // Update registry
        self.registry.complete_task(agent_id)?;

        info!("Task {} completed by agent {}", task_id, agent_id);

        Ok(())
    }

    /// Publish message to NATS
    async fn publish_message(
        &self,
        nats: &async_nats::Client,
        message: AgentMessage,
    ) -> Result<(), CoordinatorError> {
        let subject = match &message {
            AgentMessage::TaskAssigned(_) => crate::messages::subjects::TASKS_ASSIGNED,
            AgentMessage::TaskStarted(_) => crate::messages::subjects::TASKS_STARTED,
            AgentMessage::TaskProgress(_) => crate::messages::subjects::TASKS_PROGRESS,
            AgentMessage::TaskCompleted(_) => crate::messages::subjects::TASKS_COMPLETED,
            AgentMessage::TaskFailed(_) => crate::messages::subjects::TASKS_FAILED,
            AgentMessage::Heartbeat(_) => crate::messages::subjects::AGENT_HEARTBEAT,
            AgentMessage::AgentRegistered(_) => crate::messages::subjects::AGENT_REGISTERED,
            AgentMessage::AgentStopped(_) => crate::messages::subjects::AGENT_STOPPED,
            AgentMessage::ProposalGenerated(_) => crate::messages::subjects::PROPOSALS_GENERATED,
            AgentMessage::ProposalApproved(_) => crate::messages::subjects::PROPOSALS_APPROVED,
            AgentMessage::ProposalRejected(_) => crate::messages::subjects::PROPOSALS_REJECTED,
        };

        let bytes = message
            .to_bytes()
            .map_err(|e| CoordinatorError::NatsError(e.to_string()))?;

        nats.publish(subject.to_string(), bytes.into())
            .await
            .map_err(|e| CoordinatorError::NatsError(e.to_string()))?;

        debug!("Published {} to {}", message.message_type(), subject);

        Ok(())
    }

    /// Subscribe to agent heartbeats
    pub async fn subscribe_heartbeats(&self) -> Result<async_nats::Subscriber, CoordinatorError> {
        if let Some(nats) = &self.nats_client {
            let subject = crate::messages::subjects::AGENT_HEARTBEAT.to_string();
            let sub = nats
                .subscribe(subject)
                .await
                .map_err(|e| CoordinatorError::NatsError(e.to_string()))?;

            info!("Subscribed to agent heartbeats");
            Ok(sub)
        } else {
            Err(CoordinatorError::NatsError(
                "NATS client not configured".to_string(),
            ))
        }
    }

    /// Handle heartbeat message
    pub async fn handle_heartbeat(&self, agent_id: &str) -> Result<(), CoordinatorError> {
        self.registry.heartbeat(agent_id)?;
        debug!("Updated heartbeat for agent {}", agent_id);
        Ok(())
    }

    /// Get registry reference
    pub fn registry(&self) -> Arc<AgentRegistry> {
        Arc::clone(&self.registry)
    }

    /// Shared reference to the registry (for hot-reload coordination).
    pub fn registry_arc(&self) -> Arc<AgentRegistry> {
        Arc::clone(&self.registry)
    }

    /// Drain all agents for `role`: removes them from the registry and drops
    /// their in-process executor channels.
    ///
    /// Learning profiles keyed by `stable_id` are intentionally preserved so
    /// that re-spawned executors immediately benefit from past expertise.
    ///
    /// The dropped `Sender` causes the executor loop to exit once it drains
    /// any in-flight messages.
    pub fn drain_role(&self, role: &str) -> Vec<String> {
        let ids = self.registry.drain_role(role);
        for id in &ids {
            self.executor_channels.remove(id);
        }
        ids
    }

    /// Start coordinator (subscribe to NATS topics)
    pub async fn start(&self) -> Result<(), CoordinatorError> {
        if self.nats_client.is_none() {
            warn!("NATS client not configured, running in standalone mode");
            return Ok(());
        }

        info!("Agent coordinator started");
        Ok(())
    }

    /// Stop coordinator
    pub async fn stop(&self) -> Result<(), CoordinatorError> {
        info!("Agent coordinator stopped");
        Ok(())
    }

    /// Check if coordinator is ready to accept tasks
    pub async fn is_ready(&self) -> bool {
        self.registry.total_count() > 0
    }

    /// Get count of registered agents
    pub async fn get_agent_count(&self) -> usize {
        self.registry.total_count()
    }

    /// Load learning profile for agent from KG execution history.
    /// Queries KG for task-type specific executions and builds expertise
    /// metrics. This is the core integration between KG persistence and
    /// learning profiles.
    ///
    /// Process:
    /// 1. Query KG for task-type specific executions (limited to recent)
    /// 2. Convert PersistedExecution to ExecutionData
    /// 3. Calculate TaskTypeExpertise with recency bias
    /// 4. Return complete LearningProfile
    ///
    /// Note: Requires KG persistence layer with populated kg_executions table.
    pub async fn load_learning_profile_from_kg(
        &self,
        agent_id: &str,
        task_type: &str,
        kg_persistence: &vapora_knowledge_graph::KGPersistence,
    ) -> Result<LearningProfile, CoordinatorError> {
        debug!(
            "Loading learning profile for agent {} task_type {} from KG",
            agent_id, task_type
        );

        // Query KG for recent task-type specific executions
        let persisted_executions = kg_persistence
            .get_executions_for_task_type(agent_id, task_type, 100)
            .await
            .map_err(|e| CoordinatorError::InvalidTaskState(format!("KG query failed: {}", e)))?;

        // Convert to ExecutionData for learning calculations
        let execution_data: Vec<ExecutionData> = persisted_executions
            .iter()
            .filter_map(|pe| {
                let timestamp = chrono::DateTime::parse_from_rfc3339(&pe.executed_at)
                    .ok()
                    .map(|dt| dt.with_timezone(&Utc));

                timestamp.map(|ts| ExecutionData {
                    timestamp: ts,
                    duration_ms: pe.duration_ms,
                    success: pe.outcome == "success",
                })
            })
            .collect();

        debug!(
            "Converted {} persisted executions to ExecutionData for learning",
            execution_data.len()
        );

        // Create learning profile and compute expertise
        let mut profile = LearningProfile::new(agent_id.to_string());
        let expertise = TaskTypeExpertise::from_executions(execution_data, task_type);
        profile.set_task_type_expertise(task_type.to_string(), expertise);

        info!(
            "Loaded learning profile for agent {} task_type {} (success_rate={:.2}, \
             confidence={:.2})",
            agent_id,
            task_type,
            profile.get_task_type_score(task_type),
            profile.get_confidence(task_type)
        );

        Ok(profile)
    }

    /// Batch load learning profiles for multiple agents from KG.
    /// Loads per-task-type expertise for all registered agents.
    pub async fn load_all_learning_profiles(
        &self,
        task_type: &str,
        kg_persistence: &vapora_knowledge_graph::KGPersistence,
    ) -> Result<(), CoordinatorError> {
        let agents = self.registry.list_all();
        debug!(
            "Batch loading learning profiles for {} agents (task_type: {})",
            agents.len(),
            task_type
        );

        for agent in agents {
            let key = agent.stable_id_or_role().to_string();
            match self
                .load_learning_profile_from_kg(&key, task_type, kg_persistence)
                .await
            {
                Ok(profile) => {
                    self.update_learning_profile(&key, profile)?;
                }
                Err(e) => {
                    warn!(
                        "Failed to load learning profile for agent {} (stable_id: {}): {}",
                        agent.id, key, e
                    );
                    // Continue with other agents on failure
                }
            }
        }

        info!(
            "Batch loaded learning profiles for task_type: {}",
            task_type
        );
        Ok(())
    }

    /// Update learning profile for an agent
    /// Allows synchronization of learning data from Knowledge Graph
    pub fn update_learning_profile(
        &self,
        agent_id: &str,
        profile: LearningProfile,
    ) -> Result<(), CoordinatorError> {
        let mut profiles = self.learning_profiles.write().map_err(|_| {
            CoordinatorError::InvalidTaskState(
                "Failed to acquire write lock on learning profiles".to_string(),
            )
        })?;
        profiles.insert(agent_id.to_string(), profile);
        debug!("Updated learning profile for agent {}", agent_id);
        Ok(())
    }

    /// Get learning profile for an agent
    pub fn get_learning_profile(&self, agent_id: &str) -> Option<LearningProfile> {
        let profiles = self
            .learning_profiles
            .read()
            .map(|p| p.get(agent_id).cloned())
            .ok()
            .flatten();
        profiles
    }

    /// Get all learning profiles
    pub fn get_all_learning_profiles(&self) -> HashMap<String, LearningProfile> {
        self.learning_profiles
            .read()
            .map(|p| p.clone())
            .unwrap_or_default()
    }

    /// Clear all learning profiles (useful for testing)
    pub fn clear_learning_profiles(&self) -> Result<(), CoordinatorError> {
        let mut profiles = self.learning_profiles.write().map_err(|_| {
            CoordinatorError::InvalidTaskState("Failed to acquire write lock".to_string())
        })?;
        profiles.clear();
        debug!("Cleared all learning profiles");
        Ok(())
    }
}

/// Extract task type from title/description for learning-based scoring
/// Uses simple heuristics to identify task categories from text
fn extract_task_type(title: &str, description: &str, role: &str) -> String {
    let combined = format!("{} {}", title.to_lowercase(), description.to_lowercase());

    // Check for known task types in combined text
    if combined.contains("code") || combined.contains("implement") || combined.contains("develop") {
        return "coding".to_string();
    }
    if combined.contains("test") || combined.contains("verify") {
        return "testing".to_string();
    }
    if combined.contains("review") || combined.contains("inspect") {
        return "review".to_string();
    }
    if combined.contains("document") || combined.contains("write") || combined.contains("doc") {
        return "documentation".to_string();
    }
    if combined.contains("design") || combined.contains("architect") || combined.contains("plan") {
        return "architecture".to_string();
    }
    if combined.contains("bug") || combined.contains("fix") || combined.contains("issue") {
        return "debugging".to_string();
    }
    if combined.contains("refactor") || combined.contains("improve") || combined.contains("clean") {
        return "refactoring".to_string();
    }

    // Default to role if no specific task type detected
    role.to_string()
}

/// Resolve schema directory path.
/// Priority: VAPORA_SCHEMA_DIR env var > workspace root detection > relative
/// fallback
fn resolve_schema_dir() -> PathBuf {
    // Check env var first
    if let Ok(dir) = std::env::var("VAPORA_SCHEMA_DIR") {
        let path = PathBuf::from(&dir);
        if path.is_absolute() {
            return path;
        }
    }

    // Try to find workspace root by traversing up from CARGO_MANIFEST_DIR
    if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
        let mut current = PathBuf::from(&manifest_dir);

        // Traverse up looking for workspace root (contains schemas/ directory)
        for _ in 0..5 {
            let schemas_path = current.join("schemas");
            if schemas_path.is_dir() {
                return schemas_path;
            }
            if !current.pop() {
                break;
            }
        }
    }

    // Try current working directory
    let cwd_schemas = std::env::current_dir()
        .map(|p| p.join("schemas"))
        .unwrap_or_else(|_| PathBuf::from("schemas"));

    if cwd_schemas.is_dir() {
        return cwd_schemas;
    }

    // Fallback to relative path (will fail gracefully if not found)
    PathBuf::from("schemas")
}

impl Default for AgentCoordinator {
    fn default() -> Self {
        Self::with_registry(Arc::new(AgentRegistry::default()))
    }
}

impl Drop for AgentCoordinator {
    fn drop(&mut self) {
        debug!("AgentCoordinator dropped, profile sync task will continue in background");
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::registry::AgentMetadata;

    #[tokio::test]
    async fn test_coordinator_creation() {
        // Set schema directory for tests (relative to workspace root)
        std::env::set_var("VAPORA_SCHEMA_DIR", "../../schemas");

        let registry = Arc::new(AgentRegistry::new(5));
        let coordinator = AgentCoordinator::with_registry(registry);

        assert!(coordinator.nats_client.is_none());
    }

    #[tokio::test]
    async fn test_task_assignment() {
        // Set schema directory for tests (relative to workspace root)
        std::env::set_var("VAPORA_SCHEMA_DIR", "../../schemas");

        let registry = Arc::new(AgentRegistry::new(5));

        // Register an agent
        let agent = AgentMetadata::new(
            "developer".to_string(),
            "Developer 1".to_string(),
            "claude".to_string(),
            "claude-sonnet-4".to_string(),
            vec!["coding".to_string()],
        );
        registry.register_agent(agent).unwrap();

        let coordinator = AgentCoordinator::with_registry(registry);

        let task_id = coordinator
            .assign_task(
                "developer",
                "Test task".to_string(),
                "Description".to_string(),
                "{}".to_string(),
                80,
            )
            .await;

        match &task_id {
            Ok(_) => {}
            Err(e) => eprintln!("Task assignment failed: {}", e),
        }
        assert!(task_id.is_ok());
    }

    #[test]
    fn test_profile_survives_role_drain() {
        let registry = Arc::new(AgentRegistry::new(5));
        let agent = AgentMetadata::new(
            "developer".to_string(),
            "Dev 1".to_string(),
            "claude".to_string(),
            "claude-sonnet-4".to_string(),
            vec![],
        );
        registry.register_agent(agent).unwrap();

        let coordinator = AgentCoordinator::with_registry(Arc::clone(&registry));

        // Insert a profile under the stable_id key
        let profile = crate::learning_profile::LearningProfile::new("developer".to_string());
        coordinator
            .update_learning_profile("developer", profile)
            .unwrap();

        // Drain the role (removes agents + channels)
        let drained = coordinator.drain_role("developer");
        assert_eq!(drained.len(), 1);

        // Profile must survive the drain
        let retained = coordinator.get_learning_profile("developer");
        assert!(retained.is_some(), "profile must survive drain_role");
    }

    #[tokio::test]
    async fn test_no_available_agent() {
        // Set schema directory for tests (relative to workspace root)
        std::env::set_var("VAPORA_SCHEMA_DIR", "../../schemas");

        let registry = Arc::new(AgentRegistry::new(5));
        let coordinator = AgentCoordinator::with_registry(registry);

        let result = coordinator
            .assign_task(
                "nonexistent",
                "Test task".to_string(),
                "Description".to_string(),
                "{}".to_string(),
                80,
            )
            .await;

        assert!(result.is_err());
    }
}
