Skip to main content

oxigdal_core/error/
mod.rs

1//! Error types for `OxiGDAL`
2//!
3//! This module provides a comprehensive error hierarchy for all `OxiGDAL` operations.
4//! All error types implement [`std::error::Error`] via [`thiserror`].
5//!
6//! # Error Codes
7//!
8//! Each error variant has an associated error code (e.g., E001, E002) for easier
9//! debugging and documentation. Error codes are stable across versions.
10//!
11//! # Helper Methods
12//!
13//! All error types provide:
14//! - `code()` - Returns the error code
15//! - `suggestion()` - Returns helpful hints for fixing the error
16//! - `context()` - Returns additional context about the error
17//!
18//! # Builder Pattern
19//!
20//! For simple errors, use the direct constructors:
21//!
22//! ```ignore
23//! use oxigdal_core::error::OxiGdalError;
24//!
25//! let err = OxiGdalError::io_error("Cannot read file");
26//! ```
27//!
28//! For errors with rich context, use the builder pattern via [`ErrorBuilder`]:
29//!
30//! ```ignore
31//! use oxigdal_core::error::OxiGdalError;
32//!
33//! let err = OxiGdalError::io_error_builder("Cannot read file")
34//!     .with_path("/data/file.tif")
35//!     .with_operation("read_raster")
36//!     .with_suggestion("Check file permissions")
37//!     .build();
38//! ```
39//!
40//! # When to Use Which Error Type
41//!
42//! - **IoError**: File I/O, network operations, HTTP requests
43//! - **FormatError**: File format parsing, magic number validation, header parsing
44//! - **CrsError**: Coordinate system operations, transformations, WKT/EPSG handling
45//! - **CompressionError**: Compression/decompression operations
46//! - **InvalidParameter**: Parameter validation failures
47//! - **NotSupported**: Unsupported features or operations
48//! - **OutOfBounds**: Index or range validation failures
49//! - **Internal**: Internal invariant violations, allocation failures
50//!
51//! # Examples of Rich Error Messages
52//!
53//! ## File Not Found with Context
54//!
55//! ```ignore
56//! use oxigdal_core::error::OxiGdalError;
57//! use std::path::Path;
58//!
59//! fn read_geotiff(path: &Path) -> Result<(), OxiGdalError> {
60//!     if !path.exists() {
61//!         return Err(OxiGdalError::io_error_builder("GeoTIFF file not found")
62//!             .with_path(path)
63//!             .with_operation("read_geotiff")
64//!             .with_suggestion("Verify the file path and ensure the file exists")
65//!             .build());
66//!     }
67//!     Ok(())
68//! }
69//! ```
70//!
71//! ## Parameter Validation with Constraints
72//!
73//! ```ignore
74//! use oxigdal_core::error::{OxiGdalError, Result};
75//!
76//! fn create_raster(width: usize, height: usize) -> Result<()> {
77//!     if width == 0 || width > 65535 {
78//!         return Err(OxiGdalError::invalid_parameter_builder("width", "must be between 1 and 65535")
79//!             .with_parameter("value", width.to_string())
80//!             .with_parameter("min", "1")
81//!             .with_parameter("max", "65535")
82//!             .with_operation("create_raster")
83//!             .build());
84//!     }
85//!     Ok(())
86//! }
87//! ```
88//!
89//! ## Format Error with Details
90//!
91//! ```ignore
92//! use oxigdal_core::error::{OxiGdalError, FormatError};
93//!
94//! fn parse_header(data: &[u8]) -> Result<(), OxiGdalError> {
95//!     if data.len() < 4 {
96//!         return Err(FormatError::InvalidHeader {
97//!             message: format!("Header too short: expected at least 4 bytes, got {}", data.len())
98//!         }.into());
99//!     }
100//!     Ok(())
101//! }
102//! ```
103//!
104//! ## CRS Transformation Error
105//!
106//! ```ignore
107//! use oxigdal_core::error::{OxiGdalError, CrsError};
108//!
109//! fn transform_coordinates(src_epsg: u32, dst_epsg: u32) -> Result<(), OxiGdalError> {
110//!     if src_epsg == dst_epsg {
111//!         return Err(CrsError::TransformationError {
112//!             source_crs: format!("EPSG:{}", src_epsg),
113//!             target_crs: format!("EPSG:{}", dst_epsg),
114//!             message: "Source and target CRS are identical".to_string(),
115//!         }.into());
116//!     }
117//!     Ok(())
118//! }
119//! ```
120
121pub mod builder;
122pub mod extensions;
123pub mod methods;
124pub mod types;
125
126pub use builder::*;
127pub use extensions::*;
128pub use types::*;
129
130/// The main result type for `OxiGDAL` operations
131pub type Result<T> = core::result::Result<T, OxiGdalError>;
132
133#[cfg(test)]
134mod tests {
135    #![allow(clippy::expect_used, clippy::useless_vec)]
136
137    use super::*;
138
139    #[test]
140    fn test_error_display() {
141        let err = OxiGdalError::InvalidParameter {
142            parameter: "width",
143            message: "must be positive".to_string(),
144        };
145        assert!(err.to_string().contains("width"));
146        assert!(err.to_string().contains("must be positive"));
147    }
148
149    #[test]
150    fn test_io_error_conversion() {
151        let io_err = IoError::NotFound {
152            path: "/test/path".to_string(),
153        };
154        let gdal_err: OxiGdalError = io_err.into();
155        assert!(matches!(
156            gdal_err,
157            OxiGdalError::Io(IoError::NotFound { .. })
158        ));
159    }
160
161    #[test]
162    fn test_format_error_conversion() {
163        let format_err = FormatError::InvalidMagic {
164            expected: &[0x49, 0x49],
165            actual: [0x00, 0x00, 0x00, 0x00],
166        };
167        let gdal_err: OxiGdalError = format_err.into();
168        assert!(matches!(
169            gdal_err,
170            OxiGdalError::Format(FormatError::InvalidMagic { .. })
171        ));
172    }
173
174    #[test]
175    fn test_error_codes() {
176        let err = OxiGdalError::InvalidParameter {
177            parameter: "test",
178            message: "test message".to_string(),
179        };
180        assert_eq!(err.code(), "E001");
181
182        let err = OxiGdalError::NotSupported {
183            operation: "test".to_string(),
184        };
185        assert_eq!(err.code(), "E002");
186
187        let io_err = IoError::NotFound {
188            path: "/test".to_string(),
189        };
190        assert_eq!(io_err.code(), "E100");
191    }
192
193    #[test]
194    fn test_error_suggestions() {
195        let err = OxiGdalError::InvalidParameter {
196            parameter: "test",
197            message: "test message".to_string(),
198        };
199        assert!(err.suggestion().is_some());
200        assert!(err.suggestion().is_some_and(|s| s.contains("parameter")));
201
202        let io_err = IoError::NotFound {
203            path: "/test".to_string(),
204        };
205        assert!(io_err.suggestion().is_some());
206        assert!(io_err.suggestion().is_some_and(|s| s.contains("file")));
207    }
208
209    #[test]
210    fn test_error_context() {
211        let err = OxiGdalError::InvalidParameter {
212            parameter: "test_param",
213            message: "test message".to_string(),
214        };
215        let ctx = err.context();
216        assert_eq!(ctx.category, "parameter_validation");
217
218        let io_err = IoError::NotFound {
219            path: "/test/path".to_string(),
220        };
221        let ctx = io_err.context();
222        assert_eq!(ctx.category, "file_not_found");
223    }
224
225    #[test]
226    fn test_error_aggregator() {
227        let mut agg = ErrorAggregator::new();
228        assert!(!agg.has_errors());
229        assert_eq!(agg.count(), 0);
230
231        agg.add(OxiGdalError::InvalidParameter {
232            parameter: "test1",
233            message: "error 1".to_string(),
234        });
235        assert!(agg.has_errors());
236        assert_eq!(agg.count(), 1);
237
238        agg.add(OxiGdalError::InvalidParameter {
239            parameter: "test2",
240            message: "error 2".to_string(),
241        });
242        assert_eq!(agg.count(), 2);
243
244        let result = agg.into_result();
245        assert!(result.is_err());
246    }
247
248    #[test]
249    fn test_error_aggregator_with_results() {
250        let mut agg = ErrorAggregator::new();
251
252        let ok_result: Result<i32> = Ok(42);
253        let value = agg.add_result(ok_result);
254        assert_eq!(value, Some(42));
255        assert!(!agg.has_errors());
256
257        let err_result: Result<i32> = Err(OxiGdalError::InvalidParameter {
258            parameter: "test",
259            message: "error".to_string(),
260        });
261        let value = agg.add_result(err_result);
262        assert_eq!(value, None);
263        assert!(agg.has_errors());
264        assert_eq!(agg.count(), 1);
265    }
266
267    #[test]
268    fn test_result_ext_context() {
269        let result: Result<i32> = Err(OxiGdalError::InvalidParameter {
270            parameter: "test",
271            message: "original".to_string(),
272        });
273
274        let with_ctx = result.context("added context");
275        assert!(with_ctx.is_err());
276        if let Err(e) = with_ctx {
277            assert!(matches!(e, OxiGdalError::Internal { .. }));
278        }
279    }
280
281    #[test]
282    fn test_result_ext_with_context() {
283        let result: Result<i32> = Err(OxiGdalError::InvalidParameter {
284            parameter: "test",
285            message: "original".to_string(),
286        });
287
288        let with_ctx = result.with_context(|| "lazy context".to_string());
289        assert!(with_ctx.is_err());
290        if let Err(e) = with_ctx {
291            assert!(matches!(e, OxiGdalError::Internal { .. }));
292        }
293    }
294
295    #[test]
296    #[cfg(feature = "std")]
297    fn test_from_path() {
298        use std::path::Path;
299
300        let path = Path::new("/test/file.tif");
301        let err = OxiGdalError::from_path(path, std::io::ErrorKind::NotFound);
302        assert!(matches!(err, OxiGdalError::Io(IoError::NotFound { .. })));
303
304        let err = OxiGdalError::from_path(path, std::io::ErrorKind::PermissionDenied);
305        assert!(matches!(
306            err,
307            OxiGdalError::Io(IoError::PermissionDenied { .. })
308        ));
309    }
310
311    #[test]
312    fn test_error_builder_basic() {
313        let builder = OxiGdalError::io_error_builder("Test error");
314        let err = builder.build();
315        assert!(matches!(err, OxiGdalError::Io(IoError::Read { .. })));
316    }
317
318    #[test]
319    #[cfg(feature = "std")]
320    fn test_error_builder_with_path() {
321        use std::path::Path;
322
323        let builder = OxiGdalError::io_error_builder("Cannot read file")
324            .with_path(Path::new("/data/test.tif"));
325
326        assert_eq!(builder.file_path(), Some(Path::new("/data/test.tif")));
327
328        let err = builder.build();
329        assert!(matches!(err, OxiGdalError::Io(IoError::Read { .. })));
330    }
331
332    #[test]
333    fn test_error_builder_with_operation() {
334        let builder = OxiGdalError::io_error_builder("Test error").with_operation("read_raster");
335
336        assert_eq!(builder.operation_name(), Some("read_raster"));
337
338        let err = builder.build();
339        assert!(matches!(err, OxiGdalError::Io(IoError::Read { .. })));
340    }
341
342    #[test]
343    fn test_error_builder_with_parameters() {
344        let builder = OxiGdalError::invalid_parameter_builder("width", "must be positive")
345            .with_parameter("value", "-10")
346            .with_parameter("minimum", "1");
347
348        let params = builder.parameters();
349        assert_eq!(params.get("value"), Some(&"-10".to_string()));
350        assert_eq!(params.get("minimum"), Some(&"1".to_string()));
351
352        let err = builder.build();
353        assert!(matches!(err, OxiGdalError::InvalidParameter { .. }));
354    }
355
356    #[test]
357    fn test_error_builder_with_suggestion() {
358        let builder = OxiGdalError::io_error_builder("Cannot read file")
359            .with_suggestion("Check file permissions and ensure the file exists");
360
361        let suggestion = builder.suggestion();
362        assert_eq!(
363            suggestion,
364            Some("Check file permissions and ensure the file exists".to_string())
365        );
366
367        let err = builder.build();
368        assert!(matches!(err, OxiGdalError::Io(IoError::Read { .. })));
369    }
370
371    #[test]
372    fn test_error_builder_custom_suggestion_overrides_default() {
373        let builder = OxiGdalError::invalid_parameter_builder("test", "invalid")
374            .with_suggestion("Custom suggestion");
375
376        let suggestion = builder.suggestion();
377        assert_eq!(suggestion, Some("Custom suggestion".to_string()));
378    }
379
380    #[test]
381    #[cfg(feature = "std")]
382    fn test_error_builder_fluent_api() {
383        use std::path::Path;
384
385        let builder = OxiGdalError::io_error_builder("Cannot read file")
386            .with_path(Path::new("/data/test.tif"))
387            .with_operation("read_raster")
388            .with_parameter("band", "1")
389            .with_parameter("window", "0,0,512,512")
390            .with_suggestion("Verify file exists and is accessible");
391
392        assert_eq!(builder.file_path(), Some(Path::new("/data/test.tif")));
393        assert_eq!(builder.operation_name(), Some("read_raster"));
394        assert_eq!(builder.parameters().get("band"), Some(&"1".to_string()));
395        assert_eq!(
396            builder.parameters().get("window"),
397            Some(&"0,0,512,512".to_string())
398        );
399        assert!(builder.suggestion().is_some());
400
401        let err = builder.build();
402        assert!(matches!(err, OxiGdalError::Io(IoError::Read { .. })));
403    }
404
405    #[test]
406    fn test_error_builder_context() {
407        let builder = OxiGdalError::invalid_parameter_builder("width", "must be positive")
408            .with_parameter("value", "-10")
409            .with_operation("create_raster");
410
411        let ctx = builder.build_context();
412        assert_eq!(ctx.category, "parameter_validation");
413        assert!(ctx.operation.is_some());
414        assert_eq!(ctx.operation.as_deref(), Some("create_raster"));
415        assert!(!ctx.parameters.is_empty());
416    }
417
418    #[test]
419    fn test_error_builder_into_error() {
420        let builder = OxiGdalError::io_error_builder("Test error");
421        let err = builder.into_error();
422        assert!(matches!(err, OxiGdalError::Io(IoError::Read { .. })));
423    }
424
425    #[test]
426    fn test_error_builder_error_ref() {
427        let builder = OxiGdalError::io_error_builder("Test error");
428        let err_ref = builder.error();
429        assert!(matches!(err_ref, OxiGdalError::Io(IoError::Read { .. })));
430    }
431
432    #[test]
433    fn test_error_builder_with_multiple_parameters() {
434        let mut builder = OxiGdalError::invalid_parameter_builder("size", "invalid");
435        builder = builder.with_parameter("width", "1024");
436        builder = builder.with_parameter("height", "768");
437        builder = builder.with_parameter("bands", "3");
438
439        let params = builder.parameters();
440        assert_eq!(params.len(), 3);
441        assert_eq!(params.get("width"), Some(&"1024".to_string()));
442        assert_eq!(params.get("height"), Some(&"768".to_string()));
443        assert_eq!(params.get("bands"), Some(&"3".to_string()));
444    }
445
446    #[test]
447    fn test_error_builder_allocation_error() {
448        let builder = OxiGdalError::allocation_error_builder("Failed to allocate buffer");
449        let err = builder.build();
450        assert!(matches!(err, OxiGdalError::Internal { .. }));
451        assert!(err.to_string().contains("Allocation error"));
452    }
453
454    #[test]
455    fn test_error_builder_invalid_state() {
456        let builder = OxiGdalError::invalid_state_builder("Dataset already closed");
457        let err = builder.build();
458        assert!(matches!(err, OxiGdalError::Internal { .. }));
459        assert!(err.to_string().contains("Invalid state"));
460    }
461
462    #[test]
463    fn test_error_builder_not_supported() {
464        let builder = OxiGdalError::not_supported_builder("write_compressed_tiff");
465        let err = builder.build();
466        assert!(matches!(err, OxiGdalError::NotSupported { .. }));
467    }
468
469    #[test]
470    fn test_error_builder_edge_cases() {
471        // Empty operation name
472        let builder = OxiGdalError::io_error_builder("test").with_operation("");
473        assert_eq!(builder.operation_name(), Some(""));
474
475        // Empty parameter values
476        let builder = OxiGdalError::io_error_builder("test").with_parameter("key", "");
477        assert_eq!(builder.parameters().get("key"), Some(&"".to_string()));
478
479        // Empty suggestion
480        let builder = OxiGdalError::io_error_builder("test").with_suggestion("");
481        assert_eq!(builder.suggestion(), Some("".to_string()));
482    }
483
484    #[test]
485    #[cfg(feature = "std")]
486    fn test_error_context_with_builder_fields() {
487        use std::path::Path;
488
489        let builder = OxiGdalError::io_error_builder("Test")
490            .with_path(Path::new("/test"))
491            .with_operation("test_op")
492            .with_parameter("key", "value")
493            .with_suggestion("test suggestion");
494
495        let ctx = builder.build_context();
496        assert!(ctx.file_path.is_some());
497        assert_eq!(ctx.file_path.as_deref(), Some(Path::new("/test")));
498        assert_eq!(ctx.operation.as_deref(), Some("test_op"));
499        assert_eq!(ctx.parameters.get("key"), Some(&"value".to_string()));
500        assert_eq!(ctx.custom_suggestion.as_deref(), Some("test suggestion"));
501    }
502
503    // Integration tests for error code consistency
504    #[test]
505    fn test_error_code_consistency_io_errors() {
506        // Verify all I/O error codes are unique and in expected range
507        let errors = vec![
508            IoError::NotFound {
509                path: "test".to_string(),
510            },
511            IoError::PermissionDenied {
512                path: "test".to_string(),
513            },
514            IoError::Network {
515                message: "test".to_string(),
516            },
517            IoError::UnexpectedEof { offset: 0 },
518            IoError::Read {
519                message: "test".to_string(),
520            },
521            IoError::Write {
522                message: "test".to_string(),
523            },
524            IoError::Seek { position: 0 },
525            IoError::Http {
526                status: 404,
527                message: "test".to_string(),
528            },
529        ];
530
531        let codes: Vec<&str> = errors.iter().map(|e| e.code()).collect();
532
533        // All codes should start with E1 (I/O error range)
534        for code in &codes {
535            assert!(code.starts_with("E1"));
536        }
537
538        // All codes should be unique
539        for (i, code1) in codes.iter().enumerate() {
540            for (j, code2) in codes.iter().enumerate() {
541                if i != j {
542                    assert_ne!(code1, code2, "Duplicate error codes found");
543                }
544            }
545        }
546    }
547
548    #[test]
549    fn test_error_code_consistency_format_errors() {
550        // Verify all format error codes are unique and in expected range
551        let errors = vec![
552            FormatError::InvalidMagic {
553                expected: &[0x49],
554                actual: [0, 0, 0, 0],
555            },
556            FormatError::InvalidHeader {
557                message: "test".to_string(),
558            },
559            FormatError::UnsupportedVersion { version: 1 },
560            FormatError::InvalidTag {
561                tag: 256,
562                message: "test".to_string(),
563            },
564            FormatError::MissingTag { tag: "test" },
565            FormatError::InvalidDataType { type_id: 1 },
566            FormatError::CorruptData {
567                offset: 0,
568                message: "test".to_string(),
569            },
570            FormatError::InvalidGeoKey {
571                key_id: 1024,
572                message: "test".to_string(),
573            },
574        ];
575
576        let codes: Vec<&str> = errors.iter().map(|e| e.code()).collect();
577
578        // All codes should start with E2 (format error range)
579        for code in &codes {
580            assert!(code.starts_with("E2"));
581        }
582
583        // All codes should be unique
584        for (i, code1) in codes.iter().enumerate() {
585            for (j, code2) in codes.iter().enumerate() {
586                if i != j {
587                    assert_ne!(code1, code2, "Duplicate error codes found");
588                }
589            }
590        }
591    }
592
593    #[test]
594    fn test_error_code_consistency_crs_errors() {
595        let errors = vec![
596            CrsError::UnknownCrs {
597                identifier: "test".to_string(),
598            },
599            CrsError::InvalidWkt {
600                message: "test".to_string(),
601            },
602            CrsError::InvalidEpsg { code: 0 },
603            CrsError::TransformationError {
604                source_crs: "EPSG:4326".to_string(),
605                target_crs: "EPSG:3857".to_string(),
606                message: "test".to_string(),
607            },
608            CrsError::DatumNotFound {
609                datum: "WGS84".to_string(),
610            },
611        ];
612
613        let codes: Vec<&str> = errors.iter().map(|e| e.code()).collect();
614
615        // All codes should start with E3 (CRS error range)
616        for code in &codes {
617            assert!(code.starts_with("E3"));
618        }
619
620        // All codes should be unique
621        for (i, code1) in codes.iter().enumerate() {
622            for (j, code2) in codes.iter().enumerate() {
623                if i != j {
624                    assert_ne!(code1, code2, "Duplicate error codes found");
625                }
626            }
627        }
628    }
629
630    #[test]
631    fn test_error_code_consistency_compression_errors() {
632        let errors = vec![
633            CompressionError::UnknownMethod { method: 99 },
634            CompressionError::DecompressionFailed {
635                message: "test".to_string(),
636            },
637            CompressionError::CompressionFailed {
638                message: "test".to_string(),
639            },
640            CompressionError::InvalidData {
641                message: "test".to_string(),
642            },
643        ];
644
645        let codes: Vec<&str> = errors.iter().map(|e| e.code()).collect();
646
647        // All codes should start with E4 (compression error range)
648        for code in &codes {
649            assert!(code.starts_with("E4"));
650        }
651
652        // All codes should be unique
653        for (i, code1) in codes.iter().enumerate() {
654            for (j, code2) in codes.iter().enumerate() {
655                if i != j {
656                    assert_ne!(code1, code2, "Duplicate error codes found");
657                }
658            }
659        }
660    }
661
662    #[test]
663    fn test_error_code_consistency_top_level_errors() {
664        let errors = vec![
665            OxiGdalError::InvalidParameter {
666                parameter: "test",
667                message: "test".to_string(),
668            },
669            OxiGdalError::NotSupported {
670                operation: "test".to_string(),
671            },
672            OxiGdalError::OutOfBounds {
673                message: "test".to_string(),
674            },
675            OxiGdalError::Internal {
676                message: "test".to_string(),
677            },
678        ];
679
680        let codes: Vec<&str> = errors.iter().map(|e| e.code()).collect();
681
682        // All codes should start with E0 (top-level error range)
683        for code in &codes {
684            assert!(code.starts_with("E0"));
685        }
686
687        // All codes should be unique
688        for (i, code1) in codes.iter().enumerate() {
689            for (j, code2) in codes.iter().enumerate() {
690                if i != j {
691                    assert_ne!(code1, code2, "Duplicate error codes found");
692                }
693            }
694        }
695    }
696
697    // Integration tests for suggestion quality
698    #[test]
699    fn test_suggestion_quality_io_errors() {
700        let test_cases = vec![
701            (
702                IoError::NotFound {
703                    path: "/test".to_string(),
704                },
705                vec!["file", "path", "exist"],
706            ),
707            (
708                IoError::PermissionDenied {
709                    path: "/test".to_string(),
710                },
711                vec!["permission"],
712            ),
713            (
714                IoError::Network {
715                    message: "timeout".to_string(),
716                },
717                vec!["network", "connectivity"],
718            ),
719            (
720                IoError::UnexpectedEof { offset: 100 },
721                vec!["truncated", "corrupted"],
722            ),
723            (
724                IoError::Http {
725                    status: 404,
726                    message: "Not Found".to_string(),
727                },
728                vec!["not found", "resource"],
729            ),
730            (
731                IoError::Http {
732                    status: 403,
733                    message: "Forbidden".to_string(),
734                },
735                vec!["forbidden", "authentication", "credentials"],
736            ),
737            (
738                IoError::Http {
739                    status: 500,
740                    message: "Server Error".to_string(),
741                },
742                vec!["server", "later"],
743            ),
744        ];
745
746        for (error, keywords) in test_cases {
747            let suggestion = error.suggestion();
748            assert!(
749                suggestion.is_some(),
750                "Error should have a suggestion: {:?}",
751                error
752            );
753
754            let suggestion_text = suggestion.expect("Expected suggestion").to_lowercase();
755            let has_keyword = keywords.iter().any(|kw| suggestion_text.contains(kw));
756            assert!(
757                has_keyword,
758                "Suggestion '{}' should contain at least one keyword from {:?}",
759                suggestion_text, keywords
760            );
761        }
762    }
763
764    #[test]
765    fn test_suggestion_quality_format_errors() {
766        let test_cases = vec![
767            (
768                FormatError::InvalidMagic {
769                    expected: &[0x49, 0x49],
770                    actual: [0, 0, 0, 0],
771                },
772                vec!["format", "file type", "verify"],
773            ),
774            (
775                FormatError::UnsupportedVersion { version: 999 },
776                vec!["version", "supported", "converting"],
777            ),
778            (
779                FormatError::MissingTag { tag: "ImageWidth" },
780                vec!["required", "missing", "incomplete", "corrupted"],
781            ),
782            (
783                FormatError::CorruptData {
784                    offset: 1024,
785                    message: "checksum mismatch".to_string(),
786                },
787                vec!["corruption", "backup", "recovering"],
788            ),
789        ];
790
791        for (error, keywords) in test_cases {
792            let suggestion = error.suggestion();
793            assert!(
794                suggestion.is_some(),
795                "Error should have a suggestion: {:?}",
796                error
797            );
798
799            let suggestion_text = suggestion.expect("Expected suggestion").to_lowercase();
800            let has_keyword = keywords.iter().any(|kw| suggestion_text.contains(kw));
801            assert!(
802                has_keyword,
803                "Suggestion '{}' should contain at least one keyword from {:?}",
804                suggestion_text, keywords
805            );
806        }
807    }
808
809    #[test]
810    fn test_suggestion_quality_crs_errors() {
811        let test_cases = vec![
812            (
813                CrsError::UnknownCrs {
814                    identifier: "CUSTOM:123".to_string(),
815                },
816                vec!["verify", "epsg", "identifier"],
817            ),
818            (
819                CrsError::InvalidWkt {
820                    message: "parse error".to_string(),
821                },
822                vec!["wkt", "syntax", "bracket"],
823            ),
824            (
825                CrsError::InvalidEpsg { code: 999999 },
826                vec!["valid", "epsg.io"],
827            ),
828            (
829                CrsError::TransformationError {
830                    source_crs: "EPSG:4326".to_string(),
831                    target_crs: "CUSTOM:1".to_string(),
832                    message: "no transformation path".to_string(),
833                },
834                vec!["compatible", "transformation", "parameters"],
835            ),
836        ];
837
838        for (error, keywords) in test_cases {
839            let suggestion = error.suggestion();
840            assert!(
841                suggestion.is_some(),
842                "Error should have a suggestion: {:?}",
843                error
844            );
845
846            let suggestion_text = suggestion.expect("Expected suggestion").to_lowercase();
847            let has_keyword = keywords.iter().any(|kw| suggestion_text.contains(kw));
848            assert!(
849                has_keyword,
850                "Suggestion '{}' should contain at least one keyword from {:?}",
851                suggestion_text, keywords
852            );
853        }
854    }
855
856    #[test]
857    fn test_suggestion_quality_top_level_errors() {
858        let test_cases = vec![
859            (
860                OxiGdalError::InvalidParameter {
861                    parameter: "width",
862                    message: "must be positive".to_string(),
863                },
864                vec!["parameter", "documentation", "valid"],
865            ),
866            (
867                OxiGdalError::NotSupported {
868                    operation: "write_jp2".to_string(),
869                },
870                vec!["feature", "enabled", "alternative"],
871            ),
872            (
873                OxiGdalError::OutOfBounds {
874                    message: "index out of range".to_string(),
875                },
876                vec!["verify", "indices", "range", "valid"],
877            ),
878            (
879                OxiGdalError::Internal {
880                    message: "unexpected null pointer".to_string(),
881                },
882                vec!["bug", "report"],
883            ),
884        ];
885
886        for (error, keywords) in test_cases {
887            let suggestion = error.suggestion();
888            assert!(
889                suggestion.is_some(),
890                "Error should have a suggestion: {:?}",
891                error
892            );
893
894            let suggestion_text = suggestion.expect("Expected suggestion").to_lowercase();
895            let has_keyword = keywords.iter().any(|kw| suggestion_text.contains(kw));
896            assert!(
897                has_keyword,
898                "Suggestion '{}' should contain at least one keyword from {:?}",
899                suggestion_text, keywords
900            );
901        }
902    }
903
904    // Integration tests for context propagation
905    #[test]
906    fn test_context_propagation_io_errors() {
907        let error = IoError::NotFound {
908            path: "/data/test.tif".to_string(),
909        };
910        let context = error.context();
911
912        assert_eq!(context.category, "file_not_found");
913        assert!(!context.details.is_empty());
914
915        let path_detail = context.details.iter().find(|(k, _)| k == "path");
916        assert!(path_detail.is_some());
917        assert_eq!(
918            path_detail.expect("Expected path detail").1,
919            "/data/test.tif"
920        );
921    }
922
923    #[test]
924    fn test_context_propagation_format_errors() {
925        let error = FormatError::InvalidTag {
926            tag: 256,
927            message: "unsupported tag type".to_string(),
928        };
929        let context = error.context();
930
931        assert_eq!(context.category, "invalid_tag");
932        assert!(!context.details.is_empty());
933
934        let tag_detail = context.details.iter().find(|(k, _)| k == "tag");
935        assert!(tag_detail.is_some());
936        assert_eq!(tag_detail.expect("Expected tag detail").1, "256");
937
938        let message_detail = context.details.iter().find(|(k, _)| k == "message");
939        assert!(message_detail.is_some());
940        assert_eq!(
941            message_detail.expect("Expected message detail").1,
942            "unsupported tag type"
943        );
944    }
945
946    #[test]
947    fn test_context_propagation_crs_errors() {
948        let error = CrsError::TransformationError {
949            source_crs: "EPSG:4326".to_string(),
950            target_crs: "EPSG:3857".to_string(),
951            message: "datum shift required".to_string(),
952        };
953        let context = error.context();
954
955        assert_eq!(context.category, "transformation_error");
956        assert!(!context.details.is_empty());
957
958        let src_detail = context.details.iter().find(|(k, _)| k == "source_crs");
959        assert!(src_detail.is_some());
960        assert_eq!(
961            src_detail.expect("Expected source_crs detail").1,
962            "EPSG:4326"
963        );
964
965        let tgt_detail = context.details.iter().find(|(k, _)| k == "target_crs");
966        assert!(tgt_detail.is_some());
967        assert_eq!(
968            tgt_detail.expect("Expected target_crs detail").1,
969            "EPSG:3857"
970        );
971    }
972
973    #[test]
974    fn test_context_propagation_through_conversion() {
975        let io_error = IoError::Network {
976            message: "connection timeout".to_string(),
977        };
978        let gdal_error: OxiGdalError = io_error.into();
979
980        let context = gdal_error.context();
981        assert_eq!(context.category, "network_error");
982
983        let msg_detail = context.details.iter().find(|(k, _)| k == "message");
984        assert!(msg_detail.is_some());
985        assert_eq!(
986            msg_detail.expect("Expected message detail").1,
987            "connection timeout"
988        );
989    }
990
991    #[test]
992    #[cfg(feature = "std")]
993    fn test_context_propagation_with_error_builder() {
994        use std::path::Path;
995
996        let builder = OxiGdalError::io_error_builder("Cannot read GeoTIFF")
997            .with_path(Path::new("/data/terrain.tif"))
998            .with_operation("read_geotiff")
999            .with_parameter("band", "1")
1000            .with_parameter("window", "0,0,512,512");
1001
1002        let context = builder.build_context();
1003
1004        // Verify file path is propagated
1005        assert_eq!(
1006            context.file_path,
1007            Some(Path::new("/data/terrain.tif").to_path_buf())
1008        );
1009
1010        // Verify operation is propagated
1011        assert_eq!(context.operation.as_deref(), Some("read_geotiff"));
1012
1013        // Verify parameters are propagated
1014        assert_eq!(context.parameters.get("band"), Some(&"1".to_string()));
1015        assert_eq!(
1016            context.parameters.get("window"),
1017            Some(&"0,0,512,512".to_string())
1018        );
1019    }
1020
1021    #[test]
1022    fn test_error_builder_context_with_custom_suggestion() {
1023        let builder = OxiGdalError::invalid_parameter_builder("buffer_size", "must be power of 2")
1024            .with_parameter("value", "1000")
1025            .with_suggestion("Use 512, 1024, 2048, or 4096");
1026
1027        let context = builder.build_context();
1028
1029        // Verify custom suggestion is in context
1030        assert_eq!(
1031            context.custom_suggestion.as_deref(),
1032            Some("Use 512, 1024, 2048, or 4096")
1033        );
1034    }
1035
1036    #[test]
1037    fn test_error_context_detail_chain() {
1038        let mut context = ErrorContext::new("test_category");
1039        context = context
1040            .with_detail("key1", "value1")
1041            .with_detail("key2", "value2")
1042            .with_detail("key3", "value3");
1043
1044        assert_eq!(context.category, "test_category");
1045        assert_eq!(context.details.len(), 3);
1046
1047        // Verify order is preserved
1048        assert_eq!(
1049            context.details[0],
1050            ("key1".to_string(), "value1".to_string())
1051        );
1052        assert_eq!(
1053            context.details[1],
1054            ("key2".to_string(), "value2".to_string())
1055        );
1056        assert_eq!(
1057            context.details[2],
1058            ("key3".to_string(), "value3".to_string())
1059        );
1060    }
1061
1062    #[test]
1063    fn test_error_builder_into_conversion() {
1064        let builder = OxiGdalError::io_error_builder("Test error");
1065        let error: OxiGdalError = builder.into();
1066
1067        assert!(matches!(error, OxiGdalError::Io(IoError::Read { .. })));
1068    }
1069
1070    #[test]
1071    fn test_comprehensive_error_workflow() {
1072        // This test simulates a complete error handling workflow
1073
1074        // Step 1: Create an error with full context
1075        #[cfg(feature = "std")]
1076        let error = {
1077            use std::path::Path;
1078            OxiGdalError::io_error_builder("Cannot open GeoTIFF file")
1079                .with_path(Path::new("/data/terrain.tif"))
1080                .with_operation("open_geotiff")
1081                .with_parameter("mode", "read")
1082                .with_suggestion("Check if file exists and is readable")
1083                .build()
1084        };
1085
1086        #[cfg(not(feature = "std"))]
1087        let error = OxiGdalError::io_error("Cannot open GeoTIFF file");
1088
1089        // Step 2: Verify error code
1090        assert_eq!(error.code(), "E104");
1091
1092        // Step 3: Verify suggestion is present
1093        let suggestion = error.suggestion();
1094        assert!(suggestion.is_some());
1095
1096        // Step 4: Get context
1097        let context = error.context();
1098        assert!(!context.details.is_empty());
1099
1100        // Step 5: Verify error can be displayed
1101        let error_string = error.to_string();
1102        assert!(!error_string.is_empty());
1103    }
1104}