//! Phase 81.6 — sequential `NexoPlugin::init()` driver. Each plugin
//! gets its outcome recorded; a single failure logs a warn and the
//! loop continues. 81.6 ships with an empty handles map (every
//! plugin records `NoHandle`); 81.7+ will populate it.
//!
//! `tokio::spawn` / panic-catch sandbox is intentionally NOT used —
//! callers assume `init()` is well-behaved. If a real plugin starts
//! misbehaving, the follow-up wraps each call in `tokio::time::timeout`.

use std::collections::BTreeMap;
use std::path::Path;
use std::sync::Arc;
use std::time::Instant;

use serde::Serialize;

use nexo_plugin_manifest::PluginManifest;
use serde_yaml::Value;

use crate::agent::plugin_config_loader::{config_error_kind, load_plugin_config};
use crate::agent::plugin_host::{NexoPlugin, PluginInitContext};
use crate::agent::scoped_tool_registry::{NamespaceEnforcement, NamespaceViolation};

use super::factory::{FactoryInstantiateError, PluginFactoryRegistry};
use super::subprocess::subprocess_plugin_factory;
use super::NexoPluginRegistrySnapshot;

#[derive(Clone, Debug, Serialize)]
#[serde(tag = "outcome", rename_all = "snake_case")]
pub enum InitOutcome {
    Ok {
        duration_ms: u64,
    },
    Failed {
        error: String,
    },
    /// 81.6 placeholder — the manifest declares a plugin but no
    /// concrete `NexoPlugin` handle was produced. Manifest-driven
    /// instantiation lands in Phase 81.7.
    NoHandle,
}

/// Drive `NexoPlugin::init()` once per plugin in registry order.
/// Sequential — single failure logs warn + records `Failed`; the
/// loop never aborts. Plugins absent from `handles` record
/// `NoHandle` (the common case until 81.7 ships).
///
/// `ctx_factory` is a closure invoked once per plugin id that
/// constructs a fresh [`PluginInitContext`]. The closure must be
/// callable but is never called for plugins recording `NoHandle`,
/// so 81.6 callers can pass an `unreachable!()` body.
pub async fn run_plugin_init_loop<'env, F>(
    snapshot: &NexoPluginRegistrySnapshot,
    handles: &BTreeMap<String, Arc<dyn NexoPlugin>>,
    mut ctx_factory: F,
) -> BTreeMap<String, InitOutcome>
where
    F: FnMut(&PluginManifest, &Arc<Value>) -> PluginInitContext<'env>,
{
    let mut outcomes = BTreeMap::new();
    for plugin in &snapshot.plugins {
        let id = plugin.manifest.plugin.id.clone();
        let Some(handle) = handles.get(&id).cloned() else {
            outcomes.insert(id, InitOutcome::NoHandle);
            continue;
        };
        let empty_cfg: Arc<Value> = Arc::new(Value::Mapping(serde_yaml::Mapping::new()));
        let mut ctx = ctx_factory(&plugin.manifest, &empty_cfg);
        let start = Instant::now();
        match handle.init(&mut ctx).await {
            Ok(()) => {
                let duration_ms = start.elapsed().as_millis() as u64;
                outcomes.insert(id, InitOutcome::Ok { duration_ms });
            }
            Err(e) => {
                let error = e.to_string();
                tracing::warn!(
                    target: "plugins.init",
                    plugin_id = %id,
                    error = %error,
                    "plugin init failed; continuing"
                );
                outcomes.insert(id, InitOutcome::Failed { error });
            }
        }
    }
    outcomes
}

/// Phase 81.12.0 — factory-driven init loop. For each plugin in
/// the snapshot, look up a factory in `factory_registry`; if
/// found, instantiate via the factory closure + call its `init()`.
/// Plugins WITHOUT a factory record `InitOutcome::NoHandle` so
/// operators with partial migration see consistent output during
/// the 81.12.a-e per-plugin slices. Sequential per snapshot order;
/// one failure logs `tracing::warn!` and the loop never aborts.
/// Phase 81.17.b — return shape for the factory-driven init loop.
/// Outcomes describe what happened per plugin id; `handles` carries
/// the live `Arc<dyn NexoPlugin>` instances that successfully
/// passed `init()`. Callers MUST retain `handles` for the daemon's
/// lifetime — dropping them triggers `Arc::drop` on the underlying
/// plugin which, for `SubprocessNexoPlugin`, SIGKILLs the child
/// process via `tokio::process::Command::kill_on_drop(true)`.
pub struct FactoryInitResult {
    pub outcomes: BTreeMap<String, InitOutcome>,
    pub handles: BTreeMap<String, Arc<dyn NexoPlugin>>,
}

/// Phase 81.3 — convert collected violations into a human-readable
/// "first-3-then-count" sample string. Used by the init loop to
/// enrich `InitOutcome::Failed` when Strict mode rejects.
fn format_violation_sample(violations: &[NamespaceViolation]) -> String {
    let take = violations.len().min(3);
    let head: Vec<String> = violations
        .iter()
        .take(take)
        .map(|v| format!("{}={}", v.attempted_name, v.reason.as_str()))
        .collect();
    if violations.len() > take {
        format!("{} … (+{} more)", head.join(", "), violations.len() - take)
    } else {
        head.join(", ")
    }
}

/// Phase 81.4 — load + validate per-plugin config dir BEFORE
/// `init()` runs. On failure, returns `InitOutcome::Failed` so the
/// caller skips `init()` and records the outcome. `tracing::warn!`
/// is the operator-visible signal; broker emit deferred to 81.4.b.
fn try_load_plugin_config(
    plugin_id: &str,
    plugin_root: &Path,
    config_dir: &Path,
    manifest: &PluginManifest,
) -> Result<Arc<Value>, InitOutcome> {
    match load_plugin_config(plugin_root, config_dir, manifest) {
        Ok(cfg) => Ok(Arc::new(cfg.merged)),
        Err(err) => {
            let kind = config_error_kind(&err);
            let error = err.to_string();
            tracing::warn!(
                target: "plugins.init",
                plugin_id = %plugin_id,
                kind = %kind,
                %error,
                "plugin config load failed; skipping init"
            );
            Err(InitOutcome::Failed {
                error: format!("config load: {error}"),
            })
        }
    }
}

/// Phase 81.26 — after `init()` + channel + LLM + hook
/// registrations succeed, register every backend name from
/// `manifest.plugin.extends.memory_backends` as a
/// `RemoteVectorBackend`. Only fires when the handle is a
/// `SubprocessNexoPlugin`. Failure escalates to
/// `InitOutcome::Failed`.
async fn register_remote_vector_backends_after_init(
    plugin_id: &str,
    handle: &Arc<dyn NexoPlugin>,
    vector_backend_registry: &Arc<crate::agent::vector_backend_registry::VectorBackendRegistry>,
) -> Option<InitOutcome> {
    let any = handle.as_any();
    let sub = match any
        .downcast_ref::<crate::agent::nexo_plugin_registry::subprocess::SubprocessNexoPlugin>()
    {
        Some(s) => s,
        None => return None,
    };
    match sub
        .register_remote_vector_backends(vector_backend_registry)
        .await
    {
        Ok(_) => None,
        Err(e) => {
            let error = format!("vector backend register: {e}");
            tracing::warn!(
                target: "plugins.init",
                plugin_id = %plugin_id,
                %error,
                "remote vector backend registration failed"
            );
            Some(InitOutcome::Failed { error })
        }
    }
}

/// Phase 81.29 — after `init()` + channel + LLM + hook + vector
/// registrations succeed, register every tool name from
/// `manifest.plugin.extends.tools` (intersected with the
/// initialize-reply tools array) as a `RemoteToolHandler` in the
/// per-plugin scoped tool registry. Only fires when the handle
/// is a `SubprocessNexoPlugin`; other concrete types skip
/// silently. Failure escalates to `InitOutcome::Failed` so the
/// agent loop never tries to dispatch a tool whose subprocess
/// registration is broken.
async fn register_remote_tool_handlers_after_init(
    plugin_id: &str,
    handle: &Arc<dyn NexoPlugin>,
    scoped_tool_registry: &Arc<crate::agent::scoped_tool_registry::ScopedToolRegistry>,
) -> Option<InitOutcome> {
    let any = handle.as_any();
    let sub = match any
        .downcast_ref::<crate::agent::nexo_plugin_registry::subprocess::SubprocessNexoPlugin>()
    {
        Some(s) => s,
        None => return None,
    };
    // Synthetic instance plugins (id of form `<base>.<instance>`)
    // share the base plugin's tool catalog because the child binary
    // embeds the base manifest. The base factory already registered
    // every tool name; the instance variant must NOT re-register or
    // it hits "already registered" and aborts init for the per-instance
    // wiring (pairing slots, inbound binding routing). Skip remote
    // tool handler registration for synthetic instances — the tools
    // are reachable through the base plugin's handler anyway.
    if plugin_id.contains('.') {
        return None;
    }
    match sub
        .register_remote_tool_handlers(scoped_tool_registry)
        .await
    {
        Ok(names) => {
            if !names.is_empty() {
                tracing::info!(
                    target: "plugins.init",
                    plugin_id = %plugin_id,
                    registered_count = names.len(),
                    "registered remote tools"
                );
            }
            None
        }
        Err(e) => {
            let error = format!("tool handler register: {e}");
            tracing::warn!(
                target: "plugins.init",
                plugin_id = %plugin_id,
                %error,
                "remote tool handler registration failed"
            );
            Some(InitOutcome::Failed { error })
        }
    }
}

/// Phase 81.27 — after `init()` + channel + LLM registrations
/// succeed, register every hook name from
/// `manifest.plugin.extends.hooks` as a `RemoteHookHandler`.
/// Only fires when the handle is a `SubprocessNexoPlugin`; other
/// concrete types skip silently. Failure escalates to
/// `InitOutcome::Failed`. Continue-on-error semantic lives
/// inside `RemoteHookHandler::on_hook` itself; this hook only
/// fires when registration plumbing fails (e.g. Inner not yet
/// populated — shouldn't happen post-init).
async fn register_remote_hook_handlers_after_init(
    plugin_id: &str,
    handle: &Arc<dyn NexoPlugin>,
    hook_registry: &Arc<crate::agent::hook_registry::HookRegistry>,
) -> Option<InitOutcome> {
    let any = handle.as_any();
    let sub = match any
        .downcast_ref::<crate::agent::nexo_plugin_registry::subprocess::SubprocessNexoPlugin>()
    {
        Some(s) => s,
        None => return None,
    };
    match sub.register_remote_hook_handlers(hook_registry).await {
        Ok(_) => None,
        Err(e) => {
            let error = format!("hook handler register: {e}");
            tracing::warn!(
                target: "plugins.init",
                plugin_id = %plugin_id,
                %error,
                "remote hook handler registration failed"
            );
            Some(InitOutcome::Failed { error })
        }
    }
}

/// Phase 81.25 — after `init()` + channel registration succeed,
/// register every provider name from
/// `manifest.plugin.extends.llm_providers` as a `RemoteLlmFactory`.
/// Only fires when the handle is a `SubprocessNexoPlugin`; other
/// concrete types skip silently. Failure escalates to
/// `InitOutcome::Failed`.
async fn register_remote_llm_providers_after_init(
    plugin_id: &str,
    handle: &Arc<dyn NexoPlugin>,
    llm_registry: &Arc<nexo_llm::LlmRegistry>,
) -> Option<InitOutcome> {
    let any = handle.as_any();
    let sub = match any
        .downcast_ref::<crate::agent::nexo_plugin_registry::subprocess::SubprocessNexoPlugin>()
    {
        Some(s) => s,
        None => return None,
    };
    match sub.register_remote_llm_providers(llm_registry).await {
        Ok(_) => None,
        Err(e) => {
            let error = format!("llm provider register: {e}");
            tracing::warn!(
                target: "plugins.init",
                plugin_id = %plugin_id,
                %error,
                "remote LLM provider registration failed"
            );
            Some(InitOutcome::Failed { error })
        }
    }
}

/// Phase 81.24 — after `init()` returns Ok, register every kind
/// from `manifest.plugin.extends.channels` as a
/// `RemoteChannelAdapter`. Only fires when the handle is a
/// `SubprocessNexoPlugin`; other concrete types skip silently.
/// Failure escalates to `InitOutcome::Failed`.
async fn register_remote_channels_after_init(
    plugin_id: &str,
    handle: &Arc<dyn NexoPlugin>,
    channel_adapter_registry: &Arc<crate::agent::channel_adapter::ChannelAdapterRegistry>,
) -> Option<InitOutcome> {
    let any = handle.as_any();
    let sub = match any
        .downcast_ref::<crate::agent::nexo_plugin_registry::subprocess::SubprocessNexoPlugin>()
    {
        Some(s) => s,
        None => return None,
    };
    match sub
        .register_remote_channel_adapters(channel_adapter_registry)
        .await
    {
        Ok(_) => None,
        Err(e) => {
            let error = format!("channel adapter register: {e}");
            tracing::warn!(
                target: "plugins.init",
                plugin_id = %plugin_id,
                %error,
                "remote channel adapter registration failed"
            );
            Some(InitOutcome::Failed { error })
        }
    }
}

/// Phase 81.21.b.b — after every other post-init step succeeds,
/// spawn the per-plugin auto-respawn supervisor task. Only fires
/// when the handle is a `SubprocessNexoPlugin`; in-tree plugins
/// don't need a respawn loop (they share the daemon's lifetime).
/// Returns `None` always — `spawn_supervisor_loop` is
/// fire-and-forget and can't fail.
fn start_plugin_supervisor_loop_after_init(
    plugin_id: &str,
    handle: &Arc<dyn NexoPlugin>,
    ctx: &PluginInitContext<'_>,
) -> Option<InitOutcome> {
    let any = handle.as_any();
    let sub = match any
        .downcast_ref::<crate::agent::nexo_plugin_registry::subprocess::SubprocessNexoPlugin>()
    {
        Some(s) => s,
        None => return None,
    };
    // Re-derive the LlmServices the same way `init()` did so
    // respawned children speak through the same provider plumbing.
    let llm = Some(crate::agent::nexo_plugin_registry::subprocess::LlmServices {
        registry: ctx.llm_registry.clone(),
        config: ctx.llm_config.clone(),
    });
    // The respawn loop needs `Arc<SubprocessNexoPlugin>` (typed,
    // not `Arc<dyn NexoPlugin>`). The factory stashed a
    // `Weak<SubprocessNexoPlugin>` inside the concrete struct
    // immediately after `Arc::new`, before coercing to
    // `Arc<dyn>`; upgrading that Weak gives us the typed Arc
    // back without a clone. If the upgrade fails (only possible
    // for hand-built plugins constructed outside the factory —
    // i.e. tests), skip the supervisor loop silently.
    let Some(arc_sub) = sub.weak_self_arc() else {
        tracing::debug!(
            target: "plugins.init",
            plugin_id = %plugin_id,
            "supervisor loop skipped (no Weak<Self> populated; factory bypassed)"
        );
        return None;
    };
    arc_sub.spawn_supervisor_loop(
        ctx.shutdown.clone(),
        Some(ctx.broker.clone()),
        ctx.long_term_memory.clone(),
        llm,
    );
    tracing::debug!(
        target: "plugins.init",
        plugin_id = %plugin_id,
        "supervisor loop spawned (auto-respawn per manifest.supervisor.respawn)"
    );
    None
}

/// Phase 81.3 — drain ScopedToolRegistry after `init()` returns and,
/// in Strict mode, escalate any violations to `InitOutcome::Failed`.
/// Returns `None` when the post-init outcome is unchanged.
fn check_namespace_after_init(plugin_id: &str, ctx: &PluginInitContext<'_>) -> Option<InitOutcome> {
    let violations = ctx.tool_registry.drain_violations();
    if violations.is_empty() {
        return None;
    }
    if ctx.tool_registry.mode() != NamespaceEnforcement::Strict {
        // Warn mode — log only; init outcome stays Ok.
        tracing::warn!(
            target: "plugins.init",
            plugin_id = %plugin_id,
            count = violations.len(),
            "plugin tool-namespace violations recorded (warn mode; init succeeded)",
        );
        return None;
    }
    let sample = format_violation_sample(&violations);
    let error = format!(
        "plugin `{plugin_id}` violated tool namespace policy ({} violation(s); first 3: {sample})",
        violations.len()
    );
    tracing::warn!(
        target: "plugins.init",
        plugin_id = %plugin_id,
        count = violations.len(),
        %sample,
        "tool namespace violations rejected (strict mode)",
    );
    Some(InitOutcome::Failed { error })
}

pub async fn run_plugin_init_loop_with_factory<'env, F>(
    snapshot: &NexoPluginRegistrySnapshot,
    factory_registry: &PluginFactoryRegistry,
    config_dir: &Path,
    channel_adapter_registry: &Arc<crate::agent::channel_adapter::ChannelAdapterRegistry>,
    llm_registry: &Arc<nexo_llm::LlmRegistry>,
    hook_registry: &Arc<crate::agent::hook_registry::HookRegistry>,
    vector_backend_registry: &Arc<crate::agent::vector_backend_registry::VectorBackendRegistry>,
    mut ctx_factory: F,
) -> FactoryInitResult
where
    F: FnMut(&PluginManifest, &Arc<Value>) -> PluginInitContext<'env>,
{
    let mut outcomes = BTreeMap::new();
    let mut handles: BTreeMap<String, Arc<dyn NexoPlugin>> = BTreeMap::new();
    for plugin in &snapshot.plugins {
        let id = plugin.manifest.plugin.id.clone();
        // Phase 81.4 — load + validate the plugin's config dir
        // BEFORE any factory work. Failure here aborts the
        // plugin's load with `InitOutcome::Failed`; the factory
        // closure (and any subsequent init step) never runs.
        // We pre-compute eagerly so both the auto-subprocess
        // fallback and the registered-factory path see a
        // pre-validated config.
        let plugin_cfg =
            match try_load_plugin_config(&id, &plugin.root_dir, config_dir, &plugin.manifest) {
                Ok(cfg) => cfg,
                Err(failed) => {
                    outcomes.insert(id, failed);
                    continue;
                }
            };
        // Phase 81.17 — auto-subprocess fallback. If no in-tree
        // factory was registered for this id BUT the manifest
        // declares an `[plugin.entrypoint]` with a non-empty
        // `command`, build a `SubprocessNexoPlugin` factory inline
        // and use it. Operator-registered factories take priority
        // — they're the override path for in-tree migrations.
        // Manifests without entrypoint.command keep recording
        // NoHandle (the partial-migration shape from 81.12.a-d).
        if !factory_registry.is_registered(&id) {
            if plugin.manifest.plugin.entrypoint.is_subprocess() {
                let auto_factory = subprocess_plugin_factory(plugin.manifest.clone());
                match auto_factory(&plugin.manifest) {
                    Ok(handle) => {
                        let mut ctx = ctx_factory(&plugin.manifest, &plugin_cfg);
                        let start = std::time::Instant::now();
                        match handle.init(&mut ctx).await {
                            Ok(()) => {
                                let duration_ms = start.elapsed().as_millis() as u64;
                                if let Some(failed) = check_namespace_after_init(&id, &ctx) {
                                    outcomes.insert(id, failed);
                                } else if let Some(failed) = register_remote_channels_after_init(
                                    &id,
                                    &handle,
                                    channel_adapter_registry,
                                )
                                .await
                                {
                                    outcomes.insert(id, failed);
                                } else if let Some(failed) =
                                    register_remote_llm_providers_after_init(
                                        &id,
                                        &handle,
                                        llm_registry,
                                    )
                                    .await
                                {
                                    outcomes.insert(id, failed);
                                } else if let Some(failed) =
                                    register_remote_hook_handlers_after_init(
                                        &id,
                                        &handle,
                                        hook_registry,
                                    )
                                    .await
                                {
                                    outcomes.insert(id, failed);
                                } else if let Some(failed) =
                                    register_remote_vector_backends_after_init(
                                        &id,
                                        &handle,
                                        vector_backend_registry,
                                    )
                                    .await
                                {
                                    outcomes.insert(id, failed);
                                } else if let Some(failed) =
                                    register_remote_tool_handlers_after_init(
                                        &id,
                                        &handle,
                                        &ctx.tool_registry,
                                    )
                                    .await
                                {
                                    outcomes.insert(id, failed);
                                } else if let Some(failed) =
                                    start_plugin_supervisor_loop_after_init(
                                        &id, &handle, &ctx,
                                    )
                                {
                                    outcomes.insert(id, failed);
                                } else {
                                    outcomes.insert(id.clone(), InitOutcome::Ok { duration_ms });
                                    handles.insert(id, handle);
                                }
                            }
                            Err(e) => {
                                // Walk the `std::error::Error::source` chain so
                                // `PluginInitError::Other { source }` shows its
                                // real cause (the wrapper's Display alone says
                                // only "plugin `whatsapp` init failed").
                                let mut chain = e.to_string();
                                let mut src: Option<&dyn std::error::Error> =
                                    std::error::Error::source(&e);
                                while let Some(cause) = src {
                                    use std::fmt::Write;
                                    let _ = write!(&mut chain, " ← {cause}");
                                    src = cause.source();
                                }
                                tracing::warn!(
                                    target: "plugins.init",
                                    plugin_id = %id,
                                    error = %chain,
                                    "subprocess plugin init failed; continuing"
                                );
                                outcomes.insert(id, InitOutcome::Failed { error: chain });
                            }
                        }
                    }
                    Err(source) => {
                        let error = format!("auto-subprocess factory failed: {source}");
                        tracing::warn!(
                            target: "plugins.init",
                            plugin_id = %id,
                            %error,
                            "auto-subprocess plugin construction failed"
                        );
                        outcomes.insert(id, InitOutcome::Failed { error });
                    }
                }
                continue;
            }
            outcomes.insert(id, InitOutcome::NoHandle);
            continue;
        }
        match factory_registry.instantiate(&id, &plugin.manifest) {
            Err(FactoryInstantiateError::NotRegistered { .. }) => {
                outcomes.insert(id, InitOutcome::NoHandle);
            }
            Err(FactoryInstantiateError::FactoryFailed { source, .. }) => {
                let error = format!("factory failed: {source}");
                tracing::warn!(
                    target: "plugins.init",
                    plugin_id = %id,
                    %error,
                    "plugin factory failed; recording Failed outcome"
                );
                outcomes.insert(id, InitOutcome::Failed { error });
            }
            Ok(handle) => {
                let mut ctx = ctx_factory(&plugin.manifest, &plugin_cfg);
                let start = std::time::Instant::now();
                match handle.init(&mut ctx).await {
                    Ok(()) => {
                        let duration_ms = start.elapsed().as_millis() as u64;
                        if let Some(failed) = check_namespace_after_init(&id, &ctx) {
                            outcomes.insert(id, failed);
                        } else if let Some(failed) = register_remote_channels_after_init(
                            &id,
                            &handle,
                            channel_adapter_registry,
                        )
                        .await
                        {
                            outcomes.insert(id, failed);
                        } else if let Some(failed) =
                            register_remote_llm_providers_after_init(&id, &handle, llm_registry)
                                .await
                        {
                            outcomes.insert(id, failed);
                        } else if let Some(failed) =
                            register_remote_hook_handlers_after_init(&id, &handle, hook_registry)
                                .await
                        {
                            outcomes.insert(id, failed);
                        } else if let Some(failed) = register_remote_vector_backends_after_init(
                            &id,
                            &handle,
                            vector_backend_registry,
                        )
                        .await
                        {
                            outcomes.insert(id, failed);
                        } else if let Some(failed) = register_remote_tool_handlers_after_init(
                            &id,
                            &handle,
                            &ctx.tool_registry,
                        )
                        .await
                        {
                            outcomes.insert(id, failed);
                        } else if let Some(failed) = start_plugin_supervisor_loop_after_init(
                            &id, &handle, &ctx,
                        )
                        {
                            outcomes.insert(id, failed);
                        } else {
                            outcomes.insert(id.clone(), InitOutcome::Ok { duration_ms });
                            handles.insert(id, handle);
                        }
                    }
                    Err(e) => {
                        // Walk error source chain like the subprocess
                        // path above so PluginInitError::Other surfaces
                        // its real cause (Display alone says only
                        // "plugin `<id>` init failed").
                        let mut chain = e.to_string();
                        let mut src: Option<&dyn std::error::Error> =
                            std::error::Error::source(&e);
                        while let Some(cause) = src {
                            use std::fmt::Write;
                            let _ = write!(&mut chain, " ← {cause}");
                            src = cause.source();
                        }
                        tracing::warn!(
                            target: "plugins.init",
                            plugin_id = %id,
                            error = %chain,
                            "plugin init failed; continuing"
                        );
                        outcomes.insert(id, InitOutcome::Failed { error: chain });
                    }
                }
            }
        }
    }
    FactoryInitResult { outcomes, handles }
}

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

    use nexo_plugin_manifest::PluginManifest;

    use super::super::report::PluginDiscoveryReport;
    use super::super::DiscoveredPlugin;

    fn discovered(plugin_id: &str) -> DiscoveredPlugin {
        let raw = format!(
            "[plugin]\n\
             id = \"{plugin_id}\"\n\
             version = \"0.1.0\"\n\
             name = \"{plugin_id}\"\n\
             description = \"fixture\"\n\
             min_nexo_version = \">=0.0.1\"\n",
        );
        let manifest: PluginManifest = toml::from_str(&raw).unwrap();
        DiscoveredPlugin {
            manifest,
            root_dir: PathBuf::from("/tmp/fake"),
            manifest_path: PathBuf::from("/tmp/fake/nexo-plugin.toml"),
        }
    }

    fn snapshot_with(plugins: Vec<DiscoveredPlugin>) -> NexoPluginRegistrySnapshot {
        NexoPluginRegistrySnapshot {
            plugins,
            last_report: PluginDiscoveryReport::default(),
            skill_roots: std::collections::BTreeMap::new(),
        }
    }

    #[tokio::test]
    async fn init_loop_records_no_handle_when_handles_empty() {
        let snap = snapshot_with(vec![discovered("a"), discovered("b")]);
        let outcomes = run_plugin_init_loop(
            &snap,
            &BTreeMap::new(),
            |_m, _cfg| -> PluginInitContext<'_> {
                unreachable!("ctx_factory should not be called when handles is empty");
            },
        )
        .await;
        assert_eq!(outcomes.len(), 2);
        assert!(matches!(outcomes.get("a"), Some(InitOutcome::NoHandle)));
        assert!(matches!(outcomes.get("b"), Some(InitOutcome::NoHandle)));
    }

    #[test]
    fn init_outcome_serializes_to_json() {
        // Smoke check the wire format the doctor CLI + admin-ui rely
        // on: typed enum with a `outcome` discriminator + snake_case
        // variants.
        let ok = InitOutcome::Ok { duration_ms: 12 };
        let s = serde_json::to_string(&ok).unwrap();
        assert!(s.contains("\"outcome\":\"ok\""));
        assert!(s.contains("\"duration_ms\":12"));

        let failed = InitOutcome::Failed {
            error: "boom".into(),
        };
        let s = serde_json::to_string(&failed).unwrap();
        assert!(s.contains("\"outcome\":\"failed\""));
        assert!(s.contains("\"error\":\"boom\""));

        let none = InitOutcome::NoHandle;
        let s = serde_json::to_string(&none).unwrap();
        assert!(s.contains("\"outcome\":\"no_handle\""));
    }

    /// Phase 81.12.0 — factory-driven init loop records `Failed`
    /// for plugins whose factory closure errors and `NoHandle` for
    /// the unregistered ones. We use a closure that returns Err so
    /// the helper short-circuits BEFORE invoking `ctx_factory` —
    /// building a real `PluginInitContext` is heavy and not
    /// required to validate the dispatch path.
    #[tokio::test]
    async fn run_plugin_init_loop_with_factory_routes_registered_vs_unregistered() {
        use super::super::factory::{PluginFactory, PluginFactoryRegistry};
        use crate::agent::plugin_host::PluginInitContext;

        let snap = snapshot_with(vec![discovered("alpha"), discovered("beta")]);
        let mut registry = PluginFactoryRegistry::new();
        let factory: PluginFactory = Box::new(|_m| {
            let err: super::super::factory::BoxError =
                Box::new(std::io::Error::other("forced failure for test"));
            Err(err)
        });
        registry.register("alpha", factory).unwrap();

        let cfg_dir = tempfile::tempdir().unwrap();
        let chan_reg = Arc::new(crate::agent::channel_adapter::ChannelAdapterRegistry::new());
        let llm_reg = Arc::new(nexo_llm::LlmRegistry::new());
        let hook_reg = Arc::new(crate::agent::hook_registry::HookRegistry::new());
        let vec_reg = Arc::new(crate::agent::vector_backend_registry::VectorBackendRegistry::new());
        let result = run_plugin_init_loop_with_factory(
            &snap,
            &registry,
            cfg_dir.path(),
            &chan_reg,
            &llm_reg,
            &hook_reg,
            &vec_reg,
            |_m, _cfg| -> PluginInitContext<'_> {
                unreachable!("ctx_factory must NOT be invoked when the factory closure returns Err")
            },
        )
        .await;

        // alpha registered → factory failed → Failed outcome.
        match result.outcomes.get("alpha") {
            Some(InitOutcome::Failed { error }) => {
                assert!(error.contains("forced failure"));
            }
            other => panic!("alpha must be Failed (factory closure errored), got {other:?}"),
        }
        // beta unregistered → NoHandle.
        assert!(matches!(
            result.outcomes.get("beta"),
            Some(InitOutcome::NoHandle)
        ));
        // No handles ever produced (factory short-circuited; no
        // init() call).
        assert!(result.handles.is_empty());
    }

    /// Phase 81.17 — auto-subprocess fallback wires
    /// `subprocess_plugin_factory(manifest)` inline when no in-tree
    /// factory is registered for a manifest with
    /// `entrypoint.command`. We verify the factory itself produces a
    /// usable `Arc<dyn NexoPlugin>` via the standalone helper —
    /// end-to-end coverage of the init-loop fallback (with real
    /// spawn + handshake) is in
    /// `crates/core/tests/subprocess_plugin_e2e.rs` because building
    /// a complete `PluginInitContext` for a unit test is heavier
    /// than the fallback logic itself warrants.
    #[test]
    fn format_violation_sample_truncates_after_three() {
        use crate::agent::scoped_tool_registry::{NamespaceViolation, NamespaceViolationReason};
        let violations = vec![
            NamespaceViolation {
                plugin_id: "p".into(),
                attempted_name: "agent_x".into(),
                reason: NamespaceViolationReason::ReservedPrefix("agent_"),
            },
            NamespaceViolation {
                plugin_id: "p".into(),
                attempted_name: "p_a".into(),
                reason: NamespaceViolationReason::NotInExpose,
            },
            NamespaceViolation {
                plugin_id: "p".into(),
                attempted_name: "p_b".into(),
                reason: NamespaceViolationReason::OutOfNamespace,
            },
            NamespaceViolation {
                plugin_id: "p".into(),
                attempted_name: "p_c".into(),
                reason: NamespaceViolationReason::Collision,
            },
        ];
        let sample = format_violation_sample(&violations);
        assert!(sample.contains("agent_x=ReservedPrefix"));
        assert!(sample.contains("p_a=NotInExpose"));
        assert!(sample.contains("p_b=OutOfNamespace"));
        assert!(sample.contains("(+1 more)"));
        assert!(!sample.contains("p_c"));
    }

    #[tokio::test]
    async fn auto_subprocess_factory_produces_usable_handle() {
        let mut manifest = discovered("auto_subproc").manifest;
        manifest.plugin.entrypoint = nexo_plugin_manifest::EntrypointSection {
            command: Some("/bin/true".to_string()),
            ..Default::default()
        };
        let factory = subprocess_plugin_factory(manifest.clone());
        match factory(&manifest) {
            Ok(plugin) => assert_eq!(plugin.manifest().plugin.id, "auto_subproc"),
            Err(e) => panic!("auto-subprocess factory must build handle, got {e}"),
        }
    }

    /// Phase 81.17 — manifests WITHOUT `entrypoint.command` keep
    /// their pre-81.17 `NoHandle` outcome. Empty entrypoint section
    /// is the in-tree-plugin shape (browser/telegram/whatsapp/email
    /// dormant manifests in 81.12.a-d) — those must NOT accidentally
    /// be instantiated as subprocesses.
    #[tokio::test]
    async fn auto_subprocess_fallback_skips_manifests_without_entrypoint() {
        use super::super::factory::PluginFactoryRegistry;

        // discovered() builds a manifest WITHOUT entrypoint, which
        // serde fills with EntrypointSection::default() — command =
        // None, is_subprocess() == false.
        let snap = snapshot_with(vec![discovered("in_tree_only")]);
        let registry = PluginFactoryRegistry::new();
        let cfg_dir = tempfile::tempdir().unwrap();
        let chan_reg = Arc::new(crate::agent::channel_adapter::ChannelAdapterRegistry::new());
        let llm_reg = Arc::new(nexo_llm::LlmRegistry::new());
        let hook_reg = Arc::new(crate::agent::hook_registry::HookRegistry::new());
        let vec_reg = Arc::new(crate::agent::vector_backend_registry::VectorBackendRegistry::new());
        let result = run_plugin_init_loop_with_factory(
            &snap,
            &registry,
            cfg_dir.path(),
            &chan_reg,
            &llm_reg,
            &hook_reg,
            &vec_reg,
            |_m, _cfg| -> PluginInitContext<'_> {
                unreachable!("ctx_factory must NOT be invoked for non-subprocess manifests")
            },
        )
        .await;
        assert!(
            matches!(
                result.outcomes.get("in_tree_only"),
                Some(InitOutcome::NoHandle)
            ),
            "in-tree manifest without entrypoint must record NoHandle"
        );
        assert!(result.handles.is_empty(), "no handles for NoHandle outcome");
    }

    /// Phase 81.4 — when the plugin's manifest declares
    /// `config.schema_path` pointing at a non-existent file,
    /// the loader returns `SchemaRead` and the init-loop
    /// records `InitOutcome::Failed` BEFORE invoking the
    /// closure (closure body is `unreachable!`).
    #[tokio::test]
    async fn init_loop_records_failed_when_config_load_fails() {
        use super::super::factory::PluginFactory;

        let raw = "[plugin]\n\
                   id = \"slack\"\n\
                   version = \"0.1.0\"\n\
                   name = \"slack\"\n\
                   description = \"fixture\"\n\
                   min_nexo_version = \">=0.0.1\"\n\
                   \n\
                   [plugin.config]\n\
                   schema_path = \"missing-schema.json\"\n";
        let manifest: PluginManifest = toml::from_str(raw).unwrap();
        let plugin_root = tempfile::tempdir().unwrap();
        let snap = snapshot_with(vec![DiscoveredPlugin {
            manifest,
            root_dir: plugin_root.path().to_path_buf(),
            manifest_path: plugin_root.path().join("nexo-plugin.toml"),
        }]);

        // Place a yaml file so the loader actually tries to
        // validate (otherwise empty dir → empty config and the
        // schema read fires anyway because schema_path is set).
        let cfg_dir = tempfile::tempdir().unwrap();
        let plugin_cfg_dir = cfg_dir.path().join("plugins").join("slack");
        std::fs::create_dir_all(&plugin_cfg_dir).unwrap();
        std::fs::write(plugin_cfg_dir.join("00.yaml"), "x: 1\n").unwrap();

        // Register a factory whose closure body is irrelevant —
        // we expect the loader to fail BEFORE it's invoked.
        let mut registry = PluginFactoryRegistry::new();
        let factory: PluginFactory = Box::new(|_m| {
            let err: super::super::factory::BoxError =
                Box::new(std::io::Error::other("factory should not be reached"));
            Err(err)
        });
        registry.register("slack", factory).unwrap();

        let chan_reg = Arc::new(crate::agent::channel_adapter::ChannelAdapterRegistry::new());
        let llm_reg = Arc::new(nexo_llm::LlmRegistry::new());
        let hook_reg = Arc::new(crate::agent::hook_registry::HookRegistry::new());
        let vec_reg = Arc::new(crate::agent::vector_backend_registry::VectorBackendRegistry::new());
        let result = run_plugin_init_loop_with_factory(
            &snap,
            &registry,
            cfg_dir.path(),
            &chan_reg,
            &llm_reg,
            &hook_reg,
            &vec_reg,
            |_m, _cfg| -> PluginInitContext<'_> {
                unreachable!("ctx_factory must NOT be invoked when config load fails")
            },
        )
        .await;

        // The loader returns SchemaRead Err → InitOutcome::Failed
        // surfaces with "config load: …" prefix; factory closure
        // never ran, so no "factory should not be reached" string
        // in the error.
        match result.outcomes.get("slack") {
            Some(InitOutcome::Failed { error }) => {
                assert!(
                    error.starts_with("config load:"),
                    "expected config-load failure, got {error}"
                );
                assert!(error.contains("missing-schema.json"));
            }
            other => panic!("expected Failed, got {other:?}"),
        }
        assert!(result.handles.is_empty());
    }
}
