// Copyright (c) Huawei Technologies Co., Ltd. 2026-2026. All rights reserved.

//! Dynamic plugin lifecycle manager.
//!
//! Handles loading shared libraries, symbol resolution, and calling
//! plugin init/start/stop/uninit functions via FFI.

use std::collections::HashSet;
use std::ffi::{c_char, CString};
use std::sync::OnceLock;

use libloading::Library;
use parking_lot::Mutex;
use serde_json::json;
use thiserror::Error;

use crate::config_manager::get_agent_config;
use crate::types::{KdcCallbacks, KdcPluginSetCbFn, PluginConfig};

#[derive(Error, Debug)]
pub enum PluginError {
    #[error("failed to load plugin '{name}': {reason}")]
    LoadFailed { name: String, reason: String },
    #[error("plugin '{name}' missing symbol '{symbol}': {reason}")]
    SymbolMissing {
        name: String,
        symbol: String,
        reason: String,
    },
    #[error("plugin '{name}' lifecycle error, code: {code}")]
    LifecycleError { name: String, code: u32 },
}

#[derive(Clone, Copy, Default)]
pub struct PluginState {
    pub initialized: bool,
    pub started: bool,
}

pub struct PluginManager {
    libraries: Vec<Library>,
    plugin_configs: Vec<PluginConfig>,
    plugin_states: Vec<PluginState>,
    callbacks: Option<KdcCallbacks>,
}

static PLUGIN_MANAGER: OnceLock<Mutex<PluginManager>> = OnceLock::new();

pub fn get_manager() -> &'static Mutex<PluginManager> {
    PLUGIN_MANAGER.get_or_init(|| {
        Mutex::new(PluginManager {
            libraries: Vec::new(),
            plugin_configs: Vec::new(),
            plugin_states: Vec::new(),
            callbacks: None,
        })
    })
}

pub fn load_plugins() -> Result<(), PluginError> {
    let config = get_agent_config().ok_or(PluginError::LoadFailed {
        name: "internal".to_string(),
        reason: "ConfigCenter not initialized".to_string(),
    })?;
    let mut manager = get_manager().lock();
    let mut seen_names: HashSet<String> = HashSet::new();

    for plugin in &config.plugins {
        let name = plugin.name.as_deref();
        let path = plugin.path.as_deref();

        if name.is_none() && path.is_none() {
            log::warn!("skipping plugin entry with no name and no path");
            continue;
        }
        if name.is_none() {
            log::warn!(
                "skipping plugin at path '{}': missing 'name' field",
                path.unwrap_or("(unknown)")
            );
            continue;
        }
        let name_str = name.unwrap();
        if path.is_none() {
            log::warn!("skipping plugin '{}': missing 'path' field", name_str);
            continue;
        }
        let path_str = path.unwrap();
        if seen_names.contains(name_str) {
            log::warn!(
                "skipping plugin '{}': duplicate name, already loaded",
                name_str
            );
            continue;
        }
        seen_names.insert(name_str.to_string());

        match unsafe { Library::new(path_str) } {
            Ok(lib) => {
                log::info!("loaded plugin: {}", name_str);
                manager.plugin_configs.push(plugin.clone());
                manager.libraries.push(lib);
                manager.plugin_states.push(PluginState::default());
            }
            Err(e) => {
                log::error!("failed to load plugin '{}': {}", name_str, e);
            }
        }
    }

    if manager.libraries.is_empty() {
        log::warn!("no plugins loaded, plugin list is empty or all entries were skipped");
    }

    Ok(())
}

fn for_each_plugin<F>(reverse: bool, mut action: F) -> Result<(), PluginError>
where
    F: FnMut(&Library, &str, &PluginConfig) -> Option<PluginError>,
{
    let manager = get_manager().lock();
    let n = manager.libraries.len();

    for idx in 0..n {
        let i = if reverse { n - 1 - idx } else { idx };
        let library = &manager.libraries[i];
        let name = manager
            .plugin_configs
            .get(i)
            .map(|p| p.name.as_deref().unwrap_or("(unnamed)"))
            .unwrap_or("unknown");
        let config = match manager.plugin_configs.get(i) {
            Some(c) => c,
            None => continue,
        };

        if let Some(err) = action(library, name, config) {
            return Err(err);
        }
    }

    Ok(())
}

enum StateCheck {
    Proceed,
    Skip(&'static str),
    SilentSkip,
}

fn for_each_with_state(
    symbol: &[u8],
    reverse: bool,
    ctx: *const c_char,
    void_return: bool,
    check_state: fn(&PluginState) -> StateCheck,
    on_success: fn(usize),
) -> (Vec<String>, Vec<String>) {
    let n = { get_manager().lock().libraries.len() };
    let mut succeeded: Vec<String> = Vec::new();
    let mut failed: Vec<String> = Vec::new();
    let sym_display = std::str::from_utf8(symbol)
        .unwrap_or("(invalid)")
        .trim_end_matches('\0');

    for idx in 0..n {
        let i = if reverse { n - 1 - idx } else { idx };

        let name = {
            let manager = get_manager().lock();
            let name = manager
                .plugin_configs
                .get(i)
                .map(|p| p.name.as_deref().unwrap_or("(unnamed)").to_string())
                .unwrap_or_else(|| format!("plugin[{}]", i));

            match check_state(&manager.plugin_states[i]) {
                StateCheck::Proceed => name,
                StateCheck::Skip(reason) => {
                    log::warn!("plugin '{}' {}", name, reason);
                    continue;
                }
                StateCheck::SilentSkip => continue,
            }
        };

        if void_return {
            let func: unsafe extern "C" fn(*const c_char) = {
                let manager = get_manager().lock();
                match unsafe {
                    manager.libraries[i].get::<unsafe extern "C" fn(*const c_char)>(symbol)
                } {
                    Ok(f) => *f,
                    Err(e) => {
                        log::error!("plugin '{}' missing symbol '{}': {}", name, sym_display, e);
                        failed.push(name);
                        continue;
                    }
                }
            };
            unsafe { func(ctx) };
            on_success(i);
            succeeded.push(name);
        } else {
            let func: unsafe extern "C" fn(*const c_char) -> u32 = {
                let manager = get_manager().lock();
                match unsafe {
                    manager.libraries[i].get::<unsafe extern "C" fn(*const c_char) -> u32>(symbol)
                } {
                    Ok(f) => *f,
                    Err(e) => {
                        log::error!("plugin '{}' missing symbol '{}': {}", name, sym_display, e);
                        failed.push(name);
                        continue;
                    }
                }
            };
            let ret = unsafe { func(ctx) };

            if ret == 0 {
                on_success(i);
                succeeded.push(name);
            } else {
                log::error!("plugin '{}' {} failed, code: {}", name, sym_display, ret);
                failed.push(name);
            }
        }
    }

    (succeeded, failed)
}

pub fn set_callbacks_for_all_plugins(callbacks: KdcCallbacks) -> Result<(), PluginError> {
    let mut manager = get_manager().lock();
    manager.callbacks = Some(callbacks);
    drop(manager);

    for_each_plugin(false, |library, name, _config| {
        unsafe {
            match library.get::<KdcPluginSetCbFn>(b"KdcPluginSetCb\0") {
                Ok(set_cb) => {
                    set_cb(callbacks);
                }
                Err(e) => {
                    log::error!("plugin '{}' missing symbol 'KdcPluginSetCb': {}", name, e);
                }
            }
        }
        None
    })
}

pub fn init_all_plugins() -> Result<(), PluginError> {
    let manager = get_manager().lock();
    let n = manager.libraries.len();
    drop(manager);
    let mut succeeded: Vec<String> = Vec::new();
    let mut failed: Vec<String> = Vec::new();

    for i in 0..n {
        let (name, context_json) = {
            let manager = get_manager().lock();
            let config = match manager.plugin_configs.get(i) {
                Some(c) => c.clone(),
                None => {
                    let name = format!("plugin[{}]", i);
                    log::error!("{} config missing", name);
                    failed.push(name);
                    continue;
                }
            };
            let name = config.name.as_deref().unwrap_or("(unnamed)").to_string();
            let plugin_param = config.param.clone();
            let mut context_map = match plugin_param {
                serde_json::Value::Object(map) => map,
                _ => serde_json::Map::new(),
            };
            let data_path = get_agent_config()
                .map(|c| c.data_path.clone())
                .unwrap_or_default();
            context_map.insert("dataPath".to_string(), json!(data_path));
            let context_json =
                match CString::new(serde_json::Value::Object(context_map).to_string()) {
                    Ok(s) => s,
                    Err(_) => {
                        log::error!("plugin '{}' context JSON contains null byte", name);
                        failed.push(name);
                        continue;
                    }
                };
            (name, context_json)
        };

        let func: unsafe extern "C" fn(*const c_char) -> u32 = {
            let manager = get_manager().lock();
            match unsafe {
                manager.libraries[i]
                    .get::<unsafe extern "C" fn(*const c_char) -> u32>(b"KdcPluginInit\0")
            } {
                Ok(f) => *f,
                Err(e) => {
                    log::error!("plugin '{}' missing symbol 'KdcPluginInit': {}", name, e);
                    failed.push(name);
                    continue;
                }
            }
        };
        let ret = unsafe { func(context_json.as_ptr()) };

        if ret == 0 {
            get_manager().lock().plugin_states[i].initialized = true;
            succeeded.push(name);
        } else {
            log::error!("plugin '{}' init failed, code: {}", name, ret);
            failed.push(name);
        }
    }

    log::info!(
        "plugin init complete: succeeded={:?}, failed={:?}",
        succeeded,
        failed
    );
    Ok(())
}

pub fn start_all_plugins() -> Result<(), PluginError> {
    let empty_ctx: *const c_char = b"\0".as_ptr() as *const c_char;
    let (succeeded, failed) = for_each_with_state(
        b"KdcPluginStart\0",
        false,
        empty_ctx,
        false,
        |state| {
            if !state.initialized {
                StateCheck::Skip("not initialized, skipping start")
            } else {
                StateCheck::Proceed
            }
        },
        |i| {
            get_manager().lock().plugin_states[i].started = true;
        },
    );
    log::info!(
        "plugin start complete: succeeded={:?}, failed={:?}",
        succeeded,
        failed
    );
    Ok(())
}

pub fn stop_all_plugins() -> Result<(), PluginError> {
    let empty_ctx: *const c_char = b"\0".as_ptr() as *const c_char;
    for_each_with_state(
        b"KdcPluginStop\0",
        true,
        empty_ctx,
        false,
        |state| {
            if !state.started {
                StateCheck::Skip("not started, skipping stop")
            } else {
                StateCheck::Proceed
            }
        },
        |i| {
            get_manager().lock().plugin_states[i].started = false;
        },
    );
    Ok(())
}

pub fn uninit_all_plugins() -> Result<(), PluginError> {
    let empty_ctx: *const c_char = b"\0".as_ptr() as *const c_char;
    for_each_with_state(
        b"KdcPluginUninit\0",
        true,
        empty_ctx,
        true,
        |state| {
            if state.started {
                return StateCheck::Skip("still started, skipping uninit");
            }
            if !state.initialized {
                return StateCheck::SilentSkip;
            }
            StateCheck::Proceed
        },
        |i| {
            get_manager().lock().plugin_states[i].initialized = false;
        },
    );
    Ok(())
}

#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
    use super::*;

    extern "C" fn noop_log(
        _level: crate::types::KdcLogLevel,
        _message: *const std::os::raw::c_char,
        _file: *const std::os::raw::c_char,
        _line: i32,
    ) {
    }

    extern "C" fn noop_register(
        _msg_type: u32,
        _cb: crate::types::KdcRequestHandlerCbFunc,
        _freecb: crate::types::KdcFreeResponsePtr,
    ) -> u32 {
        0
    }

    extern "C" fn noop_unregister(_msg_type: u32) -> u32 {
        0
    }

    extern "C" fn noop_db_execute(_sql: *const std::os::raw::c_char) -> u32 {
        0
    }

    extern "C" fn noop_db_query(
        _sql: *const std::os::raw::c_char,
        _out_json: *mut *mut std::os::raw::c_char,
    ) -> u32 {
        0
    }

    extern "C" fn noop_db_free(_s: *mut std::os::raw::c_char) {}

    extern "C" fn noop_db_sync() -> u32 {
        0
    }

    fn noop_callbacks() -> KdcCallbacks {
        KdcCallbacks {
            kdclog: noop_log,
            kdc_register_request_handle: noop_register,
            kdc_unregister_request_handle: noop_unregister,
            kdc_db_execute: noop_db_execute,
            kdc_db_query: noop_db_query,
            kdc_db_free: noop_db_free,
            kdc_db_sync: noop_db_sync,
        }
    }

    // Test: verify PluginError LoadFailed display message contains name and reason
    #[test]
    fn test_plugin_error_load_failed_message() {
        let err = PluginError::LoadFailed {
            name: "test_plugin".to_string(),
            reason: "file not found".to_string(),
        };
        let msg = format!("{}", err);
        assert!(msg.contains("test_plugin"), "should contain plugin name");
        assert!(msg.contains("file not found"), "should contain reason");
    }

    // Test: verify PluginError SymbolMissing display message contains all fields
    #[test]
    fn test_plugin_error_symbol_missing_message() {
        let err = PluginError::SymbolMissing {
            name: "my_plugin".to_string(),
            symbol: "KdcPluginInit".to_string(),
            reason: "undefined symbol".to_string(),
        };
        let msg = format!("{}", err);
        assert!(msg.contains("my_plugin"), "should contain plugin name");
        assert!(msg.contains("KdcPluginInit"), "should contain symbol name");
        assert!(msg.contains("undefined symbol"), "should contain reason");
    }

    // Test: verify PluginError LifecycleError display message contains name and code
    #[test]
    fn test_plugin_error_lifecycle_message() {
        let err = PluginError::LifecycleError {
            name: "worker_plugin".to_string(),
            code: 42,
        };
        let msg = format!("{}", err);
        assert!(msg.contains("worker_plugin"), "should contain plugin name");
        assert!(msg.contains("42"), "should contain error code");
    }

    // Test: verify PluginError Debug format shows variant names
    #[test]
    fn test_plugin_error_debug() {
        let err = PluginError::LoadFailed {
            name: "p".to_string(),
            reason: "r".to_string(),
        };
        assert!(format!("{:?}", err).contains("LoadFailed"));

        let err2 = PluginError::SymbolMissing {
            name: "p".to_string(),
            symbol: "s".to_string(),
            reason: "r".to_string(),
        };
        assert!(format!("{:?}", err2).contains("SymbolMissing"));

        let err3 = PluginError::LifecycleError {
            name: "p".to_string(),
            code: 1,
        };
        assert!(format!("{:?}", err3).contains("LifecycleError"));
    }

    // Test: verify PluginState default has initialized=false and started=false
    #[test]
    fn test_plugin_state_default() {
        let state = PluginState::default();
        assert!(!state.initialized, "default initialized should be false");
        assert!(!state.started, "default started should be false");
    }

    // Test: verify get_manager returns the same singleton instance
    #[test]
    fn test_plugin_manager_singleton() {
        let ptr1 = get_manager();
        let ptr2 = get_manager();
        assert!(
            std::ptr::eq(ptr1, ptr2),
            "get_manager should return the same singleton instance"
        );
    }

    // Test: verify set_callbacks_for_all_plugins succeeds
    #[test]
    fn test_set_callbacks_for_all_plugins_no_lock_error() {
        let callbacks = noop_callbacks();
        let result = set_callbacks_for_all_plugins(callbacks);
        assert!(
            result.is_ok(),
            "set_callbacks should succeed: {:?}",
            result.err()
        );
        let manager = get_manager().lock();
        assert!(manager.callbacks.is_some());
    }

    // Test: verify init_all_plugins always returns Ok
    #[test]
    fn test_init_all_plugins_ok() {
        let result = init_all_plugins();
        assert!(
            result.is_ok(),
            "init_all_plugins should return Ok: {:?}",
            result.err()
        );
    }

    // Test: verify load_plugins returns error when config is not initialized
    #[test]
    fn test_load_plugins_no_config() {
        if get_agent_config().is_some() {
            eprintln!("skipping: config already initialized");
            return;
        }
        let result = load_plugins();
        assert!(result.is_err(), "should fail without config");
        if let Err(PluginError::LoadFailed { name, reason }) = &result {
            assert_eq!(name, "internal");
            assert!(reason.contains("ConfigCenter not initialized"));
        } else {
            panic!("expected LoadFailed error, got: {:?}", result);
        }
    }

    // Test: verify load_plugins succeeds when config exists
    #[test]
    fn test_load_plugins_with_existing_config() {
        if get_agent_config().is_none() {
            eprintln!("skipping: config not initialized");
            return;
        }
        let result = load_plugins();
        assert!(
            result.is_ok(),
            "load_plugins should handle gracefully: {:?}",
            result.err()
        );
    }

    // Test: verify PluginConfig handles missing optional fields via serde
    #[test]
    fn test_plugin_config_missing_fields() {
        let json_str = r#"{"param":{"k":1}}"#;
        let cfg: PluginConfig = serde_json::from_str(json_str).unwrap();
        assert!(cfg.name.is_none());
        assert!(cfg.path.is_none());

        let json_with_path = r#"{"path":"/x.so","param":{}}"#;
        let cfg2: PluginConfig = serde_json::from_str(json_with_path).unwrap();
        assert!(cfg2.name.is_none());
        assert_eq!(cfg2.path.as_deref(), Some("/x.so"));
    }

    // Test: verify PluginConfig can represent missing name, missing path, and both missing
    #[test]
    fn test_plugin_config_option_fields() {
        let cfg_no_name = PluginConfig {
            name: None,
            path: Some("/tmp/test.so".to_string()),
            param: json!({}),
        };
        assert!(cfg_no_name.name.is_none());
        assert_eq!(cfg_no_name.path.as_deref(), Some("/tmp/test.so"));

        let cfg_no_path = PluginConfig {
            name: Some("test_plugin".to_string()),
            path: None,
            param: json!({}),
        };
        assert_eq!(cfg_no_path.name.as_deref(), Some("test_plugin"));
        assert!(cfg_no_path.path.is_none());

        let cfg_both = PluginConfig {
            name: None,
            path: None,
            param: json!({}),
        };
        assert!(cfg_both.name.is_none());
        assert!(cfg_both.path.is_none());
    }

    // Compile a mock .so plugin with all lifecycle functions returning 0 (success)
    fn compile_mock_plugin() -> Option<std::path::PathBuf> {
        let dir = std::env::temp_dir().join("kdc_plugin_test");
        let _ = std::fs::create_dir_all(&dir);
        let so_path = dir.join("libmock_plugin.so");
        if so_path.exists() {
            return Some(so_path);
        }
        let c_code = r#"
#include <stddef.h>
#include <stdint.h>

typedef struct {
    void (*kdclog)(int, const char*, const char*, int);
    unsigned int (*reg)(unsigned int, void*, void*);
    unsigned int (*unreg)(unsigned int);
    unsigned int (*db_execute)(const char*);
    unsigned int (*db_query)(const char*, char**);
    void (*db_free)(char*);
    unsigned int (*db_sync)(void);
} KdcCallbacks;

static KdcCallbacks g_callbacks = {0};

uint32_t KdcPluginInit(const char* ctx) { return 0; }
uint32_t KdcPluginStart(const char* ctx) { return 0; }
uint32_t KdcPluginStop(const char* ctx) { return 0; }
void KdcPluginUninit(const char* ctx) {}
uint32_t KdcPluginSetCb(KdcCallbacks cb) { g_callbacks = cb; return 0; }
"#;
        let c_path = dir.join("mock_plugin.c");
        std::fs::write(&c_path, c_code).unwrap();
        let output = std::process::Command::new("gcc")
            .args(["-shared", "-fPIC", "-o"])
            .arg(&so_path)
            .arg(&c_path)
            .output();
        match output {
            Ok(out) if out.status.success() => Some(so_path),
            _ => None,
        }
    }

    // Test: verify loading a mock .so plugin into the manager works
    #[test]
    fn test_z_load_plugins_with_mock_so() {
        let so_path = match compile_mock_plugin() {
            Some(p) => p,
            None => {
                eprintln!("skipping: gcc not available");
                return;
            }
        };

        let plugin_cfg = PluginConfig {
            name: Some("mock_plugin".to_string()),
            path: Some(so_path.to_str().unwrap().to_string()),
            param: serde_json::json!({"testKey": "testVal"}),
        };

        {
            let mut manager = get_manager().lock();
            let lib = unsafe { Library::new(&so_path).unwrap() };
            manager.libraries.push(lib);
            manager.plugin_configs.push(plugin_cfg);
            manager.plugin_states.push(PluginState::default());
        }

        let manager = get_manager().lock();
        assert!(
            !manager.libraries.is_empty(),
            "libraries should not be empty after loading mock"
        );
        assert!(
            manager
                .plugin_configs
                .iter()
                .any(|p| p.name.as_deref() == Some("mock_plugin")),
            "should contain mock_plugin config"
        );
    }

    // Compile a mock plugin where all lifecycle functions return 1 (error)
    fn compile_bad_mock_plugin() -> Option<std::path::PathBuf> {
        let dir = std::env::temp_dir().join("kdc_plugin_test_bad");
        let _ = std::fs::create_dir_all(&dir);
        let so_path = dir.join("libbad_mock_plugin.so");
        if so_path.exists() {
            return Some(so_path);
        }
        let c_code = r#"
#include <stddef.h>
#include <stdint.h>

typedef struct {
    void (*kdclog)(int, const char*, const char*, int);
    unsigned int (*reg)(unsigned int, void*, void*);
    unsigned int (*unreg)(unsigned int);
    unsigned int (*db_execute)(const char*);
    unsigned int (*db_query)(const char*, char**);
    void (*db_free)(char*);
    unsigned int (*db_sync)(void);
} KdcCallbacks;

uint32_t KdcPluginInit(const char* ctx) { return 1; }
uint32_t KdcPluginStart(const char* ctx) { return 1; }
uint32_t KdcPluginStop(const char* ctx) { return 1; }
void KdcPluginUninit(const char* ctx) {}
uint32_t KdcPluginSetCb(KdcCallbacks cb) { return 0; }
"#;
        let c_path = dir.join("bad_mock_plugin.c");
        std::fs::write(&c_path, c_code).unwrap();
        let output = std::process::Command::new("gcc")
            .args(["-shared", "-fPIC", "-o"])
            .arg(&so_path)
            .arg(&c_path)
            .output();
        match output {
            Ok(out) if out.status.success() => Some(so_path),
            _ => None,
        }
    }

    // Test: verify init_all_plugins handles plugin returning non-zero error code
    #[test]
    fn test_zz_bad_mock_init_error() {
        let so_path = match compile_bad_mock_plugin() {
            Some(p) => p,
            None => {
                eprintln!("skipping: gcc not available");
                return;
            }
        };

        let plugin_cfg = PluginConfig {
            name: Some("bad_mock".to_string()),
            path: Some(so_path.to_str().unwrap().to_string()),
            param: serde_json::json!({}),
        };

        {
            let mut manager = get_manager().lock();
            let lib = unsafe { Library::new(&so_path).unwrap() };
            manager.libraries.push(lib);
            manager.plugin_configs.push(plugin_cfg);
            manager.plugin_states.push(PluginState::default());
        }

        let result = init_all_plugins();
        assert!(
            result.is_ok(),
            "init_all_plugins always returns Ok: {:?}",
            result.err()
        );
    }

    // Test: verify for_each_plugin works with the currently loaded libraries
    #[test]
    fn test_zz_for_each_plugin_with_loaded() {
        let manager = get_manager().lock();
        if manager.libraries.is_empty() {
            eprintln!("skipping: no libraries loaded");
            return;
        }
        drop(manager);

        let mut count = 0usize;
        let result = for_each_plugin(false, |_lib, _name, _config| {
            count += 1;
            None
        });
        assert!(result.is_ok(), "for_each_plugin should succeed");
        assert!(count > 0, "should have iterated over at least one plugin");
    }

    // Test: verify for_each_plugin in reverse order
    #[test]
    fn test_zz_for_each_plugin_reverse() {
        let manager = get_manager().lock();
        if manager.libraries.is_empty() {
            eprintln!("skipping: no libraries loaded");
            return;
        }
        drop(manager);

        let mut count = 0usize;
        let result = for_each_plugin(true, |_lib, _name, _config| {
            count += 1;
            None
        });
        assert!(result.is_ok(), "for_each_plugin reverse should succeed");
        assert!(count > 0, "should have iterated over at least one plugin");
    }

    // Test: verify for_each_plugin returns error when closure returns Some
    #[test]
    fn test_zz_for_each_plugin_error() {
        let manager = get_manager().lock();
        if manager.libraries.is_empty() {
            eprintln!("skipping: no libraries loaded");
            return;
        }
        drop(manager);

        let result = for_each_plugin(false, |_lib, name, _config| {
            Some(PluginError::LifecycleError {
                name: name.to_string(),
                code: 99,
            })
        });
        assert!(result.is_err(), "should propagate error from closure");
        if let Err(PluginError::LifecycleError { code, .. }) = result {
            assert_eq!(code, 99);
        }
    }

    // Test: verify for_each_with_state handles missing symbol gracefully
    #[test]
    fn test_for_each_with_state_missing_symbol() {
        let so_path = match compile_mock_plugin() {
            Some(p) => p,
            None => {
                eprintln!("skipping: gcc not available");
                return;
            }
        };

        let plugin_cfg = PluginConfig {
            name: Some("missing_sym_test".to_string()),
            path: Some(so_path.to_str().unwrap().to_string()),
            param: serde_json::json!({}),
        };

        let idx = {
            let mut manager = get_manager().lock();
            let lib = unsafe { Library::new(&so_path).unwrap() };
            manager.libraries.push(lib);
            manager.plugin_configs.push(plugin_cfg);
            manager.plugin_states.push(PluginState::default());
            manager.libraries.len() - 1
        };

        let empty_ctx: *const c_char = b"\0".as_ptr() as *const c_char;
        let (_succeeded, failed) = for_each_with_state(
            b"NonExistentSymbol\0",
            false,
            empty_ctx,
            false,
            |_state| StateCheck::Proceed,
            |_| {},
        );
        assert!(failed.len() >= 1);
        let _ = idx;
    }

    // Test: verify for_each_with_state with StateCheck::Skip returns empty succeeded
    #[test]
    fn test_for_each_with_state_all_skip() {
        let so_path = match compile_mock_plugin() {
            Some(p) => p,
            None => {
                eprintln!("skipping: gcc not available");
                return;
            }
        };

        let plugin_cfg = PluginConfig {
            name: Some("skip_test".to_string()),
            path: Some(so_path.to_str().unwrap().to_string()),
            param: serde_json::json!({}),
        };

        {
            let mut manager = get_manager().lock();
            let lib = unsafe { Library::new(&so_path).unwrap() };
            manager.libraries.push(lib);
            manager.plugin_configs.push(plugin_cfg);
            manager.plugin_states.push(PluginState::default());
        }

        let empty_ctx: *const c_char = b"\0".as_ptr() as *const c_char;
        let (succeeded, failed) = for_each_with_state(
            b"KdcPluginInit\0",
            false,
            empty_ctx,
            false,
            |_state| StateCheck::Skip("forcefully skipping for test"),
            |_| {},
        );
        assert!(succeeded.is_empty());
        assert!(failed.is_empty());
    }

    // Test: verify for_each_with_state with StateCheck::SilentSkip returns empty
    #[test]
    fn test_for_each_with_state_all_silent_skip() {
        let so_path = match compile_mock_plugin() {
            Some(p) => p,
            None => {
                eprintln!("skipping: gcc not available");
                return;
            }
        };

        let plugin_cfg = PluginConfig {
            name: Some("silent_skip_test".to_string()),
            path: Some(so_path.to_str().unwrap().to_string()),
            param: serde_json::json!({}),
        };

        {
            let mut manager = get_manager().lock();
            let lib = unsafe { Library::new(&so_path).unwrap() };
            manager.libraries.push(lib);
            manager.plugin_configs.push(plugin_cfg);
            manager.plugin_states.push(PluginState::default());
        }

        let empty_ctx: *const c_char = b"\0".as_ptr() as *const c_char;
        let (succeeded, failed) = for_each_with_state(
            b"KdcPluginInit\0",
            false,
            empty_ctx,
            false,
            |_state| StateCheck::SilentSkip,
            |_| {},
        );
        assert!(succeeded.is_empty());
        assert!(failed.is_empty());
    }

    // Test: verify for_each_with_state with successful plugin call
    #[test]
    fn test_for_each_with_state_proceed_success() {
        let so_path = match compile_mock_plugin() {
            Some(p) => p,
            None => {
                eprintln!("skipping: gcc not available");
                return;
            }
        };

        let plugin_cfg = PluginConfig {
            name: Some("proceed_test".to_string()),
            path: Some(so_path.to_str().unwrap().to_string()),
            param: serde_json::json!({}),
        };

        {
            let mut manager = get_manager().lock();
            let lib = unsafe { Library::new(&so_path).unwrap() };
            manager.libraries.push(lib);
            manager.plugin_configs.push(plugin_cfg);
            manager.plugin_states.push(PluginState {
                initialized: true,
                started: false,
            });
        }

        let empty_ctx: *const c_char = b"\0".as_ptr() as *const c_char;
        let (succeeded, _failed) = for_each_with_state(
            b"KdcPluginInit\0",
            false,
            empty_ctx,
            false,
            |_state| StateCheck::Proceed,
            |_| {},
        );
        assert!(
            succeeded.iter().any(|n| n == "proceed_test"),
            "proceed_test should succeed"
        );
    }

    // Test: verify for_each_with_state with plugin returning error code
    #[test]
    fn test_for_each_with_state_proceed_error() {
        let so_path = match compile_bad_mock_plugin() {
            Some(p) => p,
            None => {
                eprintln!("skipping: gcc not available");
                return;
            }
        };

        let plugin_cfg = PluginConfig {
            name: Some("error_test".to_string()),
            path: Some(so_path.to_str().unwrap().to_string()),
            param: serde_json::json!({}),
        };

        {
            let mut manager = get_manager().lock();
            let lib = unsafe { Library::new(&so_path).unwrap() };
            manager.libraries.push(lib);
            manager.plugin_configs.push(plugin_cfg);
            manager.plugin_states.push(PluginState {
                initialized: true,
                started: true,
            });
        }

        let empty_ctx: *const c_char = b"\0".as_ptr() as *const c_char;
        let (_succeeded, failed) = for_each_with_state(
            b"KdcPluginInit\0",
            false,
            empty_ctx,
            false,
            |_state| StateCheck::Proceed,
            |_| {},
        );
        assert!(
            failed.iter().any(|n| n == "error_test"),
            "error_test should fail"
        );
    }

    // Test: verify init_all_plugins handles config with param as non-object
    #[test]
    fn test_init_all_plugins_with_non_object_param() {
        let so_path = match compile_mock_plugin() {
            Some(p) => p,
            None => {
                eprintln!("skipping: gcc not available");
                return;
            }
        };

        let plugin_cfg = PluginConfig {
            name: Some("non_obj_param_test".to_string()),
            path: Some(so_path.to_str().unwrap().to_string()),
            param: serde_json::json!("not_an_object"),
        };

        {
            let mut manager = get_manager().lock();
            let lib = unsafe { Library::new(&so_path).unwrap() };
            manager.libraries.push(lib);
            manager.plugin_configs.push(plugin_cfg);
            manager.plugin_states.push(PluginState::default());
        }

        let result = init_all_plugins();
        assert!(result.is_ok(), "init_all_plugins should return Ok: {:?}", result.err());
    }

    // Test: verify start/stop/uninit lifecycle with initialized and started plugins
    #[test]
    fn test_full_lifecycle_with_mock_plugin() {
        let so_path = match compile_mock_plugin() {
            Some(p) => p,
            None => {
                eprintln!("skipping: gcc not available");
                return;
            }
        };

        let plugin_cfg = PluginConfig {
            name: Some("lifecycle_test".to_string()),
            path: Some(so_path.to_str().unwrap().to_string()),
            param: serde_json::json!({"key": "value"}),
        };

        {
            let mut manager = get_manager().lock();
            let lib = unsafe { Library::new(&so_path).unwrap() };
            manager.libraries.push(lib);
            manager.plugin_configs.push(plugin_cfg);
            manager.plugin_states.push(PluginState::default());
        }

        // Step 1: set callbacks
        let callbacks = noop_callbacks();
        let _ = set_callbacks_for_all_plugins(callbacks);

        // Step 2: init
        let result = init_all_plugins();
        assert!(result.is_ok());

        // Verify initialized
        {
            let manager = get_manager().lock();
            let last_idx = manager.plugin_states.len() - 1;
            assert!(manager.plugin_states[last_idx].initialized);
        }

        // Step 3: start
        let result = start_all_plugins();
        assert!(result.is_ok());

        // Verify started
        {
            let manager = get_manager().lock();
            let last_idx = manager.plugin_states.len() - 1;
            assert!(manager.plugin_states[last_idx].started);
        }

        // Step 4: stop
        let result = stop_all_plugins();
        assert!(result.is_ok());

        // Verify stopped (started = false)
        {
            let manager = get_manager().lock();
            let last_idx = manager.plugin_states.len() - 1;
            assert!(!manager.plugin_states[last_idx].started);
        }

        // Step 5: uninit
        let result = uninit_all_plugins();
        assert!(result.is_ok());

        // Verify uninitialized
        {
            let manager = get_manager().lock();
            let last_idx = manager.plugin_states.len() - 1;
            assert!(!manager.plugin_states[last_idx].initialized);
        }
    }

    // Test: verify uninit_all_plugins skips plugins that are still started
    #[test]
    fn test_uninit_skips_started_plugins() {
        let so_path = match compile_mock_plugin() {
            Some(p) => p,
            None => {
                eprintln!("skipping: gcc not available");
                return;
            }
        };

        let plugin_cfg = PluginConfig {
            name: Some("started_uninit_test".to_string()),
            path: Some(so_path.to_str().unwrap().to_string()),
            param: serde_json::json!({}),
        };

        {
            let mut manager = get_manager().lock();
            let lib = unsafe { Library::new(&so_path).unwrap() };
            manager.libraries.push(lib);
            manager.plugin_configs.push(plugin_cfg);
            manager.plugin_states.push(PluginState {
                initialized: true,
                started: true,
            });
        }

        // uninit should skip because started=true
        let result = uninit_all_plugins();
        assert!(result.is_ok());

        // Verify still initialized and started
        {
            let manager = get_manager().lock();
            let last_idx = manager.plugin_states.len() - 1;
            assert!(manager.plugin_states[last_idx].initialized);
            assert!(manager.plugin_states[last_idx].started);
        }
    }

    // Test: verify start_all_plugins skips uninitialized plugins
    #[test]
    fn test_start_skips_uninitialized() {
        let so_path = match compile_mock_plugin() {
            Some(p) => p,
            None => {
                eprintln!("skipping: gcc not available");
                return;
            }
        };

        let plugin_cfg = PluginConfig {
            name: Some("uninit_start_test".to_string()),
            path: Some(so_path.to_str().unwrap().to_string()),
            param: serde_json::json!({}),
        };

        {
            let mut manager = get_manager().lock();
            let lib = unsafe { Library::new(&so_path).unwrap() };
            manager.libraries.push(lib);
            manager.plugin_configs.push(plugin_cfg);
            manager.plugin_states.push(PluginState {
                initialized: false,
                started: false,
            });
        }

        let result = start_all_plugins();
        assert!(result.is_ok());

        // Verify still not started
        {
            let manager = get_manager().lock();
            let last_idx = manager.plugin_states.len() - 1;
            assert!(!manager.plugin_states[last_idx].started);
        }
    }

    // Test: verify stop_all_plugins skips plugins that were not started
    #[test]
    fn test_stop_skips_not_started() {
        let so_path = match compile_mock_plugin() {
            Some(p) => p,
            None => {
                eprintln!("skipping: gcc not available");
                return;
            }
        };

        let plugin_cfg = PluginConfig {
            name: Some("not_started_stop_test".to_string()),
            path: Some(so_path.to_str().unwrap().to_string()),
            param: serde_json::json!({}),
        };

        {
            let mut manager = get_manager().lock();
            let lib = unsafe { Library::new(&so_path).unwrap() };
            manager.libraries.push(lib);
            manager.plugin_configs.push(plugin_cfg);
            manager.plugin_states.push(PluginState {
                initialized: true,
                started: false,
            });
        }

        let result = stop_all_plugins();
        assert!(result.is_ok());
    }

    // Test: verify for_each_with_state reverse order processes plugins correctly
    #[test]
    fn test_for_each_with_state_reverse_order() {
        let so_path = match compile_mock_plugin() {
            Some(p) => p,
            None => {
                eprintln!("skipping: gcc not available");
                return;
            }
        };

        // Load two plugins to test reverse ordering
        for suffix in &["rev_a", "rev_b"] {
            let plugin_cfg = PluginConfig {
                name: Some(suffix.to_string()),
                path: Some(so_path.to_str().unwrap().to_string()),
                param: serde_json::json!({}),
            };
            let mut manager = get_manager().lock();
            let lib = unsafe { Library::new(&so_path).unwrap() };
            manager.libraries.push(lib);
            manager.plugin_configs.push(plugin_cfg);
            manager.plugin_states.push(PluginState {
                initialized: true,
                started: false,
            });
        }

        let empty_ctx: *const c_char = b"\0".as_ptr() as *const c_char;
        let (succeeded, _failed) = for_each_with_state(
            b"KdcPluginInit\0",
            true,
            empty_ctx,
            false,
            |_state| StateCheck::Proceed,
            |i| {
                get_manager().lock().plugin_states[i].initialized = true;
            },
        );
        assert!(
            succeeded.iter().any(|n| n == "rev_a"),
            "rev_a should succeed"
        );
        assert!(
            succeeded.iter().any(|n| n == "rev_b"),
            "rev_b should succeed"
        );
    }

    // Test: verify full lifecycle via load_plugins -> init -> start -> stop -> uninit with config
    #[test]
    fn test_z_full_lifecycle_via_config() {
        let config = get_agent_config();
        if config.is_none() {
            eprintln!("skipping: config not initialized");
            return;
        }
        let config = config.unwrap();
        if config.plugins.is_empty() {
            eprintln!("skipping: no plugins in config");
            return;
        }

        let callbacks = noop_callbacks();
        let _ = set_callbacks_for_all_plugins(callbacks);

        let result = init_all_plugins();
        assert!(result.is_ok(), "init should succeed: {:?}", result.err());

        let result = start_all_plugins();
        assert!(result.is_ok(), "start should succeed: {:?}", result.err());

        let result = stop_all_plugins();
        assert!(result.is_ok(), "stop should succeed: {:?}", result.err());

        let result = uninit_all_plugins();
        assert!(result.is_ok(), "uninit should succeed: {:?}", result.err());
    }

    // Test: verify load_plugins handles all plugins failing to load (empty libraries)
    #[test]
    fn test_z_load_plugins_all_fail_empty_libraries() {
        let config = get_agent_config();
        if config.is_none() {
            return;
        }
        let cfg = config.unwrap();

        let saved_files: Vec<(String, Vec<u8>)> = cfg
            .plugins
            .iter()
            .filter_map(|p| p.path.as_ref())
            .filter_map(|p| std::fs::read(p).ok().map(|d| (p.clone(), d)))
            .collect();

        for (path, _) in &saved_files {
            let _ = std::fs::remove_file(path);
        }

        {
            let mut mgr = get_manager().lock();
            mgr.libraries.clear();
            mgr.plugin_configs.clear();
            mgr.plugin_states.clear();
        }

        let result = load_plugins();
        assert!(result.is_ok(), "load_plugins should succeed: {:?}", result.err());

        {
            let mgr = get_manager().lock();
            assert!(mgr.libraries.is_empty(), "libraries should be empty when all plugin files removed, got {} libs", mgr.libraries.len());
        }

        for (path, data) in &saved_files {
            if let Some(parent) = std::path::Path::new(path).parent() {
                let _ = std::fs::create_dir_all(parent);
            }
            let _ = std::fs::write(path, data);
        }

        let _ = load_plugins();
    }
}