ccxt_core/
error.rs

1//! # Error Handling for CCXT Rust
2//!
3//! This module provides a comprehensive, production-grade error handling system for the
4//! `ccxt-rust` library. It is designed following Rust community best practices and the
5//! principles outlined in the [Error Handling Project Group](https://blog.rust-lang.org/inside-rust/2021/07/01/What-the-error-handling-project-group-is-working-towards.html).
6//!
7//! ## Design Philosophy
8//!
9//! The error handling system is built around these core principles:
10//!
11//! 1. **Type Safety**: Strongly-typed errors using `thiserror` for compile-time guarantees
12//! 2. **API Stability**: All public enums use `#[non_exhaustive]` for forward compatibility
13//! 3. **Zero Panic**: No `unwrap()` or `expect()` on recoverable error paths
14//! 4. **Context Rich**: Full error chain support with context attachment
15//! 5. **Performance**: Optimized memory layout using `Cow<'static, str>` and `Box`
16//! 6. **Thread Safety**: All error types implement `Send + Sync + 'static`
17//! 7. **Observability**: Integration with `tracing` for structured logging
18//!
19//! ## Error Hierarchy
20//!
21//! ```text
22//! Error (main error type)
23//! ├── Exchange      - Exchange-specific API errors
24//! ├── Network       - Network/transport layer errors (via NetworkError)
25//! ├── Parse         - Response parsing errors (via ParseError)
26//! ├── Order         - Order management errors (via OrderError)
27//! ├── Authentication - API key/signature errors
28//! ├── RateLimit     - Rate limiting with retry information
29//! ├── Timeout       - Operation timeout
30//! ├── InvalidRequest - Invalid parameters
31//! ├── MarketNotFound - Unknown trading pair
32//! ├── WebSocket     - WebSocket communication errors
33//! └── Context       - Error with additional context
34//! ```
35//!
36//! ## Quick Start
37//!
38//! ### Basic Error Handling
39//!
40//! ```rust
41//! use ccxt_core::error::{Error, Result};
42//!
43//! fn fetch_price(symbol: &str) -> Result<f64> {
44//!     if symbol.is_empty() {
45//!         return Err(Error::invalid_request("Symbol cannot be empty"));
46//!     }
47//!     // ... fetch price logic
48//!     Ok(42000.0)
49//! }
50//! ```
51//!
52//! ### Adding Context to Errors
53//!
54//! ```rust
55//! use ccxt_core::error::{Error, Result, ContextExt};
56//!
57//! fn process_order(order_id: &str) -> Result<()> {
58//!     validate_order(order_id)
59//!         .context("Failed to validate order")?;
60//!     
61//!     submit_order(order_id)
62//!         .with_context(|| format!("Failed to submit order {}", order_id))?;
63//!     
64//!     Ok(())
65//! }
66//! # fn validate_order(_: &str) -> Result<()> { Ok(()) }
67//! # fn submit_order(_: &str) -> Result<()> { Ok(()) }
68//! ```
69//!
70//! ### Handling Specific Error Types
71//!
72//! ```rust
73//! use ccxt_core::error::{Error, NetworkError};
74//!
75//! fn handle_error(err: Error) {
76//!     // Check if error is retryable
77//!     if err.is_retryable() {
78//!         if let Some(duration) = err.retry_after() {
79//!             println!("Retry after {:?}", duration);
80//!         }
81//!     }
82//!     
83//!     // Check for specific error types through context layers
84//!     if let Some(msg) = err.as_authentication() {
85//!         println!("Auth error: {}", msg);
86//!     }
87//!     
88//!     // Get full error report
89//!     println!("Error report:\n{}", err.report());
90//! }
91//! ```
92//!
93//! ### Creating Exchange Errors
94//!
95//! ```rust
96//! use ccxt_core::error::Error;
97//!
98//! // Simple exchange error
99//! let err = Error::exchange("-1121", "Invalid symbol");
100//!
101//! // Exchange error with raw response data
102//! let err = Error::exchange_with_data(
103//!     "400",
104//!     "Bad Request",
105//!     serde_json::json!({"code": -1121, "msg": "Invalid symbol"})
106//! );
107//! ```
108//!
109//! ## Memory Optimization
110//!
111//! The `Error` enum is optimized to be ≤56 bytes on 64-bit systems:
112//!
113//! - Large variants (`Exchange`, `Network`, `Parse`, `Order`, `WebSocket`, `Context`)
114//!   are boxed to keep the enum size small
115//! - String fields use `Cow<'static, str>` to avoid allocation for static strings
116//! - Use `Error::authentication("static message")` for zero-allocation errors
117//! - Use `Error::authentication(format!("dynamic {}", value))` when needed
118//!
119//! ## Feature Flags
120//!
121//! - `backtrace`: Enable backtrace capture in `ExchangeErrorDetails` for debugging
122//!
123//! ## Integration with anyhow
124//!
125//! For application-level code, errors can be converted to `anyhow::Error`:
126//!
127//! ```rust
128//! use ccxt_core::error::Error;
129//!
130//! fn app_main() -> anyhow::Result<()> {
131//!     let result: Result<(), Error> = Err(Error::timeout("Operation timed out"));
132//!     result?; // Automatically converts to anyhow::Error
133//!     Ok(())
134//! }
135//! ```
136
137use std::borrow::Cow;
138use std::error::Error as StdError;
139use std::fmt;
140use std::time::Duration;
141
142use thiserror::Error;
143
144#[cfg(feature = "backtrace")]
145use std::backtrace::Backtrace;
146
147/// Result type alias for all CCXT operations.
148pub type Result<T> = std::result::Result<T, Error>;
149
150/// Maximum length for error messages to prevent memory bloat from large HTTP responses.
151const MAX_ERROR_MESSAGE_LEN: usize = 1024;
152
153/// Truncates a string to a maximum length, adding "... (truncated)" if needed.
154fn truncate_message(mut msg: String) -> String {
155    if msg.len() > MAX_ERROR_MESSAGE_LEN {
156        msg.truncate(MAX_ERROR_MESSAGE_LEN);
157        msg.push_str("... (truncated)");
158    }
159    msg
160}
161
162/// Details for exchange-specific errors.
163///
164/// Extracted to a separate struct and boxed to keep Error enum size small.
165///
166/// Note: `#[non_exhaustive]` allows adding fields in future versions without breaking changes.
167///
168/// # Example
169///
170/// ```rust
171/// use ccxt_core::error::ExchangeErrorDetails;
172///
173/// let details = ExchangeErrorDetails::new("400", "Bad Request");
174/// assert_eq!(details.code, "400");
175/// ```
176#[derive(Debug)]
177#[non_exhaustive]
178pub struct ExchangeErrorDetails {
179    /// Error code as String to support all exchange formats (numeric, alphanumeric).
180    pub code: String,
181    /// Descriptive message from the exchange.
182    pub message: String,
183    /// Optional raw response data for debugging.
184    pub data: Option<serde_json::Value>,
185    /// Backtrace captured at error creation (feature-gated).
186    #[cfg(feature = "backtrace")]
187    pub backtrace: Backtrace,
188}
189
190impl ExchangeErrorDetails {
191    /// Creates a new `ExchangeErrorDetails` with the given code and message.
192    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
193        Self {
194            code: code.into(),
195            message: message.into(),
196            data: None,
197            #[cfg(feature = "backtrace")]
198            backtrace: Backtrace::capture(),
199        }
200    }
201
202    /// Creates a new `ExchangeErrorDetails` with raw response data.
203    pub fn with_data(
204        code: impl Into<String>,
205        message: impl Into<String>,
206        data: serde_json::Value,
207    ) -> Self {
208        Self {
209            code: code.into(),
210            message: message.into(),
211            data: Some(data),
212            #[cfg(feature = "backtrace")]
213            backtrace: Backtrace::capture(),
214        }
215    }
216}
217
218impl fmt::Display for ExchangeErrorDetails {
219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        write!(f, "{} (code: {})", self.message, self.code)
221    }
222}
223
224/// Encapsulated network errors hiding implementation details.
225///
226/// This type wraps all network-related errors without exposing third-party
227/// library types (like `reqwest::Error`) in the public API. This ensures
228/// API stability even when underlying HTTP libraries change.
229///
230/// # Retryable Errors
231///
232/// The following variants are considered retryable:
233/// - [`NetworkError::Timeout`] - Request timed out, may succeed on retry
234/// - [`NetworkError::ConnectionFailed`] - Connection failed, may be transient
235///
236/// # Example
237///
238/// ```rust
239/// use ccxt_core::error::{Error, NetworkError};
240///
241/// fn handle_network_error(err: NetworkError) {
242///     match &err {
243///         NetworkError::RequestFailed { status, message } => {
244///             println!("HTTP {}: {}", status, message);
245///         }
246///         NetworkError::Timeout => {
247///             println!("Request timed out, consider retrying");
248///         }
249///         NetworkError::ConnectionFailed(msg) => {
250///             println!("Connection failed: {}", msg);
251///         }
252///         _ => println!("Network error: {}", err),
253///     }
254/// }
255/// ```
256#[derive(Error, Debug)]
257#[non_exhaustive]
258pub enum NetworkError {
259    /// Request failed with HTTP status code.
260    #[error("Request failed with status {status}: {message}")]
261    RequestFailed {
262        /// HTTP status code
263        status: u16,
264        /// Error message
265        message: String,
266    },
267
268    /// Request timed out.
269    #[error("Request timeout")]
270    Timeout,
271
272    /// Connection failed.
273    #[error("Connection failed: {0}")]
274    ConnectionFailed(String),
275
276    /// DNS resolution failed.
277    #[error("DNS resolution failed: {0}")]
278    DnsResolution(String),
279
280    /// SSL/TLS error.
281    #[error("SSL/TLS error: {0}")]
282    Ssl(String),
283
284    /// Opaque transport error for underlying issues.
285    /// Uses `Box<dyn StdError>` to hide implementation details while preserving the source.
286    #[error("Transport error")]
287    Transport(#[source] Box<dyn StdError + Send + Sync + 'static>),
288}
289
290/// Errors related to parsing exchange responses.
291///
292/// This type handles all parsing failures including JSON deserialization,
293/// decimal number parsing, timestamp parsing, and missing/invalid fields.
294///
295/// # Memory Optimization
296///
297/// Uses `Cow<'static, str>` for field names and messages to avoid allocation
298/// when using static strings. Use the helper constructors for ergonomic creation:
299///
300/// ```rust
301/// use ccxt_core::error::ParseError;
302///
303/// // Zero allocation (static string)
304/// let err = ParseError::missing_field("price");
305///
306/// // Allocation only when needed (dynamic string)
307/// let field_name = format!("field_{}", 42);
308/// let err = ParseError::missing_field_owned(field_name);
309///
310/// // Invalid value with context
311/// let err = ParseError::invalid_value("amount", "must be positive");
312/// ```
313///
314/// # Example
315///
316/// ```rust
317/// use ccxt_core::error::{Error, ParseError, Result};
318///
319/// fn parse_price(json: &serde_json::Value) -> Result<f64> {
320///     json.get("price")
321///         .and_then(|v| v.as_f64())
322///         .ok_or_else(|| Error::from(ParseError::missing_field("price")))
323/// }
324/// ```
325#[derive(Error, Debug)]
326#[non_exhaustive]
327pub enum ParseError {
328    /// Failed to parse decimal number.
329    #[error("Failed to parse decimal: {0}")]
330    Decimal(#[from] rust_decimal::Error),
331
332    /// Failed to deserialize JSON.
333    #[error("Failed to deserialize JSON: {0}")]
334    Json(#[from] serde_json::Error),
335
336    /// Failed to parse timestamp.
337    #[error("Failed to parse timestamp: {0}")]
338    Timestamp(Cow<'static, str>),
339
340    /// Missing required field in response.
341    #[error("Missing required field: {0}")]
342    MissingField(Cow<'static, str>),
343
344    /// Invalid value for a field.
345    #[error("Invalid value for '{field}': {message}")]
346    InvalidValue {
347        /// Field name
348        field: Cow<'static, str>,
349        /// Error message
350        message: Cow<'static, str>,
351    },
352
353    /// Invalid format for a field.
354    #[error("Invalid format for '{field}': {message}")]
355    InvalidFormat {
356        /// Field name
357        field: Cow<'static, str>,
358        /// Error message
359        message: Cow<'static, str>,
360    },
361}
362
363impl ParseError {
364    /// Creates a `MissingField` error with a static string (no allocation).
365    #[must_use]
366    pub fn missing_field(field: &'static str) -> Self {
367        Self::MissingField(Cow::Borrowed(field))
368    }
369
370    /// Creates a `MissingField` error with a dynamic string.
371    #[must_use]
372    pub fn missing_field_owned(field: String) -> Self {
373        Self::MissingField(Cow::Owned(field))
374    }
375
376    /// Creates an `InvalidValue` error.
377    pub fn invalid_value(
378        field: impl Into<Cow<'static, str>>,
379        message: impl Into<Cow<'static, str>>,
380    ) -> Self {
381        Self::InvalidValue {
382            field: field.into(),
383            message: message.into(),
384        }
385    }
386
387    /// Creates a `Timestamp` error with a static string (no allocation).
388    #[must_use]
389    pub fn timestamp(message: &'static str) -> Self {
390        Self::Timestamp(Cow::Borrowed(message))
391    }
392
393    /// Creates a `Timestamp` error with a dynamic string.
394    #[must_use]
395    pub fn timestamp_owned(message: String) -> Self {
396        Self::Timestamp(Cow::Owned(message))
397    }
398
399    /// Creates an `InvalidFormat` error.
400    pub fn invalid_format(
401        field: impl Into<Cow<'static, str>>,
402        message: impl Into<Cow<'static, str>>,
403    ) -> Self {
404        Self::InvalidFormat {
405            field: field.into(),
406            message: message.into(),
407        }
408    }
409}
410
411/// Errors related to order management operations.
412///
413/// This type covers all order lifecycle errors including creation,
414/// cancellation, and modification failures.
415///
416/// # Example
417///
418/// ```rust
419/// use ccxt_core::error::{Error, OrderError};
420///
421/// fn cancel_order(order_id: &str) -> Result<(), Error> {
422///     // Simulate order not found
423///     if order_id == "unknown" {
424///         return Err(OrderError::CancellationFailed(
425///             format!("Order {} not found", order_id)
426///         ).into());
427///     }
428///     Ok(())
429/// }
430/// ```
431#[derive(Error, Debug)]
432#[non_exhaustive]
433pub enum OrderError {
434    /// Order creation failed.
435    #[error("Order creation failed: {0}")]
436    CreationFailed(String),
437
438    /// Order cancellation failed.
439    #[error("Order cancellation failed: {0}")]
440    CancellationFailed(String),
441
442    /// Order modification failed.
443    #[error("Order modification failed: {0}")]
444    ModificationFailed(String),
445
446    /// Invalid order parameters.
447    #[error("Invalid order parameters: {0}")]
448    InvalidParameters(String),
449}
450
451/// The primary error type for the `ccxt-rust` library.
452///
453/// Design constraints:
454/// - All large variants are boxed to keep enum size ≤ 56 bytes
455/// - Uses `Cow<'static, str>` for zero-allocation static strings
456/// - Verify with: `assert!(std::mem::size_of::<Error>() <= 56);`
457///
458/// # Example
459///
460/// ```rust
461/// use ccxt_core::error::Error;
462///
463/// let err = Error::authentication("Invalid API key");
464/// assert!(err.to_string().contains("Invalid API key"));
465/// ```
466#[derive(Error, Debug)]
467#[non_exhaustive]
468pub enum Error {
469    /// Exchange-specific errors returned by the exchange API.
470    /// Boxed to reduce enum size (`ExchangeErrorDetails` is large).
471    #[error("Exchange error: {0}")]
472    Exchange(Box<ExchangeErrorDetails>),
473
474    /// Network-related errors encapsulating transport layer issues.
475    /// Boxed to reduce enum size.
476    #[error("Network error: {0}")]
477    Network(Box<NetworkError>),
478
479    /// Authentication errors (invalid API key, signature, etc.).
480    #[error("Authentication error: {0}")]
481    Authentication(Cow<'static, str>),
482
483    /// Rate limit exceeded with optional retry information.
484    #[error("Rate limit exceeded: {message}")]
485    RateLimit {
486        /// Error message
487        message: Cow<'static, str>,
488        /// Optional duration to wait before retrying
489        retry_after: Option<Duration>,
490    },
491
492    /// Invalid request parameters.
493    #[error("Invalid request: {0}")]
494    InvalidRequest(Cow<'static, str>),
495
496    /// Order-related errors. Boxed to reduce enum size.
497    #[error("Order error: {0}")]
498    Order(Box<OrderError>),
499
500    /// Insufficient balance for an operation.
501    #[error("Insufficient balance: {0}")]
502    InsufficientBalance(Cow<'static, str>),
503
504    /// Invalid order format or parameters.
505    #[error("Invalid order: {0}")]
506    InvalidOrder(Cow<'static, str>),
507
508    /// Order not found on the exchange.
509    #[error("Order not found: {0}")]
510    OrderNotFound(Cow<'static, str>),
511
512    /// Market symbol not found or not supported.
513    #[error("Market not found: {0}")]
514    MarketNotFound(Cow<'static, str>),
515
516    /// Errors during response parsing. Boxed to reduce enum size.
517    #[error("Parse error: {0}")]
518    Parse(Box<ParseError>),
519
520    /// WebSocket communication errors.
521    /// Uses `Box<dyn StdError>` to preserve original error for downcast.
522    #[error("WebSocket error: {0}")]
523    WebSocket(#[source] Box<dyn StdError + Send + Sync + 'static>),
524
525    /// Operation timeout.
526    #[error("Timeout: {0}")]
527    Timeout(Cow<'static, str>),
528
529    /// Feature not implemented for this exchange.
530    #[error("Not implemented: {0}")]
531    NotImplemented(Cow<'static, str>),
532
533    /// Error with additional context, preserving the error chain.
534    #[error("{context}")]
535    Context {
536        /// Context message describing what operation failed
537        context: String,
538        /// The underlying error
539        #[source]
540        source: Box<Error>,
541    },
542}
543
544impl Error {
545    // ==================== Constructor Methods ====================
546
547    /// Creates a new exchange error.
548    ///
549    /// # Example
550    ///
551    /// ```rust
552    /// use ccxt_core::error::Error;
553    ///
554    /// let err = Error::exchange("400", "Bad Request");
555    /// ```
556    pub fn exchange(code: impl Into<String>, message: impl Into<String>) -> Self {
557        Self::Exchange(Box::new(ExchangeErrorDetails::new(code, message)))
558    }
559
560    /// Creates a new exchange error with raw response data.
561    pub fn exchange_with_data(
562        code: impl Into<String>,
563        message: impl Into<String>,
564        data: serde_json::Value,
565    ) -> Self {
566        Self::Exchange(Box::new(ExchangeErrorDetails::with_data(
567            code, message, data,
568        )))
569    }
570
571    /// Creates a new rate limit error with optional retry duration.
572    /// Accepts both `&'static str` (zero allocation) and `String`.
573    ///
574    /// # Example
575    ///
576    /// ```rust
577    /// use ccxt_core::error::Error;
578    /// use std::time::Duration;
579    ///
580    /// // Zero allocation (static string):
581    /// let err = Error::rate_limit("Too many requests", Some(Duration::from_secs(60)));
582    ///
583    /// // Allocation (dynamic string):
584    /// let err = Error::rate_limit(format!("Rate limit: {}", 429), None);
585    /// ```
586    pub fn rate_limit(
587        message: impl Into<Cow<'static, str>>,
588        retry_after: Option<Duration>,
589    ) -> Self {
590        Self::RateLimit {
591            message: message.into(),
592            retry_after,
593        }
594    }
595
596    /// Creates an authentication error.
597    /// Accepts both `&'static str` (zero allocation) and `String`.
598    pub fn authentication(msg: impl Into<Cow<'static, str>>) -> Self {
599        Self::Authentication(msg.into())
600    }
601
602    /// Creates a generic error (for backwards compatibility).
603    pub fn generic(msg: impl Into<Cow<'static, str>>) -> Self {
604        Self::InvalidRequest(msg.into())
605    }
606
607    /// Creates a network error from a message.
608    pub fn network(msg: impl Into<String>) -> Self {
609        Self::Network(Box::new(NetworkError::ConnectionFailed(msg.into())))
610    }
611
612    /// Creates a market not found error.
613    /// Accepts both `&'static str` (zero allocation) and `String`.
614    pub fn market_not_found(symbol: impl Into<Cow<'static, str>>) -> Self {
615        Self::MarketNotFound(symbol.into())
616    }
617
618    /// Creates a not implemented error.
619    /// Accepts both `&'static str` (zero allocation) and `String`.
620    pub fn not_implemented(feature: impl Into<Cow<'static, str>>) -> Self {
621        Self::NotImplemented(feature.into())
622    }
623
624    /// Creates an invalid request error.
625    pub fn invalid_request(msg: impl Into<Cow<'static, str>>) -> Self {
626        Self::InvalidRequest(msg.into())
627    }
628
629    /// Creates an invalid argument error (alias for `invalid_request`).
630    pub fn invalid_argument(msg: impl Into<Cow<'static, str>>) -> Self {
631        Self::InvalidRequest(msg.into())
632    }
633
634    /// Creates a bad symbol error (alias for `invalid_request`).
635    pub fn bad_symbol(symbol: impl Into<String>) -> Self {
636        let s = symbol.into();
637        Self::InvalidRequest(Cow::Owned(format!("Bad symbol: {s}")))
638    }
639
640    /// Creates an insufficient balance error.
641    pub fn insufficient_balance(msg: impl Into<Cow<'static, str>>) -> Self {
642        Self::InsufficientBalance(msg.into())
643    }
644
645    /// Creates a timeout error.
646    pub fn timeout(msg: impl Into<Cow<'static, str>>) -> Self {
647        Self::Timeout(msg.into())
648    }
649
650    /// Creates a WebSocket error from a message string.
651    pub fn websocket(msg: impl Into<String>) -> Self {
652        Self::WebSocket(Box::new(SimpleError(msg.into())))
653    }
654
655    /// Creates a WebSocket error from any error type.
656    pub fn websocket_error<E: StdError + Send + Sync + 'static>(err: E) -> Self {
657        Self::WebSocket(Box::new(err))
658    }
659
660    // ==================== Context Methods ====================
661
662    /// Attaches context to an existing error.
663    ///
664    /// # Example
665    ///
666    /// ```rust
667    /// use ccxt_core::error::Error;
668    ///
669    /// let err = Error::network("Connection refused")
670    ///     .context("Failed to fetch ticker for BTC/USDT");
671    /// ```
672    #[must_use]
673    pub fn context(self, context: impl Into<String>) -> Self {
674        Self::Context {
675            context: context.into(),
676            source: Box::new(self),
677        }
678    }
679
680    // ==================== Chain Traversal Methods ====================
681
682    /// Internal helper: creates an iterator that traverses the error chain.
683    /// Automatically penetrates Context layers.
684    fn iter_chain(&self) -> impl Iterator<Item = &Error> {
685        std::iter::successors(Some(self), |err| match err {
686            Error::Context { source, .. } => Some(source.as_ref()),
687            _ => None,
688        })
689    }
690
691    /// Returns the root cause of the error, skipping Context layers.
692    #[must_use]
693    pub fn root_cause(&self) -> &Error {
694        self.iter_chain().last().unwrap_or(self)
695    }
696
697    /// Finds a specific error variant in the chain (penetrates Context layers).
698    /// Useful for handling wrapped errors without manual unwrapping.
699    pub fn find_variant<F>(&self, matcher: F) -> Option<&Error>
700    where
701        F: Fn(&Error) -> bool,
702    {
703        self.iter_chain().find(|e| matcher(e))
704    }
705
706    /// Generates a detailed error report with the full chain.
707    ///
708    /// # Example
709    ///
710    /// ```rust
711    /// use ccxt_core::error::Error;
712    ///
713    /// let err = Error::network("Connection refused")
714    ///     .context("Failed to fetch ticker");
715    /// println!("{}", err.report());
716    /// // Output:
717    /// // Failed to fetch ticker
718    /// // Caused by: Network error: Connection failed: Connection refused
719    /// ```
720    #[must_use]
721    pub fn report(&self) -> String {
722        use std::fmt::Write;
723        let mut report = String::new();
724        report.push_str(&self.to_string());
725
726        let mut current: Option<&(dyn StdError + 'static)> = self.source();
727        while let Some(err) = current {
728            let _ = write!(report, "\nCaused by: {err}");
729            current = err.source();
730        }
731        report
732    }
733
734    // ==================== Helper Methods (Context Penetrating) ====================
735
736    /// Checks if this error is retryable (penetrates Context layers).
737    ///
738    /// Returns `true` for:
739    /// - `NetworkError::Timeout`
740    /// - `NetworkError::ConnectionFailed`
741    /// - `RateLimit`
742    /// - `Timeout`
743    #[must_use]
744    pub fn is_retryable(&self) -> bool {
745        match self {
746            Error::Network(ne) => matches!(
747                ne.as_ref(),
748                NetworkError::Timeout | NetworkError::ConnectionFailed(_)
749            ),
750            Error::RateLimit { .. } | Error::Timeout(_) => true,
751            Error::Context { source, .. } => source.is_retryable(),
752            _ => false,
753        }
754    }
755
756    /// Returns the retry delay if this is a rate limit error (penetrates Context layers).
757    #[must_use]
758    pub fn retry_after(&self) -> Option<Duration> {
759        match self {
760            Error::RateLimit { retry_after, .. } => *retry_after,
761            Error::Context { source, .. } => source.retry_after(),
762            _ => None,
763        }
764    }
765
766    /// Checks if this is a rate limit error (penetrates Context layers).
767    /// Returns the message and optional retry duration.
768    #[must_use]
769    pub fn as_rate_limit(&self) -> Option<(&str, Option<Duration>)> {
770        match self {
771            Error::RateLimit {
772                message,
773                retry_after,
774            } => Some((message.as_ref(), *retry_after)),
775            Error::Context { source, .. } => source.as_rate_limit(),
776            _ => None,
777        }
778    }
779
780    /// Checks if this is an authentication error (penetrates Context layers).
781    /// Returns the error message.
782    #[must_use]
783    pub fn as_authentication(&self) -> Option<&str> {
784        match self {
785            Error::Authentication(msg) => Some(msg.as_ref()),
786            Error::Context { source, .. } => source.as_authentication(),
787            _ => None,
788        }
789    }
790
791    /// Attempts to downcast the WebSocket error to a specific type.
792    #[must_use]
793    pub fn downcast_websocket<T: StdError + 'static>(&self) -> Option<&T> {
794        match self {
795            Error::WebSocket(e) => e.downcast_ref::<T>(),
796            Error::Context { source, .. } => source.downcast_websocket(),
797            _ => None,
798        }
799    }
800}
801
802/// A simple error type for wrapping string messages.
803/// Used internally for WebSocket errors created from strings.
804#[derive(Debug)]
805struct SimpleError(String);
806
807impl fmt::Display for SimpleError {
808    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
809        write!(f, "{}", self.0)
810    }
811}
812
813impl StdError for SimpleError {}
814
815// ==================== From Implementations ====================
816
817impl From<NetworkError> for Error {
818    fn from(e: NetworkError) -> Self {
819        Error::Network(Box::new(e))
820    }
821}
822
823impl From<Box<NetworkError>> for Error {
824    fn from(e: Box<NetworkError>) -> Self {
825        Error::Network(e)
826    }
827}
828
829impl From<ParseError> for Error {
830    fn from(e: ParseError) -> Self {
831        Error::Parse(Box::new(e))
832    }
833}
834
835impl From<Box<ParseError>> for Error {
836    fn from(e: Box<ParseError>) -> Self {
837        Error::Parse(e)
838    }
839}
840
841impl From<OrderError> for Error {
842    fn from(e: OrderError) -> Self {
843        Error::Order(Box::new(e))
844    }
845}
846
847impl From<Box<OrderError>> for Error {
848    fn from(e: Box<OrderError>) -> Self {
849        Error::Order(e)
850    }
851}
852
853impl From<serde_json::Error> for Error {
854    fn from(e: serde_json::Error) -> Self {
855        Error::Parse(Box::new(ParseError::Json(e)))
856    }
857}
858
859impl From<rust_decimal::Error> for Error {
860    fn from(e: rust_decimal::Error) -> Self {
861        Error::Parse(Box::new(ParseError::Decimal(e)))
862    }
863}
864
865// Feature-gated: only available when reqwest is enabled
866impl From<reqwest::Error> for NetworkError {
867    fn from(e: reqwest::Error) -> Self {
868        if e.is_timeout() {
869            NetworkError::Timeout
870        } else if e.is_connect() {
871            NetworkError::ConnectionFailed(truncate_message(e.to_string()))
872        } else if let Some(status) = e.status() {
873            NetworkError::RequestFailed {
874                status: status.as_u16(),
875                message: truncate_message(e.to_string()),
876            }
877        } else {
878            NetworkError::Transport(Box::new(e))
879        }
880    }
881}
882
883impl From<reqwest::Error> for Error {
884    fn from(e: reqwest::Error) -> Self {
885        Error::Network(Box::new(NetworkError::from(e)))
886    }
887}
888
889// ==================== ContextExt Trait ====================
890
891/// Extension trait for ergonomic error context attachment.
892///
893/// This trait provides methods to add context to errors, making it easier
894/// to understand where and why an error occurred. It works with both
895/// `Result<T, E>` and `Option<T>` types.
896///
897/// # When to Use
898///
899/// - Use `context()` when you have a static context message
900/// - Use `with_context()` when the context message is expensive to compute
901///   (it's only evaluated on error)
902///
903/// # Library vs Application Code
904///
905/// - **Library code**: Use this trait for adding context within the library
906/// - **Application code**: Consider using `anyhow::Context` for richer error handling
907///
908/// # Examples
909///
910/// ## Adding Context to Results
911///
912/// ```rust
913/// use ccxt_core::error::{Error, Result, ContextExt};
914///
915/// fn fetch_ticker(symbol: &str) -> Result<f64> {
916///     // Static context (always evaluated)
917///     let data = fetch_raw_data()
918///         .context("Failed to fetch raw data")?;
919///     
920///     // Lazy context (only evaluated on error)
921///     parse_price(&data)
922///         .with_context(|| format!("Failed to parse price for {}", symbol))
923/// }
924/// # fn fetch_raw_data() -> Result<String> { Ok("{}".to_string()) }
925/// # fn parse_price(_: &str) -> Result<f64> { Ok(42.0) }
926/// ```
927///
928/// ## Adding Context to Options
929///
930/// ```rust
931/// use ccxt_core::error::{Result, ContextExt};
932///
933/// fn get_required_field(json: &serde_json::Value) -> Result<&str> {
934///     json.get("field")
935///         .and_then(|v| v.as_str())
936///         .context("Missing required field 'field'")
937/// }
938/// ```
939pub trait ContextExt<T, E> {
940    /// Adds context to an error.
941    fn context<C>(self, context: C) -> Result<T>
942    where
943        C: fmt::Display + Send + Sync + 'static;
944
945    /// Adds lazy context to an error (only evaluated on error).
946    fn with_context<C, F>(self, f: F) -> Result<T>
947    where
948        C: fmt::Display + Send + Sync + 'static,
949        F: FnOnce() -> C;
950}
951
952impl<T, E> ContextExt<T, E> for std::result::Result<T, E>
953where
954    E: Into<Error>,
955{
956    fn context<C>(self, context: C) -> Result<T>
957    where
958        C: fmt::Display + Send + Sync + 'static,
959    {
960        self.map_err(|e| e.into().context(context.to_string()))
961    }
962
963    fn with_context<C, F>(self, f: F) -> Result<T>
964    where
965        C: fmt::Display + Send + Sync + 'static,
966        F: FnOnce() -> C,
967    {
968        self.map_err(|e| e.into().context(f().to_string()))
969    }
970}
971
972impl<T> ContextExt<T, Error> for Option<T> {
973    fn context<C>(self, context: C) -> Result<T>
974    where
975        C: fmt::Display + Send + Sync + 'static,
976    {
977        self.ok_or_else(|| Error::generic(context.to_string()))
978    }
979
980    fn with_context<C, F>(self, f: F) -> Result<T>
981    where
982        C: fmt::Display + Send + Sync + 'static,
983        F: FnOnce() -> C,
984    {
985        self.ok_or_else(|| Error::generic(f().to_string()))
986    }
987}
988
989// ==================== Legacy Compatibility ====================
990
991/// Helper trait for adding context to errors (legacy alias for ContextExt).
992#[deprecated(since = "0.2.0", note = "Use ContextExt instead")]
993pub trait ErrorContext<T>: Sized {
994    /// Add context to an error
995    fn context(self, context: impl fmt::Display) -> Result<T>;
996}
997
998#[allow(deprecated)]
999impl<T, E: Into<Error>> ErrorContext<T> for std::result::Result<T, E> {
1000    fn context(self, context: impl fmt::Display) -> Result<T> {
1001        self.map_err(|e| e.into().context(context.to_string()))
1002    }
1003}
1004
1005#[cfg(test)]
1006mod tests {
1007    use super::*;
1008
1009    #[test]
1010    fn test_exchange_error_details_display() {
1011        let details = ExchangeErrorDetails::new("400", "Bad Request");
1012        let display = format!("{}", details);
1013        assert!(display.contains("400"));
1014        assert!(display.contains("Bad Request"));
1015    }
1016
1017    #[test]
1018    fn test_exchange_error_details_with_data() {
1019        let data = serde_json::json!({"error": "test"});
1020        let details = ExchangeErrorDetails::with_data("500", "Internal Error", data.clone());
1021        assert_eq!(details.code, "500");
1022        assert_eq!(details.message, "Internal Error");
1023        assert_eq!(details.data, Some(data));
1024    }
1025
1026    #[test]
1027    fn test_error_exchange_creation() {
1028        let err = Error::exchange("400", "Bad Request");
1029        if let Error::Exchange(details) = &err {
1030            assert_eq!(details.code, "400");
1031            assert_eq!(details.message, "Bad Request");
1032        } else {
1033            panic!("Expected Exchange variant");
1034        }
1035    }
1036
1037    #[test]
1038    fn test_error_exchange_string_code() {
1039        // Test that string codes (not just numeric) work
1040        let err = Error::exchange("INVALID_SYMBOL", "Symbol not found");
1041        if let Error::Exchange(details) = &err {
1042            assert_eq!(details.code, "INVALID_SYMBOL");
1043        } else {
1044            panic!("Expected Exchange variant");
1045        }
1046    }
1047
1048    #[test]
1049    fn test_error_authentication() {
1050        let err = Error::authentication("Invalid API key");
1051        assert!(matches!(err, Error::Authentication(_)));
1052        assert!(err.to_string().contains("Invalid API key"));
1053    }
1054
1055    #[test]
1056    fn test_error_rate_limit() {
1057        let err = Error::rate_limit("Too many requests", Some(Duration::from_secs(60)));
1058        if let Error::RateLimit {
1059            message,
1060            retry_after,
1061        } = &err
1062        {
1063            assert_eq!(message.as_ref(), "Too many requests");
1064            assert_eq!(*retry_after, Some(Duration::from_secs(60)));
1065        } else {
1066            panic!("Expected RateLimit variant");
1067        }
1068    }
1069
1070    #[test]
1071    fn test_error_market_not_found() {
1072        let err = Error::market_not_found("BTC/USDT");
1073        assert!(matches!(err, Error::MarketNotFound(_)));
1074        assert!(err.to_string().contains("BTC/USDT"));
1075    }
1076
1077    #[test]
1078    fn test_error_context() {
1079        let base = Error::network("Connection refused");
1080        let with_context = base.context("Failed to fetch ticker");
1081
1082        assert!(matches!(with_context, Error::Context { .. }));
1083        assert!(with_context.to_string().contains("Failed to fetch ticker"));
1084    }
1085
1086    #[test]
1087    fn test_error_context_chain() {
1088        let base = Error::network("Connection refused");
1089        let ctx1 = base.context("Layer 1");
1090        let ctx2 = ctx1.context("Layer 2");
1091
1092        // Check that report contains all layers
1093        let report = ctx2.report();
1094        assert!(report.contains("Layer 2"));
1095        assert!(report.contains("Layer 1"));
1096        assert!(report.contains("Connection refused"));
1097    }
1098
1099    #[test]
1100    fn test_error_root_cause() {
1101        let base = Error::network("Connection refused");
1102        let ctx1 = base.context("Layer 1");
1103        let ctx2 = ctx1.context("Layer 2");
1104
1105        let root = ctx2.root_cause();
1106        assert!(matches!(root, Error::Network(_)));
1107    }
1108
1109    #[test]
1110    fn test_error_is_retryable() {
1111        // Retryable errors
1112        assert!(Error::rate_limit("test", None).is_retryable());
1113        assert!(Error::timeout("test").is_retryable());
1114        assert!(Error::from(NetworkError::Timeout).is_retryable());
1115        assert!(Error::from(NetworkError::ConnectionFailed("test".to_string())).is_retryable());
1116
1117        // Non-retryable errors
1118        assert!(!Error::authentication("test").is_retryable());
1119        assert!(!Error::invalid_request("test").is_retryable());
1120        assert!(!Error::market_not_found("test").is_retryable());
1121    }
1122
1123    #[test]
1124    fn test_error_is_retryable_through_context() {
1125        let err = Error::rate_limit("test", Some(Duration::from_secs(30)))
1126            .context("Layer 1")
1127            .context("Layer 2");
1128
1129        assert!(err.is_retryable());
1130        assert_eq!(err.retry_after(), Some(Duration::from_secs(30)));
1131    }
1132
1133    #[test]
1134    fn test_error_as_rate_limit() {
1135        let err = Error::rate_limit("Too many requests", Some(Duration::from_secs(60)));
1136        let (msg, retry) = err.as_rate_limit().unwrap();
1137        assert_eq!(msg, "Too many requests");
1138        assert_eq!(retry, Some(Duration::from_secs(60)));
1139    }
1140
1141    #[test]
1142    fn test_error_as_rate_limit_through_context() {
1143        let err = Error::rate_limit("Too many requests", Some(Duration::from_secs(60)))
1144            .context("Wrapped");
1145        let (msg, retry) = err.as_rate_limit().unwrap();
1146        assert_eq!(msg, "Too many requests");
1147        assert_eq!(retry, Some(Duration::from_secs(60)));
1148    }
1149
1150    #[test]
1151    fn test_error_as_authentication() {
1152        let err = Error::authentication("Invalid key");
1153        assert_eq!(err.as_authentication(), Some("Invalid key"));
1154    }
1155
1156    #[test]
1157    fn test_error_as_authentication_through_context() {
1158        let err = Error::authentication("Invalid key").context("Wrapped");
1159        assert_eq!(err.as_authentication(), Some("Invalid key"));
1160    }
1161
1162    #[test]
1163    fn test_network_error_request_failed() {
1164        let err = NetworkError::RequestFailed {
1165            status: 404,
1166            message: "Not Found".to_string(),
1167        };
1168        assert!(err.to_string().contains("404"));
1169        assert!(err.to_string().contains("Not Found"));
1170    }
1171
1172    #[test]
1173    fn test_parse_error_missing_field() {
1174        let err = ParseError::missing_field("price");
1175        assert!(err.to_string().contains("price"));
1176    }
1177
1178    #[test]
1179    fn test_parse_error_invalid_value() {
1180        let err = ParseError::invalid_value("amount", "must be positive");
1181        let display = err.to_string();
1182        assert!(display.contains("amount"));
1183        assert!(display.contains("must be positive"));
1184    }
1185
1186    #[test]
1187    fn test_error_display() {
1188        let err = Error::exchange("400", "Bad Request");
1189        let display = format!("{}", err);
1190        assert!(display.contains("400"));
1191        assert!(display.contains("Bad Request"));
1192    }
1193
1194    #[test]
1195    fn test_context_ext_result() {
1196        let result: std::result::Result<(), Error> = Err(Error::network("test"));
1197        let with_context = ContextExt::context(result, "Operation failed");
1198        assert!(with_context.is_err());
1199        let err = with_context.unwrap_err();
1200        assert!(err.to_string().contains("Operation failed"));
1201    }
1202
1203    #[test]
1204    fn test_context_ext_option() {
1205        let opt: Option<i32> = None;
1206        let result = opt.context("Value not found");
1207        assert!(result.is_err());
1208        let err = result.unwrap_err();
1209        assert!(err.to_string().contains("Value not found"));
1210    }
1211
1212    #[test]
1213    fn test_from_serde_json_error() {
1214        let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
1215        let err: Error = json_err.into();
1216        assert!(matches!(err, Error::Parse(_)));
1217    }
1218
1219    #[test]
1220    fn test_from_network_error() {
1221        let network_err = NetworkError::Timeout;
1222        let err: Error = network_err.into();
1223        assert!(matches!(err, Error::Network(_)));
1224    }
1225
1226    #[test]
1227    fn test_from_order_error() {
1228        let order_err = OrderError::CreationFailed("test".to_string());
1229        let err: Error = order_err.into();
1230        assert!(matches!(err, Error::Order(_)));
1231    }
1232
1233    #[test]
1234    fn test_truncate_message() {
1235        let short = "short message".to_string();
1236        assert_eq!(truncate_message(short.clone()), short);
1237
1238        let long = "x".repeat(2000);
1239        let truncated = truncate_message(long);
1240        assert!(truncated.len() < 2000);
1241        assert!(truncated.ends_with("... (truncated)"));
1242    }
1243
1244    // Static assertion tests for Send + Sync
1245    #[test]
1246    fn error_is_send_sync_static() {
1247        fn assert_traits<T: Send + Sync + 'static + StdError>() {}
1248        assert_traits::<Error>();
1249        assert_traits::<NetworkError>();
1250        assert_traits::<ParseError>();
1251        assert_traits::<OrderError>();
1252    }
1253
1254    #[test]
1255    fn error_size_is_reasonable() {
1256        let size = std::mem::size_of::<Error>();
1257        // Target: Error size ≤ 56 bytes on 64-bit systems
1258        assert!(
1259            size <= 56,
1260            "Error enum size {} exceeds 56 bytes, consider boxing large variants",
1261            size
1262        );
1263    }
1264}
1265
1266// ==================== Property-Based Tests ====================
1267
1268#[cfg(test)]
1269mod property_tests {
1270    use super::*;
1271    use proptest::prelude::*;
1272    use std::thread;
1273
1274    // **Feature: error-handling-refactoring, Property 1: Error Send + Sync Guarantee**
1275    // **Validates: Requirements 1.4, 9.2**
1276    //
1277    // This module verifies that all Error variants implement Send + Sync + 'static,
1278    // ensuring thread-safe error propagation across async boundaries.
1279
1280    /// Strategy to generate arbitrary error codes (numeric, alphanumeric, special chars)
1281    fn arb_error_code() -> impl Strategy<Value = String> {
1282        prop_oneof![
1283            // Numeric codes
1284            (100u32..600).prop_map(|n| n.to_string()),
1285            // Alphanumeric codes
1286            "[A-Z_]{3,20}",
1287            // Mixed codes
1288            "[A-Za-z0-9_-]{1,30}",
1289        ]
1290    }
1291
1292    /// Strategy to generate arbitrary error messages
1293    fn arb_error_message() -> impl Strategy<Value = String> {
1294        prop_oneof![
1295            Just("".to_string()),
1296            "[a-zA-Z0-9 .,!?-]{1,100}",
1297            // Unicode messages
1298            "\\PC{1,50}",
1299        ]
1300    }
1301
1302    /// Strategy to generate arbitrary Duration values
1303    fn arb_duration() -> impl Strategy<Value = Duration> {
1304        (0u64..=u64::MAX / 2).prop_map(Duration::from_nanos)
1305    }
1306
1307    /// Strategy to generate arbitrary optional Duration values
1308    fn arb_optional_duration() -> impl Strategy<Value = Option<Duration>> {
1309        prop_oneof![Just(None), arb_duration().prop_map(Some),]
1310    }
1311
1312    /// Strategy to generate arbitrary Error variants
1313    fn arb_error() -> impl Strategy<Value = Error> {
1314        prop_oneof![
1315            // Exchange errors
1316            (arb_error_code(), arb_error_message())
1317                .prop_map(|(code, msg)| Error::exchange(code, msg)),
1318            // Authentication errors
1319            arb_error_message().prop_map(|msg| Error::authentication(msg)),
1320            // Rate limit errors
1321            (arb_error_message(), arb_optional_duration())
1322                .prop_map(|(msg, retry)| Error::rate_limit(msg, retry)),
1323            // Invalid request errors
1324            arb_error_message().prop_map(|msg| Error::invalid_request(msg)),
1325            // Market not found errors
1326            arb_error_message().prop_map(|msg| Error::market_not_found(msg)),
1327            // Timeout errors
1328            arb_error_message().prop_map(|msg| Error::timeout(msg)),
1329            // Not implemented errors
1330            arb_error_message().prop_map(|msg| Error::not_implemented(msg)),
1331            // Network errors
1332            arb_error_message().prop_map(|msg| Error::network(msg)),
1333            // WebSocket errors
1334            arb_error_message().prop_map(|msg| Error::websocket(msg)),
1335            // Insufficient balance errors
1336            arb_error_message().prop_map(|msg| Error::insufficient_balance(msg)),
1337        ]
1338    }
1339
1340    /// Strategy to generate NetworkError variants
1341    fn arb_network_error() -> impl Strategy<Value = NetworkError> {
1342        prop_oneof![
1343            // Use prop_map with unit to avoid Clone requirement
1344            Just(()).prop_map(|_| NetworkError::Timeout),
1345            arb_error_message().prop_map(NetworkError::ConnectionFailed),
1346            arb_error_message().prop_map(NetworkError::DnsResolution),
1347            arb_error_message().prop_map(NetworkError::Ssl),
1348            (100u16..600, arb_error_message()).prop_map(|(status, msg)| {
1349                NetworkError::RequestFailed {
1350                    status,
1351                    message: msg,
1352                }
1353            }),
1354        ]
1355    }
1356
1357    /// Strategy to generate ParseError variants
1358    fn arb_parse_error() -> impl Strategy<Value = ParseError> {
1359        prop_oneof![
1360            arb_error_message().prop_map(|msg| ParseError::MissingField(Cow::Owned(msg))),
1361            arb_error_message().prop_map(|msg| ParseError::Timestamp(Cow::Owned(msg))),
1362            (arb_error_message(), arb_error_message()).prop_map(|(field, msg)| {
1363                ParseError::InvalidValue {
1364                    field: Cow::Owned(field),
1365                    message: Cow::Owned(msg),
1366                }
1367            }),
1368            (arb_error_message(), arb_error_message()).prop_map(|(field, msg)| {
1369                ParseError::InvalidFormat {
1370                    field: Cow::Owned(field),
1371                    message: Cow::Owned(msg),
1372                }
1373            }),
1374        ]
1375    }
1376
1377    /// Strategy to generate OrderError variants
1378    fn arb_order_error() -> impl Strategy<Value = OrderError> {
1379        prop_oneof![
1380            arb_error_message().prop_map(OrderError::CreationFailed),
1381            arb_error_message().prop_map(OrderError::CancellationFailed),
1382            arb_error_message().prop_map(OrderError::ModificationFailed),
1383            arb_error_message().prop_map(OrderError::InvalidParameters),
1384        ]
1385    }
1386
1387    // ==================== Property 1: Error Send + Sync Guarantee ====================
1388
1389    proptest! {
1390        #![proptest_config(ProptestConfig::with_cases(100))]
1391
1392        /// **Feature: error-handling-refactoring, Property 1: Error Send + Sync Guarantee**
1393        ///
1394        /// *For any* Error instance, the type must implement both `Send` and `Sync` traits,
1395        /// ensuring thread-safe error propagation across async boundaries.
1396        ///
1397        /// **Validates: Requirements 1.4, 9.2**
1398        #[test]
1399        fn prop_error_is_send_sync(error in arb_error()) {
1400            // Compile-time assertion: Error must be Send + Sync + 'static
1401            fn assert_send_sync_static<T: Send + Sync + 'static>(_: &T) {}
1402            assert_send_sync_static(&error);
1403
1404            // Runtime verification: Error can be sent across threads
1405            let error_string = error.to_string();
1406            let handle = thread::spawn(move || {
1407                // Error was successfully moved to another thread (Send)
1408                error.to_string()
1409            });
1410            let result = handle.join().expect("Thread should not panic");
1411            prop_assert_eq!(result, error_string);
1412        }
1413
1414        /// **Feature: error-handling-refactoring, Property 12: Error Thread Safety with 'static Bound**
1415        ///
1416        /// *For any* Error instance, the type must implement `Send + Sync + 'static` and
1417        /// `std::error::Error`, ensuring it can be used with `anyhow` and across async task boundaries.
1418        ///
1419        /// **Validates: Requirements 1.4, 9.2**
1420        #[test]
1421        fn prop_error_thread_safety_with_static_bound(error in arb_error()) {
1422            // Compile-time assertion: Error must implement std::error::Error
1423            fn assert_std_error<T: StdError + Send + Sync + 'static>(_: &T) {}
1424            assert_std_error(&error);
1425
1426            // Verify error can be boxed as dyn Error (required for anyhow compatibility)
1427            let boxed: Box<dyn StdError + Send + Sync + 'static> = Box::new(error);
1428
1429            // Verify the boxed error can be sent across threads
1430            let handle = thread::spawn(move || {
1431                boxed.to_string()
1432            });
1433            let _ = handle.join().expect("Thread should not panic");
1434        }
1435
1436        /// Property test for NetworkError Send + Sync guarantee
1437        #[test]
1438        fn prop_network_error_is_send_sync(error in arb_network_error()) {
1439            fn assert_send_sync_static<T: Send + Sync + 'static + StdError>(_: &T) {}
1440            assert_send_sync_static(&error);
1441
1442            let error_string = error.to_string();
1443            let handle = thread::spawn(move || error.to_string());
1444            let result = handle.join().expect("Thread should not panic");
1445            prop_assert_eq!(result, error_string);
1446        }
1447
1448        /// Property test for ParseError Send + Sync guarantee
1449        #[test]
1450        fn prop_parse_error_is_send_sync(error in arb_parse_error()) {
1451            fn assert_send_sync_static<T: Send + Sync + 'static + StdError>(_: &T) {}
1452            assert_send_sync_static(&error);
1453
1454            let error_string = error.to_string();
1455            let handle = thread::spawn(move || error.to_string());
1456            let result = handle.join().expect("Thread should not panic");
1457            prop_assert_eq!(result, error_string);
1458        }
1459
1460        /// Property test for OrderError Send + Sync guarantee
1461        #[test]
1462        fn prop_order_error_is_send_sync(error in arb_order_error()) {
1463            fn assert_send_sync_static<T: Send + Sync + 'static + StdError>(_: &T) {}
1464            assert_send_sync_static(&error);
1465
1466            let error_string = error.to_string();
1467            let handle = thread::spawn(move || error.to_string());
1468            let result = handle.join().expect("Thread should not panic");
1469            prop_assert_eq!(result, error_string);
1470        }
1471
1472        /// Property test for Error with Context layers - Send + Sync guarantee
1473        #[test]
1474        fn prop_error_with_context_is_send_sync(
1475            base_error in arb_error(),
1476            context1 in arb_error_message(),
1477            context2 in arb_error_message()
1478        ) {
1479            let error_with_context = base_error
1480                .context(context1)
1481                .context(context2);
1482
1483            fn assert_send_sync_static<T: Send + Sync + 'static + StdError>(_: &T) {}
1484            assert_send_sync_static(&error_with_context);
1485
1486            let error_string = error_with_context.to_string();
1487            let handle = thread::spawn(move || error_with_context.to_string());
1488            let result = handle.join().expect("Thread should not panic");
1489            prop_assert_eq!(result, error_string);
1490        }
1491
1492        /// Property test for Error converted from NetworkError - Send + Sync guarantee
1493        #[test]
1494        fn prop_error_from_network_error_is_send_sync(network_error in arb_network_error()) {
1495            let error: Error = network_error.into();
1496
1497            fn assert_send_sync_static<T: Send + Sync + 'static + StdError>(_: &T) {}
1498            assert_send_sync_static(&error);
1499
1500            let error_string = error.to_string();
1501            let handle = thread::spawn(move || error.to_string());
1502            let result = handle.join().expect("Thread should not panic");
1503            prop_assert_eq!(result, error_string);
1504        }
1505
1506        /// Property test for Error converted from ParseError - Send + Sync guarantee
1507        #[test]
1508        fn prop_error_from_parse_error_is_send_sync(parse_error in arb_parse_error()) {
1509            let error: Error = parse_error.into();
1510
1511            fn assert_send_sync_static<T: Send + Sync + 'static + StdError>(_: &T) {}
1512            assert_send_sync_static(&error);
1513
1514            let error_string = error.to_string();
1515            let handle = thread::spawn(move || error.to_string());
1516            let result = handle.join().expect("Thread should not panic");
1517            prop_assert_eq!(result, error_string);
1518        }
1519
1520        /// Property test for Error converted from OrderError - Send + Sync guarantee
1521        #[test]
1522        fn prop_error_from_order_error_is_send_sync(order_error in arb_order_error()) {
1523            let error: Error = order_error.into();
1524
1525            fn assert_send_sync_static<T: Send + Sync + 'static + StdError>(_: &T) {}
1526            assert_send_sync_static(&error);
1527
1528            let error_string = error.to_string();
1529            let handle = thread::spawn(move || error.to_string());
1530            let result = handle.join().expect("Thread should not panic");
1531            prop_assert_eq!(result, error_string);
1532        }
1533    }
1534
1535    // ==================== Static Compile-Time Assertions ====================
1536
1537    /// Static assertions that verify traits at compile time.
1538    /// These tests will fail to compile if the traits are not implemented.
1539    #[test]
1540    fn static_assert_error_traits() {
1541        // Compile-time assertions using const fn
1542        const fn assert_send<T: Send>() {}
1543        const fn assert_sync<T: Sync>() {}
1544        const fn assert_static<T: 'static>() {}
1545        const fn assert_std_error<T: StdError>() {}
1546
1547        // Error type
1548        assert_send::<Error>();
1549        assert_sync::<Error>();
1550        assert_static::<Error>();
1551        assert_std_error::<Error>();
1552
1553        // NetworkError type
1554        assert_send::<NetworkError>();
1555        assert_sync::<NetworkError>();
1556        assert_static::<NetworkError>();
1557        assert_std_error::<NetworkError>();
1558
1559        // ParseError type
1560        assert_send::<ParseError>();
1561        assert_sync::<ParseError>();
1562        assert_static::<ParseError>();
1563        assert_std_error::<ParseError>();
1564
1565        // OrderError type
1566        assert_send::<OrderError>();
1567        assert_sync::<OrderError>();
1568        assert_static::<OrderError>();
1569        assert_std_error::<OrderError>();
1570
1571        // ExchangeErrorDetails type (not StdError, but should be Send + Sync)
1572        assert_send::<ExchangeErrorDetails>();
1573        assert_sync::<ExchangeErrorDetails>();
1574        assert_static::<ExchangeErrorDetails>();
1575    }
1576
1577    /// Verify that Error can be used with anyhow (requires Send + Sync + 'static + StdError)
1578    #[test]
1579    fn static_assert_anyhow_compatibility() {
1580        fn can_convert_to_anyhow<E: StdError + Send + Sync + 'static>(_: E) -> anyhow::Error {
1581            anyhow::Error::msg("test")
1582        }
1583
1584        // These should compile, proving anyhow compatibility
1585        let _ = can_convert_to_anyhow(Error::authentication("test"));
1586        let _ = can_convert_to_anyhow(NetworkError::Timeout);
1587        let _ = can_convert_to_anyhow(ParseError::missing_field("test"));
1588        let _ = can_convert_to_anyhow(OrderError::CreationFailed("test".to_string()));
1589    }
1590
1591    /// Verify that Error can be used in async contexts (requires Send + 'static)
1592    #[test]
1593    fn static_assert_async_compatibility() {
1594        fn can_be_spawned<F: std::future::Future + Send + 'static>(_: F) {}
1595
1596        // This should compile, proving async compatibility
1597        async fn returns_error() -> std::result::Result<(), Error> {
1598            Err(Error::authentication("test"))
1599        }
1600
1601        can_be_spawned(returns_error());
1602    }
1603
1604    // ==================== Property 5: HTTP Status Code Preservation ====================
1605
1606    proptest! {
1607        #![proptest_config(ProptestConfig::with_cases(100))]
1608
1609        /// **Feature: error-handling-refactoring, Property 5: HTTP Status Code Preservation**
1610        ///
1611        /// *For any* valid HTTP status code (100-599), creating a `NetworkError::RequestFailed`
1612        /// must preserve the exact status code value.
1613        ///
1614        /// **Validates: Requirements 3.3**
1615        #[test]
1616        fn prop_http_status_code_preservation(status in 100u16..600u16, message in arb_error_message()) {
1617            // Create NetworkError::RequestFailed with the given status code
1618            let network_error = NetworkError::RequestFailed {
1619                status,
1620                message: message.clone(),
1621            };
1622
1623            // Verify the status code is preserved
1624            if let NetworkError::RequestFailed { status: preserved_status, message: preserved_message } = &network_error {
1625                prop_assert_eq!(
1626                    *preserved_status, status,
1627                    "HTTP status code {} was not preserved, got {}",
1628                    status, preserved_status
1629                );
1630                prop_assert_eq!(
1631                    preserved_message, &message,
1632                    "Error message was not preserved"
1633                );
1634            } else {
1635                prop_assert!(false, "Expected RequestFailed variant");
1636            }
1637
1638            // Verify the status code appears in the Display output
1639            let display = network_error.to_string();
1640            prop_assert!(
1641                display.contains(&status.to_string()),
1642                "Status code {} not found in display output: {}",
1643                status, display
1644            );
1645        }
1646
1647        /// **Feature: error-handling-refactoring, Property 5: HTTP Status Code Preservation**
1648        ///
1649        /// *For any* valid HTTP status code (100-599), when wrapped in Error::Network,
1650        /// the status code must still be accessible and preserved.
1651        ///
1652        /// **Validates: Requirements 3.3**
1653        #[test]
1654        fn prop_http_status_code_preservation_through_error(status in 100u16..600u16, message in arb_error_message()) {
1655            // Create NetworkError::RequestFailed and wrap in Error
1656            let network_error = NetworkError::RequestFailed {
1657                status,
1658                message: message.clone(),
1659            };
1660            let error: Error = network_error.into();
1661
1662            // Verify the error is a Network variant
1663            if let Error::Network(boxed_network_error) = &error {
1664                if let NetworkError::RequestFailed { status: preserved_status, .. } = boxed_network_error.as_ref() {
1665                    prop_assert_eq!(
1666                        *preserved_status, status,
1667                        "HTTP status code {} was not preserved through Error wrapper, got {}",
1668                        status, preserved_status
1669                    );
1670                } else {
1671                    prop_assert!(false, "Expected RequestFailed variant inside Network");
1672                }
1673            } else {
1674                prop_assert!(false, "Expected Network variant");
1675            }
1676
1677            // Verify the status code appears in the Error's Display output
1678            let display = error.to_string();
1679            prop_assert!(
1680                display.contains(&status.to_string()),
1681                "Status code {} not found in Error display output: {}",
1682                status, display
1683            );
1684        }
1685
1686        /// **Feature: error-handling-refactoring, Property 5: HTTP Status Code Preservation**
1687        ///
1688        /// *For any* valid HTTP status code (100-599), when wrapped in Error::Network
1689        /// and then wrapped with context, the status code must still be preserved
1690        /// and accessible via root_cause().
1691        ///
1692        /// **Validates: Requirements 3.3**
1693        #[test]
1694        fn prop_http_status_code_preservation_through_context(
1695            status in 100u16..600u16,
1696            message in arb_error_message(),
1697            context in "[a-zA-Z0-9 ]{1,50}"
1698        ) {
1699            // Create NetworkError::RequestFailed, wrap in Error, then add context
1700            let network_error = NetworkError::RequestFailed {
1701                status,
1702                message: message.clone(),
1703            };
1704            let error: Error = network_error.into();
1705            let error_with_context = error.context(context);
1706
1707            // Verify the status code is preserved in root_cause
1708            let root = error_with_context.root_cause();
1709            if let Error::Network(boxed_network_error) = root {
1710                if let NetworkError::RequestFailed { status: preserved_status, .. } = boxed_network_error.as_ref() {
1711                    prop_assert_eq!(
1712                        *preserved_status, status,
1713                        "HTTP status code {} was not preserved through context, got {}",
1714                        status, preserved_status
1715                    );
1716                } else {
1717                    prop_assert!(false, "Expected RequestFailed variant in root_cause");
1718                }
1719            } else {
1720                prop_assert!(false, "Expected Network variant in root_cause");
1721            }
1722
1723            // Verify the status code appears in the report
1724            let report = error_with_context.report();
1725            prop_assert!(
1726                report.contains(&status.to_string()),
1727                "Status code {} not found in error report: {}",
1728                status, report
1729            );
1730        }
1731    }
1732
1733    // ==================== Property 7: Error Context Chain Preservation ====================
1734
1735    proptest! {
1736        #![proptest_config(ProptestConfig::with_cases(100))]
1737
1738        /// **Feature: error-handling-refactoring, Property 7: Error Context Chain Preservation**
1739        ///
1740        /// *For any* base error and sequence of context strings, wrapping the error with
1741        /// `context()` calls must preserve the entire chain such that:
1742        /// - `source()` returns the previous error in the chain
1743        /// - `root_cause()` returns the original base error
1744        /// - `report()` contains all context strings and the base error message
1745        ///
1746        /// **Validates: Requirements 4.1, 4.2, 4.3, 4.4**
1747        #[test]
1748        fn prop_error_context_chain_preservation_single_context(
1749            base_error in arb_error(),
1750            context_str in "[a-zA-Z0-9 .,!?-]{1,100}"
1751        ) {
1752            // Capture the base error's string representation before wrapping
1753            let base_error_string = base_error.to_string();
1754
1755            // Wrap with a single context
1756            let wrapped = base_error.context(context_str.clone());
1757
1758            // 1. Verify source() returns the previous error in the chain
1759            let source = wrapped.source();
1760            prop_assert!(
1761                source.is_some(),
1762                "source() should return Some for Context variant"
1763            );
1764            let source_string = source.unwrap().to_string();
1765            prop_assert!(
1766                source_string.contains(&base_error_string) || base_error_string.contains(&source_string) || source_string == base_error_string,
1767                "source() should return the base error. Expected to contain '{}', got '{}'",
1768                base_error_string, source_string
1769            );
1770
1771            // 2. Verify root_cause() returns the original base error
1772            let root = wrapped.root_cause();
1773            let root_string = root.to_string();
1774            prop_assert!(
1775                root_string == base_error_string || root_string.contains(&base_error_string) || base_error_string.contains(&root_string),
1776                "root_cause() should return the original base error. Expected '{}', got '{}'",
1777                base_error_string, root_string
1778            );
1779
1780            // 3. Verify report() contains the context string
1781            let report = wrapped.report();
1782            prop_assert!(
1783                report.contains(&context_str),
1784                "report() should contain context string '{}'. Got: {}",
1785                context_str, report
1786            );
1787
1788            // 4. Verify report() contains the base error message
1789            prop_assert!(
1790                report.contains(&base_error_string),
1791                "report() should contain base error message '{}'. Got: {}",
1792                base_error_string, report
1793            );
1794
1795            // 5. Verify the wrapped error's Display shows the context
1796            let wrapped_display = wrapped.to_string();
1797            prop_assert!(
1798                wrapped_display.contains(&context_str),
1799                "Wrapped error Display should contain context '{}'. Got: {}",
1800                context_str, wrapped_display
1801            );
1802        }
1803
1804        /// **Feature: error-handling-refactoring, Property 7: Error Context Chain Preservation**
1805        ///
1806        /// *For any* base error and multiple context strings, wrapping the error with
1807        /// multiple `context()` calls must preserve the entire chain.
1808        ///
1809        /// **Validates: Requirements 4.1, 4.2, 4.3, 4.4**
1810        #[test]
1811        fn prop_error_context_chain_preservation_multiple_contexts(
1812            base_error in arb_error(),
1813            context1 in "[a-zA-Z0-9]{5,20}",
1814            context2 in "[a-zA-Z0-9]{5,20}",
1815            context3 in "[a-zA-Z0-9]{5,20}"
1816        ) {
1817            // Capture the base error's string representation before wrapping
1818            let base_error_string = base_error.to_string();
1819
1820            // Wrap with multiple contexts
1821            let wrapped = base_error
1822                .context(context1.clone())
1823                .context(context2.clone())
1824                .context(context3.clone());
1825
1826            // 1. Verify source() chain exists
1827            // The outermost context's source should be the middle context
1828            let source1 = wrapped.source();
1829            prop_assert!(source1.is_some(), "First source() should return Some");
1830
1831            let source2 = source1.unwrap().source();
1832            prop_assert!(source2.is_some(), "Second source() should return Some");
1833
1834            let source3 = source2.unwrap().source();
1835            prop_assert!(source3.is_some(), "Third source() should return Some");
1836
1837            // 2. Verify root_cause() returns the original base error
1838            let root = wrapped.root_cause();
1839            let root_string = root.to_string();
1840            prop_assert!(
1841                root_string == base_error_string || root_string.contains(&base_error_string) || base_error_string.contains(&root_string),
1842                "root_cause() should return the original base error. Expected '{}', got '{}'",
1843                base_error_string, root_string
1844            );
1845
1846            // 3. Verify report() contains all context strings
1847            let report = wrapped.report();
1848            prop_assert!(
1849                report.contains(&context1),
1850                "report() should contain context1 '{}'. Got: {}",
1851                context1, report
1852            );
1853            prop_assert!(
1854                report.contains(&context2),
1855                "report() should contain context2 '{}'. Got: {}",
1856                context2, report
1857            );
1858            prop_assert!(
1859                report.contains(&context3),
1860                "report() should contain context3 '{}'. Got: {}",
1861                context3, report
1862            );
1863
1864            // 4. Verify report() contains the base error message
1865            prop_assert!(
1866                report.contains(&base_error_string),
1867                "report() should contain base error message '{}'. Got: {}",
1868                base_error_string, report
1869            );
1870
1871            // 5. Verify the outermost context is shown in Display
1872            let wrapped_display = wrapped.to_string();
1873            prop_assert!(
1874                wrapped_display.contains(&context3),
1875                "Wrapped error Display should contain outermost context '{}'. Got: {}",
1876                context3, wrapped_display
1877            );
1878        }
1879
1880        /// **Feature: error-handling-refactoring, Property 7: Error Context Chain Preservation**
1881        ///
1882        /// *For any* base error and variable number of context strings (1-10),
1883        /// the context chain depth should match the number of context() calls.
1884        ///
1885        /// **Validates: Requirements 4.1, 4.2, 4.3, 4.4**
1886        #[test]
1887        fn prop_error_context_chain_depth(
1888            base_error in arb_error(),
1889            contexts in proptest::collection::vec("[a-zA-Z0-9]{3,15}", 1..=10)
1890        ) {
1891            // Capture the base error's string representation before wrapping
1892            let base_error_string = base_error.to_string();
1893            let num_contexts = contexts.len();
1894
1895            // Wrap with all contexts
1896            let mut wrapped = base_error;
1897            for ctx in &contexts {
1898                wrapped = wrapped.context(ctx.clone());
1899            }
1900
1901            // 1. Verify the chain depth by counting source() calls
1902            let mut depth = 0;
1903            let mut current: Option<&(dyn StdError + 'static)> = wrapped.source();
1904            while let Some(err) = current {
1905                depth += 1;
1906                current = err.source();
1907            }
1908            // The depth should be at least num_contexts (base error may or may not have source)
1909            prop_assert!(
1910                depth >= num_contexts,
1911                "Chain depth {} should be at least {} (number of contexts)",
1912                depth, num_contexts
1913            );
1914
1915            // 2. Verify root_cause() returns the original base error
1916            let root = wrapped.root_cause();
1917            let root_string = root.to_string();
1918            prop_assert!(
1919                root_string == base_error_string || root_string.contains(&base_error_string) || base_error_string.contains(&root_string),
1920                "root_cause() should return the original base error. Expected '{}', got '{}'",
1921                base_error_string, root_string
1922            );
1923
1924            // 3. Verify report() contains all context strings
1925            let report = wrapped.report();
1926            for ctx in &contexts {
1927                prop_assert!(
1928                    report.contains(ctx),
1929                    "report() should contain context '{}'. Got: {}",
1930                    ctx, report
1931                );
1932            }
1933
1934            // 4. Verify report() contains the base error message
1935            prop_assert!(
1936                report.contains(&base_error_string),
1937                "report() should contain base error message '{}'. Got: {}",
1938                base_error_string, report
1939            );
1940        }
1941
1942        /// **Feature: error-handling-refactoring, Property 7: Error Context Chain Preservation**
1943        ///
1944        /// *For any* error wrapped with context, the Context variant should preserve
1945        /// the source error exactly (not just its string representation).
1946        ///
1947        /// **Validates: Requirements 4.4**
1948        #[test]
1949        fn prop_error_context_preserves_source_error_variant(
1950            context_str in "[a-zA-Z0-9 ]{1,50}"
1951        ) {
1952            // Test with specific error variants to verify variant preservation
1953
1954            // Test with RateLimit variant
1955            let rate_limit_err = Error::rate_limit("test rate limit", Some(Duration::from_secs(30)));
1956            let wrapped_rate_limit = rate_limit_err.context(context_str.clone());
1957
1958            // The wrapped error should still be identifiable as a rate limit error
1959            prop_assert!(
1960                wrapped_rate_limit.as_rate_limit().is_some(),
1961                "as_rate_limit() should work through context"
1962            );
1963            prop_assert!(
1964                wrapped_rate_limit.is_retryable(),
1965                "is_retryable() should work through context for RateLimit"
1966            );
1967            prop_assert_eq!(
1968                wrapped_rate_limit.retry_after(),
1969                Some(Duration::from_secs(30)),
1970                "retry_after() should work through context"
1971            );
1972
1973            // Test with Authentication variant
1974            let auth_err = Error::authentication("test auth error");
1975            let wrapped_auth = auth_err.context(context_str.clone());
1976
1977            prop_assert!(
1978                wrapped_auth.as_authentication().is_some(),
1979                "as_authentication() should work through context"
1980            );
1981
1982            // Test with Timeout variant
1983            let timeout_err = Error::timeout("test timeout");
1984            let wrapped_timeout = timeout_err.context(context_str.clone());
1985
1986            prop_assert!(
1987                wrapped_timeout.is_retryable(),
1988                "is_retryable() should work through context for Timeout"
1989            );
1990
1991            // Test with Network variant
1992            let network_err = Error::from(NetworkError::Timeout);
1993            let wrapped_network = network_err.context(context_str);
1994
1995            prop_assert!(
1996                wrapped_network.is_retryable(),
1997                "is_retryable() should work through context for NetworkError::Timeout"
1998            );
1999        }
2000    }
2001
2002    // ==================== Property 14: Enum Size Constraint ====================
2003
2004    /// **Feature: error-handling-refactoring, Property 14: Enum Size Constraint**
2005    ///
2006    /// *For any* build configuration on 64-bit systems, `std::mem::size_of::<Error>()`
2007    /// must be less than or equal to 56 bytes to ensure efficient stack allocation
2008    /// and cache performance.
2009    ///
2010    /// **Validates: Performance optimization**
2011    ///
2012    /// Note: This is a static property that doesn't vary with input, but we verify
2013    /// it through property testing to ensure the constraint holds regardless of
2014    /// how Error variants are constructed.
2015    #[test]
2016    fn static_assert_error_size_constraint() {
2017        // The Error enum size is a compile-time constant
2018        const ERROR_SIZE: usize = std::mem::size_of::<Error>();
2019        const MAX_ALLOWED_SIZE: usize = 56;
2020
2021        // Static assertion at compile time (will fail compilation if violated)
2022        const _: () = assert!(
2023            ERROR_SIZE <= MAX_ALLOWED_SIZE,
2024            // Note: const panic messages don't support formatting
2025        );
2026
2027        // Runtime assertion with detailed message for debugging
2028        assert!(
2029            ERROR_SIZE <= MAX_ALLOWED_SIZE,
2030            "Error enum size {} bytes exceeds maximum allowed {} bytes. \
2031             Consider boxing large variants to reduce enum size.",
2032            ERROR_SIZE,
2033            MAX_ALLOWED_SIZE
2034        );
2035
2036        // Also verify sub-error types are reasonably sized
2037        let network_error_size = std::mem::size_of::<NetworkError>();
2038        let parse_error_size = std::mem::size_of::<ParseError>();
2039        let order_error_size = std::mem::size_of::<OrderError>();
2040
2041        // These are boxed in Error, so their size doesn't directly affect Error size,
2042        // but we still want them to be reasonable
2043        assert!(
2044            network_error_size <= 80,
2045            "NetworkError size {} bytes is unexpectedly large",
2046            network_error_size
2047        );
2048        assert!(
2049            parse_error_size <= 80,
2050            "ParseError size {} bytes is unexpectedly large",
2051            parse_error_size
2052        );
2053        assert!(
2054            order_error_size <= 48,
2055            "OrderError size {} bytes is unexpectedly large",
2056            order_error_size
2057        );
2058    }
2059
2060    proptest! {
2061        #![proptest_config(ProptestConfig::with_cases(100))]
2062
2063        /// **Feature: error-handling-refactoring, Property 14: Enum Size Constraint**
2064        ///
2065        /// *For any* Error instance constructed with arbitrary data, the enum size
2066        /// must remain within the 56-byte constraint. This verifies that boxing
2067        /// strategy is effective regardless of the data stored in variants.
2068        ///
2069        /// **Validates: Performance optimization**
2070        #[test]
2071        fn prop_error_size_constraint_with_arbitrary_data(error in arb_error()) {
2072            // The size of the Error enum is constant regardless of the data inside
2073            // (because large data is boxed), but we verify this property holds
2074            // for all constructed variants
2075            const MAX_ALLOWED_SIZE: usize = 56;
2076            let error_size = std::mem::size_of_val(&error);
2077
2078            // size_of_val returns the size of the enum itself, not the heap data
2079            // This should always equal size_of::<Error>()
2080            prop_assert!(
2081                error_size <= MAX_ALLOWED_SIZE,
2082                "Error size {} bytes exceeds {} bytes for variant: {:?}",
2083                error_size,
2084                MAX_ALLOWED_SIZE,
2085                std::mem::discriminant(&error)
2086            );
2087
2088            // Verify the size is consistent with the type's size
2089            prop_assert_eq!(
2090                error_size,
2091                std::mem::size_of::<Error>(),
2092                "size_of_val should equal size_of::<Error>()"
2093            );
2094        }
2095
2096        /// Property test verifying that wrapping errors in Context doesn't change enum size
2097        #[test]
2098        fn prop_error_size_with_context_layers(
2099            base_error in arb_error(),
2100            context1 in "[a-zA-Z0-9 ]{1,50}",
2101            context2 in "[a-zA-Z0-9 ]{1,50}",
2102            context3 in "[a-zA-Z0-9 ]{1,50}"
2103        ) {
2104            const MAX_ALLOWED_SIZE: usize = 56;
2105
2106            // Wrap with multiple context layers
2107            let wrapped = base_error
2108                .context(context1)
2109                .context(context2)
2110                .context(context3);
2111
2112            let wrapped_size = std::mem::size_of_val(&wrapped);
2113
2114            // Size should remain constant regardless of context depth
2115            // (because Context variant boxes the source error)
2116            prop_assert!(
2117                wrapped_size <= MAX_ALLOWED_SIZE,
2118                "Error with context size {} bytes exceeds {} bytes",
2119                wrapped_size,
2120                MAX_ALLOWED_SIZE
2121            );
2122
2123            prop_assert_eq!(
2124                wrapped_size,
2125                std::mem::size_of::<Error>(),
2126                "Wrapped error size should equal base Error size"
2127            );
2128        }
2129
2130        /// Property test verifying NetworkError size is reasonable
2131        #[test]
2132        fn prop_network_error_size_constraint(error in arb_network_error()) {
2133            const MAX_ALLOWED_SIZE: usize = 80;
2134            let error_size = std::mem::size_of_val(&error);
2135
2136            prop_assert!(
2137                error_size <= MAX_ALLOWED_SIZE,
2138                "NetworkError size {} bytes exceeds {} bytes",
2139                error_size,
2140                MAX_ALLOWED_SIZE
2141            );
2142        }
2143
2144        /// Property test verifying ParseError size is reasonable
2145        #[test]
2146        fn prop_parse_error_size_constraint(error in arb_parse_error()) {
2147            const MAX_ALLOWED_SIZE: usize = 80;
2148            let error_size = std::mem::size_of_val(&error);
2149
2150            prop_assert!(
2151                error_size <= MAX_ALLOWED_SIZE,
2152                "ParseError size {} bytes exceeds {} bytes",
2153                error_size,
2154                MAX_ALLOWED_SIZE
2155            );
2156        }
2157
2158        /// Property test verifying OrderError size is reasonable
2159        #[test]
2160        fn prop_order_error_size_constraint(error in arb_order_error()) {
2161            const MAX_ALLOWED_SIZE: usize = 48;
2162            let error_size = std::mem::size_of_val(&error);
2163
2164            prop_assert!(
2165                error_size <= MAX_ALLOWED_SIZE,
2166                "OrderError size {} bytes exceeds {} bytes",
2167                error_size,
2168                MAX_ALLOWED_SIZE
2169            );
2170        }
2171    }
2172
2173    // ==================== Property 9: Error Display Non-Empty ====================
2174
2175    proptest! {
2176        #![proptest_config(ProptestConfig::with_cases(100))]
2177
2178        /// **Feature: error-handling-refactoring, Property 9: Error Display Non-Empty**
2179        ///
2180        /// *For any* Error variant with valid construction parameters, calling `to_string()`
2181        /// must return a non-empty string.
2182        ///
2183        /// **Validates: Requirements 8.1**
2184        #[test]
2185        fn prop_error_display_non_empty(error in arb_error()) {
2186            let display = error.to_string();
2187            prop_assert!(
2188                !display.is_empty(),
2189                "Error::to_string() returned empty string for error: {:?}",
2190                error
2191            );
2192            prop_assert!(
2193                display.trim().len() > 0,
2194                "Error::to_string() returned whitespace-only string for error: {:?}",
2195                error
2196            );
2197        }
2198
2199        /// **Feature: error-handling-refactoring, Property 9: Error Display Non-Empty**
2200        ///
2201        /// *For any* NetworkError variant, calling `to_string()` must return a non-empty string.
2202        ///
2203        /// **Validates: Requirements 8.1**
2204        #[test]
2205        fn prop_network_error_display_non_empty(error in arb_network_error()) {
2206            let display = error.to_string();
2207            prop_assert!(
2208                !display.is_empty(),
2209                "NetworkError::to_string() returned empty string for error: {:?}",
2210                error
2211            );
2212        }
2213
2214        /// **Feature: error-handling-refactoring, Property 9: Error Display Non-Empty**
2215        ///
2216        /// *For any* ParseError variant, calling `to_string()` must return a non-empty string.
2217        ///
2218        /// **Validates: Requirements 8.1**
2219        #[test]
2220        fn prop_parse_error_display_non_empty(error in arb_parse_error()) {
2221            let display = error.to_string();
2222            prop_assert!(
2223                !display.is_empty(),
2224                "ParseError::to_string() returned empty string for error: {:?}",
2225                error
2226            );
2227        }
2228
2229        /// **Feature: error-handling-refactoring, Property 9: Error Display Non-Empty**
2230        ///
2231        /// *For any* OrderError variant, calling `to_string()` must return a non-empty string.
2232        ///
2233        /// **Validates: Requirements 8.1**
2234        #[test]
2235        fn prop_order_error_display_non_empty(error in arb_order_error()) {
2236            let display = error.to_string();
2237            prop_assert!(
2238                !display.is_empty(),
2239                "OrderError::to_string() returned empty string for error: {:?}",
2240                error
2241            );
2242        }
2243
2244        /// **Feature: error-handling-refactoring, Property 9: Error Display Non-Empty**
2245        ///
2246        /// *For any* Error wrapped with context, calling `to_string()` must return a non-empty string.
2247        ///
2248        /// **Validates: Requirements 8.1**
2249        #[test]
2250        fn prop_error_with_context_display_non_empty(
2251            base_error in arb_error(),
2252            context in "[a-zA-Z0-9 ]{1,50}"
2253        ) {
2254            let wrapped = base_error.context(context);
2255            let display = wrapped.to_string();
2256            prop_assert!(
2257                !display.is_empty(),
2258                "Error with context::to_string() returned empty string for error: {:?}",
2259                wrapped
2260            );
2261        }
2262    }
2263
2264    // ==================== Property 11: Retryable Error Classification ====================
2265
2266    proptest! {
2267        #![proptest_config(ProptestConfig::with_cases(100))]
2268
2269        /// **Feature: error-handling-refactoring, Property 11: Retryable Error Classification**
2270        ///
2271        /// *For any* error, the `is_retryable()` method must return `true` if and only if
2272        /// the error is one of: `NetworkError::Timeout`, `NetworkError::ConnectionFailed`,
2273        /// `RateLimit`, or `Timeout`. This must also work through Context layers.
2274        ///
2275        /// **Validates: Requirements 3.2 (implicit retry behavior)**
2276        #[test]
2277        fn prop_retryable_error_classification_rate_limit(
2278            message in arb_error_message(),
2279            retry_after in arb_optional_duration()
2280        ) {
2281            let error = Error::rate_limit(message, retry_after);
2282            prop_assert!(
2283                error.is_retryable(),
2284                "RateLimit error should be retryable"
2285            );
2286        }
2287
2288        /// **Feature: error-handling-refactoring, Property 11: Retryable Error Classification**
2289        ///
2290        /// Timeout errors must be classified as retryable.
2291        ///
2292        /// **Validates: Requirements 3.2**
2293        #[test]
2294        fn prop_retryable_error_classification_timeout(message in arb_error_message()) {
2295            let error = Error::timeout(message);
2296            prop_assert!(
2297                error.is_retryable(),
2298                "Timeout error should be retryable"
2299            );
2300        }
2301
2302        /// **Feature: error-handling-refactoring, Property 11: Retryable Error Classification**
2303        ///
2304        /// NetworkError::Timeout must be classified as retryable.
2305        ///
2306        /// **Validates: Requirements 3.2**
2307        #[test]
2308        fn prop_retryable_error_classification_network_timeout(_dummy in Just(())) {
2309            let error: Error = NetworkError::Timeout.into();
2310            prop_assert!(
2311                error.is_retryable(),
2312                "NetworkError::Timeout should be retryable"
2313            );
2314        }
2315
2316        /// **Feature: error-handling-refactoring, Property 11: Retryable Error Classification**
2317        ///
2318        /// NetworkError::ConnectionFailed must be classified as retryable.
2319        ///
2320        /// **Validates: Requirements 3.2**
2321        #[test]
2322        fn prop_retryable_error_classification_connection_failed(message in arb_error_message()) {
2323            let error: Error = NetworkError::ConnectionFailed(message).into();
2324            prop_assert!(
2325                error.is_retryable(),
2326                "NetworkError::ConnectionFailed should be retryable"
2327            );
2328        }
2329
2330        /// **Feature: error-handling-refactoring, Property 11: Retryable Error Classification**
2331        ///
2332        /// Authentication errors must NOT be classified as retryable.
2333        ///
2334        /// **Validates: Requirements 3.2**
2335        #[test]
2336        fn prop_non_retryable_error_classification_authentication(message in arb_error_message()) {
2337            let error = Error::authentication(message);
2338            prop_assert!(
2339                !error.is_retryable(),
2340                "Authentication error should NOT be retryable"
2341            );
2342        }
2343
2344        /// **Feature: error-handling-refactoring, Property 11: Retryable Error Classification**
2345        ///
2346        /// InvalidRequest errors must NOT be classified as retryable.
2347        ///
2348        /// **Validates: Requirements 3.2**
2349        #[test]
2350        fn prop_non_retryable_error_classification_invalid_request(message in arb_error_message()) {
2351            let error = Error::invalid_request(message);
2352            prop_assert!(
2353                !error.is_retryable(),
2354                "InvalidRequest error should NOT be retryable"
2355            );
2356        }
2357
2358        /// **Feature: error-handling-refactoring, Property 11: Retryable Error Classification**
2359        ///
2360        /// MarketNotFound errors must NOT be classified as retryable.
2361        ///
2362        /// **Validates: Requirements 3.2**
2363        #[test]
2364        fn prop_non_retryable_error_classification_market_not_found(message in arb_error_message()) {
2365            let error = Error::market_not_found(message);
2366            prop_assert!(
2367                !error.is_retryable(),
2368                "MarketNotFound error should NOT be retryable"
2369            );
2370        }
2371
2372        /// **Feature: error-handling-refactoring, Property 11: Retryable Error Classification**
2373        ///
2374        /// Exchange errors must NOT be classified as retryable.
2375        ///
2376        /// **Validates: Requirements 3.2**
2377        #[test]
2378        fn prop_non_retryable_error_classification_exchange(
2379            code in arb_error_code(),
2380            message in arb_error_message()
2381        ) {
2382            let error = Error::exchange(code, message);
2383            prop_assert!(
2384                !error.is_retryable(),
2385                "Exchange error should NOT be retryable"
2386            );
2387        }
2388
2389        /// **Feature: error-handling-refactoring, Property 11: Retryable Error Classification**
2390        ///
2391        /// Retryable errors wrapped in Context layers must still be classified as retryable.
2392        ///
2393        /// **Validates: Requirements 3.2**
2394        #[test]
2395        fn prop_retryable_error_through_context(
2396            message in arb_error_message(),
2397            retry_after in arb_optional_duration(),
2398            context1 in "[a-zA-Z0-9 ]{1,30}",
2399            context2 in "[a-zA-Z0-9 ]{1,30}"
2400        ) {
2401            let error = Error::rate_limit(message, retry_after)
2402                .context(context1)
2403                .context(context2);
2404            prop_assert!(
2405                error.is_retryable(),
2406                "RateLimit error wrapped in context should still be retryable"
2407            );
2408        }
2409
2410        /// **Feature: error-handling-refactoring, Property 11: Retryable Error Classification**
2411        ///
2412        /// Non-retryable errors wrapped in Context layers must still NOT be classified as retryable.
2413        ///
2414        /// **Validates: Requirements 3.2**
2415        #[test]
2416        fn prop_non_retryable_error_through_context(
2417            message in arb_error_message(),
2418            context1 in "[a-zA-Z0-9 ]{1,30}",
2419            context2 in "[a-zA-Z0-9 ]{1,30}"
2420        ) {
2421            let error = Error::authentication(message)
2422                .context(context1)
2423                .context(context2);
2424            prop_assert!(
2425                !error.is_retryable(),
2426                "Authentication error wrapped in context should still NOT be retryable"
2427            );
2428        }
2429
2430        /// **Feature: error-handling-refactoring, Property 11: Retryable Error Classification**
2431        ///
2432        /// NetworkError::RequestFailed must NOT be classified as retryable (it's a definitive failure).
2433        ///
2434        /// **Validates: Requirements 3.2**
2435        #[test]
2436        fn prop_non_retryable_error_classification_request_failed(
2437            status in 100u16..600u16,
2438            message in arb_error_message()
2439        ) {
2440            let error: Error = NetworkError::RequestFailed { status, message }.into();
2441            prop_assert!(
2442                !error.is_retryable(),
2443                "NetworkError::RequestFailed should NOT be retryable"
2444            );
2445        }
2446
2447        /// **Feature: error-handling-refactoring, Property 11: Retryable Error Classification**
2448        ///
2449        /// NetworkError::DnsResolution must NOT be classified as retryable.
2450        ///
2451        /// **Validates: Requirements 3.2**
2452        #[test]
2453        fn prop_non_retryable_error_classification_dns_resolution(message in arb_error_message()) {
2454            let error: Error = NetworkError::DnsResolution(message).into();
2455            prop_assert!(
2456                !error.is_retryable(),
2457                "NetworkError::DnsResolution should NOT be retryable"
2458            );
2459        }
2460
2461        /// **Feature: error-handling-refactoring, Property 11: Retryable Error Classification**
2462        ///
2463        /// NetworkError::Ssl must NOT be classified as retryable.
2464        ///
2465        /// **Validates: Requirements 3.2**
2466        #[test]
2467        fn prop_non_retryable_error_classification_ssl(message in arb_error_message()) {
2468            let error: Error = NetworkError::Ssl(message).into();
2469            prop_assert!(
2470                !error.is_retryable(),
2471                "NetworkError::Ssl should NOT be retryable"
2472            );
2473        }
2474    }
2475
2476    // ==================== Unit Tests for From Implementations ====================
2477
2478    /// Unit tests for From implementations to verify error information is preserved.
2479    /// **Validates: Requirements 9.4**
2480    #[test]
2481    fn test_from_network_error_preserves_info() {
2482        // Test Timeout
2483        let network_err = NetworkError::Timeout;
2484        let error: Error = network_err.into();
2485        assert!(matches!(error, Error::Network(_)));
2486        assert!(error.to_string().contains("timeout") || error.to_string().contains("Timeout"));
2487
2488        // Test ConnectionFailed
2489        let network_err = NetworkError::ConnectionFailed("Connection refused".to_string());
2490        let error: Error = network_err.into();
2491        assert!(matches!(error, Error::Network(_)));
2492        assert!(error.to_string().contains("Connection refused"));
2493
2494        // Test RequestFailed
2495        let network_err = NetworkError::RequestFailed {
2496            status: 404,
2497            message: "Not Found".to_string(),
2498        };
2499        let error: Error = network_err.into();
2500        assert!(matches!(error, Error::Network(_)));
2501        assert!(error.to_string().contains("404"));
2502        assert!(error.to_string().contains("Not Found"));
2503
2504        // Test DnsResolution
2505        let network_err = NetworkError::DnsResolution("DNS lookup failed".to_string());
2506        let error: Error = network_err.into();
2507        assert!(matches!(error, Error::Network(_)));
2508        assert!(error.to_string().contains("DNS"));
2509
2510        // Test Ssl
2511        let network_err = NetworkError::Ssl("Certificate expired".to_string());
2512        let error: Error = network_err.into();
2513        assert!(matches!(error, Error::Network(_)));
2514        assert!(error.to_string().contains("Certificate expired"));
2515    }
2516
2517    #[test]
2518    fn test_from_parse_error_preserves_info() {
2519        // Test MissingField
2520        let parse_err = ParseError::missing_field("price");
2521        let error: Error = parse_err.into();
2522        assert!(matches!(error, Error::Parse(_)));
2523        assert!(error.to_string().contains("price"));
2524
2525        // Test InvalidValue
2526        let parse_err = ParseError::invalid_value("amount", "must be positive");
2527        let error: Error = parse_err.into();
2528        assert!(matches!(error, Error::Parse(_)));
2529        assert!(error.to_string().contains("amount"));
2530        assert!(error.to_string().contains("must be positive"));
2531
2532        // Test Timestamp
2533        let parse_err = ParseError::timestamp("invalid timestamp format");
2534        let error: Error = parse_err.into();
2535        assert!(matches!(error, Error::Parse(_)));
2536        assert!(error.to_string().contains("timestamp"));
2537
2538        // Test InvalidFormat
2539        let parse_err = ParseError::invalid_format("date", "expected ISO 8601");
2540        let error: Error = parse_err.into();
2541        assert!(matches!(error, Error::Parse(_)));
2542        assert!(error.to_string().contains("date"));
2543        assert!(error.to_string().contains("ISO 8601"));
2544    }
2545
2546    #[test]
2547    fn test_from_order_error_preserves_info() {
2548        // Test CreationFailed
2549        let order_err = OrderError::CreationFailed("Insufficient margin".to_string());
2550        let error: Error = order_err.into();
2551        assert!(matches!(error, Error::Order(_)));
2552        assert!(error.to_string().contains("Insufficient margin"));
2553
2554        // Test CancellationFailed
2555        let order_err = OrderError::CancellationFailed("Order already filled".to_string());
2556        let error: Error = order_err.into();
2557        assert!(matches!(error, Error::Order(_)));
2558        assert!(error.to_string().contains("Order already filled"));
2559
2560        // Test ModificationFailed
2561        let order_err = OrderError::ModificationFailed("Cannot modify filled order".to_string());
2562        let error: Error = order_err.into();
2563        assert!(matches!(error, Error::Order(_)));
2564        assert!(error.to_string().contains("Cannot modify"));
2565
2566        // Test InvalidParameters
2567        let order_err = OrderError::InvalidParameters("Invalid quantity".to_string());
2568        let error: Error = order_err.into();
2569        assert!(matches!(error, Error::Order(_)));
2570        assert!(error.to_string().contains("Invalid quantity"));
2571    }
2572
2573    #[test]
2574    fn test_from_serde_json_error_preserves_info() {
2575        let json_err = serde_json::from_str::<serde_json::Value>("{ invalid json }").unwrap_err();
2576        let error: Error = json_err.into();
2577        assert!(matches!(error, Error::Parse(_)));
2578        // The error message should contain some indication of JSON parsing failure
2579        let display = error.to_string();
2580        assert!(
2581            display.contains("JSON") || display.contains("json") || display.contains("parse"),
2582            "Expected JSON-related error message, got: {}",
2583            display
2584        );
2585    }
2586
2587    #[test]
2588    fn test_from_rust_decimal_error_preserves_info() {
2589        use rust_decimal::Decimal;
2590        use std::str::FromStr;
2591
2592        let decimal_err = Decimal::from_str("not_a_number").unwrap_err();
2593        let error: Error = decimal_err.into();
2594        assert!(matches!(error, Error::Parse(_)));
2595        // The error message should contain some indication of decimal parsing failure
2596        let display = error.to_string();
2597        assert!(
2598            display.contains("decimal") || display.contains("Decimal") || display.contains("parse"),
2599            "Expected decimal-related error message, got: {}",
2600            display
2601        );
2602    }
2603
2604    #[test]
2605    fn test_from_boxed_network_error_preserves_info() {
2606        let network_err = Box::new(NetworkError::Timeout);
2607        let error: Error = network_err.into();
2608        assert!(matches!(error, Error::Network(_)));
2609        assert!(error.to_string().contains("timeout") || error.to_string().contains("Timeout"));
2610    }
2611
2612    #[test]
2613    fn test_from_boxed_parse_error_preserves_info() {
2614        let parse_err = Box::new(ParseError::missing_field("symbol"));
2615        let error: Error = parse_err.into();
2616        assert!(matches!(error, Error::Parse(_)));
2617        assert!(error.to_string().contains("symbol"));
2618    }
2619
2620    #[test]
2621    fn test_from_boxed_order_error_preserves_info() {
2622        let order_err = Box::new(OrderError::CreationFailed("Test failure".to_string()));
2623        let error: Error = order_err.into();
2624        assert!(matches!(error, Error::Order(_)));
2625        assert!(error.to_string().contains("Test failure"));
2626    }
2627
2628    // ==================== Unit Tests for From<reqwest::Error> ====================
2629    // **Validates: Requirements 9.4**
2630    //
2631    // Note: reqwest::Error cannot be easily constructed directly in tests,
2632    // so we test the conversion logic by creating actual network errors
2633    // using tokio runtime.
2634
2635    /// Test that reqwest timeout errors are converted to NetworkError::Timeout
2636    #[tokio::test]
2637    async fn test_from_reqwest_timeout_error() {
2638        // Create a client with a very short timeout
2639        let client = reqwest::Client::builder()
2640            .timeout(std::time::Duration::from_nanos(1))
2641            .build()
2642            .expect("Failed to build client");
2643
2644        // Try to make a request that will timeout
2645        let result = client.get("https://httpbin.org/delay/10").send().await;
2646
2647        if let Err(reqwest_err) = result {
2648            // Convert to NetworkError
2649            let network_err: NetworkError = reqwest_err.into();
2650
2651            // Timeout errors should be converted to NetworkError::Timeout
2652            // Note: The actual error type depends on whether the timeout happens
2653            // during connection or during the request
2654            let is_timeout_or_connection = matches!(
2655                network_err,
2656                NetworkError::Timeout
2657                    | NetworkError::ConnectionFailed(_)
2658                    | NetworkError::Transport(_)
2659            );
2660            assert!(
2661                is_timeout_or_connection,
2662                "Expected Timeout, ConnectionFailed, or Transport variant, got: {:?}",
2663                network_err
2664            );
2665        }
2666        // If the request somehow succeeds (unlikely with 1ns timeout), that's also fine
2667    }
2668
2669    /// Test that reqwest connection errors are converted to NetworkError::ConnectionFailed
2670    #[tokio::test]
2671    async fn test_from_reqwest_connection_error() {
2672        let client = reqwest::Client::new();
2673
2674        // Try to connect to a non-existent server
2675        let result = client.get("http://127.0.0.1:1").send().await;
2676
2677        if let Err(reqwest_err) = result {
2678            // Verify it's a connection error
2679            assert!(
2680                reqwest_err.is_connect(),
2681                "Expected connection error, got: {:?}",
2682                reqwest_err
2683            );
2684
2685            // Convert to NetworkError
2686            let network_err: NetworkError = reqwest_err.into();
2687
2688            // Connection errors should be converted to NetworkError::ConnectionFailed
2689            assert!(
2690                matches!(network_err, NetworkError::ConnectionFailed(_)),
2691                "Expected ConnectionFailed variant, got: {:?}",
2692                network_err
2693            );
2694
2695            // Convert to Error and verify the chain
2696            let error: Error = network_err.into();
2697            assert!(matches!(error, Error::Network(_)));
2698        }
2699    }
2700
2701    /// Test that the From<reqwest::Error> for Error conversion works correctly
2702    #[tokio::test]
2703    async fn test_from_reqwest_error_to_error() {
2704        let client = reqwest::Client::new();
2705
2706        // Try to connect to a non-existent server
2707        let result = client.get("http://127.0.0.1:1").send().await;
2708
2709        if let Err(reqwest_err) = result {
2710            // Convert directly to Error (not via NetworkError)
2711            let error: Error = reqwest_err.into();
2712
2713            // Should be wrapped in Error::Network
2714            assert!(
2715                matches!(error, Error::Network(_)),
2716                "Expected Network variant, got: {:?}",
2717                error
2718            );
2719
2720            // The error message should contain some connection-related info
2721            let display = error.to_string();
2722            assert!(!display.is_empty(), "Error display should not be empty");
2723        }
2724    }
2725
2726    /// Test that reqwest errors preserve information through the conversion chain
2727    #[tokio::test]
2728    async fn test_reqwest_error_preserves_info_through_chain() {
2729        let client = reqwest::Client::new();
2730
2731        // Try to connect to a non-existent server
2732        let result = client.get("http://127.0.0.1:1").send().await;
2733
2734        if let Err(reqwest_err) = result {
2735            let _original_message = reqwest_err.to_string();
2736
2737            // Convert to Error
2738            let error: Error = reqwest_err.into();
2739
2740            // The error should be retryable (connection failures are retryable)
2741            assert!(
2742                error.is_retryable(),
2743                "Connection failed errors should be retryable"
2744            );
2745
2746            // Add context and verify chain is preserved
2747            let error_with_context = error.context("Failed to fetch data");
2748            assert!(
2749                error_with_context.is_retryable(),
2750                "Retryable status should be preserved through context"
2751            );
2752
2753            // Verify the report contains the original error info
2754            let report = error_with_context.report();
2755            assert!(
2756                report.contains("Failed to fetch data"),
2757                "Report should contain context"
2758            );
2759            // The original error info should be somewhere in the chain
2760            assert!(
2761                report.contains("Network")
2762                    || report.contains("Connection")
2763                    || report.contains("connect"),
2764                "Report should contain network error info, got: {}",
2765                report
2766            );
2767        }
2768    }
2769
2770    /// Test truncate_message function for long error messages
2771    #[test]
2772    fn test_truncate_message_preserves_short_messages() {
2773        let short = "Short message".to_string();
2774        let result = truncate_message(short.clone());
2775        assert_eq!(result, short);
2776    }
2777
2778    #[test]
2779    fn test_truncate_message_truncates_long_messages() {
2780        let long = "x".repeat(2000);
2781        let result = truncate_message(long);
2782        assert!(result.len() < 2000);
2783        assert!(result.len() <= MAX_ERROR_MESSAGE_LEN + 20); // Allow for "... (truncated)" suffix
2784        assert!(result.ends_with("... (truncated)"));
2785    }
2786
2787    #[test]
2788    fn test_truncate_message_boundary() {
2789        // Test exactly at the boundary
2790        let exact = "x".repeat(MAX_ERROR_MESSAGE_LEN);
2791        let result = truncate_message(exact.clone());
2792        assert_eq!(result, exact); // Should not be truncated
2793
2794        // Test one over the boundary
2795        let over = "x".repeat(MAX_ERROR_MESSAGE_LEN + 1);
2796        let result = truncate_message(over);
2797        assert!(result.ends_with("... (truncated)"));
2798    }
2799
2800    // ==================== Comprehensive From Implementation Tests ====================
2801    // **Validates: Requirements 9.4**
2802
2803    /// Test that all NetworkError variants convert correctly to Error
2804    #[test]
2805    fn test_all_network_error_variants_convert_to_error() {
2806        // Timeout
2807        let err: Error = NetworkError::Timeout.into();
2808        assert!(matches!(err, Error::Network(_)));
2809        assert!(err.to_string().to_lowercase().contains("timeout"));
2810
2811        // ConnectionFailed
2812        let err: Error = NetworkError::ConnectionFailed("refused".to_string()).into();
2813        assert!(matches!(err, Error::Network(_)));
2814        assert!(err.to_string().contains("refused"));
2815
2816        // DnsResolution
2817        let err: Error = NetworkError::DnsResolution("lookup failed".to_string()).into();
2818        assert!(matches!(err, Error::Network(_)));
2819        assert!(err.to_string().contains("lookup failed"));
2820
2821        // Ssl
2822        let err: Error = NetworkError::Ssl("cert error".to_string()).into();
2823        assert!(matches!(err, Error::Network(_)));
2824        assert!(err.to_string().contains("cert error"));
2825
2826        // RequestFailed
2827        let err: Error = NetworkError::RequestFailed {
2828            status: 500,
2829            message: "Internal Server Error".to_string(),
2830        }
2831        .into();
2832        assert!(matches!(err, Error::Network(_)));
2833        assert!(err.to_string().contains("500"));
2834        assert!(err.to_string().contains("Internal Server Error"));
2835
2836        // Transport (with a simple error)
2837        let simple_err = std::io::Error::new(std::io::ErrorKind::Other, "transport error");
2838        let err: Error = NetworkError::Transport(Box::new(simple_err)).into();
2839        assert!(matches!(err, Error::Network(_)));
2840    }
2841
2842    /// Test that all ParseError variants convert correctly to Error
2843    #[test]
2844    fn test_all_parse_error_variants_convert_to_error() {
2845        // MissingField (static)
2846        let err: Error = ParseError::missing_field("price").into();
2847        assert!(matches!(err, Error::Parse(_)));
2848        assert!(err.to_string().contains("price"));
2849
2850        // MissingField (owned)
2851        let err: Error = ParseError::missing_field_owned("dynamic_field".to_string()).into();
2852        assert!(matches!(err, Error::Parse(_)));
2853        assert!(err.to_string().contains("dynamic_field"));
2854
2855        // InvalidValue
2856        let err: Error = ParseError::invalid_value("amount", "negative value").into();
2857        assert!(matches!(err, Error::Parse(_)));
2858        assert!(err.to_string().contains("amount"));
2859        assert!(err.to_string().contains("negative value"));
2860
2861        // Timestamp (static)
2862        let err: Error = ParseError::timestamp("invalid format").into();
2863        assert!(matches!(err, Error::Parse(_)));
2864        assert!(err.to_string().contains("timestamp"));
2865
2866        // Timestamp (owned)
2867        let err: Error = ParseError::timestamp_owned("dynamic timestamp error".to_string()).into();
2868        assert!(matches!(err, Error::Parse(_)));
2869        assert!(err.to_string().contains("dynamic timestamp error"));
2870
2871        // InvalidFormat
2872        let err: Error = ParseError::invalid_format("date", "expected YYYY-MM-DD").into();
2873        assert!(matches!(err, Error::Parse(_)));
2874        assert!(err.to_string().contains("date"));
2875        assert!(err.to_string().contains("YYYY-MM-DD"));
2876
2877        // Json (via serde_json::Error)
2878        let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
2879        let err: Error = ParseError::Json(json_err).into();
2880        assert!(matches!(err, Error::Parse(_)));
2881
2882        // Decimal (via rust_decimal::Error)
2883        use rust_decimal::Decimal;
2884        use std::str::FromStr;
2885        let decimal_err = Decimal::from_str("not_a_number").unwrap_err();
2886        let err: Error = ParseError::Decimal(decimal_err).into();
2887        assert!(matches!(err, Error::Parse(_)));
2888    }
2889
2890    /// Test that all OrderError variants convert correctly to Error
2891    #[test]
2892    fn test_all_order_error_variants_convert_to_error() {
2893        // CreationFailed
2894        let err: Error = OrderError::CreationFailed("insufficient funds".to_string()).into();
2895        assert!(matches!(err, Error::Order(_)));
2896        assert!(err.to_string().contains("insufficient funds"));
2897
2898        // CancellationFailed
2899        let err: Error = OrderError::CancellationFailed("order not found".to_string()).into();
2900        assert!(matches!(err, Error::Order(_)));
2901        assert!(err.to_string().contains("order not found"));
2902
2903        // ModificationFailed
2904        let err: Error = OrderError::ModificationFailed("order already filled".to_string()).into();
2905        assert!(matches!(err, Error::Order(_)));
2906        assert!(err.to_string().contains("order already filled"));
2907
2908        // InvalidParameters
2909        let err: Error = OrderError::InvalidParameters("invalid quantity".to_string()).into();
2910        assert!(matches!(err, Error::Order(_)));
2911        assert!(err.to_string().contains("invalid quantity"));
2912    }
2913
2914    /// Test that From implementations work with the ? operator
2915    #[test]
2916    fn test_from_implementations_with_question_mark() {
2917        fn parse_json() -> Result<serde_json::Value> {
2918            let value: serde_json::Value = serde_json::from_str("invalid")?;
2919            Ok(value)
2920        }
2921
2922        let result = parse_json();
2923        assert!(result.is_err());
2924        let err = result.unwrap_err();
2925        assert!(matches!(err, Error::Parse(_)));
2926    }
2927
2928    /// Test that From implementations preserve error source chain
2929    #[test]
2930    fn test_from_implementations_preserve_source() {
2931        // serde_json::Error -> ParseError -> Error
2932        let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
2933        let parse_err = ParseError::Json(json_err);
2934        let error: Error = parse_err.into();
2935
2936        // The Error should have a source
2937        if let Error::Parse(boxed_parse) = &error {
2938            if let ParseError::Json(inner) = boxed_parse.as_ref() {
2939                // The inner serde_json::Error should be accessible
2940                assert!(!inner.to_string().is_empty());
2941            } else {
2942                panic!("Expected ParseError::Json variant");
2943            }
2944        } else {
2945            panic!("Expected Error::Parse variant");
2946        }
2947
2948        // rust_decimal::Error -> ParseError -> Error
2949        use rust_decimal::Decimal;
2950        use std::str::FromStr;
2951        let decimal_err = Decimal::from_str("not_a_number").unwrap_err();
2952        let parse_err = ParseError::Decimal(decimal_err);
2953        let error: Error = parse_err.into();
2954
2955        if let Error::Parse(boxed_parse) = &error {
2956            if let ParseError::Decimal(inner) = boxed_parse.as_ref() {
2957                assert!(!inner.to_string().is_empty());
2958            } else {
2959                panic!("Expected ParseError::Decimal variant");
2960            }
2961        } else {
2962            panic!("Expected Error::Parse variant");
2963        }
2964    }
2965}