// Location: ./crates/cpex-core/src/registry.rs
// Copyright 2025
// SPDX-License-Identifier: Apache-2.0
// Authors: Teryl Taylor
//
// Plugin and hook registries.
//
// PluginRef wraps a plugin implementation with the manager's
// authoritative config. The config comes from the config loader,
// NOT from the plugin — the plugin never provides its own config
// to the manager. This prevents a plugin from tampering with its
// own priority, mode, or capabilities.
//
// Trust flows one direction:
//   config loader → manager → PluginRef → executor
// The plugin is just a recipient, not a source.
//
// The registry supports two registration paths:
//
// 1. **Typed** (`register::<H>()`) — for Rust plugins implementing
//    a handler trait generated by define_hook!. The handler is stored
//    type-erased alongside the PluginRef. At dispatch time, the typed
//    path (`invoke::<H>()`) downcasts back; the dynamic path
//    (`invoke_by_name()`) calls through the type-erased interface.
//
// 2. **Name-based** (`register_for_names::<H>()`) — same handler
//    registered under multiple hook names (the CMF pattern).
//
// Mirrors the Python framework's PluginRef and PluginInstanceRegistry
// in cpex/framework/base.py and cpex/framework/registry.py.

use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};

use std::sync::Arc;
use uuid::Uuid;

use crate::context::PluginContext;
use crate::hooks::payload::{Extensions, PluginPayload};
use crate::hooks::trait_def::HookTypeDef;
use crate::hooks::HookType;
use crate::plugin::{Plugin, PluginConfig, PluginMode};

// ---------------------------------------------------------------------------
// Plugin Ref — trusted wrapper
// ---------------------------------------------------------------------------

/// Manager-owned wrapper that pairs a plugin with its authoritative config.
///
/// The `trusted_config` comes from the config loader / manager — never
/// from the plugin itself. The executor reads all scheduling decisions
/// (mode, priority, hooks, capabilities, on_error) from this config.
///
/// The plugin receives a copy of its config at construction time so it
/// can read its own settings during hook execution. But the manager/executor
/// never reads config back from the plugin.
///
/// Trust flow:
/// ```text
/// config loader → manager → PluginRef.trusted_config → executor
///                        ↘ plugin (receives a copy, cannot influence scheduling)
/// ```
#[derive(Clone)]
pub struct PluginRef {
    /// The plugin implementation.
    plugin: Arc<dyn Plugin>,

    /// Authoritative config from the config loader.
    /// The executor uses this for all scheduling and capability decisions.
    trusted_config: PluginConfig,

    /// Unique identifier assigned by the registry.
    /// Stored as `Uuid` (16 bytes, `Copy`) rather than a 36-char `String`
    /// to avoid heap allocation per registered plugin and to give
    /// downstream `HashMap<Uuid, _>` keys fixed-size hashing.
    id: Uuid,

    /// Runtime circuit breaker — set to true when `on_error: Disable`
    /// triggers. Once set, `mode()` returns `Disabled` and the plugin
    /// is skipped by `group_by_mode()` on all subsequent invocations.
    /// Uses `Arc<AtomicBool>` so clones (in HookEntry) share the same flag.
    disabled: Arc<AtomicBool>,
}

impl PluginRef {
    /// Create a new PluginRef with an independently-sourced config.
    ///
    /// The `trusted_config` must come from the config loader or manager,
    /// NOT from `plugin.config()`. The plugin may hold its own copy
    /// for reading during execute(), but the manager never consults it.
    pub fn new(plugin: Arc<dyn Plugin>, trusted_config: PluginConfig) -> Self {
        Self {
            plugin,
            trusted_config,
            id: Uuid::new_v4(),
            disabled: Arc::new(AtomicBool::new(false)),
        }
    }

    /// The authoritative config used by the executor for all decisions.
    pub fn trusted_config(&self) -> &PluginConfig {
        &self.trusted_config
    }

    /// The plugin implementation (for calling initialize/shutdown).
    pub fn plugin(&self) -> &Arc<dyn Plugin> {
        &self.plugin
    }

    /// Unique identifier assigned at registration.
    /// Returned by value — `Uuid` is `Copy` (16 bytes).
    pub fn id(&self) -> Uuid {
        self.id
    }

    /// Convenience: plugin name from the trusted config.
    pub fn name(&self) -> &str {
        &self.trusted_config.name
    }

    /// Effective mode — returns `Disabled` if the runtime circuit breaker
    /// has tripped, otherwise returns the configured mode.
    ///
    /// `Acquire` on the load pairs with `Release` on the disable() store
    /// so weak-memory-ordering hardware (ARM64) propagates the disable
    /// promptly across threads.
    pub fn mode(&self) -> PluginMode {
        if self.disabled.load(Ordering::Acquire) {
            PluginMode::Disabled
        } else {
            self.trusted_config.mode
        }
    }

    /// Runtime-disable this plugin (one-way circuit breaker).
    ///
    /// Called by the executor when a plugin errors with `on_error: Disable`.
    /// All clones of this PluginRef (in HookEntry, etc.) share the same
    /// `AtomicBool`, so the disable is visible across the system.
    ///
    /// `Release` ordering establishes a happens-before with `Acquire`
    /// loads in `is_disabled()` and `mode()` — required for correctness
    /// on weak-memory hardware (ARM64) where `Relaxed` allows the new
    /// value to remain unobserved by other threads for an unbounded window.
    pub fn disable(&self) {
        self.disabled.store(true, Ordering::Release);
    }

    /// Whether this plugin has been runtime-disabled.
    /// `Acquire` pairs with the `Release` in `disable()` (see `mode()`).
    pub fn is_disabled(&self) -> bool {
        self.disabled.load(Ordering::Acquire)
    }

    /// Convenience: plugin priority from the trusted config.
    pub fn priority(&self) -> i32 {
        self.trusted_config.priority
    }
}

// ---------------------------------------------------------------------------
// Type-Erased Hook Handler
// ---------------------------------------------------------------------------

/// Type-erased interface for calling a hook handler.
///
/// The executor uses this to dispatch hooks without knowing the
/// concrete handler trait at compile time. Each handler wraps a
/// plugin that implements a specific handler trait (e.g.,
/// `CmfHookHandler`) and translates between type-erased payloads
/// and the typed handler method.
///
/// The executor dispatches through this trait for all five phases.
/// The handler receives a borrowed payload — the framework retains
/// ownership. Plugins clone only when modifying.
///
/// `invoke` is async so that plugins can perform I/O (HTTP calls,
/// Redis, vault lookups) without blocking the tokio runtime, and
/// so that `tokio::time::timeout` can actually observe and cancel
/// long-running handlers.
#[async_trait::async_trait]
pub trait AnyHookHandler: Send + Sync {
    /// Call the handler with a borrowed payload.
    ///
    /// Returns an `ErasedResultFields` (see executor module) wrapped
    /// as `Box<dyn Any>`. If the handler modified the payload, the
    /// modified copy is in `ErasedResultFields.modified_payload`.
    async fn invoke(
        &self,
        payload: &dyn PluginPayload,
        extensions: &Extensions,
        ctx: &mut PluginContext,
    ) -> Result<Box<dyn std::any::Any + Send + Sync>, Box<crate::error::PluginError>>;

    /// The hook type name this handler was registered for.
    fn hook_type_name(&self) -> &'static str;
}

// ---------------------------------------------------------------------------
// Hook Entry — PluginRef + handler paired together
// ---------------------------------------------------------------------------

/// A registered hook handler paired with its PluginRef.
///
/// The executor uses `plugin_ref` for scheduling decisions (mode,
/// priority, capabilities) and `handler` for actual dispatch.
///
/// `plugin_ref` is `Arc<PluginRef>` so cloning a `HookEntry` is two
/// reference-count bumps rather than a deep clone of the embedded
/// `PluginConfig`. `group_by_mode` (called once per invoke) clones N
/// entries — keeping that cheap matters at high request rates.
#[derive(Clone)]
pub struct HookEntry {
    /// The plugin wrapper with authoritative config.
    pub plugin_ref: Arc<PluginRef>,

    /// The type-erased handler for this specific hook.
    pub handler: Arc<dyn AnyHookHandler>,
}

// ---------------------------------------------------------------------------
// Plugin Registry
// ---------------------------------------------------------------------------

/// Manages registered plugin instances and hook handler mappings.
///
/// Stores `PluginRef` wrappers by name and `HookEntry` (PluginRef +
/// handler) by hook name. The executor reads scheduling decisions
/// from `PluginRef.trusted_config` and dispatches through the
/// type-erased handler.
///
/// Supports two registration patterns:
///
/// - `register::<H>()` — typed registration for a single hook name
///   (derived from `H::NAME`).
/// - `register_for_names::<H>()` — typed registration for multiple
///   hook names (the CMF pattern where one handler covers
///   `cmf.tool_pre_invoke`, `cmf.llm_input`, etc.).
///
/// `Clone` is cheap-ish: the HashMaps duplicate, but their values are all
/// `Arc`-counted (`Arc<PluginRef>`, `Arc<dyn AnyHookHandler>`), so the
/// inner data is shared. Used by `PluginManager`'s `ArcSwap` snapshot
/// pattern, where every mutating method clones the registry, mutates the
/// clone, and atomically swaps in a new snapshot.
#[derive(Clone)]
pub struct PluginRegistry {
    /// Plugins keyed by name (for lookup and lifecycle). Wrapped in `Arc`
    /// so the same instance is shared with every `HookEntry` in
    /// `hook_index` — registering a plugin allocates one `PluginRef`,
    /// not one per hook.
    plugins: HashMap<String, Arc<PluginRef>>,

    /// Hook name → list of HookEntries, sorted by priority.
    hook_index: HashMap<HookType, Vec<HookEntry>>,
}

impl PluginRegistry {
    /// Create an empty registry.
    pub fn new() -> Self {
        Self {
            plugins: HashMap::new(),
            hook_index: HashMap::new(),
        }
    }

    /// Register a typed hook handler for its primary hook name.
    ///
    /// The handler is registered under `H::NAME`. The `config` must
    /// come from the config loader — not from the plugin. The plugin
    /// must implement the handler trait generated by `define_hook!`.
    ///
    /// # Type Parameters
    ///
    /// - `H` — the hook type (implements `HookTypeDef`).
    ///
    /// # Arguments
    ///
    /// - `plugin` — the plugin implementation (must also implement the handler trait).
    /// - `config` — authoritative config from the config loader.
    /// - `handler` — type-erased handler wrapping the plugin's handler trait impl.
    pub fn register<H: HookTypeDef>(
        &mut self,
        plugin: Arc<dyn Plugin>,
        config: PluginConfig,
        handler: Arc<dyn AnyHookHandler>,
    ) -> Result<(), String> {
        self.register_for_names_inner(plugin, config, handler, &[H::NAME])
    }

    /// Register a typed hook handler for multiple hook names.
    ///
    /// This is the CMF pattern — one handler trait impl covers multiple
    /// hook names (`cmf.tool_pre_invoke`, `cmf.llm_input`, etc.).
    ///
    /// # Arguments
    ///
    /// - `plugin` — the plugin implementation.
    /// - `config` — authoritative config from the config loader.
    /// - `handler` — type-erased handler.
    /// - `names` — hook names to register under.
    pub fn register_for_names<H: HookTypeDef>(
        &mut self,
        plugin: Arc<dyn Plugin>,
        config: PluginConfig,
        handler: Arc<dyn AnyHookHandler>,
        names: &[&str],
    ) -> Result<(), String> {
        self.register_for_names_inner(plugin, config, handler, names)
    }

    /// Register a plugin with a handler for multiple hook names.
    ///
    /// Like `register_for_names` but without requiring a `HookTypeDef`
    /// type parameter. Used by the config-driven factory path where
    /// the hook type is not known at compile time — the factory
    /// provides the handler directly.
    pub fn register_for_names_with_handler(
        &mut self,
        plugin: Arc<dyn Plugin>,
        config: PluginConfig,
        handler: Arc<dyn AnyHookHandler>,
        names: &[&str],
    ) -> Result<(), String> {
        self.register_for_names_inner(plugin, config, handler, names)
    }

    /// Register a plugin with multiple handlers, each for a specific hook.
    ///
    /// Used when a plugin implements multiple hook types with different
    /// payloads (e.g., `ToolPreInvoke` and `ToolPostInvoke`). Each
    /// handler is registered under its paired hook name.
    ///
    /// The plugin is registered once in the name index. Each handler
    /// gets its own `HookEntry` in the hook index under the specified name.
    pub fn register_multi_handler(
        &mut self,
        plugin: Arc<dyn Plugin>,
        config: PluginConfig,
        handlers: Vec<(&str, Arc<dyn AnyHookHandler>)>,
    ) -> Result<(), String> {
        let name = config.name.clone();

        if self.plugins.contains_key(&name) {
            return Err(format!("plugin '{}' is already registered", name));
        }

        let plugin_ref = Arc::new(PluginRef::new(plugin, config));

        for (hook_name, handler) in &handlers {
            let hook_type = HookType::new(*hook_name);
            let entry = HookEntry {
                plugin_ref: Arc::clone(&plugin_ref),
                handler: Arc::clone(handler),
            };
            self.hook_index.entry(hook_type).or_default().push(entry);
        }

        // Sort each affected hook's entry list by trusted priority
        for (hook_name, _) in &handlers {
            let hook_type = HookType::new(*hook_name);
            if let Some(entries) = self.hook_index.get_mut(&hook_type) {
                entries.sort_by_key(|e| e.plugin_ref.priority());
            }
        }

        self.plugins.insert(name, plugin_ref);
        Ok(())
    }

    /// Internal: register handler under one or more hook names.
    fn register_for_names_inner(
        &mut self,
        plugin: Arc<dyn Plugin>,
        config: PluginConfig,
        handler: Arc<dyn AnyHookHandler>,
        names: &[&str],
    ) -> Result<(), String> {
        let name = config.name.clone();

        if self.plugins.contains_key(&name) {
            return Err(format!("plugin '{}' is already registered", name));
        }

        let plugin_ref = Arc::new(PluginRef::new(plugin, config));

        // Add to hook index for each specified hook name
        for hook_name in names {
            let hook_type = HookType::new(*hook_name);
            let entry = HookEntry {
                plugin_ref: Arc::clone(&plugin_ref),
                handler: Arc::clone(&handler),
            };
            self.hook_index.entry(hook_type).or_default().push(entry);
        }

        // Sort each affected hook's entry list by trusted priority
        for hook_name in names {
            let hook_type = HookType::new(*hook_name);
            if let Some(entries) = self.hook_index.get_mut(&hook_type) {
                entries.sort_by_key(|e| e.plugin_ref.priority());
            }
        }

        self.plugins.insert(name, plugin_ref);
        Ok(())
    }

    /// Unregister a plugin by name.
    ///
    /// Removes the PluginRef from the name index and all HookEntries
    /// from the hook index. Returns the (Arc-wrapped) PluginRef if found.
    pub fn unregister(&mut self, name: &str) -> Option<Arc<PluginRef>> {
        let plugin_ref = self.plugins.remove(name)?;

        // Remove from hook index
        for entries in self.hook_index.values_mut() {
            entries.retain(|e| e.plugin_ref.name() != name);
        }

        // Clean up empty hook entries
        self.hook_index.retain(|_, entries| !entries.is_empty());

        Some(plugin_ref)
    }

    /// Look up a PluginRef by name. Returns an `Arc` clone so callers
    /// don't hold borrows on internal storage — works with snapshot-based
    /// dispatch where the registry may sit behind a transient guard.
    pub fn get(&self, name: &str) -> Option<Arc<PluginRef>> {
        self.plugins.get(name).map(Arc::clone)
    }

    /// Returns all HookEntries for a given hook name, sorted by priority.
    ///
    /// Returns an empty slice if no plugins are registered for the hook.
    pub fn entries_for_hook(&self, hook_type: &HookType) -> &[HookEntry] {
        self.hook_index
            .get(hook_type)
            .map(|v| v.as_slice())
            .unwrap_or(&[])
    }

    /// Whether any plugins are registered for the given hook name.
    pub fn has_hooks_for(&self, hook_type: &HookType) -> bool {
        self.hook_index
            .get(hook_type)
            .map(|v| !v.is_empty())
            .unwrap_or(false)
    }

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

    /// All registered plugin names. Returns owned `String`s so callers
    /// don't hold borrows on internal storage — works with snapshot-based
    /// dispatch where the registry may sit behind a transient guard.
    pub fn plugin_names(&self) -> Vec<String> {
        self.plugins.keys().cloned().collect()
    }

    /// Returns every (hook_name, HookEntry) pair where the entry's plugin
    /// matches the given name. Used by external orchestrators that need
    /// to build pre-resolved dispatch lineups for a single plugin across
    /// every hook it registered to (e.g. apl-cpex deciding which entry
    /// handles step-style invocations vs field-style invocations for the
    /// same plugin). Owned tuples — no borrows held on the registry.
    pub fn entries_for_plugin(&self, plugin_name: &str) -> Vec<(String, HookEntry)> {
        let mut out = Vec::new();
        for (hook_type, entries) in &self.hook_index {
            for entry in entries {
                if entry.plugin_ref.name() == plugin_name {
                    out.push((hook_type.as_str().to_string(), entry.clone()));
                }
            }
        }
        out
    }
}

impl Default for PluginRegistry {
    fn default() -> Self {
        Self::new()
    }
}

// ---------------------------------------------------------------------------
// Group HookEntries by mode (used by the executor)
// ---------------------------------------------------------------------------

/// Groups a list of HookEntries by their execution mode.
///
/// Reads mode from `plugin_ref.trusted_config` — never from the plugin.
/// Returns a tuple of five vectors in execution order:
/// (sequential, transform, audit, concurrent, fire_and_forget).
/// Disabled plugins are excluded.
pub type GroupedHookEntries = (
    Vec<HookEntry>,
    Vec<HookEntry>,
    Vec<HookEntry>,
    Vec<HookEntry>,
    Vec<HookEntry>,
);

pub fn group_by_mode(entries: &[HookEntry]) -> GroupedHookEntries {
    let mut sequential = Vec::new();
    let mut transform = Vec::new();
    let mut audit = Vec::new();
    let mut concurrent = Vec::new();
    let mut fire_and_forget = Vec::new();

    for entry in entries {
        match entry.plugin_ref.mode() {
            PluginMode::Sequential => sequential.push(entry.clone()),
            PluginMode::Transform => transform.push(entry.clone()),
            PluginMode::Audit => audit.push(entry.clone()),
            PluginMode::Concurrent => concurrent.push(entry.clone()),
            PluginMode::FireAndForget => fire_and_forget.push(entry.clone()),
            PluginMode::Disabled => {}, // skip
        }
    }

    (sequential, transform, audit, concurrent, fire_and_forget)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::error::PluginError;
    use crate::hooks::payload::PluginPayload;
    use crate::hooks::PluginResult;
    use async_trait::async_trait;

    // -- Test payload and hook type --

    #[derive(Debug, Clone)]
    #[allow(dead_code)] // test fixture — typed shape is the point, not field reads
    struct TestPayload {
        value: String,
    }
    crate::impl_plugin_payload!(TestPayload);

    // -- Test handler (type-erased wrapper) --

    /// A simple AnyHookHandler that wraps a function for testing.
    struct TestHandler;

    #[async_trait]
    impl AnyHookHandler for TestHandler {
        async fn invoke(
            &self,
            _payload: &dyn PluginPayload,
            _extensions: &Extensions,
            _ctx: &mut PluginContext,
        ) -> Result<Box<dyn std::any::Any + Send + Sync>, Box<PluginError>> {
            let result: PluginResult<TestPayload> = PluginResult::allow();
            Ok(crate::executor::erase_result(result))
        }

        fn hook_type_name(&self) -> &'static str {
            "test_hook"
        }
    }

    // -- Test plugin --

    struct TestPlugin {
        cfg: PluginConfig,
    }

    fn make_config(name: &str, hooks: Vec<&str>, priority: i32) -> PluginConfig {
        PluginConfig {
            name: name.to_string(),
            kind: "test".to_string(),
            description: None,
            author: None,
            version: None,
            hooks: hooks.into_iter().map(String::from).collect(),
            mode: PluginMode::Sequential,
            priority,
            on_error: Default::default(),
            capabilities: Default::default(),
            tags: Vec::new(),
            conditions: Vec::new(),
            config: None,
        }
    }

    impl TestPlugin {
        fn new(cfg: PluginConfig) -> Self {
            Self { cfg }
        }
    }

    #[async_trait]
    impl Plugin for TestPlugin {
        fn config(&self) -> &PluginConfig {
            &self.cfg
        }
        async fn initialize(&self) -> Result<(), Box<PluginError>> {
            Ok(())
        }
        async fn shutdown(&self) -> Result<(), Box<PluginError>> {
            Ok(())
        }
    }

    // -- Tests --

    #[test]
    fn test_register_typed_and_lookup() {
        let mut reg = PluginRegistry::new();
        let config = make_config("test-plugin", vec!["test_hook"], 10);
        let plugin = Arc::new(TestPlugin::new(config.clone()));
        let handler: Arc<dyn AnyHookHandler> = Arc::new(TestHandler);

        // Use register_for_names_inner directly since we don't have a real HookTypeDef
        reg.register_for_names_inner(plugin, config, handler, &["test_hook"])
            .unwrap();

        assert_eq!(reg.plugin_count(), 1);
        assert!(reg.get("test-plugin").is_some());
        assert!(reg.has_hooks_for(&HookType::new("test_hook")));
        assert!(!reg.has_hooks_for(&HookType::new("other_hook")));

        let entries = reg.entries_for_hook(&HookType::new("test_hook"));
        assert_eq!(entries.len(), 1);
        assert_eq!(entries[0].plugin_ref.name(), "test-plugin");
    }

    #[test]
    fn test_register_for_multiple_names() {
        let mut reg = PluginRegistry::new();
        let config = make_config("cmf-plugin", vec![], 10);
        let plugin = Arc::new(TestPlugin::new(config.clone()));
        let handler: Arc<dyn AnyHookHandler> = Arc::new(TestHandler);

        reg.register_for_names_inner(
            plugin,
            config,
            handler,
            &[
                "cmf.tool_pre_invoke",
                "cmf.tool_post_invoke",
                "cmf.llm_input",
            ],
        )
        .unwrap();

        assert_eq!(reg.plugin_count(), 1);
        assert!(reg.has_hooks_for(&HookType::new("cmf.tool_pre_invoke")));
        assert!(reg.has_hooks_for(&HookType::new("cmf.tool_post_invoke")));
        assert!(reg.has_hooks_for(&HookType::new("cmf.llm_input")));
        assert!(!reg.has_hooks_for(&HookType::new("cmf.llm_output")));
    }

    #[test]
    fn test_duplicate_registration_fails() {
        let mut reg = PluginRegistry::new();
        let c1 = make_config("dup", vec![], 10);
        let c2 = make_config("dup", vec![], 20);
        let p1 = Arc::new(TestPlugin::new(c1.clone()));
        let p2 = Arc::new(TestPlugin::new(c2.clone()));
        let h1: Arc<dyn AnyHookHandler> = Arc::new(TestHandler);
        let h2: Arc<dyn AnyHookHandler> = Arc::new(TestHandler);

        assert!(reg
            .register_for_names_inner(p1, c1, h1, &["hook_a"])
            .is_ok());
        assert!(reg
            .register_for_names_inner(p2, c2, h2, &["hook_a"])
            .is_err());
    }

    #[test]
    fn test_priority_ordering_uses_trusted_config() {
        let mut reg = PluginRegistry::new();
        let c_low = make_config("low", vec![], 100);
        let c_high = make_config("high", vec![], 10);
        let p_low = Arc::new(TestPlugin::new(c_low.clone()));
        let p_high = Arc::new(TestPlugin::new(c_high.clone()));
        let h1: Arc<dyn AnyHookHandler> = Arc::new(TestHandler);
        let h2: Arc<dyn AnyHookHandler> = Arc::new(TestHandler);

        reg.register_for_names_inner(p_low, c_low, h1, &["hook_a"])
            .unwrap();
        reg.register_for_names_inner(p_high, c_high, h2, &["hook_a"])
            .unwrap();

        let entries = reg.entries_for_hook(&HookType::new("hook_a"));
        assert_eq!(entries[0].plugin_ref.name(), "high"); // priority 10 first
        assert_eq!(entries[1].plugin_ref.name(), "low"); // priority 100 second
    }

    #[test]
    fn test_unregister() {
        let mut reg = PluginRegistry::new();
        let config = make_config("removable", vec![], 10);
        let plugin = Arc::new(TestPlugin::new(config.clone()));
        let handler: Arc<dyn AnyHookHandler> = Arc::new(TestHandler);

        reg.register_for_names_inner(plugin, config, handler, &["hook_a"])
            .unwrap();

        assert_eq!(reg.plugin_count(), 1);
        reg.unregister("removable");
        assert_eq!(reg.plugin_count(), 0);
        assert!(!reg.has_hooks_for(&HookType::new("hook_a")));
    }

    #[test]
    fn test_plugin_ref_id_is_unique() {
        let c1 = make_config("a", vec![], 10);
        let c2 = make_config("b", vec![], 10);
        let p1 = Arc::new(TestPlugin::new(c1.clone()));
        let p2 = Arc::new(TestPlugin::new(c2.clone()));
        let ref1 = PluginRef::new(p1, c1);
        let ref2 = PluginRef::new(p2, c2);
        assert_ne!(ref1.id(), ref2.id());
    }

    #[test]
    fn test_tampered_plugin_config_ignored() {
        let trusted = make_config("sneaky", vec![], 100);
        let mut tampered = trusted.clone();
        tampered.priority = 1;
        let plugin = Arc::new(TestPlugin::new(tampered));

        let plugin_ref = PluginRef::new(plugin, trusted);
        assert_eq!(plugin_ref.priority(), 100);
    }

    #[tokio::test]
    async fn test_handler_invoke() {
        let handler = TestHandler;
        let payload = TestPayload {
            value: "test".into(),
        };
        let ext = Extensions::default();
        let mut ctx = PluginContext::new();

        let result = handler
            .invoke(&payload as &dyn PluginPayload, &ext, &mut ctx)
            .await
            .unwrap();
        let fields = crate::executor::extract_erased(result).unwrap();
        assert!(fields.continue_processing);
    }
}
