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