//! loglik_optimizer::adapter — bridge between `LogLikelihood` and Argmin.
//!
//! Purpose
//! -------
//! Expose a thin adapter that wraps a user-provided [`LogLikelihood`] as an
//! `argmin` optimization problem. The adapter converts maximization of a
//! log-likelihood `ℓ(θ)` into minimization of a cost `c(θ) = -ℓ(θ)` and
//! wires analytic or finite-difference gradients into Argmin’s
//! [`CostFunction`] and [`Gradient`] traits.
//!
//! Key behaviors
//! -------------
//! - Evaluate the user’s log-likelihood and present it to Argmin as a
//!   **negative** cost (`c(θ) = -ℓ(θ)`).
//! - Provide the gradient of the cost either by negating an analytic
//!   log-likelihood gradient or by finite differencing the cost when no
//!   analytic gradient is available.
//! - Enforce basic gradient validation (shape, finiteness) and propagate
//!   user-level [`OptError`] values through Argmin’s error channel.
//!
//! Invariants & assumptions
//! ------------------------
//! - The `LogLikelihood` implementation’s `value` method returns the
//!   **log-likelihood** `ℓ(θ)` (not the cost) and must treat invalid inputs
//!   as recoverable [`OptError`] values, not panics.
//! - Any analytic gradient exposed by [`LogLikelihood::grad`] is the gradient
//!   of the log-likelihood (`∇ℓ(θ)`); this adapter is responsible for
//!   flipping the sign to obtain the cost gradient (`∇c(θ) = -∇ℓ(θ)`).
//! - Parameter vectors and gradients use the canonical optimizer aliases
//!   [`Theta`] and [`Grad`]; cost values use [`Cost`]. All are assumed to be
//!   finite whenever optimization proceeds.
//!
//! Conventions
//! -----------
//! - Cost is always defined as `c(θ) = -ℓ(θ)`, so Argmin minimizes cost while
//!   user-facing APIs still speak in terms of maximizing log-likelihood.
//! - Gradient evaluation prefers analytic gradients when available; if
//!   [`LogLikelihood::grad`] returns [`OptError::GradientNotImplemented`], the
//!   adapter computes a finite-difference gradient of the **cost**.
//! - Errors generated by the user’s log-likelihood or by validation helpers
//!   are expressed as crate-level [`OptError`] values and then wrapped inside
//!   [`argmin::core::Error`] for the trait implementations.
//! - The adapter does not perform any logging or I/O and never uses `unsafe`;
//!   all failures are reported via [`Result`].
//!
//! Downstream usage
//! ----------------
//! - Optimizer runners construct an [`ArgMinAdapter`] around a concrete model
//!   implementing [`LogLikelihood`] and pass it, together with initial
//!   parameters, into Argmin solvers (e.g., L-BFGS).
//! - Higher-level APIs (e.g., Python bindings) never interact with this
//!   module directly; they work with [`LogLikelihood`], [`MLEOptions`], and
//!   the normalized [`OptimOutcome`] instead.
//! - This module is the only place where Argmin’s [`CostFunction`] and
//!   [`Gradient`] traits are implemented for user models; other optimizer
//!   components depend on this adapter rather than on Argmin directly.
//!
//! Testing notes
//! -------------
//! - Unit tests in this module verify:
//!   - The sign convention `c(θ) = -ℓ(θ)` in [`CostFunction::cost`].
//!   - Correct use of analytic gradients (including sign flip and validation).
//!   - Finite-difference fallback behavior when gradients are not implemented,
//!     including error propagation from the underlying log-likelihood.
//! - Integration tests in the optimizer layer exercise this adapter
//!   implicitly via full optimization runs on toy models.
use crate::optimization::{
    errors::OptError,
    loglik_optimizer::{
        finite_diff::run_fd_diff,
        traits::LogLikelihood,
        types::{Cost, Grad, Theta},
        validation::validate_grad,
    },
};
use argmin::core::{CostFunction, Error, Gradient};
use finitediff::FiniteDiff;
use std::cell::RefCell;

/// ArgMinAdapter — expose a `LogLikelihood` as an Argmin optimization problem.
///
/// Purpose
/// -------
/// Wrap a user-implemented [`LogLikelihood`] together with its data so it
/// can be consumed by Argmin solvers via the [`CostFunction`] and
/// [`Gradient`] traits. The adapter handles sign conventions (`ℓ` vs cost)
/// and gradient strategy (analytic vs finite difference) on behalf of the
/// rest of the optimizer.
///
/// Key behaviors
/// -------------
/// - Calls the underlying [`LogLikelihood::value`] to obtain the log-likelihood
///   `ℓ(θ)` and presents `c(θ) = -ℓ(θ)` to Argmin as the cost.
/// - Uses [`LogLikelihood::grad`] when implemented, negating and validating the
///   resulting gradient before returning it as the cost gradient.
/// - Falls back to finite differencing of the cost when no analytic gradient
///   is provided, using central differences first and forward differences as
///   a validated fallback.
///
/// Parameters
/// ----------
/// Constructed via [`ArgMinAdapter::new`]:
/// - `f`: `&'a F`
///   Borrowed model implementing [`LogLikelihood`] for some data type.
/// - `data`: `&'a F::Data`
///   Borrowed data associated with the model; treated as read-only by the
///   optimizer.
///
/// Fields
/// ------
/// - `f`: `&'a F`
///   Underlying log-likelihood implementation used for all evaluations.
/// - `data`: `&'a F::Data`
///   User data passed through to `value` and `grad` calls.
///
/// Invariants
/// ----------
/// - The references `f` and `data` must remain valid for the lifetime `'a`
///   of the adapter; callers are responsible for ensuring this.
/// - `LogLikelihood::value` must never panic for user-level input errors;
///   it should return descriptive [`OptError`] values instead.
/// - When [`LogLikelihood::grad`] returns `Ok(grad)`, the gradient must be
///   dimensionally consistent with [`Theta`] and contain only finite values;
///   otherwise validation will fail.
///
/// Performance
/// -----------
/// - The adapter itself is a small, cheaply-cloned struct containing only
///   two references; cloning does not copy model or data.
/// - Finite-difference gradients incur multiple calls to `cost` per
///   evaluation; analytic gradients are preferred whenever available.
///
/// Notes
/// -----
/// - This type is generic over any `F: LogLikelihood` and is typically
///   constructed locally inside optimization routines; it is not intended
///   as part of the public API surface for end users.
/// - All Argmin-facing behavior (cost, gradient) should go through this
///   adapter so that sign conventions and validation remain centralized.
#[derive(Debug, Clone)]
pub struct ArgMinAdapter<'a, F: LogLikelihood> {
    pub f: &'a F,
    pub data: &'a F::Data,
}

impl<'a, F: LogLikelihood> CostFunction for ArgMinAdapter<'a, F> {
    type Param = Theta;
    type Output = Cost;

    /// Evaluate the cost `c(θ) = -ℓ(θ)` for the wrapped log-likelihood.
    ///
    /// Parameters
    /// ----------
    /// - `theta`: `&Self::Param` (`&Theta`)
    ///   Parameter vector at which the log-likelihood is to be evaluated.
    ///   The underlying [`LogLikelihood`] implementation is responsible for
    ///   checking that `theta` has the expected dimension and contains only
    ///   finite values.
    ///
    /// Returns
    /// -------
    /// `Result<Self::Output, Error>` (`Result<Cost, argmin::core::Error>`)
    ///   - `Ok(cost)` when the log-likelihood evaluation succeeds and the
    ///     returned value is finite, with `cost = -ℓ(θ)`.
    ///   - `Err(e)` when either the log-likelihood itself fails or the
    ///     resulting value is non-finite.
    ///
    /// Errors
    /// ------
    /// - Wraps any [`OptError`] produced by [`LogLikelihood::value`] into
    ///   [`argmin::core::Error`] via the standard error conversion used by `?`.
    /// - Wraps [`OptError::NonFiniteCost`] into [`argmin::core::Error`]
    ///   when the returned log-likelihood value is `NaN` or infinite.
    ///
    /// Panics
    /// ------
    /// - Never panics; all failures are reported via the `Result` return type.
    ///
    /// Safety
    /// ------
    /// - No `unsafe` code is used.
    ///
    /// Notes
    /// -----
    /// - This method *always* negates the log-likelihood, so callers and
    ///   higher-level APIs should work exclusively in terms of log-likelihood
    ///   values and leave cost handling to the adapter.
    /// - Finiteness is enforced explicitly via `is_finite`; non-finite values
    ///   are treated as hard errors, not as large penalties.
    ///
    /// Examples
    /// --------
    /// ```ignore
    /// use rust_timeseries::optimization::loglik_optimizer::adapter::ArgMinAdapter;
    /// use rust_timeseries::optimization::loglik_optimizer::traits::LogLikelihood;
    ///
    /// struct SimpleLL;
    ///
    /// impl LogLikelihood for SimpleLL {
    ///     type Data = ();
    ///     fn value(&self, theta: &Theta, _data: &Self::Data) -> OptResult<Cost> {
    ///         Ok(theta.iter().copied().sum())
    ///     }
    ///     fn check(&self, _theta: &Theta, _data: &Self::Data) -> OptResult<()> {
    ///         Ok(())
    ///     }
    /// }
    ///
    /// let model = SimpleLL;
    /// let data = ();
    /// let adapter = ArgMinAdapter::new(&model, &data);
    /// let theta = Theta::from(vec![1.0_f64, 2.0_f64]);
    /// let cost = adapter.cost(&theta).unwrap();
    /// assert_eq!(cost, -3.0);
    /// ```
    fn cost(&self, theta: &Self::Param) -> Result<Self::Output, Error> {
        let output = self.f.value(theta, self.data)?;
        if !output.is_finite() {
            return Err((OptError::NonFiniteCost { value: output }).into());
        }
        Ok(-output)
    }
}

impl<'a, F: LogLikelihood> Gradient for ArgMinAdapter<'a, F> {
    type Param = Theta;
    type Gradient = Grad;

    /// Evaluate the gradient of the cost at `θ`.
    ///
    /// Parameters
    /// ----------
    /// - `theta`: `&Self::Param` (`&Theta`)
    ///   Parameter vector at which the cost gradient is to be evaluated.
    ///
    /// Returns
    /// -------
    /// `Result<Self::Gradient, Error>` (`Result<Grad, argmin::core::Error>`)
    ///   - `Ok(grad)` containing the gradient of the cost `c(θ)` at the given
    ///     point, with the same dimension as `theta`.
    ///   - `Err(e)` if analytic gradient evaluation fails, cost evaluation
    ///     fails during finite differencing, or gradient validation fails.
    ///
    /// Errors
    /// ------
    /// - If [`LogLikelihood::grad`] returns an `Err` other than
    ///   [`OptError::GradientNotImplemented`], that `OptError` is wrapped
    ///   into [`argmin::core::Error`] and returned.
    /// - When [`LogLikelihood::grad`] is not implemented, any error raised by
    ///   the underlying cost evaluation during finite differencing is captured
    ///   and surfaced via [`run_fd_diff`], again wrapped as an Argmin
    ///   [`Error`] whose source is an [`OptError`].
    /// - Validation failures from [`validate_grad`] (dimension mismatch or
    ///   non-finite entries) are reported as [`argmin::core::Error`] values
    ///   whose source is the corresponding [`OptError`] variant.
    ///
    /// Panics
    /// ------
    /// - Never panics; failures are represented as errors rather than panics.
    ///
    /// Safety
    /// ------
    /// - No `unsafe` code is used.
    ///
    /// Notes
    /// -----
    /// - If [`LogLikelihood::grad`] returns `Ok(g)`, this method validates `g`
    ///   and returns `-g` as the cost gradient, since `c(θ) = -ℓ(θ)`.
    /// - If [`LogLikelihood::grad`] returns
    ///   [`OptError::GradientNotImplemented`], the adapter constructs a cost
    ///   closure and:
    ///   - attempts a **central** finite-difference gradient first,
    ///   - falls back to a **forward** finite-difference gradient via
    ///     [`run_fd_diff`] if a cost evaluation fails or the central gradient
    ///     fails validation.
    /// - The finite-difference closure returns `NaN` when an error occurs but
    ///   also records the first error in a shared `closure_err` cell; after
    ///   the FD routine completes, this cell is inspected and, if set, the
    ///   error is turned back into a normal `Result` via [`run_fd_diff`].
    ///
    /// Examples
    /// --------
    /// ```ignore
    /// use rust_timeseries::optimization::loglik_optimizer::adapter::ArgMinAdapter;
    /// use rust_timeseries::optimization::loglik_optimizer::traits::LogLikelihood;
    ///
    /// struct QuadLL;
    ///
    /// impl LogLikelihood for QuadLL {
    ///     type Data = ();
    ///     // ℓ(θ) = -0.5 * ||θ||², so grad ℓ(θ) = -θ.
    ///     fn value(&self, theta: &Theta, _data: &Self::Data) -> OptResult<Cost> {
    ///         Ok(-0.5 * theta.dot(theta))
    ///     }
    ///     fn check(&self, _theta: &Theta, _data: &Self::Data) -> OptResult<()> {
    ///         Ok(())
    ///     }
    ///     fn grad(&self, theta: &Theta, _data: &Self::Data) -> OptResult<Grad> {
    ///         Ok(theta.mapv(|x| -x))
    ///     }
    /// }
    ///
    /// let model = QuadLL;
    /// let data = ();
    /// let adapter = ArgMinAdapter::new(&model, &data);
    /// let theta = Theta::from(vec![1.0_f64, -2.0_f64]);
    /// let grad = adapter.gradient(&theta).unwrap();
    /// // For c(θ) = -ℓ(θ) = 0.5 * ||θ||², we have ∇c(θ) = θ.
    /// assert_eq!(grad, theta);
    /// ```
    fn gradient(&self, theta: &Self::Param) -> Result<Self::Gradient, Error> {
        let dim = theta.len();
        match self.f.grad(theta, self.data) {
            Ok(g) => {
                validate_grad(&g, dim)?;
                Ok(-g)
            }
            Err(e) => match e {
                OptError::GradientNotImplemented => {
                    let closure_err: RefCell<Option<Error>> = RefCell::new(None);
                    let cost_func = |theta: &Theta| -> f64 {
                        match self.cost(theta) {
                            Ok(val) => val,
                            Err(e) => {
                                let mut slot = closure_err.borrow_mut();
                                if slot.is_none() {
                                    *slot = Some(e);
                                }
                                f64::NAN
                            }
                        }
                    };
                    let mut fd_grad = theta.central_diff(&cost_func);
                    if closure_err.borrow().is_some() {
                        fd_grad = run_fd_diff(theta, &cost_func, &closure_err)?;
                        return Ok(fd_grad);
                    }
                    match validate_grad(&fd_grad, dim) {
                        Ok(()) => Ok(fd_grad),
                        Err(_) => {
                            fd_grad = run_fd_diff(theta, &cost_func, &closure_err)?;
                            Ok(fd_grad)
                        }
                    }
                }
                _ => Err(e.into()),
            },
        }
    }
}

impl<'a, F: LogLikelihood> ArgMinAdapter<'a, F> {
    /// Construct a new adapter over a user `LogLikelihood` and its data.
    ///
    /// Parameters
    /// ----------
    /// - `f`: `&'a F`
    ///   Borrowed reference to a model implementing [`LogLikelihood`]. The
    ///   model is treated as immutable by the optimizer layer.
    /// - `data`: `&'a F::Data`
    ///   Borrowed reference to the data associated with the model. Lifetime
    ///   `'a` must cover the entire optimization run.
    ///
    /// Returns
    /// -------
    /// `ArgMinAdapter<'a, F>`
    ///   A small wrapper struct that implements [`CostFunction`] and
    ///   [`Gradient`] for the given model and data, enabling it to be used as
    ///   an Argmin optimization problem.
    ///
    /// Errors
    /// ------
    /// - Never returns an error; this is a simple constructor.
    ///
    /// Panics
    /// ------
    /// - Never panics.
    ///
    /// Safety
    /// ------
    /// - No `unsafe` code is used. The caller is responsible for ensuring
    ///   that `f` and `data` remain valid for the lifetime `'a`.
    ///
    /// Notes
    /// -----
    /// - Typical usage is to construct an adapter once per optimization run
    ///   and pass it by value into an Argmin solver. The adapter itself is
    ///   cheap to clone.
    ///
    /// Examples
    /// --------
    /// ```ignore
    /// use rust_timeseries::optimization::loglik_optimizer::adapter::ArgMinAdapter;
    /// use rust_timeseries::optimization::loglik_optimizer::traits::LogLikelihood;
    ///
    /// struct MyLL;
    ///
    /// impl LogLikelihood for MyLL {
    ///     type Data = ();
    ///     fn value(&self, theta: &Theta, _data: &Self::Data) -> OptResult<Cost> {
    ///         Ok(theta.iter().copied().sum())
    ///     }
    ///     fn check(&self, _theta: &Theta, _data: &Self::Data) -> OptResult<()> {
    ///         Ok(())
    ///     }
    /// }
    ///
    /// let model = MyLL;
    /// let data = ();
    /// let adapter = ArgMinAdapter::new(&model, &data);
    /// // `adapter` can now be passed into an Argmin solver.
    /// ```
    pub fn new(f: &'a F, data: &'a F::Data) -> Self {
        Self { f, data }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::optimization::errors::{OptError, OptResult};
    use crate::optimization::loglik_optimizer::traits::LogLikelihood;
    use crate::optimization::loglik_optimizer::types::{Cost, Grad, Theta};
    use argmin::core::Error;

    // -------------------------------------------------------------------------
    // Scope
    // -----
    // These tests cover:
    // - Sign convention for `CostFunction::cost` (`c(θ) = -ℓ(θ)`).
    // - Validation and error handling for analytic gradients in `gradient`.
    // - Finite-difference fallback behavior when gradients are not implemented.
    // - Basic error propagation from the underlying log-likelihood into
    //   Argmin's `Error` type.
    //
    // They intentionally DO NOT cover:
    // - Detailed numerical properties of finite-difference schemes
    //   (step sizes, convergence rates).
    // - End-to-end optimizer behavior (handled in higher-level integration
    //   tests that exercise the full L-BFGS pipeline).
    // -------------------------------------------------------------------------

    /// SimpleLL — finite log-likelihood with no analytic gradient.
    ///
    /// ℓ(θ) = ∑ᵢ θᵢ, used to test cost negation and FD fallback.
    struct SimpleLL;

    impl LogLikelihood for SimpleLL {
        type Data = ();

        fn value(&self, theta: &Theta, _data: &Self::Data) -> OptResult<Cost> {
            Ok(theta.iter().copied().sum())
        }

        fn check(&self, _theta: &Theta, _data: &Self::Data) -> OptResult<()> {
            Ok(())
        }
        // `grad` uses the default `GradientNotImplemented` behavior.
    }

    /// QuadLL — quadratic log-likelihood with analytic gradient.
    ///
    /// ℓ(θ) = -0.5 * ||θ||², so ∇ℓ(θ) = -θ and ∇c(θ) = θ.
    struct QuadLL;

    impl LogLikelihood for QuadLL {
        type Data = ();

        fn value(&self, theta: &Theta, _data: &Self::Data) -> OptResult<Cost> {
            Ok(-0.5 * theta.dot(theta))
        }

        fn check(&self, _theta: &Theta, _data: &Self::Data) -> OptResult<()> {
            Ok(())
        }

        fn grad(&self, theta: &Theta, _data: &Self::Data) -> OptResult<Grad> {
            Ok(theta.mapv(|x| -x))
        }
    }

    /// QuadNoGradLL — same value as `QuadLL` but no analytic gradient.
    ///
    /// Used to force finite-difference fallback in `gradient`.
    struct QuadNoGradLL;

    impl LogLikelihood for QuadNoGradLL {
        type Data = ();

        fn value(&self, theta: &Theta, _data: &Self::Data) -> OptResult<Cost> {
            Ok(-0.5 * theta.dot(theta))
        }

        fn check(&self, _theta: &Theta, _data: &Self::Data) -> OptResult<()> {
            Ok(())
        }
        // `grad` uses the default `GradientNotImplemented`.
    }

    /// NonFiniteValueLL — always returns a non-finite log-likelihood.
    struct NonFiniteValueLL;

    impl LogLikelihood for NonFiniteValueLL {
        type Data = ();

        fn value(&self, _theta: &Theta, _data: &Self::Data) -> OptResult<Cost> {
            Ok(f64::NAN)
        }

        fn check(&self, _theta: &Theta, _data: &Self::Data) -> OptResult<()> {
            Ok(())
        }
    }

    /// BadDimGradLL — analytic gradient with incorrect dimension.
    struct BadDimGradLL;

    impl LogLikelihood for BadDimGradLL {
        type Data = ();

        fn value(&self, _theta: &Theta, _data: &Self::Data) -> OptResult<Cost> {
            Ok(0.0)
        }

        fn check(&self, _theta: &Theta, _data: &Self::Data) -> OptResult<()> {
            Ok(())
        }

        fn grad(&self, _theta: &Theta, _data: &Self::Data) -> OptResult<Grad> {
            // Always return a length-1 gradient, regardless of θ.
            Ok(Grad::from(vec![0.0_f64]))
        }
    }

    /// NonFiniteGradLL — analytic gradient with a non-finite entry.
    struct NonFiniteGradLL;

    impl LogLikelihood for NonFiniteGradLL {
        type Data = ();

        fn value(&self, _theta: &Theta, _data: &Self::Data) -> OptResult<Cost> {
            Ok(0.0)
        }

        fn check(&self, _theta: &Theta, _data: &Self::Data) -> OptResult<()> {
            Ok(())
        }

        fn grad(&self, theta: &Theta, _data: &Self::Data) -> OptResult<Grad> {
            // Same length as θ but with a NaN in the first coordinate.
            let mut g = theta.clone();
            if let Some(first) = g.get_mut(0) {
                *first = f64::NAN;
            }
            Ok(g)
        }
    }

    /// ErrorValueLL — log-likelihood that always errors in `value`.
    ///
    /// Used to test error propagation through finite-difference gradient paths.
    struct ErrorValueLL;

    impl LogLikelihood for ErrorValueLL {
        type Data = ();

        fn value(&self, _theta: &Theta, _data: &Self::Data) -> OptResult<Cost> {
            Err(OptError::InvalidLogLikInput { value: 123.0 })
        }

        fn check(&self, _theta: &Theta, _data: &Self::Data) -> OptResult<()> {
            Ok(())
        }

        // `grad` uses `GradientNotImplemented`, forcing FD.
    }

    #[test]
    // Purpose
    // -------
    // Verify that `CostFunction::cost` returns the negative log-likelihood
    // value `c(θ) = -ℓ(θ)` for finite inputs.
    //
    // Given
    // -----
    // - A `SimpleLL` model with ℓ(θ) = ∑ᵢ θᵢ and finite θ.
    //
    // Expect
    // ------
    // - `adapter.cost(θ)` returns `Ok(-∑ᵢ θᵢ)`.
    fn cost_returns_negative_loglik_for_finite_value() {
        // Arrange
        let model = SimpleLL;
        let data = ();
        let adapter = ArgMinAdapter::new(&model, &data);
        let theta = Theta::from(vec![1.0_f64, 2.0_f64, -3.0_f64]); // sum = 0.0

        // Act
        let cost = adapter.cost(&theta).expect("cost should succeed for finite ℓ");

        // Assert
        assert_eq!(cost, -0.0_f64);
    }

    #[test]
    // Purpose
    // -------
    // Ensure that `CostFunction::cost` rejects non-finite log-likelihood
    // values and surfaces `OptError::NonFiniteCost` through Argmin's `Error`.
    //
    // Given
    // -----
    // - A `NonFiniteValueLL` model whose `value` returns `NaN`.
    //
    // Expect
    // ------
    // - `adapter.cost(θ)` returns an `Error` that downcasts to
    //   `OptError::NonFiniteCost`.
    fn cost_errors_on_non_finite_loglik() {
        // Arrange
        let model = NonFiniteValueLL;
        let data = ();
        let adapter = ArgMinAdapter::new(&model, &data);
        let theta = Theta::from(vec![0.0_f64, 1.0_f64]);

        // Act
        let err: Error = adapter.cost(&theta).expect_err("non-finite ℓ should error");

        // Assert
        let opt_err: OptError = err.downcast().expect("expected OptError");
        match opt_err {
            OptError::NonFiniteCost { value } => {
                assert!(value.is_nan(), "stored value should be the original NaN");
            }
            other => panic!("Expected NonFiniteCost, got {other:?}"),
        }
    }

    #[test]
    // Purpose
    // -------
    // Check that `gradient` uses the analytic log-likelihood gradient when
    // available, applies the sign flip, and validates dimensions.
    //
    // Given
    // -----
    // - A `QuadLL` model with ℓ(θ) = -0.5 ||θ||² and ∇ℓ(θ) = -θ.
    //
    // Expect
    // ------
    // - `adapter.gradient(θ)` returns `θ` (since ∇c(θ) = -∇ℓ(θ) = θ).
    fn gradient_with_analytic_grad_applies_sign_flip_and_validation() {
        // Arrange
        let model = QuadLL;
        let data = ();
        let adapter = ArgMinAdapter::new(&model, &data);
        let theta = Theta::from(vec![1.0_f64, -2.0_f64, 0.5_f64]);

        // Act
        let grad = adapter.gradient(&theta).expect("analytic gradient should succeed");

        // Assert
        assert_eq!(grad.len(), theta.len());
        for (g_i, t_i) in grad.iter().zip(theta.iter()) {
            assert!((*g_i - *t_i).abs() < 1e-12);
        }
    }

    #[test]
    // Purpose
    // -------
    // Verify that `gradient` reports a validation error when the analytic
    // gradient has the wrong dimension.
    //
    // Given
    // -----
    // - A `BadDimGradLL` model whose `grad` always returns a length-1 vector.
    // - A parameter vector `θ` of length 2.
    //
    // Expect
    // ------
    // - `adapter.gradient(θ)` returns an `Error` that downcasts to
    //   `OptError::GradientDimMismatch`.
    fn gradient_errors_when_analytic_grad_has_wrong_dimension() {
        // Arrange
        let model = BadDimGradLL;
        let data = ();
        let adapter = ArgMinAdapter::new(&model, &data);
        let theta = Theta::from(vec![0.0_f64, 1.0_f64]);

        // Act
        let err: Error = adapter.gradient(&theta).expect_err("dimension mismatch should error");

        // Assert
        let opt_err: OptError = err.downcast().expect("expected OptError");
        match opt_err {
            OptError::GradientDimMismatch { expected, found } => {
                assert_eq!(expected, theta.len());
                assert_eq!(found, 1);
            }
            other => panic!("Expected GradientDimMismatch, got {other:?}"),
        }
    }

    #[test]
    // Purpose
    // -------
    // Verify that `gradient` reports a validation error when the analytic
    // gradient contains non-finite entries.
    //
    // Given
    // -----
    // - A `NonFiniteGradLL` model whose gradient has a NaN coordinate.
    //
    // Expect
    // ------
    // - `adapter.gradient(θ)` returns an `Error` that downcasts to
    //   `OptError::InvalidGradient`.
    fn gradient_errors_when_analytic_grad_has_non_finite_entry() {
        // Arrange
        let model = NonFiniteGradLL;
        let data = ();
        let adapter = ArgMinAdapter::new(&model, &data);
        let theta = Theta::from(vec![1.0_f64, 2.0_f64]);

        // Act
        let err: Error = adapter.gradient(&theta).expect_err("non-finite gradient should error");

        // Assert
        let opt_err: OptError = err.downcast().expect("expected OptError");
        match opt_err {
            OptError::InvalidGradient { index, .. } => {
                assert_eq!(index, 0);
            }
            other => panic!("Expected InvalidGradient, got {other:?}"),
        }
    }

    #[test]
    // Purpose
    // -------
    // Confirm that `gradient` falls back to finite differencing of the cost
    // when the log-likelihood does not implement an analytic gradient.
    //
    // Given
    // -----
    // - A `QuadNoGradLL` model with ℓ(θ) = -0.5 ||θ||² and no `grad`.
    //
    // Expect
    // ------
    // - `adapter.gradient(θ)` returns a gradient numerically close to θ,
    //   consistent with ∇c(θ) = θ.
    fn gradient_falls_back_to_fd_when_gradient_not_implemented() {
        // Arrange
        let model = QuadNoGradLL;
        let data = ();
        let adapter = ArgMinAdapter::new(&model, &data);
        let theta = Theta::from(vec![1.0_f64, -2.0_f64]);

        // Act
        let grad =
            adapter.gradient(&theta).expect("FD gradient should succeed for well-behaved cost");

        // Assert
        assert_eq!(grad.len(), theta.len());
        for (g_i, t_i) in grad.iter().zip(theta.iter()) {
            assert!((*g_i - *t_i).abs() < 1e-6, "FD gradient should match θ");
        }
    }

    #[test]
    // Purpose
    // -------
    // Ensure that errors raised by the log-likelihood during cost evaluation
    // are propagated through the finite-difference gradient path.
    //
    // Given
    // -----
    // - An `ErrorValueLL` model whose `value` always returns
    //   `OptError::InvalidLogLikInput`.
    // - No analytic gradient (`GradientNotImplemented`).
    //
    // Expect
    // ------
    // - `adapter.gradient(θ)` returns an `Error` that ultimately carries a
    //   `BackendError` whose text includes the original message.
    fn gradient_fd_propagates_cost_error_from_loglik() {
        // Arrange
        let model = ErrorValueLL;
        let data = ();
        let adapter = ArgMinAdapter::new(&model, &data);
        let theta = Theta::from(vec![0.0_f64, 1.0_f64]);

        // Act
        let err: Error =
            adapter.gradient(&theta).expect_err("log-likelihood error should propagate through FD");

        // Assert
        let opt_err: OptError = err.downcast().expect("expected OptError");
        match opt_err {
            OptError::BackendError { text } => {
                assert!(
                    text.contains("Invalid input to log-likelihood function: 123"),
                    "backend message should preserve original log-likelihood error, got: {text}"
                );
            }
            other => panic!("Expected BackendError, got {other:?}"),
        }
    }

    #[test]
    // Purpose
    // -------
    // Sanity check that `ArgMinAdapter::new` wires references to model and
    // data without modification.
    //
    // Given
    // -----
    // - A `SimpleLL` model instance and unit data `()`.
    //
    // Expect
    // ------
    // - The constructed `ArgMinAdapter` exposes the same references via
    //   its public fields.
    fn argminadapter_new_preserves_model_and_data_references() {
        // Arrange
        let model = SimpleLL;
        let data = ();
        let adapter = ArgMinAdapter::new(&model, &data);

        // Act / Assert
        // (Just ensure the references are the same by address comparison.)
        let model_ptr: *const SimpleLL = &model;
        let adapter_ptr: *const SimpleLL = adapter.f;
        assert_eq!(model_ptr, adapter_ptr);
    }
}
