//! Plugin Manager — Phase-92 (T92.2 + T92.5)
//!
//! Core plugin lifecycle management:
//! - Register / unregister plugins by endpoint URL
//! - Health checking (manual + periodic)
//! - Safe plugin invocation with graceful degradation
//! - Capability-based plugin discovery
//!
//! # Graceful Degradation Principles
//!
//! 1. **Compile-time independence**: No plugin is required at build time.
//! 2. **Crash isolation**: A plugin crash never affects the main service.
//! 3. **Timeout control**: All calls have a configurable timeout (default 30s).
//! 4. **Retry strategy**: Transient failures are retried (default 2 times).
//! 5. **Option returns**: All plugin calls return `Option<T>` when used via `try_*` methods.

#[cfg(feature = "http-client")]
use std::time::Duration;

use dashmap::DashMap;

#[cfg(feature = "http-client")]
use super::protocol::{
    self, CapabilitiesResponse, HealthResponse,
    IndexRequest, IndexResponse, ProcessRequest, ProcessResponse,
    PluginCallRequest, PluginCallResponse,
};

use super::types::{
    PluginCapability, PluginError, PluginInfo,
    PluginManifest, PluginResult, PluginStatus, PluginType,
};

#[cfg(feature = "http-client")]
use super::types::PluginCallOptions;

// ============================================================================
// PluginManagerConfig
// ============================================================================

/// Configuration for the `PluginManager`.
#[derive(Debug, Clone)]
pub struct PluginManagerConfig {
    /// Default timeout for plugin calls in seconds.
    pub default_timeout_secs: u64,

    /// Default number of retries for transient failures.
    pub default_max_retries: u32,

    /// Health check interval in seconds (for background polling).
    pub health_check_interval_secs: u64,

    /// Whether to automatically perform health checks on registration.
    pub health_check_on_register: bool,
}

impl Default for PluginManagerConfig {
    fn default() -> Self {
        Self {
            default_timeout_secs: 30,
            default_max_retries: 2,
            health_check_interval_secs: 60,
            health_check_on_register: true,
        }
    }
}

// ============================================================================
// PluginManager
// ============================================================================

/// Central plugin lifecycle manager.
///
/// Thread-safe: uses `DashMap` for concurrent access.
///
/// # Usage
///
/// ```rust,ignore
/// let manager = PluginManager::new(PluginManagerConfig::default());
///
/// // Register a plugin by endpoint
/// manager.register("http://localhost:8800").await?;
///
/// // Safe call with graceful degradation
/// if let Some(result) = manager.try_index("pageindex", "pdf", request).await {
///     println!("Indexed: {:?}", result);
/// } else {
///     println!("Plugin unavailable, skipping PDF indexing");
/// }
/// ```
pub struct PluginManager {
    /// Registered plugins (name → info).
    plugins: DashMap<String, PluginInfo>,

    /// Configuration.
    config: PluginManagerConfig,

    /// HTTP client for plugin communication.
    #[cfg(feature = "http-client")]
    client: reqwest::Client,
}

impl std::fmt::Debug for PluginManager {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("PluginManager")
            .field("plugin_count", &self.plugins.len())
            .field("config", &self.config)
            .finish()
    }
}

impl PluginManager {
    /// Create a new PluginManager with the given configuration.
    pub fn new(config: PluginManagerConfig) -> Self {
        #[cfg(feature = "http-client")]
        let client = reqwest::Client::builder()
            .timeout(Duration::from_secs(config.default_timeout_secs))
            .connect_timeout(Duration::from_secs(5))
            .pool_max_idle_per_host(2)
            .build()
            .unwrap_or_default();

        Self {
            plugins: DashMap::new(),
            config,
            #[cfg(feature = "http-client")]
            client,
        }
    }

    /// Create a PluginManager with default configuration.
    pub fn default_manager() -> Self {
        Self::new(PluginManagerConfig::default())
    }

    // ========================================================================
    // Registration
    // ========================================================================

    /// Register a plugin by its HTTP endpoint.
    ///
    /// 1. Calls `GET /api/v1/capabilities` to discover the plugin's manifest.
    /// 2. Performs an initial health check.
    /// 3. Adds the plugin to the registry.
    ///
    /// If a plugin with the same name already exists, it is replaced.
    #[cfg(feature = "http-client")]
    pub async fn register(&self, endpoint: &str) -> PluginResult<PluginInfo> {
        // 1. Fetch capabilities
        let caps_url = protocol::capabilities_url(endpoint);
        let caps_resp = self.client
            .get(&caps_url)
            .timeout(Duration::from_secs(10))
            .send()
            .await
            .map_err(|e| PluginError::HttpError(format!("Failed to reach {}: {}", caps_url, e)))?;

        if !caps_resp.status().is_success() {
            return Err(PluginError::CallFailed(format!(
                "Capabilities endpoint returned {}", caps_resp.status()
            )));
        }

        let caps: CapabilitiesResponse = caps_resp.json().await
            .map_err(|e| PluginError::InvalidResponse(format!("Failed to parse capabilities: {}", e)))?;

        let manifest = caps.to_manifest();
        let mut info = PluginInfo::new(manifest, endpoint);

        // 2. Health check
        if self.config.health_check_on_register {
            match self.check_health_for(endpoint).await {
                Ok(health) => {
                    if health.is_healthy() && health.is_compatible() {
                        info.status = PluginStatus::Connected;
                        info.last_health_check = Some(
                            std::time::SystemTime::now()
                                .duration_since(std::time::UNIX_EPOCH)
                                .unwrap_or_default()
                                .as_secs()
                        );
                    } else if !health.is_compatible() {
                        return Err(PluginError::VersionMismatch(format!(
                            "Plugin API version {} is not compatible with host version {}",
                            health.api_version, protocol::SUPPORTED_API_VERSION
                        )));
                    } else {
                        info.status = PluginStatus::Error;
                        info.last_error = health.message;
                    }
                }
                Err(e) => {
                    tracing::warn!(
                        plugin = %info.manifest.name,
                        endpoint = %endpoint,
                        error = %e,
                        "Health check failed during registration"
                    );
                    info.status = PluginStatus::Disconnected;
                    info.last_error = Some(e.to_string());
                }
            }
        }

        let name = info.manifest.name.clone();
        let result = info.clone();
        self.plugins.insert(name.clone(), info);

        tracing::info!(
            plugin = %name,
            version = %result.manifest.version,
            plugin_type = %result.manifest.plugin_type,
            status = %result.status,
            capabilities = ?result.manifest.capabilities,
            "Plugin registered"
        );

        Ok(result)
    }

    /// Register a plugin from a pre-built manifest (no HTTP discovery).
    ///
    /// Useful for testing or when the manifest is known ahead of time.
    pub fn register_manual(&self, manifest: PluginManifest, endpoint: &str) -> PluginInfo {
        let info = PluginInfo::new(manifest, endpoint);
        let name = info.manifest.name.clone();
        let result = info.clone();
        self.plugins.insert(name, info);
        result
    }

    /// Unregister a plugin by name.
    pub fn unregister(&self, name: &str) -> bool {
        let removed = self.plugins.remove(name).is_some();
        if removed {
            tracing::info!(plugin = %name, "Plugin unregistered");
        }
        removed
    }

    // ========================================================================
    // Query / Discovery
    // ========================================================================

    /// List all registered plugins.
    pub fn list(&self) -> Vec<PluginInfo> {
        self.plugins
            .iter()
            .map(|entry| entry.value().clone())
            .collect()
    }

    /// Get a specific plugin by name.
    pub fn get_plugin(&self, name: &str) -> Option<PluginInfo> {
        self.plugins.get(name).map(|entry| entry.value().clone())
    }

    /// Get the number of registered plugins.
    pub fn plugin_count(&self) -> usize {
        self.plugins.len()
    }

    /// Get the number of available (connected + enabled) plugins.
    pub fn available_count(&self) -> usize {
        self.plugins
            .iter()
            .filter(|entry| entry.value().is_available())
            .count()
    }

    /// Find plugins by capability.
    pub fn find_by_capability(&self, capability: &PluginCapability) -> Vec<PluginInfo> {
        self.plugins
            .iter()
            .filter(|entry| entry.value().manifest.has_capability(capability))
            .map(|entry| entry.value().clone())
            .collect()
    }

    /// Find plugins by type.
    pub fn find_by_type(&self, plugin_type: PluginType) -> Vec<PluginInfo> {
        self.plugins
            .iter()
            .filter(|entry| entry.value().manifest.plugin_type == plugin_type)
            .map(|entry| entry.value().clone())
            .collect()
    }

    // ========================================================================
    // Enable / Disable
    // ========================================================================

    /// Enable a plugin.
    pub fn enable(&self, name: &str) -> PluginResult<()> {
        let mut entry = self.plugins.get_mut(name)
            .ok_or_else(|| PluginError::NotFound(name.to_string()))?;
        entry.enabled = true;
        tracing::info!(plugin = %name, "Plugin enabled");
        Ok(())
    }

    /// Disable a plugin (keeps it registered but won't be called).
    pub fn disable(&self, name: &str) -> PluginResult<()> {
        let mut entry = self.plugins.get_mut(name)
            .ok_or_else(|| PluginError::NotFound(name.to_string()))?;
        entry.enabled = false;
        entry.status = PluginStatus::Disabled;
        tracing::info!(plugin = %name, "Plugin disabled");
        Ok(())
    }

    // ========================================================================
    // Health Check
    // ========================================================================

    /// Check health of a single plugin by name.
    #[cfg(feature = "http-client")]
    pub async fn health_check(&self, name: &str) -> PluginResult<HealthResponse> {
        let endpoint = {
            let entry = self.plugins.get(name)
                .ok_or_else(|| PluginError::NotFound(name.to_string()))?;
            entry.endpoint.clone()
        };

        let result = self.check_health_for(&endpoint).await;

        // Update status
        match &result {
            Ok(health) if health.is_healthy() => {
                if let Some(mut entry) = self.plugins.get_mut(name) {
                    entry.status = PluginStatus::Connected;
                    entry.last_health_check = Some(
                        std::time::SystemTime::now()
                            .duration_since(std::time::UNIX_EPOCH)
                            .unwrap_or_default()
                            .as_secs()
                    );
                    entry.last_error = None;
                }
            }
            Ok(health) => {
                if let Some(mut entry) = self.plugins.get_mut(name) {
                    entry.status = PluginStatus::Error;
                    entry.last_error = health.message.clone();
                }
            }
            Err(e) => {
                if let Some(mut entry) = self.plugins.get_mut(name) {
                    entry.status = PluginStatus::Disconnected;
                    entry.last_error = Some(e.to_string());
                }
            }
        }

        result
    }

    /// Check health of all registered plugins.
    #[cfg(feature = "http-client")]
    pub async fn health_check_all(&self) -> Vec<(String, PluginResult<HealthResponse>)> {
        let names: Vec<String> = self.plugins
            .iter()
            .filter(|entry| entry.value().enabled)
            .map(|entry| entry.key().clone())
            .collect();

        let mut results = Vec::with_capacity(names.len());
        for name in names {
            let result = self.health_check(&name).await;
            results.push((name, result));
        }
        results
    }

    /// Internal: perform health check for a given endpoint.
    #[cfg(feature = "http-client")]
    async fn check_health_for(&self, endpoint: &str) -> PluginResult<HealthResponse> {
        let url = protocol::health_url(endpoint);
        let resp = self.client
            .get(&url)
            .timeout(Duration::from_secs(10))
            .send()
            .await
            .map_err(|e| {
                if e.is_timeout() {
                    PluginError::Timeout(10)
                } else {
                    PluginError::HttpError(format!("Health check failed: {}", e))
                }
            })?;

        if !resp.status().is_success() {
            return Err(PluginError::CallFailed(format!(
                "Health endpoint returned {}", resp.status()
            )));
        }

        resp.json::<HealthResponse>().await
            .map_err(|e| PluginError::InvalidResponse(format!("Failed to parse health response: {}", e)))
    }

    // ========================================================================
    // Plugin Calls (with graceful degradation)
    // ========================================================================

    /// Call a plugin's index endpoint. Returns `None` if the plugin is unavailable.
    ///
    /// This is the primary graceful degradation API: callers can use
    /// `if let Some(result) = manager.try_index(...)` to handle plugin absence.
    #[cfg(feature = "http-client")]
    pub async fn try_index(
        &self,
        plugin_name: &str,
        format: &str,
        request: &IndexRequest,
    ) -> Option<IndexResponse> {
        match self.call_index(plugin_name, format, request, &PluginCallOptions::default()).await {
            Ok(resp) => Some(resp),
            Err(e) => {
                tracing::warn!(
                    plugin = %plugin_name,
                    format = %format,
                    error = %e,
                    "Plugin index call failed (graceful degradation)"
                );
                None
            }
        }
    }

    /// Call a plugin's process endpoint. Returns `None` if the plugin is unavailable.
    #[cfg(feature = "http-client")]
    pub async fn try_process(
        &self,
        plugin_name: &str,
        action: &str,
        request: &ProcessRequest,
    ) -> Option<ProcessResponse> {
        match self.call_process(plugin_name, action, request, &PluginCallOptions::default()).await {
            Ok(resp) => Some(resp),
            Err(e) => {
                tracing::warn!(
                    plugin = %plugin_name,
                    action = %action,
                    error = %e,
                    "Plugin process call failed (graceful degradation)"
                );
                None
            }
        }
    }

    /// Generic plugin call. Returns `None` if the plugin is unavailable.
    #[cfg(feature = "http-client")]
    pub async fn try_call(
        &self,
        plugin_name: &str,
        action: &str,
        payload: serde_json::Value,
    ) -> Option<PluginCallResponse> {
        let request = PluginCallRequest {
            action: action.to_string(),
            payload,
        };
        match self.call_generic(plugin_name, &request, &PluginCallOptions::default()).await {
            Ok(resp) => Some(resp),
            Err(e) => {
                tracing::warn!(
                    plugin = %plugin_name,
                    action = %action,
                    error = %e,
                    "Plugin call failed (graceful degradation)"
                );
                None
            }
        }
    }

    // ========================================================================
    // Internal call methods (with retries)
    // ========================================================================

    /// Call a plugin's index endpoint with explicit options and error handling.
    #[cfg(feature = "http-client")]
    pub async fn call_index(
        &self,
        plugin_name: &str,
        format: &str,
        request: &IndexRequest,
        options: &PluginCallOptions,
    ) -> PluginResult<IndexResponse> {
        let endpoint = self.get_available_endpoint(plugin_name)?;
        let url = protocol::index_url(&endpoint, format);

        let resp = self.call_with_retry(&url, request, options).await?;

        // Record success
        if let Some(mut entry) = self.plugins.get_mut(plugin_name) {
            entry.record_success();
        }

        serde_json::from_value(resp)
            .map_err(|e| PluginError::InvalidResponse(format!("Failed to parse index response: {}", e)))
    }

    /// Call a plugin's process endpoint with explicit options and error handling.
    #[cfg(feature = "http-client")]
    pub async fn call_process(
        &self,
        plugin_name: &str,
        action: &str,
        request: &ProcessRequest,
        options: &PluginCallOptions,
    ) -> PluginResult<ProcessResponse> {
        let endpoint = self.get_available_endpoint(plugin_name)?;
        let url = protocol::process_url(&endpoint, action);

        let resp = self.call_with_retry(&url, request, options).await?;

        if let Some(mut entry) = self.plugins.get_mut(plugin_name) {
            entry.record_success();
        }

        serde_json::from_value(resp)
            .map_err(|e| PluginError::InvalidResponse(format!("Failed to parse process response: {}", e)))
    }

    /// Generic plugin call with explicit options.
    #[cfg(feature = "http-client")]
    pub async fn call_generic(
        &self,
        plugin_name: &str,
        request: &PluginCallRequest,
        options: &PluginCallOptions,
    ) -> PluginResult<PluginCallResponse> {
        let endpoint = self.get_available_endpoint(plugin_name)?;
        let url = format!(
            "{}/api/v1/{}",
            endpoint.trim_end_matches('/'),
            request.action
        );

        let resp = self.call_with_retry(&url, &request.payload, options).await?;

        if let Some(mut entry) = self.plugins.get_mut(plugin_name) {
            entry.record_success();
        }

        serde_json::from_value(resp)
            .map_err(|e| PluginError::InvalidResponse(format!("Failed to parse response: {}", e)))
    }

    /// Get the endpoint URL for an available plugin.
    #[cfg(any(feature = "http-client", test))]
    fn get_available_endpoint(&self, name: &str) -> PluginResult<String> {
        let entry = self.plugins.get(name)
            .ok_or_else(|| PluginError::NotFound(name.to_string()))?;

        if !entry.enabled {
            return Err(PluginError::Unavailable(format!("Plugin '{}' is disabled", name)));
        }

        // Allow calls to plugins in Unknown status (first call after registration without health check)
        if entry.status == PluginStatus::Disconnected || entry.status == PluginStatus::Error {
            return Err(PluginError::Unavailable(format!(
                "Plugin '{}' is {} (last error: {})",
                name,
                entry.status,
                entry.last_error.as_deref().unwrap_or("none")
            )));
        }

        Ok(entry.endpoint.clone())
    }

    /// HTTP POST with retry logic.
    #[cfg(feature = "http-client")]
    async fn call_with_retry<T: serde::Serialize>(
        &self,
        url: &str,
        body: &T,
        options: &PluginCallOptions,
    ) -> PluginResult<serde_json::Value> {
        let timeout = Duration::from_secs(options.timeout_secs);
        let mut last_error = None;

        for attempt in 0..=options.max_retries {
            if attempt > 0 {
                // Exponential backoff: 100ms, 200ms, 400ms...
                let delay = Duration::from_millis(100 * (1 << (attempt - 1)));
                tokio::time::sleep(delay).await;
                tracing::debug!(url = %url, attempt = attempt, "Retrying plugin call");
            }

            match self.client
                .post(url)
                .json(body)
                .timeout(timeout)
                .send()
                .await
            {
                Ok(resp) => {
                    if resp.status().is_success() {
                        let json: serde_json::Value = resp.json().await
                            .map_err(|e| PluginError::InvalidResponse(e.to_string()))?;
                        return Ok(json);
                    } else if resp.status().is_server_error() {
                        // Server error → retryable
                        let status = resp.status();
                        let body_text = resp.text().await.unwrap_or_default();
                        last_error = Some(PluginError::CallFailed(format!(
                            "HTTP {} from {}: {}",
                            status, url, body_text
                        )));
                        continue;
                    } else {
                        // Client error → not retryable
                        let status = resp.status();
                        let body_text = resp.text().await.unwrap_or_default();
                        return Err(PluginError::CallFailed(format!(
                            "HTTP {} from {}: {}",
                            status, url, body_text
                        )));
                    }
                }
                Err(e) => {
                    if e.is_timeout() {
                        last_error = Some(PluginError::Timeout(options.timeout_secs));
                    } else if e.is_connect() {
                        last_error = Some(PluginError::HttpError(format!(
                            "Connection failed to {}: {}", url, e
                        )));
                    } else {
                        last_error = Some(PluginError::HttpError(e.to_string()));
                    }
                    continue;
                }
            }
        }

        // All retries exhausted — record failure
        let err = last_error.unwrap_or_else(|| PluginError::CallFailed("Unknown error".to_string()));

        // Try to find the plugin name from the URL and record failure
        for mut entry in self.plugins.iter_mut() {
            if url.starts_with(&entry.value().endpoint) {
                entry.value_mut().record_failure(err.to_string());
                break;
            }
        }

        Err(err)
    }
}

// ============================================================================
// Tests
// ============================================================================

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

    fn make_test_manifest(name: &str) -> PluginManifest {
        PluginManifest::new(name, "1.0.0", PluginType::Indexer)
            .with_capability(PluginCapability::PdfIndexing)
            .with_capability(PluginCapability::MarkdownIndexing)
    }

    #[test]
    fn test_manager_register_manual() {
        let manager = PluginManager::default_manager();

        let manifest = make_test_manifest("test-plugin");
        let info = manager.register_manual(manifest, "http://localhost:8800");

        assert_eq!(info.manifest.name, "test-plugin");
        assert_eq!(info.endpoint, "http://localhost:8800");
        assert_eq!(info.status, PluginStatus::Unknown);
        assert_eq!(manager.plugin_count(), 1);
    }

    #[test]
    fn test_manager_unregister() {
        let manager = PluginManager::default_manager();

        let manifest = make_test_manifest("test-plugin");
        manager.register_manual(manifest, "http://localhost:8800");
        assert_eq!(manager.plugin_count(), 1);

        assert!(manager.unregister("test-plugin"));
        assert_eq!(manager.plugin_count(), 0);

        assert!(!manager.unregister("nonexistent"));
    }

    #[test]
    fn test_manager_list_and_get() {
        let manager = PluginManager::default_manager();

        manager.register_manual(make_test_manifest("p1"), "http://localhost:8801");
        manager.register_manual(make_test_manifest("p2"), "http://localhost:8802");

        let list = manager.list();
        assert_eq!(list.len(), 2);

        let p1 = manager.get_plugin("p1");
        assert!(p1.is_some());
        assert_eq!(p1.unwrap().endpoint, "http://localhost:8801");

        assert!(manager.get_plugin("p3").is_none());
    }

    #[test]
    fn test_manager_enable_disable() {
        let manager = PluginManager::default_manager();
        manager.register_manual(make_test_manifest("p1"), "http://localhost:8800");

        // Disable
        manager.disable("p1").unwrap();
        let info = manager.get_plugin("p1").unwrap();
        assert!(!info.enabled);
        assert_eq!(info.status, PluginStatus::Disabled);

        // Enable
        manager.enable("p1").unwrap();
        let info = manager.get_plugin("p1").unwrap();
        assert!(info.enabled);

        // Not found
        assert!(manager.disable("nonexistent").is_err());
    }

    #[test]
    fn test_manager_find_by_capability() {
        let manager = PluginManager::default_manager();

        let m1 = PluginManifest::new("indexer", "1.0.0", PluginType::Indexer)
            .with_capability(PluginCapability::PdfIndexing);
        let m2 = PluginManifest::new("processor", "1.0.0", PluginType::Processor)
            .with_capability(PluginCapability::SummaryGeneration);

        manager.register_manual(m1, "http://localhost:8801");
        manager.register_manual(m2, "http://localhost:8802");

        let pdf_plugins = manager.find_by_capability(&PluginCapability::PdfIndexing);
        assert_eq!(pdf_plugins.len(), 1);
        assert_eq!(pdf_plugins[0].manifest.name, "indexer");

        let summary_plugins = manager.find_by_capability(&PluginCapability::SummaryGeneration);
        assert_eq!(summary_plugins.len(), 1);
        assert_eq!(summary_plugins[0].manifest.name, "processor");

        let vision_plugins = manager.find_by_capability(&PluginCapability::Vision);
        assert!(vision_plugins.is_empty());
    }

    #[test]
    fn test_manager_find_by_type() {
        let manager = PluginManager::default_manager();

        let m1 = PluginManifest::new("idx1", "1.0.0", PluginType::Indexer);
        let m2 = PluginManifest::new("idx2", "1.0.0", PluginType::Indexer);
        let m3 = PluginManifest::new("proc1", "1.0.0", PluginType::Processor);

        manager.register_manual(m1, "http://localhost:8801");
        manager.register_manual(m2, "http://localhost:8802");
        manager.register_manual(m3, "http://localhost:8803");

        let indexers = manager.find_by_type(PluginType::Indexer);
        assert_eq!(indexers.len(), 2);

        let processors = manager.find_by_type(PluginType::Processor);
        assert_eq!(processors.len(), 1);
    }

    #[test]
    fn test_manager_get_available_endpoint() {
        let manager = PluginManager::default_manager();

        // Not found
        assert!(manager.get_available_endpoint("nonexistent").is_err());

        // Unknown status → allowed (first call scenario)
        manager.register_manual(make_test_manifest("p1"), "http://localhost:8800");
        assert!(manager.get_available_endpoint("p1").is_ok());

        // Disabled → not allowed
        manager.disable("p1").unwrap();
        assert!(manager.get_available_endpoint("p1").is_err());

        // Re-enable
        manager.enable("p1").unwrap();
        assert!(manager.get_available_endpoint("p1").is_ok());

        // Disconnected → not allowed
        if let Some(mut entry) = manager.plugins.get_mut("p1") {
            entry.status = PluginStatus::Disconnected;
        }
        assert!(manager.get_available_endpoint("p1").is_err());
    }

    #[test]
    fn test_manager_available_count() {
        let manager = PluginManager::default_manager();

        let m1 = make_test_manifest("p1");
        let m2 = make_test_manifest("p2");

        let mut info1 = manager.register_manual(m1, "http://localhost:8801");
        let info2 = manager.register_manual(m2, "http://localhost:8802");

        // Both unknown → 0 available
        assert_eq!(manager.available_count(), 0);

        // Set one to connected
        if let Some(mut entry) = manager.plugins.get_mut("p1") {
            entry.status = PluginStatus::Connected;
        }
        assert_eq!(manager.available_count(), 1);

        // Set both to connected
        if let Some(mut entry) = manager.plugins.get_mut("p2") {
            entry.status = PluginStatus::Connected;
        }
        assert_eq!(manager.available_count(), 2);

        // Disable one
        manager.disable("p1").unwrap();
        assert_eq!(manager.available_count(), 1);
    }

    #[test]
    fn test_manager_replace_on_duplicate_register() {
        let manager = PluginManager::default_manager();

        let m1 = PluginManifest::new("same-name", "1.0.0", PluginType::Indexer);
        manager.register_manual(m1, "http://localhost:8801");

        let m2 = PluginManifest::new("same-name", "2.0.0", PluginType::Processor);
        manager.register_manual(m2, "http://localhost:8802");

        assert_eq!(manager.plugin_count(), 1);
        let info = manager.get_plugin("same-name").unwrap();
        assert_eq!(info.manifest.version, "2.0.0");
        assert_eq!(info.manifest.plugin_type, PluginType::Processor);
        assert_eq!(info.endpoint, "http://localhost:8802");
    }
}
