mockforge_schema/
lib.rs

1//! JSON Schema generation for MockForge configuration files
2//!
3//! This crate provides functionality to generate JSON Schema definitions
4//! from MockForge's configuration structs, enabling IDE autocomplete and
5//! validation for `mockforge.yaml`, `mockforge.toml`, persona files, and blueprint files.
6
7use schemars::schema_for;
8
9/// Generate JSON Schema for MockForge ServerConfig (main config)
10///
11/// This function generates a complete JSON Schema that can be used by
12/// IDEs and editors to provide autocomplete, validation, and documentation
13/// for MockForge configuration files.
14///
15/// # Returns
16///
17/// A JSON Schema object as a serde_json::Value
18///
19/// # Example
20///
21/// ```rust
22/// use mockforge_schema::generate_config_schema;
23/// use serde_json;
24///
25/// let schema = generate_config_schema();
26/// let schema_json = serde_json::to_string_pretty(&schema).unwrap();
27/// println!("{}", schema_json);
28/// ```
29pub fn generate_config_schema() -> serde_json::Value {
30    // Generate schema from ServerConfig
31    // ServerConfig needs to have JsonSchema derive (via feature flag)
32    let schema = schema_for!(mockforge_core::ServerConfig);
33
34    let mut schema_value = serde_json::to_value(schema).expect("Failed to serialize schema");
35
36    // Add metadata for better IDE support
37    if let Some(obj) = schema_value.as_object_mut() {
38        obj.insert(
39            "$schema".to_string(),
40            serde_json::json!("http://json-schema.org/draft-07/schema#"),
41        );
42        obj.insert("title".to_string(), serde_json::json!("MockForge Server Configuration"));
43        obj.insert(
44            "description".to_string(),
45            serde_json::json!(
46                "Complete configuration schema for MockForge mock server. \
47             This schema provides autocomplete and validation for mockforge.yaml files."
48            ),
49        );
50    }
51
52    schema_value
53}
54
55/// Generate JSON Schema for Reality configuration
56///
57/// Generates schema for the Reality slider configuration used to control
58/// mock environment realism levels.
59pub fn generate_reality_schema() -> serde_json::Value {
60    let schema = schema_for!(mockforge_core::config::RealitySliderConfig);
61
62    let mut schema_value =
63        serde_json::to_value(schema).expect("Failed to serialize reality schema");
64
65    // Add metadata for better IDE support
66    if let Some(obj) = schema_value.as_object_mut() {
67        obj.insert(
68            "$schema".to_string(),
69            serde_json::json!("http://json-schema.org/draft-07/schema#"),
70        );
71        obj.insert("title".to_string(), serde_json::json!("MockForge Reality Configuration"));
72        obj.insert(
73            "description".to_string(),
74            serde_json::json!(
75                "Reality slider configuration for controlling mock environment realism. \
76             Maps reality levels (1-5) to specific subsystem settings."
77            ),
78        );
79    }
80
81    schema_value
82}
83
84/// Generate JSON Schema for Persona configuration
85///
86/// Generates schema for persona profiles that define consistent data patterns.
87/// Note: This generates a schema for the persona registry config structure.
88pub fn generate_persona_schema() -> serde_json::Value {
89    // Generate schema for PersonaRegistryConfig which contains persona definitions
90    let schema = schema_for!(mockforge_core::config::PersonaRegistryConfig);
91
92    let mut schema_value =
93        serde_json::to_value(schema).expect("Failed to serialize persona schema");
94
95    // Add metadata for better IDE support
96    if let Some(obj) = schema_value.as_object_mut() {
97        obj.insert(
98            "$schema".to_string(),
99            serde_json::json!("http://json-schema.org/draft-07/schema#"),
100        );
101        obj.insert("title".to_string(), serde_json::json!("MockForge Persona Configuration"));
102        obj.insert(
103            "description".to_string(),
104            serde_json::json!(
105                "Persona configuration for consistent, personality-driven data generation. \
106             Defines personas with unique IDs, domains, traits, and deterministic seeds."
107            ),
108        );
109    }
110
111    schema_value
112}
113
114/// Generate JSON Schema for Blueprint metadata
115///
116/// Generates schema for blueprint.yaml files that define app archetypes.
117/// Note: Blueprint structs are in mockforge-cli, so we generate a manual schema
118/// based on the known structure.
119pub fn generate_blueprint_schema() -> serde_json::Value {
120    // Manual schema for blueprint metadata since it's in a different crate
121    // This matches the BlueprintMetadata structure
122    serde_json::json!({
123        "$schema": "http://json-schema.org/draft-07/schema#",
124        "title": "MockForge Blueprint Configuration",
125        "description": "Blueprint metadata schema for predefined app archetypes. \
126                       Blueprints provide pre-configured personas, reality defaults, \
127                       flows, scenarios, and playground collections.",
128        "type": "object",
129        "required": ["manifest_version", "name", "version", "title", "description", "author", "category"],
130        "properties": {
131            "manifest_version": {
132                "type": "string",
133                "description": "Blueprint manifest version (e.g., '1.0')",
134                "example": "1.0"
135            },
136            "name": {
137                "type": "string",
138                "description": "Unique blueprint identifier (e.g., 'b2c-saas', 'ecommerce')",
139                "pattern": "^[a-z0-9-]+$"
140            },
141            "version": {
142                "type": "string",
143                "description": "Blueprint version (semver)",
144                "pattern": "^\\d+\\.\\d+\\.\\d+$"
145            },
146            "title": {
147                "type": "string",
148                "description": "Human-readable blueprint title"
149            },
150            "description": {
151                "type": "string",
152                "description": "Detailed description of what this blueprint provides"
153            },
154            "author": {
155                "type": "string",
156                "description": "Blueprint author name"
157            },
158            "author_email": {
159                "type": "string",
160                "format": "email",
161                "description": "Blueprint author email (optional)"
162            },
163            "category": {
164                "type": "string",
165                "description": "Blueprint category (e.g., 'saas', 'ecommerce', 'banking')",
166                "enum": ["saas", "ecommerce", "banking", "fintech", "healthcare", "other"]
167            },
168            "tags": {
169                "type": "array",
170                "items": {
171                    "type": "string"
172                },
173                "description": "Tags for categorizing and searching blueprints"
174            },
175            "setup": {
176                "type": "object",
177                "description": "What this blueprint sets up",
178                "properties": {
179                    "personas": {
180                        "type": "array",
181                        "items": {
182                            "type": "object",
183                            "required": ["id", "name"],
184                            "properties": {
185                                "id": {
186                                    "type": "string",
187                                    "description": "Persona identifier"
188                                },
189                                "name": {
190                                    "type": "string",
191                                    "description": "Persona display name"
192                                },
193                                "description": {
194                                    "type": "string",
195                                    "description": "Persona description (optional)"
196                                }
197                            }
198                        }
199                    },
200                    "reality": {
201                        "type": "object",
202                        "properties": {
203                            "level": {
204                                "type": "string",
205                                "enum": ["static", "light", "moderate", "high", "chaos"],
206                                "description": "Default reality level for this blueprint"
207                            },
208                            "description": {
209                                "type": "string",
210                                "description": "Why this reality level is chosen"
211                            }
212                        }
213                    },
214                    "flows": {
215                        "type": "array",
216                        "items": {
217                            "type": "object",
218                            "required": ["id", "name"],
219                            "properties": {
220                                "id": {
221                                    "type": "string"
222                                },
223                                "name": {
224                                    "type": "string"
225                                },
226                                "description": {
227                                    "type": "string"
228                                }
229                            }
230                        }
231                    },
232                    "scenarios": {
233                        "type": "array",
234                        "items": {
235                            "type": "object",
236                            "required": ["id", "name", "type", "file"],
237                            "properties": {
238                                "id": {
239                                    "type": "string",
240                                    "description": "Scenario identifier"
241                                },
242                                "name": {
243                                    "type": "string",
244                                    "description": "Scenario display name"
245                                },
246                                "type": {
247                                    "type": "string",
248                                    "enum": ["happy_path", "known_failure", "slow_path"],
249                                    "description": "Scenario type"
250                                },
251                                "description": {
252                                    "type": "string",
253                                    "description": "Scenario description (optional)"
254                                },
255                                "file": {
256                                    "type": "string",
257                                    "description": "Path to scenario YAML file"
258                                }
259                            }
260                        }
261                    },
262                    "playground": {
263                        "type": "object",
264                        "properties": {
265                            "enabled": {
266                                "type": "boolean",
267                                "default": true
268                            },
269                            "collection_file": {
270                                "type": "string",
271                                "description": "Path to playground collection file"
272                            }
273                        }
274                    }
275                }
276            },
277            "compatibility": {
278                "type": "object",
279                "properties": {
280                    "min_version": {
281                        "type": "string",
282                        "description": "Minimum MockForge version required"
283                    },
284                    "max_version": {
285                        "type": "string",
286                        "description": "Maximum MockForge version (null for latest)"
287                    },
288                    "required_features": {
289                        "type": "array",
290                        "items": {
291                            "type": "string"
292                        }
293                    },
294                    "protocols": {
295                        "type": "array",
296                        "items": {
297                            "type": "string",
298                            "enum": ["http", "websocket", "grpc", "graphql", "mqtt"]
299                        }
300                    }
301                }
302            },
303            "files": {
304                "type": "array",
305                "items": {
306                    "type": "string"
307                },
308                "description": "List of files included in this blueprint"
309            },
310            "readme": {
311                "type": "string",
312                "description": "Path to README file (optional)"
313            },
314            "contracts": {
315                "type": "array",
316                "items": {
317                    "type": "object",
318                    "required": ["file"],
319                    "properties": {
320                        "file": {
321                            "type": "string",
322                            "description": "Path to contract schema file"
323                        },
324                        "description": {
325                            "type": "string",
326                            "description": "Contract description (optional)"
327                        }
328                    }
329                }
330            }
331        }
332    })
333}
334
335/// Generate JSON Schema and return as a formatted JSON string
336///
337/// # Returns
338///
339/// A pretty-printed JSON string containing the schema
340pub fn generate_config_schema_json() -> String {
341    let schema = generate_config_schema();
342    serde_json::to_string_pretty(&schema).expect("Failed to format schema as JSON")
343}
344
345/// Generate all schemas and return them as a map
346///
347/// Returns a HashMap with schema names as keys and JSON Schema values.
348pub fn generate_all_schemas() -> std::collections::HashMap<String, serde_json::Value> {
349    let mut schemas = std::collections::HashMap::new();
350
351    schemas.insert("mockforge-config".to_string(), generate_config_schema());
352    schemas.insert("reality-config".to_string(), generate_reality_schema());
353    schemas.insert("persona-config".to_string(), generate_persona_schema());
354    schemas.insert("blueprint-config".to_string(), generate_blueprint_schema());
355
356    schemas
357}
358
359/// Validation result for config file validation
360#[derive(Debug, Clone)]
361pub struct ValidationResult {
362    /// Whether validation passed
363    pub valid: bool,
364    /// File path that was validated
365    pub file_path: String,
366    /// Schema type used for validation
367    pub schema_type: String,
368    /// Validation errors (empty if valid)
369    pub errors: Vec<String>,
370}
371
372impl ValidationResult {
373    /// Create a successful validation result
374    pub fn success(file_path: String, schema_type: String) -> Self {
375        Self {
376            valid: true,
377            file_path,
378            schema_type,
379            errors: Vec::new(),
380        }
381    }
382
383    /// Create a failed validation result
384    pub fn failure(file_path: String, schema_type: String, errors: Vec<String>) -> Self {
385        Self {
386            valid: false,
387            file_path,
388            schema_type,
389            errors,
390        }
391    }
392}
393
394/// Validate a config file against its corresponding JSON Schema
395///
396/// # Arguments
397///
398/// * `file_path` - Path to the config file (YAML or JSON)
399/// * `schema_type` - Type of schema to validate against (config, reality, persona, blueprint)
400/// * `schema` - The JSON Schema to validate against
401///
402/// # Returns
403///
404/// A ValidationResult indicating whether validation passed and any errors
405pub fn validate_config_file(
406    file_path: &std::path::Path,
407    schema_type: &str,
408    schema: &serde_json::Value,
409) -> Result<ValidationResult, Box<dyn std::error::Error>> {
410    use jsonschema::{Draft, Validator as SchemaValidator};
411    use std::fs;
412
413    // Read and parse the config file
414    let content = fs::read_to_string(file_path)?;
415    let config_value: serde_json::Value = if file_path
416        .extension()
417        .and_then(|ext| ext.to_str())
418        .map(|ext| ext.eq_ignore_ascii_case("yaml") || ext.eq_ignore_ascii_case("yml"))
419        .unwrap_or(false)
420    {
421        // Parse YAML
422        serde_yaml::from_str(&content).map_err(|e| format!("Failed to parse YAML: {}", e))?
423    } else {
424        // Parse JSON
425        serde_json::from_str(&content).map_err(|e| format!("Failed to parse JSON: {}", e))?
426    };
427
428    // Compile the schema
429    let compiled_schema = SchemaValidator::options()
430        .with_draft(Draft::Draft7)
431        .build(schema)
432        .map_err(|e| format!("Failed to compile schema: {}", e))?;
433
434    // Validate
435    let mut errors = Vec::new();
436    for error in compiled_schema.iter_errors(&config_value) {
437        errors.push(format!("{}: {}", error.instance_path, error));
438    }
439
440    if errors.is_empty() {
441        Ok(ValidationResult::success(
442            file_path.to_string_lossy().to_string(),
443            schema_type.to_string(),
444        ))
445    } else {
446        Ok(ValidationResult::failure(
447            file_path.to_string_lossy().to_string(),
448            schema_type.to_string(),
449            errors,
450        ))
451    }
452}
453
454/// Auto-detect schema type from file path or content
455///
456/// Attempts to determine which schema should be used to validate a file
457/// based on its path or content.
458pub fn detect_schema_type(file_path: &std::path::Path) -> Option<String> {
459    let file_name = file_path.file_name()?.to_string_lossy().to_lowercase();
460    let path_str = file_path.to_string_lossy().to_lowercase();
461
462    // Check file name patterns
463    if file_name == "mockforge.yaml"
464        || file_name == "mockforge.yml"
465        || file_name == "mockforge.json"
466    {
467        return Some("mockforge-config".to_string());
468    }
469
470    if file_name == "blueprint.yaml" || file_name == "blueprint.yml" {
471        return Some("blueprint-config".to_string());
472    }
473
474    if path_str.contains("reality") {
475        return Some("reality-config".to_string());
476    }
477
478    if path_str.contains("persona") {
479        return Some("persona-config".to_string());
480    }
481
482    // Default to main config
483    Some("mockforge-config".to_string())
484}
485
486#[cfg(test)]
487mod tests {
488    use super::*;
489    use std::io::Write;
490    use std::path::PathBuf;
491
492    // ==================== Schema Generation Tests ====================
493
494    #[test]
495    fn test_generate_config_schema() {
496        let schema = generate_config_schema();
497        assert!(schema.is_object());
498
499        let obj = schema.as_object().unwrap();
500
501        // Verify metadata was added
502        assert!(obj.contains_key("$schema"));
503        assert_eq!(
504            obj.get("$schema").unwrap(),
505            &serde_json::json!("http://json-schema.org/draft-07/schema#")
506        );
507        assert!(obj.contains_key("title"));
508        assert_eq!(obj.get("title").unwrap(), &serde_json::json!("MockForge Server Configuration"));
509        assert!(obj.contains_key("description"));
510    }
511
512    #[test]
513    fn test_generate_reality_schema() {
514        let schema = generate_reality_schema();
515        assert!(schema.is_object());
516
517        let obj = schema.as_object().unwrap();
518
519        // Verify metadata was added
520        assert!(obj.contains_key("$schema"));
521        assert!(obj.contains_key("title"));
522        assert_eq!(
523            obj.get("title").unwrap(),
524            &serde_json::json!("MockForge Reality Configuration")
525        );
526        assert!(obj.contains_key("description"));
527    }
528
529    #[test]
530    fn test_generate_persona_schema() {
531        let schema = generate_persona_schema();
532        assert!(schema.is_object());
533
534        let obj = schema.as_object().unwrap();
535
536        // Verify metadata was added
537        assert!(obj.contains_key("$schema"));
538        assert!(obj.contains_key("title"));
539        assert_eq!(
540            obj.get("title").unwrap(),
541            &serde_json::json!("MockForge Persona Configuration")
542        );
543        assert!(obj.contains_key("description"));
544    }
545
546    #[test]
547    fn test_generate_blueprint_schema() {
548        let schema = generate_blueprint_schema();
549        assert!(schema.is_object());
550
551        let obj = schema.as_object().unwrap();
552
553        // Verify required metadata
554        assert!(obj.contains_key("$schema"));
555        assert!(obj.contains_key("title"));
556        assert_eq!(
557            obj.get("title").unwrap(),
558            &serde_json::json!("MockForge Blueprint Configuration")
559        );
560
561        // Verify type is object
562        assert_eq!(obj.get("type").unwrap(), &serde_json::json!("object"));
563
564        // Verify required fields are specified
565        assert!(obj.contains_key("required"));
566        let required = obj.get("required").unwrap().as_array().unwrap();
567        assert!(required.contains(&serde_json::json!("name")));
568        assert!(required.contains(&serde_json::json!("version")));
569        assert!(required.contains(&serde_json::json!("title")));
570
571        // Verify properties exist
572        assert!(obj.contains_key("properties"));
573        let props = obj.get("properties").unwrap().as_object().unwrap();
574        assert!(props.contains_key("manifest_version"));
575        assert!(props.contains_key("name"));
576        assert!(props.contains_key("version"));
577        assert!(props.contains_key("category"));
578        assert!(props.contains_key("setup"));
579        assert!(props.contains_key("compatibility"));
580    }
581
582    #[test]
583    fn test_generate_blueprint_schema_category_enum() {
584        let schema = generate_blueprint_schema();
585        let obj = schema.as_object().unwrap();
586        let props = obj.get("properties").unwrap().as_object().unwrap();
587        let category = props.get("category").unwrap().as_object().unwrap();
588
589        // Verify category has enum values
590        assert!(category.contains_key("enum"));
591        let enum_values = category.get("enum").unwrap().as_array().unwrap();
592        assert!(enum_values.contains(&serde_json::json!("saas")));
593        assert!(enum_values.contains(&serde_json::json!("ecommerce")));
594        assert!(enum_values.contains(&serde_json::json!("banking")));
595    }
596
597    #[test]
598    fn test_generate_config_schema_json() {
599        let json = generate_config_schema_json();
600        assert!(!json.is_empty());
601
602        // Verify it's valid JSON
603        let parsed: Result<serde_json::Value, _> = serde_json::from_str(&json);
604        assert!(parsed.is_ok());
605
606        // Verify it's the same as generate_config_schema
607        let schema = generate_config_schema();
608        let reparsed = serde_json::from_str::<serde_json::Value>(&json).unwrap();
609
610        // Check key fields match
611        assert_eq!(reparsed.get("title").unwrap(), schema.get("title").unwrap());
612    }
613
614    #[test]
615    fn test_generate_all_schemas() {
616        let schemas = generate_all_schemas();
617
618        // Verify all expected schemas are present
619        assert_eq!(schemas.len(), 4);
620        assert!(schemas.contains_key("mockforge-config"));
621        assert!(schemas.contains_key("reality-config"));
622        assert!(schemas.contains_key("persona-config"));
623        assert!(schemas.contains_key("blueprint-config"));
624
625        // Verify each schema is valid
626        for (name, schema) in &schemas {
627            assert!(schema.is_object(), "Schema {} should be an object", name);
628        }
629    }
630
631    // ==================== ValidationResult Tests ====================
632
633    #[test]
634    fn test_validation_result_success() {
635        let result = ValidationResult::success(
636            "/path/to/config.yaml".to_string(),
637            "mockforge-config".to_string(),
638        );
639
640        assert!(result.valid);
641        assert_eq!(result.file_path, "/path/to/config.yaml");
642        assert_eq!(result.schema_type, "mockforge-config");
643        assert!(result.errors.is_empty());
644    }
645
646    #[test]
647    fn test_validation_result_failure() {
648        let errors = vec![
649            "Missing required field: name".to_string(),
650            "Invalid type for port".to_string(),
651        ];
652
653        let result = ValidationResult::failure(
654            "/path/to/invalid.yaml".to_string(),
655            "mockforge-config".to_string(),
656            errors.clone(),
657        );
658
659        assert!(!result.valid);
660        assert_eq!(result.file_path, "/path/to/invalid.yaml");
661        assert_eq!(result.schema_type, "mockforge-config");
662        assert_eq!(result.errors.len(), 2);
663        assert_eq!(result.errors, errors);
664    }
665
666    #[test]
667    fn test_validation_result_debug() {
668        let result = ValidationResult::success("test.yaml".to_string(), "config".to_string());
669        let debug_str = format!("{:?}", result);
670        assert!(debug_str.contains("ValidationResult"));
671        assert!(debug_str.contains("valid"));
672        assert!(debug_str.contains("test.yaml"));
673    }
674
675    #[test]
676    fn test_validation_result_clone() {
677        let result = ValidationResult::failure(
678            "test.yaml".to_string(),
679            "config".to_string(),
680            vec!["error1".to_string()],
681        );
682        let cloned = result.clone();
683
684        assert_eq!(cloned.valid, result.valid);
685        assert_eq!(cloned.file_path, result.file_path);
686        assert_eq!(cloned.schema_type, result.schema_type);
687        assert_eq!(cloned.errors, result.errors);
688    }
689
690    // ==================== detect_schema_type Tests ====================
691
692    #[test]
693    fn test_detect_schema_type_mockforge_yaml() {
694        let path = PathBuf::from("/project/mockforge.yaml");
695        assert_eq!(detect_schema_type(&path), Some("mockforge-config".to_string()));
696    }
697
698    #[test]
699    fn test_detect_schema_type_mockforge_yml() {
700        let path = PathBuf::from("/project/mockforge.yml");
701        assert_eq!(detect_schema_type(&path), Some("mockforge-config".to_string()));
702    }
703
704    #[test]
705    fn test_detect_schema_type_mockforge_json() {
706        let path = PathBuf::from("/project/mockforge.json");
707        assert_eq!(detect_schema_type(&path), Some("mockforge-config".to_string()));
708    }
709
710    #[test]
711    fn test_detect_schema_type_blueprint_yaml() {
712        let path = PathBuf::from("/blueprints/saas/blueprint.yaml");
713        assert_eq!(detect_schema_type(&path), Some("blueprint-config".to_string()));
714    }
715
716    #[test]
717    fn test_detect_schema_type_blueprint_yml() {
718        let path = PathBuf::from("/blueprints/ecommerce/blueprint.yml");
719        assert_eq!(detect_schema_type(&path), Some("blueprint-config".to_string()));
720    }
721
722    #[test]
723    fn test_detect_schema_type_reality_path() {
724        let path = PathBuf::from("/config/reality/settings.yaml");
725        assert_eq!(detect_schema_type(&path), Some("reality-config".to_string()));
726    }
727
728    #[test]
729    fn test_detect_schema_type_persona_path() {
730        let path = PathBuf::from("/config/persona/developer.yaml");
731        assert_eq!(detect_schema_type(&path), Some("persona-config".to_string()));
732    }
733
734    #[test]
735    fn test_detect_schema_type_default() {
736        let path = PathBuf::from("/some/other/config.yaml");
737        assert_eq!(detect_schema_type(&path), Some("mockforge-config".to_string()));
738    }
739
740    #[test]
741    fn test_detect_schema_type_case_insensitive() {
742        let path = PathBuf::from("/Project/MOCKFORGE.YAML");
743        assert_eq!(detect_schema_type(&path), Some("mockforge-config".to_string()));
744
745        let path2 = PathBuf::from("/blueprints/Blueprint.YML");
746        assert_eq!(detect_schema_type(&path2), Some("blueprint-config".to_string()));
747    }
748
749    // ==================== validate_config_file Tests ====================
750
751    #[test]
752    fn test_validate_yaml_file_valid() {
753        // Create a simple schema
754        let schema = serde_json::json!({
755            "$schema": "http://json-schema.org/draft-07/schema#",
756            "type": "object",
757            "properties": {
758                "name": { "type": "string" },
759                "port": { "type": "integer" }
760            },
761            "required": ["name"]
762        });
763
764        // Create a temp file
765        let temp_dir = std::env::temp_dir();
766        let file_path = temp_dir.join("test_valid_config.yaml");
767        let mut file = std::fs::File::create(&file_path).unwrap();
768        writeln!(file, "name: test-service\nport: 8080").unwrap();
769
770        let result = validate_config_file(&file_path, "test-config", &schema).unwrap();
771
772        assert!(result.valid);
773        assert!(result.errors.is_empty());
774
775        // Cleanup
776        std::fs::remove_file(&file_path).ok();
777    }
778
779    #[test]
780    fn test_validate_json_file_valid() {
781        let schema = serde_json::json!({
782            "$schema": "http://json-schema.org/draft-07/schema#",
783            "type": "object",
784            "properties": {
785                "name": { "type": "string" }
786            },
787            "required": ["name"]
788        });
789
790        let temp_dir = std::env::temp_dir();
791        let file_path = temp_dir.join("test_valid_config.json");
792        let content = r#"{"name": "test-service"}"#;
793        std::fs::write(&file_path, content).unwrap();
794
795        let result = validate_config_file(&file_path, "test-config", &schema).unwrap();
796
797        assert!(result.valid);
798        assert!(result.errors.is_empty());
799
800        std::fs::remove_file(&file_path).ok();
801    }
802
803    #[test]
804    fn test_validate_yaml_file_missing_required() {
805        let schema = serde_json::json!({
806            "$schema": "http://json-schema.org/draft-07/schema#",
807            "type": "object",
808            "properties": {
809                "name": { "type": "string" },
810                "port": { "type": "integer" }
811            },
812            "required": ["name", "port"]
813        });
814
815        let temp_dir = std::env::temp_dir();
816        let file_path = temp_dir.join("test_missing_required.yaml");
817        let mut file = std::fs::File::create(&file_path).unwrap();
818        writeln!(file, "name: test-service").unwrap(); // Missing port
819
820        let result = validate_config_file(&file_path, "test-config", &schema).unwrap();
821
822        assert!(!result.valid);
823        assert!(!result.errors.is_empty());
824        // Should have error about missing "port"
825        let has_port_error = result.errors.iter().any(|e| e.contains("port"));
826        assert!(has_port_error, "Expected error about missing 'port'");
827
828        std::fs::remove_file(&file_path).ok();
829    }
830
831    #[test]
832    fn test_validate_yaml_file_wrong_type() {
833        let schema = serde_json::json!({
834            "$schema": "http://json-schema.org/draft-07/schema#",
835            "type": "object",
836            "properties": {
837                "port": { "type": "integer" }
838            }
839        });
840
841        let temp_dir = std::env::temp_dir();
842        let file_path = temp_dir.join("test_wrong_type.yaml");
843        let mut file = std::fs::File::create(&file_path).unwrap();
844        writeln!(file, "port: not-a-number").unwrap(); // String instead of integer
845
846        let result = validate_config_file(&file_path, "test-config", &schema).unwrap();
847
848        assert!(!result.valid);
849        assert!(!result.errors.is_empty());
850
851        std::fs::remove_file(&file_path).ok();
852    }
853
854    #[test]
855    fn test_validate_file_not_found() {
856        let schema = serde_json::json!({
857            "type": "object"
858        });
859
860        let file_path = PathBuf::from("/nonexistent/path/config.yaml");
861        let result = validate_config_file(&file_path, "test-config", &schema);
862
863        assert!(result.is_err());
864    }
865
866    #[test]
867    fn test_validate_invalid_yaml_syntax() {
868        let schema = serde_json::json!({
869            "type": "object"
870        });
871
872        let temp_dir = std::env::temp_dir();
873        let file_path = temp_dir.join("test_invalid_yaml.yaml");
874        let mut file = std::fs::File::create(&file_path).unwrap();
875        writeln!(file, "invalid: yaml: syntax: [unclosed").unwrap();
876
877        let result = validate_config_file(&file_path, "test-config", &schema);
878
879        assert!(result.is_err());
880
881        std::fs::remove_file(&file_path).ok();
882    }
883
884    #[test]
885    fn test_validate_invalid_json_syntax() {
886        let schema = serde_json::json!({
887            "type": "object"
888        });
889
890        let temp_dir = std::env::temp_dir();
891        let file_path = temp_dir.join("test_invalid.json");
892        let content = r#"{"unclosed": "#;
893        std::fs::write(&file_path, content).unwrap();
894
895        let result = validate_config_file(&file_path, "test-config", &schema);
896
897        assert!(result.is_err());
898
899        std::fs::remove_file(&file_path).ok();
900    }
901
902    #[test]
903    fn test_validate_yml_extension() {
904        let schema = serde_json::json!({
905            "type": "object"
906        });
907
908        let temp_dir = std::env::temp_dir();
909        let file_path = temp_dir.join("test_config.yml"); // .yml extension
910        let mut file = std::fs::File::create(&file_path).unwrap();
911        writeln!(file, "key: value").unwrap();
912
913        let result = validate_config_file(&file_path, "test-config", &schema).unwrap();
914
915        assert!(result.valid);
916
917        std::fs::remove_file(&file_path).ok();
918    }
919
920    // ==================== Edge Cases ====================
921
922    #[test]
923    fn test_empty_yaml_file() {
924        let schema = serde_json::json!({
925            "type": "object"
926        });
927
928        let temp_dir = std::env::temp_dir();
929        let file_path = temp_dir.join("test_empty.yaml");
930        std::fs::File::create(&file_path).unwrap();
931
932        // Empty YAML parses as null
933        let result = validate_config_file(&file_path, "test-config", &schema);
934        // Could be valid or invalid depending on schema, but shouldn't crash
935        assert!(result.is_ok() || result.is_err());
936
937        std::fs::remove_file(&file_path).ok();
938    }
939
940    #[test]
941    fn test_schema_with_additional_properties_false() {
942        let schema = serde_json::json!({
943            "$schema": "http://json-schema.org/draft-07/schema#",
944            "type": "object",
945            "properties": {
946                "name": { "type": "string" }
947            },
948            "additionalProperties": false
949        });
950
951        let temp_dir = std::env::temp_dir();
952        let file_path = temp_dir.join("test_extra_props.yaml");
953        let mut file = std::fs::File::create(&file_path).unwrap();
954        writeln!(file, "name: test\nextra: not-allowed").unwrap();
955
956        let result = validate_config_file(&file_path, "test-config", &schema).unwrap();
957
958        assert!(!result.valid);
959        assert!(!result.errors.is_empty());
960
961        std::fs::remove_file(&file_path).ok();
962    }
963
964    #[test]
965    fn test_nested_validation_error() {
966        let schema = serde_json::json!({
967            "$schema": "http://json-schema.org/draft-07/schema#",
968            "type": "object",
969            "properties": {
970                "server": {
971                    "type": "object",
972                    "properties": {
973                        "port": { "type": "integer", "minimum": 1, "maximum": 65535 }
974                    }
975                }
976            }
977        });
978
979        let temp_dir = std::env::temp_dir();
980        let file_path = temp_dir.join("test_nested.yaml");
981        let mut file = std::fs::File::create(&file_path).unwrap();
982        writeln!(file, "server:\n  port: 99999").unwrap(); // Port out of range
983
984        let result = validate_config_file(&file_path, "test-config", &schema).unwrap();
985
986        assert!(!result.valid);
987        // Error should reference the nested path
988        let has_port_error = result.errors.iter().any(|e| e.contains("port"));
989        assert!(has_port_error);
990
991        std::fs::remove_file(&file_path).ok();
992    }
993
994    #[test]
995    fn test_validation_result_empty_errors() {
996        let result =
997            ValidationResult::failure("test.yaml".to_string(), "config".to_string(), vec![]);
998
999        // Even with empty errors vec, valid should be false
1000        assert!(!result.valid);
1001        assert!(result.errors.is_empty());
1002    }
1003
1004    #[test]
1005    fn test_blueprint_schema_setup_structure() {
1006        let schema = generate_blueprint_schema();
1007        let obj = schema.as_object().unwrap();
1008        let props = obj.get("properties").unwrap().as_object().unwrap();
1009        let setup = props.get("setup").unwrap().as_object().unwrap();
1010
1011        assert_eq!(setup.get("type").unwrap(), &serde_json::json!("object"));
1012
1013        let setup_props = setup.get("properties").unwrap().as_object().unwrap();
1014        assert!(setup_props.contains_key("personas"));
1015        assert!(setup_props.contains_key("reality"));
1016        assert!(setup_props.contains_key("flows"));
1017        assert!(setup_props.contains_key("scenarios"));
1018        assert!(setup_props.contains_key("playground"));
1019    }
1020
1021    // ==================== Additional Schema Structure Tests ====================
1022
1023    #[test]
1024    fn test_config_schema_has_required_properties() {
1025        let schema = generate_config_schema();
1026        let obj = schema.as_object().unwrap();
1027
1028        // Verify it's an object with properties
1029        assert!(obj.contains_key("properties"));
1030        assert!(obj.contains_key("$schema"));
1031        assert!(obj.contains_key("title"));
1032        assert!(obj.contains_key("description"));
1033    }
1034
1035    #[test]
1036    fn test_reality_schema_structure() {
1037        let schema = generate_reality_schema();
1038        let obj = schema.as_object().unwrap();
1039
1040        // Verify metadata
1041        assert_eq!(
1042            obj.get("$schema").unwrap(),
1043            &serde_json::json!("http://json-schema.org/draft-07/schema#")
1044        );
1045        assert!(obj.contains_key("properties") || obj.contains_key("definitions"));
1046    }
1047
1048    #[test]
1049    fn test_persona_schema_structure() {
1050        let schema = generate_persona_schema();
1051        let obj = schema.as_object().unwrap();
1052
1053        // Verify metadata
1054        assert_eq!(
1055            obj.get("$schema").unwrap(),
1056            &serde_json::json!("http://json-schema.org/draft-07/schema#")
1057        );
1058        assert!(obj.contains_key("properties") || obj.contains_key("definitions"));
1059    }
1060
1061    #[test]
1062    fn test_blueprint_schema_compatibility_structure() {
1063        let schema = generate_blueprint_schema();
1064        let obj = schema.as_object().unwrap();
1065        let props = obj.get("properties").unwrap().as_object().unwrap();
1066
1067        assert!(props.contains_key("compatibility"));
1068        let compatibility = props.get("compatibility").unwrap().as_object().unwrap();
1069        assert_eq!(compatibility.get("type").unwrap(), &serde_json::json!("object"));
1070
1071        let compat_props = compatibility.get("properties").unwrap().as_object().unwrap();
1072        assert!(compat_props.contains_key("min_version"));
1073        assert!(compat_props.contains_key("max_version"));
1074        assert!(compat_props.contains_key("required_features"));
1075        assert!(compat_props.contains_key("protocols"));
1076    }
1077
1078    #[test]
1079    fn test_blueprint_schema_protocols_enum() {
1080        let schema = generate_blueprint_schema();
1081        let obj = schema.as_object().unwrap();
1082        let props = obj.get("properties").unwrap().as_object().unwrap();
1083        let compatibility = props.get("compatibility").unwrap().as_object().unwrap();
1084        let compat_props = compatibility.get("properties").unwrap().as_object().unwrap();
1085        let protocols = compat_props.get("protocols").unwrap().as_object().unwrap();
1086
1087        // Check it's an array
1088        assert_eq!(protocols.get("type").unwrap(), &serde_json::json!("array"));
1089
1090        // Check items have enum
1091        let items = protocols.get("items").unwrap().as_object().unwrap();
1092        assert!(items.contains_key("enum"));
1093        let enum_values = items.get("enum").unwrap().as_array().unwrap();
1094        assert!(enum_values.contains(&serde_json::json!("http")));
1095        assert!(enum_values.contains(&serde_json::json!("websocket")));
1096        assert!(enum_values.contains(&serde_json::json!("grpc")));
1097    }
1098
1099    #[test]
1100    fn test_blueprint_schema_name_pattern() {
1101        let schema = generate_blueprint_schema();
1102        let obj = schema.as_object().unwrap();
1103        let props = obj.get("properties").unwrap().as_object().unwrap();
1104        let name = props.get("name").unwrap().as_object().unwrap();
1105
1106        // Verify name has pattern validation
1107        assert!(name.contains_key("pattern"));
1108        assert_eq!(name.get("pattern").unwrap(), &serde_json::json!("^[a-z0-9-]+$"));
1109    }
1110
1111    #[test]
1112    fn test_blueprint_schema_version_pattern() {
1113        let schema = generate_blueprint_schema();
1114        let obj = schema.as_object().unwrap();
1115        let props = obj.get("properties").unwrap().as_object().unwrap();
1116        let version = props.get("version").unwrap().as_object().unwrap();
1117
1118        // Verify version has semver pattern
1119        assert!(version.contains_key("pattern"));
1120        let pattern = version.get("pattern").unwrap().as_str().unwrap();
1121        assert!(pattern.contains("\\d+"));
1122    }
1123
1124    #[test]
1125    fn test_all_schemas_are_valid_json() {
1126        let schemas = generate_all_schemas();
1127
1128        for (name, schema) in schemas {
1129            // Each schema should be serializable to JSON
1130            let json_str = serde_json::to_string(&schema).unwrap();
1131            assert!(!json_str.is_empty(), "Schema {} should serialize to non-empty JSON", name);
1132
1133            // And deserializable back
1134            let reparsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1135            assert_eq!(reparsed, schema, "Schema {} should round-trip correctly", name);
1136        }
1137    }
1138
1139    // ==================== Additional Validation Tests ====================
1140
1141    #[test]
1142    fn test_validate_with_multiple_validation_errors() {
1143        let schema = serde_json::json!({
1144            "$schema": "http://json-schema.org/draft-07/schema#",
1145            "type": "object",
1146            "properties": {
1147                "name": { "type": "string" },
1148                "port": { "type": "integer", "minimum": 1, "maximum": 65535 },
1149                "enabled": { "type": "boolean" }
1150            },
1151            "required": ["name", "port", "enabled"]
1152        });
1153
1154        let temp_dir = std::env::temp_dir();
1155        let file_path = temp_dir.join("test_multi_errors.yaml");
1156        let mut file = std::fs::File::create(&file_path).unwrap();
1157        writeln!(file, "port: not-a-number").unwrap(); // Wrong type + missing required
1158
1159        let result = validate_config_file(&file_path, "test-config", &schema).unwrap();
1160
1161        assert!(!result.valid);
1162        // Should have multiple errors
1163        assert!(
1164            result.errors.len() >= 2,
1165            "Expected at least 2 errors, got {}",
1166            result.errors.len()
1167        );
1168
1169        std::fs::remove_file(&file_path).ok();
1170    }
1171
1172    #[test]
1173    fn test_validate_array_schema() {
1174        let schema = serde_json::json!({
1175            "$schema": "http://json-schema.org/draft-07/schema#",
1176            "type": "object",
1177            "properties": {
1178                "items": {
1179                    "type": "array",
1180                    "items": { "type": "string" },
1181                    "minItems": 1
1182                }
1183            },
1184            "required": ["items"]
1185        });
1186
1187        let temp_dir = std::env::temp_dir();
1188        let file_path = temp_dir.join("test_array.yaml");
1189        let mut file = std::fs::File::create(&file_path).unwrap();
1190        writeln!(file, "items:\n  - item1\n  - item2").unwrap();
1191
1192        let result = validate_config_file(&file_path, "test-config", &schema).unwrap();
1193
1194        assert!(result.valid);
1195        assert!(result.errors.is_empty());
1196
1197        std::fs::remove_file(&file_path).ok();
1198    }
1199
1200    #[test]
1201    fn test_validate_array_schema_empty_array() {
1202        let schema = serde_json::json!({
1203            "$schema": "http://json-schema.org/draft-07/schema#",
1204            "type": "object",
1205            "properties": {
1206                "items": {
1207                    "type": "array",
1208                    "items": { "type": "string" },
1209                    "minItems": 1
1210                }
1211            }
1212        });
1213
1214        let temp_dir = std::env::temp_dir();
1215        let file_path = temp_dir.join("test_empty_array.yaml");
1216        let mut file = std::fs::File::create(&file_path).unwrap();
1217        writeln!(file, "items: []").unwrap();
1218
1219        let result = validate_config_file(&file_path, "test-config", &schema).unwrap();
1220
1221        assert!(!result.valid);
1222        assert!(!result.errors.is_empty());
1223
1224        std::fs::remove_file(&file_path).ok();
1225    }
1226
1227    #[test]
1228    fn test_validate_enum_schema_valid() {
1229        let schema = serde_json::json!({
1230            "$schema": "http://json-schema.org/draft-07/schema#",
1231            "type": "object",
1232            "properties": {
1233                "level": {
1234                    "type": "string",
1235                    "enum": ["low", "medium", "high"]
1236                }
1237            }
1238        });
1239
1240        let temp_dir = std::env::temp_dir();
1241        let file_path = temp_dir.join("test_enum_valid.yaml");
1242        let mut file = std::fs::File::create(&file_path).unwrap();
1243        writeln!(file, "level: medium").unwrap();
1244
1245        let result = validate_config_file(&file_path, "test-config", &schema).unwrap();
1246
1247        assert!(result.valid);
1248
1249        std::fs::remove_file(&file_path).ok();
1250    }
1251
1252    #[test]
1253    fn test_validate_enum_schema_invalid() {
1254        let schema = serde_json::json!({
1255            "$schema": "http://json-schema.org/draft-07/schema#",
1256            "type": "object",
1257            "properties": {
1258                "level": {
1259                    "type": "string",
1260                    "enum": ["low", "medium", "high"]
1261                }
1262            }
1263        });
1264
1265        let temp_dir = std::env::temp_dir();
1266        let file_path = temp_dir.join("test_enum_invalid.yaml");
1267        let mut file = std::fs::File::create(&file_path).unwrap();
1268        writeln!(file, "level: invalid").unwrap();
1269
1270        let result = validate_config_file(&file_path, "test-config", &schema).unwrap();
1271
1272        assert!(!result.valid);
1273        assert!(!result.errors.is_empty());
1274
1275        std::fs::remove_file(&file_path).ok();
1276    }
1277
1278    #[test]
1279    fn test_validate_pattern_string_valid() {
1280        let schema = serde_json::json!({
1281            "$schema": "http://json-schema.org/draft-07/schema#",
1282            "type": "object",
1283            "properties": {
1284                "email": {
1285                    "type": "string",
1286                    "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
1287                }
1288            }
1289        });
1290
1291        let temp_dir = std::env::temp_dir();
1292        let file_path = temp_dir.join("test_pattern_valid.yaml");
1293        let mut file = std::fs::File::create(&file_path).unwrap();
1294        writeln!(file, "email: test@example.com").unwrap();
1295
1296        let result = validate_config_file(&file_path, "test-config", &schema).unwrap();
1297
1298        assert!(result.valid);
1299
1300        std::fs::remove_file(&file_path).ok();
1301    }
1302
1303    #[test]
1304    fn test_validate_pattern_string_invalid() {
1305        let schema = serde_json::json!({
1306            "$schema": "http://json-schema.org/draft-07/schema#",
1307            "type": "object",
1308            "properties": {
1309                "email": {
1310                    "type": "string",
1311                    "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
1312                }
1313            }
1314        });
1315
1316        let temp_dir = std::env::temp_dir();
1317        let file_path = temp_dir.join("test_pattern_invalid.yaml");
1318        let mut file = std::fs::File::create(&file_path).unwrap();
1319        writeln!(file, "email: not-an-email").unwrap();
1320
1321        let result = validate_config_file(&file_path, "test-config", &schema).unwrap();
1322
1323        assert!(!result.valid);
1324
1325        std::fs::remove_file(&file_path).ok();
1326    }
1327
1328    #[test]
1329    fn test_validate_number_constraints() {
1330        let schema = serde_json::json!({
1331            "$schema": "http://json-schema.org/draft-07/schema#",
1332            "type": "object",
1333            "properties": {
1334                "percentage": {
1335                    "type": "number",
1336                    "minimum": 0,
1337                    "maximum": 100
1338                }
1339            }
1340        });
1341
1342        let temp_dir = std::env::temp_dir();
1343
1344        // Test valid
1345        let file_path = temp_dir.join("test_number_valid.yaml");
1346        let mut file = std::fs::File::create(&file_path).unwrap();
1347        writeln!(file, "percentage: 50.5").unwrap();
1348        let result = validate_config_file(&file_path, "test-config", &schema).unwrap();
1349        assert!(result.valid);
1350        std::fs::remove_file(&file_path).ok();
1351
1352        // Test invalid - too high
1353        let file_path = temp_dir.join("test_number_invalid.yaml");
1354        let mut file = std::fs::File::create(&file_path).unwrap();
1355        writeln!(file, "percentage: 150").unwrap();
1356        let result = validate_config_file(&file_path, "test-config", &schema).unwrap();
1357        assert!(!result.valid);
1358        std::fs::remove_file(&file_path).ok();
1359    }
1360
1361    // ==================== detect_schema_type Edge Cases ====================
1362
1363    #[test]
1364    fn test_detect_schema_type_no_extension() {
1365        let path = PathBuf::from("/config/mockforge");
1366        // Should return default
1367        assert_eq!(detect_schema_type(&path), Some("mockforge-config".to_string()));
1368    }
1369
1370    #[test]
1371    fn test_detect_schema_type_nested_persona_path() {
1372        let path = PathBuf::from("/very/deep/path/with/persona/in/middle/config.yaml");
1373        assert_eq!(detect_schema_type(&path), Some("persona-config".to_string()));
1374    }
1375
1376    #[test]
1377    fn test_detect_schema_type_nested_reality_path() {
1378        let path = PathBuf::from("/config/reality-slider/settings.yml");
1379        assert_eq!(detect_schema_type(&path), Some("reality-config".to_string()));
1380    }
1381
1382    #[test]
1383    fn test_detect_schema_type_mixed_case_path() {
1384        let path = PathBuf::from("/Config/REALITY/Settings.YML");
1385        assert_eq!(detect_schema_type(&path), Some("reality-config".to_string()));
1386    }
1387
1388    // ==================== Schema JSON String Tests ====================
1389
1390    #[test]
1391    fn test_generate_config_schema_json_pretty_printed() {
1392        let json = generate_config_schema_json();
1393
1394        // Should contain newlines (pretty printed)
1395        assert!(json.contains('\n'));
1396
1397        // Should contain proper indentation
1398        assert!(json.contains("  ") || json.contains("    "));
1399    }
1400
1401    #[test]
1402    fn test_generate_config_schema_json_has_schema_url() {
1403        let json = generate_config_schema_json();
1404        assert!(json.contains("http://json-schema.org/draft-07/schema#"));
1405    }
1406
1407    // ==================== Validation with Real Schema Tests ====================
1408
1409    #[test]
1410    fn test_validate_with_generated_config_schema() {
1411        let schema = generate_config_schema();
1412
1413        // Create a minimal valid config
1414        let temp_dir = std::env::temp_dir();
1415        let file_path = temp_dir.join("test_real_config.yaml");
1416        let mut file = std::fs::File::create(&file_path).unwrap();
1417        // Write minimal ServerConfig structure
1418        writeln!(file, "port: 8080").unwrap();
1419
1420        // This might fail or succeed depending on ServerConfig requirements
1421        // The test is to ensure it doesn't panic
1422        let result = validate_config_file(&file_path, "mockforge-config", &schema);
1423        assert!(result.is_ok() || result.is_err()); // Either is fine, just don't panic
1424
1425        std::fs::remove_file(&file_path).ok();
1426    }
1427
1428    #[test]
1429    fn test_validate_complex_json_structure() {
1430        let schema = serde_json::json!({
1431            "$schema": "http://json-schema.org/draft-07/schema#",
1432            "type": "object",
1433            "properties": {
1434                "server": {
1435                    "type": "object",
1436                    "properties": {
1437                        "host": { "type": "string" },
1438                        "port": { "type": "integer" },
1439                        "ssl": {
1440                            "type": "object",
1441                            "properties": {
1442                                "enabled": { "type": "boolean" },
1443                                "cert_path": { "type": "string" }
1444                            }
1445                        }
1446                    }
1447                }
1448            }
1449        });
1450
1451        let temp_dir = std::env::temp_dir();
1452        let file_path = temp_dir.join("test_complex.json");
1453        let content = r#"{
1454            "server": {
1455                "host": "localhost",
1456                "port": 8080,
1457                "ssl": {
1458                    "enabled": true,
1459                    "cert_path": "/path/to/cert"
1460                }
1461            }
1462        }"#;
1463        std::fs::write(&file_path, content).unwrap();
1464
1465        let result = validate_config_file(&file_path, "test-config", &schema).unwrap();
1466        assert!(result.valid);
1467
1468        std::fs::remove_file(&file_path).ok();
1469    }
1470
1471    // ==================== Edge Cases for File Extensions ====================
1472
1473    #[test]
1474    fn test_validate_uppercase_yaml_extension() {
1475        let schema = serde_json::json!({
1476            "type": "object"
1477        });
1478
1479        let temp_dir = std::env::temp_dir();
1480        let file_path = temp_dir.join("test_config.YAML");
1481        let mut file = std::fs::File::create(&file_path).unwrap();
1482        writeln!(file, "key: value").unwrap();
1483
1484        let result = validate_config_file(&file_path, "test-config", &schema).unwrap();
1485        assert!(result.valid);
1486
1487        std::fs::remove_file(&file_path).ok();
1488    }
1489
1490    #[test]
1491    fn test_validate_uppercase_json_extension() {
1492        let schema = serde_json::json!({
1493            "type": "object"
1494        });
1495
1496        let temp_dir = std::env::temp_dir();
1497        let file_path = temp_dir.join("test_config.JSON");
1498        let content = r#"{"key": "value"}"#;
1499        std::fs::write(&file_path, content).unwrap();
1500
1501        let result = validate_config_file(&file_path, "test-config", &schema).unwrap();
1502        assert!(result.valid);
1503
1504        std::fs::remove_file(&file_path).ok();
1505    }
1506
1507    #[test]
1508    fn test_validate_no_extension_treats_as_json() {
1509        let schema = serde_json::json!({
1510            "type": "object"
1511        });
1512
1513        let temp_dir = std::env::temp_dir();
1514        let file_path = temp_dir.join("test_config_no_ext");
1515        let content = r#"{"key": "value"}"#;
1516        std::fs::write(&file_path, content).unwrap();
1517
1518        let result = validate_config_file(&file_path, "test-config", &schema).unwrap();
1519        assert!(result.valid);
1520
1521        std::fs::remove_file(&file_path).ok();
1522    }
1523
1524    // ==================== Additional ValidationResult Tests ====================
1525
1526    #[test]
1527    fn test_validation_result_with_long_error_messages() {
1528        let errors = vec![
1529            "Error at /path/to/nested/property: expected integer but got string 'invalid'"
1530                .to_string(),
1531            "Error at /another/path: value must be between 1 and 100 but got 150".to_string(),
1532            "Error at /required/field: this field is required but was not provided".to_string(),
1533        ];
1534
1535        let result = ValidationResult::failure(
1536            "/path/to/config.yaml".to_string(),
1537            "test-config".to_string(),
1538            errors.clone(),
1539        );
1540
1541        assert!(!result.valid);
1542        assert_eq!(result.errors.len(), 3);
1543        assert_eq!(result.errors, errors);
1544    }
1545
1546    #[test]
1547    fn test_validation_result_with_special_characters_in_path() {
1548        let result = ValidationResult::success(
1549            "/path/with spaces/and-special_chars/config.yaml".to_string(),
1550            "mockforge-config".to_string(),
1551        );
1552
1553        assert!(result.valid);
1554        assert_eq!(result.file_path, "/path/with spaces/and-special_chars/config.yaml");
1555    }
1556
1557    // ==================== Schema Consistency Tests ====================
1558
1559    #[test]
1560    fn test_all_schemas_have_schema_key() {
1561        let schemas = generate_all_schemas();
1562
1563        for (name, schema) in schemas {
1564            let obj = schema.as_object().unwrap();
1565            assert!(obj.contains_key("$schema"), "Schema {} should have $schema key", name);
1566        }
1567    }
1568
1569    #[test]
1570    fn test_all_schemas_have_title() {
1571        let schemas = generate_all_schemas();
1572
1573        for (name, schema) in schemas {
1574            let obj = schema.as_object().unwrap();
1575            assert!(obj.contains_key("title"), "Schema {} should have title", name);
1576        }
1577    }
1578
1579    #[test]
1580    fn test_all_schemas_have_description() {
1581        let schemas = generate_all_schemas();
1582
1583        for (name, schema) in schemas {
1584            let obj = schema.as_object().unwrap();
1585            assert!(obj.contains_key("description"), "Schema {} should have description", name);
1586        }
1587    }
1588
1589    #[test]
1590    fn test_schema_titles_are_unique() {
1591        let schemas = generate_all_schemas();
1592        let mut titles = std::collections::HashSet::new();
1593
1594        for (_name, schema) in schemas {
1595            let obj = schema.as_object().unwrap();
1596            let title = obj.get("title").unwrap().as_str().unwrap();
1597            assert!(titles.insert(title.to_string()), "Duplicate title found: {}", title);
1598        }
1599    }
1600
1601    // ==================== Integration Tests ====================
1602
1603    #[test]
1604    fn test_end_to_end_config_validation_workflow() {
1605        // Generate schema
1606        let schemas = generate_all_schemas();
1607        let config_schema = schemas.get("mockforge-config").unwrap();
1608
1609        // Create a test file
1610        let temp_dir = std::env::temp_dir();
1611        let file_path = temp_dir.join("mockforge.yaml");
1612        let mut file = std::fs::File::create(&file_path).unwrap();
1613        writeln!(file, "port: 8080").unwrap();
1614
1615        // Detect schema type
1616        let detected_type = detect_schema_type(&file_path);
1617        assert_eq!(detected_type, Some("mockforge-config".to_string()));
1618
1619        // Validate
1620        let result = validate_config_file(&file_path, "mockforge-config", config_schema);
1621        assert!(result.is_ok());
1622
1623        std::fs::remove_file(&file_path).ok();
1624    }
1625
1626    #[test]
1627    fn test_blueprint_validation_workflow() {
1628        let schemas = generate_all_schemas();
1629        let blueprint_schema = schemas.get("blueprint-config").unwrap();
1630
1631        let temp_dir = std::env::temp_dir();
1632        let file_path = temp_dir.join("blueprint.yaml");
1633        let mut file = std::fs::File::create(&file_path).unwrap();
1634        writeln!(file, "manifest_version: '1.0'").unwrap();
1635        writeln!(file, "name: test-blueprint").unwrap();
1636        writeln!(file, "version: 1.0.0").unwrap();
1637        writeln!(file, "title: Test Blueprint").unwrap();
1638        writeln!(file, "description: A test blueprint").unwrap();
1639        writeln!(file, "author: Test Author").unwrap();
1640        writeln!(file, "category: saas").unwrap();
1641
1642        let detected_type = detect_schema_type(&file_path);
1643        assert_eq!(detected_type, Some("blueprint-config".to_string()));
1644
1645        let result =
1646            validate_config_file(&file_path, "blueprint-config", blueprint_schema).unwrap();
1647        assert!(result.valid, "Blueprint validation should succeed");
1648
1649        std::fs::remove_file(&file_path).ok();
1650    }
1651
1652    #[test]
1653    fn test_validate_yaml_with_comments() {
1654        let schema = serde_json::json!({
1655            "type": "object",
1656            "properties": {
1657                "name": { "type": "string" }
1658            }
1659        });
1660
1661        let temp_dir = std::env::temp_dir();
1662        let file_path = temp_dir.join("test_with_comments.yaml");
1663        let mut file = std::fs::File::create(&file_path).unwrap();
1664        writeln!(file, "# This is a comment").unwrap();
1665        writeln!(file, "name: test-service  # inline comment").unwrap();
1666
1667        let result = validate_config_file(&file_path, "test-config", &schema).unwrap();
1668        assert!(result.valid);
1669
1670        std::fs::remove_file(&file_path).ok();
1671    }
1672
1673    #[test]
1674    fn test_validate_yaml_with_anchors_and_aliases() {
1675        let schema = serde_json::json!({
1676            "type": "object",
1677            "properties": {
1678                "config1": { "type": "object" },
1679                "config2": { "type": "object" }
1680            }
1681        });
1682
1683        let temp_dir = std::env::temp_dir();
1684        let file_path = temp_dir.join("test_anchors.yaml");
1685        let mut file = std::fs::File::create(&file_path).unwrap();
1686        writeln!(file, "config1: &defaults").unwrap();
1687        writeln!(file, "  key: value").unwrap();
1688        writeln!(file, "config2: *defaults").unwrap();
1689
1690        let result = validate_config_file(&file_path, "test-config", &schema).unwrap();
1691        assert!(result.valid);
1692
1693        std::fs::remove_file(&file_path).ok();
1694    }
1695}