Skip to main content

mockforge_runtime_daemon/
auto_generator.rs

1//! Auto-generator for creating mocks from 404 responses
2//!
3//! This module handles the automatic generation of mocks, types, client stubs,
4//! OpenAPI schema entries, and scenarios when a 404 is detected.
5
6use anyhow::{Context, Result};
7use serde_json::json;
8use std::path::PathBuf;
9use tracing::{debug, info, warn};
10
11use crate::config::RuntimeDaemonConfig;
12
13/// Auto-generator for creating mocks from 404s
14pub struct AutoGenerator {
15    /// Configuration
16    config: RuntimeDaemonConfig,
17    /// Base URL for the MockForge management API
18    management_api_url: String,
19}
20
21impl AutoGenerator {
22    /// Create a new auto-generator
23    pub fn new(config: RuntimeDaemonConfig, management_api_url: String) -> Self {
24        Self {
25            config,
26            management_api_url,
27        }
28    }
29
30    /// Generate a mock from a 404 response
31    ///
32    /// This is the main entry point that orchestrates all the generation steps:
33    /// 1. Create mock endpoint with intelligent response
34    /// 2. Generate type (TypeScript/JSON schema)
35    /// 3. Generate client stub code
36    /// 4. Add to OpenAPI schema
37    /// 5. Add example response
38    /// 6. Set up basic scenario
39    pub async fn generate_mock_from_404(&self, method: &str, path: &str) -> Result<()> {
40        info!("Generating mock for {} {}", method, path);
41
42        // Step 1: Create basic mock endpoint with intelligent response
43        let mock_id = self.create_mock_endpoint(method, path).await?;
44        debug!("Created mock endpoint with ID: {}", mock_id);
45
46        // Step 2: Generate type (if enabled)
47        if self.config.generate_types {
48            if let Err(e) = self.generate_type(method, path).await {
49                warn!("Failed to generate type: {}", e);
50            }
51        }
52
53        // Step 3: Generate client stub (if enabled)
54        if self.config.generate_client_stubs {
55            if let Err(e) = self.generate_client_stub(method, path).await {
56                warn!("Failed to generate client stub: {}", e);
57            }
58        }
59
60        // Step 4: Update OpenAPI schema (if enabled)
61        if self.config.update_openapi {
62            if let Err(e) = self.update_openapi_schema(method, path).await {
63                warn!("Failed to update OpenAPI schema: {}", e);
64            }
65        }
66
67        // Step 5: Create scenario (if enabled)
68        if self.config.create_scenario {
69            if let Err(e) = self.create_scenario(method, path, &mock_id).await {
70                warn!("Failed to create scenario: {}", e);
71            }
72        }
73
74        info!("Completed mock generation for {} {}", method, path);
75        Ok(())
76    }
77
78    /// Create a mock endpoint with a basic intelligent response
79    async fn create_mock_endpoint(&self, method: &str, path: &str) -> Result<String> {
80        // Generate a basic response based on the path
81        let response_body = self.generate_intelligent_response(method, path).await?;
82
83        // Create mock configuration
84        let mock_config = json!({
85            "method": method,
86            "path": path,
87            "status_code": 200,
88            "body": response_body,
89            "name": format!("Auto-generated: {} {}", method, path),
90            "enabled": true,
91        });
92
93        // Call the management API to create the mock
94        let client = reqwest::Client::new();
95        let url = format!("{}/__mockforge/api/mocks", self.management_api_url);
96
97        let response = client
98            .post(&url)
99            .json(&mock_config)
100            .send()
101            .await
102            .context("Failed to send request to management API")?;
103
104        if !response.status().is_success() {
105            let status = response.status();
106            let text = response.text().await.unwrap_or_default();
107            anyhow::bail!("Management API returned {}: {}", status, text);
108        }
109
110        let created_mock: serde_json::Value =
111            response.json().await.context("Failed to parse response from management API")?;
112
113        let mock_id = created_mock
114            .get("id")
115            .and_then(|v| v.as_str())
116            .ok_or_else(|| anyhow::anyhow!("Response missing 'id' field"))?
117            .to_string();
118
119        Ok(mock_id)
120    }
121
122    /// Generate an intelligent response based on the method and path
123    async fn generate_intelligent_response(
124        &self,
125        method: &str,
126        path: &str,
127    ) -> Result<serde_json::Value> {
128        // Use AI generation if enabled
129        #[cfg(feature = "ai")]
130        if self.config.ai_generation {
131            return self.generate_ai_response(method, path).await;
132        }
133
134        // Fallback to pattern-based generation
135
136        // Infer entity type from path
137        let entity_type = self.infer_entity_type(path);
138
139        // Generate a basic response structure
140        let response = match method.to_uppercase().as_str() {
141            "GET" => {
142                // For GET, return a single object or array based on path
143                if path.ends_with('/')
144                    || path.split('/').next_back().unwrap_or("").parse::<u64>().is_err()
145                {
146                    // Looks like a collection endpoint
147                    json!([{
148                        "id": "{{uuid}}",
149                        "name": format!("Sample {}", entity_type),
150                        "created_at": "{{now}}",
151                    }])
152                } else {
153                    // Looks like a single resource endpoint
154                    json!({
155                        "id": path.split('/').next_back().unwrap_or("123"),
156                        "name": format!("Sample {}", entity_type),
157                        "created_at": "{{now}}",
158                    })
159                }
160            }
161            "POST" => {
162                // For POST, return the created resource
163                json!({
164                    "id": "{{uuid}}",
165                    "name": format!("New {}", entity_type),
166                    "created_at": "{{now}}",
167                    "status": "created",
168                })
169            }
170            "PUT" | "PATCH" => {
171                // For PUT/PATCH, return the updated resource
172                json!({
173                    "id": path.split('/').next_back().unwrap_or("123"),
174                    "name": format!("Updated {}", entity_type),
175                    "updated_at": "{{now}}",
176                })
177            }
178            "DELETE" => {
179                // For DELETE, return success status
180                json!({
181                    "success": true,
182                    "message": "Resource deleted",
183                })
184            }
185            _ => {
186                // Default response
187                json!({
188                    "message": "Auto-generated response",
189                    "method": method,
190                    "path": path,
191                })
192            }
193        };
194
195        Ok(response)
196    }
197
198    /// Generate AI-powered response using IntelligentMockGenerator
199    #[cfg(feature = "ai")]
200    async fn generate_ai_response(&self, method: &str, path: &str) -> Result<serde_json::Value> {
201        use mockforge_data::{IntelligentMockConfig, IntelligentMockGenerator, ResponseMode};
202        use std::collections::HashMap;
203
204        // Infer entity type and build a prompt
205        let entity_type = self.infer_entity_type(path);
206        let prompt = format!(
207            "Generate a realistic {} API response for {} {} endpoint. \
208            The response should be appropriate for a {} operation and include realistic data \
209            for a {} entity.",
210            entity_type, method, path, method, entity_type
211        );
212
213        // Build schema based on inferred entity type
214        let schema = self.build_schema_for_entity(&entity_type, method);
215
216        // Create intelligent mock config
217        let mut ai_config = IntelligentMockConfig::new(ResponseMode::Intelligent)
218            .with_prompt(prompt)
219            .with_schema(schema)
220            .with_count(1);
221
222        // Try to load RAG config from environment
223        if let Ok(rag_config) = self.load_rag_config_from_env() {
224            ai_config = ai_config.with_rag_config(rag_config);
225        }
226
227        // Create generator and generate response
228        let mut generator = IntelligentMockGenerator::new(ai_config)
229            .context("Failed to create intelligent mock generator")?;
230
231        let response = generator.generate().await.context("Failed to generate AI response")?;
232
233        info!("Generated AI-powered response for {} {}", method, path);
234        Ok(response)
235    }
236
237    /// Load RAG configuration from environment variables
238    #[cfg(feature = "ai")]
239    fn load_rag_config_from_env(&self) -> Result<mockforge_data::RagConfig> {
240        use mockforge_data::{EmbeddingProvider, LlmProvider, RagConfig};
241
242        // Try to determine provider from environment
243        let provider = std::env::var("MOCKFORGE_RAG_PROVIDER")
244            .unwrap_or_else(|_| "openai".to_string())
245            .to_lowercase();
246
247        let provider = match provider.as_str() {
248            "openai" => LlmProvider::OpenAI,
249            "anthropic" => LlmProvider::Anthropic,
250            "ollama" => LlmProvider::Ollama,
251            _ => LlmProvider::OpenAI,
252        };
253
254        let model =
255            std::env::var("MOCKFORGE_RAG_MODEL").unwrap_or_else(|_| "gpt-3.5-turbo".to_string());
256
257        let api_key = std::env::var("MOCKFORGE_RAG_API_KEY").ok();
258
259        let api_endpoint = std::env::var("MOCKFORGE_RAG_API_ENDPOINT")
260            .unwrap_or_else(|_| "https://api.openai.com/v1/chat/completions".to_string());
261
262        let mut config = RagConfig::default();
263        config.provider = provider;
264        config.model = model;
265        config.api_key = api_key;
266        config.api_endpoint = api_endpoint;
267
268        // Set embedding provider to match LLM provider
269        config.embedding_provider = match config.provider {
270            LlmProvider::OpenAI => EmbeddingProvider::OpenAI,
271            LlmProvider::Anthropic => EmbeddingProvider::OpenAI, // Anthropic doesn't have embeddings, use OpenAI
272            LlmProvider::Ollama => EmbeddingProvider::OpenAI, // Ollama embedding not yet supported, fallback to OpenAI
273            LlmProvider::OpenAICompatible => EmbeddingProvider::OpenAI,
274        };
275
276        Ok(config)
277    }
278
279    /// Build a basic JSON schema for an entity type
280    fn build_schema_for_entity(&self, entity_type: &str, method: &str) -> serde_json::Value {
281        let base_schema = json!({
282            "type": "object",
283            "properties": {
284                "id": {
285                    "type": "string",
286                    "format": "uuid"
287                },
288                "name": {
289                    "type": "string"
290                },
291                "created_at": {
292                    "type": "string",
293                    "format": "date-time"
294                }
295            },
296            "required": ["id", "name"]
297        });
298
299        // Adjust schema based on HTTP method
300        match method.to_uppercase().as_str() {
301            "GET" => {
302                // For GET, might be array or single object
303                if entity_type.ends_with('s') {
304                    json!({
305                        "type": "array",
306                        "items": base_schema
307                    })
308                } else {
309                    base_schema
310                }
311            }
312            "POST" => {
313                // For POST, add status field
314                let mut schema = base_schema.clone();
315                if let Some(props) = schema.get_mut("properties").and_then(|p| p.as_object_mut()) {
316                    props.insert(
317                        "status".to_string(),
318                        json!({
319                            "type": "string",
320                            "enum": ["created", "pending", "active"]
321                        }),
322                    );
323                }
324                schema
325            }
326            "PUT" | "PATCH" => {
327                // For PUT/PATCH, add updated_at
328                let mut schema = base_schema.clone();
329                if let Some(props) = schema.get_mut("properties").and_then(|p| p.as_object_mut()) {
330                    props.insert(
331                        "updated_at".to_string(),
332                        json!({
333                            "type": "string",
334                            "format": "date-time"
335                        }),
336                    );
337                }
338                schema
339            }
340            _ => base_schema,
341        }
342    }
343
344    /// Infer entity type from path
345    fn infer_entity_type(&self, path: &str) -> String {
346        // Extract entity type from path (e.g., /api/users -> "user")
347        let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
348
349        // Skip common API prefixes like "api", "v1", "v2", etc.
350        let skip_prefixes = ["api", "v1", "v2", "v3", "v4", "v5"];
351        let meaningful_parts: Vec<&str> = parts
352            .iter()
353            .skip_while(|part| skip_prefixes.contains(&part.to_lowercase().as_str()))
354            .copied()
355            .collect();
356
357        if meaningful_parts.is_empty() {
358            return "resource".to_string();
359        }
360
361        // If the last part is numeric (like an ID), use the second-to-last part instead
362        let candidate = if let Some(last_part) = meaningful_parts.last() {
363            // Check if last part is numeric (ID parameter)
364            if last_part.parse::<u64>().is_ok() || last_part.parse::<i64>().is_ok() {
365                // Use the second-to-last part if available
366                meaningful_parts.get(meaningful_parts.len().saturating_sub(2))
367            } else {
368                Some(last_part)
369            }
370        } else {
371            None
372        };
373
374        if let Some(part) = candidate {
375            // Remove common prefixes and pluralization
376            let entity = part
377                .trim_end_matches('s') // Remove plural 's'
378                .to_lowercase();
379
380            if !entity.is_empty() {
381                return entity;
382            }
383        }
384
385        "resource".to_string()
386    }
387
388    /// Generate a type (TypeScript/JSON schema) for the endpoint
389    async fn generate_type(&self, method: &str, path: &str) -> Result<()> {
390        use std::path::PathBuf;
391
392        // Determine output directory (use workspace_dir if configured, otherwise current dir)
393        let output_dir = if let Some(ref workspace_dir) = self.config.workspace_dir {
394            PathBuf::from(workspace_dir)
395        } else {
396            PathBuf::from(".")
397        };
398
399        // Create types directory if it doesn't exist
400        let types_dir = output_dir.join("types");
401        if !types_dir.exists() {
402            std::fs::create_dir_all(&types_dir).context("Failed to create types directory")?;
403        }
404
405        // Generate TypeScript type from the response schema
406        let entity_type = self.infer_entity_type(path);
407        let type_name = self.sanitize_type_name(&entity_type);
408
409        // Get the response schema we built earlier
410        let schema = self.build_schema_for_entity(&entity_type, method);
411
412        // Generate TypeScript interface
413        let ts_type = self.generate_typescript_interface(&type_name, &schema, method)?;
414
415        // Write TypeScript type file
416        let ts_file = types_dir.join(format!("{}.ts", type_name.to_lowercase()));
417        std::fs::write(&ts_file, ts_type).context("Failed to write TypeScript type file")?;
418
419        // Also generate JSON schema
420        let json_schema = self.generate_json_schema(&type_name, &schema)?;
421        let json_file = types_dir.join(format!("{}.schema.json", type_name.to_lowercase()));
422        std::fs::write(&json_file, serde_json::to_string_pretty(&json_schema)?)
423            .context("Failed to write JSON schema file")?;
424
425        info!(
426            "Generated types for {} {}: {} and {}.schema.json",
427            method,
428            path,
429            ts_file.display(),
430            json_file.display()
431        );
432
433        Ok(())
434    }
435
436    /// Generate TypeScript interface from schema
437    fn generate_typescript_interface(
438        &self,
439        type_name: &str,
440        schema: &serde_json::Value,
441        method: &str,
442    ) -> Result<String> {
443        let mut code = String::new();
444        code.push_str(&format!("// Generated TypeScript type for {} {}\n", method, type_name));
445        code.push_str("// Auto-generated by MockForge Runtime Daemon\n\n");
446
447        // Determine if it's an array or object
448        let schema_type = schema.get("type").and_then(|v| v.as_str()).unwrap_or("object");
449
450        if schema_type == "array" {
451            // Generate array type
452            if let Some(items) = schema.get("items") {
453                let item_type_name = format!("{}Item", type_name);
454                code.push_str(&self.generate_typescript_interface(
455                    &item_type_name,
456                    items,
457                    method,
458                )?);
459                code.push_str(&format!("export type {} = {}[];\n", type_name, item_type_name));
460            } else {
461                code.push_str(&format!("export type {} = any[];\n", type_name));
462            }
463        } else {
464            // Generate interface
465            code.push_str(&format!("export interface {} {{\n", type_name));
466
467            if let Some(properties) = schema.get("properties").and_then(|p| p.as_object()) {
468                let required = schema
469                    .get("required")
470                    .and_then(|r| r.as_array())
471                    .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>())
472                    .unwrap_or_default();
473
474                for (prop_name, prop_schema) in properties {
475                    let prop_type = self.schema_value_to_typescript_type(prop_schema)?;
476                    let is_optional = !required.contains(&prop_name.as_str());
477                    let optional_marker = if is_optional { "?" } else { "" };
478
479                    code.push_str(&format!("  {}{}: {};\n", prop_name, optional_marker, prop_type));
480                }
481            }
482
483            code.push_str("}\n");
484        }
485
486        Ok(code)
487    }
488
489    /// Convert a JSON schema value to TypeScript type string
490    fn schema_value_to_typescript_type(&self, schema: &serde_json::Value) -> Result<String> {
491        let schema_type = schema.get("type").and_then(|v| v.as_str()).unwrap_or("any");
492
493        match schema_type {
494            "string" => {
495                // Check format
496                if let Some(format) = schema.get("format").and_then(|f| f.as_str()) {
497                    match format {
498                        "date-time" | "date" => Ok("string".to_string()),
499                        "uuid" => Ok("string".to_string()),
500                        _ => Ok("string".to_string()),
501                    }
502                } else {
503                    Ok("string".to_string())
504                }
505            }
506            "integer" | "number" => Ok("number".to_string()),
507            "boolean" => Ok("boolean".to_string()),
508            "array" => {
509                if let Some(items) = schema.get("items") {
510                    let item_type = self.schema_value_to_typescript_type(items)?;
511                    Ok(format!("{}[]", item_type))
512                } else {
513                    Ok("any[]".to_string())
514                }
515            }
516            "object" => {
517                if schema.get("properties").is_some() {
518                    // Inline object type
519                    Ok("Record<string, any>".to_string())
520                } else {
521                    Ok("Record<string, any>".to_string())
522                }
523            }
524            _ => Ok("any".to_string()),
525        }
526    }
527
528    /// Generate JSON schema from the type
529    fn generate_json_schema(
530        &self,
531        type_name: &str,
532        schema: &serde_json::Value,
533    ) -> Result<serde_json::Value> {
534        let mut json_schema = json!({
535            "$schema": "http://json-schema.org/draft-07/schema#",
536            "title": type_name,
537            "type": schema.get("type").unwrap_or(&json!("object")),
538        });
539
540        if let Some(properties) = schema.get("properties") {
541            json_schema["properties"] = properties.clone();
542        }
543
544        if let Some(required) = schema.get("required") {
545            json_schema["required"] = required.clone();
546        }
547
548        Ok(json_schema)
549    }
550
551    /// Sanitize a name to be a valid TypeScript type name
552    fn sanitize_type_name(&self, name: &str) -> String {
553        let mut result = String::new();
554        let mut capitalize_next = true;
555
556        for ch in name.chars() {
557            match ch {
558                '-' | '_' | ' ' => capitalize_next = true,
559                ch if ch.is_alphanumeric() => {
560                    if capitalize_next {
561                        result.push(ch.to_uppercase().next().unwrap_or(ch));
562                        capitalize_next = false;
563                    } else {
564                        result.push(ch);
565                    }
566                }
567                _ => {}
568            }
569        }
570
571        if result.is_empty() {
572            "Resource".to_string()
573        } else {
574            // Ensure first character is uppercase
575            let mut chars = result.chars();
576            if let Some(first) = chars.next() {
577                format!("{}{}", first.to_uppercase(), chars.as_str())
578            } else {
579                "Resource".to_string()
580            }
581        }
582    }
583
584    /// Generate a client stub for the endpoint
585    async fn generate_client_stub(&self, method: &str, path: &str) -> Result<()> {
586        use std::path::PathBuf;
587        use tokio::fs;
588
589        // Determine output directory
590        let output_dir = if let Some(ref workspace_dir) = self.config.workspace_dir {
591            PathBuf::from(workspace_dir)
592        } else {
593            PathBuf::from(".")
594        };
595
596        // Create client-stubs directory if it doesn't exist
597        let stubs_dir = output_dir.join("client-stubs");
598        if !stubs_dir.exists() {
599            fs::create_dir_all(&stubs_dir)
600                .await
601                .context("Failed to create client-stubs directory")?;
602        }
603
604        // Generate client stub code
605        let entity_type = self.infer_entity_type(path);
606        let function_name = self.generate_function_name(method, path);
607        let stub_code =
608            self.generate_client_stub_code(method, path, &function_name, &entity_type)?;
609
610        // Write TypeScript client stub file
611        let stub_file = stubs_dir.join(format!("{}.ts", function_name.to_lowercase()));
612        fs::write(&stub_file, stub_code)
613            .await
614            .context("Failed to write client stub file")?;
615
616        info!("Generated client stub for {} {}: {}", method, path, stub_file.display());
617        Ok(())
618    }
619
620    /// Generate function name from method and path
621    fn generate_function_name(&self, method: &str, path: &str) -> String {
622        let entity_type = self.infer_entity_type(path);
623        let method_prefix = match method.to_uppercase().as_str() {
624            "GET" => "get",
625            "POST" => "create",
626            "PUT" => "update",
627            "PATCH" => "patch",
628            "DELETE" => "delete",
629            _ => "call",
630        };
631
632        // Check if path has an ID parameter (single resource)
633        let has_id = path
634            .split('/')
635            .any(|segment| segment.starts_with('{') && segment.ends_with('}'));
636
637        if has_id && method.to_uppercase() == "GET" {
638            format!("{}{}", method_prefix, self.sanitize_type_name(&entity_type))
639        } else if method.to_uppercase() == "GET" {
640            format!("list{}s", self.sanitize_type_name(&entity_type))
641        } else {
642            format!("{}{}", method_prefix, self.sanitize_type_name(&entity_type))
643        }
644    }
645
646    /// Generate client stub TypeScript code
647    fn generate_client_stub_code(
648        &self,
649        method: &str,
650        path: &str,
651        function_name: &str,
652        entity_type: &str,
653    ) -> Result<String> {
654        let method_upper = method.to_uppercase();
655        let type_name = self.sanitize_type_name(entity_type);
656
657        // Extract path parameters
658        let path_params: Vec<String> = path
659            .split('/')
660            .filter_map(|segment| {
661                if segment.starts_with('{') && segment.ends_with('}') {
662                    Some(segment.trim_matches(|c| c == '{' || c == '}').to_string())
663                } else {
664                    None
665                }
666            })
667            .collect();
668
669        // Build function parameters
670        let mut params = String::new();
671        if !path_params.is_empty() {
672            for param in &path_params {
673                params.push_str(&format!("{}: string", param));
674                params.push_str(", ");
675            }
676        }
677
678        // Add request body parameter for POST/PUT/PATCH
679        if matches!(method_upper.as_str(), "POST" | "PUT" | "PATCH") {
680            params.push_str(&format!("data?: Partial<{}>", type_name));
681        }
682
683        // Add query parameters for GET
684        if method_upper == "GET" {
685            if !params.is_empty() {
686                params.push_str(", ");
687            }
688            params.push_str("queryParams?: Record<string, any>");
689        }
690
691        // Build endpoint path with template literals
692        let mut endpoint_path = path.to_string();
693        for param in &path_params {
694            endpoint_path =
695                endpoint_path.replace(&format!("{{{}}}", param), &format!("${{{}}}", param));
696        }
697
698        // Generate the stub code
699        let stub = format!(
700            r#"// Auto-generated client stub for {} {}
701// Generated by MockForge Runtime Daemon
702
703import type {{ {} }} from '../types/{}';
704
705/**
706 * {} {} endpoint
707 *
708 * @param {} - Request parameters
709 * @returns Promise resolving to {} response
710 */
711export async function {}({}): Promise<{}> {{
712  const endpoint = `{}`;
713  const url = `${{baseUrl}}${{endpoint}}`;
714
715  const response = await fetch(url, {{
716    method: '{}',
717    headers: {{
718      'Content-Type': 'application/json',
719      ...(headers || {{}}),
720    }},
721    {}{}
722  }});
723
724  if (!response.ok) {{
725    throw new Error(`Request failed: ${{response.status}} ${{response.statusText}}`);
726  }}
727
728  return response.json();
729}}
730
731/**
732 * Base URL configuration
733 * Override this to point to your API server
734 */
735export let baseUrl = 'http://localhost:3000';
736"#,
737            method,
738            path,
739            type_name,
740            entity_type.to_lowercase(),
741            method,
742            path,
743            if params.is_empty() {
744                "headers?: Record<string, string>"
745            } else {
746                &params
747            },
748            type_name,
749            function_name,
750            if params.is_empty() {
751                "headers?: Record<string, string>"
752            } else {
753                &params
754            },
755            type_name,
756            endpoint_path,
757            method_upper,
758            if matches!(method_upper.as_str(), "POST" | "PUT" | "PATCH") {
759                "body: JSON.stringify(data || {}),\n    ".to_string()
760            } else if method_upper == "GET" && !path_params.is_empty() {
761                format!("{}const queryString = queryParams ? '?' + new URLSearchParams(queryParams).toString() : '';\n  const urlWithQuery = url + queryString;\n  ",
762                    if !path_params.is_empty() { "" } else { "" })
763            } else {
764                String::new()
765            },
766            if method_upper == "GET" && !path_params.is_empty() {
767                "url: urlWithQuery,\n    ".to_string()
768            } else {
769                String::new()
770            }
771        );
772
773        Ok(stub)
774    }
775
776    /// Update the OpenAPI schema with the new endpoint
777    async fn update_openapi_schema(&self, method: &str, path: &str) -> Result<()> {
778        use mockforge_core::openapi::OpenApiSpec;
779
780        // Determine OpenAPI spec file path
781        let spec_path = self.find_or_create_openapi_spec_path().await?;
782
783        // Load existing spec or create new one
784        let mut spec = if spec_path.exists() {
785            OpenApiSpec::from_file(&spec_path)
786                .await
787                .context("Failed to load existing OpenAPI spec")?
788        } else {
789            // Create a new OpenAPI spec
790            self.create_new_openapi_spec().await?
791        };
792
793        // Add the new endpoint to the spec
794        self.add_endpoint_to_spec(&mut spec, method, path).await?;
795
796        // Save the updated spec
797        self.save_openapi_spec(&spec, &spec_path).await?;
798
799        info!("Updated OpenAPI schema at {} with {} {}", spec_path.display(), method, path);
800        Ok(())
801    }
802
803    /// Find existing OpenAPI spec file or determine where to create one
804    async fn find_or_create_openapi_spec_path(&self) -> Result<PathBuf> {
805        use std::path::PathBuf;
806
807        // Check common locations
808        let possible_paths = vec![
809            PathBuf::from("openapi.yaml"),
810            PathBuf::from("openapi.yml"),
811            PathBuf::from("openapi.json"),
812            PathBuf::from("api.yaml"),
813            PathBuf::from("api.yml"),
814            PathBuf::from("api.json"),
815        ];
816
817        // Also check in workspace directory if configured
818        let mut all_paths = possible_paths.clone();
819        if let Some(ref workspace_dir) = self.config.workspace_dir {
820            for path in possible_paths {
821                all_paths.push(PathBuf::from(workspace_dir).join(path));
822            }
823        }
824
825        // Find first existing spec file
826        for path in &all_paths {
827            if path.exists() {
828                return Ok(path.clone());
829            }
830        }
831
832        // If none found, use default location (workspace_dir or current dir)
833        let default_path = if let Some(ref workspace_dir) = self.config.workspace_dir {
834            PathBuf::from(workspace_dir).join("openapi.yaml")
835        } else {
836            PathBuf::from("openapi.yaml")
837        };
838
839        Ok(default_path)
840    }
841
842    /// Create a new OpenAPI spec
843    async fn create_new_openapi_spec(&self) -> Result<mockforge_core::openapi::OpenApiSpec> {
844        use mockforge_core::openapi::OpenApiSpec;
845        use serde_json::json;
846
847        let spec_json = json!({
848            "openapi": "3.0.3",
849            "info": {
850                "title": "Auto-generated API",
851                "version": "1.0.0",
852                "description": "API specification auto-generated by MockForge Runtime Daemon"
853            },
854            "paths": {},
855            "components": {
856                "schemas": {}
857            }
858        });
859
860        OpenApiSpec::from_json(spec_json).context("Failed to create new OpenAPI spec")
861    }
862
863    /// Add an endpoint to the OpenAPI spec
864    async fn add_endpoint_to_spec(
865        &self,
866        spec: &mut mockforge_core::openapi::OpenApiSpec,
867        method: &str,
868        path: &str,
869    ) -> Result<()> {
870        // Get the raw document to modify
871        let mut spec_json = spec
872            .raw_document
873            .clone()
874            .ok_or_else(|| anyhow::anyhow!("OpenAPI spec missing raw document"))?;
875
876        // Ensure paths object exists
877        if spec_json.get("paths").is_none() {
878            spec_json["paths"] = json!({});
879        }
880
881        let paths = spec_json
882            .get_mut("paths")
883            .and_then(|p| p.as_object_mut())
884            .ok_or_else(|| anyhow::anyhow!("Failed to get paths object"))?;
885
886        // Get or create path item
887        let path_entry = paths.entry(path.to_string()).or_insert_with(|| json!({}));
888
889        // Convert method to lowercase for OpenAPI
890        let method_lower = method.to_lowercase();
891
892        // Create operation
893        let operation = json!({
894            "summary": format!("Auto-generated {} endpoint", method),
895            "description": format!("Endpoint auto-generated by MockForge Runtime Daemon for {} {}", method, path),
896            "operationId": self.generate_operation_id(method, path),
897            "responses": {
898                "200": {
899                    "description": "Successful response",
900                    "content": {
901                        "application/json": {
902                            "schema": self.build_schema_for_entity(&self.infer_entity_type(path), method)
903                        }
904                    }
905                }
906            }
907        });
908
909        // Add the operation to the path
910        path_entry[method_lower] = operation;
911
912        // Reload the spec from the updated JSON
913        *spec = mockforge_core::openapi::OpenApiSpec::from_json(spec_json)
914            .context("Failed to reload OpenAPI spec after update")?;
915
916        Ok(())
917    }
918
919    /// Generate an operation ID from method and path
920    fn generate_operation_id(&self, method: &str, path: &str) -> String {
921        let entity_type = self.infer_entity_type(path);
922        let method_lower = method.to_lowercase();
923
924        // Convert path segments to camelCase
925        let path_parts: Vec<&str> =
926            path.split('/').filter(|s| !s.is_empty() && !s.starts_with('{')).collect();
927
928        if path_parts.is_empty() {
929            format!("{}_{}", method_lower, entity_type)
930        } else {
931            let mut op_id = String::new();
932            op_id.push_str(&method_lower);
933            for part in path_parts {
934                let mut chars = part.chars();
935                if let Some(first) = chars.next() {
936                    op_id.push(first.to_uppercase().next().unwrap_or(first));
937                    op_id.push_str(chars.as_str());
938                }
939            }
940            op_id
941        }
942    }
943
944    /// Save OpenAPI spec to file
945    async fn save_openapi_spec(
946        &self,
947        spec: &mockforge_core::openapi::OpenApiSpec,
948        path: &PathBuf,
949    ) -> Result<()> {
950        use tokio::fs;
951
952        let spec_json = spec
953            .raw_document
954            .clone()
955            .ok_or_else(|| anyhow::anyhow!("OpenAPI spec missing raw document"))?;
956
957        // Determine format based on file extension
958        let is_yaml = path
959            .extension()
960            .and_then(|s| s.to_str())
961            .map(|s| s == "yaml" || s == "yml")
962            .unwrap_or(false);
963
964        let content = if is_yaml {
965            serde_yaml::to_string(&spec_json).context("Failed to serialize OpenAPI spec to YAML")?
966        } else {
967            serde_json::to_string_pretty(&spec_json)
968                .context("Failed to serialize OpenAPI spec to JSON")?
969        };
970
971        // Create parent directory if it doesn't exist
972        if let Some(parent) = path.parent() {
973            fs::create_dir_all(parent)
974                .await
975                .context("Failed to create OpenAPI spec directory")?;
976        }
977
978        fs::write(path, content).await.context("Failed to write OpenAPI spec file")?;
979
980        Ok(())
981    }
982
983    /// Create a basic scenario for the endpoint
984    async fn create_scenario(&self, method: &str, path: &str, mock_id: &str) -> Result<()> {
985        use std::path::PathBuf;
986        use tokio::fs;
987
988        // Determine output directory
989        let output_dir = if let Some(ref workspace_dir) = self.config.workspace_dir {
990            PathBuf::from(workspace_dir)
991        } else {
992            PathBuf::from(".")
993        };
994
995        // Create scenarios directory if it doesn't exist
996        let scenarios_dir = output_dir.join("scenarios");
997        if !scenarios_dir.exists() {
998            fs::create_dir_all(&scenarios_dir)
999                .await
1000                .context("Failed to create scenarios directory")?;
1001        }
1002
1003        // Generate scenario name from endpoint
1004        let entity_type = self.infer_entity_type(path);
1005        let scenario_name = format!("auto-{}-{}", entity_type, method.to_lowercase());
1006        let scenario_dir = scenarios_dir.join(&scenario_name);
1007
1008        // Create scenario directory
1009        if !scenario_dir.exists() {
1010            fs::create_dir_all(&scenario_dir)
1011                .await
1012                .context("Failed to create scenario directory")?;
1013        }
1014
1015        // Generate scenario manifest
1016        let manifest = self.generate_scenario_manifest(&scenario_name, method, path, mock_id)?;
1017
1018        // Write scenario.yaml
1019        let manifest_path = scenario_dir.join("scenario.yaml");
1020        let manifest_yaml =
1021            serde_yaml::to_string(&manifest).context("Failed to serialize scenario manifest")?;
1022        fs::write(&manifest_path, manifest_yaml)
1023            .await
1024            .context("Failed to write scenario manifest")?;
1025
1026        // Create a basic config.yaml for the scenario
1027        let config = self.generate_scenario_config(method, path, mock_id)?;
1028        let config_path = scenario_dir.join("config.yaml");
1029        let config_yaml =
1030            serde_yaml::to_string(&config).context("Failed to serialize scenario config")?;
1031        fs::write(&config_path, config_yaml)
1032            .await
1033            .context("Failed to write scenario config")?;
1034
1035        info!("Created scenario '{}' at {}", scenario_name, scenario_dir.display());
1036        Ok(())
1037    }
1038
1039    /// Generate scenario manifest YAML structure
1040    fn generate_scenario_manifest(
1041        &self,
1042        scenario_name: &str,
1043        method: &str,
1044        path: &str,
1045        _mock_id: &str,
1046    ) -> Result<serde_json::Value> {
1047        use chrono::Utc;
1048
1049        let entity_type = self.infer_entity_type(path);
1050        let title = format!("Auto-generated {} {} Scenario", method, entity_type);
1051
1052        let manifest = json!({
1053            "manifest_version": "1.0",
1054            "name": scenario_name,
1055            "version": "1.0.0",
1056            "title": title,
1057            "description": format!(
1058                "Auto-generated scenario for {} {} endpoint. Created by MockForge Runtime Daemon.",
1059                method, path
1060            ),
1061            "author": "MockForge Runtime Daemon",
1062            "author_email": None::<String>,
1063            "category": "other",
1064            "tags": ["auto-generated", "runtime-daemon", entity_type],
1065            "compatibility": {
1066                "min_version": "0.3.0",
1067                "max_version": null,
1068                "required_features": [],
1069                "protocols": ["http"]
1070            },
1071            "files": [
1072                "scenario.yaml",
1073                "config.yaml"
1074            ],
1075            "readme": None::<String>,
1076            "example_usage": format!(
1077                "# Use this scenario\nmockforge scenario use {}\n\n# Start server\nmockforge serve --config config.yaml",
1078                scenario_name
1079            ),
1080            "required_features": [],
1081            "plugin_dependencies": [],
1082            "metadata": {
1083                "auto_generated": true,
1084                "endpoint": path,
1085                "method": method,
1086                "entity_type": entity_type
1087            },
1088            "created_at": Utc::now().to_rfc3339(),
1089            "updated_at": Utc::now().to_rfc3339()
1090        });
1091
1092        Ok(manifest)
1093    }
1094
1095    /// Generate scenario config YAML structure
1096    fn generate_scenario_config(
1097        &self,
1098        method: &str,
1099        path: &str,
1100        mock_id: &str,
1101    ) -> Result<serde_json::Value> {
1102        let entity_type = self.infer_entity_type(path);
1103        let response_body =
1104            serde_json::to_value(self.build_schema_for_entity(&entity_type, method))?;
1105
1106        let config = json!({
1107            "http": {
1108                "enabled": true,
1109                "port": 3000,
1110                "mocks": [
1111                    {
1112                        "id": mock_id,
1113                        "method": method,
1114                        "path": path,
1115                        "status_code": 200,
1116                        "body": response_body,
1117                        "name": format!("Auto-generated: {} {}", method, path),
1118                        "enabled": true
1119                    }
1120                ]
1121            }
1122        });
1123
1124        Ok(config)
1125    }
1126}
1127
1128#[cfg(test)]
1129mod tests {
1130    use super::*;
1131
1132    fn create_test_generator() -> AutoGenerator {
1133        let config = RuntimeDaemonConfig::default();
1134        AutoGenerator::new(config, "http://localhost:3000".to_string())
1135    }
1136
1137    // infer_entity_type tests
1138    #[test]
1139    fn test_infer_entity_type() {
1140        let generator = create_test_generator();
1141
1142        assert_eq!(generator.infer_entity_type("/api/users"), "user");
1143        assert_eq!(generator.infer_entity_type("/api/products"), "product");
1144        assert_eq!(generator.infer_entity_type("/api/orders/123"), "order");
1145        assert_eq!(generator.infer_entity_type("/api"), "resource");
1146    }
1147
1148    #[test]
1149    fn test_infer_entity_type_with_versions() {
1150        let generator = create_test_generator();
1151
1152        assert_eq!(generator.infer_entity_type("/v1/users"), "user");
1153        assert_eq!(generator.infer_entity_type("/v2/products"), "product");
1154        assert_eq!(generator.infer_entity_type("/api/v1/orders"), "order");
1155        assert_eq!(generator.infer_entity_type("/api/v3/items"), "item");
1156    }
1157
1158    #[test]
1159    fn test_infer_entity_type_nested_paths() {
1160        let generator = create_test_generator();
1161
1162        assert_eq!(generator.infer_entity_type("/api/users/123/orders"), "order");
1163        assert_eq!(generator.infer_entity_type("/api/stores/456/products"), "product");
1164    }
1165
1166    #[test]
1167    fn test_infer_entity_type_numeric_id() {
1168        let generator = create_test_generator();
1169
1170        assert_eq!(generator.infer_entity_type("/api/users/123"), "user");
1171        assert_eq!(generator.infer_entity_type("/api/products/99999"), "product");
1172    }
1173
1174    #[test]
1175    fn test_infer_entity_type_empty_path() {
1176        let generator = create_test_generator();
1177
1178        assert_eq!(generator.infer_entity_type("/"), "resource");
1179        assert_eq!(generator.infer_entity_type(""), "resource");
1180    }
1181
1182    // sanitize_type_name tests
1183    #[test]
1184    fn test_sanitize_type_name_basic() {
1185        let generator = create_test_generator();
1186
1187        assert_eq!(generator.sanitize_type_name("user"), "User");
1188        assert_eq!(generator.sanitize_type_name("product"), "Product");
1189    }
1190
1191    #[test]
1192    fn test_sanitize_type_name_with_hyphens() {
1193        let generator = create_test_generator();
1194
1195        assert_eq!(generator.sanitize_type_name("user-account"), "UserAccount");
1196        assert_eq!(generator.sanitize_type_name("product-item"), "ProductItem");
1197    }
1198
1199    #[test]
1200    fn test_sanitize_type_name_with_underscores() {
1201        let generator = create_test_generator();
1202
1203        assert_eq!(generator.sanitize_type_name("user_account"), "UserAccount");
1204        assert_eq!(generator.sanitize_type_name("product_item"), "ProductItem");
1205    }
1206
1207    #[test]
1208    fn test_sanitize_type_name_with_spaces() {
1209        let generator = create_test_generator();
1210
1211        assert_eq!(generator.sanitize_type_name("user account"), "UserAccount");
1212        assert_eq!(generator.sanitize_type_name("product item"), "ProductItem");
1213    }
1214
1215    #[test]
1216    fn test_sanitize_type_name_empty() {
1217        let generator = create_test_generator();
1218
1219        assert_eq!(generator.sanitize_type_name(""), "Resource");
1220    }
1221
1222    #[test]
1223    fn test_sanitize_type_name_special_chars() {
1224        let generator = create_test_generator();
1225
1226        // Special chars like @ and ! are stripped but don't trigger capitalization
1227        // Only -, _, and space trigger capitalization of the next char
1228        assert_eq!(generator.sanitize_type_name("user@123"), "User123");
1229        assert_eq!(generator.sanitize_type_name("product!item"), "Productitem");
1230        // With hyphen or underscore, capitalization is triggered
1231        assert_eq!(generator.sanitize_type_name("product-item"), "ProductItem");
1232    }
1233
1234    // generate_function_name tests
1235    #[test]
1236    fn test_generate_function_name_get_list() {
1237        let generator = create_test_generator();
1238
1239        assert_eq!(generator.generate_function_name("GET", "/api/users"), "listUsers");
1240        assert_eq!(generator.generate_function_name("GET", "/api/products"), "listProducts");
1241    }
1242
1243    #[test]
1244    fn test_generate_function_name_get_single() {
1245        let generator = create_test_generator();
1246
1247        // When path has {id} parameter, the entity is inferred from the path segment
1248        // The function extracts "id" as the entity type since it's the last non-numeric segment
1249        assert_eq!(generator.generate_function_name("GET", "/api/users/{id}"), "getId");
1250        assert_eq!(
1251            generator.generate_function_name("GET", "/api/products/{productId}"),
1252            "getProductid"
1253        );
1254    }
1255
1256    #[test]
1257    fn test_generate_function_name_post() {
1258        let generator = create_test_generator();
1259
1260        assert_eq!(generator.generate_function_name("POST", "/api/users"), "createUser");
1261        assert_eq!(generator.generate_function_name("POST", "/api/products"), "createProduct");
1262    }
1263
1264    #[test]
1265    fn test_generate_function_name_put() {
1266        let generator = create_test_generator();
1267
1268        // Entity type is inferred from the last path segment (which is {id})
1269        assert_eq!(generator.generate_function_name("PUT", "/api/users/{id}"), "updateId");
1270        // Without path params, uses entity type directly
1271        assert_eq!(generator.generate_function_name("PUT", "/api/users"), "updateUser");
1272    }
1273
1274    #[test]
1275    fn test_generate_function_name_patch() {
1276        let generator = create_test_generator();
1277
1278        // Entity type is inferred from the last path segment
1279        assert_eq!(generator.generate_function_name("PATCH", "/api/users/{id}"), "patchId");
1280        assert_eq!(generator.generate_function_name("PATCH", "/api/users"), "patchUser");
1281    }
1282
1283    #[test]
1284    fn test_generate_function_name_delete() {
1285        let generator = create_test_generator();
1286
1287        // Entity type is inferred from the last path segment
1288        assert_eq!(generator.generate_function_name("DELETE", "/api/users/{id}"), "deleteId");
1289        assert_eq!(generator.generate_function_name("DELETE", "/api/users"), "deleteUser");
1290    }
1291
1292    #[test]
1293    fn test_generate_function_name_unknown_method() {
1294        let generator = create_test_generator();
1295
1296        assert_eq!(generator.generate_function_name("OPTIONS", "/api/users"), "callUser");
1297    }
1298
1299    // generate_operation_id tests
1300    #[test]
1301    fn test_generate_operation_id_basic() {
1302        let generator = create_test_generator();
1303
1304        let op_id = generator.generate_operation_id("GET", "/api/users");
1305        assert!(op_id.starts_with("get"));
1306        assert!(op_id.contains("Api"));
1307        assert!(op_id.contains("Users"));
1308    }
1309
1310    #[test]
1311    fn test_generate_operation_id_post() {
1312        let generator = create_test_generator();
1313
1314        let op_id = generator.generate_operation_id("POST", "/api/users");
1315        assert!(op_id.starts_with("post"));
1316    }
1317
1318    #[test]
1319    fn test_generate_operation_id_with_id_param() {
1320        let generator = create_test_generator();
1321
1322        // Path params ({id}) should be excluded from operation ID
1323        let op_id = generator.generate_operation_id("GET", "/api/users/{id}");
1324        assert!(!op_id.contains("{"));
1325        assert!(!op_id.contains("}"));
1326    }
1327
1328    // schema_value_to_typescript_type tests
1329    #[test]
1330    fn test_schema_value_to_typescript_type_string() {
1331        let generator = create_test_generator();
1332
1333        let schema = serde_json::json!({"type": "string"});
1334        assert_eq!(generator.schema_value_to_typescript_type(&schema).unwrap(), "string");
1335    }
1336
1337    #[test]
1338    fn test_schema_value_to_typescript_type_integer() {
1339        let generator = create_test_generator();
1340
1341        let schema = serde_json::json!({"type": "integer"});
1342        assert_eq!(generator.schema_value_to_typescript_type(&schema).unwrap(), "number");
1343    }
1344
1345    #[test]
1346    fn test_schema_value_to_typescript_type_number() {
1347        let generator = create_test_generator();
1348
1349        let schema = serde_json::json!({"type": "number"});
1350        assert_eq!(generator.schema_value_to_typescript_type(&schema).unwrap(), "number");
1351    }
1352
1353    #[test]
1354    fn test_schema_value_to_typescript_type_boolean() {
1355        let generator = create_test_generator();
1356
1357        let schema = serde_json::json!({"type": "boolean"});
1358        assert_eq!(generator.schema_value_to_typescript_type(&schema).unwrap(), "boolean");
1359    }
1360
1361    #[test]
1362    fn test_schema_value_to_typescript_type_array() {
1363        let generator = create_test_generator();
1364
1365        let schema = serde_json::json!({
1366            "type": "array",
1367            "items": {"type": "string"}
1368        });
1369        assert_eq!(generator.schema_value_to_typescript_type(&schema).unwrap(), "string[]");
1370    }
1371
1372    #[test]
1373    fn test_schema_value_to_typescript_type_array_no_items() {
1374        let generator = create_test_generator();
1375
1376        let schema = serde_json::json!({"type": "array"});
1377        assert_eq!(generator.schema_value_to_typescript_type(&schema).unwrap(), "any[]");
1378    }
1379
1380    #[test]
1381    fn test_schema_value_to_typescript_type_object() {
1382        let generator = create_test_generator();
1383
1384        let schema = serde_json::json!({"type": "object"});
1385        assert_eq!(
1386            generator.schema_value_to_typescript_type(&schema).unwrap(),
1387            "Record<string, any>"
1388        );
1389    }
1390
1391    #[test]
1392    fn test_schema_value_to_typescript_type_unknown() {
1393        let generator = create_test_generator();
1394
1395        let schema = serde_json::json!({"type": "unknown_type"});
1396        assert_eq!(generator.schema_value_to_typescript_type(&schema).unwrap(), "any");
1397    }
1398
1399    #[test]
1400    fn test_schema_value_to_typescript_type_date_format() {
1401        let generator = create_test_generator();
1402
1403        let schema = serde_json::json!({"type": "string", "format": "date-time"});
1404        assert_eq!(generator.schema_value_to_typescript_type(&schema).unwrap(), "string");
1405    }
1406
1407    // build_schema_for_entity tests
1408    #[test]
1409    fn test_build_schema_for_entity_get_single() {
1410        let generator = create_test_generator();
1411
1412        let schema = generator.build_schema_for_entity("user", "GET");
1413        assert_eq!(schema["type"], "object");
1414        assert!(schema["properties"]["id"].is_object());
1415        assert!(schema["properties"]["name"].is_object());
1416    }
1417
1418    #[test]
1419    fn test_build_schema_for_entity_get_plural() {
1420        let generator = create_test_generator();
1421
1422        let schema = generator.build_schema_for_entity("users", "GET");
1423        assert_eq!(schema["type"], "array");
1424        assert!(schema["items"].is_object());
1425    }
1426
1427    #[test]
1428    fn test_build_schema_for_entity_post() {
1429        let generator = create_test_generator();
1430
1431        let schema = generator.build_schema_for_entity("user", "POST");
1432        assert_eq!(schema["type"], "object");
1433        assert!(schema["properties"]["status"].is_object());
1434    }
1435
1436    #[test]
1437    fn test_build_schema_for_entity_put() {
1438        let generator = create_test_generator();
1439
1440        let schema = generator.build_schema_for_entity("user", "PUT");
1441        assert_eq!(schema["type"], "object");
1442        assert!(schema["properties"]["updated_at"].is_object());
1443    }
1444
1445    #[test]
1446    fn test_build_schema_for_entity_patch() {
1447        let generator = create_test_generator();
1448
1449        let schema = generator.build_schema_for_entity("user", "PATCH");
1450        assert_eq!(schema["type"], "object");
1451        assert!(schema["properties"]["updated_at"].is_object());
1452    }
1453
1454    #[test]
1455    fn test_build_schema_for_entity_delete() {
1456        let generator = create_test_generator();
1457
1458        let schema = generator.build_schema_for_entity("user", "DELETE");
1459        assert_eq!(schema["type"], "object");
1460        // DELETE returns base schema
1461    }
1462
1463    // generate_intelligent_response tests
1464    #[tokio::test]
1465    async fn test_generate_intelligent_response_get_collection() {
1466        let generator = create_test_generator();
1467
1468        let response = generator.generate_intelligent_response("GET", "/api/users/").await.unwrap();
1469        assert!(response.is_array());
1470    }
1471
1472    #[tokio::test]
1473    async fn test_generate_intelligent_response_get_single() {
1474        let generator = create_test_generator();
1475
1476        let response =
1477            generator.generate_intelligent_response("GET", "/api/users/123").await.unwrap();
1478        assert!(response.is_object());
1479        assert_eq!(response["id"], "123");
1480    }
1481
1482    #[tokio::test]
1483    async fn test_generate_intelligent_response_post() {
1484        let generator = create_test_generator();
1485
1486        let response = generator.generate_intelligent_response("POST", "/api/users").await.unwrap();
1487        assert!(response.is_object());
1488        assert_eq!(response["status"], "created");
1489    }
1490
1491    #[tokio::test]
1492    async fn test_generate_intelligent_response_put() {
1493        let generator = create_test_generator();
1494
1495        let response =
1496            generator.generate_intelligent_response("PUT", "/api/users/123").await.unwrap();
1497        assert!(response.is_object());
1498        assert!(response["name"].as_str().unwrap().contains("Updated"));
1499    }
1500
1501    #[tokio::test]
1502    async fn test_generate_intelligent_response_delete() {
1503        let generator = create_test_generator();
1504
1505        let response = generator
1506            .generate_intelligent_response("DELETE", "/api/users/123")
1507            .await
1508            .unwrap();
1509        assert!(response.is_object());
1510        assert_eq!(response["success"], true);
1511    }
1512
1513    #[tokio::test]
1514    async fn test_generate_intelligent_response_unknown_method() {
1515        let generator = create_test_generator();
1516
1517        let response =
1518            generator.generate_intelligent_response("OPTIONS", "/api/users").await.unwrap();
1519        assert!(response.is_object());
1520        assert!(response["message"].is_string());
1521    }
1522
1523    // generate_typescript_interface tests
1524    #[test]
1525    fn test_generate_typescript_interface_object() {
1526        let generator = create_test_generator();
1527
1528        let schema = serde_json::json!({
1529            "type": "object",
1530            "properties": {
1531                "id": {"type": "string"},
1532                "name": {"type": "string"}
1533            },
1534            "required": ["id"]
1535        });
1536
1537        let ts_code = generator.generate_typescript_interface("User", &schema, "GET").unwrap();
1538        assert!(ts_code.contains("export interface User"));
1539        assert!(ts_code.contains("id: string"));
1540        assert!(ts_code.contains("name?: string")); // Optional since not in required
1541    }
1542
1543    #[test]
1544    fn test_generate_typescript_interface_array() {
1545        let generator = create_test_generator();
1546
1547        let schema = serde_json::json!({
1548            "type": "array",
1549            "items": {
1550                "type": "object",
1551                "properties": {
1552                    "id": {"type": "string"}
1553                }
1554            }
1555        });
1556
1557        let ts_code = generator.generate_typescript_interface("Users", &schema, "GET").unwrap();
1558        assert!(ts_code.contains("export type Users = UsersItem[]"));
1559    }
1560
1561    // generate_json_schema tests
1562    #[test]
1563    fn test_generate_json_schema() {
1564        let generator = create_test_generator();
1565
1566        let schema = serde_json::json!({
1567            "type": "object",
1568            "properties": {
1569                "id": {"type": "string"}
1570            },
1571            "required": ["id"]
1572        });
1573
1574        let json_schema = generator.generate_json_schema("User", &schema).unwrap();
1575        assert_eq!(json_schema["$schema"], "http://json-schema.org/draft-07/schema#");
1576        assert_eq!(json_schema["title"], "User");
1577        assert_eq!(json_schema["type"], "object");
1578    }
1579
1580    // generate_scenario_manifest tests
1581    #[test]
1582    fn test_generate_scenario_manifest() {
1583        let generator = create_test_generator();
1584
1585        let manifest = generator
1586            .generate_scenario_manifest("test-scenario", "GET", "/api/users", "mock-123")
1587            .unwrap();
1588
1589        assert_eq!(manifest["name"], "test-scenario");
1590        assert_eq!(manifest["manifest_version"], "1.0");
1591        assert!(manifest["metadata"]["auto_generated"].as_bool().unwrap());
1592    }
1593
1594    // generate_scenario_config tests
1595    #[test]
1596    fn test_generate_scenario_config() {
1597        let generator = create_test_generator();
1598
1599        let config = generator.generate_scenario_config("GET", "/api/users", "mock-123").unwrap();
1600
1601        assert!(config["http"]["enabled"].as_bool().unwrap());
1602        assert!(config["http"]["mocks"].is_array());
1603    }
1604
1605    // AutoGenerator creation tests
1606    #[test]
1607    fn test_auto_generator_new() {
1608        let config = RuntimeDaemonConfig::default();
1609        let generator = AutoGenerator::new(config, "http://localhost:3000".to_string());
1610        assert!(!generator.config.enabled);
1611        assert_eq!(generator.management_api_url, "http://localhost:3000");
1612    }
1613
1614    #[test]
1615    fn test_auto_generator_with_custom_config() {
1616        let config = RuntimeDaemonConfig {
1617            enabled: true,
1618            ai_generation: true,
1619            generate_types: true,
1620            generate_client_stubs: true,
1621            workspace_dir: Some("/tmp/workspace".to_string()),
1622            ..Default::default()
1623        };
1624        let generator = AutoGenerator::new(config, "http://api.example.com".to_string());
1625
1626        assert!(generator.config.enabled);
1627        assert!(generator.config.ai_generation);
1628        assert!(generator.config.generate_types);
1629        assert!(generator.config.generate_client_stubs);
1630        assert_eq!(generator.config.workspace_dir, Some("/tmp/workspace".to_string()));
1631    }
1632}