boxmux_lib/model/
app.rs

1use crate::model::muxbox::*;
2use crate::{model::layout::Layout, Bounds};
3use crate::live_yaml_sync::LiveYamlSync;
4
5use std::fs::File;
6use std::io::Read;
7
8use petgraph::graph::{DiGraph, NodeIndex};
9use petgraph::visit::EdgeRef;
10use serde::{Deserialize, Serialize};
11
12// F0200: Serializable wrapper for complete app state
13#[derive(Debug, Serialize)]
14struct SerializableApp {
15    app: App,
16}
17
18// Implement custom serialization that includes proper app wrapper
19impl SerializableApp {
20    fn to_yaml_string(&self) -> Result<String, Box<dyn std::error::Error>> {
21        // Create proper app structure for YAML
22        let yaml_content = serde_yaml::to_string(&self)?;
23        Ok(yaml_content)
24    }
25}
26use serde_yaml;
27use std::collections::HashMap;
28use std::sync::Arc;
29
30use crate::validation::SchemaValidator;
31use crate::{calculate_bounds_map, Config, FieldUpdate, Updatable};
32use core::hash::Hash;
33use regex::Regex;
34use std::env;
35use std::hash::{DefaultHasher, Hasher};
36
37/// Variable context system implementing correct hierarchical precedence:
38/// Child MuxBox > Parent MuxBox > Layout > App Global > Environment > Default
39#[derive(Debug, Clone)]
40pub struct VariableContext {
41    app_vars: HashMap<String, String>,
42    layout_vars: HashMap<String, String>,
43}
44
45impl VariableContext {
46    pub fn new(
47        app_vars: Option<&HashMap<String, String>>,
48        layout_vars: Option<&HashMap<String, String>>,
49    ) -> Self {
50        Self {
51            app_vars: app_vars.cloned().unwrap_or_default(),
52            layout_vars: layout_vars.cloned().unwrap_or_default(),
53        }
54    }
55
56    /// Resolve variable with correct precedence order:
57    /// MuxBox Hierarchy (child->parent) > Layout > App > Environment > Default
58    /// This allows YAML-defined variables to override environment for granular control
59    pub fn resolve_variable(
60        &self,
61        name: &str,
62        default: &str,
63        muxbox_hierarchy: &[&MuxBox],
64    ) -> String {
65        // Walk up muxbox hierarchy from most granular (child) to least granular (root parent)
66        for muxbox in muxbox_hierarchy.iter() {
67            if let Some(variables) = &muxbox.variables {
68                if let Some(muxbox_val) = variables.get(name) {
69                    return muxbox_val.clone();
70                }
71            }
72        }
73
74        // Layout-level variables
75        if let Some(layout_val) = self.layout_vars.get(name) {
76            return layout_val.clone();
77        }
78
79        // App-global variables
80        if let Some(app_val) = self.app_vars.get(name) {
81            return app_val.clone();
82        }
83
84        // Environment variables as fallback before defaults
85        if let Ok(env_val) = env::var(name) {
86            return env_val;
87        }
88
89        // Finally, use default value
90        default.to_string()
91    }
92
93    /// Apply variable substitution to a string with hierarchical context
94    pub fn substitute_in_string(
95        &self,
96        content: &str,
97        muxbox_hierarchy: &[&MuxBox],
98    ) -> Result<String, Box<dyn std::error::Error>> {
99        let mut result = content.to_string();
100
101        // Check for nested variables and fail gracefully with location info
102        if result.contains("${") {
103            let nested_pattern = Regex::new(r"\$\{[^}]*\$\{[^}]*\}")?;
104            if let Some(nested_match) = nested_pattern.find(&result) {
105                let problematic_text = &result[nested_match.start()..nested_match.end()];
106                return Err(format!(
107                    "Nested variable substitution is not supported. Found: '{}' - Use simple variables only.",
108                    problematic_text
109                ).into());
110            }
111        }
112
113        // Pattern for variable substitution: ${VAR_NAME} or ${VAR_NAME:default_value}
114        // Updated to handle the case where default contains ${} more carefully
115        let var_pattern = Regex::new(r"\$\{([^}:]+)(?::([^}]*))?\}")?;
116
117        result = var_pattern
118            .replace_all(&result, |caps: &regex::Captures| {
119                let var_name = &caps[1];
120                let default_value = caps.get(2).map_or("", |m| m.as_str());
121
122                // Additional check for malformed nested syntax
123                if default_value.contains("${") && !default_value.ends_with("}") {
124                    return format!(
125                        "error: malformed nested variable in default for '{}'",
126                        var_name
127                    );
128                }
129
130                self.resolve_variable(var_name, default_value, muxbox_hierarchy)
131            })
132            .to_string();
133
134        // Pattern for simple environment variables: $VAR_NAME
135        let env_pattern = Regex::new(r"\$([A-Z_][A-Z0-9_]*)")?;
136
137        result = env_pattern
138            .replace_all(&result, |caps: &regex::Captures| {
139                let var_name = &caps[1];
140                self.resolve_variable(var_name, &format!("${}", var_name), muxbox_hierarchy)
141            })
142            .to_string();
143
144        Ok(result)
145    }
146}
147
148#[derive(Debug, Deserialize, Serialize)]
149pub struct TemplateRoot {
150    pub app: App,
151}
152
153#[derive(Debug, Deserialize, Serialize)]
154pub struct App {
155    pub layouts: Vec<Layout>,
156    #[serde(default)]
157    pub libs: Option<Vec<String>>,
158    #[serde(default)]
159    pub on_keypress: Option<HashMap<String, Vec<String>>>,
160    #[serde(default)]
161    pub hot_keys: Option<HashMap<String, String>>,
162    #[serde(default)]
163    pub variables: Option<HashMap<String, String>>,
164    #[serde(skip)]
165    app_graph: Option<AppGraph>,
166    #[serde(skip)]
167    pub adjusted_bounds: Option<HashMap<String, HashMap<String, Bounds>>>,
168}
169
170impl PartialEq for App {
171    fn eq(&self, other: &Self) -> bool {
172        self.layouts == other.layouts
173            && self.on_keypress == other.on_keypress
174            && self.hot_keys == other.hot_keys
175            && self.app_graph == other.app_graph
176            && self.adjusted_bounds == other.adjusted_bounds
177    }
178}
179
180impl Eq for App {}
181
182impl Default for App {
183    fn default() -> Self {
184        Self::new()
185    }
186}
187
188impl App {
189    pub fn new() -> Self {
190        App {
191            layouts: Vec::new(),
192            libs: None,
193            on_keypress: None,
194            hot_keys: None,
195            variables: None,
196            app_graph: None,
197            adjusted_bounds: None,
198        }
199    }
200
201    pub fn get_adjusted_bounds(
202        &mut self,
203        force_readjust: Option<bool>,
204    ) -> &HashMap<String, HashMap<String, Bounds>> {
205        if self.adjusted_bounds.is_none() || force_readjust.unwrap_or(false) {
206            self.adjusted_bounds = Some(self.calculate_bounds());
207        }
208        self.adjusted_bounds
209            .as_ref()
210            .expect("Failed to calculate adjusted bounds!")
211    }
212
213    pub fn get_adjusted_bounds_and_app_graph(
214        &mut self,
215        force_readjust: Option<bool>,
216    ) -> (HashMap<String, HashMap<String, Bounds>>, AppGraph) {
217        // First, get the adjusted bounds by cloning the content
218        let adjusted_bounds = self.get_adjusted_bounds(force_readjust).clone();
219
220        // Then, generate the app graph
221        let app_graph = self.generate_graph();
222
223        (adjusted_bounds, app_graph)
224    }
225
226    pub fn get_layout_by_id(&self, id: &str) -> Option<&Layout> {
227        self.layouts.iter().find(|l| l.id == id)
228    }
229
230    pub fn get_layout_by_id_mut(&mut self, id: &str) -> Option<&mut Layout> {
231        self.layouts.iter_mut().find(|l| l.id == id)
232    }
233
234    pub fn get_root_layout(&self) -> Option<&Layout> {
235        let mut roots = self.layouts.iter().filter(|l| l.root.unwrap_or(false));
236        match roots.clone().count() {
237            1 => roots.next(),
238            0 => None,
239            _ => panic!("Multiple root layouts found, which is not allowed."),
240        }
241    }
242
243    pub fn get_root_layout_mut(&mut self) -> Option<&mut Layout> {
244        let mut roots: Vec<&mut Layout> = self
245            .layouts
246            .iter_mut()
247            .filter(|l| l.root.unwrap_or(false))
248            .collect();
249
250        match roots.len() {
251            1 => Some(roots.remove(0)),
252            0 => None,
253            _ => panic!("Multiple root layouts found, which is not allowed."),
254        }
255    }
256
257    pub fn get_active_layout(&self) -> Option<&Layout> {
258        let mut actives = self.layouts.iter().filter(|l| l.active.unwrap_or(false));
259        match actives.clone().count() {
260            1 => actives.next(),
261            0 => None,
262            _ => panic!("Multiple active layouts found, which is not allowed."),
263        }
264    }
265
266    pub fn get_active_layout_mut(&mut self) -> Option<&mut Layout> {
267        let mut actives: Vec<&mut Layout> = self
268            .layouts
269            .iter_mut()
270            .filter(|l| l.active.unwrap_or(false))
271            .collect();
272
273        match actives.len() {
274            1 => Some(actives.remove(0)),
275            0 => None,
276            _ => panic!("Multiple active layouts found, which is not allowed."),
277        }
278    }
279
280    pub fn set_active_layout(&mut self, layout_id: &str) {
281        // Track whether we found the layout with the given ID.
282        let mut found_layout = false;
283
284        // Iterate through the layouts to set the active and root status.
285        for layout in &mut self.layouts {
286            if layout.id == layout_id {
287                // If the layout matches the requested ID, set it as active and root.
288                layout.active = Some(true);
289                found_layout = true;
290            } else {
291                // Otherwise, deactivate it and unset its root status.
292                layout.active = Some(false);
293            }
294        }
295
296        // Log an error if no layout with the given ID was found.
297        if !found_layout {
298            log::error!("Layout with ID '{}' not found.", layout_id);
299        }
300    }
301
302    // F0200: Set active layout with YAML persistence
303    pub fn set_active_layout_with_yaml_save(&mut self, layout_id: &str, yaml_path: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
304        self.set_active_layout(layout_id);
305        
306        // Save to YAML if path provided
307        if let Some(path) = yaml_path {
308            save_active_layout_to_yaml(path, layout_id)?;
309        }
310        
311        Ok(())
312    }
313
314    pub fn get_muxbox_by_id(&self, id: &str) -> Option<&MuxBox> {
315        for layout in &self.layouts {
316            if let Some(muxbox) = layout.get_muxbox_by_id(id) {
317                return Some(muxbox);
318            }
319        }
320        None
321    }
322
323    pub fn get_muxbox_by_id_mut(&mut self, id: &str) -> Option<&mut MuxBox> {
324        for layout in &mut self.layouts {
325            if let Some(muxbox) = layout.get_muxbox_by_id_mut(id) {
326                return Some(muxbox);
327            }
328        }
329        None
330    }
331
332    pub fn validate(&mut self) {
333        let mut validator = SchemaValidator::new();
334        match validator.validate_app(self) {
335            Ok(_) => {
336                // Apply post-validation setup
337                if let Err(e) = apply_post_validation_setup(self) {
338                    panic!("Post-validation setup error: {}", e);
339                }
340            }
341            Err(validation_errors) => {
342                let error_messages: Vec<String> = validation_errors
343                    .into_iter()
344                    .map(|e| format!("{}", e))
345                    .collect();
346                let combined_message = error_messages.join("; ");
347                panic!("Validation errors: {}", combined_message);
348            }
349        }
350    }
351
352    pub fn calculate_bounds(&mut self) -> HashMap<String, HashMap<String, Bounds>> {
353        let mut calculated_bounds: HashMap<String, HashMap<String, Bounds>> = HashMap::new();
354
355        let app_graph = self.generate_graph();
356
357        for layout in &mut self.layouts {
358            let calculated_layout_bounds = calculate_bounds_map(&app_graph, layout);
359            calculated_bounds.insert(layout.id.clone(), calculated_layout_bounds);
360        }
361
362        calculated_bounds
363    }
364
365    pub fn generate_graph(&mut self) -> AppGraph {
366        if let Some(app_graph) = self.app_graph.clone() {
367            app_graph
368        } else {
369            let mut app_graph = AppGraph::new();
370
371            for layout in &self.layouts {
372                app_graph.add_layout(layout);
373            }
374            self.app_graph = Some(app_graph.clone());
375            app_graph
376        }
377    }
378
379    pub fn replace_muxbox(&mut self, muxbox: MuxBox) {
380        for layout in &mut self.layouts {
381            if let Some(replaced) = layout.replace_muxbox_recursive(&muxbox) {
382                if replaced {
383                    return;
384                }
385            }
386        }
387    }
388}
389
390impl Clone for App {
391    fn clone(&self) -> Self {
392        App {
393            layouts: self.layouts.to_vec(),
394            libs: self.libs.clone(),
395            on_keypress: self.on_keypress.clone(),
396            hot_keys: self.hot_keys.clone(),
397            variables: self.variables.clone(),
398            app_graph: self.app_graph.clone(),
399            adjusted_bounds: self.adjusted_bounds.clone(),
400        }
401    }
402}
403
404// Implement Updatable for App
405impl Updatable for App {
406    fn generate_diff(&self, other: &Self) -> Vec<FieldUpdate> {
407        let mut updates = Vec::new();
408
409        // Compare each layout
410        for (self_layout, other_layout) in self.layouts.iter().zip(&other.layouts) {
411            updates.extend(self_layout.generate_diff(other_layout));
412        }
413
414        // Compare on_keypress
415        if self.on_keypress != other.on_keypress {
416            updates.push(FieldUpdate {
417                entity_type: crate::EntityType::App,
418                entity_id: None,
419                field_name: "on_keypress".to_string(),
420                new_value: serde_json::to_value(&other.on_keypress).unwrap(),
421            });
422        }
423
424        // Compare adjusted_bounds
425        if self.adjusted_bounds != other.adjusted_bounds {
426            updates.push(FieldUpdate {
427                entity_type: crate::EntityType::App,
428                entity_id: None,
429                field_name: "adjusted_bounds".to_string(),
430                new_value: serde_json::to_value(&other.adjusted_bounds).unwrap(),
431            });
432        }
433
434        updates
435    }
436
437    fn apply_updates(&mut self, updates: Vec<FieldUpdate>) {
438        let updates_for_layouts = updates.clone();
439        for update in updates {
440            if update.entity_id.is_some() {
441                // Skip updates that are not for the top-level entity
442                continue;
443            }
444            match update.field_name.as_str() {
445                "on_keypress" => {
446                    if let Ok(new_on_keypress) = serde_json::from_value::<
447                        Option<HashMap<String, Vec<String>>>,
448                    >(update.new_value.clone())
449                    {
450                        self.on_keypress = new_on_keypress;
451                    }
452                }
453                "adjusted_bounds" => {
454                    if let Ok(new_adjusted_bounds) = serde_json::from_value::<
455                        Option<HashMap<String, HashMap<String, Bounds>>>,
456                    >(update.new_value.clone())
457                    {
458                        self.adjusted_bounds = new_adjusted_bounds;
459                    }
460                }
461                _ => {
462                    log::warn!("Unknown field name for App: {}", update.field_name);
463                }
464            }
465        }
466        for layout in &mut self.layouts {
467            layout.apply_updates(updates_for_layouts.clone());
468        }
469    }
470}
471
472#[derive(Debug)]
473pub struct AppGraph {
474    graphs: HashMap<String, DiGraph<MuxBox, ()>>,
475    node_maps: HashMap<String, HashMap<String, NodeIndex>>,
476}
477
478impl Hash for AppGraph {
479    fn hash<H: Hasher>(&self, state: &mut H) {
480        for (key, graph) in &self.graphs {
481            key.hash(state);
482            for node in graph.node_indices() {
483                graph.node_weight(node).unwrap().hash(state);
484            }
485        }
486        for (key, node_map) in &self.node_maps {
487            key.hash(state);
488            for (node_key, node_index) in node_map {
489                node_key.hash(state);
490                node_index.hash(state);
491            }
492        }
493    }
494}
495
496impl PartialEq for AppGraph {
497    fn eq(&self, other: &Self) -> bool {
498        //compare hashes
499        let mut hasher1 = DefaultHasher::new();
500        let mut hasher2 = DefaultHasher::new();
501        self.hash(&mut hasher1);
502        other.hash(&mut hasher2);
503        hasher1.finish() == hasher2.finish()
504    }
505}
506
507impl Eq for AppGraph {}
508
509impl Default for AppGraph {
510    fn default() -> Self {
511        Self::new()
512    }
513}
514
515impl AppGraph {
516    pub fn new() -> Self {
517        AppGraph {
518            graphs: HashMap::new(),
519            node_maps: HashMap::new(),
520        }
521    }
522
523    pub fn add_layout(&mut self, layout: &Layout) {
524        let mut graph = DiGraph::new();
525        let mut node_map = HashMap::new();
526
527        if let Some(children) = &layout.children {
528            for muxbox in children {
529                self.add_muxbox_recursively(
530                    &mut graph,
531                    &mut node_map,
532                    muxbox.clone(),
533                    None,
534                    &layout.id,
535                );
536            }
537        }
538
539        self.graphs.insert(layout.id.clone(), graph);
540        self.node_maps.insert(layout.id.clone(), node_map);
541    }
542
543    fn add_muxbox_recursively(
544        &self,
545        graph: &mut DiGraph<MuxBox, ()>,
546        node_map: &mut HashMap<String, NodeIndex>,
547        mut muxbox: MuxBox,
548        parent_id: Option<String>,
549        parent_layout_id: &str,
550    ) {
551        muxbox.parent_layout_id = Some(parent_layout_id.to_string());
552        let muxbox_id = muxbox.id.clone();
553        let node_index = graph.add_node(muxbox.clone());
554        node_map.insert(muxbox_id.clone(), node_index);
555
556        if let Some(parent_id) = muxbox.parent_id.clone() {
557            if let Some(&parent_index) = node_map.get(&parent_id) {
558                graph.add_edge(parent_index, node_index, ());
559            }
560        } else if let Some(parent_id) = parent_id {
561            if let Some(&parent_index) = node_map.get(&parent_id) {
562                graph.add_edge(parent_index, node_index, ());
563            }
564        }
565
566        if let Some(children) = muxbox.children {
567            for mut child in children {
568                child.parent_id = Some(muxbox_id.clone());
569                self.add_muxbox_recursively(
570                    graph,
571                    node_map,
572                    child,
573                    Some(muxbox_id.clone()),
574                    parent_layout_id,
575                );
576            }
577        }
578    }
579
580    pub fn get_layout_muxbox_by_id(&self, layout_id: &str, muxbox_id: &str) -> Option<&MuxBox> {
581        self.node_maps.get(layout_id).and_then(|node_map| {
582            node_map.get(muxbox_id).and_then(|&index| {
583                self.graphs
584                    .get(layout_id)
585                    .and_then(|graph| graph.node_weight(index))
586            })
587        })
588    }
589
590    pub fn get_muxbox_by_id(&self, muxbox_id: &str) -> Option<&MuxBox> {
591        for (layout_id, node_map) in &self.node_maps {
592            if let Some(&index) = node_map.get(muxbox_id) {
593                return self
594                    .graphs
595                    .get(layout_id)
596                    .and_then(|graph| graph.node_weight(index));
597            }
598        }
599        None
600    }
601
602    pub fn get_children(&self, layout_id: &str, muxbox_id: &str) -> Vec<&MuxBox> {
603        if let Some(node_map) = self.node_maps.get(layout_id) {
604            if let Some(&index) = node_map.get(muxbox_id) {
605                return self.graphs[layout_id]
606                    .edges_directed(index, petgraph::Direction::Outgoing)
607                    .map(|edge| self.graphs[layout_id].node_weight(edge.target()).unwrap())
608                    .collect();
609            }
610        }
611        Vec::new()
612    }
613
614    pub fn get_layout_children(&self, layout_id: &str) -> Vec<&MuxBox> {
615        if let Some(node_map) = self.node_maps.get(layout_id) {
616            let root_node = node_map.get(layout_id).unwrap();
617            return self.graphs[layout_id]
618                .edges_directed(*root_node, petgraph::Direction::Outgoing)
619                .map(|edge| self.graphs[layout_id].node_weight(edge.target()).unwrap())
620                .collect();
621        }
622        Vec::new()
623    }
624
625    pub fn get_parent(&self, layout_id: &str, muxbox_id: &str) -> Option<&MuxBox> {
626        if let Some(node_map) = self.node_maps.get(layout_id) {
627            if let Some(&index) = node_map.get(muxbox_id) {
628                return self.graphs[layout_id]
629                    .edges_directed(index, petgraph::Direction::Incoming)
630                    .next()
631                    .and_then(|edge| self.graphs[layout_id].node_weight(edge.source()));
632            }
633        }
634        None
635    }
636}
637
638#[derive(Debug, Serialize)]
639pub struct AppContext {
640    pub app: App,
641    pub config: Config,
642    #[serde(skip)]
643    pub plugin_registry: std::sync::Arc<std::sync::Mutex<crate::plugin::PluginRegistry>>,
644    #[serde(skip)]
645    pub pty_manager: Option<std::sync::Arc<crate::pty_manager::PtyManager>>,
646    pub yaml_file_path: Option<String>,
647    #[serde(skip)]
648    pub live_yaml_sync: Option<Arc<LiveYamlSync>>,
649}
650
651impl Updatable for AppContext {
652    fn generate_diff(&self, other: &Self) -> Vec<FieldUpdate> {
653        let mut updates = Vec::new();
654
655        // Compare app
656        updates.extend(self.app.generate_diff(&other.app));
657
658        // Compare config
659        if self.config != other.config {
660            updates.push(FieldUpdate {
661                entity_type: crate::EntityType::AppContext,
662                entity_id: None,
663                field_name: "config".to_string(),
664                new_value: serde_json::to_value(&other.config).unwrap(),
665            });
666        }
667
668        updates
669    }
670
671    fn apply_updates(&mut self, updates: Vec<FieldUpdate>) {
672        let updates_for_layouts = updates.clone();
673
674        for update in updates {
675            if update.entity_id.is_some() {
676                // Skip updates that are not for the top-level entity
677                continue;
678            }
679            match update.field_name.as_str() {
680                "config" => {
681                    if let Ok(new_config) =
682                        serde_json::from_value::<Config>(update.new_value.clone())
683                    {
684                        self.config = new_config;
685                    }
686                }
687                _ => log::warn!("Unknown field name for AppContext: {}", update.field_name),
688            }
689        }
690
691        self.app.apply_updates(updates_for_layouts);
692    }
693}
694
695impl PartialEq for AppContext {
696    fn eq(&self, other: &Self) -> bool {
697        self.app == other.app && self.config == other.config
698    }
699}
700
701impl AppContext {
702    pub fn new(app: App, config: Config) -> Self {
703        // App is already validated in load_app_from_yaml
704        AppContext {
705            app,
706            config,
707            plugin_registry: std::sync::Arc::new(std::sync::Mutex::new(
708                crate::plugin::PluginRegistry::new(),
709            )),
710            pty_manager: None,
711            yaml_file_path: None,
712            live_yaml_sync: None,
713        }
714    }
715
716    pub fn new_with_pty(
717        app: App,
718        config: Config,
719        pty_manager: std::sync::Arc<crate::pty_manager::PtyManager>,
720    ) -> Self {
721        AppContext {
722            app,
723            config,
724            plugin_registry: std::sync::Arc::new(std::sync::Mutex::new(
725                crate::plugin::PluginRegistry::new(),
726            )),
727            pty_manager: Some(pty_manager),
728            yaml_file_path: None,
729            live_yaml_sync: None,
730        }
731    }
732
733    // F0190: Constructor with YAML file path for live updates
734    pub fn new_with_yaml_path(app: App, config: Config, yaml_path: String) -> Self {
735        Self::new_with_yaml_path_and_lock(app, config, yaml_path, false)
736    }
737    
738    pub fn new_with_yaml_path_and_lock(app: App, config: Config, yaml_path: String, locked: bool) -> Self {
739        let live_yaml_sync = if !locked {
740            match LiveYamlSync::new(yaml_path.clone(), true) {
741                Ok(sync) => {
742                    log::info!("Live YAML sync initialized");
743                    Some(Arc::new(sync))
744                }
745                Err(e) => {
746                    log::error!("Failed to initialize live YAML sync: {}", e);
747                    None
748                }
749            }
750        } else {
751            None
752        };
753        
754        AppContext {
755            app,
756            config,
757            plugin_registry: std::sync::Arc::new(std::sync::Mutex::new(
758                crate::plugin::PluginRegistry::new(),
759            )),
760            pty_manager: None,
761            yaml_file_path: Some(yaml_path),
762            live_yaml_sync,
763        }
764    }
765
766    pub fn new_with_pty_and_yaml(
767        app: App,
768        config: Config,
769        pty_manager: std::sync::Arc<crate::pty_manager::PtyManager>,
770        yaml_path: String,
771    ) -> Self {
772        Self::new_with_pty_and_yaml_and_lock(app, config, pty_manager, yaml_path, false)
773    }
774    
775    pub fn new_with_pty_and_yaml_and_lock(
776        app: App,
777        config: Config,
778        pty_manager: std::sync::Arc<crate::pty_manager::PtyManager>,
779        yaml_path: String,
780        locked: bool,
781    ) -> Self {
782        let live_yaml_sync = if !locked {
783            match LiveYamlSync::new(yaml_path.clone(), true) {
784                Ok(sync) => {
785                    log::info!("Live YAML sync initialized with PTY");
786                    Some(Arc::new(sync))
787                }
788                Err(e) => {
789                    log::error!("Failed to initialize live YAML sync: {}", e);
790                    None
791                }
792            }
793        } else {
794            None
795        };
796        
797        AppContext {
798            app,
799            config,
800            plugin_registry: std::sync::Arc::new(std::sync::Mutex::new(
801                crate::plugin::PluginRegistry::new(),
802            )),
803            pty_manager: Some(pty_manager),
804            yaml_file_path: Some(yaml_path),
805            live_yaml_sync,
806        }
807    }
808}
809
810impl Clone for AppContext {
811    fn clone(&self) -> Self {
812        AppContext {
813            app: self.app.clone(),
814            config: self.config.clone(),
815            plugin_registry: self.plugin_registry.clone(),
816            pty_manager: self.pty_manager.clone(),
817            yaml_file_path: self.yaml_file_path.clone(),
818            live_yaml_sync: self.live_yaml_sync.clone(),
819        }
820    }
821}
822
823impl Hash for AppContext {
824    fn hash<H: Hasher>(&self, state: &mut H) {
825        self.app.hash(state);
826        self.config.hash(state);
827        // Note: plugin_registry contains Mutex which doesn't implement Hash
828    }
829}
830
831impl Hash for App {
832    fn hash<H: Hasher>(&self, state: &mut H) {
833        for layout in &self.layouts {
834            layout.hash(state);
835        }
836    }
837}
838
839impl Clone for AppGraph {
840    fn clone(&self) -> Self {
841        let mut new_graphs = HashMap::new();
842        let mut new_node_maps = HashMap::new();
843
844        for (key, graph) in &self.graphs {
845            let new_graph = graph.clone();
846            // Using unwrap here assumes there must always be a corresponding node_map for each graph.
847            // This will panic if that invariant is broken, which is considered a critical and unexpected error.
848            let new_node_map = self.node_maps.get(key).unwrap().clone();
849            new_graphs.insert(key.clone(), new_graph);
850            new_node_maps.insert(key.clone(), new_node_map);
851        }
852
853        AppGraph {
854            graphs: new_graphs,
855            node_maps: new_node_maps,
856        }
857    }
858}
859
860pub fn load_app_from_yaml(file_path: &str) -> Result<App, Box<dyn std::error::Error>> {
861    load_app_from_yaml_with_lock(file_path, false)
862}
863
864pub fn load_app_from_yaml_with_lock(file_path: &str, _locked: bool) -> Result<App, Box<dyn std::error::Error>> {
865    let mut file = File::open(file_path)?;
866    let mut contents = String::new();
867    file.read_to_string(&mut contents)?;
868
869    // First, perform JSON schema validation if schema files exist
870    let schema_dir = "schemas";
871    if std::path::Path::new(schema_dir).exists() {
872        let mut validator = SchemaValidator::new();
873        if let Err(schema_errors) = validator.validate_with_json_schema(&contents, schema_dir) {
874            let error_messages: Vec<String> = schema_errors
875                .into_iter()
876                .map(|e| format!("{}", e))
877                .collect();
878            let combined_message = error_messages.join("\n");
879            return Err(format!("JSON Schema validation failed:\n{}", combined_message).into());
880        }
881    }
882
883    // Parse YAML first to extract variable definitions
884    let root_result: Result<TemplateRoot, _> = serde_yaml::from_str(&contents);
885
886    let mut app = match root_result {
887        Ok(root) => root.app,
888        Err(serde_error) => {
889            // Enhanced error handling with Rust-style line/column display
890            if let Some(location) = serde_error.location() {
891                let line_num = location.line();
892                let col_num = location.column();
893                let error_display = create_rust_style_error_display(
894                    &contents,
895                    file_path,
896                    line_num,
897                    col_num,
898                    &format!("{}", serde_error),
899                );
900                return Err(error_display.into());
901            }
902
903            // Fallback: try to deserialize directly into App
904            match serde_yaml::from_str::<App>(&contents) {
905                Ok(app) => app,
906                Err(app_error) => {
907                    if let Some(location) = app_error.location() {
908                        let line_num = location.line();
909                        let col_num = location.column();
910                        let error_display = create_rust_style_error_display(
911                            &contents,
912                            file_path,
913                            line_num,
914                            col_num,
915                            &format!("{}", app_error),
916                        );
917                        return Err(error_display.into());
918                    }
919                    return Err(format!("YAML parsing error: {}", app_error).into());
920                }
921            }
922        }
923    };
924
925    // Apply variable substitution AFTER parsing with hierarchical context
926    apply_variable_substitution(&mut app)?;
927
928    // Validate the app configuration using SchemaValidator
929    let mut validator = SchemaValidator::new();
930    match validator.validate_app(&app) {
931        Ok(_) => {
932            // Apply the old validation logic for setting up parent relationships and defaults
933            apply_post_validation_setup(&mut app)?;
934            Ok(app)
935        }
936        Err(validation_errors) => {
937            let error_messages: Vec<String> = validation_errors
938                .into_iter()
939                .map(|e| format!("{}", e))
940                .collect();
941            let combined_message = error_messages.join("; ");
942            Err(format!("Configuration validation errors: {}", combined_message).into())
943        }
944    }
945}
946
947fn create_rust_style_error_display(
948    contents: &str,
949    file_path: &str,
950    line_num: usize,
951    col_num: usize,
952    error_msg: &str,
953) -> String {
954    let lines: Vec<&str> = contents.lines().collect();
955
956    // Ensure we have valid line numbers (serde_yaml uses 1-based indexing)
957    if line_num == 0 || line_num > lines.len() {
958        return format!("YAML parsing error: {}", error_msg);
959    }
960
961    let error_line = lines[line_num - 1];
962    let line_num_width = format!("{}", line_num).len().max(3);
963
964    // Create the error display similar to Rust compiler
965    let mut result = String::new();
966    result.push_str(&format!("error: {}\n", error_msg));
967    result.push_str(&format!(" --> {}:{}:{}\n", file_path, line_num, col_num));
968    result.push_str(&format!("  |\n"));
969    result.push_str(&format!(
970        "{:width$} | {}\n",
971        line_num,
972        error_line,
973        width = line_num_width
974    ));
975
976    // Add column indicator with ^^^ under the problematic area
977    if col_num > 0 && col_num <= error_line.len() + 1 {
978        let spaces_before_pipe = " ".repeat(line_num_width);
979        let spaces_before_caret = " ".repeat(col_num.saturating_sub(1));
980        result.push_str(&format!(
981            "{} | {}{}\n",
982            spaces_before_pipe, spaces_before_caret, "^"
983        ));
984    }
985
986    result
987}
988
989/// Apply variable substitution to all fields in the parsed App structure
990fn apply_variable_substitution(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
991    // Create variable context with app-level variables
992    let context = VariableContext::new(app.variables.as_ref(), None);
993
994    // Apply substitution to all layouts
995    for layout in &mut app.layouts {
996        apply_layout_variable_substitution(layout, &context)?;
997    }
998
999    Ok(())
1000}
1001
1002/// Apply variable substitution to a layout and all its muxboxes
1003fn apply_layout_variable_substitution(
1004    layout: &mut Layout,
1005    context: &VariableContext,
1006) -> Result<(), Box<dyn std::error::Error>> {
1007    // Use the same context for layout (layout variables not yet implemented)
1008    let layout_context = context;
1009
1010    // Apply to layout title
1011    if let Some(ref mut title) = layout.title {
1012        *title = layout_context
1013            .substitute_in_string(title, &[])
1014            .map_err(|e| format!("Error in layout '{}' title: {}", layout.id, e))?;
1015    }
1016
1017    // Apply to all child muxboxes
1018    if let Some(ref mut children) = layout.children {
1019        for child in children {
1020            apply_muxbox_variable_substitution(child, &layout_context, &[])?;
1021        }
1022    }
1023
1024    Ok(())
1025}
1026
1027/// Apply variable substitution to a muxbox and its children with hierarchy context
1028fn apply_muxbox_variable_substitution(
1029    muxbox: &mut MuxBox,
1030    context: &VariableContext,
1031    parent_hierarchy: &[&MuxBox],
1032) -> Result<(), Box<dyn std::error::Error>> {
1033    // Create a local variable context including this muxbox's variables
1034    let local_context = if let Some(ref muxbox_vars) = muxbox.variables {
1035        let mut combined_app_vars = context.app_vars.clone();
1036        // Add muxbox variables to context for this muxbox and children
1037        combined_app_vars.extend(muxbox_vars.clone());
1038        VariableContext::new(Some(&combined_app_vars), Some(&context.layout_vars))
1039    } else {
1040        context.clone()
1041    };
1042
1043    // Build complete muxbox hierarchy for variable resolution
1044    let full_hierarchy = parent_hierarchy.to_vec();
1045    // Note: We can't add 'muxbox' to hierarchy due to borrowing issues
1046    // Instead, we've merged muxbox variables into the context above
1047
1048    // Apply substitution to muxbox fields with error context
1049    if let Some(ref mut title) = muxbox.title {
1050        *title = local_context
1051            .substitute_in_string(title, &full_hierarchy)
1052            .map_err(|e| format!("Error in muxbox '{}' title: {}", muxbox.id, e))?;
1053    }
1054
1055    if let Some(ref mut content) = muxbox.content {
1056        *content = local_context
1057            .substitute_in_string(content, &full_hierarchy)
1058            .map_err(|e| format!("Error in muxbox '{}' content: {}", muxbox.id, e))?;
1059    }
1060
1061    if let Some(ref mut script) = muxbox.script {
1062        for (i, script_line) in script.iter_mut().enumerate() {
1063            *script_line = local_context
1064                .substitute_in_string(script_line, &full_hierarchy)
1065                .map_err(|e| {
1066                    format!(
1067                        "Error in muxbox '{}' script line {}: {}",
1068                        muxbox.id,
1069                        i + 1,
1070                        e
1071                    )
1072                })?;
1073        }
1074    }
1075
1076    if let Some(ref mut redirect) = muxbox.redirect_output {
1077        *redirect = local_context
1078            .substitute_in_string(redirect, &full_hierarchy)
1079            .map_err(|e| format!("Error in muxbox '{}' redirect_output: {}", muxbox.id, e))?;
1080    }
1081
1082    // Apply to choices if present
1083    if let Some(ref mut choices) = muxbox.choices {
1084        for choice in choices {
1085            if let Some(ref mut choice_content) = choice.content {
1086                *choice_content = local_context
1087                    .substitute_in_string(choice_content, &full_hierarchy)
1088                    .map_err(|e| {
1089                        format!(
1090                            "Error in muxbox '{}' choice '{}' content: {}",
1091                            muxbox.id, choice.id, e
1092                        )
1093                    })?;
1094            }
1095
1096            if let Some(ref mut choice_script) = choice.script {
1097                for (i, script_line) in choice_script.iter_mut().enumerate() {
1098                    *script_line = local_context
1099                        .substitute_in_string(script_line, &full_hierarchy)
1100                        .map_err(|e| {
1101                            format!(
1102                                "Error in muxbox '{}' choice '{}' script line {}: {}",
1103                                muxbox.id,
1104                                choice.id,
1105                                i + 1,
1106                                e
1107                            )
1108                        })?;
1109                }
1110            }
1111        }
1112    }
1113
1114    // Recursively apply to child muxboxes
1115    if let Some(ref mut children) = muxbox.children {
1116        for child in children {
1117            // For children, we can't include 'muxbox' in hierarchy due to borrowing
1118            // but the local_context already includes muxbox variables
1119            apply_muxbox_variable_substitution(child, &local_context, &full_hierarchy)?;
1120        }
1121    }
1122
1123    Ok(())
1124}
1125
1126/// Legacy function kept for backward compatibility (now unused in main flow)
1127pub fn substitute_variables(content: &str) -> Result<String, Box<dyn std::error::Error>> {
1128    // This function is deprecated in favor of the new hierarchical system
1129    // but kept for any external dependencies
1130    let context = VariableContext::new(None, None);
1131    context.substitute_in_string(content, &[])
1132}
1133
1134fn apply_post_validation_setup(app: &mut App) -> Result<(), String> {
1135    // This function applies the setup logic that was previously in validate_app
1136    // after the SchemaValidator has already validated the structure
1137
1138    fn set_parent_ids(muxbox: &mut MuxBox, parent_layout_id: &str, parent_id: Option<String>) {
1139        muxbox.parent_layout_id = Some(parent_layout_id.to_string());
1140        muxbox.parent_id = parent_id;
1141
1142        if let Some(ref mut children) = muxbox.children {
1143            for child in children {
1144                set_parent_ids(child, parent_layout_id, Some(muxbox.id.clone()));
1145            }
1146        }
1147    }
1148
1149    let mut root_layout_id: Option<String> = None;
1150
1151    for layout in &mut app.layouts {
1152        let mut layout_clone = layout.clone();
1153        let muxboxes_in_tab_order = layout_clone.get_muxboxes_in_tab_order();
1154
1155        // Identify root layout
1156        if layout.root.unwrap_or(false) {
1157            root_layout_id = Some(layout.id.clone());
1158        }
1159
1160        if layout.children.is_none() {
1161            continue;
1162        }
1163
1164        // Set up parent relationships and defaults
1165        for muxbox in layout.children.as_mut().unwrap() {
1166            set_parent_ids(muxbox, &layout.id, None);
1167            if !muxboxes_in_tab_order.is_empty() && muxbox.id == muxboxes_in_tab_order[0].id {
1168                muxbox.selected = Some(true);
1169            }
1170            if let Some(choices) = &mut muxbox.choices {
1171                if !choices.is_empty() {
1172                    choices[0].selected = true;
1173                }
1174            }
1175        }
1176    }
1177
1178    // Set default root layout if none specified
1179    if root_layout_id.is_none() {
1180        if let Some(first_layout) = app.layouts.first() {
1181            root_layout_id = Some(first_layout.id.clone());
1182        }
1183    }
1184
1185    // Set the root layout as active
1186    if let Some(root_layout_id) = root_layout_id {
1187        if let Some(root_layout) = app.layouts.iter_mut().find(|l| l.id == root_layout_id) {
1188            root_layout.active = Some(true);
1189            root_layout.root = Some(true);
1190
1191            // Set all other layouts as inactive
1192            for layout in &mut app.layouts {
1193                if layout.id != root_layout_id {
1194                    layout.active = Some(false);
1195                    layout.root = Some(false);
1196                }
1197            }
1198        }
1199    }
1200
1201    Ok(())
1202}
1203
1204// The old check_unique_ids and check_muxbox_ids functions are no longer needed
1205// because SchemaValidator handles ID uniqueness validation
1206
1207#[cfg(test)]
1208mod tests {
1209    use super::*;
1210    use crate::model::common::InputBounds;
1211    use crate::model::layout::Layout;
1212    use crate::model::muxbox::MuxBox;
1213    use std::collections::HashMap;
1214
1215    // === Helper Functions ===
1216
1217    /// Creates a basic test muxbox with the given id.
1218    /// This helper demonstrates how to create a MuxBox for App testing.
1219    fn create_test_muxbox(id: &str) -> MuxBox {
1220        MuxBox {
1221            id: id.to_string(),
1222            title: Some(format!("Test MuxBox {}", id)),
1223            position: InputBounds {
1224                x1: "0%".to_string(),
1225                y1: "0%".to_string(),
1226                x2: "100%".to_string(),
1227                y2: "100%".to_string(),
1228            },
1229            tab_order: Some("1".to_string()),
1230            selected: Some(false),
1231            ..Default::default()
1232        }
1233    }
1234
1235    /// Creates a test Layout with the given id and optional children.
1236    /// This helper demonstrates how to create a Layout for App testing.
1237    fn create_test_layout(id: &str, children: Option<Vec<MuxBox>>) -> Layout {
1238        Layout {
1239            id: id.to_string(),
1240            title: Some(format!("Test Layout {}", id)),
1241            children,
1242            root: Some(false),
1243            active: Some(false),
1244            ..Default::default()
1245        }
1246    }
1247
1248    /// Creates a test App with basic layouts and muxboxes.
1249    /// This helper demonstrates how to create an App for testing.
1250    fn create_test_app() -> App {
1251        let muxbox1 = create_test_muxbox("muxbox1");
1252        let muxbox2 = create_test_muxbox("muxbox2");
1253        let layout1 = create_test_layout("layout1", Some(vec![muxbox1, muxbox2]));
1254
1255        let mut app = App::new();
1256        app.layouts.push(layout1);
1257        app
1258    }
1259
1260    /// Creates a test AppContext with a basic app configuration.
1261    /// This helper demonstrates how to create an AppContext for testing.
1262    fn create_test_app_context() -> AppContext {
1263        let app = create_test_app();
1264        AppContext::new(app, Config::default())
1265    }
1266
1267    fn load_test_app_context() -> AppContext {
1268        let current_dir = std::env::current_dir().expect("Failed to get current directory");
1269        let dashboard_path = current_dir.join("layouts/tests.yaml");
1270        let app = load_app_from_yaml(dashboard_path.to_str().unwrap()).expect("Failed to load app");
1271        AppContext::new(app, Config::default())
1272    }
1273
1274    fn setup_app_context() -> AppContext {
1275        load_test_app_context()
1276    }
1277
1278    // === App Default Tests ===
1279
1280    /// Tests that App::new() creates an app with expected default values.
1281    /// This test demonstrates the default App construction behavior.
1282    #[test]
1283    fn test_app_new() {
1284        let app = App::new();
1285        assert_eq!(app.layouts.len(), 0);
1286        assert_eq!(app.libs, None);
1287        assert_eq!(app.on_keypress, None);
1288        assert_eq!(app.app_graph, None);
1289        assert_eq!(app.adjusted_bounds, None);
1290    }
1291
1292    /// Tests that App::default() creates an app with expected default values.
1293    /// This test demonstrates the default App construction behavior.
1294    #[test]
1295    fn test_app_default() {
1296        let app = App::default();
1297        assert_eq!(app.layouts.len(), 0);
1298        assert_eq!(app.libs, None);
1299        assert_eq!(app.on_keypress, None);
1300        assert_eq!(app.app_graph, None);
1301        assert_eq!(app.adjusted_bounds, None);
1302    }
1303
1304    // === App Layout Management Tests ===
1305
1306    /// Tests that App::get_layout_by_id() finds layouts correctly.
1307    /// This test demonstrates the layout retrieval feature.
1308    #[test]
1309    fn test_app_get_layout_by_id() {
1310        let app = create_test_app();
1311
1312        let found_layout = app.get_layout_by_id("layout1");
1313        assert!(found_layout.is_some());
1314        assert_eq!(found_layout.unwrap().id, "layout1");
1315
1316        let not_found = app.get_layout_by_id("nonexistent");
1317        assert!(not_found.is_none());
1318    }
1319
1320    /// Tests that App::get_layout_by_id_mut() finds and allows modification.
1321    /// This test demonstrates the mutable layout retrieval feature.
1322    #[test]
1323    fn test_app_get_layout_by_id_mut() {
1324        let mut app = create_test_app();
1325
1326        let found_layout = app.get_layout_by_id_mut("layout1");
1327        assert!(found_layout.is_some());
1328
1329        // Modify the layout
1330        found_layout.unwrap().title = Some("Modified Layout".to_string());
1331
1332        // Verify the modification
1333        let verified_layout = app.get_layout_by_id("layout1");
1334        assert_eq!(
1335            verified_layout.unwrap().title,
1336            Some("Modified Layout".to_string())
1337        );
1338    }
1339
1340    /// Tests that App::get_layout_by_id_mut() handles empty app.
1341    /// This test demonstrates edge case handling in mutable layout retrieval.
1342    #[test]
1343    fn test_app_get_layout_by_id_mut_empty() {
1344        let mut app = App::new();
1345
1346        let found_layout = app.get_layout_by_id_mut("nonexistent");
1347        assert!(found_layout.is_none());
1348    }
1349
1350    // === App Root Layout Tests ===
1351
1352    /// Tests that App::get_root_layout() finds the root layout correctly.
1353    /// This test demonstrates the root layout retrieval feature.
1354    #[test]
1355    fn test_app_get_root_layout() {
1356        let mut app = create_test_app();
1357
1358        // Initially no root layout
1359        assert!(app.get_root_layout().is_none());
1360
1361        // Set a layout as root
1362        app.layouts[0].root = Some(true);
1363
1364        let root_layout = app.get_root_layout();
1365        assert!(root_layout.is_some());
1366        assert_eq!(root_layout.unwrap().id, "layout1");
1367    }
1368
1369    /// Tests that App::get_root_layout() panics with multiple root layouts.
1370    /// This test demonstrates the root layout validation feature.
1371    #[test]
1372    #[should_panic(expected = "Multiple root layouts found, which is not allowed.")]
1373    fn test_app_get_root_layout_multiple_panics() {
1374        let mut app = create_test_app();
1375
1376        // Add another layout and set both as root
1377        let layout2 = create_test_layout("layout2", None);
1378        app.layouts.push(layout2);
1379        app.layouts[0].root = Some(true);
1380        app.layouts[1].root = Some(true);
1381
1382        app.get_root_layout();
1383    }
1384
1385    /// Tests that App::get_root_layout_mut() finds and allows modification.
1386    /// This test demonstrates the mutable root layout retrieval feature.
1387    #[test]
1388    fn test_app_get_root_layout_mut() {
1389        let mut app = create_test_app();
1390        app.layouts[0].root = Some(true);
1391
1392        let root_layout = app.get_root_layout_mut();
1393        assert!(root_layout.is_some());
1394
1395        // Modify the root layout
1396        root_layout.unwrap().title = Some("Modified Root".to_string());
1397
1398        // Verify the modification
1399        let verified_layout = app.get_root_layout();
1400        assert_eq!(
1401            verified_layout.unwrap().title,
1402            Some("Modified Root".to_string())
1403        );
1404    }
1405
1406    // === App Active Layout Tests ===
1407
1408    /// Tests that App::get_active_layout() finds the active layout correctly.
1409    /// This test demonstrates the active layout retrieval feature.
1410    #[test]
1411    fn test_app_get_active_layout() {
1412        let mut app = create_test_app();
1413
1414        // Initially no active layout
1415        assert!(app.get_active_layout().is_none());
1416
1417        // Set a layout as active
1418        app.layouts[0].active = Some(true);
1419
1420        let active_layout = app.get_active_layout();
1421        assert!(active_layout.is_some());
1422        assert_eq!(active_layout.unwrap().id, "layout1");
1423    }
1424
1425    /// Tests that App::get_active_layout() panics with multiple active layouts.
1426    /// This test demonstrates the active layout validation feature.
1427    #[test]
1428    #[should_panic(expected = "Multiple active layouts found, which is not allowed.")]
1429    fn test_app_get_active_layout_multiple_panics() {
1430        let mut app = create_test_app();
1431
1432        // Add another layout and set both as active
1433        let layout2 = create_test_layout("layout2", None);
1434        app.layouts.push(layout2);
1435        app.layouts[0].active = Some(true);
1436        app.layouts[1].active = Some(true);
1437
1438        app.get_active_layout();
1439    }
1440
1441    /// Tests that App::get_active_layout_mut() finds and allows modification.
1442    /// This test demonstrates the mutable active layout retrieval feature.
1443    #[test]
1444    fn test_app_get_active_layout_mut() {
1445        let mut app = create_test_app();
1446        app.layouts[0].active = Some(true);
1447
1448        let active_layout = app.get_active_layout_mut();
1449        assert!(active_layout.is_some());
1450
1451        // Modify the active layout
1452        active_layout.unwrap().title = Some("Modified Active".to_string());
1453
1454        // Verify the modification
1455        let verified_layout = app.get_active_layout();
1456        assert_eq!(
1457            verified_layout.unwrap().title,
1458            Some("Modified Active".to_string())
1459        );
1460    }
1461
1462    /// Tests that App::set_active_layout() sets the correct layout as active.
1463    /// This test demonstrates the active layout setting feature.
1464    #[test]
1465    fn test_app_set_active_layout() {
1466        let mut app = create_test_app();
1467
1468        // Add another layout
1469        let layout2 = create_test_layout("layout2", None);
1470        app.layouts.push(layout2);
1471
1472        // Set layout2 as active
1473        app.set_active_layout("layout2");
1474
1475        let active_layout = app.get_active_layout();
1476        assert!(active_layout.is_some());
1477        assert_eq!(active_layout.unwrap().id, "layout2");
1478
1479        // Verify layout1 is not active
1480        let layout1 = app.get_layout_by_id("layout1").unwrap();
1481        assert_eq!(layout1.active, Some(false));
1482    }
1483
1484    /// Tests that App::set_active_layout() logs error for nonexistent layout.
1485    /// This test demonstrates error handling in active layout setting.
1486    #[test]
1487    fn test_app_set_active_layout_nonexistent() {
1488        let mut app = create_test_app();
1489
1490        // This should not panic but should log an error
1491        app.set_active_layout("nonexistent");
1492
1493        // No layout should be active
1494        assert!(app.get_active_layout().is_none());
1495    }
1496
1497    // === App MuxBox Management Tests ===
1498
1499    /// Tests that App::get_muxbox_by_id() finds muxboxes across layouts.
1500    /// This test demonstrates the cross-layout muxbox retrieval feature.
1501    #[test]
1502    fn test_app_get_muxbox_by_id() {
1503        let app = create_test_app();
1504
1505        let found_muxbox = app.get_muxbox_by_id("muxbox1");
1506        assert!(found_muxbox.is_some());
1507        assert_eq!(found_muxbox.unwrap().id, "muxbox1");
1508
1509        let not_found = app.get_muxbox_by_id("nonexistent");
1510        assert!(not_found.is_none());
1511    }
1512
1513    /// Tests that App::get_muxbox_by_id_mut() finds and allows modification.
1514    /// This test demonstrates the mutable cross-layout muxbox retrieval feature.
1515    #[test]
1516    fn test_app_get_muxbox_by_id_mut() {
1517        let mut app = create_test_app();
1518
1519        let found_muxbox = app.get_muxbox_by_id_mut("muxbox1");
1520        assert!(found_muxbox.is_some());
1521
1522        // Modify the muxbox
1523        found_muxbox.unwrap().title = Some("Modified MuxBox".to_string());
1524
1525        // Verify the modification
1526        let verified_muxbox = app.get_muxbox_by_id("muxbox1");
1527        assert_eq!(
1528            verified_muxbox.unwrap().title,
1529            Some("Modified MuxBox".to_string())
1530        );
1531    }
1532
1533    /// Tests that App::get_muxbox_by_id_mut() handles empty app.
1534    /// This test demonstrates edge case handling in mutable muxbox retrieval.
1535    #[test]
1536    fn test_app_get_muxbox_by_id_mut_empty() {
1537        let mut app = App::new();
1538
1539        let found_muxbox = app.get_muxbox_by_id_mut("nonexistent");
1540        assert!(found_muxbox.is_none());
1541    }
1542
1543    // === App Validation Tests ===
1544
1545    /// Tests that App::validate() sets up parent relationships correctly.
1546    /// This test demonstrates the app validation feature.
1547    #[test]
1548    fn test_app_validate() {
1549        let mut app = create_test_app();
1550
1551        // Before validation, parent relationships should not be set
1552        let muxbox = app.get_muxbox_by_id("muxbox1").unwrap();
1553        assert_eq!(muxbox.parent_layout_id, None);
1554        assert_eq!(muxbox.parent_id, None);
1555
1556        app.validate();
1557
1558        // After validation, parent relationships should be set
1559        let muxbox = app.get_muxbox_by_id("muxbox1").unwrap();
1560        assert_eq!(muxbox.parent_layout_id, Some("layout1".to_string()));
1561        assert_eq!(muxbox.parent_id, None); // Top-level muxbox has no parent muxbox
1562    }
1563
1564    /// Tests that App::validate() sets root layout as active.
1565    /// This test demonstrates the root layout activation feature.
1566    #[test]
1567    fn test_app_validate_root_layout_activation() {
1568        let mut app = create_test_app();
1569        app.layouts[0].root = Some(true);
1570
1571        app.validate();
1572
1573        let layout = app.get_layout_by_id("layout1").unwrap();
1574        assert_eq!(layout.active, Some(true));
1575    }
1576
1577    /// Tests that App::validate() defaults to first layout when no root.
1578    /// This test demonstrates the default root layout behavior.
1579    #[test]
1580    fn test_app_validate_default_root() {
1581        let mut app = create_test_app();
1582
1583        // Add another layout
1584        let layout2 = create_test_layout("layout2", None);
1585        app.layouts.push(layout2);
1586
1587        app.validate();
1588
1589        // First layout should be set as root and active
1590        let layout1 = app.get_layout_by_id("layout1").unwrap();
1591        assert_eq!(layout1.root, Some(true));
1592        assert_eq!(layout1.active, Some(true));
1593
1594        // Second layout should not be root or active
1595        let layout2 = app.get_layout_by_id("layout2").unwrap();
1596        assert_eq!(layout2.root, Some(false));
1597        assert_eq!(layout2.active, Some(false));
1598    }
1599
1600    /// Tests that App::validate() panics with no layouts.
1601    /// This test demonstrates the empty app validation behavior.
1602    #[test]
1603    #[should_panic(expected = "Required field 'layouts' is missing")]
1604    fn test_app_validate_empty_panics() {
1605        let mut app = App::new();
1606        app.validate();
1607    }
1608
1609    /// Tests that App::validate() panics with duplicate IDs.
1610    /// This test demonstrates the duplicate ID validation feature.
1611    #[test]
1612    #[should_panic(expected = "Duplicate ID 'muxbox1' found in muxboxes")]
1613    fn test_app_validate_duplicate_ids_panics() {
1614        let mut app = App::new();
1615
1616        // Create two muxboxes with the same ID
1617        let muxbox1a = create_test_muxbox("muxbox1");
1618        let muxbox1b = create_test_muxbox("muxbox1"); // Duplicate ID
1619
1620        let layout = create_test_layout("layout1", Some(vec![muxbox1a, muxbox1b]));
1621        app.layouts.push(layout);
1622
1623        app.validate();
1624    }
1625
1626    /// Tests that App::validate() panics with multiple root layouts.
1627    /// This test demonstrates the multiple root layout validation feature.
1628    #[test]
1629    #[should_panic(
1630        expected = "Schema structure error: Multiple root layouts detected. Only one layout can be marked as 'root: true'."
1631    )]
1632    fn test_app_validate_multiple_root_panics() {
1633        let mut app = create_test_app();
1634
1635        // Add another layout and set both as root
1636        let mut layout2 = create_test_layout("layout2", None);
1637        layout2.root = Some(true);
1638        app.layouts.push(layout2);
1639        app.layouts[0].root = Some(true);
1640
1641        app.validate();
1642    }
1643
1644    // === App Bounds Calculation Tests ===
1645
1646    /// Tests that App::calculate_bounds() calculates bounds for all layouts.
1647    /// This test demonstrates the bounds calculation feature.
1648    #[test]
1649    fn test_app_calculate_bounds() {
1650        let mut app = create_test_app();
1651
1652        let bounds = app.calculate_bounds();
1653        assert!(bounds.contains_key("layout1"));
1654
1655        let layout_bounds = bounds.get("layout1").unwrap();
1656        assert!(layout_bounds.contains_key("muxbox1"));
1657        assert!(layout_bounds.contains_key("muxbox2"));
1658    }
1659
1660    /// Tests that App::get_adjusted_bounds() caches bounds correctly.
1661    /// This test demonstrates the bounds caching feature.
1662    #[test]
1663    fn test_app_get_adjusted_bounds() {
1664        let mut app = create_test_app();
1665
1666        // First call should calculate bounds
1667        let bounds1 = app.get_adjusted_bounds(None).clone();
1668        assert!(bounds1.contains_key("layout1"));
1669
1670        // Second call should return cached bounds
1671        let bounds2 = app.get_adjusted_bounds(None).clone();
1672        assert_eq!(bounds1, bounds2);
1673
1674        // Force recalculation
1675        let bounds3 = app.get_adjusted_bounds(Some(true));
1676        assert!(bounds3.contains_key("layout1"));
1677    }
1678
1679    /// Tests that App::get_adjusted_bounds_and_app_graph() returns both.
1680    /// This test demonstrates the combined bounds and graph retrieval feature.
1681    #[test]
1682    fn test_app_get_adjusted_bounds_and_app_graph() {
1683        let mut app = create_test_app();
1684
1685        let (bounds, app_graph) = app.get_adjusted_bounds_and_app_graph(None);
1686
1687        assert!(bounds.contains_key("layout1"));
1688        assert!(app_graph.graphs.contains_key("layout1"));
1689    }
1690
1691    // === App Graph Generation Tests ===
1692
1693    /// Tests that App::generate_graph() creates graph for all layouts.
1694    /// This test demonstrates the graph generation feature.
1695    #[test]
1696    fn test_app_generate_graph() {
1697        let mut app = create_test_app();
1698
1699        let app_graph = app.generate_graph();
1700        assert!(app_graph.graphs.contains_key("layout1"));
1701
1702        let graph = &app_graph.graphs["layout1"];
1703        assert_eq!(graph.node_count(), 2); // muxbox1 and muxbox2
1704    }
1705
1706    /// Tests that App::generate_graph() caches graph correctly.
1707    /// This test demonstrates the graph caching feature.
1708    #[test]
1709    fn test_app_generate_graph_caching() {
1710        let mut app = create_test_app();
1711
1712        // First call should generate graph
1713        let graph1 = app.generate_graph();
1714
1715        // Second call should return cached graph
1716        let graph2 = app.generate_graph();
1717        assert_eq!(graph1, graph2);
1718    }
1719
1720    // === App MuxBox Replacement Tests ===
1721
1722    /// Tests that App::replace_muxbox() replaces muxboxes correctly.
1723    /// This test demonstrates the muxbox replacement feature.
1724    #[test]
1725    fn test_app_replace_muxbox() {
1726        let mut app = create_test_app();
1727
1728        // Create a replacement muxbox
1729        let mut replacement_muxbox = create_test_muxbox("muxbox1");
1730        replacement_muxbox.title = Some("Replaced MuxBox".to_string());
1731
1732        app.replace_muxbox(replacement_muxbox);
1733
1734        // Verify the muxbox was replaced
1735        let replaced_muxbox = app.get_muxbox_by_id("muxbox1").unwrap();
1736        assert_eq!(replaced_muxbox.title, Some("Replaced MuxBox".to_string()));
1737    }
1738
1739    /// Tests that App::replace_muxbox() handles nonexistent muxboxes.
1740    /// This test demonstrates edge case handling in muxbox replacement.
1741    #[test]
1742    fn test_app_replace_muxbox_nonexistent() {
1743        let mut app = create_test_app();
1744
1745        // Create a replacement muxbox with nonexistent ID
1746        let replacement_muxbox = create_test_muxbox("nonexistent");
1747
1748        // This should not panic
1749        app.replace_muxbox(replacement_muxbox);
1750
1751        // Original muxboxes should be unchanged
1752        let original_muxbox = app.get_muxbox_by_id("muxbox1").unwrap();
1753        assert_eq!(
1754            original_muxbox.title,
1755            Some("Test MuxBox muxbox1".to_string())
1756        );
1757    }
1758
1759    // === App Clone Tests ===
1760
1761    /// Tests that App implements Clone correctly.
1762    /// This test demonstrates App cloning behavior.
1763    #[test]
1764    fn test_app_clone() {
1765        let app1 = create_test_app();
1766        let app2 = app1.clone();
1767
1768        assert_eq!(app1.layouts.len(), app2.layouts.len());
1769        assert_eq!(app1.layouts[0].id, app2.layouts[0].id);
1770        assert_eq!(app1.libs, app2.libs);
1771        assert_eq!(app1.on_keypress, app2.on_keypress);
1772    }
1773
1774    /// Tests that App cloning includes all nested structures.
1775    /// This test demonstrates comprehensive App cloning.
1776    #[test]
1777    fn test_app_clone_comprehensive() {
1778        let mut app1 = create_test_app();
1779        app1.libs = Some(vec!["lib1.sh".to_string(), "lib2.sh".to_string()]);
1780
1781        let mut keypress_map = HashMap::new();
1782        keypress_map.insert("ctrl+c".to_string(), vec!["exit".to_string()]);
1783        app1.on_keypress = Some(keypress_map);
1784
1785        let app2 = app1.clone();
1786
1787        assert_eq!(app1.libs, app2.libs);
1788        assert_eq!(app1.on_keypress, app2.on_keypress);
1789        assert_eq!(
1790            app1.layouts[0].children.as_ref().unwrap().len(),
1791            app2.layouts[0].children.as_ref().unwrap().len()
1792        );
1793    }
1794
1795    // === App Hash Tests ===
1796
1797    /// Tests that App implements Hash correctly.
1798    /// This test demonstrates App hashing behavior.
1799    #[test]
1800    fn test_app_hash() {
1801        let app1 = create_test_app();
1802        let app2 = create_test_app();
1803        let mut app3 = create_test_app();
1804        app3.layouts[0].id = "different".to_string();
1805
1806        use std::collections::hash_map::DefaultHasher;
1807        use std::hash::{Hash, Hasher};
1808
1809        let mut hasher1 = DefaultHasher::new();
1810        let mut hasher2 = DefaultHasher::new();
1811        let mut hasher3 = DefaultHasher::new();
1812
1813        app1.hash(&mut hasher1);
1814        app2.hash(&mut hasher2);
1815        app3.hash(&mut hasher3);
1816
1817        assert_eq!(hasher1.finish(), hasher2.finish());
1818        assert_ne!(hasher1.finish(), hasher3.finish());
1819    }
1820
1821    // === App PartialEq Tests ===
1822
1823    /// Tests that App implements PartialEq correctly.
1824    /// This test demonstrates App equality comparison.
1825    #[test]
1826    fn test_app_equality() {
1827        let app1 = create_test_app();
1828        let app2 = create_test_app();
1829        let mut app3 = create_test_app();
1830        app3.layouts[0].id = "different".to_string();
1831
1832        assert_eq!(app1, app2);
1833        assert_ne!(app1, app3);
1834    }
1835
1836    // === AppContext Tests ===
1837
1838    /// Tests that AppContext::new() creates context with validated app.
1839    /// This test demonstrates AppContext construction behavior.
1840    #[test]
1841    fn test_app_context_new() {
1842        let app = create_test_app();
1843        let config = Config::new(60);
1844        let app_context = AppContext::new(app, config);
1845
1846        assert_eq!(app_context.config.frame_delay, 60);
1847        assert_eq!(app_context.app.layouts.len(), 1);
1848    }
1849
1850    /// Tests that AppContext implements Clone correctly.
1851    /// This test demonstrates AppContext cloning behavior.
1852    #[test]
1853    fn test_app_context_clone() {
1854        let app_context1 = create_test_app_context();
1855        let app_context2 = app_context1.clone();
1856
1857        assert_eq!(app_context1.config, app_context2.config);
1858        assert_eq!(app_context1.app, app_context2.app);
1859    }
1860
1861    /// Tests that AppContext implements Hash correctly.
1862    /// This test demonstrates AppContext hashing behavior.
1863    #[test]
1864    fn test_app_context_hash() {
1865        let app_context1 = create_test_app_context();
1866        let app_context2 = create_test_app_context();
1867
1868        use std::collections::hash_map::DefaultHasher;
1869        use std::hash::{Hash, Hasher};
1870
1871        let mut hasher1 = DefaultHasher::new();
1872        let mut hasher2 = DefaultHasher::new();
1873
1874        app_context1.hash(&mut hasher1);
1875        app_context2.hash(&mut hasher2);
1876
1877        assert_eq!(hasher1.finish(), hasher2.finish());
1878    }
1879
1880    /// Tests that AppContext implements PartialEq correctly.
1881    /// This test demonstrates AppContext equality comparison.
1882    #[test]
1883    fn test_app_context_equality() {
1884        let app_context1 = create_test_app_context();
1885        let app_context2 = create_test_app_context();
1886
1887        assert_eq!(app_context1, app_context2);
1888    }
1889
1890    // === AppGraph Tests ===
1891
1892    /// Tests that AppGraph::new() creates an empty graph.
1893    /// This test demonstrates AppGraph construction behavior.
1894    #[test]
1895    fn test_app_graph_new() {
1896        let app_graph = AppGraph::new();
1897        assert_eq!(app_graph.graphs.len(), 0);
1898        assert_eq!(app_graph.node_maps.len(), 0);
1899    }
1900
1901    /// Tests that AppGraph::default() creates an empty graph.
1902    /// This test demonstrates AppGraph default behavior.
1903    #[test]
1904    fn test_app_graph_default() {
1905        let app_graph = AppGraph::default();
1906        assert_eq!(app_graph.graphs.len(), 0);
1907        assert_eq!(app_graph.node_maps.len(), 0);
1908    }
1909
1910    /// Tests that AppGraph::add_layout() adds layout to graph.
1911    /// This test demonstrates the layout addition feature.
1912    #[test]
1913    fn test_app_graph_add_layout() {
1914        let layout = create_test_layout("test", Some(vec![create_test_muxbox("muxbox1")]));
1915        let mut app_graph = AppGraph::new();
1916
1917        app_graph.add_layout(&layout);
1918
1919        assert!(app_graph.graphs.contains_key("test"));
1920        assert!(app_graph.node_maps.contains_key("test"));
1921        assert_eq!(app_graph.graphs["test"].node_count(), 1);
1922    }
1923
1924    /// Tests that AppGraph::get_layout_muxbox_by_id() finds muxboxes.
1925    /// This test demonstrates the layout-specific muxbox retrieval feature.
1926    #[test]
1927    fn test_app_graph_get_layout_muxbox_by_id() {
1928        let layout = create_test_layout("test", Some(vec![create_test_muxbox("muxbox1")]));
1929        let mut app_graph = AppGraph::new();
1930        app_graph.add_layout(&layout);
1931
1932        let muxbox = app_graph.get_layout_muxbox_by_id("test", "muxbox1");
1933        assert!(muxbox.is_some());
1934        assert_eq!(muxbox.unwrap().id, "muxbox1");
1935
1936        let not_found = app_graph.get_layout_muxbox_by_id("test", "nonexistent");
1937        assert!(not_found.is_none());
1938    }
1939
1940    /// Tests that AppGraph::get_muxbox_by_id() finds muxboxes across layouts.
1941    /// This test demonstrates the cross-layout muxbox retrieval feature.
1942    #[test]
1943    fn test_app_graph_get_muxbox_by_id() {
1944        let layout1 = create_test_layout("layout1", Some(vec![create_test_muxbox("muxbox1")]));
1945        let layout2 = create_test_layout("layout2", Some(vec![create_test_muxbox("muxbox2")]));
1946        let mut app_graph = AppGraph::new();
1947        app_graph.add_layout(&layout1);
1948        app_graph.add_layout(&layout2);
1949
1950        let muxbox1 = app_graph.get_muxbox_by_id("muxbox1");
1951        assert!(muxbox1.is_some());
1952        assert_eq!(muxbox1.unwrap().id, "muxbox1");
1953
1954        let muxbox2 = app_graph.get_muxbox_by_id("muxbox2");
1955        assert!(muxbox2.is_some());
1956        assert_eq!(muxbox2.unwrap().id, "muxbox2");
1957    }
1958
1959    /// Tests that AppGraph::get_children() returns child muxboxes.
1960    /// This test demonstrates the children retrieval feature.
1961    #[test]
1962    fn test_app_graph_get_children() {
1963        let child_muxbox = create_test_muxbox("child");
1964        let mut parent_muxbox = create_test_muxbox("parent");
1965        parent_muxbox.children = Some(vec![child_muxbox]);
1966
1967        let layout = create_test_layout("test", Some(vec![parent_muxbox]));
1968        let mut app_graph = AppGraph::new();
1969        app_graph.add_layout(&layout);
1970
1971        let children = app_graph.get_children("test", "parent");
1972        assert_eq!(children.len(), 1);
1973        assert_eq!(children[0].id, "child");
1974    }
1975
1976    /// Tests that AppGraph::get_parent() returns parent muxboxes.
1977    /// This test demonstrates the parent retrieval feature.
1978    #[test]
1979    fn test_app_graph_get_parent() {
1980        let child_muxbox = create_test_muxbox("child");
1981        let mut parent_muxbox = create_test_muxbox("parent");
1982        parent_muxbox.children = Some(vec![child_muxbox]);
1983
1984        let layout = create_test_layout("test", Some(vec![parent_muxbox]));
1985        let mut app_graph = AppGraph::new();
1986        app_graph.add_layout(&layout);
1987
1988        let parent = app_graph.get_parent("test", "child");
1989        assert!(parent.is_some());
1990        assert_eq!(parent.unwrap().id, "parent");
1991    }
1992
1993    /// Tests that AppGraph implements Hash correctly.
1994    /// This test demonstrates AppGraph hashing behavior.
1995    #[test]
1996    fn test_app_graph_hash() {
1997        let layout = create_test_layout("test", Some(vec![create_test_muxbox("muxbox1")]));
1998        let mut app_graph1 = AppGraph::new();
1999        let mut app_graph2 = AppGraph::new();
2000        app_graph1.add_layout(&layout);
2001        app_graph2.add_layout(&layout);
2002
2003        use std::collections::hash_map::DefaultHasher;
2004        use std::hash::{Hash, Hasher};
2005
2006        let mut hasher1 = DefaultHasher::new();
2007        let mut hasher2 = DefaultHasher::new();
2008
2009        app_graph1.hash(&mut hasher1);
2010        app_graph2.hash(&mut hasher2);
2011
2012        assert_eq!(hasher1.finish(), hasher2.finish());
2013    }
2014
2015    /// Tests that AppGraph implements PartialEq correctly.
2016    /// This test demonstrates AppGraph equality comparison.
2017    #[test]
2018    fn test_app_graph_equality() {
2019        let layout = create_test_layout("test", Some(vec![create_test_muxbox("muxbox1")]));
2020        let mut app_graph1 = AppGraph::new();
2021        let mut app_graph2 = AppGraph::new();
2022        app_graph1.add_layout(&layout);
2023        app_graph2.add_layout(&layout);
2024
2025        assert_eq!(app_graph1, app_graph2);
2026    }
2027
2028    // === Integration Tests (from original test suite) ===
2029
2030    #[test]
2031    fn test_layout_and_muxboxes_addition() {
2032        let mut app_context = setup_app_context();
2033        let app_graph = app_context.app.generate_graph();
2034        assert!(app_graph.graphs.contains_key("dashboard"));
2035        let graph = &app_graph.graphs["dashboard"];
2036        assert_eq!(
2037            graph.node_count(),
2038            9,
2039            "Should include all muxboxes and sub-muxboxes"
2040        );
2041    }
2042
2043    #[test]
2044    fn test_get_muxbox_by_id() {
2045        let mut app_context = setup_app_context();
2046        let app_graph = app_context.app.generate_graph();
2047        let muxboxes = [
2048            "header",
2049            "title",
2050            "time",
2051            "cpu",
2052            "memory",
2053            "log",
2054            "log_input",
2055            "log_output",
2056            "footer",
2057        ];
2058        for &muxbox_id in muxboxes.iter() {
2059            let muxbox = app_graph.get_muxbox_by_id(muxbox_id);
2060            assert!(
2061                muxbox.is_some(),
2062                "MuxBox with ID {} should exist",
2063                muxbox_id
2064            );
2065        }
2066    }
2067
2068    #[test]
2069    fn test_get_children() {
2070        let mut app_context = setup_app_context();
2071        let app_graph = app_context.app.generate_graph();
2072        let children = app_graph.get_children("dashboard", "header");
2073        assert_eq!(children.len(), 2, "Header should have exactly 2 children");
2074        assert!(
2075            children.iter().any(|&p| p.id == "title"),
2076            "Title should be a child of header"
2077        );
2078        assert!(
2079            children.iter().any(|&p| p.id == "time"),
2080            "Time should be a child of header"
2081        );
2082    }
2083
2084    #[test]
2085    fn test_get_parent() {
2086        let mut app_context = setup_app_context();
2087        let app_graph = app_context.app.generate_graph();
2088        let parent = app_graph.get_parent("dashboard", "title");
2089        assert!(parent.is_some(), "Parent should exist for 'title'");
2090        assert_eq!(
2091            parent.unwrap().id,
2092            "header",
2093            "Parent of 'title' should be 'header'"
2094        );
2095    }
2096
2097    #[test]
2098    fn test_app_graph_clone() {
2099        let mut app_context = setup_app_context();
2100        let app_graph = app_context.app.generate_graph();
2101        let cloned_graph = app_graph.clone();
2102        assert_eq!(app_graph, cloned_graph);
2103    }
2104
2105    // === Load App from YAML Tests ===
2106
2107    /// Tests that load_app_from_yaml() loads app correctly.
2108    /// This test demonstrates the YAML loading feature.
2109    #[test]
2110    fn test_load_app_from_yaml() {
2111        let current_dir = std::env::current_dir().expect("Failed to get current directory");
2112        let dashboard_path = current_dir.join("layouts/tests.yaml");
2113
2114        let result = load_app_from_yaml(dashboard_path.to_str().unwrap());
2115        assert!(result.is_ok());
2116
2117        let app = result.unwrap();
2118        assert_eq!(app.layouts.len(), 1);
2119        assert_eq!(app.layouts[0].id, "dashboard");
2120    }
2121
2122    /// Tests that load_app_from_yaml() handles invalid files.
2123    /// This test demonstrates error handling in YAML loading.
2124    #[test]
2125    fn test_load_app_from_yaml_invalid_file() {
2126        let result = load_app_from_yaml("nonexistent.yaml");
2127        assert!(result.is_err());
2128    }
2129
2130    /// Tests SchemaValidator integration with load_app_from_yaml.
2131    /// This test demonstrates the comprehensive validation system integration.
2132    #[test]
2133    fn test_load_app_from_yaml_with_schema_validation() {
2134        use std::fs;
2135        use std::io::Write;
2136
2137        // Create a temporary invalid YAML file for testing validation
2138        let temp_file = "/tmp/boxmux_test_invalid.yaml";
2139        let invalid_yaml_content = r#"
2140app:
2141  layouts:
2142    - id: 'layout1'
2143      root: true
2144      children:
2145        - id: 'muxbox1'
2146          position:
2147            x1: 0%
2148            y1: 0%
2149            x2: 50%
2150            y2: 50%
2151        - id: 'muxbox1'  # Duplicate ID - should fail validation
2152          position:
2153            x1: 50%
2154            y1: 0%
2155            x2: 100%
2156            y2: 50%
2157"#;
2158
2159        // Write the invalid content to temp file
2160        let mut file = fs::File::create(temp_file).expect("Failed to create temp file");
2161        file.write_all(invalid_yaml_content.as_bytes())
2162            .expect("Failed to write temp file");
2163
2164        // Test that SchemaValidator catches the duplicate ID
2165        let result = load_app_from_yaml(temp_file);
2166        assert!(
2167            result.is_err(),
2168            "SchemaValidator should catch duplicate IDs"
2169        );
2170
2171        let error_msg = result.unwrap_err().to_string();
2172        assert!(
2173            error_msg.contains("Duplicate ID 'muxbox1' found in muxboxes"),
2174            "Error message should mention duplicate ID: {}",
2175            error_msg
2176        );
2177
2178        // Clean up
2179        let _ = fs::remove_file(temp_file);
2180
2181        // Test valid YAML passes validation
2182        let current_dir = std::env::current_dir().expect("Failed to get current directory");
2183        let valid_dashboard_path = current_dir.join("layouts/tests.yaml");
2184
2185        let valid_result = load_app_from_yaml(valid_dashboard_path.to_str().unwrap());
2186        assert!(
2187            valid_result.is_ok(),
2188            "Valid YAML should pass SchemaValidator"
2189        );
2190
2191        let app = valid_result.unwrap();
2192        assert_eq!(app.layouts.len(), 1);
2193        assert_eq!(app.layouts[0].id, "dashboard");
2194    }
2195
2196    /// Tests SchemaValidator integration with multiple root layouts error.
2197    /// This test demonstrates schema validation for structural errors.
2198    #[test]
2199    fn test_load_app_from_yaml_multiple_root_layouts_error() {
2200        use std::fs;
2201        use std::io::Write;
2202
2203        let temp_file = "/tmp/boxmux_test_multiple_roots.yaml";
2204        let multiple_roots_yaml = r#"
2205app:
2206  layouts:
2207    - id: 'layout1'
2208      root: true
2209      children:
2210        - id: 'muxbox1'
2211          position:
2212            x1: 0%
2213            y1: 0%
2214            x2: 100%
2215            y2: 100%
2216    - id: 'layout2'
2217      root: true  # Second root - should fail validation
2218      children:
2219        - id: 'muxbox2'
2220          position:
2221            x1: 0%
2222            y1: 0%
2223            x2: 100%
2224            y2: 100%
2225"#;
2226
2227        let mut file = fs::File::create(temp_file).expect("Failed to create temp file");
2228        file.write_all(multiple_roots_yaml.as_bytes())
2229            .expect("Failed to write temp file");
2230
2231        let result = load_app_from_yaml(temp_file);
2232        assert!(
2233            result.is_err(),
2234            "SchemaValidator should catch multiple root layouts"
2235        );
2236
2237        let error_msg = result.unwrap_err().to_string();
2238        assert!(
2239            error_msg.contains("Multiple root layouts detected"),
2240            "Error message should mention multiple root layouts: {}",
2241            error_msg
2242        );
2243
2244        // Clean up
2245        let _ = fs::remove_file(temp_file);
2246    }
2247}
2248
2249// F0200: Complete YAML Persistence System - Live Synchronization
2250
2251/// Save complete application state to YAML file
2252pub fn save_complete_state_to_yaml(
2253    yaml_path: &str,
2254    app_context: &AppContext,
2255) -> Result<(), Box<dyn std::error::Error>> {
2256    use std::fs;
2257
2258    // Create a complete app structure for serialization
2259    let serializable_app = SerializableApp {
2260        app: app_context.app.clone(),
2261    };
2262
2263    // Convert to YAML with proper formatting (skip wrapper)
2264    let yaml_content = serializable_app.to_yaml_string()?;
2265    
2266    // Atomic write - write to temp file then rename
2267    let temp_path = format!("{}.tmp", yaml_path);
2268    fs::write(&temp_path, yaml_content)?;
2269    fs::rename(&temp_path, yaml_path)?;
2270    
2271    log::debug!("Saved complete application state to YAML: {}", yaml_path);
2272    Ok(())
2273}
2274
2275/// Save active layout state to YAML file
2276pub fn save_active_layout_to_yaml(
2277    yaml_path: &str,
2278    active_layout_id: &str,
2279) -> Result<(), Box<dyn std::error::Error>> {
2280    use serde_yaml::Value;
2281    use std::fs;
2282
2283    // Read current YAML content
2284    let yaml_content = fs::read_to_string(yaml_path)?;
2285    let mut yaml_value: Value = serde_yaml::from_str(&yaml_content)?;
2286
2287    // Update active layout in all layouts
2288    if let Some(root_map) = yaml_value.as_mapping_mut() {
2289        if let Some(app_map) = root_map.get_mut(&Value::String("app".to_string())) {
2290            if let Value::Mapping(app_map) = app_map {
2291                if let Some(layouts_seq) = app_map.get_mut(&Value::String("layouts".to_string())) {
2292                    if let Value::Sequence(layouts_seq) = layouts_seq {
2293                        for layout_value in layouts_seq.iter_mut() {
2294                            if let Value::Mapping(layout_map) = layout_value {
2295                                if let Some(Value::String(layout_id)) = layout_map.get(&Value::String("id".to_string())) {
2296                                    let is_active = layout_id == active_layout_id;
2297                                    layout_map.insert(
2298                                        Value::String("active".to_string()),
2299                                        Value::Bool(is_active)
2300                                    );
2301                                }
2302                            }
2303                        }
2304                    }
2305                }
2306            }
2307        }
2308    }
2309
2310    // Atomic write
2311    let temp_path = format!("{}.tmp", yaml_path);
2312    let updated_yaml = serde_yaml::to_string(&yaml_value)?;
2313    fs::write(&temp_path, updated_yaml)?;
2314    fs::rename(&temp_path, yaml_path)?;
2315
2316    log::info!("Updated active layout to '{}' in YAML: {}", active_layout_id, yaml_path);
2317    Ok(())
2318}
2319
2320// F0190: YAML persistence functions for live muxbox resizing
2321pub fn save_muxbox_bounds_to_yaml(
2322    yaml_path: &str,
2323    muxbox_id: &str,
2324    new_bounds: &crate::InputBounds,
2325) -> Result<(), Box<dyn std::error::Error>> {
2326    use serde_yaml::{self, Value};
2327    use std::fs;
2328
2329    // Read the current YAML file
2330    let yaml_content = fs::read_to_string(yaml_path)?;
2331    let mut yaml_value: Value = serde_yaml::from_str(&yaml_content)?;
2332
2333    // Find and update the muxbox bounds
2334    update_muxbox_bounds_recursive(&mut yaml_value, muxbox_id, new_bounds)?;
2335
2336    // Atomic write
2337    let temp_path = format!("{}.tmp", yaml_path);
2338    let updated_yaml = serde_yaml::to_string(&yaml_value)?;
2339    fs::write(&temp_path, updated_yaml)?;
2340    fs::rename(&temp_path, yaml_path)?;
2341
2342    log::debug!("Updated muxbox {} bounds in YAML: {}", muxbox_id, yaml_path);
2343    Ok(())
2344}
2345
2346/// Save muxbox content changes to YAML
2347pub fn save_muxbox_content_to_yaml(
2348    yaml_path: &str,
2349    muxbox_id: &str,
2350    new_content: &str,
2351) -> Result<(), Box<dyn std::error::Error>> {
2352    use serde_yaml::Value;
2353    use std::fs;
2354
2355    let yaml_content = fs::read_to_string(yaml_path)?;
2356    let mut yaml_value: Value = serde_yaml::from_str(&yaml_content)?;
2357
2358    update_muxbox_field_recursive(&mut yaml_value, muxbox_id, "content", &Value::String(new_content.to_string()))?;
2359
2360    let temp_path = format!("{}.tmp", yaml_path);
2361    let updated_yaml = serde_yaml::to_string(&yaml_value)?;
2362    fs::write(&temp_path, updated_yaml)?;
2363    fs::rename(&temp_path, yaml_path)?;
2364
2365    log::debug!("Updated muxbox {} content in YAML: {}", muxbox_id, yaml_path);
2366    Ok(())
2367}
2368
2369/// Save muxbox scroll position to YAML
2370pub fn save_muxbox_scroll_to_yaml(
2371    yaml_path: &str,
2372    muxbox_id: &str,
2373    scroll_x: usize,
2374    scroll_y: usize,
2375) -> Result<(), Box<dyn std::error::Error>> {
2376    use serde_yaml::Value;
2377    use std::fs;
2378
2379    let yaml_content = fs::read_to_string(yaml_path)?;
2380    let mut yaml_value: Value = serde_yaml::from_str(&yaml_content)?;
2381
2382    update_muxbox_field_recursive(&mut yaml_value, muxbox_id, "scroll_x", &Value::Number(serde_yaml::Number::from(scroll_x)))?;
2383    update_muxbox_field_recursive(&mut yaml_value, muxbox_id, "scroll_y", &Value::Number(serde_yaml::Number::from(scroll_y)))?;
2384
2385    let temp_path = format!("{}.tmp", yaml_path);
2386    let updated_yaml = serde_yaml::to_string(&yaml_value)?;
2387    fs::write(&temp_path, updated_yaml)?;
2388    fs::rename(&temp_path, yaml_path)?;
2389
2390    log::debug!("Updated muxbox {} scroll position in YAML: {}", muxbox_id, yaml_path);
2391    Ok(())
2392}
2393
2394/// Generic function to update any muxbox field in YAML
2395fn update_muxbox_field_recursive(
2396    value: &mut serde_yaml::Value,
2397    target_muxbox_id: &str,
2398    field_name: &str,
2399    new_value: &serde_yaml::Value,
2400) -> Result<bool, Box<dyn std::error::Error>> {
2401    use serde_yaml::Value;
2402    match value {
2403        Value::Mapping(map) => {
2404            // Check if this is the target muxbox
2405            if let Some(Value::String(id)) = map.get(&Value::String("id".to_string())) {
2406                if id == target_muxbox_id {
2407                    map.insert(Value::String(field_name.to_string()), new_value.clone());
2408                    return Ok(true);
2409                }
2410            }
2411
2412            // Recursively search in all fields
2413            for (_, child_value) in map.iter_mut() {
2414                if update_muxbox_field_recursive(child_value, target_muxbox_id, field_name, new_value)? {
2415                    return Ok(true);
2416                }
2417            }
2418        }
2419        Value::Sequence(seq) => {
2420            for child_value in seq.iter_mut() {
2421                if update_muxbox_field_recursive(child_value, target_muxbox_id, field_name, new_value)? {
2422                    return Ok(true);
2423                }
2424            }
2425        }
2426        _ => {}
2427    }
2428    Ok(false)
2429}
2430
2431pub fn update_muxbox_bounds_recursive(
2432    value: &mut serde_yaml::Value,
2433    target_muxbox_id: &str,
2434    new_bounds: &crate::InputBounds,
2435) -> Result<bool, Box<dyn std::error::Error>> {
2436    use serde_yaml::Value;
2437    match value {
2438        Value::Mapping(map) => {
2439            // Check if this is the muxbox we're looking for
2440            if let Some(Value::String(id)) = map.get(&Value::String("id".to_string())) {
2441                if id == target_muxbox_id {
2442                    // Update the bounds in the position field
2443                    if let Some(position_value) =
2444                        map.get_mut(&Value::String("position".to_string()))
2445                    {
2446                        if let Value::Mapping(position_map) = position_value {
2447                            position_map.insert(
2448                                Value::String("x1".to_string()),
2449                                Value::String(new_bounds.x1.clone()),
2450                            );
2451                            position_map.insert(
2452                                Value::String("y1".to_string()),
2453                                Value::String(new_bounds.y1.clone()),
2454                            );
2455                            position_map.insert(
2456                                Value::String("x2".to_string()),
2457                                Value::String(new_bounds.x2.clone()),
2458                            );
2459                            position_map.insert(
2460                                Value::String("y2".to_string()),
2461                                Value::String(new_bounds.y2.clone()),
2462                            );
2463                            return Ok(true);
2464                        }
2465                    }
2466                    // If no position field exists, create one
2467                    let mut position_map = serde_yaml::Mapping::new();
2468                    position_map.insert(
2469                        Value::String("x1".to_string()),
2470                        Value::String(new_bounds.x1.clone()),
2471                    );
2472                    position_map.insert(
2473                        Value::String("y1".to_string()),
2474                        Value::String(new_bounds.y1.clone()),
2475                    );
2476                    position_map.insert(
2477                        Value::String("x2".to_string()),
2478                        Value::String(new_bounds.x2.clone()),
2479                    );
2480                    position_map.insert(
2481                        Value::String("y2".to_string()),
2482                        Value::String(new_bounds.y2.clone()),
2483                    );
2484                    map.insert(
2485                        Value::String("position".to_string()),
2486                        Value::Mapping(position_map),
2487                    );
2488                    return Ok(true);
2489                }
2490            }
2491
2492            // Recursively search in children and other mappings
2493            for (_, child_value) in map.iter_mut() {
2494                if update_muxbox_bounds_recursive(child_value, target_muxbox_id, new_bounds)? {
2495                    return Ok(true);
2496                }
2497            }
2498        }
2499        Value::Sequence(seq) => {
2500            // Search through sequences (like children arrays)
2501            for item in seq.iter_mut() {
2502                if update_muxbox_bounds_recursive(item, target_muxbox_id, new_bounds)? {
2503                    return Ok(true);
2504                }
2505            }
2506        }
2507        _ => {
2508            // Other value types don't contain muxboxes
2509        }
2510    }
2511
2512    Ok(false)
2513}