memscope_rs/cli/commands/html_from_json/
error_handler.rs

1//! Comprehensive error handling module for HTML generation
2//!
3//! This module provides detailed error handling, recovery mechanisms,
4//! and user-friendly error messages for the HTML generation process.
5
6use serde_json::Value;
7use std::error::Error;
8use std::fmt;
9use std::path::PathBuf;
10use std::time::Instant;
11
12/// Comprehensive error types for HTML generation
13#[derive(Debug)]
14pub enum HtmlGenerationError {
15    /// JSON file discovery failed
16    FileDiscoveryError {
17        /// Directory that was searched
18        directory: String,
19        /// Base name pattern used
20        base_name: String,
21        /// Underlying error
22        source: Box<dyn Error + Send + Sync>,
23        /// Suggested recovery actions
24        recovery_suggestions: Vec<String>,
25    },
26    /// JSON file loading failed
27    FileLoadingError {
28        /// Path to the file that failed
29        file_path: PathBuf,
30        /// File type (e.g., "memory_analysis")
31        file_type: String,
32        /// File size in bytes
33        file_size: usize,
34        /// Underlying error
35        source: Box<dyn Error + Send + Sync>,
36        /// Whether this is a recoverable error
37        recoverable: bool,
38        /// Recovery suggestions
39        recovery_suggestions: Vec<String>,
40    },
41    /// JSON parsing failed
42    JsonParsingError {
43        /// Path to the file with parsing error
44        file_path: PathBuf,
45        /// Line number where error occurred (if available)
46        line_number: Option<usize>,
47        /// Column number where error occurred (if available)
48        column_number: Option<usize>,
49        /// Parsing error details
50        parsing_error: String,
51        /// JSON content snippet around error (if available)
52        content_snippet: Option<String>,
53        /// Recovery suggestions
54        recovery_suggestions: Vec<String>,
55    },
56    /// Data validation failed
57    ValidationError {
58        /// File that failed validation
59        file_path: PathBuf,
60        /// Type of validation that failed
61        validation_type: String,
62        /// Specific validation error
63        validation_error: String,
64        /// Expected data structure
65        expected_structure: String,
66        /// Actual data structure found
67        actual_structure: String,
68        /// Recovery suggestions
69        recovery_suggestions: Vec<String>,
70    },
71    /// Data normalization failed
72    NormalizationError {
73        /// Stage where normalization failed
74        stage: String,
75        /// Number of items processed before failure
76        processed_count: usize,
77        /// Total items to process
78        total_count: usize,
79        /// Underlying error
80        source: Box<dyn Error + Send + Sync>,
81        /// Whether partial data can be used
82        partial_data_available: bool,
83        /// Recovery suggestions
84        recovery_suggestions: Vec<String>,
85    },
86    /// Template generation failed
87    TemplateError {
88        /// Template stage that failed
89        stage: String,
90        /// Template size processed so far
91        processed_size: usize,
92        /// Underlying error
93        source: Box<dyn Error + Send + Sync>,
94        /// Recovery suggestions
95        recovery_suggestions: Vec<String>,
96    },
97    /// Memory limit exceeded
98    MemoryLimitError {
99        /// Current memory usage in bytes
100        current_usage: usize,
101        /// Memory limit in bytes
102        memory_limit: usize,
103        /// Operation that caused the limit to be exceeded
104        operation: String,
105        /// Recovery suggestions
106        recovery_suggestions: Vec<String>,
107    },
108    /// Multiple errors occurred
109    MultipleErrors {
110        /// List of errors that occurred
111        errors: Vec<HtmlGenerationError>,
112        /// Whether processing can continue with partial data
113        can_continue: bool,
114        /// Recovery suggestions
115        recovery_suggestions: Vec<String>,
116    },
117}
118
119impl fmt::Display for HtmlGenerationError {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        match self {
122            HtmlGenerationError::FileDiscoveryError {
123                directory,
124                base_name,
125                source,
126                recovery_suggestions,
127            } => {
128                writeln!(f, "āŒ File Discovery Error")?;
129                writeln!(f, "   Directory: {directory}")?;
130                writeln!(f, "   Base name: {base_name}")?;
131                writeln!(f, "   Error: {source}")?;
132                if !recovery_suggestions.is_empty() {
133                    writeln!(f, "   šŸ’” Suggestions:")?;
134                    for suggestion in recovery_suggestions {
135                        writeln!(f, "      • {suggestion}")?;
136                    }
137                }
138                Ok(())
139            }
140            HtmlGenerationError::FileLoadingError {
141                file_path,
142                file_type,
143                file_size,
144                source,
145                recoverable,
146                recovery_suggestions,
147            } => {
148                writeln!(f, "āŒ File Loading Error")?;
149                writeln!(f, "   File: {}", file_path.display())?;
150                writeln!(f, "   Type: {file_type}")?;
151                writeln!(f, "   Size: {:.1} KB", *file_size as f64 / 1024.0)?;
152                writeln!(f, "   Error: {source}")?;
153                writeln!(
154                    f,
155                    "   Recoverable: {}",
156                    if *recoverable { "Yes" } else { "No" }
157                )?;
158                if !recovery_suggestions.is_empty() {
159                    writeln!(f, "   šŸ’” Suggestions:")?;
160                    for suggestion in recovery_suggestions {
161                        writeln!(f, "      • {suggestion}")?;
162                    }
163                }
164                Ok(())
165            }
166            HtmlGenerationError::JsonParsingError {
167                file_path,
168                line_number,
169                column_number,
170                parsing_error,
171                content_snippet,
172                recovery_suggestions,
173            } => {
174                writeln!(f, "āŒ JSON Parsing Error")?;
175                writeln!(f, "   File: {}", file_path.display())?;
176                if let Some(line) = line_number {
177                    write!(f, "   Location: line {line}")?;
178                    if let Some(col) = column_number {
179                        write!(f, ", column {col}")?;
180                    }
181                    writeln!(f)?;
182                }
183                writeln!(f, "   Error: {parsing_error}")?;
184                if let Some(snippet) = content_snippet {
185                    writeln!(f, "   Context:")?;
186                    writeln!(f, "   {snippet}")?;
187                }
188                if !recovery_suggestions.is_empty() {
189                    writeln!(f, "   šŸ’” Suggestions:")?;
190                    for suggestion in recovery_suggestions {
191                        writeln!(f, "      • {suggestion}")?;
192                    }
193                }
194                Ok(())
195            }
196            HtmlGenerationError::ValidationError {
197                file_path,
198                validation_type,
199                validation_error,
200                expected_structure,
201                actual_structure,
202                recovery_suggestions,
203            } => {
204                writeln!(f, "āŒ Data Validation Error")?;
205                writeln!(f, "   File: {}", file_path.display())?;
206                writeln!(f, "   Validation: {validation_type}")?;
207                writeln!(f, "   Error: {validation_error}")?;
208                writeln!(f, "   Expected: {expected_structure}")?;
209                writeln!(f, "   Found: {actual_structure}")?;
210                if !recovery_suggestions.is_empty() {
211                    writeln!(f, "   šŸ’” Suggestions:")?;
212                    for suggestion in recovery_suggestions {
213                        writeln!(f, "      • {suggestion}")?;
214                    }
215                }
216                Ok(())
217            }
218            HtmlGenerationError::NormalizationError {
219                stage,
220                processed_count,
221                total_count,
222                source,
223                partial_data_available,
224                recovery_suggestions,
225            } => {
226                writeln!(f, "āŒ Data Normalization Error")?;
227                writeln!(f, "   Stage: {stage}")?;
228                writeln!(
229                    f,
230                    "   Progress: {processed_count}/{total_count} ({:.1}%)",
231                    (*processed_count as f64 / *total_count as f64) * 100.0
232                )?;
233                writeln!(f, "   Error: {source}")?;
234                writeln!(
235                    f,
236                    "   Partial data available: {}",
237                    if *partial_data_available { "Yes" } else { "No" }
238                )?;
239                if !recovery_suggestions.is_empty() {
240                    writeln!(f, "   šŸ’” Suggestions:")?;
241                    for suggestion in recovery_suggestions {
242                        writeln!(f, "      • {suggestion}")?;
243                    }
244                }
245                Ok(())
246            }
247            HtmlGenerationError::TemplateError {
248                stage,
249                processed_size,
250                source,
251                recovery_suggestions,
252            } => {
253                writeln!(f, "āŒ Template Generation Error")?;
254                writeln!(f, "   Stage: {stage}")?;
255                writeln!(f, "   Processed: {:.1} KB", *processed_size as f64 / 1024.0)?;
256                writeln!(f, "   Error: {source}")?;
257                if !recovery_suggestions.is_empty() {
258                    writeln!(f, "   šŸ’” Suggestions:")?;
259                    for suggestion in recovery_suggestions {
260                        writeln!(f, "      • {suggestion}")?;
261                    }
262                }
263                Ok(())
264            }
265            HtmlGenerationError::MemoryLimitError {
266                current_usage,
267                memory_limit,
268                operation,
269                recovery_suggestions,
270            } => {
271                writeln!(f, "āŒ Memory Limit Exceeded")?;
272                writeln!(
273                    f,
274                    "   Current usage: {:.1} MB",
275                    *current_usage as f64 / 1024.0 / 1024.0
276                )?;
277                writeln!(
278                    f,
279                    "   Memory limit: {:.1} MB",
280                    *memory_limit as f64 / 1024.0 / 1024.0
281                )?;
282                writeln!(f, "   Operation: {operation}")?;
283                writeln!(
284                    f,
285                    "   Usage: {:.1}%",
286                    (*current_usage as f64 / *memory_limit as f64) * 100.0
287                )?;
288                if !recovery_suggestions.is_empty() {
289                    writeln!(f, "   šŸ’” Suggestions:")?;
290                    for suggestion in recovery_suggestions {
291                        writeln!(f, "      • {suggestion}")?;
292                    }
293                }
294                Ok(())
295            }
296            HtmlGenerationError::MultipleErrors {
297                errors,
298                can_continue,
299                recovery_suggestions,
300            } => {
301                writeln!(f, "āŒ Multiple Errors Occurred ({} errors)", errors.len())?;
302                writeln!(
303                    f,
304                    "   Can continue: {}",
305                    if *can_continue { "Yes" } else { "No" }
306                )?;
307                for (i, error) in errors.iter().enumerate() {
308                    writeln!(f, "   Error {}: {}", i + 1, error)?;
309                }
310                if !recovery_suggestions.is_empty() {
311                    writeln!(f, "   šŸ’” Suggestions:")?;
312                    for suggestion in recovery_suggestions {
313                        writeln!(f, "      • {suggestion}")?;
314                    }
315                }
316                Ok(())
317            }
318        }
319    }
320}
321
322impl Error for HtmlGenerationError {
323    fn source(&self) -> Option<&(dyn Error + 'static)> {
324        match self {
325            HtmlGenerationError::FileDiscoveryError { source, .. } => Some(source.as_ref()),
326            HtmlGenerationError::FileLoadingError { source, .. } => Some(source.as_ref()),
327            HtmlGenerationError::NormalizationError { source, .. } => Some(source.as_ref()),
328            HtmlGenerationError::TemplateError { source, .. } => Some(source.as_ref()),
329            _ => None,
330        }
331    }
332}
333
334/// Error recovery context
335#[derive(Debug, Clone)]
336pub struct ErrorRecoveryContext {
337    /// Whether to attempt recovery
338    pub attempt_recovery: bool,
339    /// Maximum number of retry attempts
340    pub max_retries: usize,
341    /// Whether to continue with partial data
342    pub allow_partial_data: bool,
343    /// Whether to use fallback mechanisms
344    pub use_fallbacks: bool,
345    /// Verbose error reporting
346    pub verbose_errors: bool,
347}
348
349impl Default for ErrorRecoveryContext {
350    fn default() -> Self {
351        Self {
352            attempt_recovery: true,
353            max_retries: 3,
354            allow_partial_data: true,
355            use_fallbacks: true,
356            verbose_errors: false,
357        }
358    }
359}
360
361/// Error recovery statistics
362#[derive(Debug, Default)]
363pub struct ErrorRecoveryStats {
364    /// Total errors encountered
365    pub total_errors: usize,
366    /// Errors successfully recovered
367    pub recovered_errors: usize,
368    /// Errors that required fallbacks
369    pub fallback_errors: usize,
370    /// Errors that were unrecoverable
371    pub unrecoverable_errors: usize,
372    /// Total retry attempts made
373    pub retry_attempts: usize,
374    /// Time spent on error recovery
375    pub recovery_time_ms: u64,
376}
377
378/// Comprehensive error handler for HTML generation
379pub struct HtmlErrorHandler {
380    /// Recovery context configuration
381    recovery_context: ErrorRecoveryContext,
382    /// Recovery statistics
383    stats: ErrorRecoveryStats,
384    /// Start time for timing recovery operations
385    start_time: Option<Instant>,
386}
387
388impl HtmlErrorHandler {
389    /// Create a new error handler with default settings
390    pub fn new() -> Self {
391        Self {
392            recovery_context: ErrorRecoveryContext::default(),
393            stats: ErrorRecoveryStats::default(),
394            start_time: None,
395        }
396    }
397
398    /// Create error handler with custom recovery context
399    pub fn with_context(context: ErrorRecoveryContext) -> Self {
400        Self {
401            recovery_context: context,
402            stats: ErrorRecoveryStats::default(),
403            start_time: None,
404        }
405    }
406
407    /// Handle file discovery errors with recovery
408    pub fn handle_file_discovery_error(
409        &mut self,
410        directory: &str,
411        base_name: &str,
412        error: Box<dyn Error + Send + Sync>,
413    ) -> Result<Vec<String>, Box<HtmlGenerationError>> {
414        self.start_recovery_timing();
415        self.stats.total_errors += 1;
416
417        let recovery_suggestions = vec![
418            format!("Check if directory '{directory}' exists and is readable"),
419            "Verify the base name pattern matches your JSON files".to_string(),
420            "Ensure JSON files follow the naming convention: {base_name}_{type}.json".to_string(),
421            "Check file permissions for the directory".to_string(),
422        ];
423
424        if self.recovery_context.attempt_recovery {
425            // Attempt to find alternative directories or patterns
426            if let Ok(alternatives) = self.find_alternative_directories(directory, base_name) {
427                if !alternatives.is_empty() {
428                    self.stats.recovered_errors += 1;
429                    self.end_recovery_timing();
430                    return Ok(alternatives);
431                }
432            }
433        }
434
435        self.stats.unrecoverable_errors += 1;
436        self.end_recovery_timing();
437
438        Err(Box::new(HtmlGenerationError::FileDiscoveryError {
439            directory: directory.to_string(),
440            base_name: base_name.to_string(),
441            source: error,
442            recovery_suggestions,
443        }))
444    }
445
446    /// Handle file loading errors with recovery
447    pub fn handle_file_loading_error(
448        &mut self,
449        file_path: PathBuf,
450        file_type: &str,
451        file_size: usize,
452        error: Box<dyn Error + Send + Sync>,
453    ) -> Result<Option<Value>, Box<HtmlGenerationError>> {
454        self.start_recovery_timing();
455        self.stats.total_errors += 1;
456
457        let recoverable = self.is_file_error_recoverable(&error);
458        let recovery_suggestions = self.get_file_loading_suggestions(file_type, file_size, &error);
459
460        if recoverable && self.recovery_context.attempt_recovery {
461            // Attempt recovery strategies
462            for attempt in 1..=self.recovery_context.max_retries {
463                self.stats.retry_attempts += 1;
464
465                if let Ok(recovered_data) = self.attempt_file_recovery(&file_path, file_type) {
466                    tracing::info!(
467                        "āœ… Recovered file {} after {} attempts",
468                        file_path.display(),
469                        attempt
470                    );
471                    self.stats.recovered_errors += 1;
472                    self.end_recovery_timing();
473                    return Ok(Some(recovered_data));
474                }
475
476                // Wait before retry
477                std::thread::sleep(std::time::Duration::from_millis(100 * attempt as u64));
478            }
479
480            // Try fallback if available
481            if self.recovery_context.use_fallbacks {
482                if let Ok(fallback_data) = self.get_fallback_data(file_type) {
483                    tracing::info!("āš ļø  Using fallback data for {}", file_type);
484                    self.stats.fallback_errors += 1;
485                    self.end_recovery_timing();
486                    return Ok(Some(fallback_data));
487                }
488            }
489        }
490
491        self.stats.unrecoverable_errors += 1;
492        self.end_recovery_timing();
493
494        Err(Box::new(HtmlGenerationError::FileLoadingError {
495            file_path,
496            file_type: file_type.to_string(),
497            file_size,
498            source: error,
499            recoverable,
500            recovery_suggestions,
501        }))
502    }
503
504    /// Handle JSON parsing errors with detailed context
505    pub fn handle_json_parsing_error(
506        &mut self,
507        file_path: PathBuf,
508        parsing_error: &str,
509    ) -> HtmlGenerationError {
510        self.start_recovery_timing();
511        self.stats.total_errors += 1;
512
513        let (line_number, column_number, content_snippet) =
514            self.extract_parsing_context(&file_path, parsing_error);
515
516        let recovery_suggestions = vec![
517            "Check JSON syntax for missing commas, brackets, or quotes".to_string(),
518            "Validate JSON structure using a JSON validator tool".to_string(),
519            "Ensure file encoding is UTF-8".to_string(),
520            "Check for trailing commas which are not valid in JSON".to_string(),
521            "Verify that all strings are properly quoted".to_string(),
522        ];
523
524        self.stats.unrecoverable_errors += 1;
525        self.end_recovery_timing();
526
527        HtmlGenerationError::JsonParsingError {
528            file_path,
529            line_number,
530            column_number,
531            parsing_error: parsing_error.to_string(),
532            content_snippet,
533            recovery_suggestions,
534        }
535    }
536
537    /// Handle validation errors with detailed analysis
538    pub fn handle_validation_error(
539        &mut self,
540        file_path: PathBuf,
541        validation_type: &str,
542        validation_error: &str,
543        json_data: &Value,
544    ) -> HtmlGenerationError {
545        self.start_recovery_timing();
546        self.stats.total_errors += 1;
547
548        let expected_structure = self.get_expected_structure(validation_type);
549        let actual_structure = self.analyze_json_structure(json_data);
550
551        let recovery_suggestions =
552            self.get_validation_recovery_suggestions(validation_type, json_data);
553
554        self.stats.unrecoverable_errors += 1;
555        self.end_recovery_timing();
556
557        HtmlGenerationError::ValidationError {
558            file_path,
559            validation_type: validation_type.to_string(),
560            validation_error: validation_error.to_string(),
561            expected_structure,
562            actual_structure,
563            recovery_suggestions,
564        }
565    }
566
567    /// Get error recovery statistics
568    pub fn get_stats(&self) -> &ErrorRecoveryStats {
569        &self.stats
570    }
571
572    /// Print error recovery summary
573    pub fn print_recovery_summary(&self) {
574        if self.stats.total_errors == 0 {
575            return;
576        }
577
578        tracing::info!("\nšŸ“Š Error Recovery Summary:");
579        tracing::info!("   Total errors: {}", self.stats.total_errors);
580        tracing::info!("   Recovered: {}", self.stats.recovered_errors);
581        tracing::info!("   Used fallbacks: {}", self.stats.fallback_errors);
582        tracing::info!("   Unrecoverable: {}", self.stats.unrecoverable_errors);
583        tracing::info!("   Retry attempts: {}", self.stats.retry_attempts);
584        tracing::info!("   Recovery time: {}ms", self.stats.recovery_time_ms);
585
586        let success_rate = if self.stats.total_errors > 0 {
587            ((self.stats.recovered_errors + self.stats.fallback_errors) as f64
588                / self.stats.total_errors as f64)
589                * 100.0
590        } else {
591            100.0
592        };
593        tracing::info!("   Success rate: {:.1}%", success_rate);
594    }
595
596    // Private helper methods
597
598    fn start_recovery_timing(&mut self) {
599        self.start_time = Some(Instant::now());
600    }
601
602    fn end_recovery_timing(&mut self) {
603        if let Some(start) = self.start_time.take() {
604            self.stats.recovery_time_ms += start.elapsed().as_millis() as u64;
605        }
606    }
607
608    fn find_alternative_directories(
609        &self,
610        _directory: &str,
611        _base_name: &str,
612    ) -> Result<Vec<String>, Box<dyn Error>> {
613        // Implementation would search for alternative directories
614        // For now, return empty to indicate no alternatives found
615        Ok(vec![])
616    }
617
618    #[allow(clippy::borrowed_box)]
619    fn is_file_error_recoverable(&self, error: &Box<dyn Error + Send + Sync>) -> bool {
620        let error_str = error.to_string().to_lowercase();
621
622        // Recoverable errors
623        error_str.contains("permission denied")
624            || error_str.contains("temporarily unavailable")
625            || error_str.contains("resource busy")
626            || error_str.contains("interrupted")
627    }
628
629    #[allow(clippy::borrowed_box)]
630    fn get_file_loading_suggestions(
631        &self,
632        file_type: &str,
633        file_size: usize,
634        error: &Box<dyn Error + Send + Sync>,
635    ) -> Vec<String> {
636        let mut suggestions = vec![];
637        let error_str = error.to_string().to_lowercase();
638
639        if error_str.contains("permission") {
640            suggestions.push("Check file permissions and ensure read access".to_string());
641        }
642
643        if error_str.contains("not found") {
644            suggestions.push(format!(
645                "Verify the {file_type} file exists in the expected location"
646            ));
647            suggestions
648                .push("Check the file naming convention matches the expected pattern".to_string());
649        }
650
651        if file_size > 100 * 1024 * 1024 {
652            // > 100MB
653            suggestions.push("Consider using streaming mode for large files".to_string());
654            suggestions.push("Increase memory limits if processing large datasets".to_string());
655        }
656
657        suggestions.push("Retry the operation after a brief delay".to_string());
658        suggestions.push("Check available disk space and memory".to_string());
659
660        suggestions
661    }
662
663    fn attempt_file_recovery(
664        &self,
665        file_path: &PathBuf,
666        _file_type: &str,
667    ) -> Result<Value, Box<dyn Error>> {
668        // Attempt to read the file again
669        let content = std::fs::read_to_string(file_path)?;
670        let json_value: Value = serde_json::from_str(&content)?;
671        Ok(json_value)
672    }
673
674    fn get_fallback_data(&self, file_type: &str) -> Result<Value, Box<dyn Error>> {
675        // Provide minimal fallback data structures
676        let fallback = match file_type {
677            "memory_analysis" => serde_json::json!({
678                "allocations": [],
679                "summary": {
680                    "total_allocations": 0,
681                    "active_allocations": 0,
682                    "total_memory": 0
683                }
684            }),
685            "performance" => serde_json::json!({
686                "memory_performance": {
687                    "active_memory": 0,
688                    "peak_memory": 0,
689                    "total_allocated": 0
690                },
691                "allocation_distribution": {}
692            }),
693            "unsafe_ffi" => serde_json::json!({
694                "summary": {
695                    "unsafe_count": 0,
696                    "ffi_count": 0,
697                    "safety_violations": 0
698                },
699                "enhanced_ffi_data": [],
700                "boundary_events": []
701            }),
702            "lifetime" => serde_json::json!({
703                "lifecycle_events": []
704            }),
705            "complex_types" => serde_json::json!({
706                "categorized_types": {},
707                "generic_types": []
708            }),
709            _ => serde_json::json!({}),
710        };
711
712        Ok(fallback)
713    }
714
715    fn extract_parsing_context(
716        &self,
717        file_path: &PathBuf,
718        parsing_error: &str,
719    ) -> (Option<usize>, Option<usize>, Option<String>) {
720        // Extract line and column information from parsing error
721        let line_regex = regex::Regex::new(r"line (\d+)").ok();
722        let col_regex = regex::Regex::new(r"column (\d+)").ok();
723
724        let line_number: Option<usize> = line_regex
725            .and_then(|re| re.captures(parsing_error))
726            .and_then(|caps| caps.get(1))
727            .and_then(|m| m.as_str().parse().ok());
728
729        let column_number: Option<usize> = col_regex
730            .and_then(|re| re.captures(parsing_error))
731            .and_then(|caps| caps.get(1))
732            .and_then(|m| m.as_str().parse().ok());
733
734        // Try to read file content around the error
735        let content_snippet =
736            if let (Some(line), Ok(content)) = (line_number, std::fs::read_to_string(file_path)) {
737                let lines: Vec<&str> = content.lines().collect();
738                if line > 0 && line <= lines.len() {
739                    let start = (line.saturating_sub(3)).max(1);
740                    let end = (line + 2).min(lines.len());
741                    let snippet_lines: Vec<String> = (start..=end)
742                        .map(|i| {
743                            let marker = if i == line { ">>> " } else { "    " };
744                            format!(
745                                "{marker}{i:3}: {}",
746                                lines.get(i.saturating_sub(1)).unwrap_or(&"")
747                            )
748                        })
749                        .collect();
750                    Some(snippet_lines.join("\n"))
751                } else {
752                    None
753                }
754            } else {
755                None
756            };
757
758        (line_number, column_number, content_snippet)
759    }
760
761    fn get_expected_structure(&self, validation_type: &str) -> String {
762        match validation_type {
763            "memory_analysis" => "Object with 'allocations' array and 'summary' object".to_string(),
764            "performance" => {
765                "Object with 'memory_performance' and 'allocation_distribution'".to_string()
766            }
767            "unsafe_ffi" => {
768                "Object with 'summary', 'enhanced_ffi_data', and 'boundary_events'".to_string()
769            }
770            "lifetime" => "Object with 'lifecycle_events' array".to_string(),
771            "complex_types" => "Object with 'categorized_types' and 'generic_types'".to_string(),
772            _ => "Valid JSON object or array".to_string(),
773        }
774    }
775
776    fn analyze_json_structure(&self, json_data: &Value) -> String {
777        match json_data {
778            Value::Object(obj) => {
779                let keys: Vec<String> = obj.keys().cloned().collect();
780                format!("Object with keys: [{}]", keys.join(", "))
781            }
782            Value::Array(arr) => {
783                format!("Array with {} elements", arr.len())
784            }
785            Value::String(_) => "String value".to_string(),
786            Value::Number(_) => "Number value".to_string(),
787            Value::Bool(_) => "Boolean value".to_string(),
788            Value::Null => "Null value".to_string(),
789        }
790    }
791
792    fn get_validation_recovery_suggestions(
793        &self,
794        validation_type: &str,
795        json_data: &Value,
796    ) -> Vec<String> {
797        let mut suggestions = vec![];
798
799        match validation_type {
800            "memory_analysis" => {
801                suggestions.push("Ensure the JSON contains an 'allocations' array".to_string());
802                suggestions.push("Add a 'summary' object with allocation statistics".to_string());
803                if let Value::Object(obj) = json_data {
804                    if !obj.contains_key("allocations") {
805                        suggestions.push("Add missing 'allocations' field as an array".to_string());
806                    }
807                    if !obj.contains_key("summary") {
808                        suggestions.push("Add missing 'summary' field as an object".to_string());
809                    }
810                }
811            }
812            "unsafe_ffi" => {
813                suggestions.push("Ensure the JSON contains 'enhanced_ffi_data' array".to_string());
814                suggestions
815                    .push("Add 'boundary_events' array for FFI boundary tracking".to_string());
816                suggestions.push("Include 'summary' object with FFI statistics".to_string());
817            }
818            _ => {
819                suggestions
820                    .push("Check the JSON structure matches the expected format".to_string());
821                suggestions
822                    .push("Refer to the documentation for the correct data format".to_string());
823            }
824        }
825
826        suggestions.push("Validate JSON syntax and structure".to_string());
827        suggestions.push("Check for required fields and correct data types".to_string());
828
829        suggestions
830    }
831}
832
833impl Default for HtmlErrorHandler {
834    fn default() -> Self {
835        Self::new()
836    }
837}
838
839#[cfg(test)]
840mod tests {
841    use super::*;
842    use std::io;
843
844    #[test]
845    fn test_error_handler_creation() {
846        let handler = HtmlErrorHandler::new();
847        assert_eq!(handler.stats.total_errors, 0);
848        assert!(handler.recovery_context.attempt_recovery);
849    }
850
851    #[test]
852    fn test_file_loading_error_handling() {
853        let mut handler = HtmlErrorHandler::new();
854        let error = Box::new(io::Error::new(io::ErrorKind::NotFound, "File not found"));
855
856        let result = handler.handle_file_loading_error(
857            PathBuf::from("test.json"),
858            "memory_analysis",
859            1024,
860            error,
861        );
862
863        assert!(result.is_err());
864        assert_eq!(handler.stats.total_errors, 1);
865    }
866
867    #[test]
868    fn test_fallback_data_generation() {
869        let handler = HtmlErrorHandler::new();
870
871        let fallback = handler
872            .get_fallback_data("memory_analysis")
873            .expect("Failed to get fallback data");
874        assert!(fallback.is_object());
875        assert!(fallback.get("allocations").is_some());
876        assert!(fallback.get("summary").is_some());
877    }
878
879    #[test]
880    fn test_error_recovery_context() {
881        let context = ErrorRecoveryContext {
882            attempt_recovery: false,
883            max_retries: 1,
884            allow_partial_data: false,
885            use_fallbacks: false,
886            verbose_errors: true,
887        };
888
889        let handler = HtmlErrorHandler::with_context(context);
890        assert!(!handler.recovery_context.attempt_recovery);
891        assert_eq!(handler.recovery_context.max_retries, 1);
892    }
893}