use std::any::Any;
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
use std::time::SystemTime;

use crate::error::{AsynError, AsynResult, AsynStatus};

/// A single entry in an enumeration parameter's choice list.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnumEntry {
    pub string: String,
    pub value: i32,
    pub severity: u16,
}

/// UInt32Digital interrupt-reason selector — C parity for
/// `interruptReason` (`interfaces/asynUInt32Digital.h:25-27`).
///
/// `ZeroToOne` (rising) and `OneToZero` (falling) configure each
/// mask in isolation; `Both` overwrites them together on set and
/// returns `rising | falling` on get (matching
/// `asynPortDriver.cpp:480-535`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InterruptReason {
    ZeroToOne,
    OneToZero,
    Both,
}

/// Parameter data types.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParamType {
    Int32,
    Int64,
    UInt64,
    Float64,
    Octet,
    UInt32Digital,
    Int8Array,
    Int16Array,
    Int32Array,
    Int64Array,
    UInt64Array,
    Float32Array,
    Float64Array,
    Enum,
    GenericPointer,
}

/// Parameter value. Arrays use `Arc<[T]>` for cheap cloning during interrupt broadcast.
/// Octet uses String (typically short; not intended for large binary data).
#[derive(Clone)]
pub enum ParamValue {
    Int32(i32),
    Int64(i64),
    /// Unsigned 64-bit integer (asyn upstream issue #231).
    UInt64(u64),
    Float64(f64),
    Octet(String),
    UInt32Digital(u32),
    Int8Array(Arc<[i8]>),
    Int16Array(Arc<[i16]>),
    Int32Array(Arc<[i32]>),
    Int64Array(Arc<[i64]>),
    /// Unsigned 64-bit integer array (asyn upstream issue #231).
    UInt64Array(Arc<[u64]>),
    Float32Array(Arc<[f32]>),
    Float64Array(Arc<[f64]>),
    Enum {
        index: usize,
        choices: Arc<[EnumEntry]>,
    },
    GenericPointer(Arc<dyn Any + Send + Sync>),
    Undefined,
}

impl fmt::Debug for ParamValue {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Int32(v) => write!(f, "Int32({v:?})"),
            Self::Int64(v) => write!(f, "Int64({v:?})"),
            Self::UInt64(v) => write!(f, "UInt64({v:?})"),
            Self::Float64(v) => write!(f, "Float64({v:?})"),
            Self::Octet(v) => write!(f, "Octet({v:?})"),
            Self::UInt32Digital(v) => write!(f, "UInt32Digital({v:?})"),
            Self::Int8Array(v) => write!(f, "Int8Array({v:?})"),
            Self::Int16Array(v) => write!(f, "Int16Array({v:?})"),
            Self::Int32Array(v) => write!(f, "Int32Array({v:?})"),
            Self::Int64Array(v) => write!(f, "Int64Array({v:?})"),
            Self::UInt64Array(v) => write!(f, "UInt64Array({v:?})"),
            Self::Float32Array(v) => write!(f, "Float32Array({v:?})"),
            Self::Float64Array(v) => write!(f, "Float64Array({v:?})"),
            Self::Enum { index, choices } => write!(f, "Enum(index={index}, choices={choices:?})"),
            Self::GenericPointer(v) => write!(f, "GenericPointer(<{:?}>)", (*v).type_id()),
            Self::Undefined => write!(f, "Undefined"),
        }
    }
}

impl ParamValue {
    pub fn type_name(&self) -> &'static str {
        match self {
            Self::Int32(_) => "Int32",
            Self::Int64(_) => "Int64",
            Self::UInt64(_) => "UInt64",
            Self::Float64(_) => "Float64",
            Self::Octet(_) => "Octet",
            Self::UInt32Digital(_) => "UInt32Digital",
            Self::Int8Array(_) => "Int8Array",
            Self::Int16Array(_) => "Int16Array",
            Self::Int32Array(_) => "Int32Array",
            Self::Int64Array(_) => "Int64Array",
            Self::UInt64Array(_) => "UInt64Array",
            Self::Float32Array(_) => "Float32Array",
            Self::Float64Array(_) => "Float64Array",
            Self::Enum { .. } => "Enum",
            Self::GenericPointer(_) => "GenericPointer",
            Self::Undefined => "Undefined",
        }
    }

    /// Whether this is an array or generic-pointer value. C asynPortDriver
    /// fires interrupts for these through the per-type doCallbacks*Array /
    /// doCallbacksGenericPointer paths, NOT `paramList::callCallbacks`
    /// (whose switch at asynPortDriver.cpp:846-865 handles only scalars).
    /// The callCallbacks isDefined gate (:845) therefore does not govern
    /// them, so the Rust flush must keep firing array triggers regardless
    /// of the `defined` flag.
    pub fn is_array(&self) -> bool {
        matches!(
            self,
            Self::Int8Array(_)
                | Self::Int16Array(_)
                | Self::Int32Array(_)
                | Self::Int64Array(_)
                | Self::UInt64Array(_)
                | Self::Float32Array(_)
                | Self::Float64Array(_)
                | Self::GenericPointer(_)
        )
    }
}

#[derive(Debug, Clone)]
struct ParamEntry {
    name: String,
    param_type: ParamType,
    value: ParamValue,
    /// Whether the value has been explicitly set (C parity: valueDefined).
    defined: bool,
    status: AsynStatus,
    alarm_status: u16,
    alarm_severity: u16,
    value_changed: bool,
    timestamp: Option<SystemTime>,
    /// UInt32Digital: bitmask of bits that changed (for interrupt filtering).
    /// C parity: uInt32CallbackMask in paramVal.
    uint32_interrupt_mask: u32,
    /// UInt32Digital: bits the driver wants to fire callbacks on when
    /// they transition `0 → 1`. C parity: `paramVal::uInt32RisingMask`
    /// (`asynPortDriver/paramVal.h:45`) — written by
    /// `paramList::setUInt32Interrupt(idx, mask, interruptOnZeroToOne)`
    /// (`asynPortDriver.cpp:480-497`), read by `report`/`getInterrupt`.
    uint32_rising_mask: u32,
    /// UInt32Digital: bits the driver wants to fire callbacks on when
    /// they transition `1 → 0`. C parity: `paramVal::uInt32FallingMask`
    /// (`asynPortDriver/paramVal.h:46`).
    uint32_falling_mask: u32,
}

impl ParamEntry {
    fn new(name: String, param_type: ParamType) -> Self {
        // C parity: parameters start with default values but are marked as
        // "not yet defined" (valueDefined=false in C). In C, getters still
        // return the default value but also return asynParamUndefined status.
        // In Rust, we keep the values accessible but track the defined flag.
        let value = match param_type {
            ParamType::Int32 => ParamValue::Int32(0),
            ParamType::Int64 => ParamValue::Int64(0),
            ParamType::UInt64 => ParamValue::UInt64(0),
            ParamType::Float64 => ParamValue::Float64(0.0),
            ParamType::Octet => ParamValue::Octet(String::new()),
            ParamType::UInt32Digital => ParamValue::UInt32Digital(0),
            ParamType::Int8Array => ParamValue::Int8Array(Arc::from([] as [i8; 0])),
            ParamType::Int16Array => ParamValue::Int16Array(Arc::from([] as [i16; 0])),
            ParamType::Int32Array => ParamValue::Int32Array(Arc::from([] as [i32; 0])),
            ParamType::Int64Array => ParamValue::Int64Array(Arc::from([] as [i64; 0])),
            ParamType::UInt64Array => ParamValue::UInt64Array(Arc::from([] as [u64; 0])),
            ParamType::Float32Array => ParamValue::Float32Array(Arc::from([] as [f32; 0])),
            ParamType::Float64Array => ParamValue::Float64Array(Arc::from([] as [f64; 0])),
            ParamType::Enum => ParamValue::Enum {
                index: 0,
                choices: Arc::from([EnumEntry {
                    string: String::new(),
                    value: 0,
                    severity: 0,
                }]),
            },
            ParamType::GenericPointer => ParamValue::GenericPointer(Arc::new(())),
        };
        Self {
            name,
            param_type,
            value,
            defined: false,
            status: AsynStatus::Success,
            alarm_status: 0,
            alarm_severity: 0,
            value_changed: false,
            timestamp: None,
            uint32_interrupt_mask: 0,
            uint32_rising_mask: 0,
            uint32_falling_mask: 0,
        }
    }
}

/// Parameter library managing named parameters indexed by integer.
/// Supports multiple addresses (addr 0..max_addr).
pub struct ParamList {
    max_addr: usize,
    multi_device: bool,
    /// params[addr][index] = ParamEntry
    params: Vec<Vec<ParamEntry>>,
    name_to_index: HashMap<String, usize>,
}

impl ParamList {
    pub fn new(max_addr: usize, multi_device: bool) -> Self {
        let max_addr = max_addr.max(1);
        Self {
            max_addr,
            multi_device,
            params: (0..max_addr).map(|_| Vec::new()).collect(),
            name_to_index: HashMap::new(),
        }
    }

    /// Normalize and validate address.
    /// - multi_device=false: any addr normalizes to 0
    /// - multi_device=true: addr must be in [0, max_addr)
    fn validate_addr(&self, addr: i32) -> AsynResult<usize> {
        if !self.multi_device {
            return Ok(0);
        }
        if addr < 0 || (addr as usize) >= self.max_addr {
            return Err(AsynError::AddressOutOfRange(addr));
        }
        Ok(addr as usize)
    }

    fn get_entry(&self, index: usize, addr: i32) -> AsynResult<&ParamEntry> {
        let a = self.validate_addr(addr)?;
        self.params[a]
            .get(index)
            .ok_or(AsynError::ParamIndexOutOfRange(index))
    }

    fn get_entry_mut(&mut self, index: usize, addr: i32) -> AsynResult<&mut ParamEntry> {
        let a = self.validate_addr(addr)?;
        self.params[a]
            .get_mut(index)
            .ok_or(AsynError::ParamIndexOutOfRange(index))
    }

    /// Create a parameter. Returns its index.
    /// The parameter is created at all addresses.
    ///
    /// Lax variant — when `name` already exists this returns the
    /// existing index silently (`asynSuccess`). That matches the
    /// idempotent build pattern used by `ad-core-rs::ADDriverParams`
    /// and `ad-plugins-rs::NDArrayDriverParams` (the latter create the
    /// shared `ACQUIRE`/`ACQUIRE_BUSY`/`WAIT_FOR_PLUGINS` params, then
    /// `ADDriverParams::create` re-issues `create_param` on the same
    /// names).
    ///
    /// Use [`Self::create_param_strict`] when you need C parity for
    /// `asynParamAlreadyExists` — `paramList::createParam`
    /// (`asynPortDriver/asynPortDriver.cpp:126-138`) returns that
    /// status on duplicate and the wrapper at
    /// `asynPortDriver::createParam(int list, ...)` (lines 991-1011)
    /// translates it to `asynError` with an `ASYN_TRACE_ERROR` log.
    pub fn create_param(&mut self, name: &str, param_type: ParamType) -> AsynResult<usize> {
        if let Some(&idx) = self.name_to_index.get(name) {
            return Ok(idx);
        }
        self.append_param(name, param_type)
    }

    /// Strict variant of [`Self::create_param`] — surfaces
    /// [`AsynError::ParamAlreadyExists`] on duplicate, matching the C
    /// `asynParamAlreadyExists` status returned by
    /// `paramList::createParam` (`asynPortDriver.cpp:126-138`).
    ///
    /// Use this from new callers that should match the C semantics.
    /// Existing callers (`ad-core-rs`, `ad-plugins-rs`) keep using the
    /// lax [`Self::create_param`] until they migrate; switching them
    /// requires teaching the duplicate-name shared-base-class build
    /// to look up the existing index instead of re-issuing
    /// `create_param`.
    pub fn create_param_strict(&mut self, name: &str, param_type: ParamType) -> AsynResult<usize> {
        if self.name_to_index.contains_key(name) {
            return Err(AsynError::ParamAlreadyExists(name.to_string()));
        }
        self.append_param(name, param_type)
    }

    fn append_param(&mut self, name: &str, param_type: ParamType) -> AsynResult<usize> {
        let index = self.params[0].len();
        for addr_params in &mut self.params {
            addr_params.push(ParamEntry::new(name.to_string(), param_type));
        }
        self.name_to_index.insert(name.to_string(), index);
        Ok(index)
    }

    /// Find parameter index by name.
    pub fn find_param(&self, name: &str) -> Option<usize> {
        self.name_to_index.get(name).copied()
    }

    /// Get parameter name by index.
    pub fn param_name(&self, index: usize) -> Option<&str> {
        self.params[0].get(index).map(|e| e.name.as_str())
    }

    /// Get parameter type by index.
    pub fn param_type(&self, index: usize) -> Option<ParamType> {
        self.params[0].get(index).map(|e| e.param_type)
    }

    /// Get the raw ParamValue.
    pub fn get_value(&self, index: usize, addr: i32) -> AsynResult<&ParamValue> {
        Ok(&self.get_entry(index, addr)?.value)
    }

    // --- Scalar getters/setters ---

    /// Read the cached Int32 value, falling back to the type default
    /// (`0`) if the parameter has not been set.
    ///
    /// Use [`Self::get_int32_strict`] when you need C parity for
    /// `asynParamUndefined` — the lax variant matches the
    /// `.unwrap_or(0)` convention used throughout the workspace and
    /// stays callable on never-set params.
    pub fn get_int32(&self, index: usize, addr: i32) -> AsynResult<i32> {
        match &self.get_entry(index, addr)?.value {
            ParamValue::Int32(v) => Ok(*v),
            // C EPICS asyn: asynInt32 interface reads enum index transparently
            ParamValue::Enum { index: idx, .. } => Ok(*idx as i32),
            other => Err(AsynError::TypeMismatch {
                expected: "Int32",
                actual: other.type_name(),
            }),
        }
    }

    /// Strict variant of [`Self::get_int32`] — returns
    /// [`AsynError::ParamUndefined`] when the entry has never been set.
    ///
    /// C parity: `paramVal::getInteger` (`paramVal.cpp:147-155`) checks
    /// `WrongType` first then `NotDefined`; the wrapping
    /// `paramList::getInteger` (`asynPortDriver.cpp:301-322`) translates
    /// `ParamValNotDefined` to `asynParamUndefined`.
    pub fn get_int32_strict(&self, index: usize, addr: i32) -> AsynResult<i32> {
        let entry = self.get_entry(index, addr)?;
        match &entry.value {
            ParamValue::Int32(v) => {
                if !entry.defined {
                    return Err(AsynError::ParamUndefined(index));
                }
                Ok(*v)
            }
            ParamValue::Enum { index: idx, .. } => {
                if !entry.defined {
                    return Err(AsynError::ParamUndefined(index));
                }
                Ok(*idx as i32)
            }
            other => Err(AsynError::TypeMismatch {
                expected: "Int32",
                actual: other.type_name(),
            }),
        }
    }

    pub fn set_int32(&mut self, index: usize, addr: i32, value: i32) -> AsynResult<()> {
        let entry = self.get_entry_mut(index, addr)?;
        match entry.value {
            ParamValue::Int32(ref old) => {
                // C parity: paramVal::setInteger gates on
                // `!isDefined() || (data.ival != value)` (paramVal.cpp:130-138);
                // a first set with `value == default` still flips `defined`.
                if !entry.defined || *old != value {
                    entry.value = ParamValue::Int32(value);
                    entry.value_changed = true;
                    entry.defined = true;
                }
            }
            // C EPICS asyn: asynInt32 interface writes enum index transparently
            ParamValue::Enum {
                ref choices,
                ref mut index,
            } => {
                let new_idx = value as usize;
                if new_idx >= choices.len() {
                    return Err(AsynError::Status {
                        status: AsynStatus::Error,
                        message: format!(
                            "enum index {new_idx} out of range (0..{})",
                            choices.len()
                        ),
                    });
                }
                if !entry.defined || *index != new_idx {
                    *index = new_idx;
                    entry.value_changed = true;
                    entry.defined = true;
                }
            }
            _ => {
                return Err(AsynError::TypeMismatch {
                    expected: "Int32",
                    actual: entry.value.type_name(),
                });
            }
        }
        Ok(())
    }

    /// Lax variant — returns `0.0` for an undefined Float64. Use
    /// [`Self::get_float64_strict`] for C parity.
    pub fn get_float64(&self, index: usize, addr: i32) -> AsynResult<f64> {
        match &self.get_entry(index, addr)?.value {
            ParamValue::Float64(v) => Ok(*v),
            other => Err(AsynError::TypeMismatch {
                expected: "Float64",
                actual: other.type_name(),
            }),
        }
    }

    /// Strict variant — returns [`AsynError::ParamUndefined`] when the
    /// entry has never been set. C parity:
    /// `paramVal::getDouble` (`paramVal.cpp:258-266`) +
    /// `paramList::getDouble` (`asynPortDriver.cpp:383-401`).
    pub fn get_float64_strict(&self, index: usize, addr: i32) -> AsynResult<f64> {
        let entry = self.get_entry(index, addr)?;
        match &entry.value {
            ParamValue::Float64(v) => {
                if !entry.defined {
                    return Err(AsynError::ParamUndefined(index));
                }
                Ok(*v)
            }
            other => Err(AsynError::TypeMismatch {
                expected: "Float64",
                actual: other.type_name(),
            }),
        }
    }

    pub fn set_float64(&mut self, index: usize, addr: i32, value: f64) -> AsynResult<()> {
        let entry = self.get_entry_mut(index, addr)?;
        if let ParamValue::Float64(ref old) = entry.value {
            // C parity: paramVal::setDouble (paramVal.cpp:241-252) —
            // `!isDefined() || data.dval != value` flips defined on the
            // first set even when `value` equals the type default.
            if !entry.defined || *old != value {
                entry.value = ParamValue::Float64(value);
                entry.value_changed = true;
                entry.defined = true;
            }
        } else {
            return Err(AsynError::TypeMismatch {
                expected: "Float64",
                actual: entry.value.type_name(),
            });
        }
        Ok(())
    }

    /// Lax variant — returns `0` for an undefined Int64. Use
    /// [`Self::get_int64_strict`] for C parity.
    pub fn get_int64(&self, index: usize, addr: i32) -> AsynResult<i64> {
        match &self.get_entry(index, addr)?.value {
            ParamValue::Int64(v) => Ok(*v),
            other => Err(AsynError::TypeMismatch {
                expected: "Int64",
                actual: other.type_name(),
            }),
        }
    }

    /// Strict variant — returns [`AsynError::ParamUndefined`] when the
    /// entry has never been set. C parity: `paramVal::getInteger64`
    /// (`paramVal.cpp:176-184`) + `paramList::getInteger64`
    /// (`asynPortDriver.cpp:328-349`).
    pub fn get_int64_strict(&self, index: usize, addr: i32) -> AsynResult<i64> {
        let entry = self.get_entry(index, addr)?;
        match &entry.value {
            ParamValue::Int64(v) => {
                if !entry.defined {
                    return Err(AsynError::ParamUndefined(index));
                }
                Ok(*v)
            }
            other => Err(AsynError::TypeMismatch {
                expected: "Int64",
                actual: other.type_name(),
            }),
        }
    }

    pub fn set_int64(&mut self, index: usize, addr: i32, value: i64) -> AsynResult<()> {
        let entry = self.get_entry_mut(index, addr)?;
        if let ParamValue::Int64(ref old) = entry.value {
            // C parity: paramVal::setInteger64 (paramVal.cpp:189-200).
            if !entry.defined || *old != value {
                entry.value = ParamValue::Int64(value);
                entry.value_changed = true;
                entry.defined = true;
            }
        } else {
            return Err(AsynError::TypeMismatch {
                expected: "Int64",
                actual: entry.value.type_name(),
            });
        }
        Ok(())
    }

    /// Lax variant — returns the empty string for an undefined Octet.
    /// Use [`Self::get_string_strict`] for C parity.
    pub fn get_string(&self, index: usize, addr: i32) -> AsynResult<&str> {
        match &self.get_entry(index, addr)?.value {
            ParamValue::Octet(s) => Ok(s),
            other => Err(AsynError::TypeMismatch {
                expected: "Octet",
                actual: other.type_name(),
            }),
        }
    }

    /// Strict variant — returns [`AsynError::ParamUndefined`] when the
    /// entry has never been set. C parity: `paramVal::getString`
    /// (`paramVal.cpp:283-292`) + `paramList::getString`
    /// (`asynPortDriver.cpp:543-566`).
    pub fn get_string_strict(&self, index: usize, addr: i32) -> AsynResult<&str> {
        let entry = self.get_entry(index, addr)?;
        match &entry.value {
            ParamValue::Octet(s) => {
                if !entry.defined {
                    return Err(AsynError::ParamUndefined(index));
                }
                Ok(s)
            }
            other => Err(AsynError::TypeMismatch {
                expected: "Octet",
                actual: other.type_name(),
            }),
        }
    }

    pub fn set_string(&mut self, index: usize, addr: i32, value: String) -> AsynResult<()> {
        let entry = self.get_entry_mut(index, addr)?;
        if let ParamValue::Octet(ref old) = entry.value {
            // C parity: paramVal::setString (paramVal.cpp:271-281) —
            // `!isDefined() || sval != value`.
            if !entry.defined || *old != value {
                entry.value = ParamValue::Octet(value);
                entry.value_changed = true;
                entry.defined = true;
            }
        } else {
            return Err(AsynError::TypeMismatch {
                expected: "Octet",
                actual: entry.value.type_name(),
            });
        }
        Ok(())
    }

    /// Lax variant — returns `0` for an undefined UInt32Digital. Use
    /// [`Self::get_uint32_strict`] for C parity.
    pub fn get_uint32(&self, index: usize, addr: i32) -> AsynResult<u32> {
        match &self.get_entry(index, addr)?.value {
            ParamValue::UInt32Digital(v) => Ok(*v),
            other => Err(AsynError::TypeMismatch {
                expected: "UInt32Digital",
                actual: other.type_name(),
            }),
        }
    }

    /// Strict variant — returns [`AsynError::ParamUndefined`] when the
    /// entry has never been set. C parity: `paramVal::getUInt32`
    /// (`paramVal.cpp:215-223`) + `paramList::getUInt32`
    /// (`asynPortDriver.cpp:355-376`).
    pub fn get_uint32_strict(&self, index: usize, addr: i32) -> AsynResult<u32> {
        let entry = self.get_entry(index, addr)?;
        match &entry.value {
            ParamValue::UInt32Digital(v) => {
                if !entry.defined {
                    return Err(AsynError::ParamUndefined(index));
                }
                Ok(*v)
            }
            other => Err(AsynError::TypeMismatch {
                expected: "UInt32Digital",
                actual: other.type_name(),
            }),
        }
    }

    pub fn set_uint32(
        &mut self,
        index: usize,
        addr: i32,
        value: u32,
        mask: u32,
        interrupt_mask: u32,
    ) -> AsynResult<()> {
        let entry = self.get_entry_mut(index, addr)?;
        if let ParamValue::UInt32Digital(ref old) = entry.value {
            // C parity: paramVal::setUInt32 (paramVal.cpp:198-225).
            //   if (!isDefined()) { uival = 0; setDefined(true); setValueChanged(); }
            //   newValue = (uival & ~mask) | (value & mask);
            //   if (uival != newValue) { callbackMask |= (uival ^ newValue); ... }
            // The first write must flip `defined` and mark `value_changed`
            // BEFORE any merge, even if `value & mask == 0` — otherwise
            // the equivalent of `setUIntDigitalParam(idx, 0, mask)` on a
            // fresh param leaves the entry undefined and never notifies.
            let was_defined = entry.defined;
            let starting = if was_defined { *old } else { 0 };
            let new_val = (starting & !mask) | (value & mask);
            let changed_bits = if was_defined {
                starting ^ new_val
            } else {
                new_val
            };
            if !was_defined || starting != new_val {
                // C parity: `uInt32CallbackMask |= (data.uival ^ newValue)`
                // (paramVal.cpp:215) — accumulate the union of changed bits
                // across every setUInt32 before one callParamCallbacks, not
                // just the last set's bits. The flush owner resets the mask
                // to 0 after firing (see take_uint32_interrupt_mask).
                entry.uint32_interrupt_mask |= changed_bits;
                entry.value = ParamValue::UInt32Digital(new_val);
                entry.value_changed = true;
                entry.defined = true;
            }
            // C parity: paramVal.cpp:220-224 — a non-zero `interruptMask`
            // forces those bits into the callback mask and marks the value
            // changed EVEN when the stored value is unchanged, so I/O Intr
            // callbacks gated on those bits still fire. This is the
            // `setUIntDigitalParam(.., interruptMask)` overload path; the
            // first-write `defined` flip above already ran when needed.
            if interrupt_mask != 0 {
                entry.uint32_interrupt_mask |= interrupt_mask;
                entry.value_changed = true;
            }
        } else {
            return Err(AsynError::TypeMismatch {
                expected: "UInt32Digital",
                actual: entry.value.type_name(),
            });
        }
        Ok(())
    }

    /// Get the UInt32Digital interrupt mask (accumulated changed bits).
    pub fn get_uint32_interrupt_mask(&self, index: usize, addr: i32) -> AsynResult<u32> {
        Ok(self.get_entry(index, addr)?.uint32_interrupt_mask)
    }

    /// Read the accumulated UInt32Digital callback mask AND reset it to 0.
    ///
    /// C parity: `uint32Callback` clears `uInt32CallbackMask = 0`
    /// immediately after firing (`asynPortDriver.cpp:855`). The flush is
    /// the single owner of this transition — it consumes the mask so the
    /// next flush starts clean and accumulated bits do not leak forward.
    /// A no-op returning 0 for non-UInt32 params, whose mask is always 0.
    pub fn take_uint32_interrupt_mask(&mut self, index: usize, addr: i32) -> AsynResult<u32> {
        let entry = self.get_entry_mut(index, addr)?;
        let mask = entry.uint32_interrupt_mask;
        entry.uint32_interrupt_mask = 0;
        Ok(mask)
    }

    /// Configure which bits of a UInt32Digital parameter should fire
    /// interrupts on transition.
    ///
    /// C parity: `paramList::setUInt32Interrupt`
    /// (`asynPortDriver/asynPortDriver.cpp:480-497`). The reason
    /// argument decides which mask (rising-only, falling-only, or
    /// both) is overwritten.
    pub fn set_uint32_interrupt(
        &mut self,
        index: usize,
        addr: i32,
        mask: u32,
        reason: InterruptReason,
    ) -> AsynResult<()> {
        let entry = self.get_entry_mut(index, addr)?;
        if entry.param_type != ParamType::UInt32Digital {
            return Err(AsynError::TypeMismatch {
                expected: "UInt32Digital",
                actual: entry.value.type_name(),
            });
        }
        match reason {
            InterruptReason::ZeroToOne => entry.uint32_rising_mask = mask,
            InterruptReason::OneToZero => entry.uint32_falling_mask = mask,
            InterruptReason::Both => {
                entry.uint32_rising_mask = mask;
                entry.uint32_falling_mask = mask;
            }
        }
        Ok(())
    }

    /// Clear bits from BOTH rising and falling masks for a
    /// UInt32Digital parameter — C parity:
    /// `paramList::clearUInt32Interrupt`
    /// (`asynPortDriver.cpp:504-511`). The C function does not
    /// accept an `interruptReason`; rising and falling masks are
    /// always cleared together.
    pub fn clear_uint32_interrupt(&mut self, index: usize, addr: i32, mask: u32) -> AsynResult<()> {
        let entry = self.get_entry_mut(index, addr)?;
        if entry.param_type != ParamType::UInt32Digital {
            return Err(AsynError::TypeMismatch {
                expected: "UInt32Digital",
                actual: entry.value.type_name(),
            });
        }
        entry.uint32_rising_mask &= !mask;
        entry.uint32_falling_mask &= !mask;
        Ok(())
    }

    /// Read the configured rising / falling / combined mask.
    ///
    /// C parity: `paramList::getUInt32Interrupt`
    /// (`asynPortDriver.cpp:519-535`). For `Both`, the combined mask
    /// is `rising | falling` (matching the C semantics).
    pub fn get_uint32_interrupt(
        &self,
        index: usize,
        addr: i32,
        reason: InterruptReason,
    ) -> AsynResult<u32> {
        let entry = self.get_entry(index, addr)?;
        if entry.param_type != ParamType::UInt32Digital {
            return Err(AsynError::TypeMismatch {
                expected: "UInt32Digital",
                actual: entry.value.type_name(),
            });
        }
        Ok(match reason {
            InterruptReason::ZeroToOne => entry.uint32_rising_mask,
            InterruptReason::OneToZero => entry.uint32_falling_mask,
            InterruptReason::Both => entry.uint32_rising_mask | entry.uint32_falling_mask,
        })
    }

    // --- Array getters/setters ---

    pub fn get_float64_array(&self, index: usize, addr: i32) -> AsynResult<Arc<[f64]>> {
        match &self.get_entry(index, addr)?.value {
            ParamValue::Float64Array(v) => Ok(v.clone()),
            other => Err(AsynError::TypeMismatch {
                expected: "Float64Array",
                actual: other.type_name(),
            }),
        }
    }

    pub fn set_float64_array(&mut self, index: usize, addr: i32, data: Vec<f64>) -> AsynResult<()> {
        let entry = self.get_entry_mut(index, addr)?;
        if matches!(entry.value, ParamValue::Float64Array(_)) {
            entry.value = ParamValue::Float64Array(Arc::from(data));
            entry.value_changed = true;
            entry.defined = true;
            Ok(())
        } else {
            Err(AsynError::TypeMismatch {
                expected: "Float64Array",
                actual: entry.value.type_name(),
            })
        }
    }

    pub fn get_int32_array(&self, index: usize, addr: i32) -> AsynResult<Arc<[i32]>> {
        match &self.get_entry(index, addr)?.value {
            ParamValue::Int32Array(v) => Ok(v.clone()),
            other => Err(AsynError::TypeMismatch {
                expected: "Int32Array",
                actual: other.type_name(),
            }),
        }
    }

    pub fn set_int32_array(&mut self, index: usize, addr: i32, data: Vec<i32>) -> AsynResult<()> {
        let entry = self.get_entry_mut(index, addr)?;
        if matches!(entry.value, ParamValue::Int32Array(_)) {
            entry.value = ParamValue::Int32Array(Arc::from(data));
            entry.value_changed = true;
            entry.defined = true;
            Ok(())
        } else {
            Err(AsynError::TypeMismatch {
                expected: "Int32Array",
                actual: entry.value.type_name(),
            })
        }
    }

    pub fn get_int8_array(&self, index: usize, addr: i32) -> AsynResult<Arc<[i8]>> {
        match &self.get_entry(index, addr)?.value {
            ParamValue::Int8Array(v) => Ok(v.clone()),
            other => Err(AsynError::TypeMismatch {
                expected: "Int8Array",
                actual: other.type_name(),
            }),
        }
    }

    pub fn set_int8_array(&mut self, index: usize, addr: i32, data: Vec<i8>) -> AsynResult<()> {
        let entry = self.get_entry_mut(index, addr)?;
        if matches!(entry.value, ParamValue::Int8Array(_)) {
            entry.value = ParamValue::Int8Array(Arc::from(data));
            entry.value_changed = true;
            entry.defined = true;
            Ok(())
        } else {
            Err(AsynError::TypeMismatch {
                expected: "Int8Array",
                actual: entry.value.type_name(),
            })
        }
    }

    pub fn get_int16_array(&self, index: usize, addr: i32) -> AsynResult<Arc<[i16]>> {
        match &self.get_entry(index, addr)?.value {
            ParamValue::Int16Array(v) => Ok(v.clone()),
            other => Err(AsynError::TypeMismatch {
                expected: "Int16Array",
                actual: other.type_name(),
            }),
        }
    }

    pub fn set_int16_array(&mut self, index: usize, addr: i32, data: Vec<i16>) -> AsynResult<()> {
        let entry = self.get_entry_mut(index, addr)?;
        if matches!(entry.value, ParamValue::Int16Array(_)) {
            entry.value = ParamValue::Int16Array(Arc::from(data));
            entry.value_changed = true;
            entry.defined = true;
            Ok(())
        } else {
            Err(AsynError::TypeMismatch {
                expected: "Int16Array",
                actual: entry.value.type_name(),
            })
        }
    }

    pub fn get_int64_array(&self, index: usize, addr: i32) -> AsynResult<Arc<[i64]>> {
        match &self.get_entry(index, addr)?.value {
            ParamValue::Int64Array(v) => Ok(v.clone()),
            other => Err(AsynError::TypeMismatch {
                expected: "Int64Array",
                actual: other.type_name(),
            }),
        }
    }

    pub fn set_int64_array(&mut self, index: usize, addr: i32, data: Vec<i64>) -> AsynResult<()> {
        let entry = self.get_entry_mut(index, addr)?;
        if matches!(entry.value, ParamValue::Int64Array(_)) {
            entry.value = ParamValue::Int64Array(Arc::from(data));
            entry.value_changed = true;
            entry.defined = true;
            Ok(())
        } else {
            Err(AsynError::TypeMismatch {
                expected: "Int64Array",
                actual: entry.value.type_name(),
            })
        }
    }

    pub fn get_float32_array(&self, index: usize, addr: i32) -> AsynResult<Arc<[f32]>> {
        match &self.get_entry(index, addr)?.value {
            ParamValue::Float32Array(v) => Ok(v.clone()),
            other => Err(AsynError::TypeMismatch {
                expected: "Float32Array",
                actual: other.type_name(),
            }),
        }
    }

    pub fn set_float32_array(&mut self, index: usize, addr: i32, data: Vec<f32>) -> AsynResult<()> {
        let entry = self.get_entry_mut(index, addr)?;
        if matches!(entry.value, ParamValue::Float32Array(_)) {
            entry.value = ParamValue::Float32Array(Arc::from(data));
            entry.value_changed = true;
            entry.defined = true;
            Ok(())
        } else {
            Err(AsynError::TypeMismatch {
                expected: "Float32Array",
                actual: entry.value.type_name(),
            })
        }
    }

    // --- Enum getters/setters ---

    pub fn get_enum(&self, index: usize, addr: i32) -> AsynResult<(usize, Arc<[EnumEntry]>)> {
        match &self.get_entry(index, addr)?.value {
            ParamValue::Enum {
                index: idx,
                choices,
            } => Ok((*idx, choices.clone())),
            other => Err(AsynError::TypeMismatch {
                expected: "Enum",
                actual: other.type_name(),
            }),
        }
    }

    pub fn set_enum_index(&mut self, index: usize, addr: i32, value: usize) -> AsynResult<()> {
        let entry = self.get_entry_mut(index, addr)?;
        if let ParamValue::Enum {
            ref choices,
            index: ref mut idx,
        } = entry.value
        {
            if value >= choices.len() {
                return Err(AsynError::Status {
                    status: AsynStatus::Error,
                    message: format!("enum index {value} out of range (0..{})", choices.len()),
                });
            }
            // C parity: enum index is stored through paramVal::setInteger,
            // which gates on `!isDefined() || data != value` — a first set
            // to index 0 (the default) must still flip `defined`.
            if !entry.defined || *idx != value {
                *idx = value;
                entry.value_changed = true;
                entry.defined = true;
            }
        } else {
            return Err(AsynError::TypeMismatch {
                expected: "Enum",
                actual: entry.value.type_name(),
            });
        }
        Ok(())
    }

    pub fn set_enum_choices(
        &mut self,
        index: usize,
        addr: i32,
        choices: Arc<[EnumEntry]>,
    ) -> AsynResult<()> {
        let entry = self.get_entry_mut(index, addr)?;
        if let ParamValue::Enum {
            index: ref mut idx,
            choices: ref mut ch,
        } = entry.value
        {
            *ch = choices;
            // Reset index if out of range
            if *idx >= ch.len() {
                *idx = 0;
            }
            entry.value_changed = true;
            entry.defined = true;
        } else {
            return Err(AsynError::TypeMismatch {
                expected: "Enum",
                actual: entry.value.type_name(),
            });
        }
        Ok(())
    }

    // --- GenericPointer getters/setters ---

    pub fn get_generic_pointer(
        &self,
        index: usize,
        addr: i32,
    ) -> AsynResult<Arc<dyn Any + Send + Sync>> {
        match &self.get_entry(index, addr)?.value {
            ParamValue::GenericPointer(v) => Ok(v.clone()),
            other => Err(AsynError::TypeMismatch {
                expected: "GenericPointer",
                actual: other.type_name(),
            }),
        }
    }

    pub fn set_generic_pointer(
        &mut self,
        index: usize,
        addr: i32,
        value: Arc<dyn Any + Send + Sync>,
    ) -> AsynResult<()> {
        let entry = self.get_entry_mut(index, addr)?;
        if matches!(entry.value, ParamValue::GenericPointer(_)) {
            entry.value = ParamValue::GenericPointer(value);
            entry.value_changed = true;
            entry.defined = true; // Any is not comparable
            Ok(())
        } else {
            Err(AsynError::TypeMismatch {
                expected: "GenericPointer",
                actual: entry.value.type_name(),
            })
        }
    }

    /// Check if a parameter has been explicitly set (C parity: valueDefined).
    pub fn is_param_defined(&self, index: usize, addr: i32) -> AsynResult<bool> {
        Ok(self.get_entry(index, addr)?.defined)
    }

    // --- Status ---

    pub fn set_param_status(
        &mut self,
        index: usize,
        addr: i32,
        status: AsynStatus,
        alarm_status: u16,
        alarm_severity: u16,
    ) -> AsynResult<()> {
        let entry = self.get_entry_mut(index, addr)?;
        // C paramVal::setStatus / setAlarmStatus / setAlarmSeverity
        // (paramVal.cpp:71-104) each call setValueChanged() when their
        // field actually changes; the paramList wrappers
        // (asynPortDriver.cpp:420-472) then run registerParameterChange ->
        // setFlag, marking the param dirty. So a STATUS- or ALARM-only
        // transition (no value change) is still delivered on the next
        // callParamCallbacks. Each changed setter also forces the full
        // UInt32Digital callback mask (0xFFFFFFFF) so every subscriber bit
        // fires. Pre-fix Rust assigned the fields without marking
        // value_changed, so a status/alarm-only transition was silently
        // dropped by take_changed (the standard setParamStatus() +
        // callParamCallbacks() alarm-push pattern never fired).
        let changed = entry.status != status
            || entry.alarm_status != alarm_status
            || entry.alarm_severity != alarm_severity;
        entry.status = status;
        entry.alarm_status = alarm_status;
        entry.alarm_severity = alarm_severity;
        if changed {
            entry.value_changed = true;
            if entry.param_type == ParamType::UInt32Digital {
                entry.uint32_interrupt_mask = 0xFFFF_FFFF;
            }
        }
        Ok(())
    }

    pub fn get_param_status(&self, index: usize, addr: i32) -> AsynResult<(AsynStatus, u16, u16)> {
        let entry = self.get_entry(index, addr)?;
        Ok((entry.status, entry.alarm_status, entry.alarm_severity))
    }

    // --- Timestamp ---

    pub fn set_timestamp(&mut self, index: usize, addr: i32, ts: SystemTime) -> AsynResult<()> {
        self.get_entry_mut(index, addr)?.timestamp = Some(ts);
        Ok(())
    }

    pub fn get_timestamp(&self, index: usize, addr: i32) -> AsynResult<Option<SystemTime>> {
        Ok(self.get_entry(index, addr)?.timestamp)
    }

    // --- Change tracking ---

    /// Clear the changed flag for a single parameter. Returns true if it was changed.
    pub fn take_changed_single(&mut self, index: usize, addr: i32) -> AsynResult<bool> {
        let entry = self.get_entry_mut(index, addr)?;
        let was_changed = entry.value_changed;
        entry.value_changed = false;
        Ok(was_changed)
    }

    /// Mark a parameter as changed without modifying its value.
    /// Useful for triggering I/O Intr on params whose data is served via
    /// read_*_array overrides rather than the param cache.
    pub fn mark_changed(&mut self, index: usize, addr: i32) -> AsynResult<()> {
        self.get_entry_mut(index, addr)?.value_changed = true;
        Ok(())
    }

    /// Returns indices of parameters whose values changed since last call, then clears flags.
    pub fn take_changed(&mut self, addr: i32) -> AsynResult<Vec<usize>> {
        let a = self.validate_addr(addr)?;
        let mut changed = Vec::new();
        for (i, entry) in self.params[a].iter_mut().enumerate() {
            if entry.value_changed {
                entry.value_changed = false;
                changed.push(i);
            }
        }
        Ok(changed)
    }

    /// Number of parameters.
    pub fn len(&self) -> usize {
        self.params[0].len()
    }

    pub fn is_empty(&self) -> bool {
        self.params[0].is_empty()
    }
}

/// Flat parameter-group helper — C++ `asynParamSet` equivalent.
///
/// C asyn (`asynPortDriver/asynParamSet.h`) defines:
///
/// ```cpp
/// struct asynParam {
///     const char* name;
///     asynParamType type;
///     int* index;
/// };
/// class asynParamSet {
/// protected:
///     void add(const char* name, asynParamType type, int* index);
/// public:
///     std::vector<asynParam> getParamDefinitions();
/// };
/// ```
///
/// and `asynPortDriver::createParams` (asynPortDriver.cpp:4115-4126)
/// iterates the vector calling `createParam(name, type, index)` so the
/// driver subclass gets its int member back-filled.
///
/// In Rust we cannot stash a `&mut i32` for the duration of the build,
/// so the idiomatic equivalent is "register names + types, then run
/// `create_all` and read back the assigned indices as a Vec keyed by
/// add-order." Callers persist the returned `Vec<usize>` (or wrap it
/// in a domain-specific struct) the same way C++ drivers persist
/// their `int paramIdx` members.
///
/// The structure is intentionally a **flat list** — C asyn has no
/// tree / topology layer for parameter groups; that idea is invented
/// (see `docs/asyn-missing.md` notes on PVI).
#[derive(Default, Debug, Clone)]
pub struct AsynParamSet {
    defs: Vec<(String, ParamType)>,
}

impl AsynParamSet {
    pub fn new() -> Self {
        Self { defs: Vec::new() }
    }

    /// Append a parameter definition. Mirrors C++ `asynParamSet::add`.
    /// Returns the slot index — the position the param will occupy in
    /// the `Vec<usize>` returned by [`Self::create_all`].
    pub fn add(&mut self, name: &str, ty: ParamType) -> usize {
        let slot = self.defs.len();
        self.defs.push((name.to_string(), ty));
        slot
    }

    /// C++ `asynPortDriver::createParams` (asynPortDriver.cpp:4119-4125):
    /// iterate the definitions in registration order, call
    /// `createParam` for each, abort with `asynError` on the first
    /// failure. Returns the assigned indices in the same order as
    /// [`Self::add`] calls so the caller can recover slot→index
    /// without name lookup.
    pub fn create_all(&self, params: &mut ParamList) -> AsynResult<Vec<usize>> {
        let mut indices = Vec::with_capacity(self.defs.len());
        for (name, ty) in &self.defs {
            indices.push(params.create_param(name, *ty)?);
        }
        Ok(indices)
    }

    /// Number of definitions registered. Cheap inspector for tests.
    pub fn len(&self) -> usize {
        self.defs.len()
    }

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

    /// Iterate definitions in registration order — read-only view.
    pub fn iter(&self) -> impl Iterator<Item = (&str, ParamType)> {
        self.defs.iter().map(|(n, t)| (n.as_str(), *t))
    }
}

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

    #[test]
    fn test_create_and_find() {
        let mut pl = ParamList::new(1, false);
        let i0 = pl.create_param("TEMP", ParamType::Float64).unwrap();
        let i1 = pl.create_param("COUNT", ParamType::Int32).unwrap();
        assert_eq!(i0, 0);
        assert_eq!(i1, 1);
        assert_eq!(pl.find_param("TEMP"), Some(0));
        assert_eq!(pl.find_param("COUNT"), Some(1));
        assert_eq!(pl.find_param("NOPE"), None);
        // Duplicate create returns same index
        assert_eq!(pl.create_param("TEMP", ParamType::Float64).unwrap(), 0);
    }

    #[test]
    fn test_create_param_strict_duplicate_returns_already_exists() {
        // C parity: paramList::createParam (asynPortDriver.cpp:130) —
        // a second createParam with an already-known name returns
        // `asynParamAlreadyExists`; the asynPortDriver wrapper
        // (asynPortDriver.cpp:991-1011) translates that to
        // `asynError` with a TRACE_ERROR log. The lax `create_param`
        // intentionally absorbs the duplicate for `ad-core-rs` /
        // `ad-plugins-rs` idempotent build chains; the strict
        // variant is what C-parity-sensitive callers reach for.
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param_strict("VAL", ParamType::Int32).unwrap();
        assert_eq!(idx, 0);
        match pl.create_param_strict("VAL", ParamType::Int32) {
            Err(AsynError::ParamAlreadyExists(name)) => assert_eq!(name, "VAL"),
            other => panic!("expected ParamAlreadyExists, got {other:?}"),
        }
        // Strict and lax variants share the registry: a name created
        // via the lax path must still be visible to strict and
        // produce ParamAlreadyExists.
        match pl.create_param_strict("VAL", ParamType::Int32) {
            Err(AsynError::ParamAlreadyExists(_)) => {}
            other => panic!("strict must observe lax-created names, got {other:?}"),
        }
        // ...and vice versa.
        assert_eq!(pl.create_param("VAL", ParamType::Int32).unwrap(), 0);
    }

    #[test]
    fn test_create_param_strict_distinct_names_succeed() {
        let mut pl = ParamList::new(1, false);
        let a = pl.create_param_strict("A", ParamType::Int32).unwrap();
        let b = pl.create_param_strict("B", ParamType::Float64).unwrap();
        assert_eq!(a, 0);
        assert_eq!(b, 1);
        assert_eq!(pl.find_param("A"), Some(0));
        assert_eq!(pl.find_param("B"), Some(1));
    }

    #[test]
    fn test_uint32_set_get_clear_interrupt_masks() {
        // C parity: paramList::setUInt32Interrupt /
        // paramList::clearUInt32Interrupt / paramList::getUInt32Interrupt
        // at asynPortDriver.cpp:480-535. ZeroToOne writes the rising
        // mask only; OneToZero writes the falling mask only; Both
        // overwrites them together. clear strips bits from BOTH
        // masks. get(Both) returns rising | falling.
        let mut pl = ParamList::new(1, false);
        let idx = pl
            .create_param_strict("BITS", ParamType::UInt32Digital)
            .unwrap();

        pl.set_uint32_interrupt(idx, 0, 0xF0, InterruptReason::ZeroToOne)
            .unwrap();
        assert_eq!(
            pl.get_uint32_interrupt(idx, 0, InterruptReason::ZeroToOne)
                .unwrap(),
            0xF0
        );
        assert_eq!(
            pl.get_uint32_interrupt(idx, 0, InterruptReason::OneToZero)
                .unwrap(),
            0x00
        );

        pl.set_uint32_interrupt(idx, 0, 0x0F, InterruptReason::OneToZero)
            .unwrap();
        assert_eq!(
            pl.get_uint32_interrupt(idx, 0, InterruptReason::Both)
                .unwrap(),
            0xFF
        );

        // clear strips from both masks.
        pl.clear_uint32_interrupt(idx, 0, 0x10).unwrap();
        assert_eq!(
            pl.get_uint32_interrupt(idx, 0, InterruptReason::ZeroToOne)
                .unwrap(),
            0xE0
        );
        // No falling bit at 0x10 to begin with; clear is a no-op there.
        assert_eq!(
            pl.get_uint32_interrupt(idx, 0, InterruptReason::OneToZero)
                .unwrap(),
            0x0F
        );

        // Both overwrites symmetric.
        pl.set_uint32_interrupt(idx, 0, 0xAA, InterruptReason::Both)
            .unwrap();
        assert_eq!(
            pl.get_uint32_interrupt(idx, 0, InterruptReason::ZeroToOne)
                .unwrap(),
            0xAA
        );
        assert_eq!(
            pl.get_uint32_interrupt(idx, 0, InterruptReason::OneToZero)
                .unwrap(),
            0xAA
        );
        assert_eq!(
            pl.get_uint32_interrupt(idx, 0, InterruptReason::Both)
                .unwrap(),
            0xAA
        );
    }

    #[test]
    fn test_uint32_interrupt_type_mismatch_rejects_non_uint32() {
        // C parity: setUInt32Interrupt/getUInt32Interrupt/clearUInt32Interrupt
        // return `asynParamWrongType` when called on a non-UInt32Digital
        // param (asynPortDriver.cpp:483/507/522).
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("VAL", ParamType::Int32).unwrap();
        match pl.set_uint32_interrupt(idx, 0, 0xFF, InterruptReason::Both) {
            Err(AsynError::TypeMismatch { expected, .. }) => {
                assert_eq!(expected, "UInt32Digital")
            }
            other => panic!("expected TypeMismatch, got {other:?}"),
        }
        match pl.clear_uint32_interrupt(idx, 0, 0xFF) {
            Err(AsynError::TypeMismatch { .. }) => {}
            other => panic!("expected TypeMismatch, got {other:?}"),
        }
        match pl.get_uint32_interrupt(idx, 0, InterruptReason::Both) {
            Err(AsynError::TypeMismatch { .. }) => {}
            other => panic!("expected TypeMismatch, got {other:?}"),
        }
    }

    #[test]
    fn test_get_set_int32() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("VAL", ParamType::Int32).unwrap();
        assert_eq!(pl.get_int32(idx, 0).unwrap(), 0);
        pl.set_int32(idx, 0, 42).unwrap();
        assert_eq!(pl.get_int32(idx, 0).unwrap(), 42);
    }

    #[test]
    fn test_get_set_float64() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("TEMP", ParamType::Float64).unwrap();
        pl.set_float64(idx, 0, 3.14).unwrap();
        assert!((pl.get_float64(idx, 0).unwrap() - 3.14).abs() < 1e-10);
    }

    #[test]
    fn test_get_set_string() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("MSG", ParamType::Octet).unwrap();
        pl.set_string(idx, 0, "hello".into()).unwrap();
        assert_eq!(pl.get_string(idx, 0).unwrap(), "hello");
    }

    #[test]
    fn test_get_set_uint32_mask() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("BITS", ParamType::UInt32Digital).unwrap();
        pl.set_uint32(idx, 0, 0xFF, 0x0F, 0).unwrap();
        assert_eq!(pl.get_uint32(idx, 0).unwrap(), 0x0F);
        pl.set_uint32(idx, 0, 0xFF, 0xF0, 0).unwrap();
        assert_eq!(pl.get_uint32(idx, 0).unwrap(), 0xFF);
    }

    #[test]
    fn test_multi_addr_isolation() {
        let mut pl = ParamList::new(3, true);
        let idx = pl.create_param("VAL", ParamType::Int32).unwrap();
        pl.set_int32(idx, 0, 10).unwrap();
        pl.set_int32(idx, 1, 20).unwrap();
        pl.set_int32(idx, 2, 30).unwrap();
        assert_eq!(pl.get_int32(idx, 0).unwrap(), 10);
        assert_eq!(pl.get_int32(idx, 1).unwrap(), 20);
        assert_eq!(pl.get_int32(idx, 2).unwrap(), 30);
    }

    #[test]
    fn test_addr_out_of_range() {
        let pl = ParamList::new(2, true);
        assert!(pl.validate_addr(-1).is_err());
        assert!(pl.validate_addr(2).is_err());
        assert!(pl.validate_addr(0).is_ok());
        assert!(pl.validate_addr(1).is_ok());
    }

    #[test]
    fn test_addr_normalize_single_device() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("V", ParamType::Int32).unwrap();
        pl.set_int32(idx, 0, 99).unwrap();
        // Any addr normalizes to 0 for single-device
        assert_eq!(pl.get_int32(idx, 5).unwrap(), 99);
        assert_eq!(pl.get_int32(idx, -1).unwrap(), 99);
    }

    #[test]
    fn test_index_out_of_range() {
        let pl = ParamList::new(1, false);
        assert!(pl.get_int32(999, 0).is_err());
    }

    #[test]
    fn test_type_mismatch() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("VAL", ParamType::Int32).unwrap();
        assert!(pl.get_float64(idx, 0).is_err());
        assert!(pl.set_float64(idx, 0, 1.0).is_err());
    }

    #[test]
    fn test_change_tracking() {
        let mut pl = ParamList::new(1, false);
        let i0 = pl.create_param("A", ParamType::Int32).unwrap();
        let i1 = pl.create_param("B", ParamType::Float64).unwrap();

        pl.set_int32(i0, 0, 1).unwrap();
        pl.set_float64(i1, 0, 2.0).unwrap();

        let changed = pl.take_changed(0).unwrap();
        assert_eq!(changed.len(), 2);
        assert_eq!(changed[0], 0);
        assert_eq!(changed[1], 1);

        // Second call returns empty
        let changed2 = pl.take_changed(0).unwrap();
        assert!(changed2.is_empty());
    }

    #[test]
    fn test_same_value_no_change() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("V", ParamType::Int32).unwrap();
        pl.set_int32(idx, 0, 42).unwrap();
        let _ = pl.take_changed(0).unwrap(); // clear

        // Set same value
        pl.set_int32(idx, 0, 42).unwrap();
        let changed = pl.take_changed(0).unwrap();
        assert!(changed.is_empty());
    }

    #[test]
    fn test_array_params() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("WF", ParamType::Float64Array).unwrap();
        pl.set_float64_array(idx, 0, vec![1.0, 2.0, 3.0]).unwrap();
        let arr = pl.get_float64_array(idx, 0).unwrap();
        assert_eq!(&*arr, &[1.0, 2.0, 3.0]);
    }

    #[test]
    fn test_param_status() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("V", ParamType::Int32).unwrap();
        pl.set_param_status(idx, 0, AsynStatus::Timeout, 1, 2)
            .unwrap();
        let (st, as_, sev) = pl.get_param_status(idx, 0).unwrap();
        assert_eq!(st, AsynStatus::Timeout);
        assert_eq!(as_, 1);
        assert_eq!(sev, 2);
    }

    #[test]
    fn test_param_name_and_type() {
        let mut pl = ParamList::new(1, false);
        pl.create_param("TEMP", ParamType::Float64).unwrap();
        assert_eq!(pl.param_name(0), Some("TEMP"));
        assert_eq!(pl.param_type(0), Some(ParamType::Float64));
        assert_eq!(pl.param_name(99), None);
    }

    #[test]
    fn test_timestamp_none_by_default() {
        let mut pl = ParamList::new(1, false);
        pl.create_param("V", ParamType::Int32).unwrap();
        assert_eq!(pl.get_timestamp(0, 0).unwrap(), None);
    }

    #[test]
    fn test_timestamp_set_get() {
        let mut pl = ParamList::new(1, false);
        pl.create_param("V", ParamType::Int32).unwrap();
        let ts = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(12345);
        pl.set_timestamp(0, 0, ts).unwrap();
        assert_eq!(pl.get_timestamp(0, 0).unwrap(), Some(ts));
    }

    #[test]
    fn test_take_changed_returns_indices() {
        let mut pl = ParamList::new(1, false);
        pl.create_param("A", ParamType::Int32).unwrap();
        pl.create_param("B", ParamType::Float64).unwrap();
        pl.create_param("C", ParamType::Octet).unwrap();

        pl.set_int32(0, 0, 1).unwrap();
        pl.set_string(2, 0, "x".into()).unwrap();

        let changed = pl.take_changed(0).unwrap();
        assert_eq!(changed, vec![0, 2]);
    }

    // --- Enum tests ---

    #[test]
    fn test_enum_default_sentinel() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("MODE", ParamType::Enum).unwrap();
        let (index, choices) = pl.get_enum(idx, 0).unwrap();
        assert_eq!(index, 0);
        assert_eq!(choices.len(), 1);
        assert_eq!(choices[0].string, "");
        assert_eq!(choices[0].value, 0);
        assert_eq!(choices[0].severity, 0);
    }

    #[test]
    fn test_enum_set_get_index() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("MODE", ParamType::Enum).unwrap();
        let choices: Arc<[EnumEntry]> = Arc::from(vec![
            EnumEntry {
                string: "Off".into(),
                value: 0,
                severity: 0,
            },
            EnumEntry {
                string: "On".into(),
                value: 1,
                severity: 0,
            },
        ]);
        pl.set_enum_choices(idx, 0, choices).unwrap();
        pl.set_enum_index(idx, 0, 1).unwrap();
        let (index, _) = pl.get_enum(idx, 0).unwrap();
        assert_eq!(index, 1);
    }

    #[test]
    fn test_enum_index_out_of_range() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("MODE", ParamType::Enum).unwrap();
        // Default has 1 choice (sentinel), so index=1 is out of range
        assert!(pl.set_enum_index(idx, 0, 1).is_err());
    }

    #[test]
    fn test_enum_type_mismatch() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("VAL", ParamType::Int32).unwrap();
        assert!(pl.get_enum(idx, 0).is_err());
    }

    #[test]
    fn test_enum_choices_update_resets_index() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("MODE", ParamType::Enum).unwrap();
        let choices: Arc<[EnumEntry]> = Arc::from(vec![
            EnumEntry {
                string: "A".into(),
                value: 0,
                severity: 0,
            },
            EnumEntry {
                string: "B".into(),
                value: 1,
                severity: 0,
            },
            EnumEntry {
                string: "C".into(),
                value: 2,
                severity: 0,
            },
        ]);
        pl.set_enum_choices(idx, 0, choices).unwrap();
        pl.set_enum_index(idx, 0, 2).unwrap();
        // Now shrink choices — index 2 is out of range, should reset to 0
        let new_choices: Arc<[EnumEntry]> = Arc::from(vec![EnumEntry {
            string: "X".into(),
            value: 0,
            severity: 0,
        }]);
        pl.set_enum_choices(idx, 0, new_choices).unwrap();
        let (index, choices) = pl.get_enum(idx, 0).unwrap();
        assert_eq!(index, 0);
        assert_eq!(choices.len(), 1);
    }

    #[test]
    fn test_enum_set_choices_marks_changed() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("MODE", ParamType::Enum).unwrap();
        let _ = pl.take_changed(0).unwrap(); // clear initial
        let choices: Arc<[EnumEntry]> = Arc::from(vec![EnumEntry {
            string: "A".into(),
            value: 0,
            severity: 0,
        }]);
        pl.set_enum_choices(idx, 0, choices).unwrap();
        let changed = pl.take_changed(0).unwrap();
        assert!(changed.contains(&idx));
    }

    // --- GenericPointer tests ---

    #[test]
    fn test_generic_pointer_default() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("PTR", ParamType::GenericPointer).unwrap();
        let val = pl.get_generic_pointer(idx, 0).unwrap();
        assert!(val.downcast_ref::<()>().is_some());
    }

    #[test]
    fn test_generic_pointer_set_get_downcast() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("PTR", ParamType::GenericPointer).unwrap();
        let data: Arc<dyn Any + Send + Sync> = Arc::new(42i32);
        pl.set_generic_pointer(idx, 0, data).unwrap();
        let val = pl.get_generic_pointer(idx, 0).unwrap();
        assert_eq!(*val.downcast_ref::<i32>().unwrap(), 42);
    }

    #[test]
    fn test_generic_pointer_downcast_wrong_type() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("PTR", ParamType::GenericPointer).unwrap();
        let data: Arc<dyn Any + Send + Sync> = Arc::new(42i32);
        pl.set_generic_pointer(idx, 0, data).unwrap();
        let val = pl.get_generic_pointer(idx, 0).unwrap();
        assert!(val.downcast_ref::<String>().is_none());
    }

    #[test]
    fn test_generic_pointer_type_mismatch() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("VAL", ParamType::Int32).unwrap();
        assert!(pl.get_generic_pointer(idx, 0).is_err());
    }

    #[test]
    fn test_generic_pointer_debug_shows_type_id() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("PTR", ParamType::GenericPointer).unwrap();
        let data: Arc<dyn Any + Send + Sync> = Arc::new(vec![1, 2, 3]);
        pl.set_generic_pointer(idx, 0, data).unwrap();
        let val = pl.get_value(idx, 0).unwrap();
        let s = format!("{val:?}");
        assert!(s.contains("GenericPointer"));
        assert!(s.contains("TypeId"));
    }

    // --- Int64 tests ---

    #[test]
    fn test_get_set_int64() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("BIG", ParamType::Int64).unwrap();
        assert_eq!(pl.get_int64(idx, 0).unwrap(), 0);
        pl.set_int64(idx, 0, i64::MAX).unwrap();
        assert_eq!(pl.get_int64(idx, 0).unwrap(), i64::MAX);
    }

    #[test]
    fn test_int64_type_mismatch() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("V", ParamType::Int32).unwrap();
        assert!(pl.get_int64(idx, 0).is_err());
        assert!(pl.set_int64(idx, 0, 1).is_err());
    }

    #[test]
    fn test_int64_same_value_no_change() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("V", ParamType::Int64).unwrap();
        pl.set_int64(idx, 0, 42).unwrap();
        let _ = pl.take_changed(0).unwrap();
        pl.set_int64(idx, 0, 42).unwrap();
        let changed = pl.take_changed(0).unwrap();
        assert!(changed.is_empty());
    }

    #[test]
    fn test_int64_change_tracking() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("V", ParamType::Int64).unwrap();
        pl.set_int64(idx, 0, 100).unwrap();
        let changed = pl.take_changed(0).unwrap();
        assert_eq!(changed, vec![idx]);
    }

    // --- AsynParamSet (C++ asynParamSet equivalent) ---

    #[test]
    fn asyn_param_set_add_returns_slot_index_in_order() {
        let mut set = AsynParamSet::new();
        // C++ `add()` returns void; we expose the slot index so a
        // Rust caller can recover indices via the Vec returned by
        // create_all(). Order matters: registration order == slot
        // order == result-Vec order.
        assert_eq!(set.add("Temperature", ParamType::Float64), 0);
        assert_eq!(set.add("Status", ParamType::Int32), 1);
        assert_eq!(set.add("Tag", ParamType::Octet), 2);
        assert_eq!(set.len(), 3);
    }

    #[test]
    fn asyn_param_set_create_all_assigns_indices_in_order() {
        let mut set = AsynParamSet::new();
        let temp_slot = set.add("Temperature", ParamType::Float64);
        let status_slot = set.add("Status", ParamType::Int32);
        let tag_slot = set.add("Tag", ParamType::Octet);

        let mut pl = ParamList::new(1, false);
        let indices = set.create_all(&mut pl).unwrap();
        assert_eq!(indices.len(), 3);
        assert_eq!(indices[temp_slot], 0);
        assert_eq!(indices[status_slot], 1);
        assert_eq!(indices[tag_slot], 2);
        // ParamList agrees on the names.
        assert_eq!(pl.find_param("Temperature"), Some(0));
        assert_eq!(pl.find_param("Status"), Some(1));
        assert_eq!(pl.find_param("Tag"), Some(2));
    }

    #[test]
    fn asyn_param_set_iter_preserves_registration_order() {
        let mut set = AsynParamSet::new();
        set.add("A", ParamType::Int32);
        set.add("B", ParamType::Float64);
        set.add("C", ParamType::Octet);
        let names: Vec<&str> = set.iter().map(|(n, _)| n).collect();
        assert_eq!(names, vec!["A", "B", "C"]);
    }

    #[test]
    fn asyn_param_set_empty_create_all_is_noop() {
        let set = AsynParamSet::new();
        let mut pl = ParamList::new(1, false);
        let idx = set.create_all(&mut pl).unwrap();
        assert!(idx.is_empty());
        assert!(pl.is_empty());
    }

    #[test]
    fn asyn_param_set_duplicate_name_returns_existing_index() {
        // ParamList::create_param returns the existing index on dup
        // (silent dedup); AsynParamSet inherits that — two add()
        // calls with the same name resolve to the same final index.
        // Matches C asyn where createParam dedupes via name_to_index.
        let mut set = AsynParamSet::new();
        set.add("X", ParamType::Int32);
        set.add("X", ParamType::Int32);
        let mut pl = ParamList::new(1, false);
        let idx = set.create_all(&mut pl).unwrap();
        assert_eq!(idx, vec![0, 0]);
    }

    // ------------------------------------------------------------------
    // Strict-getter / C-parity tests for asynParamUndefined.
    // The lax `get_*` variants stay at the type default; the `_strict`
    // variants surface the C status. The setters now flip `defined` on a
    // first set even when the value equals the type default
    // (C `paramVal::setInteger/setDouble/setString/setUInt32`).
    // ------------------------------------------------------------------

    #[test]
    fn get_int32_strict_undefined_returns_param_undefined() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("U", ParamType::Int32).unwrap();
        // Lax getter still works (returns the type default).
        assert_eq!(pl.get_int32(idx, 0).unwrap(), 0);
        // Strict getter surfaces asynParamUndefined.
        let err = pl.get_int32_strict(idx, 0).unwrap_err();
        assert!(matches!(err, AsynError::ParamUndefined(i) if i == idx));
        pl.set_int32(idx, 0, 7).unwrap();
        assert_eq!(pl.get_int32_strict(idx, 0).unwrap(), 7);
    }

    #[test]
    fn get_float64_strict_undefined_returns_param_undefined() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("U", ParamType::Float64).unwrap();
        assert_eq!(pl.get_float64(idx, 0).unwrap(), 0.0);
        assert!(matches!(
            pl.get_float64_strict(idx, 0).unwrap_err(),
            AsynError::ParamUndefined(i) if i == idx
        ));
    }

    #[test]
    fn get_int64_strict_undefined_returns_param_undefined() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("U", ParamType::Int64).unwrap();
        assert!(matches!(
            pl.get_int64_strict(idx, 0).unwrap_err(),
            AsynError::ParamUndefined(i) if i == idx
        ));
    }

    #[test]
    fn get_uint32_strict_undefined_returns_param_undefined() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("U", ParamType::UInt32Digital).unwrap();
        assert!(matches!(
            pl.get_uint32_strict(idx, 0).unwrap_err(),
            AsynError::ParamUndefined(i) if i == idx
        ));
    }

    #[test]
    fn get_string_strict_undefined_returns_param_undefined() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("U", ParamType::Octet).unwrap();
        assert!(matches!(
            pl.get_string_strict(idx, 0).unwrap_err(),
            AsynError::ParamUndefined(i) if i == idx
        ));
    }

    #[test]
    fn get_strict_checks_type_before_undefined_c_parity() {
        // C parity: paramVal::getInteger throws ParamValWrongType before
        // ParamValNotDefined (paramVal.cpp:147-155). Reading a Float64
        // param via get_int32_strict on a never-set entry must surface
        // TypeMismatch, not ParamUndefined.
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("F", ParamType::Float64).unwrap();
        assert!(matches!(
            pl.get_int32_strict(idx, 0).unwrap_err(),
            AsynError::TypeMismatch {
                expected: "Int32",
                ..
            }
        ));
    }

    #[test]
    fn set_int32_first_write_with_default_value_flips_defined() {
        // C parity: paramVal::setInteger checks `!isDefined() || value != old`
        // so writing `0` to a fresh Int32 still flips defined+value_changed.
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("V", ParamType::Int32).unwrap();
        assert!(!pl.is_param_defined(idx, 0).unwrap());
        pl.set_int32(idx, 0, 0).unwrap();
        assert!(pl.is_param_defined(idx, 0).unwrap());
        assert!(pl.take_changed_single(idx, 0).unwrap());
        assert_eq!(pl.get_int32_strict(idx, 0).unwrap(), 0);
    }

    #[test]
    fn set_float64_first_write_with_default_value_flips_defined() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("V", ParamType::Float64).unwrap();
        pl.set_float64(idx, 0, 0.0).unwrap();
        assert!(pl.is_param_defined(idx, 0).unwrap());
        assert_eq!(pl.get_float64_strict(idx, 0).unwrap(), 0.0);
    }

    #[test]
    fn set_uint32_first_write_zero_mask_zero_flips_defined() {
        // C parity: paramVal::setUInt32 (paramVal.cpp:200-208) flips
        // defined+value_changed on the first set even when the resulting
        // value is 0 — driver code commonly does `setUIntDigitalParam(idx, 0, mask)`
        // to publish an initial zero state and expects the callback to fire.
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("V", ParamType::UInt32Digital).unwrap();
        pl.set_uint32(idx, 0, 0, 0xFFFF, 0).unwrap();
        assert!(pl.is_param_defined(idx, 0).unwrap());
        assert!(pl.take_changed_single(idx, 0).unwrap());
        assert_eq!(pl.get_uint32_strict(idx, 0).unwrap(), 0);
    }

    #[test]
    fn set_string_first_write_empty_flips_defined() {
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("V", ParamType::Octet).unwrap();
        pl.set_string(idx, 0, String::new()).unwrap();
        assert!(pl.is_param_defined(idx, 0).unwrap());
        assert_eq!(pl.get_string_strict(idx, 0).unwrap(), "");
    }

    #[test]
    fn set_enum_index_first_write_to_default_index_flips_defined() {
        // C parity: enum index is stored through paramVal::setInteger,
        // which gates on `!isDefined() || data != value` — a first set
        // to index 0 (the type default) must still flip defined and
        // value_changed so the initial selection fires its I/O Intr.
        // A freshly created Enum param carries one sentinel choice and
        // is undefined; set_enum_index(.., 0) must define it.
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("MODE", ParamType::Enum).unwrap();
        assert!(!pl.is_param_defined(idx, 0).unwrap());
        pl.set_enum_index(idx, 0, 0).unwrap();
        assert!(pl.is_param_defined(idx, 0).unwrap());
        assert!(pl.take_changed_single(idx, 0).unwrap());
    }

    #[test]
    fn set_param_status_only_change_marks_value_changed() {
        // C paramVal::setStatus (paramVal.cpp:71-78) calls setValueChanged()
        // on a status transition with no value change, so the standard
        // setParamStatus() + callParamCallbacks() alarm-push delivers an
        // I/O Intr. The param value itself is never touched here.
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("S", ParamType::Int32).unwrap();
        let _ = pl.take_changed(0).unwrap();
        pl.set_param_status(idx, 0, AsynStatus::Timeout, 0, 0)
            .unwrap();
        let changed = pl.take_changed(0).unwrap();
        assert!(
            changed.contains(&idx),
            "a status-only transition must mark the param value_changed"
        );
    }

    #[test]
    fn set_param_alarm_only_change_marks_value_changed() {
        // C setAlarmStatus / setAlarmSeverity (paramVal.cpp:84-104) also
        // call setValueChanged() on change — status can stay Success.
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("S", ParamType::Float64).unwrap();
        let _ = pl.take_changed(0).unwrap();
        pl.set_param_status(idx, 0, AsynStatus::Success, 7, 2)
            .unwrap();
        assert!(
            pl.take_changed_single(idx, 0).unwrap(),
            "an alarm-status/severity-only transition must mark the param value_changed"
        );
    }

    #[test]
    fn set_param_status_change_forces_full_uint32_mask() {
        // C setStatus on a UInt32Digital param forces uInt32CallbackMask =
        // 0xFFFFFFFF (paramVal.cpp:76) so every subscriber bit fires.
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("U", ParamType::UInt32Digital).unwrap();
        let _ = pl.take_changed(0).unwrap();
        pl.set_param_status(idx, 0, AsynStatus::Error, 0, 0)
            .unwrap();
        assert!(pl.take_changed_single(idx, 0).unwrap());
        assert_eq!(
            pl.get_uint32_interrupt_mask(idx, 0).unwrap(),
            0xFFFF_FFFF,
            "a UInt32Digital status change must force the full callback mask"
        );
    }

    #[test]
    fn set_uint32_force_interrupt_mask_on_unchanged_value_notifies() {
        // C paramVal::setUInt32 (paramVal.cpp:220-224): a non-zero
        // `interruptMask` ORs those bits into uInt32CallbackMask AND calls
        // setValueChanged() UNCONDITIONALLY — even when the merged value is
        // identical to the stored value. This is the setUIntDigitalParam
        // interruptMask overload (asynPortDriver.cpp:1381): a driver forces
        // an I/O Intr on specific bits without changing the value.
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("U", ParamType::UInt32Digital).unwrap();
        // Define the param at 0x05 and drain the resulting change + mask.
        pl.set_uint32(idx, 0, 0x05, 0x0F, 0).unwrap();
        let _ = pl.take_changed(0).unwrap();
        let _ = pl.take_uint32_interrupt_mask(idx, 0).unwrap();
        // Re-set the SAME value (no value-bit change) but force bit 0x02.
        pl.set_uint32(idx, 0, 0x05, 0x0F, 0x02).unwrap();
        assert_eq!(
            pl.get_uint32(idx, 0).unwrap(),
            0x05,
            "a forced-interrupt-only set must not change the stored value"
        );
        assert!(
            pl.take_changed(0).unwrap().contains(&idx),
            "a forced interruptMask must mark value_changed even on an unchanged value"
        );
        assert_eq!(
            pl.take_uint32_interrupt_mask(idx, 0).unwrap(),
            0x02,
            "the forced interruptMask bits must land in the callback mask"
        );
    }

    #[test]
    fn set_uint32_accumulates_callback_mask_across_sets() {
        // C `uInt32CallbackMask |= (uival ^ newValue)` (paramVal.cpp:215):
        // two setUInt32 calls before one callParamCallbacks must accumulate
        // the union of changed bits, not keep only the last set's bits.
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("U", ParamType::UInt32Digital).unwrap();
        pl.set_uint32(idx, 0, 0x01, 0x01, 0).unwrap(); // define + change bit 0
        pl.set_uint32(idx, 0, 0x02, 0x02, 0).unwrap(); // change bit 1
        assert_eq!(
            pl.get_uint32_interrupt_mask(idx, 0).unwrap(),
            0x03,
            "callback mask must accumulate the union of changed bits since the last flush"
        );
    }

    #[test]
    fn take_uint32_interrupt_mask_reads_and_resets() {
        // C uint32Callback resets uInt32CallbackMask = 0 after firing
        // (asynPortDriver.cpp:855).
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("U", ParamType::UInt32Digital).unwrap();
        pl.set_uint32(idx, 0, 0x05, 0x0F, 0).unwrap();
        assert_eq!(pl.take_uint32_interrupt_mask(idx, 0).unwrap(), 0x05);
        assert_eq!(
            pl.get_uint32_interrupt_mask(idx, 0).unwrap(),
            0,
            "callback mask must be reset to 0 after take"
        );
    }

    #[test]
    fn set_param_status_no_change_does_not_mark_value_changed() {
        // C gates on `status_ != status` etc., so re-asserting the
        // existing status/alarm (the fresh-param default Success/0/0) is a
        // no-op and must NOT mark the param dirty.
        let mut pl = ParamList::new(1, false);
        let idx = pl.create_param("S", ParamType::Int32).unwrap();
        let _ = pl.take_changed(0).unwrap();
        pl.set_param_status(idx, 0, AsynStatus::Success, 0, 0)
            .unwrap();
        assert!(
            !pl.take_changed_single(idx, 0).unwrap(),
            "re-asserting the same status/alarm must not mark the param value_changed"
        );
    }
}
