//! 示例动态插件 — 展示如何使用 `showen-plugin-sdk` 编写较完整的插件。
//!
//! 这个示例特意覆盖几个第三方开发者最常见的需求：
//! 1. 使用 `MessageSender` 发送点对点、广播、管理层以及原始 `Envelope` 消息。
//! 2. 在 `handle_message` 中处理多种 `Message` 变体。
//! 3. 用 `serde` 解析配置，并在解析后做显式校验。
//! 4. 用 `thread + sleep` 模拟一个简单的定时后台任务。
//! 5. 提供完整的 `capabilities` 和 `self_test` 实现。
//! 6. 用注释解释每个阶段应该做什么，作为插件作者的参考模板。

use serde::{Deserialize, Serialize};
use showen_plugin_sdk::{
    export_plugin, CapabilityTestResult, Destination, DeviceCommand, DeviceResponse, Envelope,
    Message, MessageSender, PluginInfo, ShowenPlugin,
};
use std::sync::{
    atomic::{AtomicBool, Ordering},
    Arc,
};
use std::thread::{self, JoinHandle};
use std::time::Duration;

const PLUGIN_ID: &str = "example-plugin";
const CAP_MESSAGE_SENDER: &str = "message_sender";
const CAP_MESSAGE_ROUTING: &str = "message_routing";
const CAP_CONFIG_PARSING: &str = "config_parsing";
const CAP_BACKGROUND_TASK: &str = "background_task";
const CAP_SELF_TEST: &str = "self_test";
const CAP_REQUEST_RESPONSE: &str = "request_response";

/// 示例插件配置。
///
/// 这里使用 `#[serde(default)]` 而不是依赖调用方传完整配置，目的是让示例更健壮：
/// - 新增字段后，旧配置仍然可以工作。
/// - 每个字段都有清晰的默认值。
/// - `validate()` 负责做业务约束校验，把解析错误和业务错误分开。
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(default, deny_unknown_fields)]
struct ExamplePluginConfig {
    /// 后台任务的心跳间隔，单位毫秒。
    heartbeat_interval_ms: u64,
    /// 启动后示例消息要发给哪个插件。
    target_plugin: String,
    /// 是否在 `start()` 时发送一组教学用途的示例消息。
    announce_on_start: bool,
    /// DevicePlugin 的插件 ID，用于演示请求/响应模式。
    device_plugin: String,
    /// 是否在 `start()` 时请求一次显示信息。
    request_display_info_on_start: bool,
    /// 是否启用后台定时任务。
    enable_periodic_task: bool,
    /// 周期性上报里携带的示例文本。
    periodic_payload: String,
    /// 用来演示可配置的自测失败项。
    optional_test_should_fail: bool,
}

impl Default for ExamplePluginConfig {
    fn default() -> Self {
        Self {
            heartbeat_interval_ms: 5_000,
            target_plugin: PLUGIN_ID.to_string(),
            announce_on_start: true,
            device_plugin: "device".to_string(),
            request_display_info_on_start: true,
            enable_periodic_task: true,
            periodic_payload: "heartbeat-from-example-plugin".to_string(),
            optional_test_should_fail: false,
        }
    }
}

impl ExamplePluginConfig {
    /// 解析 JSON 配置，并在成功后执行额外校验。
    fn from_json(config_json: &str) -> Result<Self, String> {
        let trimmed = config_json.trim();
        let mut config = if trimmed.is_empty() || trimmed == "null" {
            Self::default()
        } else {
            serde_json::from_str::<Self>(trimmed)
                .map_err(|error| format!("failed to parse example plugin config: {error}"))?
        };

        config.normalize();
        config.validate()?;
        Ok(config)
    }

    /// 处理 `ConfigReloaded` 的场景，调用方式与初始化时保持一致。
    fn from_value(value: serde_json::Value) -> Result<Self, String> {
        let mut config = serde_json::from_value::<Self>(value)
            .map_err(|error| format!("failed to decode reloaded config: {error}"))?;
        config.normalize();
        config.validate()?;
        Ok(config)
    }

    fn normalize(&mut self) {
        self.target_plugin = self.target_plugin.trim().to_string();
        self.device_plugin = self.device_plugin.trim().to_string();
        self.periodic_payload = self.periodic_payload.trim().to_string();
    }

    fn validate(&self) -> Result<(), String> {
        if self.target_plugin.trim().is_empty() {
            return Err("config field `target_plugin` must not be empty".to_string());
        }

        if self.request_display_info_on_start && self.device_plugin.trim().is_empty() {
            return Err(
                "config field `device_plugin` must not be empty when `request_display_info_on_start` is enabled"
                    .to_string(),
            );
        }

        if self.periodic_payload.trim().is_empty() {
            return Err("config field `periodic_payload` must not be empty".to_string());
        }

        if self.enable_periodic_task && self.heartbeat_interval_ms < 100 {
            return Err(
                "config field `heartbeat_interval_ms` must be at least 100 when the periodic task is enabled"
                    .to_string(),
            );
        }

        Ok(())
    }
}

/// 一个更完整的示例插件实现。
pub struct ExamplePlugin {
    /// `MessageSender` 会在 `init()` 时由宿主注入。
    ///
    /// 用 `Arc` 包起来是为了后台线程可以安全共享同一个 sender。
    sender: Option<Arc<MessageSender>>,
    /// 当前生效配置。
    config: ExamplePluginConfig,
    /// 用于结束后台线程的停止信号。
    worker_stop: Arc<AtomicBool>,
    /// 后台线程句柄；在 `stop()` 和配置重载时负责回收。
    worker: Option<JoinHandle<()>>,
    /// 用于演示 `self_test()` 如何暴露可选失败项。
    fail_optional_test: bool,
}

impl ExamplePlugin {
    pub fn new() -> Self {
        Self {
            sender: None,
            config: ExamplePluginConfig::default(),
            worker_stop: Arc::new(AtomicBool::new(false)),
            worker: None,
            fail_optional_test: false,
        }
    }

    fn sender(&self) -> Result<&Arc<MessageSender>, String> {
        self.sender
            .as_ref()
            .ok_or_else(|| "plugin sender is not initialized; call init() first".to_string())
    }

    /// 发送一个“插件已准备好”的管理消息。
    ///
    /// 这是插件初始化阶段最常见的通知类型之一。
    fn notify_ready(&self) -> Result<(), String> {
        self.sender()?
            .send_to_manager(PLUGIN_ID, Message::PluginReady(PLUGIN_ID.to_string()));
        Ok(())
    }

    /// 演示 `MessageSender` 的四种常见用法：
    /// - `send_to_manager()`
    /// - `broadcast()`
    /// - `send_to()`
    /// - `send()` + 原始 `Envelope`
    fn emit_demo_messages(&self) -> Result<(), String> {
        let sender = self.sender()?;

        sender.send_to_manager(
            PLUGIN_ID,
            Message::Custom {
                kind: "example.lifecycle".to_string(),
                payload: "start() completed".to_string(),
            },
        );

        sender.broadcast(
            PLUGIN_ID,
            Message::StateChanged {
                old_state: "initialized".to_string(),
                new_state: "running".to_string(),
            },
        );

        sender.send_to(
            PLUGIN_ID,
            &self.config.target_plugin,
            Message::Trigger {
                name: "example.handshake".to_string(),
                value: "hello-from-example-plugin".to_string(),
            },
        );

        let config_payload = serde_json::to_string(&self.config)
            .map_err(|error| format!("failed to serialize config snapshot: {error}"))?;
        sender.send(&Envelope {
            from: PLUGIN_ID.to_string(),
            to: Destination::Manager,
            message: Message::Custom {
                kind: "example.config_snapshot".to_string(),
                payload: config_payload,
            },
        });

        Ok(())
    }

    /// 演示典型的请求/响应模式：向 DevicePlugin 请求一次显示信息，随后在
    /// `handle_message()` 里处理 `Message::DeviceResponse`。
    fn request_display_info(&self) -> Result<(), String> {
        let sender = self.sender()?;
        sender.send_to(
            PLUGIN_ID,
            &self.config.device_plugin,
            Message::DeviceCommand(DeviceCommand::GetDisplayInfo),
        );

        sender.send_to_manager(
            PLUGIN_ID,
            Message::Custom {
                kind: "example.request_sent".to_string(),
                payload: format!(
                    "requested DeviceCommand::GetDisplayInfo from {}",
                    self.config.device_plugin
                ),
            },
        );

        Ok(())
    }

    /// 启动一个简单的后台线程，周期性向管理层发送心跳消息。
    ///
    /// 这里故意使用 `thread::sleep`，因为这是第三方插件开发者最容易理解的最小示例。
    fn start_worker(&mut self) -> Result<(), String> {
        self.stop_worker()?;

        if !self.config.enable_periodic_task {
            eprintln!("[ExamplePlugin] periodic task disabled by config");
            return Ok(());
        }

        let sender = Arc::clone(self.sender()?);
        let stop_flag = Arc::clone(&self.worker_stop);
        let interval = Duration::from_millis(self.config.heartbeat_interval_ms);
        let payload = self.config.periodic_payload.clone();

        stop_flag.store(false, Ordering::SeqCst);
        self.worker = Some(thread::spawn(move || {
            let mut tick = 0_u64;

            loop {
                thread::sleep(interval);
                if stop_flag.load(Ordering::SeqCst) {
                    break;
                }

                tick += 1;
                sender.send_to_manager(
                    PLUGIN_ID,
                    Message::Custom {
                        kind: "example.heartbeat".to_string(),
                        payload: format!("{{\"tick\":{tick},\"payload\":{payload:?}}}"),
                    },
                );
            }
        }));

        Ok(())
    }

    /// 停止后台线程并回收资源。
    fn stop_worker(&mut self) -> Result<(), String> {
        self.worker_stop.store(true, Ordering::SeqCst);

        if let Some(handle) = self.worker.take() {
            handle
                .join()
                .map_err(|_| "example background worker panicked".to_string())?;
        }

        Ok(())
    }

    /// 统一处理触发器消息，让 `handle_message()` 保持可读性。
    fn handle_trigger(&mut self, name: String, value: String) -> Result<(), String> {
        eprintln!("[ExamplePlugin] trigger received: {name}={value}");

        match name.as_str() {
            "example.report_self_test" => {
                let payload = serde_json::to_string(&self.self_test())
                    .map_err(|error| format!("failed to serialize self_test results: {error}"))?;
                self.sender()?.send_to_manager(
                    PLUGIN_ID,
                    Message::Custom {
                        kind: "example.self_test_report".to_string(),
                        payload,
                    },
                );
            }
            "example.send_demo_messages" => {
                self.emit_demo_messages()?;
            }
            "example.request_display_info" => {
                self.request_display_info()?;
            }
            _ => {
                self.sender()?.send_to_manager(
                    PLUGIN_ID,
                    Message::Custom {
                        kind: "example.trigger_ack".to_string(),
                        payload: format!("unknown trigger ignored: {name}={value}"),
                    },
                );
            }
        }

        Ok(())
    }

    /// 演示如何为自定义消息协议做显式解析和友好错误提示。
    ///
    /// 这里把 `payload` 约定为 JSON，模拟一个“订阅更新”场景；真实业务可以换成
    /// 自己的协议名称和字段。
    fn handle_custom_message(&self, kind: String, payload: String) -> Result<(), String> {
        eprintln!("[ExamplePlugin] custom message: kind={kind}, payload={payload}");

        if kind != "example.subscription.update" {
            return Ok(());
        }

        let parsed: serde_json::Value = serde_json::from_str(&payload).map_err(|error| {
            format!(
                "custom message `example.subscription.update` expects JSON payload, got parse error: {error}"
            )
        })?;

        let topic = parsed
            .get("topic")
            .and_then(serde_json::Value::as_str)
            .map(str::trim)
            .filter(|topic| !topic.is_empty())
            .ok_or_else(|| {
                "custom message `example.subscription.update` requires non-empty string field `topic`"
                    .to_string()
            })?;

        self.sender()?.send_to_manager(
            PLUGIN_ID,
            Message::Custom {
                kind: "example.subscription.ack".to_string(),
                payload: format!("subscription update processed for topic `{topic}`"),
            },
        );

        Ok(())
    }

    /// 处理配置热重载。
    ///
    /// 最佳实践：
    /// 1. 先解析并校验新配置。
    /// 2. 仅在成功后替换运行时状态。
    /// 3. 如果配置会影响后台任务，重启对应任务。
    fn reload_config(&mut self, value: serde_json::Value) -> Result<(), String> {
        let next_config = ExamplePluginConfig::from_value(value)?;
        self.config = next_config;
        self.fail_optional_test = self.config.optional_test_should_fail;
        self.start_worker()?;

        let payload = serde_json::to_string(&self.config)
            .map_err(|error| format!("failed to serialize reloaded config: {error}"))?;
        self.sender()?.send_to_manager(
            PLUGIN_ID,
            Message::Custom {
                kind: "example.config_reloaded".to_string(),
                payload,
            },
        );

        Ok(())
    }
}

impl ShowenPlugin for ExamplePlugin {
    fn info(&self) -> PluginInfo {
        PluginInfo {
            name: PLUGIN_ID.to_string(),
            version: env!("CARGO_PKG_VERSION").to_string(),
            description: "Feature-complete example plugin for third-party developers".to_string(),
            platform: "Any".to_string(),
        }
    }

    fn capabilities(&self) -> Vec<String> {
        vec![
            CAP_MESSAGE_SENDER.to_string(),
            CAP_MESSAGE_ROUTING.to_string(),
            CAP_CONFIG_PARSING.to_string(),
            CAP_BACKGROUND_TASK.to_string(),
            CAP_SELF_TEST.to_string(),
            CAP_REQUEST_RESPONSE.to_string(),
        ]
    }

    fn self_test(&mut self) -> Vec<CapabilityTestResult> {
        let config_ok = self.config.validate().is_ok();
        let worker_ok = !self.config.enable_periodic_task || self.worker.is_some();
        let sender_ready = self.sender.is_some();

        vec![
            CapabilityTestResult {
                capability: CAP_MESSAGE_SENDER.to_string(),
                passed: sender_ready,
                message: if sender_ready {
                    "MessageSender injected during init()".to_string()
                } else {
                    "MessageSender not available yet".to_string()
                },
            },
            CapabilityTestResult {
                capability: CAP_MESSAGE_ROUTING.to_string(),
                passed: sender_ready,
                message: "send_to_manager / broadcast / send_to / send are implemented".to_string(),
            },
            CapabilityTestResult {
                capability: CAP_CONFIG_PARSING.to_string(),
                passed: config_ok,
                message: if config_ok {
                    "config parsed with serde defaults + validation".to_string()
                } else {
                    "current config failed validation".to_string()
                },
            },
            CapabilityTestResult {
                capability: CAP_BACKGROUND_TASK.to_string(),
                passed: worker_ok,
                message: if self.config.enable_periodic_task {
                    if worker_ok {
                        "periodic worker thread is running".to_string()
                    } else {
                        "periodic worker thread is not running".to_string()
                    }
                } else {
                    "periodic task disabled by config".to_string()
                },
            },
            CapabilityTestResult {
                capability: CAP_SELF_TEST.to_string(),
                passed: !self.fail_optional_test,
                message: if self.fail_optional_test {
                    "optional self-test failure requested by config".to_string()
                } else {
                    "self-test harness operational".to_string()
                },
            },
            CapabilityTestResult {
                capability: CAP_REQUEST_RESPONSE.to_string(),
                passed: !self.config.request_display_info_on_start
                    || !self.config.device_plugin.is_empty(),
                message: if self.config.request_display_info_on_start {
                    format!(
                        "request/response demo targets DevicePlugin `{}`",
                        self.config.device_plugin
                    )
                } else {
                    "request/response demo disabled by config".to_string()
                },
            },
        ]
    }

    fn init(&mut self, config_json: &str, sender: MessageSender) -> Result<(), String> {
        eprintln!(
            "[ExamplePlugin] init called, received config bytes: {}",
            config_json.len()
        );

        let config = ExamplePluginConfig::from_json(config_json)?;
        self.config = config;
        self.fail_optional_test = self.config.optional_test_should_fail;
        self.sender = Some(Arc::new(sender));

        self.notify_ready()?;
        Ok(())
    }

    fn start(&mut self) -> Result<(), String> {
        eprintln!("[ExamplePlugin] start called");

        if self.config.announce_on_start {
            self.emit_demo_messages()?;
        }

        if self.config.request_display_info_on_start {
            self.request_display_info()?;
        }

        self.start_worker()?;
        Ok(())
    }

    fn handle_message(&mut self, message: Message) -> Result<(), String> {
        match message {
            Message::PlayerCommand(payload) => {
                eprintln!("[ExamplePlugin] player command: {payload}");
            }
            Message::PlayerStatus(payload) => {
                eprintln!("[ExamplePlugin] player status: {payload}");
            }
            Message::Trigger { name, value } => {
                self.handle_trigger(name, value)?;
            }
            Message::StateChanged {
                old_state,
                new_state,
            } => {
                eprintln!("[ExamplePlugin] state changed: {old_state} -> {new_state}");
            }
            Message::ScreenLockRequest(locked) => {
                eprintln!("[ExamplePlugin] screen lock requested: {locked}");
            }
            Message::CursorVisibility(visible) => {
                eprintln!("[ExamplePlugin] cursor visibility requested: {visible}");
            }
            Message::WifiCommand(payload) => {
                eprintln!("[ExamplePlugin] wifi command payload: {payload}");
            }
            Message::WifiResult(result) => {
                eprintln!("[ExamplePlugin] wifi result: {result}");
            }
            Message::WifiProvisioned { ssid, ip } => {
                eprintln!("[ExamplePlugin] wifi provisioned: ssid={ssid}, ip={ip}");
                self.sender()?.broadcast(
                    PLUGIN_ID,
                    Message::Custom {
                        kind: "example.wifi_provisioned".to_string(),
                        payload: format!("ssid={ssid}, ip={ip}"),
                    },
                );
            }
            Message::ConfigReloaded(config) => {
                self.reload_config(config)?;
            }
            Message::ConfigReloadRequest => {
                self.sender()?.send_to_manager(
                    PLUGIN_ID,
                    Message::Custom {
                        kind: "example.reload_request".to_string(),
                        payload: "host should respond with ConfigReloaded(JSON)".to_string(),
                    },
                );
            }
            Message::Shutdown => {
                eprintln!("[ExamplePlugin] shutdown requested");
                self.stop()?;
            }
            Message::PluginReady(plugin_name) => {
                eprintln!("[ExamplePlugin] observed peer readiness: {plugin_name}");
            }
            Message::DeviceCommand(command) => {
                eprintln!("[ExamplePlugin] device command received (not handled): {command:?}");
            }
            Message::DeviceResponse(response) => {
                eprintln!("[ExamplePlugin] device response received: {response:?}");

                let summary = match response {
                    DeviceResponse::DisplayInfo {
                        width,
                        height,
                        format,
                    } => format!(
                        "display info received: width={width}, height={height}, format={format:?}"
                    ),
                    DeviceResponse::BatteryLevel(level) => {
                        format!("battery level response received: {level}%")
                    }
                    DeviceResponse::SensorData { sensor, value } => {
                        format!("sensor response received: {sensor:?}={value}")
                    }
                    DeviceResponse::Ok => "device command completed successfully".to_string(),
                    DeviceResponse::Error(error) => format!("device command failed: {error}"),
                    DeviceResponse::Custom(value) => format!("custom device response: {value}"),
                };

                self.sender()?.send_to_manager(
                    PLUGIN_ID,
                    Message::Custom {
                        kind: "example.device_response".to_string(),
                        payload: summary,
                    },
                );
            }
            Message::DeviceEvent(_event) => {
                eprintln!("[ExamplePlugin] device event received (not handled)");
            }
            Message::Custom { kind, payload } => {
                self.handle_custom_message(kind, payload)?;
            }
        }

        Ok(())
    }

    fn stop(&mut self) -> Result<(), String> {
        eprintln!("[ExamplePlugin] stop called");
        self.stop_worker()?;
        self.sender = None;
        Ok(())
    }
}

// 导出动态插件 FFI 接口。
//
// `export_plugin!` 会生成主程序约定的完整 ABI：
// - `create` / `destroy`: 创建和销毁插件实例
// - `get_info`: 返回 `PluginInfo` 的 JSON
// - `init`: 接收配置 JSON 和宿主注入的消息发送回调
// - `start` / `stop`: 驱动插件生命周期
// - `handle_message`: 接收宿主转发的 JSON 消息并反序列化为 `Message`
// - `get_capabilities` / `self_test`: 向宿主暴露能力声明和自检结果
// - `free_string`: 释放由当前插件分配给宿主的 `FfiString`
export_plugin!(ExamplePlugin, ExamplePlugin::new());

#[cfg(test)]
mod tests {
    use super::*;
    use std::ffi::{c_char, c_void, CStr};
    use std::sync::Mutex;
    use std::thread;

    struct Recorder {
        envelopes: Box<Mutex<Vec<Envelope>>>,
    }

    impl Recorder {
        fn new() -> Self {
            Self {
                envelopes: Box::new(Mutex::new(Vec::new())),
            }
        }

        fn sender(&self) -> MessageSender {
            let ctx = (&*self.envelopes as *const Mutex<Vec<Envelope>>) as *mut c_void;
            MessageSender::new(ctx, record_envelope)
        }

        fn snapshot(&self) -> Vec<Envelope> {
            self.envelopes
                .lock()
                .expect("recorder mutex poisoned")
                .clone()
        }
    }

    unsafe extern "C" fn record_envelope(ctx: *mut c_void, envelope_json: *const c_char) {
        let storage = unsafe { &*(ctx as *const Mutex<Vec<Envelope>>) };
        let raw = unsafe { CStr::from_ptr(envelope_json) }
            .to_str()
            .expect("callback JSON should be valid UTF-8");
        let envelope =
            serde_json::from_str::<Envelope>(raw).expect("callback JSON should decode to Envelope");
        storage
            .lock()
            .expect("recorder mutex poisoned")
            .push(envelope);
    }

    #[test]
    fn config_defaults_when_json_is_empty() {
        assert_eq!(
            ExamplePluginConfig::from_json("   ").expect("empty config should use defaults"),
            ExamplePluginConfig::default()
        );
    }

    #[test]
    fn config_rejects_too_small_heartbeat_interval() {
        let error = ExamplePluginConfig::from_json(
            r#"{"heartbeat_interval_ms":99,"enable_periodic_task":true}"#,
        )
        .expect_err("config should be rejected");

        assert!(error.contains("heartbeat_interval_ms"));
    }

    #[test]
    fn config_rejects_missing_device_plugin_when_request_response_is_enabled() {
        let error = ExamplePluginConfig::from_json(
            r#"{"device_plugin":"   ","request_display_info_on_start":true}"#,
        )
        .expect_err("config should be rejected");

        assert!(error.contains("device_plugin"));
    }

    #[test]
    fn start_sends_demo_messages_and_heartbeat() {
        let recorder = Recorder::new();
        let mut plugin = ExamplePlugin::new();

        plugin
            .init(
                r#"{
                    "heartbeat_interval_ms": 100,
                    "target_plugin": "example-plugin",
                    "announce_on_start": true,
                    "enable_periodic_task": true,
                    "periodic_payload": "test-heartbeat"
                }"#,
                recorder.sender(),
            )
            .expect("init should succeed");

        plugin.start().expect("start should succeed");
        thread::sleep(Duration::from_millis(130));
        plugin.stop().expect("stop should succeed");

        let envelopes = recorder.snapshot();
        assert!(
            envelopes
                .iter()
                .any(|env| matches!(env.message, Message::PluginReady(_))),
            "expected PluginReady message"
        );
        assert!(
            envelopes
                .iter()
                .any(|env| { matches!(env.message, Message::StateChanged { .. }) }),
            "expected StateChanged broadcast"
        );
        assert!(
            envelopes
                .iter()
                .any(|env| matches!(env.message, Message::Trigger { .. })),
            "expected Trigger direct message"
        );
        assert!(
            envelopes.iter().any(|env| match &env.message {
                Message::Custom { kind, .. } => kind == "example.heartbeat",
                _ => false,
            }),
            "expected heartbeat custom message"
        );
    }

    #[test]
    fn config_reloaded_updates_runtime_state() {
        let recorder = Recorder::new();
        let mut plugin = ExamplePlugin::new();

        plugin
            .init(
                r#"{
                    "announce_on_start": false,
                    "enable_periodic_task": false
                }"#,
                recorder.sender(),
            )
            .expect("init should succeed");

        plugin.start().expect("start should succeed");
        plugin
            .handle_message(Message::ConfigReloaded(serde_json::json!({
                "heartbeat_interval_ms": 100,
                "target_plugin": "example-plugin",
                "device_plugin": "device",
                "announce_on_start": false,
                "request_display_info_on_start": false,
                "enable_periodic_task": true,
                "periodic_payload": "reloaded-heartbeat",
                "optional_test_should_fail": true
            })))
            .expect("config reload should succeed");

        assert!(plugin.config.enable_periodic_task);
        assert_eq!(plugin.config.periodic_payload, "reloaded-heartbeat");
        assert!(plugin.fail_optional_test);
        assert!(plugin.worker.is_some());

        plugin.stop().expect("stop should succeed");
    }

    #[test]
    fn start_requests_display_info_when_enabled() {
        let recorder = Recorder::new();
        let mut plugin = ExamplePlugin::new();

        plugin
            .init(
                r#"{
                    "announce_on_start": false,
                    "request_display_info_on_start": true,
                    "device_plugin": "device"
                }"#,
                recorder.sender(),
            )
            .expect("init should succeed");

        plugin.start().expect("start should succeed");
        plugin.stop().expect("stop should succeed");

        let envelopes = recorder.snapshot();
        assert!(
            envelopes.iter().any(|env| {
                matches!(
                    (&env.to, &env.message),
                    (
                        Destination::Plugin(plugin_id),
                        Message::DeviceCommand(DeviceCommand::GetDisplayInfo)
                    ) if plugin_id == "device"
                )
            }),
            "expected GetDisplayInfo request to device plugin"
        );
    }

    #[test]
    fn custom_subscription_update_requires_valid_payload() {
        let recorder = Recorder::new();
        let mut plugin = ExamplePlugin::new();

        plugin
            .init("{}", recorder.sender())
            .expect("init should succeed");

        let error = plugin
            .handle_message(Message::Custom {
                kind: "example.subscription.update".to_string(),
                payload: "not-json".to_string(),
            })
            .expect_err("invalid payload should be rejected");

        assert!(error.contains("expects JSON payload"));
    }
}
