//! Device types and registry — stable aliases for discovered hardware.
//!
//! The LLM always refers to devices by alias (`"pico0"`, `"arduino0"`), never
//! by raw `/dev/` paths. The `DeviceRegistry` assigns these aliases at startup
//! and provides lookup + context building for tool execution.

use super::transport::Transport;
use std::collections::HashMap;
use std::sync::Arc;

// ── DeviceRuntime ─────────────────────────────────────────────────────────────

/// The software runtime / execution environment of a device.
///
/// Determines which host-side tooling is used for code deployment and execution.
/// Currently only [`MicroPython`](DeviceRuntime::MicroPython) is implemented;
/// other variants return a clear "not yet supported" error.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeviceRuntime {
    /// MicroPython — uses `mpremote` for code read/write/exec.
    MicroPython,
    /// CircuitPython — `mpremote`-compatible (future).
    CircuitPython,
    /// Arduino — `arduino-cli` for sketch upload (future).
    Arduino,
    /// STM32 / probe-rs based flashing and debugging (future).
    Nucleus,
    /// Linux / Raspberry Pi — ssh/shell execution (future).
    Linux,
    /// Total Phase Aardvark I2C/SPI/GPIO USB adapter.
    Aardvark,
}

impl DeviceRuntime {
    /// Derive the default runtime from a [`DeviceKind`].
    pub fn from_kind(kind: &DeviceKind) -> Self {
        match kind {
            DeviceKind::Pico | DeviceKind::Esp32 | DeviceKind::Generic => Self::MicroPython,
            DeviceKind::Arduino => Self::Arduino,
            DeviceKind::Nucleo => Self::Nucleus,
            DeviceKind::Aardvark => Self::Aardvark,
        }
    }
}

impl std::fmt::Display for DeviceRuntime {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::MicroPython => write!(f, "MicroPython"),
            Self::CircuitPython => write!(f, "CircuitPython"),
            Self::Arduino => write!(f, "Arduino"),
            Self::Nucleus => write!(f, "Nucleus"),
            Self::Linux => write!(f, "Linux"),
            Self::Aardvark => write!(f, "Aardvark"),
        }
    }
}

// ── DeviceKind ────────────────────────────────────────────────────────────────

/// The category of a discovered hardware device.
///
/// Derived from USB Vendor ID or, for unknown VIDs, from a successful
/// ping handshake (which yields `Generic`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeviceKind {
    /// Raspberry Pi Pico / Pico W (VID `0x2E8A`).
    Pico,
    /// Arduino Uno, Mega, etc. (VID `0x2341`).
    Arduino,
    /// ESP32 via CP2102 bridge (VID `0x10C4`).
    Esp32,
    /// STM32 Nucleo (VID `0x0483`).
    Nucleo,
    /// Unknown VID that passed the KylinBot firmware ping handshake.
    Generic,
    /// Total Phase Aardvark USB adapter (VID `0x2B76`).
    Aardvark,
}

impl DeviceKind {
    /// Derive the device kind from a USB Vendor ID.
    /// Returns `None` if the VID is unknown (0 or unrecognised).
    pub fn from_vid(vid: u16) -> Option<Self> {
        match vid {
            0x2e8a => Some(Self::Pico),
            0x2341 => Some(Self::Arduino),
            0x10c4 => Some(Self::Esp32),
            0x0483 => Some(Self::Nucleo),
            0x2b76 => Some(Self::Aardvark),
            _ => None,
        }
    }
}

impl std::fmt::Display for DeviceKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Pico => write!(f, "pico"),
            Self::Arduino => write!(f, "arduino"),
            Self::Esp32 => write!(f, "esp32"),
            Self::Nucleo => write!(f, "nucleo"),
            Self::Generic => write!(f, "generic"),
            Self::Aardvark => write!(f, "aardvark"),
        }
    }
}

/// Capability flags for a connected device.
///
/// Populated from device handshake or static board metadata.
/// Tools can check capabilities before attempting unsupported operations.
#[derive(Debug, Clone, Default)]
#[allow(clippy::struct_excessive_bools)]
pub struct DeviceCapabilities {
    pub gpio: bool,
    pub i2c: bool,
    pub spi: bool,
    pub swd: bool,
    pub uart: bool,
    pub adc: bool,
    pub pwm: bool,
}

/// A discovered and registered hardware device.
#[derive(Debug, Clone)]
pub struct Device {
    /// Stable session alias (e.g. `"pico0"`, `"arduino0"`, `"nucleo0"`).
    pub alias: String,
    /// Board name from registry (e.g. `"raspberry-pi-pico"`, `"arduino-uno"`).
    pub board_name: String,
    /// Device category derived from VID or ping handshake.
    pub kind: DeviceKind,
    /// Software runtime that determines how code is deployed/executed.
    pub runtime: DeviceRuntime,
    /// USB Vendor ID (if USB-connected).
    pub vid: Option<u16>,
    /// USB Product ID (if USB-connected).
    pub pid: Option<u16>,
    /// Raw device path (e.g. `"/dev/ttyACM0"`) — internal use only.
    /// Tools MUST NOT use this directly; always go through Transport.
    pub device_path: Option<String>,
    /// Architecture description (e.g. `"ARM Cortex-M0+"`).
    pub architecture: Option<String>,
    /// Firmware identifier reported by device during ping handshake.
    pub firmware: Option<String>,
}

impl Device {
    /// Convenience accessor — same as `device_path` (matches the Phase 2 spec naming).
    pub fn port(&self) -> Option<&str> {
        self.device_path.as_deref()
    }
}

/// Context passed to hardware tools during execution.
///
/// Provides the tool with access to the device identity, transport layer,
/// and capability flags without the tool managing connections itself.
pub struct DeviceContext {
    /// The device this tool is operating on.
    pub device: Arc<Device>,
    /// Transport for sending commands to the device.
    pub transport: Arc<dyn Transport>,
    /// Device capabilities (gpio, i2c, spi, etc.).
    pub capabilities: DeviceCapabilities,
}

/// A registered device entry with its transport and capabilities.
struct RegisteredDevice {
    device: Arc<Device>,
    transport: Option<Arc<dyn Transport>>,
    capabilities: DeviceCapabilities,
}

/// Summary string returned by [`DeviceRegistry::prompt_summary`] when no
/// devices are registered.  Exported so callers can compare against it without
/// duplicating the literal.
pub const NO_HW_DEVICES_SUMMARY: &str = "No hardware devices connected.";

/// Registry of discovered devices with stable session aliases.
///
/// - Scans at startup (via `hardware::discover`)
/// - Assigns aliases: `pico0`, `pico1`, `arduino0`, `nucleo0`, `device0`, etc.
/// - Provides alias-based lookup for tool dispatch
/// - Generates prompt summaries for LLM context
pub struct DeviceRegistry {
    devices: HashMap<String, RegisteredDevice>,
    alias_counters: HashMap<String, u32>,
}

impl DeviceRegistry {
    /// Create an empty registry.
    pub fn new() -> Self {
        Self {
            devices: HashMap::new(),
            alias_counters: HashMap::new(),
        }
    }

    /// Register a discovered device and assign a stable alias.
    ///
    /// Returns the assigned alias (e.g. `"pico0"`).
    pub fn register(
        &mut self,
        board_name: &str,
        vid: Option<u16>,
        pid: Option<u16>,
        device_path: Option<String>,
        architecture: Option<String>,
    ) -> String {
        let prefix = alias_prefix(board_name);
        let counter = self.alias_counters.entry(prefix.clone()).or_insert(0);
        let alias = format!("{}{}", prefix, counter);
        *counter += 1;

        let kind = vid
            .and_then(DeviceKind::from_vid)
            .unwrap_or(DeviceKind::Generic);
        let runtime = DeviceRuntime::from_kind(&kind);

        let device = Arc::new(Device {
            alias: alias.clone(),
            board_name: board_name.to_string(),
            kind,
            runtime,
            vid,
            pid,
            device_path,
            architecture,
            firmware: None,
        });

        self.devices.insert(
            alias.clone(),
            RegisteredDevice {
                device,
                transport: None,
                capabilities: DeviceCapabilities::default(),
            },
        );

        alias
    }

    /// Attach a transport and capabilities to a previously registered device.
    ///
    /// Returns `Err` when `alias` is not found in the registry (should not
    /// happen in normal usage because callers pass aliases from `register`).
    pub fn attach_transport(
        &mut self,
        alias: &str,
        transport: Arc<dyn Transport>,
        capabilities: DeviceCapabilities,
    ) -> anyhow::Result<()> {
        if let Some(entry) = self.devices.get_mut(alias) {
            entry.transport = Some(transport);
            entry.capabilities = capabilities;
            Ok(())
        } else {
            Err(anyhow::anyhow!("unknown device alias: {}", alias))
        }
    }

    /// Look up a device by alias.
    pub fn get_device(&self, alias: &str) -> Option<Arc<Device>> {
        self.devices.get(alias).map(|e| e.device.clone())
    }

    /// Build a `DeviceContext` for a device by alias.
    ///
    /// Returns `None` if the alias is unknown or no transport is attached.
    pub fn context(&self, alias: &str) -> Option<DeviceContext> {
        self.devices.get(alias).and_then(|e| {
            e.transport.as_ref().map(|t| DeviceContext {
                device: e.device.clone(),
                transport: t.clone(),
                capabilities: e.capabilities.clone(),
            })
        })
    }

    /// List all registered device aliases.
    pub fn aliases(&self) -> Vec<&str> {
        self.devices.keys().map(|s| s.as_str()).collect()
    }

    /// Return a summary of connected devices for the LLM system prompt.
    pub fn prompt_summary(&self) -> String {
        if self.devices.is_empty() {
            return NO_HW_DEVICES_SUMMARY.to_string();
        }

        let mut lines = vec!["Connected devices:".to_string()];
        let mut sorted_aliases: Vec<&String> = self.devices.keys().collect();
        sorted_aliases.sort();
        for alias in sorted_aliases {
            let entry = &self.devices[alias];
            let status = entry
                .transport
                .as_ref()
                .map(|t| {
                    if t.is_connected() {
                        "connected"
                    } else {
                        "disconnected"
                    }
                })
                .unwrap_or("no transport");
            let arch = entry
                .device
                .architecture
                .as_deref()
                .unwrap_or("unknown arch");
            lines.push(format!(
                "  {} — {} ({}) [{}]",
                alias, entry.device.board_name, arch, status
            ));
        }
        lines.join("\n")
    }

    /// Resolve a GPIO-capable device alias from tool arguments.
    ///
    /// If `args["device"]` is provided, uses that alias directly.
    /// Otherwise, auto-selects the single GPIO-capable device, returning an
    /// error description if zero or multiple GPIO devices are available.
    ///
    /// On success returns `(alias, DeviceContext)` — both are owned / Arc-based
    /// so the caller can drop the registry lock before doing async I/O.
    pub fn resolve_gpio_device(
        &self,
        args: &serde_json::Value,
    ) -> Result<(String, DeviceContext), String> {
        let device_alias: String = match args.get("device").and_then(|v| v.as_str()) {
            Some(a) => a.to_string(),
            None => {
                let gpio_aliases: Vec<String> = self
                    .aliases()
                    .into_iter()
                    .filter(|a| {
                        self.context(a)
                            .map(|c| c.capabilities.gpio)
                            .unwrap_or(false)
                    })
                    .map(|a| a.to_string())
                    .collect();
                match gpio_aliases.as_slice() {
                    [single] => single.clone(),
                    [] => {
                        return Err("no GPIO-capable device found; specify \"device\" parameter"
                            .to_string());
                    }
                    _ => {
                        return Err(format!(
                            "multiple devices available ({}); specify \"device\" parameter",
                            gpio_aliases.join(", ")
                        ));
                    }
                }
            }
        };

        let ctx = self.context(&device_alias).ok_or_else(|| {
            format!(
                "device '{}' not found or has no transport attached",
                device_alias
            )
        })?;

        // Verify the device advertises GPIO capability.
        if !ctx.capabilities.gpio {
            return Err(format!(
                "device '{}' does not support GPIO; specify a GPIO-capable device",
                device_alias
            ));
        }

        Ok((device_alias, ctx))
    }

    /// Return `true` when at least one Aardvark adapter is registered.
    pub fn has_aardvark(&self) -> bool {
        self.devices
            .values()
            .any(|e| e.device.kind == DeviceKind::Aardvark)
    }

    /// Resolve an Aardvark device from tool arguments.
    ///
    /// If `args["device"]` is provided, uses that alias directly.
    /// Otherwise auto-selects the single Aardvark device, returning an error
    /// description if zero or multiple Aardvark devices are available.
    ///
    /// Returns `(alias, DeviceContext)` — both are owned/Arc-based so the
    /// caller can drop the registry lock before doing async I/O.
    pub fn resolve_aardvark_device(
        &self,
        args: &serde_json::Value,
    ) -> Result<(String, DeviceContext), String> {
        let device_alias: String = match args.get("device").and_then(|v| v.as_str()) {
            Some(a) => a.to_string(),
            None => {
                let aardvark_aliases: Vec<String> = self
                    .aliases()
                    .into_iter()
                    .filter(|a| {
                        self.devices
                            .get(*a)
                            .map(|e| e.device.kind == DeviceKind::Aardvark)
                            .unwrap_or(false)
                    })
                    .map(|a| a.to_string())
                    .collect();
                match aardvark_aliases.as_slice() {
                    [single] => single.clone(),
                    [] => {
                        return Err("no Aardvark adapter found; is it plugged in?".to_string());
                    }
                    _ => {
                        return Err(format!(
                            "multiple Aardvark adapters available ({}); \
                             specify \"device\" parameter",
                            aardvark_aliases.join(", ")
                        ));
                    }
                }
            }
        };

        let ctx = self.context(&device_alias).ok_or_else(|| {
            format!("device '{device_alias}' not found or has no transport attached")
        })?;

        Ok((device_alias, ctx))
    }

    /// Number of registered devices.
    pub fn len(&self) -> usize {
        self.devices.len()
    }

    /// Whether the registry is empty.
    pub fn is_empty(&self) -> bool {
        self.devices.is_empty()
    }

    /// Look up a device by alias (alias for `get_device` matching the Phase 2 spec).
    pub fn get(&self, alias: &str) -> Option<Arc<Device>> {
        self.get_device(alias)
    }

    /// Return all registered devices.
    pub fn all(&self) -> Vec<Arc<Device>> {
        self.devices.values().map(|e| e.device.clone()).collect()
    }

    /// One-line summary per device: `"pico0: raspberry-pi-pico /dev/ttyACM0"`.
    ///
    /// Suitable for CLI output and debug logging.
    pub fn summary(&self) -> String {
        if self.devices.is_empty() {
            return String::new();
        }
        let mut lines: Vec<String> = self
            .devices
            .values()
            .map(|e| {
                let path = e.device.port().unwrap_or("(native)");
                format!("{}: {} {}", e.device.alias, e.device.board_name, path)
            })
            .collect();
        lines.sort(); // deterministic for tests
        lines.join("\n")
    }

    /// Discover all connected serial devices and populate the registry.
    ///
    /// Steps:
    /// 1. Call `discover::scan_serial_devices()` to enumerate port paths + VID/PID.
    /// 2. For each device with a recognised VID: register and attach a transport.
    /// 3. For unknown VID (`0`): attempt a 300 ms ping handshake; register only
    ///    if the device responds with KylinBot firmware.
    /// 4. Return the populated registry.
    ///
    /// Returns an empty registry when no devices are found or the `hardware`
    /// feature is disabled.
    #[cfg(feature = "hardware")]
    pub async fn discover() -> Self {
        use super::{
            discover::scan_serial_devices,
            serial::{DEFAULT_BAUD, HardwareSerialTransport},
        };

        let mut registry = Self::new();

        for info in scan_serial_devices() {
            let is_known_vid = info.vid != 0;

            // For unknown VIDs, run the ping handshake before registering.
            // This avoids registering random USB-serial adapters.
            // If the probe succeeds we reuse the same transport instance below.
            let probe_transport = if !is_known_vid {
                let probe = HardwareSerialTransport::new(&info.port_path, DEFAULT_BAUD);
                if !probe.ping_handshake().await {
                    tracing::debug!(
                        port = %info.port_path,
                        "skipping unknown device: no KylinBot firmware response"
                    );
                    continue;
                }
                Some(probe)
            } else {
                None
            };

            let board_name = info.board_name.as_deref().unwrap_or("unknown").to_string();

            let alias = registry.register(
                &board_name,
                if info.vid != 0 { Some(info.vid) } else { None },
                if info.pid != 0 { Some(info.pid) } else { None },
                Some(info.port_path.clone()),
                info.architecture,
            );

            // For unknown-VID devices that passed ping: mark as Generic.
            // (register() will have already set kind = Generic for vid=None)

            let transport: Arc<dyn super::transport::Transport> =
                if let Some(probe) = probe_transport {
                    Arc::new(probe)
                } else {
                    Arc::new(HardwareSerialTransport::new(&info.port_path, DEFAULT_BAUD))
                };
            let caps = DeviceCapabilities {
                gpio: true, // assume GPIO; Phase 3 will populate via capabilities handshake
                ..DeviceCapabilities::default()
            };
            registry.attach_transport(&alias, transport, caps)
                .unwrap_or_else(|e| tracing::warn!(alias = %alias, err = %e, "attach_transport: unexpected unknown alias"));

            tracing::info!(
                alias = %alias,
                port  = %info.port_path,
                vid   = %info.vid,
                "device registered"
            );
        }

        registry
    }
}

impl DeviceRegistry {
    /// Reconnect a device after reboot/reflash.
    ///
    /// Drops the old transport, creates a fresh [`HardwareSerialTransport`] for
    /// the given (or existing) port path, runs the ping handshake to confirm
    /// KylinBot firmware is alive, and re-attaches the transport.
    ///
    /// Pass `new_port` when the OS assigned a different path after reboot;
    /// pass `None` to reuse the device's current path.
    #[cfg(feature = "hardware")]
    pub async fn reconnect(&mut self, alias: &str, new_port: Option<&str>) -> anyhow::Result<()> {
        use super::serial::{DEFAULT_BAUD, HardwareSerialTransport};

        let entry = self
            .devices
            .get_mut(alias)
            .ok_or_else(|| anyhow::anyhow!("unknown device alias: {alias}"))?;

        // Determine the port path — prefer the caller's override.
        let port_path = match new_port {
            Some(p) => {
                // Update the device record with the new path.
                let mut updated = (*entry.device).clone();
                updated.device_path = Some(p.to_string());
                entry.device = Arc::new(updated);
                p.to_string()
            }
            None => entry
                .device
                .device_path
                .clone()
                .ok_or_else(|| anyhow::anyhow!("device {alias} has no port path"))?,
        };

        // Drop the stale transport.
        entry.transport = None;

        // Create a fresh transport and verify firmware is alive.
        let transport = HardwareSerialTransport::new(&port_path, DEFAULT_BAUD);
        if !transport.ping_handshake().await {
            anyhow::bail!(
                "ping handshake failed after reconnect on {port_path} — \
                 firmware may not be running"
            );
        }

        entry.transport = Some(Arc::new(transport) as Arc<dyn super::transport::Transport>);
        entry.capabilities.gpio = true;

        tracing::info!(alias = %alias, port = %port_path, "device reconnected");
        Ok(())
    }
}

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

/// Derive alias prefix from board name.
fn alias_prefix(board_name: &str) -> String {
    match board_name {
        s if s.starts_with("raspberry-pi-pico") || s.starts_with("pico") => "pico".to_string(),
        s if s.starts_with("arduino") => "arduino".to_string(),
        s if s.starts_with("esp32") || s.starts_with("esp") => "esp".to_string(),
        s if s.starts_with("nucleo") || s.starts_with("stm32") => "nucleo".to_string(),
        s if s.starts_with("rpi") || s == "raspberry-pi" => "rpi".to_string(),
        _ => "device".to_string(),
    }
}

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

    #[test]
    fn alias_prefix_pico_variants() {
        assert_eq!(alias_prefix("raspberry-pi-pico"), "pico");
        assert_eq!(alias_prefix("pico-w"), "pico");
        assert_eq!(alias_prefix("pico"), "pico");
    }

    #[test]
    fn alias_prefix_arduino() {
        assert_eq!(alias_prefix("arduino-uno"), "arduino");
        assert_eq!(alias_prefix("arduino-mega"), "arduino");
    }

    #[test]
    fn alias_prefix_esp() {
        assert_eq!(alias_prefix("esp32"), "esp");
        assert_eq!(alias_prefix("esp32-s3"), "esp");
    }

    #[test]
    fn alias_prefix_nucleo() {
        assert_eq!(alias_prefix("nucleo-f401re"), "nucleo");
        assert_eq!(alias_prefix("stm32-discovery"), "nucleo");
    }

    #[test]
    fn alias_prefix_rpi() {
        assert_eq!(alias_prefix("rpi-gpio"), "rpi");
        assert_eq!(alias_prefix("raspberry-pi"), "rpi");
    }

    #[test]
    fn alias_prefix_unknown() {
        assert_eq!(alias_prefix("custom-board"), "device");
    }

    #[test]
    fn registry_assigns_sequential_aliases() {
        let mut reg = DeviceRegistry::new();
        let a1 = reg.register("raspberry-pi-pico", Some(0x2E8A), Some(0x000A), None, None);
        let a2 = reg.register("raspberry-pi-pico", Some(0x2E8A), Some(0x000A), None, None);
        let a3 = reg.register("arduino-uno", Some(0x2341), Some(0x0043), None, None);

        assert_eq!(a1, "pico0");
        assert_eq!(a2, "pico1");
        assert_eq!(a3, "arduino0");
        assert_eq!(reg.len(), 3);
    }

    #[test]
    fn registry_get_device_by_alias() {
        let mut reg = DeviceRegistry::new();
        let alias = reg.register(
            "nucleo-f401re",
            Some(0x0483),
            Some(0x374B),
            Some("/dev/ttyACM0".to_string()),
            Some("ARM Cortex-M4".to_string()),
        );

        let device = reg.get_device(&alias).unwrap();
        assert_eq!(device.alias, "nucleo0");
        assert_eq!(device.board_name, "nucleo-f401re");
        assert_eq!(device.vid, Some(0x0483));
        assert_eq!(device.architecture.as_deref(), Some("ARM Cortex-M4"));
    }

    #[test]
    fn registry_unknown_alias_returns_none() {
        let reg = DeviceRegistry::new();
        assert!(reg.get_device("nonexistent").is_none());
        assert!(reg.context("nonexistent").is_none());
    }

    #[test]
    fn registry_context_none_without_transport() {
        let mut reg = DeviceRegistry::new();
        let alias = reg.register("pico", None, None, None, None);
        // No transport attached → context returns None.
        assert!(reg.context(&alias).is_none());
    }

    #[test]
    fn registry_prompt_summary_empty() {
        let reg = DeviceRegistry::new();
        assert_eq!(reg.prompt_summary(), NO_HW_DEVICES_SUMMARY);
    }

    #[test]
    fn registry_prompt_summary_with_devices() {
        let mut reg = DeviceRegistry::new();
        reg.register(
            "raspberry-pi-pico",
            Some(0x2E8A),
            None,
            None,
            Some("ARM Cortex-M0+".to_string()),
        );
        let summary = reg.prompt_summary();
        assert!(summary.contains("pico0"));
        assert!(summary.contains("raspberry-pi-pico"));
        assert!(summary.contains("ARM Cortex-M0+"));
        assert!(summary.contains("no transport"));
    }

    #[test]
    fn device_capabilities_default_all_false() {
        let caps = DeviceCapabilities::default();
        assert!(!caps.gpio);
        assert!(!caps.i2c);
        assert!(!caps.spi);
        assert!(!caps.swd);
        assert!(!caps.uart);
        assert!(!caps.adc);
        assert!(!caps.pwm);
    }

    #[test]
    fn registry_default_is_empty() {
        let reg = DeviceRegistry::default();
        assert!(reg.is_empty());
        assert_eq!(reg.len(), 0);
    }

    #[test]
    fn registry_aliases_returns_all() {
        let mut reg = DeviceRegistry::new();
        reg.register("pico", None, None, None, None);
        reg.register("arduino-uno", None, None, None, None);
        let mut aliases = reg.aliases();
        aliases.sort_unstable();
        assert_eq!(aliases, vec!["arduino0", "pico0"]);
    }

    // ── Phase 2 new tests ────────────────────────────────────────────────────

    #[test]
    fn device_kind_from_vid_known() {
        assert_eq!(DeviceKind::from_vid(0x2e8a), Some(DeviceKind::Pico));
        assert_eq!(DeviceKind::from_vid(0x2341), Some(DeviceKind::Arduino));
        assert_eq!(DeviceKind::from_vid(0x10c4), Some(DeviceKind::Esp32));
        assert_eq!(DeviceKind::from_vid(0x0483), Some(DeviceKind::Nucleo));
    }

    #[test]
    fn device_kind_from_vid_unknown() {
        assert_eq!(DeviceKind::from_vid(0x0000), None);
        assert_eq!(DeviceKind::from_vid(0xffff), None);
    }

    #[test]
    fn device_kind_display() {
        assert_eq!(DeviceKind::Pico.to_string(), "pico");
        assert_eq!(DeviceKind::Arduino.to_string(), "arduino");
        assert_eq!(DeviceKind::Esp32.to_string(), "esp32");
        assert_eq!(DeviceKind::Nucleo.to_string(), "nucleo");
        assert_eq!(DeviceKind::Generic.to_string(), "generic");
    }

    #[test]
    fn register_sets_kind_from_vid() {
        let mut reg = DeviceRegistry::new();
        let a = reg.register("raspberry-pi-pico", Some(0x2e8a), Some(0x000a), None, None);
        assert_eq!(reg.get(&a).unwrap().kind, DeviceKind::Pico);

        let b = reg.register("arduino-uno", Some(0x2341), Some(0x0043), None, None);
        assert_eq!(reg.get(&b).unwrap().kind, DeviceKind::Arduino);

        let c = reg.register("unknown-device", None, None, None, None);
        assert_eq!(reg.get(&c).unwrap().kind, DeviceKind::Generic);
    }

    #[test]
    fn device_port_returns_device_path() {
        let mut reg = DeviceRegistry::new();
        let alias = reg.register(
            "raspberry-pi-pico",
            Some(0x2e8a),
            None,
            Some("/dev/ttyACM0".to_string()),
            None,
        );
        let device = reg.get(&alias).unwrap();
        assert_eq!(device.port(), Some("/dev/ttyACM0"));
    }

    #[test]
    fn device_port_none_without_path() {
        let mut reg = DeviceRegistry::new();
        let alias = reg.register("pico", None, None, None, None);
        assert!(reg.get(&alias).unwrap().port().is_none());
    }

    #[test]
    fn registry_get_is_alias_for_get_device() {
        let mut reg = DeviceRegistry::new();
        let alias = reg.register("raspberry-pi-pico", Some(0x2e8a), None, None, None);
        let via_get = reg.get(&alias);
        let via_get_device = reg.get_device(&alias);
        assert!(via_get.is_some());
        assert!(via_get_device.is_some());
        assert_eq!(via_get.unwrap().alias, via_get_device.unwrap().alias);
    }

    #[test]
    fn registry_all_returns_every_device() {
        let mut reg = DeviceRegistry::new();
        reg.register("raspberry-pi-pico", Some(0x2e8a), None, None, None);
        reg.register("arduino-uno", Some(0x2341), None, None, None);
        assert_eq!(reg.all().len(), 2);
    }

    #[test]
    fn registry_summary_one_liner_per_device() {
        let mut reg = DeviceRegistry::new();
        reg.register(
            "raspberry-pi-pico",
            Some(0x2e8a),
            None,
            Some("/dev/ttyACM0".to_string()),
            None,
        );
        let s = reg.summary();
        assert!(s.contains("pico0"));
        assert!(s.contains("raspberry-pi-pico"));
        assert!(s.contains("/dev/ttyACM0"));
    }

    #[test]
    fn registry_summary_empty_when_no_devices() {
        let reg = DeviceRegistry::new();
        assert_eq!(reg.summary(), "");
    }
}
