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