/// Source of an NDAttribute value.
#[derive(Debug, Clone, PartialEq)]
pub enum NDAttrSource {
    Driver,
    Param {
        port_name: String,
        param_name: String,
    },
    EpicsPV,
    Function,
    Constant,
    Undefined,
}

/// Data type tags for NDAttribute values.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NDAttrDataType {
    Int8,
    UInt8,
    Int16,
    UInt16,
    Int32,
    UInt32,
    Int64,
    UInt64,
    Float32,
    Float64,
    String,
}

/// Typed value stored in an NDAttribute.
#[derive(Debug, Clone, PartialEq)]
pub enum NDAttrValue {
    Int8(i8),
    UInt8(u8),
    Int16(i16),
    UInt16(u16),
    Int32(i32),
    UInt32(u32),
    Int64(i64),
    UInt64(u64),
    Float32(f32),
    Float64(f64),
    String(String),
    Undefined,
}

impl NDAttrValue {
    pub fn data_type(&self) -> NDAttrDataType {
        match self {
            Self::Int8(_) => NDAttrDataType::Int8,
            Self::UInt8(_) => NDAttrDataType::UInt8,
            Self::Int16(_) => NDAttrDataType::Int16,
            Self::UInt16(_) => NDAttrDataType::UInt16,
            Self::Int32(_) => NDAttrDataType::Int32,
            Self::UInt32(_) => NDAttrDataType::UInt32,
            Self::Int64(_) => NDAttrDataType::Int64,
            Self::UInt64(_) => NDAttrDataType::UInt64,
            Self::Float32(_) => NDAttrDataType::Float32,
            Self::Float64(_) => NDAttrDataType::Float64,
            Self::String(_) => NDAttrDataType::String,
            Self::Undefined => NDAttrDataType::Int32,
        }
    }

    pub fn as_f64(&self) -> Option<f64> {
        match self {
            Self::Int8(v) => Some(*v as f64),
            Self::UInt8(v) => Some(*v as f64),
            Self::Int16(v) => Some(*v as f64),
            Self::UInt16(v) => Some(*v as f64),
            Self::Int32(v) => Some(*v as f64),
            Self::UInt32(v) => Some(*v as f64),
            Self::Int64(v) => Some(*v as f64),
            Self::UInt64(v) => Some(*v as f64),
            Self::Float32(v) => Some(*v as f64),
            Self::Float64(v) => Some(*v),
            Self::String(_) | Self::Undefined => None,
        }
    }

    pub fn as_i64(&self) -> Option<i64> {
        match self {
            Self::Int8(v) => Some(*v as i64),
            Self::UInt8(v) => Some(*v as i64),
            Self::Int16(v) => Some(*v as i64),
            Self::UInt16(v) => Some(*v as i64),
            Self::Int32(v) => Some(*v as i64),
            Self::UInt32(v) => Some(*v as i64),
            Self::Int64(v) => Some(*v),
            Self::UInt64(v) => Some(*v as i64),
            Self::Float32(v) => Some(*v as i64),
            Self::Float64(v) => Some(*v as i64),
            Self::String(_) | Self::Undefined => None,
        }
    }

    pub fn as_string(&self) -> String {
        match self {
            Self::Int8(v) => v.to_string(),
            Self::UInt8(v) => v.to_string(),
            Self::Int16(v) => v.to_string(),
            Self::UInt16(v) => v.to_string(),
            Self::Int32(v) => v.to_string(),
            Self::UInt32(v) => v.to_string(),
            Self::Int64(v) => v.to_string(),
            Self::UInt64(v) => v.to_string(),
            Self::Float32(v) => v.to_string(),
            Self::Float64(v) => v.to_string(),
            Self::String(v) => v.clone(),
            Self::Undefined => String::new(),
        }
    }
}

/// A live source backing an [`NDAttribute`].
///
/// C++ models live attributes with `paramAttribute`, `PVAttribute` and
/// `functAttribute` subclasses, each overriding `updateValue()` to re-read
/// from a driver parameter, an EPICS PV, or a user function. The Rust port
/// keeps `NDAttribute` a single concrete type and delegates re-evaluation to
/// an implementation of this trait. `Constant` / `Driver` attributes have no
/// source object — their value is static (C++ `NDAttribute::updateValue` is a
/// no-op for those).
pub trait NDAttributeSource: Send + Sync {
    /// Re-evaluate the attribute from its underlying source.
    fn evaluate(&self) -> NDAttrValue;

    /// Downcast hook so the driver can recognize a `ParamAttributeSource` (or
    /// `EpicsPvAttributeSource`) and feed its [`LiveValueCell`].
    fn as_any(&self) -> &dyn std::any::Any;
}

/// A thread-safe value cell shared between an attribute source and the agent
/// that feeds it fresh values (the driver for `Param`, a CA-monitor task for
/// `EpicsPV`). The feeder calls [`LiveValueCell::set`]; the attribute source
/// reads via [`LiveValueCell::get`].
#[derive(Debug)]
pub struct LiveValueCell {
    value: std::sync::RwLock<NDAttrValue>,
}

impl LiveValueCell {
    /// Create a cell with an initial (typically `Undefined`) value.
    pub fn new(initial: NDAttrValue) -> std::sync::Arc<Self> {
        std::sync::Arc::new(Self {
            value: std::sync::RwLock::new(initial),
        })
    }

    /// Store a fresh value (called by the feeder — driver / CA-monitor task).
    pub fn set(&self, value: NDAttrValue) {
        if let Ok(mut guard) = self.value.write() {
            *guard = value;
        }
    }

    /// Read the current value.
    pub fn get(&self) -> NDAttrValue {
        self.value
            .read()
            .map(|g| g.clone())
            .unwrap_or(NDAttrValue::Undefined)
    }
}

impl NDAttributeSource for LiveValueCell {
    fn evaluate(&self) -> NDAttrValue {
        self.get()
    }
    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}

/// Live source for a `Param`-type attribute (C++ `paramAttribute`).
///
/// The driver owns the asyn parameter library; on each
/// `NDAttributeList::update_values()` cycle the driver refreshes this source's
/// [`LiveValueCell`] with the current value of the parameter named
/// `param_name`. `evaluate()` then returns that fresh value — mirroring C++
/// `paramAttribute::updateValue()`, which reads `pDriver->getXxxParam()`.
#[derive(Debug)]
pub struct ParamAttributeSource {
    /// The asyn parameter name this attribute reads (the XML `source`).
    pub param_name: String,
    /// The address (asyn `paramAddr`) to read from.
    pub addr: i32,
    cell: std::sync::Arc<LiveValueCell>,
}

impl ParamAttributeSource {
    pub fn new(param_name: impl Into<String>, addr: i32) -> std::sync::Arc<Self> {
        std::sync::Arc::new(Self {
            param_name: param_name.into(),
            addr,
            cell: LiveValueCell::new(NDAttrValue::Undefined),
        })
    }

    /// The shared cell the driver writes fresh parameter values into.
    pub fn cell(&self) -> &std::sync::Arc<LiveValueCell> {
        &self.cell
    }
}

impl NDAttributeSource for ParamAttributeSource {
    fn evaluate(&self) -> NDAttrValue {
        self.cell.get()
    }
    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}

/// A user attribute function: receives the `functParam` string and returns the
/// attribute's current value. Port of C++ `NDAttributeFunction`
/// (`functAttribute` calls `pFunction(functParam, &functionPvt, this)`).
pub type NDAttributeFunction = std::sync::Arc<dyn Fn(&str) -> NDAttrValue + Send + Sync>;

/// Registry of named attribute functions (C++ `registryFunctionFind` /
/// `registerNDAttributeFunction`).
///
/// A `Function`-type attribute names a function here; `FunctionAttributeSource`
/// looks it up and calls it on every `evaluate()`.
#[derive(Default)]
pub struct NDAttributeFunctionRegistry {
    funcs: std::sync::RwLock<std::collections::HashMap<String, NDAttributeFunction>>,
}

impl NDAttributeFunctionRegistry {
    pub fn new() -> std::sync::Arc<Self> {
        std::sync::Arc::new(Self::default())
    }

    /// Register an attribute function under `name` (C++
    /// `registerNDAttributeFunction`).
    pub fn register(
        &self,
        name: impl Into<String>,
        func: impl Fn(&str) -> NDAttrValue + Send + Sync + 'static,
    ) {
        if let Ok(mut g) = self.funcs.write() {
            g.insert(name.into(), std::sync::Arc::new(func));
        }
    }

    /// Look up a registered function by name (C++ `registryFunctionFind`).
    pub fn find(&self, name: &str) -> Option<NDAttributeFunction> {
        self.funcs.read().ok()?.get(name).cloned()
    }
}

/// Live source for a `Function`-type attribute (C++ `functAttribute`).
///
/// Holds a reference to the function registry, the function name (XML
/// `source`) and the function parameter string (XML `param`). `evaluate()`
/// looks the function up and calls it — matching `functAttribute::updateValue`.
/// If the function is not registered, `evaluate()` returns `Undefined`
/// (C++ `updateValue` returns `asynError` and leaves the value unchanged).
pub struct FunctionAttributeSource {
    registry: std::sync::Arc<NDAttributeFunctionRegistry>,
    function_name: String,
    function_param: String,
}

impl FunctionAttributeSource {
    pub fn new(
        registry: std::sync::Arc<NDAttributeFunctionRegistry>,
        function_name: impl Into<String>,
        function_param: impl Into<String>,
    ) -> std::sync::Arc<Self> {
        std::sync::Arc::new(Self {
            registry,
            function_name: function_name.into(),
            function_param: function_param.into(),
        })
    }
}

impl NDAttributeSource for FunctionAttributeSource {
    fn evaluate(&self) -> NDAttrValue {
        match self.registry.find(&self.function_name) {
            Some(f) => f(&self.function_param),
            None => NDAttrValue::Undefined,
        }
    }
    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}

/// Live source for an `EPICS_PV`-type attribute (C++ `PVAttribute`).
///
/// A CA-monitor task feeds fresh values into the shared [`LiveValueCell`];
/// `evaluate()` returns the latest monitored value. This struct is the
/// pluggable backend — the CA-monitor feeder task that drives the cell is
/// provided separately (it requires a live `epics-ca-rs` CA client).
#[derive(Debug)]
pub struct EpicsPvAttributeSource {
    /// The PV name this attribute monitors (the XML `source`).
    pub pv_name: String,
    cell: std::sync::Arc<LiveValueCell>,
}

impl EpicsPvAttributeSource {
    pub fn new(pv_name: impl Into<String>) -> std::sync::Arc<Self> {
        std::sync::Arc::new(Self {
            pv_name: pv_name.into(),
            cell: LiveValueCell::new(NDAttrValue::Undefined),
        })
    }

    /// The shared cell a CA-monitor task writes fresh PV values into.
    pub fn cell(&self) -> &std::sync::Arc<LiveValueCell> {
        &self.cell
    }
}

impl NDAttributeSource for EpicsPvAttributeSource {
    fn evaluate(&self) -> NDAttrValue {
        self.cell.get()
    }
    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}

/// CA-monitor feeder for `EPICS_PV`-type attributes (C++ `PVAttribute`'s
/// channel-access monitor callback).
///
/// Requires the `ioc` feature (the `epics-ca-rs` CA client). Spawns a tokio
/// task that subscribes to the attribute's PV via [`epics_ca_rs::client::CaClient::camonitor`]
/// and writes every monitored value into the source's [`LiveValueCell`], so
/// `evaluate()` returns the latest value. The task exits when the monitor
/// stream ends; the returned [`tokio::task::JoinHandle`] lets the caller abort
/// it on shutdown.
#[cfg(feature = "ioc")]
pub fn spawn_ca_monitor(
    client: std::sync::Arc<epics_ca_rs::client::CaClient>,
    source: std::sync::Arc<EpicsPvAttributeSource>,
) -> tokio::task::JoinHandle<()> {
    let pv_name = source.pv_name.clone();
    let cell = source.cell().clone();
    tokio::spawn(async move {
        let _ = client
            .camonitor(&pv_name, |value| {
                cell.set(epics_value_to_nd_attr(&value));
            })
            .await;
    })
}

/// Convert an `epics-ca-rs` [`epics_ca_rs::EpicsValue`] scalar to an
/// [`NDAttrValue`]. Array values collapse to their first element (an NDArray
/// attribute is scalar); empty arrays yield `Undefined`.
#[cfg(feature = "ioc")]
fn epics_value_to_nd_attr(value: &epics_ca_rs::EpicsValue) -> NDAttrValue {
    use epics_ca_rs::EpicsValue as E;
    match value {
        E::String(s) => NDAttrValue::String(s.clone()),
        E::Short(v) => NDAttrValue::Int16(*v),
        E::Float(v) => NDAttrValue::Float32(*v),
        E::Enum(v) => NDAttrValue::UInt16(*v),
        E::Char(v) => NDAttrValue::UInt8(*v),
        E::Long(v) => NDAttrValue::Int32(*v),
        E::Double(v) => NDAttrValue::Float64(*v),
        E::Int64(v) => NDAttrValue::Int64(*v),
        E::UInt64(v) => NDAttrValue::UInt64(*v),
        E::ShortArray(a) => a
            .first()
            .map_or(NDAttrValue::Undefined, |v| NDAttrValue::Int16(*v)),
        E::FloatArray(a) => a
            .first()
            .map_or(NDAttrValue::Undefined, |v| NDAttrValue::Float32(*v)),
        E::EnumArray(a) => a
            .first()
            .map_or(NDAttrValue::Undefined, |v| NDAttrValue::UInt16(*v)),
        E::DoubleArray(a) => a
            .first()
            .map_or(NDAttrValue::Undefined, |v| NDAttrValue::Float64(*v)),
        E::LongArray(a) => a
            .first()
            .map_or(NDAttrValue::Undefined, |v| NDAttrValue::Int32(*v)),
        E::CharArray(a) => a
            .first()
            .map_or(NDAttrValue::Undefined, |v| NDAttrValue::UInt8(*v)),
        E::Int64Array(a) => a
            .first()
            .map_or(NDAttrValue::Undefined, |v| NDAttrValue::Int64(*v)),
        E::UInt64Array(a) => a
            .first()
            .map_or(NDAttrValue::Undefined, |v| NDAttrValue::UInt64(*v)),
        E::StringArray(a) => a
            .first()
            .map_or(NDAttrValue::Undefined, |v| NDAttrValue::String(v.clone())),
    }
}

/// A named attribute attached to an NDArray.
#[derive(Clone)]
pub struct NDAttribute {
    pub name: String,
    pub description: String,
    pub source: NDAttrSource,
    pub value: NDAttrValue,
    /// Optional live source. When present, [`NDAttribute::update`] re-reads the
    /// value from it; when absent the attribute is a static value (Constant /
    /// Driver), and `update` is a no-op — matching C++ behavior.
    pub source_impl: Option<std::sync::Arc<dyn NDAttributeSource>>,
}

impl std::fmt::Debug for NDAttribute {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("NDAttribute")
            .field("name", &self.name)
            .field("description", &self.description)
            .field("source", &self.source)
            .field("value", &self.value)
            .field("has_source_impl", &self.source_impl.is_some())
            .finish()
    }
}

impl NDAttribute {
    /// Construct a static-valued attribute (no live source).
    pub fn new_static(
        name: impl Into<String>,
        description: impl Into<String>,
        source: NDAttrSource,
        value: NDAttrValue,
    ) -> Self {
        Self {
            name: name.into(),
            description: description.into(),
            source,
            value,
            source_impl: None,
        }
    }

    /// Construct a source-backed attribute. The value is evaluated immediately.
    pub fn new_with_source(
        name: impl Into<String>,
        description: impl Into<String>,
        source: NDAttrSource,
        source_impl: std::sync::Arc<dyn NDAttributeSource>,
    ) -> Self {
        let value = source_impl.evaluate();
        Self {
            name: name.into(),
            description: description.into(),
            source,
            value,
            source_impl: Some(source_impl),
        }
    }

    /// Re-evaluate this attribute from its live source.
    ///
    /// Mirrors C++ `NDAttribute::updateValue()` — a no-op for attributes
    /// without a live source (Constant / Driver), and a re-read for
    /// source-backed attributes (Param / EpicsPV / Function).
    pub fn update(&mut self) {
        if let Some(src) = &self.source_impl {
            self.value = src.evaluate();
        }
    }

    /// If this attribute is backed by a [`ParamAttributeSource`], return it so
    /// the driver can feed its [`LiveValueCell`] from the parameter library.
    pub fn param_source(&self) -> Option<&ParamAttributeSource> {
        self.source_impl
            .as_ref()
            .and_then(|s| s.as_any().downcast_ref::<ParamAttributeSource>())
    }

    /// If this attribute is backed by an [`EpicsPvAttributeSource`], return it
    /// so a CA-monitor task can feed its [`LiveValueCell`].
    pub fn epics_pv_source(&self) -> Option<&EpicsPvAttributeSource> {
        self.source_impl
            .as_ref()
            .and_then(|s| s.as_any().downcast_ref::<EpicsPvAttributeSource>())
    }
}

/// Collection of NDAttributes on an NDArray.
#[derive(Debug, Clone, Default)]
pub struct NDAttributeList {
    attrs: Vec<NDAttribute>,
}

impl NDAttributeList {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn add(&mut self, attr: NDAttribute) {
        if let Some(existing) = self.attrs.iter_mut().find(|a| a.name == attr.name) {
            *existing = attr;
        } else {
            self.attrs.push(attr);
        }
    }

    pub fn get(&self, name: &str) -> Option<&NDAttribute> {
        self.attrs.iter().find(|a| a.name == name)
    }

    pub fn remove(&mut self, name: &str) -> bool {
        let len_before = self.attrs.len();
        self.attrs.retain(|a| a.name != name);
        self.attrs.len() < len_before
    }

    pub fn clear(&mut self) {
        self.attrs.clear();
    }

    pub fn iter(&self) -> impl Iterator<Item = &NDAttribute> {
        self.attrs.iter()
    }

    pub fn len(&self) -> usize {
        self.attrs.len()
    }

    pub fn is_empty(&self) -> bool {
        self.attrs.is_empty()
    }

    /// Merge attributes from another list: adds new ones, updates existing by name.
    pub fn copy_from(&mut self, other: &NDAttributeList) {
        for attr in other.iter() {
            self.add(attr.clone());
        }
    }

    /// Re-evaluate every attribute from its live source.
    ///
    /// Mirrors C++ `NDAttributeList::updateValues()`, which is called before
    /// the list is copied onto an outgoing NDArray so each attribute carries a
    /// fresh value. Static (Constant / Driver) attributes are unaffected.
    pub fn update_values(&mut self) {
        for attr in self.attrs.iter_mut() {
            attr.update();
        }
    }
}

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

    #[test]
    fn test_add_get() {
        let mut list = NDAttributeList::new();
        list.add(NDAttribute {
            name: "ColorMode".into(),
            description: "Color mode".into(),
            source: NDAttrSource::Driver,
            value: NDAttrValue::Int32(0),
            source_impl: None,
        });
        let attr = list.get("ColorMode").unwrap();
        assert_eq!(attr.value, NDAttrValue::Int32(0));
    }

    #[test]
    fn test_replace_existing() {
        let mut list = NDAttributeList::new();
        list.add(NDAttribute {
            name: "Gain".into(),
            description: "".into(),
            source: NDAttrSource::Driver,
            value: NDAttrValue::Float64(1.0),
            source_impl: None,
        });
        list.add(NDAttribute {
            name: "Gain".into(),
            description: "".into(),
            source: NDAttrSource::Driver,
            value: NDAttrValue::Float64(2.5),
            source_impl: None,
        });
        assert_eq!(list.len(), 1);
        assert_eq!(list.get("Gain").unwrap().value, NDAttrValue::Float64(2.5));
    }

    #[test]
    fn test_iter() {
        let mut list = NDAttributeList::new();
        list.add(NDAttribute {
            name: "A".into(),
            description: "".into(),
            source: NDAttrSource::Constant,
            value: NDAttrValue::Int32(1),
            source_impl: None,
        });
        list.add(NDAttribute {
            name: "B".into(),
            description: "".into(),
            source: NDAttrSource::Constant,
            value: NDAttrValue::String("hello".into()),
            source_impl: None,
        });
        let names: Vec<_> = list.iter().map(|a| a.name.as_str()).collect();
        assert_eq!(names, vec!["A", "B"]);
    }

    #[test]
    fn test_get_missing() {
        let list = NDAttributeList::new();
        assert!(list.get("nope").is_none());
    }

    #[test]
    fn test_empty() {
        let list = NDAttributeList::new();
        assert!(list.is_empty());
        assert_eq!(list.len(), 0);
    }

    #[test]
    fn test_all_data_types() {
        let values = vec![
            NDAttrValue::Int8(-1),
            NDAttrValue::UInt8(255),
            NDAttrValue::Int16(-100),
            NDAttrValue::UInt16(1000),
            NDAttrValue::Int32(-50000),
            NDAttrValue::UInt32(50000),
            NDAttrValue::Int64(-1_000_000),
            NDAttrValue::UInt64(1_000_000),
            NDAttrValue::Float32(3.14),
            NDAttrValue::Float64(2.718),
            NDAttrValue::String("test".into()),
        ];
        for v in &values {
            assert_eq!(v.data_type(), v.data_type());
        }
    }

    #[test]
    fn test_source_tracking() {
        let attr = NDAttribute {
            name: "temp".into(),
            description: "temperature".into(),
            source: NDAttrSource::Param {
                port_name: "SIM1".into(),
                param_name: "TEMPERATURE".into(),
            },
            value: NDAttrValue::Float64(25.0),
            source_impl: None,
        };
        match &attr.source {
            NDAttrSource::Param {
                port_name,
                param_name,
            } => {
                assert_eq!(port_name, "SIM1");
                assert_eq!(param_name, "TEMPERATURE");
            }
            _ => panic!("wrong source type"),
        }
    }

    #[test]
    fn test_value_conversions() {
        let v = NDAttrValue::Int32(42);
        assert_eq!(v.as_f64(), Some(42.0));
        assert_eq!(v.as_i64(), Some(42));
        assert_eq!(v.as_string(), "42");

        let s = NDAttrValue::String("hello".into());
        assert_eq!(s.as_f64(), None);
        assert_eq!(s.as_i64(), None);
        assert_eq!(s.as_string(), "hello");
    }

    #[test]
    fn test_remove() {
        let mut list = NDAttributeList::new();
        list.add(NDAttribute {
            name: "A".into(),
            description: "".into(),
            source: NDAttrSource::Driver,
            value: NDAttrValue::Int32(1),
            source_impl: None,
        });
        assert!(list.remove("A"));
        assert!(list.is_empty());
        assert!(!list.remove("A"));
    }

    /// G10: a source with a live `NDAttributeSource` is re-evaluated by update().
    struct CountingSource {
        counter: std::sync::atomic::AtomicI32,
    }
    impl NDAttributeSource for CountingSource {
        fn evaluate(&self) -> NDAttrValue {
            let n = self
                .counter
                .fetch_add(1, std::sync::atomic::Ordering::SeqCst)
                + 1;
            NDAttrValue::Int32(n)
        }
        fn as_any(&self) -> &dyn std::any::Any {
            self
        }
    }

    #[test]
    fn test_attribute_update_reevaluates_source() {
        let src = std::sync::Arc::new(CountingSource {
            counter: std::sync::atomic::AtomicI32::new(0),
        });
        let mut attr =
            NDAttribute::new_with_source("live", "live source", NDAttrSource::Function, src);
        // Constructed value is the first evaluation.
        assert_eq!(attr.value, NDAttrValue::Int32(1));
        attr.update();
        assert_eq!(attr.value, NDAttrValue::Int32(2));
        attr.update();
        assert_eq!(attr.value, NDAttrValue::Int32(3));
    }

    #[test]
    fn test_static_attribute_update_is_noop() {
        // G10: static (Constant / Driver) attributes are not re-evaluated.
        let mut attr =
            NDAttribute::new_static("static", "", NDAttrSource::Constant, NDAttrValue::Int32(42));
        attr.update();
        assert_eq!(attr.value, NDAttrValue::Int32(42));
    }

    #[test]
    fn test_param_source_cell_reevaluates() {
        // G10: a ParamAttributeSource returns whatever the driver last fed
        // into its cell — update() re-reads the fresh value.
        let src = ParamAttributeSource::new("GAIN", 0);
        let mut attr = NDAttribute::new_with_source(
            "Gain",
            "",
            NDAttrSource::Param {
                port_name: String::new(),
                param_name: "GAIN".into(),
            },
            src.clone(),
        );
        assert_eq!(attr.value, NDAttrValue::Undefined);
        src.cell().set(NDAttrValue::Float64(3.5));
        attr.update();
        assert_eq!(attr.value, NDAttrValue::Float64(3.5));
        // param_source() downcast exposes the backend to the driver.
        assert_eq!(attr.param_source().unwrap().param_name, "GAIN");
    }

    #[test]
    fn test_function_source_via_registry() {
        // G10: a FunctionAttributeSource calls the named registered function.
        let registry = NDAttributeFunctionRegistry::new();
        registry.register("double", |p: &str| {
            NDAttrValue::Int32(p.parse::<i32>().unwrap_or(0) * 2)
        });
        let src = FunctionAttributeSource::new(registry.clone(), "double", "21");
        let mut attr = NDAttribute::new_with_source("Doubled", "", NDAttrSource::Function, src);
        assert_eq!(attr.value, NDAttrValue::Int32(42));
        attr.update();
        assert_eq!(attr.value, NDAttrValue::Int32(42));

        // An unregistered function evaluates to Undefined.
        let missing = FunctionAttributeSource::new(registry, "no_such", "");
        assert_eq!(missing.evaluate(), NDAttrValue::Undefined);
    }

    #[test]
    fn test_epics_pv_source_cell_feeding() {
        // G10: an EpicsPvAttributeSource returns whatever a CA-monitor task
        // last fed into its cell.
        let src = EpicsPvAttributeSource::new("TST:Temp");
        assert_eq!(src.evaluate(), NDAttrValue::Undefined);
        src.cell().set(NDAttrValue::Float64(22.4));
        assert_eq!(src.evaluate(), NDAttrValue::Float64(22.4));
    }

    #[test]
    fn test_attribute_list_update_values() {
        // G10: NDAttributeList::update_values re-evaluates every live source.
        let mut list = NDAttributeList::new();
        list.add(NDAttribute::new_with_source(
            "live",
            "",
            NDAttrSource::Function,
            std::sync::Arc::new(CountingSource {
                counter: std::sync::atomic::AtomicI32::new(0),
            }),
        ));
        list.add(NDAttribute::new_static(
            "frozen",
            "",
            NDAttrSource::Constant,
            NDAttrValue::Int32(7),
        ));
        list.update_values();
        assert_eq!(list.get("live").unwrap().value, NDAttrValue::Int32(2));
        assert_eq!(list.get("frozen").unwrap().value, NDAttrValue::Int32(7));
    }
}
