Skip to main content

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
1696    // ==================== Generated Schema Validation Tests ====================
1697
1698    #[test]
1699    fn test_config_schema_is_valid_draft7() {
1700        use jsonschema::{Draft, Validator as SchemaValidator};
1701
1702        let schema = generate_config_schema();
1703        // The generated schema should be a valid JSON Schema that can be compiled
1704        let result = SchemaValidator::options().with_draft(Draft::Draft7).build(&schema);
1705        assert!(result.is_ok(), "Config schema should be valid Draft 7: {:?}", result.err());
1706    }
1707
1708    #[test]
1709    fn test_reality_schema_is_valid_draft7() {
1710        use jsonschema::{Draft, Validator as SchemaValidator};
1711
1712        let schema = generate_reality_schema();
1713        let result = SchemaValidator::options().with_draft(Draft::Draft7).build(&schema);
1714        assert!(result.is_ok(), "Reality schema should be valid Draft 7: {:?}", result.err());
1715    }
1716
1717    #[test]
1718    fn test_persona_schema_is_valid_draft7() {
1719        use jsonschema::{Draft, Validator as SchemaValidator};
1720
1721        let schema = generate_persona_schema();
1722        let result = SchemaValidator::options().with_draft(Draft::Draft7).build(&schema);
1723        assert!(result.is_ok(), "Persona schema should be valid Draft 7: {:?}", result.err());
1724    }
1725
1726    #[test]
1727    fn test_blueprint_schema_is_valid_draft7() {
1728        use jsonschema::{Draft, Validator as SchemaValidator};
1729
1730        let schema = generate_blueprint_schema();
1731        let result = SchemaValidator::options().with_draft(Draft::Draft7).build(&schema);
1732        assert!(result.is_ok(), "Blueprint schema should be valid Draft 7: {:?}", result.err());
1733    }
1734
1735    #[test]
1736    fn test_blueprint_schema_rejects_missing_required_fields() {
1737        let schema = generate_blueprint_schema();
1738
1739        let temp_dir = std::env::temp_dir();
1740        let file_path = temp_dir.join("test_blueprint_missing.yaml");
1741        let mut file = std::fs::File::create(&file_path).unwrap();
1742        // Only provide one of the required fields
1743        writeln!(file, "name: incomplete-blueprint").unwrap();
1744
1745        let result = validate_config_file(&file_path, "blueprint-config", &schema).unwrap();
1746        assert!(!result.valid);
1747        // Should have errors about missing required fields
1748        assert!(
1749            !result.errors.is_empty(),
1750            "Should have validation errors for missing required fields"
1751        );
1752
1753        std::fs::remove_file(&file_path).ok();
1754    }
1755
1756    #[test]
1757    fn test_blueprint_schema_rejects_invalid_category() {
1758        let schema = generate_blueprint_schema();
1759
1760        let temp_dir = std::env::temp_dir();
1761        let file_path = temp_dir.join("test_blueprint_bad_cat.yaml");
1762        let mut file = std::fs::File::create(&file_path).unwrap();
1763        writeln!(file, "manifest_version: '1.0'").unwrap();
1764        writeln!(file, "name: test").unwrap();
1765        writeln!(file, "version: 1.0.0").unwrap();
1766        writeln!(file, "title: Test").unwrap();
1767        writeln!(file, "description: Desc").unwrap();
1768        writeln!(file, "author: Author").unwrap();
1769        writeln!(file, "category: invalid-category").unwrap();
1770
1771        let result = validate_config_file(&file_path, "blueprint-config", &schema).unwrap();
1772        assert!(!result.valid);
1773        let has_category_error = result.errors.iter().any(|e| e.contains("category"));
1774        assert!(
1775            has_category_error,
1776            "Should have error about invalid category, got: {:?}",
1777            result.errors
1778        );
1779
1780        std::fs::remove_file(&file_path).ok();
1781    }
1782
1783    #[test]
1784    fn test_validate_config_schema_against_empty_object() {
1785        let schema = generate_config_schema();
1786
1787        let temp_dir = std::env::temp_dir();
1788        let file_path = temp_dir.join("test_config_empty_obj.json");
1789        std::fs::write(&file_path, "{}").unwrap();
1790
1791        // Empty object may or may not be valid depending on schema -- but must not panic
1792        let result = validate_config_file(&file_path, "mockforge-config", &schema);
1793        assert!(result.is_ok());
1794
1795        std::fs::remove_file(&file_path).ok();
1796    }
1797
1798    #[test]
1799    fn test_validate_config_schema_against_wrong_type() {
1800        let schema = generate_config_schema();
1801
1802        let temp_dir = std::env::temp_dir();
1803        let file_path = temp_dir.join("test_config_wrong_type.json");
1804        // Pass an array instead of an object
1805        std::fs::write(&file_path, "[1, 2, 3]").unwrap();
1806
1807        let result = validate_config_file(&file_path, "mockforge-config", &schema).unwrap();
1808        assert!(!result.valid, "Array should not validate against config schema");
1809
1810        std::fs::remove_file(&file_path).ok();
1811    }
1812
1813    // ==================== oneOf / anyOf / allOf Schema Tests ====================
1814
1815    #[test]
1816    fn test_validate_oneof_schema_valid() {
1817        let schema = serde_json::json!({
1818            "$schema": "http://json-schema.org/draft-07/schema#",
1819            "type": "object",
1820            "properties": {
1821                "value": {
1822                    "oneOf": [
1823                        { "type": "string" },
1824                        { "type": "integer" }
1825                    ]
1826                }
1827            }
1828        });
1829
1830        let temp_dir = std::env::temp_dir();
1831
1832        // String value
1833        let file_path = temp_dir.join("test_oneof_string.json");
1834        std::fs::write(&file_path, r#"{"value": "hello"}"#).unwrap();
1835        let result = validate_config_file(&file_path, "test", &schema).unwrap();
1836        assert!(result.valid);
1837        std::fs::remove_file(&file_path).ok();
1838
1839        // Integer value
1840        let file_path = temp_dir.join("test_oneof_int.json");
1841        std::fs::write(&file_path, r#"{"value": 42}"#).unwrap();
1842        let result = validate_config_file(&file_path, "test", &schema).unwrap();
1843        assert!(result.valid);
1844        std::fs::remove_file(&file_path).ok();
1845    }
1846
1847    #[test]
1848    fn test_validate_oneof_schema_invalid() {
1849        let schema = serde_json::json!({
1850            "$schema": "http://json-schema.org/draft-07/schema#",
1851            "type": "object",
1852            "properties": {
1853                "value": {
1854                    "oneOf": [
1855                        { "type": "string" },
1856                        { "type": "integer" }
1857                    ]
1858                }
1859            }
1860        });
1861
1862        let temp_dir = std::env::temp_dir();
1863        let file_path = temp_dir.join("test_oneof_invalid.json");
1864        // Boolean matches neither string nor integer
1865        std::fs::write(&file_path, r#"{"value": true}"#).unwrap();
1866        let result = validate_config_file(&file_path, "test", &schema).unwrap();
1867        assert!(!result.valid);
1868        std::fs::remove_file(&file_path).ok();
1869    }
1870
1871    // ==================== Schema Structure Depth Tests ====================
1872
1873    #[test]
1874    fn test_config_schema_has_definitions_or_properties() {
1875        let schema = generate_config_schema();
1876        let obj = schema.as_object().unwrap();
1877
1878        // A generated schema from schemars should have either properties (inline)
1879        // or definitions (referenced). At least one must exist.
1880        let has_structure = obj.contains_key("properties")
1881            || obj.contains_key("definitions")
1882            || obj.contains_key("$ref");
1883        assert!(has_structure, "Config schema should have properties, definitions, or $ref");
1884    }
1885
1886    #[test]
1887    fn test_config_schema_serialization_roundtrip() {
1888        let schema = generate_config_schema();
1889
1890        // Serialize to JSON string and back
1891        let json_str = serde_json::to_string(&schema).unwrap();
1892        let reparsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1893        assert_eq!(schema, reparsed);
1894
1895        // Serialize to pretty JSON and back
1896        let pretty_str = serde_json::to_string_pretty(&schema).unwrap();
1897        let reparsed_pretty: serde_json::Value = serde_json::from_str(&pretty_str).unwrap();
1898        assert_eq!(schema, reparsed_pretty);
1899    }
1900
1901    #[test]
1902    fn test_detect_schema_type_empty_path() {
1903        // A path with no file name component
1904        let path = PathBuf::from("");
1905        let result = detect_schema_type(&path);
1906        assert!(result.is_none());
1907    }
1908
1909    #[test]
1910    fn test_validate_deeply_nested_yaml() {
1911        let schema = serde_json::json!({
1912            "$schema": "http://json-schema.org/draft-07/schema#",
1913            "type": "object",
1914            "properties": {
1915                "a": {
1916                    "type": "object",
1917                    "properties": {
1918                        "b": {
1919                            "type": "object",
1920                            "properties": {
1921                                "c": {
1922                                    "type": "object",
1923                                    "properties": {
1924                                        "value": { "type": "string" }
1925                                    },
1926                                    "required": ["value"]
1927                                }
1928                            }
1929                        }
1930                    }
1931                }
1932            }
1933        });
1934
1935        let temp_dir = std::env::temp_dir();
1936
1937        // Valid deeply nested
1938        let file_path = temp_dir.join("test_deep_valid.yaml");
1939        let mut file = std::fs::File::create(&file_path).unwrap();
1940        writeln!(file, "a:").unwrap();
1941        writeln!(file, "  b:").unwrap();
1942        writeln!(file, "    c:").unwrap();
1943        writeln!(file, "      value: deep").unwrap();
1944        let result = validate_config_file(&file_path, "test", &schema).unwrap();
1945        assert!(result.valid);
1946        std::fs::remove_file(&file_path).ok();
1947
1948        // Invalid deeply nested (missing required field)
1949        let file_path = temp_dir.join("test_deep_invalid.yaml");
1950        let mut file = std::fs::File::create(&file_path).unwrap();
1951        writeln!(file, "a:").unwrap();
1952        writeln!(file, "  b:").unwrap();
1953        writeln!(file, "    c:").unwrap();
1954        writeln!(file, "      other: wrong").unwrap();
1955        let result = validate_config_file(&file_path, "test", &schema).unwrap();
1956        assert!(!result.valid);
1957        std::fs::remove_file(&file_path).ok();
1958    }
1959
1960    #[test]
1961    fn test_validate_yaml_with_multiline_string() {
1962        let schema = serde_json::json!({
1963            "$schema": "http://json-schema.org/draft-07/schema#",
1964            "type": "object",
1965            "properties": {
1966                "description": { "type": "string" }
1967            }
1968        });
1969
1970        let temp_dir = std::env::temp_dir();
1971        let file_path = temp_dir.join("test_multiline.yaml");
1972        let mut file = std::fs::File::create(&file_path).unwrap();
1973        writeln!(file, "description: |").unwrap();
1974        writeln!(file, "  This is a multiline").unwrap();
1975        writeln!(file, "  description that spans").unwrap();
1976        writeln!(file, "  multiple lines.").unwrap();
1977
1978        let result = validate_config_file(&file_path, "test", &schema).unwrap();
1979        assert!(result.valid);
1980
1981        std::fs::remove_file(&file_path).ok();
1982    }
1983
1984    #[test]
1985    fn test_validate_yaml_with_numeric_string_coercion() {
1986        // YAML may coerce "true"/"false" strings to booleans and "123" to numbers
1987        let schema = serde_json::json!({
1988            "$schema": "http://json-schema.org/draft-07/schema#",
1989            "type": "object",
1990            "properties": {
1991                "flag": { "type": "boolean" }
1992            }
1993        });
1994
1995        let temp_dir = std::env::temp_dir();
1996        let file_path = temp_dir.join("test_yaml_coerce.yaml");
1997        let mut file = std::fs::File::create(&file_path).unwrap();
1998        // YAML parses bare `true` as boolean
1999        writeln!(file, "flag: true").unwrap();
2000
2001        let result = validate_config_file(&file_path, "test", &schema).unwrap();
2002        assert!(result.valid);
2003
2004        std::fs::remove_file(&file_path).ok();
2005    }
2006
2007    // ==================== Schema Generation: Valid JSON Structure ====================
2008
2009    #[test]
2010    fn test_config_schema_produces_valid_json_string() {
2011        let schema = generate_config_schema();
2012        let json_str = serde_json::to_string(&schema);
2013        assert!(json_str.is_ok(), "Schema must serialize to valid JSON");
2014        let json_str = json_str.unwrap();
2015        // Must be parseable back
2016        let reparsed: Result<serde_json::Value, _> = serde_json::from_str(&json_str);
2017        assert!(reparsed.is_ok(), "Serialized schema must be parseable JSON");
2018    }
2019
2020    #[test]
2021    fn test_reality_schema_produces_valid_json_string() {
2022        let schema = generate_reality_schema();
2023        let json_str = serde_json::to_string_pretty(&schema).unwrap();
2024        assert!(!json_str.is_empty());
2025        let reparsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
2026        assert_eq!(schema, reparsed);
2027    }
2028
2029    #[test]
2030    fn test_persona_schema_produces_valid_json_string() {
2031        let schema = generate_persona_schema();
2032        let json_str = serde_json::to_string_pretty(&schema).unwrap();
2033        assert!(!json_str.is_empty());
2034        let reparsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
2035        assert_eq!(schema, reparsed);
2036    }
2037
2038    #[test]
2039    fn test_blueprint_schema_produces_valid_json_string() {
2040        let schema = generate_blueprint_schema();
2041        let json_str = serde_json::to_string_pretty(&schema).unwrap();
2042        assert!(!json_str.is_empty());
2043        let reparsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
2044        assert_eq!(schema, reparsed);
2045    }
2046
2047    // ==================== Schema Validation: Edge Cases ====================
2048
2049    #[test]
2050    fn test_validate_empty_json_object() {
2051        let schema = serde_json::json!({
2052            "$schema": "http://json-schema.org/draft-07/schema#",
2053            "type": "object",
2054            "properties": {
2055                "name": { "type": "string" }
2056            }
2057        });
2058
2059        let temp_dir = std::env::temp_dir();
2060        let file_path = temp_dir.join("test_empty_obj_schema.json");
2061        std::fs::write(&file_path, "{}").unwrap();
2062
2063        let result = validate_config_file(&file_path, "test", &schema).unwrap();
2064        // Empty object should be valid when no fields are required
2065        assert!(result.valid);
2066
2067        std::fs::remove_file(&file_path).ok();
2068    }
2069
2070    #[test]
2071    fn test_validate_empty_json_object_with_required_fields() {
2072        let schema = serde_json::json!({
2073            "$schema": "http://json-schema.org/draft-07/schema#",
2074            "type": "object",
2075            "properties": {
2076                "name": { "type": "string" }
2077            },
2078            "required": ["name"]
2079        });
2080
2081        let temp_dir = std::env::temp_dir();
2082        let file_path = temp_dir.join("test_empty_obj_required.json");
2083        std::fs::write(&file_path, "{}").unwrap();
2084
2085        let result = validate_config_file(&file_path, "test", &schema).unwrap();
2086        assert!(!result.valid);
2087        assert!(!result.errors.is_empty());
2088
2089        std::fs::remove_file(&file_path).ok();
2090    }
2091
2092    #[test]
2093    fn test_validate_wrong_top_level_type_string() {
2094        let schema = serde_json::json!({
2095            "$schema": "http://json-schema.org/draft-07/schema#",
2096            "type": "object"
2097        });
2098
2099        let temp_dir = std::env::temp_dir();
2100        let file_path = temp_dir.join("test_wrong_type_str.json");
2101        std::fs::write(&file_path, r#""just a string""#).unwrap();
2102
2103        let result = validate_config_file(&file_path, "test", &schema).unwrap();
2104        assert!(!result.valid, "A string should not pass an object schema");
2105
2106        std::fs::remove_file(&file_path).ok();
2107    }
2108
2109    #[test]
2110    fn test_validate_wrong_top_level_type_number() {
2111        let schema = serde_json::json!({
2112            "$schema": "http://json-schema.org/draft-07/schema#",
2113            "type": "object"
2114        });
2115
2116        let temp_dir = std::env::temp_dir();
2117        let file_path = temp_dir.join("test_wrong_type_num.json");
2118        std::fs::write(&file_path, "42").unwrap();
2119
2120        let result = validate_config_file(&file_path, "test", &schema).unwrap();
2121        assert!(!result.valid, "A number should not pass an object schema");
2122
2123        std::fs::remove_file(&file_path).ok();
2124    }
2125
2126    #[test]
2127    fn test_validate_wrong_top_level_type_bool() {
2128        let schema = serde_json::json!({
2129            "$schema": "http://json-schema.org/draft-07/schema#",
2130            "type": "object"
2131        });
2132
2133        let temp_dir = std::env::temp_dir();
2134        let file_path = temp_dir.join("test_wrong_type_bool.json");
2135        std::fs::write(&file_path, "true").unwrap();
2136
2137        let result = validate_config_file(&file_path, "test", &schema).unwrap();
2138        assert!(!result.valid, "A boolean should not pass an object schema");
2139
2140        std::fs::remove_file(&file_path).ok();
2141    }
2142
2143    #[test]
2144    fn test_validate_null_json() {
2145        let schema = serde_json::json!({
2146            "$schema": "http://json-schema.org/draft-07/schema#",
2147            "type": "object"
2148        });
2149
2150        let temp_dir = std::env::temp_dir();
2151        let file_path = temp_dir.join("test_null_json.json");
2152        std::fs::write(&file_path, "null").unwrap();
2153
2154        let result = validate_config_file(&file_path, "test", &schema).unwrap();
2155        assert!(!result.valid, "null should not pass an object schema");
2156
2157        std::fs::remove_file(&file_path).ok();
2158    }
2159
2160    // ==================== Schema Validation: allOf ====================
2161
2162    #[test]
2163    fn test_validate_allof_schema_valid() {
2164        let schema = serde_json::json!({
2165            "$schema": "http://json-schema.org/draft-07/schema#",
2166            "allOf": [
2167                {
2168                    "type": "object",
2169                    "properties": { "name": { "type": "string" } },
2170                    "required": ["name"]
2171                },
2172                {
2173                    "type": "object",
2174                    "properties": { "age": { "type": "integer" } },
2175                    "required": ["age"]
2176                }
2177            ]
2178        });
2179
2180        let temp_dir = std::env::temp_dir();
2181        let file_path = temp_dir.join("test_allof_valid.json");
2182        std::fs::write(&file_path, r#"{"name": "Alice", "age": 30}"#).unwrap();
2183
2184        let result = validate_config_file(&file_path, "test", &schema).unwrap();
2185        assert!(result.valid);
2186
2187        std::fs::remove_file(&file_path).ok();
2188    }
2189
2190    #[test]
2191    fn test_validate_allof_schema_invalid() {
2192        let schema = serde_json::json!({
2193            "$schema": "http://json-schema.org/draft-07/schema#",
2194            "allOf": [
2195                {
2196                    "type": "object",
2197                    "properties": { "name": { "type": "string" } },
2198                    "required": ["name"]
2199                },
2200                {
2201                    "type": "object",
2202                    "properties": { "age": { "type": "integer" } },
2203                    "required": ["age"]
2204                }
2205            ]
2206        });
2207
2208        let temp_dir = std::env::temp_dir();
2209        let file_path = temp_dir.join("test_allof_invalid.json");
2210        // Missing "age"
2211        std::fs::write(&file_path, r#"{"name": "Alice"}"#).unwrap();
2212
2213        let result = validate_config_file(&file_path, "test", &schema).unwrap();
2214        assert!(!result.valid);
2215
2216        std::fs::remove_file(&file_path).ok();
2217    }
2218
2219    // ==================== Schema Validation: anyOf ====================
2220
2221    #[test]
2222    fn test_validate_anyof_schema_valid() {
2223        let schema = serde_json::json!({
2224            "$schema": "http://json-schema.org/draft-07/schema#",
2225            "type": "object",
2226            "properties": {
2227                "value": {
2228                    "anyOf": [
2229                        { "type": "string" },
2230                        { "type": "number" },
2231                        { "type": "boolean" }
2232                    ]
2233                }
2234            }
2235        });
2236
2237        let temp_dir = std::env::temp_dir();
2238
2239        // String
2240        let file_path = temp_dir.join("test_anyof_str.json");
2241        std::fs::write(&file_path, r#"{"value": "hello"}"#).unwrap();
2242        let result = validate_config_file(&file_path, "test", &schema).unwrap();
2243        assert!(result.valid);
2244        std::fs::remove_file(&file_path).ok();
2245
2246        // Number
2247        let file_path = temp_dir.join("test_anyof_num.json");
2248        std::fs::write(&file_path, r#"{"value": 3.14}"#).unwrap();
2249        let result = validate_config_file(&file_path, "test", &schema).unwrap();
2250        assert!(result.valid);
2251        std::fs::remove_file(&file_path).ok();
2252
2253        // Boolean
2254        let file_path = temp_dir.join("test_anyof_bool.json");
2255        std::fs::write(&file_path, r#"{"value": false}"#).unwrap();
2256        let result = validate_config_file(&file_path, "test", &schema).unwrap();
2257        assert!(result.valid);
2258        std::fs::remove_file(&file_path).ok();
2259    }
2260
2261    #[test]
2262    fn test_validate_anyof_schema_invalid() {
2263        let schema = serde_json::json!({
2264            "$schema": "http://json-schema.org/draft-07/schema#",
2265            "type": "object",
2266            "properties": {
2267                "value": {
2268                    "anyOf": [
2269                        { "type": "string" },
2270                        { "type": "integer" }
2271                    ]
2272                }
2273            }
2274        });
2275
2276        let temp_dir = std::env::temp_dir();
2277        let file_path = temp_dir.join("test_anyof_invalid.json");
2278        // Array matches neither string nor integer
2279        std::fs::write(&file_path, r#"{"value": [1, 2]}"#).unwrap();
2280        let result = validate_config_file(&file_path, "test", &schema).unwrap();
2281        assert!(!result.valid);
2282        std::fs::remove_file(&file_path).ok();
2283    }
2284
2285    // ==================== detect_schema_type: Additional Cases ====================
2286
2287    #[test]
2288    fn test_detect_schema_type_root_only() {
2289        let path = PathBuf::from("/");
2290        // Root path has no file name
2291        let result = detect_schema_type(&path);
2292        assert!(result.is_none());
2293    }
2294
2295    #[test]
2296    fn test_detect_schema_type_dot_file() {
2297        let path = PathBuf::from("/config/.hidden.yaml");
2298        // Not a known file name, not reality/persona in path, defaults to config
2299        assert_eq!(detect_schema_type(&path), Some("mockforge-config".to_string()));
2300    }
2301
2302    #[test]
2303    fn test_detect_schema_type_blueprint_in_path_not_filename() {
2304        // "blueprint" in directory path but file is not blueprint.yaml
2305        let path = PathBuf::from("/blueprints/myapp/config.yaml");
2306        // file_name is "config.yaml", path doesn't contain "reality" or "persona"
2307        assert_eq!(detect_schema_type(&path), Some("mockforge-config".to_string()));
2308    }
2309
2310    // ==================== Validation: Concurrent Temp File Safety ====================
2311
2312    #[test]
2313    fn test_validate_with_unique_temp_files() {
2314        // Ensure multiple validations don't collide on temp file names
2315        let schema = serde_json::json!({
2316            "$schema": "http://json-schema.org/draft-07/schema#",
2317            "type": "object",
2318            "properties": {
2319                "id": { "type": "integer" }
2320            },
2321            "required": ["id"]
2322        });
2323
2324        let temp_dir = std::env::temp_dir();
2325
2326        let file1 = temp_dir.join("test_concurrent_1.json");
2327        let file2 = temp_dir.join("test_concurrent_2.json");
2328
2329        std::fs::write(&file1, r#"{"id": 1}"#).unwrap();
2330        std::fs::write(&file2, r#"{"id": "not_int"}"#).unwrap();
2331
2332        let r1 = validate_config_file(&file1, "test", &schema).unwrap();
2333        let r2 = validate_config_file(&file2, "test", &schema).unwrap();
2334
2335        assert!(r1.valid);
2336        assert!(!r2.valid);
2337
2338        std::fs::remove_file(&file1).ok();
2339        std::fs::remove_file(&file2).ok();
2340    }
2341
2342    // ==================== Validation: String Length Constraints ====================
2343
2344    #[test]
2345    fn test_validate_string_length_constraints() {
2346        let schema = serde_json::json!({
2347            "$schema": "http://json-schema.org/draft-07/schema#",
2348            "type": "object",
2349            "properties": {
2350                "code": {
2351                    "type": "string",
2352                    "minLength": 3,
2353                    "maxLength": 10
2354                }
2355            }
2356        });
2357
2358        let temp_dir = std::env::temp_dir();
2359
2360        // Too short
2361        let file_path = temp_dir.join("test_strlen_short.json");
2362        std::fs::write(&file_path, r#"{"code": "ab"}"#).unwrap();
2363        let result = validate_config_file(&file_path, "test", &schema).unwrap();
2364        assert!(!result.valid);
2365        std::fs::remove_file(&file_path).ok();
2366
2367        // Valid length
2368        let file_path = temp_dir.join("test_strlen_ok.json");
2369        std::fs::write(&file_path, r#"{"code": "abc123"}"#).unwrap();
2370        let result = validate_config_file(&file_path, "test", &schema).unwrap();
2371        assert!(result.valid);
2372        std::fs::remove_file(&file_path).ok();
2373
2374        // Too long
2375        let file_path = temp_dir.join("test_strlen_long.json");
2376        std::fs::write(&file_path, r#"{"code": "waytoolongstring"}"#).unwrap();
2377        let result = validate_config_file(&file_path, "test", &schema).unwrap();
2378        assert!(!result.valid);
2379        std::fs::remove_file(&file_path).ok();
2380    }
2381
2382    // ==================== Generated Schema: Validates Known-Good Config ====================
2383
2384    #[test]
2385    fn test_blueprint_schema_validates_full_blueprint() {
2386        let schema = generate_blueprint_schema();
2387
2388        let temp_dir = std::env::temp_dir();
2389        let file_path = temp_dir.join("test_full_blueprint.json");
2390        let content = serde_json::json!({
2391            "manifest_version": "1.0",
2392            "name": "test-app",
2393            "version": "1.0.0",
2394            "title": "Test App Blueprint",
2395            "description": "A comprehensive test blueprint",
2396            "author": "Test Author",
2397            "author_email": "test@example.com",
2398            "category": "saas",
2399            "tags": ["test", "example"],
2400            "setup": {
2401                "personas": [
2402                    { "id": "admin", "name": "Admin User", "description": "Administrator" }
2403                ],
2404                "reality": {
2405                    "level": "moderate",
2406                    "description": "Balanced realism"
2407                },
2408                "flows": [
2409                    { "id": "login", "name": "Login Flow" }
2410                ],
2411                "scenarios": [
2412                    { "id": "happy", "name": "Happy Path", "type": "happy_path", "file": "scenarios/happy.yaml" }
2413                ],
2414                "playground": {
2415                    "enabled": true,
2416                    "collection_file": "playground/collection.json"
2417                }
2418            },
2419            "compatibility": {
2420                "min_version": "0.3.0",
2421                "protocols": ["http", "websocket"]
2422            },
2423            "files": ["mockforge.yaml", "scenarios/happy.yaml"]
2424        });
2425        std::fs::write(&file_path, serde_json::to_string_pretty(&content).unwrap()).unwrap();
2426
2427        let result = validate_config_file(&file_path, "blueprint-config", &schema).unwrap();
2428        assert!(result.valid, "Full blueprint should validate. Errors: {:?}", result.errors);
2429
2430        std::fs::remove_file(&file_path).ok();
2431    }
2432}