Skip to main content

dampen_dev/
reload.rs

1//! Hot-reload state preservation and coordination
2//!
3//! This module handles the hot-reload process, including model snapshotting,
4//! state restoration, and error recovery.
5
6use dampen_core::binding::UiBindable;
7use dampen_core::parser::error::ParseError;
8use dampen_core::state::AppState;
9use serde::{Serialize, de::DeserializeOwned};
10use std::collections::HashMap;
11use std::marker::PhantomData;
12use std::sync::Arc;
13use std::sync::atomic::{AtomicUsize, Ordering};
14use std::time::{Duration, Instant};
15
16/// Cache entry for parsed XML documents
17#[derive(Clone)]
18struct ParsedDocumentCache {
19    /// Cached parsed document
20    document: dampen_core::ir::DampenDocument,
21
22    /// Timestamp when cached
23    cached_at: Instant,
24}
25
26/// Tracks hot-reload state and history for debugging
27pub struct HotReloadContext<M> {
28    /// Last successful model snapshot (JSON)
29    last_model_snapshot: Option<String>,
30
31    /// Timestamp of last reload
32    last_reload_timestamp: Instant,
33
34    /// Reload count (for metrics)
35    reload_count: usize,
36
37    /// Current error state (if any)
38    error: Option<String>,
39
40    /// Cache of parsed XML documents (keyed by content hash)
41    /// This avoids re-parsing the same XML content repeatedly
42    parse_cache: HashMap<u64, ParsedDocumentCache>,
43
44    /// Maximum number of cached documents
45    max_cache_size: usize,
46
47    /// Count of cache hits for hit rate calculation
48    cache_hits: AtomicUsize,
49
50    /// Count of cache misses for hit rate calculation
51    cache_misses: AtomicUsize,
52
53    _marker: PhantomData<M>,
54}
55
56impl<M: UiBindable> HotReloadContext<M> {
57    /// Create a new hot-reload context
58    pub fn new() -> Self {
59        Self {
60            last_model_snapshot: None,
61            last_reload_timestamp: Instant::now(),
62            reload_count: 0,
63            error: None,
64            parse_cache: HashMap::new(),
65            max_cache_size: 10,
66            cache_hits: AtomicUsize::new(0),
67            cache_misses: AtomicUsize::new(0),
68            _marker: PhantomData,
69        }
70    }
71
72    /// Create a new hot-reload context with custom cache size
73    pub fn with_cache_size(cache_size: usize) -> Self {
74        Self {
75            last_model_snapshot: None,
76            last_reload_timestamp: Instant::now(),
77            reload_count: 0,
78            error: None,
79            parse_cache: HashMap::new(),
80            max_cache_size: cache_size,
81            cache_hits: AtomicUsize::new(0),
82            cache_misses: AtomicUsize::new(0),
83            _marker: PhantomData,
84        }
85    }
86
87    /// Compute content hash for caching
88    fn compute_content_hash(xml_source: &str) -> u64 {
89        use std::collections::hash_map::DefaultHasher;
90        use std::hash::{Hash, Hasher};
91
92        let mut hasher = DefaultHasher::new();
93        xml_source.hash(&mut hasher);
94        hasher.finish()
95    }
96
97    /// Try to get a parsed document from cache
98    fn get_cached_document(&self, xml_source: &str) -> Option<dampen_core::ir::DampenDocument> {
99        let content_hash = Self::compute_content_hash(xml_source);
100
101        self.parse_cache
102            .get(&content_hash)
103            .map(|entry| entry.document.clone())
104            .inspect(|_| {
105                self.cache_hits.fetch_add(1, Ordering::Relaxed);
106            })
107            .or_else(|| {
108                self.cache_misses.fetch_add(1, Ordering::Relaxed);
109                None
110            })
111    }
112
113    /// Cache a parsed document
114    fn cache_document(&mut self, xml_source: &str, document: dampen_core::ir::DampenDocument) {
115        // Evict oldest entry if cache is full
116        if self.parse_cache.len() >= self.max_cache_size
117            && let Some(oldest_key) = self
118                .parse_cache
119                .iter()
120                .min_by_key(|(_, entry)| entry.cached_at)
121                .map(|(key, _)| *key)
122        {
123            self.parse_cache.remove(&oldest_key);
124        }
125
126        let content_hash = Self::compute_content_hash(xml_source);
127
128        self.parse_cache.insert(
129            content_hash,
130            ParsedDocumentCache {
131                document,
132                cached_at: Instant::now(),
133            },
134        );
135    }
136
137    /// Clear the parse cache
138    pub fn clear_cache(&mut self) {
139        self.parse_cache.clear();
140    }
141
142    /// Get cache statistics
143    pub fn cache_stats(&self) -> (usize, usize) {
144        (self.parse_cache.len(), self.max_cache_size)
145    }
146
147    /// Get detailed performance metrics from the last reload
148    pub fn performance_metrics(&self) -> ReloadPerformanceMetrics {
149        ReloadPerformanceMetrics {
150            reload_count: self.reload_count,
151            last_reload_latency: self.last_reload_latency(),
152            cache_hit_rate: self.calculate_cache_hit_rate(),
153            cache_size: self.parse_cache.len(),
154        }
155    }
156
157    /// Calculate cache hit rate (0.0 to 1.0)
158    fn calculate_cache_hit_rate(&self) -> f64 {
159        let hits = self.cache_hits.load(Ordering::Relaxed);
160        let misses = self.cache_misses.load(Ordering::Relaxed);
161        let total = hits.saturating_add(misses);
162
163        if total == 0 {
164            0.0
165        } else {
166            hits as f64 / total as f64
167        }
168    }
169
170    /// Snapshot the current model state to JSON
171    pub fn snapshot_model(&mut self, model: &M) -> Result<(), String>
172    where
173        M: Serialize,
174    {
175        match serde_json::to_string(model) {
176            Ok(json) => {
177                self.last_model_snapshot = Some(json);
178                Ok(())
179            }
180            Err(e) => Err(format!("Failed to serialize model: {}", e)),
181        }
182    }
183
184    /// Restore the model from the last snapshot
185    pub fn restore_model(&self) -> Result<M, String>
186    where
187        M: DeserializeOwned,
188    {
189        match &self.last_model_snapshot {
190            Some(json) => serde_json::from_str(json)
191                .map_err(|e| format!("Failed to deserialize model: {}", e)),
192            None => Err("No model snapshot available".to_string()),
193        }
194    }
195
196    /// Record a reload attempt
197    pub fn record_reload(&mut self, success: bool) {
198        self.reload_count += 1;
199        self.last_reload_timestamp = Instant::now();
200        if !success {
201            self.error = Some("Reload failed".to_string());
202        } else {
203            self.error = None;
204        }
205    }
206
207    /// Record a reload with timing information
208    pub fn record_reload_with_timing(&mut self, success: bool, elapsed: Duration) {
209        self.reload_count += 1;
210        self.last_reload_timestamp = Instant::now();
211        if !success {
212            self.error = Some("Reload failed".to_string());
213        } else {
214            self.error = None;
215        }
216
217        // Log performance if it exceeds target
218        if success && elapsed.as_millis() > 300 {
219            eprintln!(
220                "Warning: Hot-reload took {}ms (target: <300ms)",
221                elapsed.as_millis()
222            );
223        }
224    }
225
226    /// Get the latency of the last reload
227    pub fn last_reload_latency(&self) -> Duration {
228        self.last_reload_timestamp.elapsed()
229    }
230}
231
232impl<M: UiBindable> Default for HotReloadContext<M> {
233    fn default() -> Self {
234        Self::new()
235    }
236}
237
238/// Performance metrics for hot-reload operations
239#[derive(Debug, Clone, Copy)]
240pub struct ReloadPerformanceMetrics {
241    /// Total number of reloads performed
242    pub reload_count: usize,
243
244    /// Latency of the last reload operation
245    pub last_reload_latency: Duration,
246
247    /// Cache hit rate (0.0 to 1.0)
248    pub cache_hit_rate: f64,
249
250    /// Current cache size
251    pub cache_size: usize,
252}
253
254impl ReloadPerformanceMetrics {
255    /// Check if the last reload met the performance target (<300ms)
256    pub fn meets_target(&self) -> bool {
257        self.last_reload_latency.as_millis() < 300
258    }
259
260    /// Get latency in milliseconds
261    pub fn latency_ms(&self) -> u128 {
262        self.last_reload_latency.as_millis()
263    }
264}
265
266/// Result type for hot-reload attempts with detailed error information
267#[derive(Debug)]
268pub enum ReloadResult<M: UiBindable> {
269    /// Reload succeeded
270    Success(AppState<M>),
271
272    /// XML parse error (reject reload)
273    ParseError(ParseError),
274
275    /// Schema validation error (reject reload)
276    ValidationError(Vec<String>),
277
278    /// Model deserialization failed, using default (accept reload with warning)
279    StateRestoreWarning(AppState<M>, String),
280}
281
282/// Attempts to hot-reload the UI from a new XML source while preserving application state.
283///
284/// This function orchestrates the entire hot-reload process:
285/// 1. Snapshot the current model state
286/// 2. Parse the new XML
287/// 3. Rebuild the handler registry
288/// 4. Validate the document (all referenced handlers exist)
289/// 5. Restore the model (or use default on failure)
290/// 6. Create a new AppState with the updated UI
291///
292/// # Arguments
293///
294/// * `xml_source` - New XML UI definition as a string
295/// * `current_state` - Current application state (for model snapshotting)
296/// * `context` - Hot-reload context for state preservation
297/// * `create_handlers` - Function to rebuild the handler registry
298///
299/// # Returns
300///
301/// A `ReloadResult` indicating success or the specific type of failure:
302/// - `Success`: Reload succeeded with model restored
303/// - `ParseError`: XML parse failed (reject reload, keep old state)
304/// - `ValidationError`: Handler validation failed (reject reload, keep old state)
305/// - `StateRestoreWarning`: Reload succeeded but model used default (accept with warning)
306///
307/// # Error Handling Matrix
308///
309/// | Error Type | Action | State Preservation |
310/// |------------|--------|-------------------|
311/// | Parse error | Reject reload | Keep old state completely |
312/// | Validation error | Reject reload | Keep old state completely |
313/// | Model restore failure | Accept reload | Use M::default() with warning |
314///
315/// # Example
316///
317/// ```no_run
318/// use dampen_dev::reload::{attempt_hot_reload, HotReloadContext};
319/// use dampen_core::{AppState, handler::HandlerRegistry};
320/// # use dampen_core::binding::UiBindable;
321/// # #[derive(Default, serde::Serialize, serde::Deserialize)]
322/// # struct Model;
323/// # impl UiBindable for Model {
324/// #     fn get_field(&self, _path: &[&str]) -> Option<dampen_core::binding::BindingValue> { None }
325/// #     fn available_fields() -> Vec<String> { vec![] }
326/// # }
327///
328/// fn handle_file_change(
329///     new_xml: &str,
330///     app_state: &AppState<Model>,
331///     context: &mut HotReloadContext<Model>,
332/// ) {
333///     let result = attempt_hot_reload(
334///         new_xml,
335///         app_state,
336///         context,
337///         || create_handler_registry(),
338///     );
339///
340///     match result {
341///         dampen_dev::reload::ReloadResult::Success(new_state) => {
342///             // Apply the new state
343///         }
344///         dampen_dev::reload::ReloadResult::ParseError(err) => {
345///             // Show error overlay, keep old UI
346///             eprintln!("Parse error: {}", err.message);
347///         }
348///         _ => {
349///             // Handle other cases
350///         }
351///     }
352/// }
353///
354/// fn create_handler_registry() -> dampen_core::handler::HandlerRegistry {
355///     dampen_core::handler::HandlerRegistry::new()
356/// }
357/// ```
358pub fn attempt_hot_reload<M, F>(
359    xml_source: &str,
360    current_state: &AppState<M>,
361    context: &mut HotReloadContext<M>,
362    create_handlers: F,
363) -> ReloadResult<M>
364where
365    M: UiBindable + Serialize + DeserializeOwned + Default,
366    F: FnOnce() -> dampen_core::handler::HandlerRegistry,
367{
368    let reload_start = Instant::now();
369
370    // Step 1: Snapshot current model state
371    if let Err(e) = context.snapshot_model(&current_state.model) {
372        // If we can't snapshot, continue with reload but warn
373        eprintln!("Warning: Failed to snapshot model: {}", e);
374    }
375
376    // Step 2: Parse new XML (with caching)
377    let new_document = if let Some(cached_doc) = context.get_cached_document(xml_source) {
378        // Cache hit - reuse parsed document
379        cached_doc
380    } else {
381        // Cache miss - parse and cache
382        match dampen_core::parser::parse(xml_source) {
383            Ok(doc) => {
384                context.cache_document(xml_source, doc.clone());
385                doc
386            }
387            Err(err) => {
388                context.record_reload(false);
389                return ReloadResult::ParseError(err);
390            }
391        }
392    };
393
394    // Step 3: Rebuild handler registry (before validation)
395    let new_handlers = create_handlers();
396
397    // Step 4: Validate the parsed document against the handler registry
398    if let Err(missing_handlers) = validate_handlers(&new_document, &new_handlers) {
399        context.record_reload(false);
400        let error_messages: Vec<String> = missing_handlers
401            .iter()
402            .map(|h| format!("Handler '{}' is referenced but not registered", h))
403            .collect();
404        return ReloadResult::ValidationError(error_messages);
405    }
406
407    // Step 5: Restore model from snapshot
408    let restored_model = match context.restore_model() {
409        Ok(model) => {
410            // Successfully restored
411            model
412        }
413        Err(e) => {
414            // Failed to restore, use default
415            eprintln!("Warning: Failed to restore model ({}), using default", e);
416
417            // Create new state with default model
418            let new_state = AppState::with_all(new_document, M::default(), new_handlers);
419
420            context.record_reload(true);
421            return ReloadResult::StateRestoreWarning(new_state, e);
422        }
423    };
424
425    // Step 6: Create new AppState with restored model and new UI
426    let new_state = AppState::with_all(new_document, restored_model, new_handlers);
427
428    let elapsed = reload_start.elapsed();
429    context.record_reload_with_timing(true, elapsed);
430    ReloadResult::Success(new_state)
431}
432
433/// Async version of `attempt_hot_reload` that performs XML parsing asynchronously.
434///
435/// This function is optimized for non-blocking hot-reload by offloading the CPU-intensive
436/// XML parsing to a background thread using `tokio::task::spawn_blocking`.
437///
438/// # Performance Benefits
439///
440/// - XML parsing happens on a thread pool, avoiding UI blocking
441/// - Reduces hot-reload latency for large XML files
442/// - Maintains UI responsiveness during reload
443///
444/// # Arguments
445///
446/// * `xml_source` - New XML UI definition as a string
447/// * `current_state` - Current application state (for model snapshotting)
448/// * `context` - Hot-reload context for state preservation
449/// * `create_handlers` - Function to rebuild the handler registry
450///
451/// # Returns
452///
453/// A `ReloadResult` wrapped in a future, indicating success or failure
454///
455/// # Example
456///
457/// ```no_run
458/// use dampen_dev::reload::{attempt_hot_reload_async, HotReloadContext};
459/// use dampen_core::{AppState, handler::HandlerRegistry};
460/// use std::sync::Arc;
461/// # use dampen_core::binding::UiBindable;
462/// # #[derive(Default, serde::Serialize, serde::Deserialize)]
463/// # struct Model;
464/// # impl UiBindable for Model {
465/// #     fn get_field(&self, _path: &[&str]) -> Option<dampen_core::binding::BindingValue> { None }
466/// #     fn available_fields() -> Vec<String> { vec![] }
467/// # }
468///
469/// async fn handle_file_change_async(
470///     new_xml: String,
471///     app_state: AppState<Model>,
472///     mut context: HotReloadContext<Model>,
473/// ) {
474///     let result = attempt_hot_reload_async(
475///         Arc::new(new_xml),
476///         &app_state,
477///         &mut context,
478///         || create_handler_registry(),
479///     ).await;
480///
481///     match result {
482///         dampen_dev::reload::ReloadResult::Success(new_state) => {
483///             // Apply the new state
484///         }
485///         _ => {
486///             // Handle errors
487///         }
488///     }
489/// }
490///
491/// fn create_handler_registry() -> dampen_core::handler::HandlerRegistry {
492///     dampen_core::handler::HandlerRegistry::new()
493/// }
494/// ```
495pub async fn attempt_hot_reload_async<M, F>(
496    xml_source: Arc<String>,
497    current_state: &AppState<M>,
498    context: &mut HotReloadContext<M>,
499    create_handlers: F,
500) -> ReloadResult<M>
501where
502    M: UiBindable + Serialize + DeserializeOwned + Default + Send + 'static,
503    F: FnOnce() -> dampen_core::handler::HandlerRegistry + Send + 'static,
504{
505    let reload_start = Instant::now();
506
507    // Step 1: Snapshot current model state (fast, can do synchronously)
508    if let Err(e) = context.snapshot_model(&current_state.model) {
509        eprintln!("Warning: Failed to snapshot model: {}", e);
510    }
511
512    // Clone snapshot for async context
513    let model_snapshot = context.last_model_snapshot.clone();
514
515    // Step 2: Parse new XML asynchronously (CPU-intensive work offloaded, with caching)
516    let new_document = if let Some(cached_doc) = context.get_cached_document(&xml_source) {
517        // Cache hit - reuse parsed document
518        cached_doc
519    } else {
520        // Cache miss - parse asynchronously and cache
521        let xml_for_parse = Arc::clone(&xml_source);
522        let parse_result =
523            tokio::task::spawn_blocking(move || dampen_core::parser::parse(&xml_for_parse)).await;
524
525        match parse_result {
526            Ok(Ok(doc)) => {
527                context.cache_document(&xml_source, doc.clone());
528                doc
529            }
530            Ok(Err(err)) => {
531                context.record_reload(false);
532                return ReloadResult::ParseError(err);
533            }
534            Err(join_err) => {
535                context.record_reload(false);
536                let error = ParseError {
537                    kind: dampen_core::parser::error::ParseErrorKind::XmlSyntax,
538                    span: dampen_core::ir::span::Span::default(),
539                    message: format!("Async parsing failed: {}", join_err),
540                    suggestion: Some(
541                        "Check if the XML file is accessible and not corrupted".to_string(),
542                    ),
543                };
544                return ReloadResult::ParseError(error);
545            }
546        }
547    };
548
549    // Step 3: Rebuild handler registry (before validation)
550    let new_handlers = create_handlers();
551
552    // Step 4: Validate the parsed document against the handler registry
553    if let Err(missing_handlers) = validate_handlers(&new_document, &new_handlers) {
554        context.record_reload(false);
555        let error_messages: Vec<String> = missing_handlers
556            .iter()
557            .map(|h| format!("Handler '{}' is referenced but not registered", h))
558            .collect();
559        return ReloadResult::ValidationError(error_messages);
560    }
561
562    // Step 5: Restore model from snapshot
563    let restored_model = match model_snapshot {
564        Some(json) => match serde_json::from_str::<M>(&json) {
565            Ok(model) => model,
566            Err(e) => {
567                eprintln!("Warning: Failed to restore model ({}), using default", e);
568                let new_state = AppState::with_all(new_document, M::default(), new_handlers);
569                context.record_reload(true);
570                return ReloadResult::StateRestoreWarning(
571                    new_state,
572                    format!("Failed to deserialize model: {}", e),
573                );
574            }
575        },
576        None => {
577            eprintln!("Warning: No model snapshot available, using default");
578            let new_state = AppState::with_all(new_document, M::default(), new_handlers);
579            context.record_reload(true);
580            return ReloadResult::StateRestoreWarning(
581                new_state,
582                "No model snapshot available".to_string(),
583            );
584        }
585    };
586
587    // Step 6: Create new AppState with restored model and new UI
588    let new_state = AppState::with_all(new_document, restored_model, new_handlers);
589
590    let elapsed = reload_start.elapsed();
591    context.record_reload_with_timing(true, elapsed);
592    ReloadResult::Success(new_state)
593}
594
595/// Collects all handler names referenced in a document.
596///
597/// This function recursively traverses the widget tree and collects all unique
598/// handler names from event bindings.
599///
600/// # Arguments
601///
602/// * `document` - The parsed UI document to scan
603///
604/// # Returns
605///
606/// A vector of unique handler names referenced in the document
607fn collect_handler_names(document: &dampen_core::ir::DampenDocument) -> Vec<String> {
608    use std::collections::HashSet;
609
610    let mut handlers = HashSet::new();
611    collect_handlers_from_node(&document.root, &mut handlers);
612    handlers.into_iter().collect()
613}
614
615/// Recursively collects handler names from a widget node and its children.
616fn collect_handlers_from_node(
617    node: &dampen_core::ir::node::WidgetNode,
618    handlers: &mut std::collections::HashSet<String>,
619) {
620    // Collect handlers from events
621    for event in &node.events {
622        handlers.insert(event.handler.clone());
623    }
624
625    // Recursively collect from children
626    for child in &node.children {
627        collect_handlers_from_node(child, handlers);
628    }
629}
630
631/// Validates that all handlers referenced in the document exist in the registry.
632///
633/// # Arguments
634///
635/// * `document` - The parsed UI document to validate
636/// * `registry` - The handler registry to check against
637///
638/// # Returns
639///
640/// `Ok(())` if all handlers exist, or `Err(Vec<String>)` with a list of missing handlers
641fn validate_handlers(
642    document: &dampen_core::ir::DampenDocument,
643    registry: &dampen_core::handler::HandlerRegistry,
644) -> Result<(), Vec<String>> {
645    let referenced_handlers = collect_handler_names(document);
646    let mut missing_handlers = Vec::new();
647
648    for handler_name in referenced_handlers {
649        if registry.get(&handler_name).is_none() {
650            missing_handlers.push(handler_name);
651        }
652    }
653
654    if missing_handlers.is_empty() {
655        Ok(())
656    } else {
657        Err(missing_handlers)
658    }
659}
660
661/// Result type for theme hot-reload attempts
662#[derive(Debug)]
663pub enum ThemeReloadResult {
664    /// Reload succeeded
665    Success,
666
667    /// Theme file parse error
668    ParseError(String),
669
670    /// Theme document validation error
671    ValidationError(String),
672
673    /// No theme context to reload
674    NoThemeContext,
675
676    /// Theme file not found
677    FileNotFound,
678}
679
680/// Attempts to hot-reload the theme from a changed theme.dampen file.
681///
682/// This function handles theme file changes specifically:
683/// 1. Read the new theme file content
684/// 2. Parse the new ThemeDocument
685/// 3. Update the ThemeContext if valid
686///
687/// # Arguments
688///
689/// * `theme_path` - Path to the theme.dampen file
690/// * `theme_context` - The current ThemeContext to reload
691///
692/// # Returns
693///
694/// A `ThemeReloadResult` indicating success or the specific type of failure
695///
696/// # Example
697///
698/// ```no_run
699/// use dampen_dev::reload::{attempt_theme_hot_reload, ThemeReloadResult};
700/// use dampen_core::state::ThemeContext;
701///
702/// fn handle_theme_file_change(theme_path: &std::path::Path, ctx: &mut Option<ThemeContext>) {
703///     let result = attempt_theme_hot_reload(theme_path, ctx);
704///
705///     match result {
706///         ThemeReloadResult::Success => {
707///             println!("Theme reloaded successfully");
708///         }
709///         ThemeReloadResult::ParseError(err) => {
710///             eprintln!("Failed to parse theme file: {}", err);
711///         }
712///         ThemeReloadResult::ValidationError(err) => {
713///             eprintln!("Theme validation failed: {}", err);
714///         }
715///         ThemeReloadResult::NoThemeContext => {
716///             eprintln!("No theme context to reload");
717///         }
718///         ThemeReloadResult::FileNotFound => {
719///             eprintln!("Theme file not found");
720///         }
721///     }
722/// }
723/// ```
724pub fn attempt_theme_hot_reload(
725    theme_path: &std::path::Path,
726    theme_context: &mut Option<dampen_core::state::ThemeContext>,
727) -> ThemeReloadResult {
728    let theme_context = match theme_context {
729        Some(ctx) => ctx,
730        None => return ThemeReloadResult::NoThemeContext,
731    };
732
733    let content = match std::fs::read_to_string(theme_path) {
734        Ok(c) => c,
735        Err(e) => return ThemeReloadResult::ParseError(format!("Failed to read file: {}", e)),
736    };
737
738    let new_doc = match dampen_core::parser::theme_parser::parse_theme_document(&content) {
739        Ok(doc) => doc,
740        Err(e) => {
741            return ThemeReloadResult::ParseError(format!(
742                "Failed to parse theme document: {}",
743                e.message
744            ));
745        }
746    };
747
748    if let Err(e) = new_doc.validate() {
749        return ThemeReloadResult::ValidationError(e.to_string());
750    }
751
752    theme_context.reload(new_doc);
753    ThemeReloadResult::Success
754}
755
756/// Check if a path is a theme file path
757pub fn is_theme_file_path(path: &std::path::Path) -> bool {
758    path.file_name()
759        .and_then(|n| n.to_str())
760        .map(|n| n == "theme.dampen")
761        .unwrap_or(false)
762}
763
764/// Find the theme directory path from a changed file path
765///
766/// If the changed file is in or under the theme directory, returns the theme directory.
767/// Otherwise returns None.
768pub fn get_theme_dir_from_path(path: &std::path::Path) -> Option<std::path::PathBuf> {
769    let path = std::fs::canonicalize(path).ok()?;
770    let theme_file_name = path.file_name()?;
771
772    if theme_file_name == "theme.dampen" {
773        return Some(path.parent()?.to_path_buf());
774    }
775
776    None
777}
778
779#[cfg(test)]
780mod tests {
781    use super::*;
782    use serde::{Deserialize, Serialize};
783
784    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
785    struct TestModel {
786        count: i32,
787        name: String,
788    }
789
790    impl UiBindable for TestModel {
791        fn get_field(&self, _path: &[&str]) -> Option<dampen_core::binding::BindingValue> {
792            None
793        }
794
795        fn available_fields() -> Vec<String> {
796            vec![]
797        }
798    }
799
800    impl Default for TestModel {
801        fn default() -> Self {
802            Self {
803                count: 0,
804                name: "default".to_string(),
805            }
806        }
807    }
808
809    #[test]
810    fn test_snapshot_model_success() {
811        let mut context = HotReloadContext::<TestModel>::new();
812        let model = TestModel {
813            count: 42,
814            name: "Alice".to_string(),
815        };
816
817        let result = context.snapshot_model(&model);
818        assert!(result.is_ok());
819        assert!(context.last_model_snapshot.is_some());
820    }
821
822    #[test]
823    fn test_restore_model_success() {
824        let mut context = HotReloadContext::<TestModel>::new();
825        let original = TestModel {
826            count: 42,
827            name: "Alice".to_string(),
828        };
829
830        // First snapshot
831        context.snapshot_model(&original).unwrap();
832
833        // Then restore
834        let restored = context.restore_model().unwrap();
835        assert_eq!(restored, original);
836    }
837
838    #[test]
839    fn test_restore_model_no_snapshot() {
840        let context = HotReloadContext::<TestModel>::new();
841
842        // Try to restore without snapshot
843        let result = context.restore_model();
844        assert!(result.is_err());
845        assert!(result.unwrap_err().contains("No model snapshot"));
846    }
847
848    #[test]
849    fn test_snapshot_restore_round_trip() {
850        let mut context = HotReloadContext::<TestModel>::new();
851        let original = TestModel {
852            count: 999,
853            name: "Bob".to_string(),
854        };
855
856        // Snapshot, modify, and restore
857        context.snapshot_model(&original).unwrap();
858
859        let mut modified = original.clone();
860        modified.count = 0;
861        modified.name = "Changed".to_string();
862
863        // Restore should get original back
864        let restored = context.restore_model().unwrap();
865        assert_eq!(restored, original);
866        assert_ne!(restored, modified);
867    }
868
869    #[test]
870    fn test_multiple_snapshots() {
871        let mut context = HotReloadContext::<TestModel>::new();
872
873        // First snapshot
874        let model1 = TestModel {
875            count: 1,
876            name: "First".to_string(),
877        };
878        context.snapshot_model(&model1).unwrap();
879
880        // Second snapshot (should overwrite first)
881        let model2 = TestModel {
882            count: 2,
883            name: "Second".to_string(),
884        };
885        context.snapshot_model(&model2).unwrap();
886
887        // Restore should get the second model
888        let restored = context.restore_model().unwrap();
889        assert_eq!(restored, model2);
890        assert_ne!(restored, model1);
891    }
892
893    #[test]
894    fn test_record_reload() {
895        let mut context = HotReloadContext::<TestModel>::new();
896
897        assert_eq!(context.reload_count, 0);
898        assert!(context.error.is_none());
899
900        // Record successful reload
901        context.record_reload(true);
902        assert_eq!(context.reload_count, 1);
903        assert!(context.error.is_none());
904
905        // Record failed reload
906        context.record_reload(false);
907        assert_eq!(context.reload_count, 2);
908        assert!(context.error.is_some());
909
910        // Record successful reload again
911        context.record_reload(true);
912        assert_eq!(context.reload_count, 3);
913        assert!(context.error.is_none());
914    }
915
916    #[test]
917    fn test_cache_hit_rate_calculated_correctly() {
918        use std::sync::atomic::Ordering;
919
920        let mut context = HotReloadContext::<TestModel>::new();
921
922        // Initially: 0 hits, 0 misses
923        assert_eq!(context.calculate_cache_hit_rate(), 0.0);
924
925        // Simulate 3 hits, 2 misses
926        context.cache_hits.store(3, Ordering::Relaxed);
927        context.cache_misses.store(2, Ordering::Relaxed);
928
929        // Hit rate = 3 / (3 + 2) = 0.6
930        assert_eq!(context.calculate_cache_hit_rate(), 0.6);
931    }
932
933    #[test]
934    fn test_cache_hit_rate_zero_division() {
935        let context = HotReloadContext::<TestModel>::new();
936        // Should not panic on division by zero
937        assert_eq!(context.calculate_cache_hit_rate(), 0.0);
938    }
939
940    #[test]
941    fn test_cache_hit_rate_full_misses() {
942        use std::sync::atomic::Ordering;
943
944        let mut context = HotReloadContext::<TestModel>::new();
945        context.cache_hits.store(0, Ordering::Relaxed);
946        context.cache_misses.store(5, Ordering::Relaxed);
947        assert_eq!(context.calculate_cache_hit_rate(), 0.0);
948    }
949
950    #[test]
951    fn test_cache_hit_rate_full_hits() {
952        use std::sync::atomic::Ordering;
953
954        let mut context = HotReloadContext::<TestModel>::new();
955        context.cache_hits.store(5, Ordering::Relaxed);
956        context.cache_misses.store(0, Ordering::Relaxed);
957        assert_eq!(context.calculate_cache_hit_rate(), 1.0);
958    }
959
960    #[test]
961    fn test_attempt_hot_reload_success() {
962        use dampen_core::handler::HandlerRegistry;
963        use dampen_core::parser;
964
965        // Create initial state with a model
966        let xml_v1 = r#"<dampen version="1.1" encoding="utf-8"><column><text value="Version 1" /></column></dampen>"#;
967        let doc_v1 = parser::parse(xml_v1).unwrap();
968        let model_v1 = TestModel {
969            count: 42,
970            name: "Alice".to_string(),
971        };
972        let registry_v1 = HandlerRegistry::new();
973        let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
974
975        // Create hot-reload context
976        let mut context = HotReloadContext::<TestModel>::new();
977
978        // New XML with changes
979        let xml_v2 = r#"<dampen version="1.1" encoding="utf-8"><column><text value="Version 2" /></column></dampen>"#;
980
981        // Attempt hot-reload
982        let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || HandlerRegistry::new());
983
984        // Should succeed and preserve model
985        match result {
986            ReloadResult::Success(new_state) => {
987                assert_eq!(new_state.model.count, 42);
988                assert_eq!(new_state.model.name, "Alice");
989                assert_eq!(context.reload_count, 1);
990            }
991            _ => panic!("Expected Success, got {:?}", result),
992        }
993    }
994
995    #[test]
996    fn test_attempt_hot_reload_parse_error() {
997        use dampen_core::handler::HandlerRegistry;
998        use dampen_core::parser;
999
1000        // Create initial state
1001        let xml_v1 = r#"<dampen version="1.1" encoding="utf-8"><column><text value="Version 1" /></column></dampen>"#;
1002        let doc_v1 = parser::parse(xml_v1).unwrap();
1003        let model_v1 = TestModel {
1004            count: 10,
1005            name: "Bob".to_string(),
1006        };
1007        let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
1008
1009        let mut context = HotReloadContext::<TestModel>::new();
1010
1011        // Invalid XML (unclosed tag)
1012        let xml_invalid = r#"<dampen version="1.1" encoding="utf-8"><column><text value="Broken"#;
1013
1014        // Attempt hot-reload
1015        let result = attempt_hot_reload(xml_invalid, &state_v1, &mut context, || {
1016            HandlerRegistry::new()
1017        });
1018
1019        // Should return ParseError
1020        match result {
1021            ReloadResult::ParseError(_err) => {
1022                // Expected
1023                assert_eq!(context.reload_count, 1); // Failed reload is recorded
1024            }
1025            _ => panic!("Expected ParseError, got {:?}", result),
1026        }
1027    }
1028
1029    #[test]
1030    fn test_attempt_hot_reload_model_restore_failure() {
1031        use dampen_core::handler::HandlerRegistry;
1032        use dampen_core::parser;
1033
1034        // Create initial state
1035        let xml_v1 = r#"<dampen version="1.1" encoding="utf-8"><column><text value="Version 1" /></column></dampen>"#;
1036        let doc_v1 = parser::parse(xml_v1).unwrap();
1037        let model_v1 = TestModel {
1038            count: 99,
1039            name: "Charlie".to_string(),
1040        };
1041        let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
1042
1043        // Create context and manually corrupt the snapshot to trigger restore failure
1044        let mut context = HotReloadContext::<TestModel>::new();
1045        context.last_model_snapshot = Some("{ invalid json }".to_string()); // Invalid JSON
1046
1047        // New valid XML
1048        let xml_v2 = r#"<dampen version="1.1" encoding="utf-8"><column><text value="Version 2" /></column></dampen>"#;
1049
1050        // Attempt hot-reload (will snapshot current model, then try to restore from corrupted snapshot)
1051        let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || HandlerRegistry::new());
1052
1053        // The function snapshots the current model first, so it will actually succeed
1054        // because the new snapshot overwrites the corrupted one.
1055        // To truly test restore failure, we need to test the restore_model method directly,
1056        // which we already do in test_restore_model_no_snapshot.
1057
1058        // This test actually validates that the snapshot-before-parse strategy works correctly.
1059        match result {
1060            ReloadResult::Success(new_state) => {
1061                // Model preserved via the snapshot taken at the start of attempt_hot_reload
1062                assert_eq!(new_state.model.count, 99);
1063                assert_eq!(new_state.model.name, "Charlie");
1064                assert_eq!(context.reload_count, 1);
1065            }
1066            _ => panic!("Expected Success, got {:?}", result),
1067        }
1068    }
1069
1070    #[test]
1071    fn test_attempt_hot_reload_preserves_model_across_multiple_reloads() {
1072        use dampen_core::handler::HandlerRegistry;
1073        use dampen_core::parser;
1074
1075        // Create initial state
1076        let xml_v1 = r#"<dampen version="1.1" encoding="utf-8"><column><text value="V1" /></column></dampen>"#;
1077        let doc_v1 = parser::parse(xml_v1).unwrap();
1078        let model_v1 = TestModel {
1079            count: 100,
1080            name: "Dave".to_string(),
1081        };
1082        let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
1083
1084        let mut context = HotReloadContext::<TestModel>::new();
1085
1086        // First reload
1087        let xml_v2 = r#"<dampen version="1.1" encoding="utf-8"><column><text value="V2" /></column></dampen>"#;
1088        let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || HandlerRegistry::new());
1089
1090        let state_v2 = match result {
1091            ReloadResult::Success(s) => s,
1092            _ => panic!("First reload failed"),
1093        };
1094
1095        assert_eq!(state_v2.model.count, 100);
1096        assert_eq!(state_v2.model.name, "Dave");
1097
1098        // Second reload
1099        let xml_v3 = r#"<dampen version="1.1" encoding="utf-8"><column><text value="V3" /></column></dampen>"#;
1100        let result = attempt_hot_reload(xml_v3, &state_v2, &mut context, || HandlerRegistry::new());
1101
1102        let state_v3 = match result {
1103            ReloadResult::Success(s) => s,
1104            _ => panic!("Second reload failed"),
1105        };
1106
1107        // Model still preserved
1108        assert_eq!(state_v3.model.count, 100);
1109        assert_eq!(state_v3.model.name, "Dave");
1110        assert_eq!(context.reload_count, 2);
1111    }
1112
1113    #[test]
1114    fn test_attempt_hot_reload_with_handler_registry() {
1115        use dampen_core::handler::HandlerRegistry;
1116        use dampen_core::parser;
1117
1118        // Create initial state
1119        let xml_v1 = r#"<dampen version="1.1" encoding="utf-8"><column><button label="Click" on_click="test" /></column></dampen>"#;
1120        let doc_v1 = parser::parse(xml_v1).unwrap();
1121        let model_v1 = TestModel {
1122            count: 5,
1123            name: "Eve".to_string(),
1124        };
1125
1126        let registry_v1 = HandlerRegistry::new();
1127        registry_v1.register_simple("test", |_model| {
1128            // Handler v1
1129        });
1130
1131        let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
1132
1133        let mut context = HotReloadContext::<TestModel>::new();
1134
1135        // New XML with different handler
1136        let xml_v2 = r#"<dampen version="1.1" encoding="utf-8"><column><button label="Click Me" on_click="test2" /></column></dampen>"#;
1137
1138        // Create NEW handler registry (simulating code change)
1139        let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1140            let registry = HandlerRegistry::new();
1141            registry.register_simple("test2", |_model| {
1142                // Handler v2
1143            });
1144            registry
1145        });
1146
1147        // Should succeed
1148        match result {
1149            ReloadResult::Success(new_state) => {
1150                // Model preserved
1151                assert_eq!(new_state.model.count, 5);
1152                assert_eq!(new_state.model.name, "Eve");
1153
1154                // Handler registry updated
1155                assert!(new_state.handler_registry.get("test2").is_some());
1156            }
1157            _ => panic!("Expected Success, got {:?}", result),
1158        }
1159    }
1160
1161    #[test]
1162    fn test_collect_handler_names() {
1163        use dampen_core::parser;
1164
1165        // XML with multiple handlers
1166        let xml = r#"
1167            <dampen version="1.1" encoding="utf-8">
1168                <column>
1169                    <button label="Click" on_click="handle_click" />
1170                    <text_input placeholder="Type" on_input="handle_input" />
1171                    <button label="Submit" on_click="handle_submit" />
1172                </column>
1173            </dampen>
1174        "#;
1175
1176        let doc = parser::parse(xml).unwrap();
1177        let handlers = collect_handler_names(&doc);
1178
1179        // Should collect all three unique handlers
1180        assert_eq!(handlers.len(), 3);
1181        assert!(handlers.contains(&"handle_click".to_string()));
1182        assert!(handlers.contains(&"handle_input".to_string()));
1183        assert!(handlers.contains(&"handle_submit".to_string()));
1184    }
1185
1186    #[test]
1187    fn test_collect_handler_names_nested() {
1188        use dampen_core::parser;
1189
1190        // XML with nested handlers
1191        let xml = r#"
1192            <dampen version="1.1" encoding="utf-8">
1193                <column>
1194                    <row>
1195                        <button label="A" on_click="handler_a" />
1196                    </row>
1197                    <row>
1198                        <button label="B" on_click="handler_b" />
1199                        <column>
1200                            <button label="C" on_click="handler_c" />
1201                        </column>
1202                    </row>
1203                </column>
1204            </dampen>
1205        "#;
1206
1207        let doc = parser::parse(xml).unwrap();
1208        let handlers = collect_handler_names(&doc);
1209
1210        // Should collect handlers from all nesting levels
1211        assert_eq!(handlers.len(), 3);
1212        assert!(handlers.contains(&"handler_a".to_string()));
1213        assert!(handlers.contains(&"handler_b".to_string()));
1214        assert!(handlers.contains(&"handler_c".to_string()));
1215    }
1216
1217    #[test]
1218    fn test_collect_handler_names_duplicates() {
1219        use dampen_core::parser;
1220
1221        // XML with duplicate handler names
1222        let xml = r#"
1223            <dampen version="1.1" encoding="utf-8">
1224                <column>
1225                    <button label="1" on_click="same_handler" />
1226                    <button label="2" on_click="same_handler" />
1227                    <button label="3" on_click="same_handler" />
1228                </column>
1229            </dampen>
1230        "#;
1231
1232        let doc = parser::parse(xml).unwrap();
1233        let handlers = collect_handler_names(&doc);
1234
1235        // Should deduplicate
1236        assert_eq!(handlers.len(), 1);
1237        assert!(handlers.contains(&"same_handler".to_string()));
1238    }
1239
1240    #[test]
1241    fn test_validate_handlers_all_present() {
1242        use dampen_core::handler::HandlerRegistry;
1243        use dampen_core::parser;
1244
1245        let xml = r#"
1246            <dampen version="1.1" encoding="utf-8">
1247                <column>
1248                    <button label="Click" on_click="test_handler" />
1249                </column>
1250            </dampen>
1251        "#;
1252
1253        let doc = parser::parse(xml).unwrap();
1254        let registry = HandlerRegistry::new();
1255        registry.register_simple("test_handler", |_model| {});
1256
1257        let result = validate_handlers(&doc, &registry);
1258        assert!(result.is_ok());
1259    }
1260
1261    #[test]
1262    fn test_validate_handlers_missing() {
1263        use dampen_core::handler::HandlerRegistry;
1264        use dampen_core::parser;
1265
1266        let xml = r#"
1267            <dampen version="1.1" encoding="utf-8">
1268                <column>
1269                    <button label="Click" on_click="missing_handler" />
1270                </column>
1271            </dampen>
1272        "#;
1273
1274        let doc = parser::parse(xml).unwrap();
1275        let registry = HandlerRegistry::new();
1276        // Registry is empty, handler not registered
1277
1278        let result = validate_handlers(&doc, &registry);
1279        assert!(result.is_err());
1280
1281        let missing = result.unwrap_err();
1282        assert_eq!(missing.len(), 1);
1283        assert_eq!(missing[0], "missing_handler");
1284    }
1285
1286    #[test]
1287    fn test_validate_handlers_multiple_missing() {
1288        use dampen_core::handler::HandlerRegistry;
1289        use dampen_core::parser;
1290
1291        let xml = r#"
1292            <dampen version="1.1" encoding="utf-8">
1293                <column>
1294                    <button label="A" on_click="handler_a" />
1295                    <button label="B" on_click="handler_b" />
1296                    <button label="C" on_click="handler_c" />
1297                </column>
1298            </dampen>
1299        "#;
1300
1301        let doc = parser::parse(xml).unwrap();
1302        let registry = HandlerRegistry::new();
1303        // Only register handler_b
1304        registry.register_simple("handler_b", |_model| {});
1305
1306        let result = validate_handlers(&doc, &registry);
1307        assert!(result.is_err());
1308
1309        let missing = result.unwrap_err();
1310        assert_eq!(missing.len(), 2);
1311        assert!(missing.contains(&"handler_a".to_string()));
1312        assert!(missing.contains(&"handler_c".to_string()));
1313    }
1314
1315    #[test]
1316    fn test_attempt_hot_reload_validation_error() {
1317        use dampen_core::handler::HandlerRegistry;
1318        use dampen_core::parser;
1319
1320        // Create initial state
1321        let xml_v1 = r#"<dampen version="1.1" encoding="utf-8"><column><text value="V1" /></column></dampen>"#;
1322        let doc_v1 = parser::parse(xml_v1).unwrap();
1323        let model_v1 = TestModel {
1324            count: 10,
1325            name: "Test".to_string(),
1326        };
1327        let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
1328
1329        let mut context = HotReloadContext::<TestModel>::new();
1330
1331        // New XML with a handler that won't be registered
1332        let xml_v2 = r#"
1333            <dampen version="1.1" encoding="utf-8">
1334                <column>
1335                    <button label="Click" on_click="unregistered_handler" />
1336                </column>
1337            </dampen>
1338        "#;
1339
1340        // Create handler registry WITHOUT the required handler
1341        let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1342            HandlerRegistry::new() // Empty registry
1343        });
1344
1345        // Should return ValidationError
1346        match result {
1347            ReloadResult::ValidationError(errors) => {
1348                assert!(!errors.is_empty());
1349                assert!(errors[0].contains("unregistered_handler"));
1350                assert_eq!(context.reload_count, 1); // Failed reload is recorded
1351            }
1352            _ => panic!("Expected ValidationError, got {:?}", result),
1353        }
1354    }
1355
1356    #[test]
1357    fn test_attempt_hot_reload_validation_success() {
1358        use dampen_core::handler::HandlerRegistry;
1359        use dampen_core::parser;
1360
1361        // Create initial state
1362        let xml_v1 = r#"<dampen version="1.1" encoding="utf-8"><column><text value="V1" /></column></dampen>"#;
1363        let doc_v1 = parser::parse(xml_v1).unwrap();
1364        let model_v1 = TestModel {
1365            count: 20,
1366            name: "Valid".to_string(),
1367        };
1368        let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
1369
1370        let mut context = HotReloadContext::<TestModel>::new();
1371
1372        // New XML with a handler
1373        let xml_v2 = r#"
1374            <dampen version="1.1" encoding="utf-8">
1375                <column>
1376                    <button label="Click" on_click="registered_handler" />
1377                </column>
1378            </dampen>
1379        "#;
1380
1381        // Create handler registry WITH the required handler
1382        let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1383            let registry = HandlerRegistry::new();
1384            registry.register_simple("registered_handler", |_model| {});
1385            registry
1386        });
1387
1388        // Should succeed
1389        match result {
1390            ReloadResult::Success(new_state) => {
1391                assert_eq!(new_state.model.count, 20);
1392                assert_eq!(new_state.model.name, "Valid");
1393                assert_eq!(context.reload_count, 1);
1394            }
1395            _ => panic!("Expected Success, got {:?}", result),
1396        }
1397    }
1398
1399    #[test]
1400    fn test_handler_registry_complete_replacement() {
1401        use dampen_core::handler::HandlerRegistry;
1402        use dampen_core::parser;
1403
1404        // Create initial state with handler "old_handler"
1405        let xml_v1 = r#"
1406            <dampen version="1.1" encoding="utf-8">
1407                <column>
1408                    <button label="Old" on_click="old_handler" />
1409                </column>
1410            </dampen>
1411        "#;
1412        let doc_v1 = parser::parse(xml_v1).unwrap();
1413        let model_v1 = TestModel {
1414            count: 1,
1415            name: "Initial".to_string(),
1416        };
1417
1418        let registry_v1 = HandlerRegistry::new();
1419        registry_v1.register_simple("old_handler", |_model| {});
1420
1421        let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
1422
1423        // Verify old handler exists
1424        assert!(state_v1.handler_registry.get("old_handler").is_some());
1425
1426        let mut context = HotReloadContext::<TestModel>::new();
1427
1428        // New XML with completely different handler
1429        let xml_v2 = r#"
1430            <dampen version="1.1" encoding="utf-8">
1431                <column>
1432                    <button label="New" on_click="new_handler" />
1433                    <button label="Another" on_click="another_handler" />
1434                </column>
1435            </dampen>
1436        "#;
1437
1438        // Rebuild registry with NEW handlers only (old_handler not included)
1439        let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1440            let registry = HandlerRegistry::new();
1441            registry.register_simple("new_handler", |_model| {});
1442            registry.register_simple("another_handler", |_model| {});
1443            registry
1444        });
1445
1446        // Should succeed
1447        match result {
1448            ReloadResult::Success(new_state) => {
1449                // Model preserved
1450                assert_eq!(new_state.model.count, 1);
1451                assert_eq!(new_state.model.name, "Initial");
1452
1453                // Old handler should NOT exist in new registry
1454                assert!(new_state.handler_registry.get("old_handler").is_none());
1455
1456                // New handlers should exist
1457                assert!(new_state.handler_registry.get("new_handler").is_some());
1458                assert!(new_state.handler_registry.get("another_handler").is_some());
1459            }
1460            _ => panic!("Expected Success, got {:?}", result),
1461        }
1462    }
1463
1464    #[test]
1465    fn test_handler_registry_rebuild_before_validation() {
1466        use dampen_core::handler::HandlerRegistry;
1467        use dampen_core::parser;
1468
1469        // This test validates that registry is rebuilt BEFORE validation happens
1470        // Scenario: Old state has handler A, new XML needs handler B
1471        // If registry is rebuilt before validation, it should succeed
1472
1473        let xml_v1 = r#"<dampen version="1.1" encoding="utf-8"><column><button on_click="handler_a" label="A" /></column></dampen>"#;
1474        let doc_v1 = parser::parse(xml_v1).unwrap();
1475        let model_v1 = TestModel {
1476            count: 100,
1477            name: "Test".to_string(),
1478        };
1479
1480        let registry_v1 = HandlerRegistry::new();
1481        registry_v1.register_simple("handler_a", |_model| {});
1482
1483        let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
1484
1485        let mut context = HotReloadContext::<TestModel>::new();
1486
1487        // New XML references handler_b (different from handler_a)
1488        let xml_v2 = r#"<dampen version="1.1" encoding="utf-8"><column><button on_click="handler_b" label="B" /></column></dampen>"#;
1489
1490        // Registry rebuild provides handler_b
1491        let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1492            let registry = HandlerRegistry::new();
1493            registry.register_simple("handler_b", |_model| {}); // Different handler!
1494            registry
1495        });
1496
1497        // Should succeed because registry was rebuilt with handler_b BEFORE validation
1498        match result {
1499            ReloadResult::Success(new_state) => {
1500                assert_eq!(new_state.model.count, 100);
1501                // Verify new handler exists
1502                assert!(new_state.handler_registry.get("handler_b").is_some());
1503                // Verify old handler is gone
1504                assert!(new_state.handler_registry.get("handler_a").is_none());
1505            }
1506            _ => panic!(
1507                "Expected Success (registry rebuilt before validation), got {:?}",
1508                result
1509            ),
1510        }
1511    }
1512}
1513
1514#[cfg(test)]
1515mod theme_reload_tests {
1516    use super::*;
1517    use dampen_core::ir::style::Color;
1518    use dampen_core::ir::theme::{SpacingScale, Theme, ThemeDocument, ThemePalette, Typography};
1519    use tempfile::TempDir;
1520
1521    fn create_test_palette(primary: &str) -> ThemePalette {
1522        ThemePalette {
1523            primary: Some(Color::from_hex(primary).unwrap()),
1524            secondary: Some(Color::from_hex("#2ecc71").unwrap()),
1525            success: Some(Color::from_hex("#27ae60").unwrap()),
1526            warning: Some(Color::from_hex("#f39c12").unwrap()),
1527            danger: Some(Color::from_hex("#e74c3c").unwrap()),
1528            background: Some(Color::from_hex("#ecf0f1").unwrap()),
1529            surface: Some(Color::from_hex("#ffffff").unwrap()),
1530            text: Some(Color::from_hex("#2c3e50").unwrap()),
1531            text_secondary: Some(Color::from_hex("#7f8c8d").unwrap()),
1532        }
1533    }
1534
1535    fn create_test_theme(name: &str, primary: &str) -> Theme {
1536        Theme {
1537            name: name.to_string(),
1538            palette: create_test_palette(primary),
1539            typography: Typography {
1540                font_family: Some("sans-serif".to_string()),
1541                font_size_base: Some(16.0),
1542                font_size_small: Some(12.0),
1543                font_size_large: Some(24.0),
1544                font_weight: dampen_core::ir::theme::FontWeight::Normal,
1545                line_height: Some(1.5),
1546            },
1547            spacing: SpacingScale { unit: Some(8.0) },
1548            base_styles: std::collections::HashMap::new(),
1549            extends: None,
1550        }
1551    }
1552
1553    fn create_test_document() -> ThemeDocument {
1554        ThemeDocument {
1555            themes: std::collections::HashMap::from([
1556                ("light".to_string(), create_test_theme("light", "#3498db")),
1557                ("dark".to_string(), create_test_theme("dark", "#5dade2")),
1558            ]),
1559            default_theme: Some("light".to_string()),
1560            follow_system: true,
1561        }
1562    }
1563
1564    fn create_test_theme_context() -> dampen_core::state::ThemeContext {
1565        let doc = create_test_document();
1566        dampen_core::state::ThemeContext::from_document(doc, None).unwrap()
1567    }
1568
1569    #[test]
1570    fn test_attempt_theme_hot_reload_success() {
1571        let temp_dir = TempDir::new().unwrap();
1572        let theme_dir = temp_dir.path().join("src/ui/theme");
1573        std::fs::create_dir_all(&theme_dir).unwrap();
1574
1575        let theme_content = r##"
1576            <dampen version="1.1" encoding="utf-8">
1577                <themes>
1578                    <theme name="light">
1579                        <palette
1580                            primary="#111111"
1581                            secondary="#2ecc71"
1582                            success="#27ae60"
1583                            warning="#f39c12"
1584                            danger="#e74c3c"
1585                            background="#ecf0f1"
1586                            surface="#ffffff"
1587                            text="#2c3e50"
1588                            text_secondary="#7f8c8d" />
1589                    </theme>
1590                </themes>
1591                <default_theme name="light" />
1592            </dampen>
1593        "##;
1594
1595        let theme_path = theme_dir.join("theme.dampen");
1596        std::fs::write(&theme_path, theme_content).unwrap();
1597
1598        let mut theme_ctx = Some(create_test_theme_context());
1599        let result = attempt_theme_hot_reload(&theme_path, &mut theme_ctx);
1600
1601        match result {
1602            ThemeReloadResult::Success => {
1603                let ctx = theme_ctx.unwrap();
1604                assert_eq!(ctx.active_name(), "light");
1605                assert_eq!(
1606                    ctx.active().palette.primary,
1607                    Some(Color::from_hex("#111111").unwrap())
1608                );
1609            }
1610            _ => panic!("Expected Success, got {:?}", result),
1611        }
1612    }
1613
1614    #[test]
1615    fn test_attempt_theme_hot_reload_parse_error() {
1616        let temp_dir = TempDir::new().unwrap();
1617        let theme_dir = temp_dir.path().join("src/ui/theme");
1618        std::fs::create_dir_all(&theme_dir).unwrap();
1619
1620        let theme_path = theme_dir.join("theme.dampen");
1621        std::fs::write(&theme_path, "invalid xml").unwrap();
1622
1623        let mut theme_ctx = Some(create_test_theme_context());
1624        let result = attempt_theme_hot_reload(&theme_path, &mut theme_ctx);
1625
1626        match result {
1627            ThemeReloadResult::ParseError(_) => {}
1628            _ => panic!("Expected ParseError, got {:?}", result),
1629        }
1630    }
1631
1632    #[test]
1633    fn test_attempt_theme_hot_reload_no_theme_context() {
1634        let temp_dir = TempDir::new().unwrap();
1635        let theme_dir = temp_dir.path().join("src/ui/theme");
1636        std::fs::create_dir_all(&theme_dir).unwrap();
1637
1638        let theme_path = theme_dir.join("theme.dampen");
1639        std::fs::write(
1640            &theme_path,
1641            r#"<dampen version="1.1" encoding="utf-8"><themes></themes></dampen>"#,
1642        )
1643        .unwrap();
1644
1645        let mut theme_ctx: Option<dampen_core::state::ThemeContext> = None;
1646        let result = attempt_theme_hot_reload(&theme_path, &mut theme_ctx);
1647
1648        match result {
1649            ThemeReloadResult::NoThemeContext => {}
1650            _ => panic!("Expected NoThemeContext, got {:?}", result),
1651        }
1652    }
1653
1654    #[test]
1655    fn test_attempt_theme_hot_reload_preserves_active_theme() {
1656        let temp_dir = TempDir::new().unwrap();
1657        let theme_dir = temp_dir.path().join("src/ui/theme");
1658        std::fs::create_dir_all(&theme_dir).unwrap();
1659
1660        let theme_content = r##"
1661            <dampen version="1.1" encoding="utf-8">
1662                <themes>
1663                    <theme name="new_theme">
1664                        <palette
1665                            primary="#ff0000"
1666                            secondary="#2ecc71"
1667                            success="#27ae60"
1668                            warning="#f39c12"
1669                            danger="#e74c3c"
1670                            background="#ecf0f1"
1671                            surface="#ffffff"
1672                            text="#2c3e50"
1673                            text_secondary="#7f8c8d" />
1674                    </theme>
1675                    <theme name="dark">
1676                        <palette
1677                            primary="#5dade2"
1678                            secondary="#52be80"
1679                            success="#27ae60"
1680                            warning="#f39c12"
1681                            danger="#ec7063"
1682                            background="#2c3e50"
1683                            surface="#34495e"
1684                            text="#ecf0f1"
1685                            text_secondary="#95a5a6" />
1686                    </theme>
1687                </themes>
1688                <default_theme name="new_theme" />
1689            </dampen>
1690        "##;
1691
1692        let theme_path = theme_dir.join("theme.dampen");
1693        std::fs::write(&theme_path, theme_content).unwrap();
1694
1695        let mut theme_ctx = Some(create_test_theme_context());
1696        theme_ctx.as_mut().unwrap().set_theme("dark").unwrap();
1697        assert_eq!(theme_ctx.as_ref().unwrap().active_name(), "dark");
1698
1699        let result = attempt_theme_hot_reload(&theme_path, &mut theme_ctx);
1700
1701        match result {
1702            ThemeReloadResult::Success => {
1703                let ctx = theme_ctx.unwrap();
1704                assert_eq!(ctx.active_name(), "dark");
1705            }
1706            _ => panic!("Expected Success, got {:?}", result),
1707        }
1708    }
1709
1710    #[test]
1711    fn test_is_theme_file_path() {
1712        assert!(is_theme_file_path(&std::path::PathBuf::from(
1713            "src/ui/theme/theme.dampen"
1714        )));
1715        assert!(!is_theme_file_path(&std::path::PathBuf::from(
1716            "src/ui/window.dampen"
1717        )));
1718        assert!(is_theme_file_path(&std::path::PathBuf::from(
1719            "/some/path/theme.dampen"
1720        )));
1721    }
1722
1723    #[test]
1724    fn test_get_theme_dir_from_path() {
1725        let temp_dir = TempDir::new().unwrap();
1726        let theme_dir = temp_dir.path().join("src/ui/theme");
1727        std::fs::create_dir_all(&theme_dir).unwrap();
1728        let theme_path = theme_dir.join("theme.dampen");
1729        std::fs::write(
1730            &theme_path,
1731            "<dampen version=\"1.0\"><themes></themes></dampen>",
1732        )
1733        .unwrap();
1734
1735        let result = get_theme_dir_from_path(&theme_path);
1736        assert!(result.is_some());
1737        assert_eq!(result.unwrap(), theme_dir);
1738    }
1739}