//! Queue Position Modeling for limit order book execution.
//!
//! Models the dynamics of queue position for limit orders using:
//! - Poisson arrival process (new orders joining behind)
//! - Exponential cancellation process (orders ahead leaving)
//! - FIFO fill semantics (orders filled from front of queue)
//!
//! Provides both a stateful streaming model (QueueModel) for real-time
//! tracking and a standalone function for one-shot probability computation.
//!
//! Key formulas:
//! - Net drain rate: cancel_rate + fill_rate - arrival_rate (for queue ahead)
//! - Fill probability: P(fill within T) = 1 - exp(-drain_rate * T / position)
//! - Expected fill time: position / drain_rate
//!
//! All math from scratch in Rust -- no external crate dependencies beyond PyO3.

use pyo3::prelude::*;
use std::sync::Mutex;

// ===========================================================================
// Frozen result types
// ===========================================================================

/// Result of a queue fill probability calculation.
#[pyclass(frozen)]
#[derive(Debug, Clone)]
pub struct FillProbResult {
    /// Probability of being filled within the time horizon.
    #[pyo3(get)]
    pub probability: f64,
    /// Expected time until fill (seconds or periods).
    #[pyo3(get)]
    pub expected_time: f64,
    /// Current estimated queue position.
    #[pyo3(get)]
    pub queue_position: f64,
}

#[pymethods]
impl FillProbResult {
    fn __repr__(&self) -> String {
        format!(
            "FillProbResult(prob={:.4}, expected_time={:.2}, position={:.1})",
            self.probability, self.expected_time, self.queue_position
        )
    }
}

// ===========================================================================
// QueueModel (stateful, Mutex-wrapped)
// ===========================================================================

/// Internal state of the queue model.
struct QueueInner {
    /// Current estimated queue size at this price level.
    queue_size: f64,
    /// Estimated number of orders ahead of us in the queue.
    orders_ahead: f64,
    /// Estimated arrival rate (orders per period joining the queue).
    arrival_rate: f64,
    /// Estimated cancel rate (orders per period leaving the queue via cancel).
    cancel_rate: f64,
    /// Cumulative arrivals observed.
    total_arrived: f64,
    /// Cumulative cancellations observed.
    total_cancelled: f64,
    /// Cumulative fills observed.
    total_filled: f64,
    /// Number of book updates processed.
    n_updates: u64,
    /// Cumulative elapsed time for rate estimation.
    total_time: f64,
}

/// Queue position model for limit order fill probability estimation.
///
/// Tracks the dynamics of a limit order's position in the queue using
/// Poisson arrivals and exponential cancellations. Updates rate estimates
/// from orderbook snapshots and computes fill probabilities.
///
/// Example:
///     model = QueueModel(initial_queue=500.0, orders_ahead=200.0)
///     model.update_book(480.0, 10.0, 25.0, 5.0)
///     prob = model.fill_probability(60.0)  # prob of fill in 60 seconds
#[pyclass]
pub struct QueueModel {
    inner: Mutex<QueueInner>,
}

#[pymethods]
impl QueueModel {
    /// Create a new queue model.
    ///
    /// Args:
    ///     initial_queue: Total queue size at the price level.
    ///     orders_ahead: Number of orders ahead of our order in the queue.
    #[new]
    #[pyo3(signature = (initial_queue, orders_ahead))]
    fn new(initial_queue: f64, orders_ahead: f64) -> PyResult<Self> {
        crate::auth::require_ultra()
            .map_err(|e| pyo3::exceptions::PyPermissionError::new_err(e.to_string()))?;

        if !initial_queue.is_finite() || initial_queue < 0.0 {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "initial_queue must be a non-negative finite number",
            ));
        }
        if !orders_ahead.is_finite() || orders_ahead < 0.0 {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "orders_ahead must be a non-negative finite number",
            ));
        }

        let orders_ahead = orders_ahead.min(initial_queue);

        Ok(Self {
            inner: Mutex::new(QueueInner {
                queue_size: initial_queue,
                orders_ahead,
                arrival_rate: 0.0,
                cancel_rate: 0.0,
                total_arrived: 0.0,
                total_cancelled: 0.0,
                total_filled: 0.0,
                n_updates: 0,
                total_time: 0.0,
            }),
        })
    }

    /// Update the model with new orderbook data.
    ///
    /// Processes observed arrivals, cancellations, and fills to update
    /// the running rate estimates using exponential moving averages.
    ///
    /// Args:
    ///     queue_size: Current total queue size at the price level.
    ///     arrived: Number of new orders that arrived since last update.
    ///     cancelled: Number of orders that were cancelled since last update.
    ///     filled: Number of orders that were filled since last update.
    #[pyo3(signature = (queue_size, arrived, cancelled, filled))]
    fn update_book(
        &self,
        queue_size: f64,
        arrived: f64,
        cancelled: f64,
        filled: f64,
    ) -> PyResult<()> {
        crate::auth::require_ultra()
            .map_err(|e| pyo3::exceptions::PyPermissionError::new_err(e.to_string()))?;

        if !queue_size.is_finite() || queue_size < 0.0 {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "queue_size must be a non-negative finite number",
            ));
        }
        if !arrived.is_finite() || arrived < 0.0 {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "arrived must be a non-negative finite number",
            ));
        }
        if !cancelled.is_finite() || cancelled < 0.0 {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "cancelled must be a non-negative finite number",
            ));
        }
        if !filled.is_finite() || filled < 0.0 {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "filled must be a non-negative finite number",
            ));
        }

        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());

        inner.n_updates += 1;
        inner.total_arrived += arrived;
        inner.total_cancelled += cancelled;
        inner.total_filled += filled;
        inner.total_time += 1.0; // each update counts as one period

        // Update queue size
        inner.queue_size = queue_size;

        // Update orders ahead: reduce by fills and cancellations (proportionally)
        // Fills come from the front of the queue (FIFO), so they directly reduce orders_ahead
        inner.orders_ahead -= filled;

        // Cancellations are assumed uniform across the queue
        if inner.queue_size + cancelled > 0.0 {
            let cancel_fraction = cancelled / (inner.queue_size + cancelled);
            inner.orders_ahead -= inner.orders_ahead * cancel_fraction;
        }

        // Clamp orders_ahead
        inner.orders_ahead = finite_or_zero(inner.orders_ahead.max(0.0).min(queue_size));

        // Update rate estimates using cumulative averages
        if inner.total_time > 0.0 {
            let t = inner.total_time;
            inner.arrival_rate = finite_or_zero(inner.total_arrived / t);
            inner.cancel_rate = finite_or_zero(inner.total_cancelled / t);
        }

        Ok(())
    }

    /// Estimate the probability of being filled within a given time horizon.
    ///
    /// Uses the current rate estimates to model the queue as a birth-death
    /// process. The drain rate (cancellations + fills ahead) determines
    /// how quickly orders ahead of us are removed.
    ///
    /// P(fill within T) = 1 - exp(-drain_rate * T / max(orders_ahead, 1))
    ///
    /// Args:
    ///     time_horizon: Time horizon in periods (same units as update frequency).
    ///
    /// Returns:
    ///     Probability of fill within the time horizon (0 to 1).
    fn fill_probability(&self, time_horizon: f64) -> PyResult<f64> {
        crate::auth::require_ultra()
            .map_err(|e| pyo3::exceptions::PyPermissionError::new_err(e.to_string()))?;

        if !time_horizon.is_finite() || time_horizon < 0.0 {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "time_horizon must be a non-negative finite number",
            ));
        }

        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());

        if inner.orders_ahead <= 0.0 {
            return Ok(1.0); // We're at the front, fill is imminent
        }

        let prob = fill_probability_impl(
            inner.queue_size,
            inner.orders_ahead,
            inner.arrival_rate,
            inner.cancel_rate,
            time_horizon,
        );

        Ok(finite_or_zero(prob))
    }

    /// Estimate the expected time until fill.
    ///
    /// E[fill_time] = orders_ahead / drain_rate
    ///
    /// Returns:
    ///     Expected time until fill in periods. Returns f64::MAX if drain rate is zero.
    fn expected_fill_time(&self) -> PyResult<f64> {
        crate::auth::require_ultra()
            .map_err(|e| pyo3::exceptions::PyPermissionError::new_err(e.to_string()))?;

        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());

        if inner.orders_ahead <= 0.0 {
            return Ok(0.0); // Already at front
        }

        let drain_rate = compute_drain_rate(
            inner.queue_size,
            inner.arrival_rate,
            inner.cancel_rate,
        );

        if drain_rate <= 1e-15 {
            return Ok(f64::MAX); // Queue not draining
        }

        Ok(finite_or_zero(inner.orders_ahead / drain_rate))
    }

    /// Get the current estimated arrival rate.
    fn arrival_rate(&self) -> f64 {
        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        finite_or_zero(inner.arrival_rate)
    }

    /// Get the current estimated cancel rate.
    fn cancel_rate(&self) -> f64 {
        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        finite_or_zero(inner.cancel_rate)
    }

    /// Get the current estimated queue position (orders ahead).
    fn queue_position(&self) -> f64 {
        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        finite_or_zero(inner.orders_ahead)
    }

    /// Get a full fill probability result with position and timing.
    fn fill_prob_result(&self, time_horizon: f64) -> PyResult<FillProbResult> {
        crate::auth::require_ultra()
            .map_err(|e| pyo3::exceptions::PyPermissionError::new_err(e.to_string()))?;

        if !time_horizon.is_finite() || time_horizon < 0.0 {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "time_horizon must be a non-negative finite number",
            ));
        }

        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());

        let prob = fill_probability_impl(
            inner.queue_size,
            inner.orders_ahead,
            inner.arrival_rate,
            inner.cancel_rate,
            time_horizon,
        );

        let drain_rate = compute_drain_rate(
            inner.queue_size,
            inner.arrival_rate,
            inner.cancel_rate,
        );

        let expected_time = if inner.orders_ahead <= 0.0 {
            0.0
        } else if drain_rate > 1e-15 {
            inner.orders_ahead / drain_rate
        } else {
            f64::MAX
        };

        Ok(FillProbResult {
            probability: finite_or_zero(prob),
            expected_time: finite_or_zero(expected_time),
            queue_position: finite_or_zero(inner.orders_ahead),
        })
    }

    fn __repr__(&self) -> String {
        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        format!(
            "QueueModel(queue={:.0}, ahead={:.0}, arrival={:.2}/t, cancel={:.2}/t, updates={})",
            inner.queue_size,
            inner.orders_ahead,
            inner.arrival_rate,
            inner.cancel_rate,
            inner.n_updates
        )
    }
}

// ===========================================================================
// Private helpers
// ===========================================================================

/// Clamp a value to finite, returning 0.0 for NaN/Inf.
#[inline]
fn finite_or_zero(v: f64) -> f64 {
    if v.is_finite() { v } else { 0.0 }
}

/// Compute the effective drain rate for orders ahead in the queue.
///
/// The drain rate captures how fast orders ahead of us leave:
/// - Fills remove orders from the front (FIFO)
/// - Cancellations remove orders proportionally across the queue
///
/// We estimate the fill rate from the cancellation rate:
/// fill_rate ~ max(cancel_rate * 0.3, 1e-6) as a conservative floor,
/// since fills are correlated with but typically less frequent than cancels.
fn compute_drain_rate(
    queue_size: f64,
    arrival_rate: f64,
    cancel_rate: f64,
) -> f64 {
    if queue_size <= 0.0 {
        return 0.0;
    }

    // Cancel drain: fraction of cancels that hit orders ahead (proportional)
    let cancel_drain = cancel_rate;

    // Fill drain: fills come from the front of the queue
    // Estimate fill rate from observed cancel rate (fills are typically a fraction of cancels)
    let fill_drain = cancel_rate * 0.3 + 1e-6;

    // Arrivals join behind us in FIFO, but a high arrival rate relative to
    // cancel+fill rate signals a sticky queue where our relative position
    // drains more slowly.  Scale drain by queue_turnover fraction.
    let gross_drain = cancel_drain + fill_drain;
    let turnover = if (gross_drain + arrival_rate) > 0.0 {
        gross_drain / (gross_drain + arrival_rate)
    } else {
        1.0
    };
    let net_drain = gross_drain * turnover;

    finite_or_zero(net_drain.max(0.0))
}

/// Compute fill probability for given parameters.
fn fill_probability_impl(
    queue_size: f64,
    orders_ahead: f64,
    arrival_rate: f64,
    cancel_rate: f64,
    time_horizon: f64,
) -> f64 {
    if orders_ahead <= 0.0 {
        return 1.0;
    }
    if time_horizon <= 0.0 {
        return 0.0;
    }

    let drain_rate = compute_drain_rate(queue_size, arrival_rate, cancel_rate);

    if drain_rate <= 1e-15 {
        return 0.0;
    }

    // Model: queue ahead is a Poisson process draining at drain_rate
    // Time to drain `orders_ahead` positions ~ orders_ahead / drain_rate
    // P(fill in T) = 1 - exp(-drain_rate * T / orders_ahead)
    let exponent = drain_rate * time_horizon / orders_ahead;
    let prob = 1.0 - (-exponent).exp();

    finite_or_zero(prob.clamp(0.0, 1.0))
}

// ===========================================================================
// Standalone function
// ===========================================================================

/// Compute the fill probability for a limit order given queue parameters.
///
/// Standalone function (no stateful model needed). Models the queue as a
/// Poisson process with given arrival and cancel rates.
///
/// Args:
///     queue_size: Total orders at this price level.
///     position: Number of orders ahead of us.
///     arrival_rate: Rate of new order arrivals (per period).
///     cancel_rate: Rate of order cancellations (per period).
///     time_horizon: Time horizon for the fill probability.
///
/// Returns:
///     Probability of being filled within the time horizon (0 to 1).
#[pyfunction]
#[pyo3(signature = (queue_size, position, arrival_rate, cancel_rate, time_horizon))]
pub fn queue_fill_prob(
    queue_size: f64,
    position: f64,
    arrival_rate: f64,
    cancel_rate: f64,
    time_horizon: f64,
) -> PyResult<f64> {
    crate::auth::require_ultra()
        .map_err(|e| pyo3::exceptions::PyPermissionError::new_err(e.to_string()))?;

    if !queue_size.is_finite() || queue_size < 0.0 {
        return Err(pyo3::exceptions::PyValueError::new_err(
            "queue_size must be a non-negative finite number",
        ));
    }
    if !position.is_finite() || position < 0.0 {
        return Err(pyo3::exceptions::PyValueError::new_err(
            "position must be a non-negative finite number",
        ));
    }
    if !arrival_rate.is_finite() || arrival_rate < 0.0 {
        return Err(pyo3::exceptions::PyValueError::new_err(
            "arrival_rate must be a non-negative finite number",
        ));
    }
    if !cancel_rate.is_finite() || cancel_rate < 0.0 {
        return Err(pyo3::exceptions::PyValueError::new_err(
            "cancel_rate must be a non-negative finite number",
        ));
    }
    if !time_horizon.is_finite() || time_horizon < 0.0 {
        return Err(pyo3::exceptions::PyValueError::new_err(
            "time_horizon must be a non-negative finite number",
        ));
    }

    let prob = fill_probability_impl(queue_size, position, arrival_rate, cancel_rate, time_horizon);
    Ok(finite_or_zero(prob))
}

/// Register queue model types and functions in the Python module.
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_class::<QueueModel>()?;
    m.add_class::<FillProbResult>()?;
    m.add_function(wrap_pyfunction!(queue_fill_prob, m)?)?;
    Ok(())
}

// ===========================================================================
// Tests
// ===========================================================================

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

    #[test]
    fn test_finite_or_zero() {
        assert_eq!(finite_or_zero(1.0), 1.0);
        assert_eq!(finite_or_zero(f64::NAN), 0.0);
        assert_eq!(finite_or_zero(f64::INFINITY), 0.0);
        assert_eq!(finite_or_zero(f64::NEG_INFINITY), 0.0);
    }

    #[test]
    fn test_queue_model_creation() {
        let model = QueueModel::new(1000.0, 500.0).unwrap();
        let inner = model.inner.lock().unwrap();
        assert!((inner.queue_size - 1000.0).abs() < 1e-10);
        assert!((inner.orders_ahead - 500.0).abs() < 1e-10);
        assert_eq!(inner.n_updates, 0);
    }

    #[test]
    fn test_queue_model_clamps_ahead() {
        // orders_ahead can't exceed queue_size
        let model = QueueModel::new(100.0, 200.0).unwrap();
        let inner = model.inner.lock().unwrap();
        assert!((inner.orders_ahead - 100.0).abs() < 1e-10);
    }

    #[test]
    fn test_queue_model_invalid_inputs() {
        assert!(QueueModel::new(-1.0, 0.0).is_err());
        assert!(QueueModel::new(0.0, -1.0).is_err());
        assert!(QueueModel::new(f64::NAN, 0.0).is_err());
    }

    #[test]
    fn test_queue_model_update() {
        let model = QueueModel::new(1000.0, 500.0).unwrap();
        model.update_book(980.0, 10.0, 25.0, 5.0).unwrap();

        let inner = model.inner.lock().unwrap();
        assert!((inner.queue_size - 980.0).abs() < 1e-10);
        assert!(inner.orders_ahead < 500.0); // Should have decreased
        assert_eq!(inner.n_updates, 1);
        assert!(inner.arrival_rate > 0.0);
        assert!(inner.cancel_rate > 0.0);
    }

    #[test]
    fn test_queue_model_update_invalid() {
        let model = QueueModel::new(1000.0, 500.0).unwrap();
        assert!(model.update_book(-1.0, 0.0, 0.0, 0.0).is_err());
        assert!(model.update_book(100.0, -1.0, 0.0, 0.0).is_err());
        assert!(model.update_book(100.0, 0.0, -1.0, 0.0).is_err());
        assert!(model.update_book(100.0, 0.0, 0.0, -1.0).is_err());
    }

    #[test]
    fn test_fill_probability_at_front() {
        let model = QueueModel::new(1000.0, 0.0).unwrap();
        let prob = model.fill_probability(10.0).unwrap();
        assert!((prob - 1.0).abs() < 1e-10);
    }

    #[test]
    fn test_fill_probability_increases_with_time() {
        let model = QueueModel::new(1000.0, 200.0).unwrap();
        // Feed some data to establish rates
        model.update_book(980.0, 10.0, 20.0, 10.0).unwrap();
        model.update_book(960.0, 8.0, 22.0, 6.0).unwrap();

        let p1 = model.fill_probability(10.0).unwrap();
        let p2 = model.fill_probability(100.0).unwrap();
        assert!(
            p2 >= p1 - 1e-10,
            "longer horizon should give higher or equal probability: p1={}, p2={}",
            p1,
            p2
        );
    }

    #[test]
    fn test_fill_probability_zero_horizon() {
        let model = QueueModel::new(1000.0, 200.0).unwrap();
        let prob = model.fill_probability(0.0).unwrap();
        assert!(prob < 1e-10);
    }

    #[test]
    fn test_expected_fill_time_at_front() {
        let model = QueueModel::new(1000.0, 0.0).unwrap();
        let time = model.expected_fill_time().unwrap();
        assert!((time - 0.0).abs() < 1e-10);
    }

    #[test]
    fn test_expected_fill_time_with_data() {
        let model = QueueModel::new(1000.0, 200.0).unwrap();
        model.update_book(980.0, 10.0, 20.0, 10.0).unwrap();
        model.update_book(960.0, 8.0, 22.0, 6.0).unwrap();
        let time = model.expected_fill_time().unwrap();
        assert!(time > 0.0);
        assert!(time.is_finite());
    }

    #[test]
    fn test_arrival_cancel_rate_accessors() {
        let model = QueueModel::new(1000.0, 500.0).unwrap();
        assert_eq!(model.arrival_rate(), 0.0);
        assert_eq!(model.cancel_rate(), 0.0);

        model.update_book(980.0, 10.0, 20.0, 5.0).unwrap();
        assert!(model.arrival_rate() > 0.0);
        assert!(model.cancel_rate() > 0.0);
    }

    #[test]
    fn test_queue_position_accessor() {
        let model = QueueModel::new(1000.0, 500.0).unwrap();
        assert!((model.queue_position() - 500.0).abs() < 1e-10);
    }

    #[test]
    fn test_fill_prob_result() {
        let model = QueueModel::new(1000.0, 200.0).unwrap();
        model.update_book(980.0, 10.0, 20.0, 10.0).unwrap();
        let result = model.fill_prob_result(60.0).unwrap();
        assert!(result.probability >= 0.0 && result.probability <= 1.0);
        assert!(result.expected_time >= 0.0);
        assert!(result.queue_position >= 0.0);
    }

    #[test]
    fn test_queue_fill_prob_standalone() {
        let prob = queue_fill_prob(1000.0, 200.0, 10.0, 20.0, 60.0).unwrap();
        assert!(prob >= 0.0 && prob <= 1.0);
        assert!(prob > 0.0); // With nonzero cancel rate, should have some fill probability
    }

    #[test]
    fn test_queue_fill_prob_at_front() {
        let prob = queue_fill_prob(1000.0, 0.0, 10.0, 20.0, 60.0).unwrap();
        assert!((prob - 1.0).abs() < 1e-10);
    }

    #[test]
    fn test_queue_fill_prob_zero_rates() {
        let prob = queue_fill_prob(1000.0, 200.0, 0.0, 0.0, 60.0).unwrap();
        // With zero cancel rate, drain rate is still > 0 due to floor
        assert!(prob >= 0.0 && prob <= 1.0);
    }

    #[test]
    fn test_queue_fill_prob_invalid() {
        assert!(queue_fill_prob(-1.0, 0.0, 0.0, 0.0, 0.0).is_err());
        assert!(queue_fill_prob(100.0, -1.0, 0.0, 0.0, 0.0).is_err());
        assert!(queue_fill_prob(100.0, 0.0, -1.0, 0.0, 0.0).is_err());
        assert!(queue_fill_prob(100.0, 0.0, 0.0, -1.0, 0.0).is_err());
        assert!(queue_fill_prob(100.0, 0.0, 0.0, 0.0, -1.0).is_err());
        assert!(queue_fill_prob(f64::NAN, 0.0, 0.0, 0.0, 0.0).is_err());
    }

    #[test]
    fn test_queue_fill_prob_increases_with_cancel_rate() {
        let p1 = queue_fill_prob(1000.0, 200.0, 5.0, 10.0, 60.0).unwrap();
        let p2 = queue_fill_prob(1000.0, 200.0, 5.0, 50.0, 60.0).unwrap();
        assert!(
            p2 > p1,
            "higher cancel rate should increase fill probability: p1={}, p2={}",
            p1,
            p2
        );
    }

    #[test]
    fn test_queue_fill_prob_decreases_with_position() {
        let p1 = queue_fill_prob(1000.0, 50.0, 5.0, 20.0, 60.0).unwrap();
        let p2 = queue_fill_prob(1000.0, 500.0, 5.0, 20.0, 60.0).unwrap();
        assert!(
            p1 > p2,
            "closer to front should have higher probability: p1={}, p2={}",
            p1,
            p2
        );
    }

    #[test]
    fn test_compute_drain_rate() {
        let dr = compute_drain_rate(1000.0, 10.0, 20.0);
        assert!(dr > 0.0);
        assert!(dr.is_finite());
    }

    #[test]
    fn test_compute_drain_rate_empty_queue() {
        let dr = compute_drain_rate(0.0, 10.0, 20.0);
        assert!((dr - 0.0).abs() < 1e-10);
    }

    #[test]
    fn test_multiple_updates_improve_estimate() {
        let model = QueueModel::new(1000.0, 500.0).unwrap();
        // Simulate several updates with consistent rates
        for _ in 0..10 {
            model.update_book(1000.0, 15.0, 20.0, 8.0).unwrap();
        }
        let rate = model.arrival_rate();
        assert!((rate - 15.0).abs() < 1e-10, "arrival rate should be 15.0, got {}", rate);
    }

    #[test]
    fn test_repr() {
        let model = QueueModel::new(1000.0, 500.0).unwrap();
        let repr = model.__repr__();
        assert!(repr.contains("QueueModel"));
        assert!(repr.contains("1000"));
    }
}
