mockforge_recorder/
test_generation.rs

1//! AI-powered test generation from recorded API interactions
2//!
3//! This module provides functionality to automatically generate test cases
4//! from recorded API requests and responses using AI/LLM capabilities.
5
6use crate::models::{Protocol, RecordedRequest, RecordedResponse};
7use crate::query::{execute_query, QueryFilter};
8use crate::{RecorderDatabase, RecorderError, Result};
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use std::collections::HashMap;
12
13/// Test format to generate
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
15#[serde(rename_all = "snake_case")]
16pub enum TestFormat {
17    /// Rust test using reqwest
18    RustReqwest,
19    /// HTTP file format (.http)
20    HttpFile,
21    /// cURL commands
22    Curl,
23    /// Postman collection
24    Postman,
25    /// k6 load test script
26    K6,
27    /// Python pytest
28    PythonPytest,
29    /// JavaScript/TypeScript Jest
30    JavaScriptJest,
31    /// Go test
32    GoTest,
33    /// Ruby RSpec
34    RubyRspec,
35    /// Java JUnit
36    JavaJunit,
37    /// C# xUnit
38    CSharpXunit,
39}
40
41/// Test generation configuration
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct TestGenerationConfig {
44    /// Test format to generate
45    pub format: TestFormat,
46    /// Include assertions for response validation
47    pub include_assertions: bool,
48    /// Include response body validation
49    pub validate_body: bool,
50    /// Include status code validation
51    pub validate_status: bool,
52    /// Include header validation
53    pub validate_headers: bool,
54    /// Include timing assertions
55    pub validate_timing: bool,
56    /// Maximum duration threshold in ms
57    pub max_duration_ms: Option<u64>,
58    /// Test suite name
59    pub suite_name: String,
60    /// Base URL for generated tests
61    pub base_url: Option<String>,
62    /// Use AI to generate intelligent test descriptions
63    pub ai_descriptions: bool,
64    /// LLM provider configuration (optional)
65    pub llm_config: Option<LlmConfig>,
66    /// Group tests by endpoint
67    pub group_by_endpoint: bool,
68    /// Include setup/teardown code
69    pub include_setup_teardown: bool,
70    /// Generate test data fixtures using AI
71    pub generate_fixtures: bool,
72    /// Suggest edge cases using AI
73    pub suggest_edge_cases: bool,
74    /// Perform test gap analysis
75    pub analyze_test_gaps: bool,
76    /// Deduplicate similar tests
77    pub deduplicate_tests: bool,
78    /// Optimize test execution order
79    pub optimize_test_order: bool,
80}
81
82impl Default for TestGenerationConfig {
83    fn default() -> Self {
84        Self {
85            format: TestFormat::RustReqwest,
86            include_assertions: true,
87            validate_body: true,
88            validate_status: true,
89            validate_headers: false,
90            validate_timing: false,
91            max_duration_ms: None,
92            suite_name: "generated_tests".to_string(),
93            base_url: Some("http://localhost:3000".to_string()),
94            ai_descriptions: false,
95            llm_config: None,
96            group_by_endpoint: true,
97            include_setup_teardown: true,
98            generate_fixtures: false,
99            suggest_edge_cases: false,
100            analyze_test_gaps: false,
101            deduplicate_tests: false,
102            optimize_test_order: false,
103        }
104    }
105}
106
107/// LLM configuration for AI-powered test generation
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct LlmConfig {
110    /// LLM provider (openai, anthropic, ollama)
111    pub provider: String,
112    /// API endpoint
113    pub api_endpoint: String,
114    /// API key
115    pub api_key: Option<String>,
116    /// Model name
117    pub model: String,
118    /// Temperature for generation
119    pub temperature: f64,
120}
121
122impl Default for LlmConfig {
123    fn default() -> Self {
124        Self {
125            provider: "ollama".to_string(),
126            api_endpoint: "http://localhost:11434/api/generate".to_string(),
127            api_key: None,
128            model: "llama2".to_string(),
129            temperature: 0.3,
130        }
131    }
132}
133
134/// Generated test case
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct GeneratedTest {
137    /// Test name/identifier
138    pub name: String,
139    /// Test description
140    pub description: String,
141    /// Test code
142    pub code: String,
143    /// Original request ID
144    pub request_id: String,
145    /// Endpoint being tested
146    pub endpoint: String,
147    /// HTTP method
148    pub method: String,
149}
150
151/// Test generation result
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct TestGenerationResult {
154    /// Generated tests
155    pub tests: Vec<GeneratedTest>,
156    /// Test suite metadata
157    pub metadata: TestSuiteMetadata,
158    /// Full test file content
159    pub test_file: String,
160}
161
162/// Test suite metadata
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct TestSuiteMetadata {
165    /// Suite name
166    pub name: String,
167    /// Total number of tests
168    pub test_count: usize,
169    /// Number of endpoints covered
170    pub endpoint_count: usize,
171    /// Protocols covered
172    pub protocols: Vec<Protocol>,
173    /// Generation timestamp
174    pub generated_at: chrono::DateTime<chrono::Utc>,
175    /// Format used
176    pub format: TestFormat,
177    /// Generated fixtures (if enabled)
178    pub fixtures: Option<Vec<TestFixture>>,
179    /// Edge case suggestions (if enabled)
180    pub edge_cases: Option<Vec<EdgeCaseSuggestion>>,
181    /// Test gap analysis (if enabled)
182    pub gap_analysis: Option<TestGapAnalysis>,
183}
184
185/// Test data fixture generated by AI
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct TestFixture {
188    /// Fixture name
189    pub name: String,
190    /// Fixture description
191    pub description: String,
192    /// Fixture data in JSON format
193    pub data: Value,
194    /// Related endpoints
195    pub endpoints: Vec<String>,
196}
197
198/// Edge case suggestion
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct EdgeCaseSuggestion {
201    /// Endpoint being tested
202    pub endpoint: String,
203    /// HTTP method
204    pub method: String,
205    /// Edge case type
206    pub case_type: String,
207    /// Description of the edge case
208    pub description: String,
209    /// Suggested test input
210    pub suggested_input: Option<Value>,
211    /// Expected behavior
212    pub expected_behavior: String,
213    /// Priority (1-5, 5 being highest)
214    pub priority: u8,
215}
216
217/// Test gap analysis result
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct TestGapAnalysis {
220    /// Endpoints without tests
221    pub untested_endpoints: Vec<String>,
222    /// HTTP methods not covered per endpoint
223    pub missing_methods: HashMap<String, Vec<String>>,
224    /// Status codes not tested
225    pub missing_status_codes: HashMap<String, Vec<u16>>,
226    /// Common error scenarios not tested
227    pub missing_error_scenarios: Vec<String>,
228    /// Coverage percentage
229    pub coverage_percentage: f64,
230    /// Recommendations
231    pub recommendations: Vec<String>,
232}
233
234/// Test generator engine
235pub struct TestGenerator {
236    database: RecorderDatabase,
237    config: TestGenerationConfig,
238}
239
240impl TestGenerator {
241    /// Create a new test generator
242    pub fn new(database: RecorderDatabase, config: TestGenerationConfig) -> Self {
243        Self { database, config }
244    }
245
246    /// Create from an Arc<RecorderDatabase>
247    pub fn from_arc(
248        database: std::sync::Arc<RecorderDatabase>,
249        config: TestGenerationConfig,
250    ) -> Self {
251        Self {
252            database: (*database).clone(),
253            config,
254        }
255    }
256
257    /// Generate tests from a query filter
258    pub async fn generate_from_filter(&self, filter: QueryFilter) -> Result<TestGenerationResult> {
259        // Execute query to get recordings
260        let query_result = execute_query(&self.database, filter).await?;
261
262        if query_result.exchanges.is_empty() {
263            return Err(RecorderError::InvalidFilter(
264                "No recordings found matching the filter".to_string(),
265            ));
266        }
267
268        // Generate tests from exchanges
269        let mut tests = Vec::new();
270        let mut endpoints = std::collections::HashSet::new();
271        let mut protocols = std::collections::HashSet::new();
272
273        for exchange in &query_result.exchanges {
274            let request = &exchange.request;
275
276            // Skip exchanges without responses
277            let Some(response) = &exchange.response else {
278                continue;
279            };
280
281            endpoints.insert(format!("{} {}", request.method, request.path));
282            protocols.insert(request.protocol);
283
284            let test = self.generate_test_for_exchange(request, response).await?;
285            tests.push(test);
286        }
287
288        // Deduplicate tests if configured
289        if self.config.deduplicate_tests {
290            tests = self.deduplicate_tests(tests);
291        }
292
293        // Optimize test order if configured
294        if self.config.optimize_test_order {
295            tests = self.optimize_test_order(tests);
296        }
297
298        // Group tests by endpoint if configured
299        if self.config.group_by_endpoint {
300            tests.sort_by(|a, b| a.endpoint.cmp(&b.endpoint));
301        }
302
303        // Generate advanced AI features
304        let fixtures = if self.config.generate_fixtures {
305            Some(self.generate_test_fixtures(&query_result.exchanges).await?)
306        } else {
307            None
308        };
309
310        let edge_cases = if self.config.suggest_edge_cases {
311            Some(self.suggest_edge_cases(&query_result.exchanges).await?)
312        } else {
313            None
314        };
315
316        let gap_analysis = if self.config.analyze_test_gaps {
317            Some(self.analyze_test_gaps(&query_result.exchanges, &tests).await?)
318        } else {
319            None
320        };
321
322        // Generate full test file
323        let test_file = self.generate_test_file(&tests)?;
324
325        // Create metadata
326        let metadata = TestSuiteMetadata {
327            name: self.config.suite_name.clone(),
328            test_count: tests.len(),
329            endpoint_count: endpoints.len(),
330            protocols: protocols.into_iter().collect(),
331            generated_at: chrono::Utc::now(),
332            format: self.config.format.clone(),
333            fixtures,
334            edge_cases,
335            gap_analysis,
336        };
337
338        Ok(TestGenerationResult {
339            tests,
340            metadata,
341            test_file,
342        })
343    }
344
345    /// Generate a single test from an exchange
346    async fn generate_test_for_exchange(
347        &self,
348        request: &RecordedRequest,
349        response: &RecordedResponse,
350    ) -> Result<GeneratedTest> {
351        let test_name = self.generate_test_name(request);
352        let description = if self.config.ai_descriptions {
353            self.generate_ai_description(request, response).await?
354        } else {
355            format!("Test {} {}", request.method, request.path)
356        };
357
358        let code = match self.config.format {
359            TestFormat::RustReqwest => self.generate_rust_test(request, response)?,
360            TestFormat::HttpFile => self.generate_http_file(request, response)?,
361            TestFormat::Curl => self.generate_curl(request, response)?,
362            TestFormat::Postman => self.generate_postman(request, response)?,
363            TestFormat::K6 => self.generate_k6(request, response)?,
364            TestFormat::PythonPytest => self.generate_python_test(request, response)?,
365            TestFormat::JavaScriptJest => self.generate_javascript_test(request, response)?,
366            TestFormat::GoTest => self.generate_go_test(request, response)?,
367            TestFormat::RubyRspec => self.generate_ruby_test(request, response)?,
368            TestFormat::JavaJunit => self.generate_java_test(request, response)?,
369            TestFormat::CSharpXunit => self.generate_csharp_test(request, response)?,
370        };
371
372        Ok(GeneratedTest {
373            name: test_name,
374            description,
375            code,
376            request_id: request.id.clone(),
377            endpoint: request.path.clone(),
378            method: request.method.clone(),
379        })
380    }
381
382    /// Generate test name from request
383    fn generate_test_name(&self, request: &RecordedRequest) -> String {
384        let method = request.method.to_lowercase();
385        let path = request
386            .path
387            .trim_start_matches('/')
388            .replace(['/', '-'], "_")
389            .replace("{", "")
390            .replace("}", "");
391
392        format!("test_{}_{}", method, path)
393    }
394
395    /// Generate AI-powered test description
396    async fn generate_ai_description(
397        &self,
398        request: &RecordedRequest,
399        response: &RecordedResponse,
400    ) -> Result<String> {
401        if let Some(llm_config) = &self.config.llm_config {
402            // Use LLM to generate meaningful description
403            let prompt = format!(
404                "Generate a concise test description for this API call:\n\
405                Method: {}\n\
406                Path: {}\n\
407                Status: {}\n\
408                \n\
409                Describe what this endpoint does and what the test validates in one sentence.",
410                request.method, request.path, response.status_code
411            );
412
413            match self.call_llm(llm_config, &prompt).await {
414                Ok(description) => Ok(description),
415                Err(_) => Ok(format!("Test {} {}", request.method, request.path)),
416            }
417        } else {
418            Ok(format!("Test {} {}", request.method, request.path))
419        }
420    }
421
422    /// Call LLM for generation
423    async fn call_llm(&self, config: &LlmConfig, prompt: &str) -> Result<String> {
424        let client = reqwest::Client::new();
425
426        match config.provider.as_str() {
427            "ollama" => {
428                let body = serde_json::json!({
429                    "model": config.model,
430                    "prompt": prompt,
431                    "stream": false,
432                    "options": {
433                        "temperature": config.temperature
434                    }
435                });
436
437                let response = client
438                    .post(&config.api_endpoint)
439                    .json(&body)
440                    .send()
441                    .await
442                    .map_err(|e| RecorderError::Replay(format!("LLM request failed: {}", e)))?;
443
444                let result: Value = response.json().await.map_err(|e| {
445                    RecorderError::Replay(format!("Failed to parse JSON response: {}", e))
446                })?;
447
448                result
449                    .get("response")
450                    .and_then(|v| v.as_str())
451                    .map(|s| s.trim().to_string())
452                    .ok_or_else(|| RecorderError::Replay("Invalid LLM response".to_string()))
453            }
454            "openai" => {
455                let body = serde_json::json!({
456                    "model": config.model,
457                    "messages": [
458                        {"role": "system", "content": "You are a helpful assistant that generates concise test descriptions."},
459                        {"role": "user", "content": prompt}
460                    ],
461                    "temperature": config.temperature,
462                    "max_tokens": 100
463                });
464
465                let mut request_builder = client.post(&config.api_endpoint).json(&body);
466
467                if let Some(api_key) = &config.api_key {
468                    request_builder =
469                        request_builder.header("Authorization", format!("Bearer {}", api_key));
470                }
471
472                let response = request_builder
473                    .send()
474                    .await
475                    .map_err(|e| RecorderError::Replay(format!("LLM request failed: {}", e)))?;
476
477                let result: Value = response.json().await.map_err(|e| {
478                    RecorderError::Replay(format!("Failed to parse JSON response: {}", e))
479                })?;
480
481                result
482                    .get("choices")
483                    .and_then(|v| v.get(0))
484                    .and_then(|v| v.get("message"))
485                    .and_then(|v| v.get("content"))
486                    .and_then(|v| v.as_str())
487                    .map(|s| s.trim().to_string())
488                    .ok_or_else(|| RecorderError::Replay("Invalid LLM response".to_string()))
489            }
490            _ => {
491                Err(RecorderError::Replay(format!("Unsupported LLM provider: {}", config.provider)))
492            }
493        }
494    }
495
496    /// Generate Rust test code
497    fn generate_rust_test(
498        &self,
499        request: &RecordedRequest,
500        response: &RecordedResponse,
501    ) -> Result<String> {
502        let base_url = self.config.base_url.as_deref().unwrap_or("http://localhost:3000");
503        let url = format!("{}{}", base_url, request.path);
504
505        let mut code = String::new();
506        let test_name = self.generate_test_name(request);
507
508        code.push_str("#[tokio::test]\n");
509        code.push_str(&format!("async fn {}() {{\n", test_name));
510        code.push_str("    let client = reqwest::Client::new();\n");
511        code.push_str(&format!(
512            "    let response = client.{}(\"{}\")\n",
513            request.method.to_lowercase(),
514            url
515        ));
516
517        // Add headers
518        if let Ok(headers) = serde_json::from_str::<HashMap<String, String>>(&request.headers) {
519            for (key, value) in headers.iter() {
520                if key.to_lowercase() != "host" {
521                    code.push_str(&format!("        .header(\"{}\", \"{}\")\n", key, value));
522                }
523            }
524        }
525
526        // Add body if present
527        if let Some(body) = &request.body {
528            if !body.is_empty() {
529                code.push_str(&format!("        .body(r#\"{}\"#)\n", body));
530            }
531        }
532
533        code.push_str("        .send()\n");
534        code.push_str("        .await\n");
535        code.push_str("        .expect(\"Failed to send request\");\n\n");
536
537        // Add assertions
538        if self.config.validate_status {
539            code.push_str(&format!(
540                "    assert_eq!(response.status().as_u16(), {});\n",
541                response.status_code
542            ));
543        }
544
545        if self.config.validate_body && response.body.is_some() {
546            code.push_str(
547                "    let body = response.text().await.expect(\"Failed to read body\");\n",
548            );
549            if let Some(body) = &response.body {
550                // Try to parse as JSON for better validation
551                if let Ok(_json) = serde_json::from_str::<Value>(body) {
552                    code.push_str("    let json: serde_json::Value = serde_json::from_str(&body).expect(\"Invalid JSON\");\n");
553                    code.push_str("    // Validate response structure\n");
554                    code.push_str("    assert!(json.is_object() || json.is_array());\n");
555                }
556            }
557        }
558
559        if self.config.validate_timing {
560            if let Some(max_duration) = self.config.max_duration_ms {
561                code.push_str(&format!(
562                    "    // Note: Add timing validation (max {} ms)\n",
563                    max_duration
564                ));
565            }
566        }
567
568        code.push_str("}\n");
569
570        Ok(code)
571    }
572
573    /// Generate HTTP file format
574    fn generate_http_file(
575        &self,
576        request: &RecordedRequest,
577        _response: &RecordedResponse,
578    ) -> Result<String> {
579        let base_url = self.config.base_url.as_deref().unwrap_or("http://localhost:3000");
580        let mut code = String::new();
581
582        code.push_str(&format!("### {} {}\n", request.method, request.path));
583        code.push_str(&format!("{} {}{}\n", request.method, base_url, request.path));
584
585        // Add headers
586        if let Ok(headers) = serde_json::from_str::<HashMap<String, String>>(&request.headers) {
587            for (key, value) in headers.iter() {
588                if key.to_lowercase() != "host" {
589                    code.push_str(&format!("{}: {}\n", key, value));
590                }
591            }
592        }
593
594        // Add body
595        if let Some(body) = &request.body {
596            if !body.is_empty() {
597                code.push('\n');
598                code.push_str(body);
599                code.push('\n');
600            }
601        }
602
603        code.push('\n');
604        Ok(code)
605    }
606
607    /// Generate cURL command
608    fn generate_curl(
609        &self,
610        request: &RecordedRequest,
611        _response: &RecordedResponse,
612    ) -> Result<String> {
613        let base_url = self.config.base_url.as_deref().unwrap_or("http://localhost:3000");
614        let url = format!("{}{}", base_url, request.path);
615
616        let mut code = format!("curl -X {} '{}'", request.method, url);
617
618        // Add headers
619        if let Ok(headers) = serde_json::from_str::<HashMap<String, String>>(&request.headers) {
620            for (key, value) in headers.iter() {
621                if key.to_lowercase() != "host" {
622                    code.push_str(&format!(" \\\n  -H '{}: {}'", key, value));
623                }
624            }
625        }
626
627        // Add body
628        if let Some(body) = &request.body {
629            if !body.is_empty() {
630                let escaped_body = body.replace('\'', "'\\''");
631                code.push_str(&format!(" \\\n  -d '{}'", escaped_body));
632            }
633        }
634
635        Ok(code)
636    }
637
638    /// Generate Postman collection item
639    fn generate_postman(
640        &self,
641        request: &RecordedRequest,
642        _response: &RecordedResponse,
643    ) -> Result<String> {
644        let base_url = self.config.base_url.as_deref().unwrap_or("http://localhost:3000");
645
646        let mut headers_vec = Vec::new();
647        if let Ok(headers) = serde_json::from_str::<HashMap<String, String>>(&request.headers) {
648            for (key, value) in headers.iter() {
649                if key.to_lowercase() != "host" {
650                    headers_vec.push(serde_json::json!({
651                        "key": key,
652                        "value": value
653                    }));
654                }
655            }
656        }
657
658        let item = serde_json::json!({
659            "name": format!("{} {}", request.method, request.path),
660            "request": {
661                "method": request.method,
662                "header": headers_vec,
663                "url": {
664                    "raw": format!("{}{}", base_url, request.path),
665                    "protocol": "http",
666                    "host": ["localhost"],
667                    "port": "3000",
668                    "path": request.path.split('/').filter(|s| !s.is_empty()).collect::<Vec<_>>()
669                },
670                "body": if let Some(body) = &request.body {
671                    serde_json::json!({
672                        "mode": "raw",
673                        "raw": body
674                    })
675                } else {
676                    serde_json::json!({})
677                }
678            }
679        });
680
681        serde_json::to_string_pretty(&item).map_err(RecorderError::Serialization)
682    }
683
684    /// Generate k6 test script
685    fn generate_k6(
686        &self,
687        request: &RecordedRequest,
688        response: &RecordedResponse,
689    ) -> Result<String> {
690        let base_url = self.config.base_url.as_deref().unwrap_or("http://localhost:3000");
691        let url = format!("{}{}", base_url, request.path);
692
693        let mut code = String::new();
694        code.push_str(&format!("  // {} {}\n", request.method, request.path));
695        code.push_str("  {\n");
696
697        let method = request.method.to_lowercase();
698
699        // Build params object
700        code.push_str("    const params = {\n");
701        code.push_str("      headers: {\n");
702        if let Ok(headers) = serde_json::from_str::<HashMap<String, String>>(&request.headers) {
703            for (key, value) in headers.iter() {
704                if key.to_lowercase() != "host" {
705                    code.push_str(&format!("        '{}': '{}',\n", key, value));
706                }
707            }
708        }
709        code.push_str("      },\n");
710        code.push_str("    };\n");
711
712        // Make request
713        if let Some(body) = &request.body {
714            if !body.is_empty() {
715                code.push_str(&format!("    const payload = `{}`;\n", body));
716                code.push_str(&format!(
717                    "    const res = http.{}('{}', payload, params);\n",
718                    method, url
719                ));
720            } else {
721                code.push_str(&format!(
722                    "    const res = http.{}('{}', null, params);\n",
723                    method, url
724                ));
725            }
726        } else {
727            code.push_str(&format!("    const res = http.{}('{}', null, params);\n", method, url));
728        }
729
730        // Add checks
731        if self.config.validate_status {
732            code.push_str("    check(res, {\n");
733            code.push_str(&format!(
734                "      'status is {}': (r) => r.status === {},\n",
735                response.status_code, response.status_code
736            ));
737            code.push_str("    });\n");
738        }
739
740        code.push_str("  }\n");
741        Ok(code)
742    }
743
744    /// Generate Python pytest
745    fn generate_python_test(
746        &self,
747        request: &RecordedRequest,
748        response: &RecordedResponse,
749    ) -> Result<String> {
750        let base_url = self.config.base_url.as_deref().unwrap_or("http://localhost:3000");
751        let url = format!("{}{}", base_url, request.path);
752        let test_name = self.generate_test_name(request);
753
754        let mut code = String::new();
755        code.push_str(&format!("def {}():\n", test_name));
756
757        // Build headers
758        code.push_str("    headers = {\n");
759        if let Ok(headers) = serde_json::from_str::<HashMap<String, String>>(&request.headers) {
760            for (key, value) in headers.iter() {
761                if key.to_lowercase() != "host" {
762                    code.push_str(&format!("        '{}': '{}',\n", key, value));
763                }
764            }
765        }
766        code.push_str("    }\n");
767
768        // Make request
769        let method = request.method.to_lowercase();
770        if let Some(body) = &request.body {
771            if !body.is_empty() {
772                code.push_str(&format!("    data = r'''{}'''\n", body));
773                code.push_str(&format!(
774                    "    response = requests.{}('{}', headers=headers, data=data)\n",
775                    method, url
776                ));
777            } else {
778                code.push_str(&format!(
779                    "    response = requests.{}('{}', headers=headers)\n",
780                    method, url
781                ));
782            }
783        } else {
784            code.push_str(&format!(
785                "    response = requests.{}('{}', headers=headers)\n",
786                method, url
787            ));
788        }
789
790        // Assertions
791        if self.config.validate_status {
792            code.push_str(&format!(
793                "    assert response.status_code == {}\n",
794                response.status_code
795            ));
796        }
797
798        Ok(code)
799    }
800
801    /// Generate JavaScript/Jest test
802    fn generate_javascript_test(
803        &self,
804        request: &RecordedRequest,
805        response: &RecordedResponse,
806    ) -> Result<String> {
807        let base_url = self.config.base_url.as_deref().unwrap_or("http://localhost:3000");
808        let url = format!("{}{}", base_url, request.path);
809        let test_name = format!("{} {}", request.method, request.path);
810
811        let mut code = String::new();
812        code.push_str(&format!("test('{}', async () => {{\n", test_name));
813
814        // Build fetch options
815        code.push_str("  const options = {\n");
816        code.push_str(&format!("    method: '{}',\n", request.method));
817        code.push_str("    headers: {\n");
818        if let Ok(headers) = serde_json::from_str::<HashMap<String, String>>(&request.headers) {
819            for (key, value) in headers.iter() {
820                if key.to_lowercase() != "host" {
821                    code.push_str(&format!("      '{}': '{}',\n", key, value));
822                }
823            }
824        }
825        code.push_str("    },\n");
826
827        if let Some(body) = &request.body {
828            if !body.is_empty() {
829                code.push_str(&format!("    body: `{}`,\n", body));
830            }
831        }
832
833        code.push_str("  };\n");
834
835        code.push_str(&format!("  const response = await fetch('{}', options);\n", url));
836
837        if self.config.validate_status {
838            code.push_str(&format!("  expect(response.status).toBe({});\n", response.status_code));
839        }
840
841        if self.config.validate_body && response.body.is_some() {
842            code.push_str("  const data = await response.json();\n");
843            code.push_str("  expect(data).toBeDefined();\n");
844        }
845
846        code.push_str("});\n");
847        Ok(code)
848    }
849
850    /// Generate Go test
851    fn generate_go_test(
852        &self,
853        request: &RecordedRequest,
854        response: &RecordedResponse,
855    ) -> Result<String> {
856        let base_url = self.config.base_url.as_deref().unwrap_or("http://localhost:3000");
857        let url = format!("{}{}", base_url, request.path);
858        let test_name = self
859            .generate_test_name(request)
860            .split('_')
861            .map(|s| {
862                let mut c = s.chars();
863                match c.next() {
864                    None => String::new(),
865                    Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
866                }
867            })
868            .collect::<String>();
869
870        let mut code = String::new();
871        code.push_str(&format!("func {}(t *testing.T) {{\n", test_name));
872
873        // Create request
874        if let Some(body) = &request.body {
875            if !body.is_empty() {
876                code.push_str(&format!("    body := strings.NewReader(`{}`)\n", body));
877                code.push_str(&format!(
878                    "    req, err := http.NewRequest(\"{}\", \"{}\", body)\n",
879                    request.method, url
880                ));
881            } else {
882                code.push_str(&format!(
883                    "    req, err := http.NewRequest(\"{}\", \"{}\", nil)\n",
884                    request.method, url
885                ));
886            }
887        } else {
888            code.push_str(&format!(
889                "    req, err := http.NewRequest(\"{}\", \"{}\", nil)\n",
890                request.method, url
891            ));
892        }
893
894        code.push_str("    if err != nil {\n");
895        code.push_str("        t.Fatal(err)\n");
896        code.push_str("    }\n");
897
898        // Add headers
899        if let Ok(headers) = serde_json::from_str::<HashMap<String, String>>(&request.headers) {
900            for (key, value) in headers.iter() {
901                if key.to_lowercase() != "host" {
902                    code.push_str(&format!("    req.Header.Set(\"{}\", \"{}\")\n", key, value));
903                }
904            }
905        }
906
907        // Send request
908        code.push_str("    client := &http.Client{}\n");
909        code.push_str("    resp, err := client.Do(req)\n");
910        code.push_str("    if err != nil {\n");
911        code.push_str("        t.Fatal(err)\n");
912        code.push_str("    }\n");
913        code.push_str("    defer resp.Body.Close()\n\n");
914
915        // Assertions
916        if self.config.validate_status {
917            code.push_str(&format!("    if resp.StatusCode != {} {{\n", response.status_code));
918            code.push_str(&format!(
919                "        t.Errorf(\"Expected status {}, got %d\", resp.StatusCode)\n",
920                response.status_code
921            ));
922            code.push_str("    }\n");
923        }
924
925        code.push_str("}\n");
926        Ok(code)
927    }
928
929    /// Generate Ruby RSpec test
930    fn generate_ruby_test(
931        &self,
932        request: &RecordedRequest,
933        response: &RecordedResponse,
934    ) -> Result<String> {
935        let base_url = self.config.base_url.as_deref().unwrap_or("http://localhost:3000");
936        let url = format!("{}{}", base_url, request.path);
937        let test_name = request
938            .path
939            .trim_start_matches('/')
940            .replace(['/', '-'], " ")
941            .replace("{", "")
942            .replace("}", "");
943
944        let mut code = String::new();
945        code.push_str(&format!("  it \"should {} {}\" do\n", request.method, test_name));
946
947        // Build request parameters
948        let mut request_params = vec![format!("method: :{}", request.method.to_lowercase())];
949
950        // Add headers
951        if let Ok(headers) = serde_json::from_str::<HashMap<String, String>>(&request.headers) {
952            let header_items: Vec<String> = headers
953                .iter()
954                .filter(|(k, _)| k.to_lowercase() != "host")
955                .map(|(k, v)| format!("'{}' => '{}'", k, v))
956                .collect();
957            if !header_items.is_empty() {
958                request_params.push(format!("headers: {{ {} }}", header_items.join(", ")));
959            }
960        }
961
962        // Add body
963        if let Some(body) = &request.body {
964            if !body.is_empty() {
965                let escaped_body = body.replace('\'', "\\'").replace('\n', "\\n");
966                request_params.push(format!("body: '{}'", escaped_body));
967            }
968        }
969
970        code.push_str(&format!(
971            "    response = HTTParty.{}('{}', {})\n",
972            request.method.to_lowercase(),
973            url,
974            request_params.join(", ")
975        ));
976
977        // Add assertions
978        if self.config.validate_status {
979            code.push_str(&format!("    expect(response.code).to eq({})\n", response.status_code));
980        }
981
982        if self.config.validate_body && response.body.is_some() {
983            if let Some(body) = &response.body {
984                if serde_json::from_str::<Value>(body).is_ok() {
985                    code.push_str("    expect(response.parsed_response).not_to be_nil\n");
986                }
987            }
988        }
989
990        code.push_str("  end\n");
991        Ok(code)
992    }
993
994    /// Generate Java JUnit test
995    fn generate_java_test(
996        &self,
997        request: &RecordedRequest,
998        response: &RecordedResponse,
999    ) -> Result<String> {
1000        let base_url = self.config.base_url.as_deref().unwrap_or("http://localhost:3000");
1001        let url = format!("{}{}", base_url, request.path);
1002        let test_name = self
1003            .generate_test_name(request)
1004            .split('_')
1005            .enumerate()
1006            .map(|(i, s)| {
1007                if i == 0 {
1008                    s.to_string()
1009                } else {
1010                    let mut c = s.chars();
1011                    match c.next() {
1012                        None => String::new(),
1013                        Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
1014                    }
1015                }
1016            })
1017            .collect::<String>();
1018
1019        let mut code = String::new();
1020        code.push_str("    @Test\n");
1021        code.push_str(&format!("    public void {}() throws Exception {{\n", test_name));
1022
1023        // Create request
1024        code.push_str("        HttpRequest request = HttpRequest.newBuilder()\n");
1025        code.push_str(&format!("            .uri(URI.create(\"{}\"))\n", url));
1026        code.push_str(&format!("            .method(\"{}\", ", request.method));
1027
1028        if let Some(body) = &request.body {
1029            if !body.is_empty() {
1030                let escaped_body = body.replace('"', "\\\"").replace('\n', "\\n");
1031                code.push_str(&format!(
1032                    "HttpRequest.BodyPublishers.ofString(\"{}\"))\n",
1033                    escaped_body
1034                ));
1035            } else {
1036                code.push_str("HttpRequest.BodyPublishers.noBody())\n");
1037            }
1038        } else {
1039            code.push_str("HttpRequest.BodyPublishers.noBody())\n");
1040        }
1041
1042        // Add headers
1043        if let Ok(headers) = serde_json::from_str::<HashMap<String, String>>(&request.headers) {
1044            for (key, value) in headers.iter() {
1045                if key.to_lowercase() != "host" {
1046                    code.push_str(&format!("            .header(\"{}\", \"{}\")\n", key, value));
1047                }
1048            }
1049        }
1050
1051        code.push_str("            .build();\n\n");
1052
1053        // Send request
1054        code.push_str("        HttpClient client = HttpClient.newHttpClient();\n");
1055        code.push_str("        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());\n\n");
1056
1057        // Assertions
1058        if self.config.validate_status {
1059            code.push_str(&format!(
1060                "        assertEquals({}, response.statusCode());\n",
1061                response.status_code
1062            ));
1063        }
1064
1065        if self.config.validate_body && response.body.is_some() {
1066            if let Some(body) = &response.body {
1067                if serde_json::from_str::<Value>(body).is_ok() {
1068                    code.push_str("        assertNotNull(response.body());\n");
1069                }
1070            }
1071        }
1072
1073        code.push_str("    }\n");
1074        Ok(code)
1075    }
1076
1077    /// Generate C# xUnit test
1078    fn generate_csharp_test(
1079        &self,
1080        request: &RecordedRequest,
1081        response: &RecordedResponse,
1082    ) -> Result<String> {
1083        let base_url = self.config.base_url.as_deref().unwrap_or("http://localhost:3000");
1084        let url = format!("{}{}", base_url, request.path);
1085        let test_name = self
1086            .generate_test_name(request)
1087            .split('_')
1088            .map(|s| {
1089                let mut c = s.chars();
1090                match c.next() {
1091                    None => String::new(),
1092                    Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
1093                }
1094            })
1095            .collect::<String>();
1096
1097        let mut code = String::new();
1098        code.push_str("        [Fact]\n");
1099        code.push_str(&format!("        public async Task {}Async()\n", test_name));
1100        code.push_str("        {\n");
1101
1102        // Create client and request
1103        code.push_str("            using var client = new HttpClient();\n");
1104
1105        // Create request message
1106        // Format method name for C# HttpMethod (e.g., "GET" -> "Get", "POST" -> "Post")
1107        let method_name = if request.method.is_empty() {
1108            "Get".to_string()
1109        } else {
1110            request.method.chars().next().unwrap_or('G').to_uppercase().collect::<String>()
1111                + &request.method.get(1..).unwrap_or("et").to_lowercase()
1112        };
1113        code.push_str(&format!(
1114            "            var request = new HttpRequestMessage(HttpMethod.{}, \"{}\");\n",
1115            method_name, url
1116        ));
1117
1118        // Add headers
1119        if let Ok(headers) = serde_json::from_str::<HashMap<String, String>>(&request.headers) {
1120            for (key, value) in headers.iter() {
1121                if key.to_lowercase() != "host" && key.to_lowercase() != "content-type" {
1122                    code.push_str(&format!(
1123                        "            request.Headers.Add(\"{}\", \"{}\");\n",
1124                        key, value
1125                    ));
1126                }
1127            }
1128        }
1129
1130        // Add body
1131        if let Some(body) = &request.body {
1132            if !body.is_empty() {
1133                let escaped_body = body.replace('"', "\\\"").replace('\n', "\\n");
1134                code.push_str(&format!("            request.Content = new StringContent(\"{}\", Encoding.UTF8, \"application/json\");\n",
1135                    escaped_body));
1136            }
1137        }
1138
1139        // Send request
1140        code.push_str("            var response = await client.SendAsync(request);\n\n");
1141
1142        // Assertions
1143        if self.config.validate_status {
1144            code.push_str(&format!(
1145                "            Assert.Equal({}, (int)response.StatusCode);\n",
1146                response.status_code
1147            ));
1148        }
1149
1150        if self.config.validate_body && response.body.is_some() {
1151            code.push_str(
1152                "            var content = await response.Content.ReadAsStringAsync();\n",
1153            );
1154            code.push_str("            Assert.NotNull(content);\n");
1155            code.push_str("            Assert.NotEmpty(content);\n");
1156        }
1157
1158        code.push_str("        }\n");
1159        Ok(code)
1160    }
1161
1162    /// Generate complete test file with imports and structure
1163    fn generate_test_file(&self, tests: &[GeneratedTest]) -> Result<String> {
1164        let mut file = String::new();
1165
1166        match self.config.format {
1167            TestFormat::RustReqwest => {
1168                file.push_str("// Generated test file\n");
1169                file.push_str("// Run with: cargo test\n\n");
1170                if self.config.include_setup_teardown {
1171                    file.push_str("use reqwest;\n");
1172                    file.push_str("use serde_json::Value;\n\n");
1173                }
1174
1175                for test in tests {
1176                    file.push_str(&test.code);
1177                    file.push('\n');
1178                }
1179            }
1180            TestFormat::HttpFile => {
1181                for test in tests {
1182                    file.push_str(&test.code);
1183                    file.push('\n');
1184                }
1185            }
1186            TestFormat::Curl => {
1187                file.push_str("#!/bin/bash\n");
1188                file.push_str("# Generated cURL commands\n\n");
1189                for test in tests {
1190                    file.push_str(&format!("# {} {}\n", test.method, test.endpoint));
1191                    file.push_str(&test.code);
1192                    file.push_str("\n\n");
1193                }
1194            }
1195            TestFormat::Postman => {
1196                let collection = serde_json::json!({
1197                    "info": {
1198                        "name": self.config.suite_name,
1199                        "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
1200                    },
1201                    "item": tests.iter().map(|t| {
1202                        serde_json::from_str::<Value>(&t.code).unwrap_or(Value::Null)
1203                    }).collect::<Vec<_>>()
1204                });
1205                file = serde_json::to_string_pretty(&collection)
1206                    .map_err(RecorderError::Serialization)?;
1207            }
1208            TestFormat::K6 => {
1209                file.push_str("import http from 'k6/http';\n");
1210                file.push_str("import { check, sleep } from 'k6';\n\n");
1211                file.push_str("export const options = {\n");
1212                file.push_str("  vus: 10,\n");
1213                file.push_str("  duration: '30s',\n");
1214                file.push_str("};\n\n");
1215                file.push_str("export default function() {\n");
1216                for test in tests {
1217                    file.push_str(&test.code);
1218                }
1219                file.push_str("  sleep(1);\n");
1220                file.push_str("}\n");
1221            }
1222            TestFormat::PythonPytest => {
1223                file.push_str("# Generated test file\n");
1224                file.push_str("# Run with: pytest\n\n");
1225                file.push_str("import requests\n");
1226                file.push_str("import pytest\n\n");
1227                for test in tests {
1228                    file.push_str(&test.code);
1229                    file.push('\n');
1230                }
1231            }
1232            TestFormat::JavaScriptJest => {
1233                file.push_str("// Generated test file\n");
1234                file.push_str("// Run with: npm test\n\n");
1235                file.push_str(&format!("describe('{}', () => {{\n", self.config.suite_name));
1236                for test in tests {
1237                    file.push_str("  ");
1238                    file.push_str(&test.code.replace("\n", "\n  "));
1239                    file.push('\n');
1240                }
1241                file.push_str("});\n");
1242            }
1243            TestFormat::GoTest => {
1244                file.push_str("package main\n\n");
1245                file.push_str("import (\n");
1246                file.push_str("    \"net/http\"\n");
1247                file.push_str("    \"strings\"\n");
1248                file.push_str("    \"testing\"\n");
1249                file.push_str(")\n\n");
1250                for test in tests {
1251                    file.push_str(&test.code);
1252                    file.push('\n');
1253                }
1254            }
1255            TestFormat::RubyRspec => {
1256                file.push_str("# Generated test file\n");
1257                file.push_str("# Run with: rspec spec/api_spec.rb\n\n");
1258                file.push_str("require 'httparty'\n");
1259                file.push_str("require 'rspec'\n\n");
1260                file.push_str(&format!("RSpec.describe '{}' do\n", self.config.suite_name));
1261                for test in tests {
1262                    file.push_str(&test.code);
1263                    file.push('\n');
1264                }
1265                file.push_str("end\n");
1266            }
1267            TestFormat::JavaJunit => {
1268                file.push_str("// Generated test file\n");
1269                file.push_str("// Run with: mvn test or gradle test\n\n");
1270                file.push_str("import org.junit.jupiter.api.Test;\n");
1271                file.push_str("import static org.junit.jupiter.api.Assertions.*;\n");
1272                file.push_str("import java.net.URI;\n");
1273                file.push_str("import java.net.http.HttpClient;\n");
1274                file.push_str("import java.net.http.HttpRequest;\n");
1275                file.push_str("import java.net.http.HttpResponse;\n\n");
1276                file.push_str(&format!(
1277                    "public class {} {{\n",
1278                    self.config.suite_name.replace("-", "_")
1279                ));
1280                for test in tests {
1281                    file.push_str(&test.code);
1282                    file.push('\n');
1283                }
1284                file.push_str("}\n");
1285            }
1286            TestFormat::CSharpXunit => {
1287                file.push_str("// Generated test file\n");
1288                file.push_str("// Run with: dotnet test\n\n");
1289                file.push_str("using System;\n");
1290                file.push_str("using System.Net.Http;\n");
1291                file.push_str("using System.Text;\n");
1292                file.push_str("using System.Threading.Tasks;\n");
1293                file.push_str("using Xunit;\n\n");
1294                file.push_str(&format!("namespace {}\n", self.config.suite_name.replace("-", "_")));
1295                file.push_str("{\n");
1296                file.push_str("    public class ApiTests\n");
1297                file.push_str("    {\n");
1298                for test in tests {
1299                    file.push_str(&test.code);
1300                    file.push('\n');
1301                }
1302                file.push_str("    }\n");
1303                file.push_str("}\n");
1304            }
1305        }
1306
1307        Ok(file)
1308    }
1309
1310    /// Deduplicate similar tests
1311    fn deduplicate_tests(&self, tests: Vec<GeneratedTest>) -> Vec<GeneratedTest> {
1312        let mut unique_tests = Vec::new();
1313        let mut seen_signatures = std::collections::HashSet::new();
1314
1315        for test in tests {
1316            // Create a signature based on method + endpoint + code structure
1317            let signature = format!("{}:{}:{}", test.method, test.endpoint, test.code.len());
1318
1319            if !seen_signatures.contains(&signature) {
1320                seen_signatures.insert(signature);
1321                unique_tests.push(test);
1322            }
1323        }
1324
1325        unique_tests
1326    }
1327
1328    /// Optimize test execution order
1329    fn optimize_test_order(&self, mut tests: Vec<GeneratedTest>) -> Vec<GeneratedTest> {
1330        // Sort tests by:
1331        // 1. GET requests first (read-only, fast)
1332        // 2. POST/PUT requests (may modify state)
1333        // 3. DELETE requests last
1334        tests.sort_by(|a, b| {
1335            let order_a = match a.method.as_str() {
1336                "GET" | "HEAD" => 0,
1337                "POST" | "PUT" | "PATCH" => 1,
1338                "DELETE" => 2,
1339                _ => 3,
1340            };
1341            let order_b = match b.method.as_str() {
1342                "GET" | "HEAD" => 0,
1343                "POST" | "PUT" | "PATCH" => 1,
1344                "DELETE" => 2,
1345                _ => 3,
1346            };
1347            order_a.cmp(&order_b).then_with(|| a.endpoint.cmp(&b.endpoint))
1348        });
1349
1350        tests
1351    }
1352
1353    /// Generate test data fixtures using AI
1354    async fn generate_test_fixtures(
1355        &self,
1356        exchanges: &[crate::models::RecordedExchange],
1357    ) -> Result<Vec<TestFixture>> {
1358        let llm_config = if let Some(config) = self.config.llm_config.as_ref() {
1359            config
1360        } else {
1361            return Ok(Vec::new());
1362        };
1363        let mut fixtures = Vec::new();
1364
1365        // Group exchanges by endpoint
1366        let mut endpoint_data: HashMap<String, Vec<&crate::models::RecordedExchange>> =
1367            HashMap::new();
1368        for exchange in exchanges {
1369            let endpoint = format!("{} {}", exchange.request.method, exchange.request.path);
1370            endpoint_data.entry(endpoint).or_default().push(exchange);
1371        }
1372
1373        // Generate fixtures for each endpoint
1374        for (endpoint, endpoint_exchanges) in endpoint_data.iter().take(5) {
1375            // Collect sample request bodies
1376            let mut sample_bodies = Vec::new();
1377            for exchange in endpoint_exchanges.iter().take(3) {
1378                if let Some(body) = &exchange.request.body {
1379                    if !body.is_empty() {
1380                        if let Ok(json) = serde_json::from_str::<Value>(body) {
1381                            sample_bodies.push(json);
1382                        }
1383                    }
1384                }
1385            }
1386
1387            if sample_bodies.is_empty() {
1388                continue;
1389            }
1390
1391            let prompt = format!(
1392                "Based on these sample API request bodies for endpoint '{}', generate a reusable test fixture in JSON format:\n{}\n\nProvide a clean JSON object with varied test data including edge cases.",
1393                endpoint,
1394                serde_json::to_string_pretty(&sample_bodies).unwrap_or_default()
1395            );
1396
1397            if let Ok(response) = self.call_llm(llm_config, &prompt).await {
1398                // Try to parse the LLM response as JSON
1399                if let Ok(data) = serde_json::from_str::<Value>(&response) {
1400                    fixtures.push(TestFixture {
1401                        name: format!("fixture_{}", endpoint.replace([' ', '/'], "_")),
1402                        description: format!("Test fixture for {}", endpoint),
1403                        data,
1404                        endpoints: vec![endpoint.clone()],
1405                    });
1406                }
1407            }
1408        }
1409
1410        Ok(fixtures)
1411    }
1412
1413    /// Suggest edge cases using AI
1414    async fn suggest_edge_cases(
1415        &self,
1416        exchanges: &[crate::models::RecordedExchange],
1417    ) -> Result<Vec<EdgeCaseSuggestion>> {
1418        let llm_config = if let Some(config) = self.config.llm_config.as_ref() {
1419            config
1420        } else {
1421            return Ok(Vec::new());
1422        };
1423        let mut edge_cases = Vec::new();
1424
1425        // Group by endpoint
1426        let mut endpoint_data: HashMap<String, Vec<&crate::models::RecordedExchange>> =
1427            HashMap::new();
1428        for exchange in exchanges {
1429            let key = format!("{} {}", exchange.request.method, exchange.request.path);
1430            endpoint_data.entry(key).or_default().push(exchange);
1431        }
1432
1433        for (endpoint_key, endpoint_exchanges) in endpoint_data.iter().take(5) {
1434            let parts: Vec<&str> = endpoint_key.splitn(2, ' ').collect();
1435            if parts.len() != 2 {
1436                continue;
1437            }
1438            let (method, endpoint) = (parts[0], parts[1]);
1439
1440            // Collect sample data
1441            let sample_exchange = endpoint_exchanges.first();
1442            let sample_body = sample_exchange
1443                .and_then(|e| e.request.body.as_ref())
1444                .map(|s| s.as_str())
1445                .unwrap_or("{}");
1446
1447            let prompt = format!(
1448                "Suggest 3 critical edge cases to test for this API endpoint:\n\
1449                Method: {}\n\
1450                Path: {}\n\
1451                Sample Request: {}\n\n\
1452                For each edge case, provide:\n\
1453                1. Type (e.g., 'validation', 'boundary', 'security')\n\
1454                2. Description\n\
1455                3. Expected behavior\n\
1456                4. Priority (1-5)\n\n\
1457                Format: type|description|behavior|priority",
1458                method, endpoint, sample_body
1459            );
1460
1461            if let Ok(response) = self.call_llm(llm_config, &prompt).await {
1462                // Parse LLM response
1463                for line in response.lines().take(3) {
1464                    let parts: Vec<&str> = line.split('|').collect();
1465                    if parts.len() >= 4 {
1466                        let priority = parts[3].trim().parse::<u8>().unwrap_or(3);
1467                        edge_cases.push(EdgeCaseSuggestion {
1468                            endpoint: endpoint.to_string(),
1469                            method: method.to_string(),
1470                            case_type: parts[0].trim().to_string(),
1471                            description: parts[1].trim().to_string(),
1472                            suggested_input: None,
1473                            expected_behavior: parts[2].trim().to_string(),
1474                            priority,
1475                        });
1476                    }
1477                }
1478            }
1479        }
1480
1481        Ok(edge_cases)
1482    }
1483
1484    /// Analyze test gaps
1485    async fn analyze_test_gaps(
1486        &self,
1487        exchanges: &[crate::models::RecordedExchange],
1488        tests: &[GeneratedTest],
1489    ) -> Result<TestGapAnalysis> {
1490        // Collect all unique endpoints from exchanges
1491        let mut all_endpoints = std::collections::HashSet::new();
1492        let mut method_by_endpoint: HashMap<String, std::collections::HashSet<String>> =
1493            HashMap::new();
1494        let mut status_codes_by_endpoint: HashMap<String, std::collections::HashSet<u16>> =
1495            HashMap::new();
1496
1497        for exchange in exchanges {
1498            let endpoint = exchange.request.path.clone();
1499            let method = exchange.request.method.clone();
1500            all_endpoints.insert(endpoint.clone());
1501            method_by_endpoint.entry(endpoint.clone()).or_default().insert(method);
1502
1503            if let Some(response) = &exchange.response {
1504                let status_code = response.status_code as u16;
1505                status_codes_by_endpoint
1506                    .entry(endpoint.clone())
1507                    .or_default()
1508                    .insert(status_code);
1509            }
1510        }
1511
1512        // Collect tested endpoints from generated tests
1513        let mut tested_endpoints = std::collections::HashSet::new();
1514        for test in tests {
1515            tested_endpoints.insert(test.endpoint.clone());
1516        }
1517
1518        // Find gaps
1519        let untested_endpoints: Vec<String> =
1520            all_endpoints.difference(&tested_endpoints).cloned().collect();
1521
1522        let mut missing_methods: HashMap<String, Vec<String>> = HashMap::new();
1523        for (endpoint, methods) in &method_by_endpoint {
1524            let tested_methods: std::collections::HashSet<String> = tests
1525                .iter()
1526                .filter(|t| &t.endpoint == endpoint)
1527                .map(|t| t.method.clone())
1528                .collect();
1529
1530            let missing: Vec<String> = methods.difference(&tested_methods).cloned().collect();
1531
1532            if !missing.is_empty() {
1533                missing_methods.insert(endpoint.clone(), missing);
1534            }
1535        }
1536
1537        let mut missing_status_codes: HashMap<String, Vec<u16>> = HashMap::new();
1538        for (endpoint, codes) in &status_codes_by_endpoint {
1539            // For now, we'll just note if error codes (4xx, 5xx) are missing
1540            let has_error_tests = codes.iter().any(|c| *c >= 400);
1541            if has_error_tests {
1542                missing_status_codes.insert(
1543                    endpoint.clone(),
1544                    codes.iter().filter(|c| **c >= 400).copied().collect(),
1545                );
1546            }
1547        }
1548
1549        let missing_error_scenarios = vec![
1550            "401 Unauthorized scenarios".to_string(),
1551            "403 Forbidden scenarios".to_string(),
1552            "404 Not Found scenarios".to_string(),
1553            "429 Rate Limiting scenarios".to_string(),
1554            "500 Internal Server Error scenarios".to_string(),
1555        ];
1556
1557        let coverage_percentage = if all_endpoints.is_empty() {
1558            100.0
1559        } else {
1560            (tested_endpoints.len() as f64 / all_endpoints.len() as f64) * 100.0
1561        };
1562
1563        let mut recommendations = Vec::new();
1564        if !untested_endpoints.is_empty() {
1565            recommendations
1566                .push(format!("Add tests for {} untested endpoints", untested_endpoints.len()));
1567        }
1568        if !missing_methods.is_empty() {
1569            recommendations.push(format!(
1570                "Add tests for missing HTTP methods on {} endpoints",
1571                missing_methods.len()
1572            ));
1573        }
1574        if coverage_percentage < 80.0 {
1575            recommendations.push("Increase test coverage to at least 80%".to_string());
1576        }
1577
1578        Ok(TestGapAnalysis {
1579            untested_endpoints,
1580            missing_methods,
1581            missing_status_codes,
1582            missing_error_scenarios,
1583            coverage_percentage,
1584            recommendations,
1585        })
1586    }
1587}
1588
1589#[cfg(test)]
1590mod tests {
1591    use super::*;
1592    use crate::models::{Protocol, RecordedRequest, RecordedResponse};
1593
1594    #[tokio::test]
1595    async fn test_generate_test_name() {
1596        let database = RecorderDatabase::new_in_memory().await.unwrap();
1597        let config = TestGenerationConfig::default();
1598        let generator = TestGenerator::new(database, config);
1599
1600        let request = RecordedRequest {
1601            id: "test".to_string(),
1602            protocol: Protocol::Http,
1603            timestamp: chrono::Utc::now(),
1604            method: "GET".to_string(),
1605            path: "/api/users/123".to_string(),
1606            query_params: None,
1607            headers: "{}".to_string(),
1608            body: None,
1609            body_encoding: "utf-8".to_string(),
1610            status_code: Some(200),
1611            duration_ms: Some(50),
1612            client_ip: None,
1613            trace_id: None,
1614            span_id: None,
1615            tags: None,
1616        };
1617
1618        let name = generator.generate_test_name(&request);
1619        assert_eq!(name, "test_get_api_users_123");
1620    }
1621
1622    #[test]
1623    fn test_default_config() {
1624        let config = TestGenerationConfig::default();
1625        assert_eq!(config.format, TestFormat::RustReqwest);
1626        assert!(config.include_assertions);
1627        assert!(config.validate_body);
1628        assert!(config.validate_status);
1629    }
1630
1631    #[tokio::test]
1632    async fn test_generate_rust_test() {
1633        let database = RecorderDatabase::new_in_memory().await.unwrap();
1634        let config = TestGenerationConfig::default();
1635        let generator = TestGenerator::new(database, config);
1636
1637        let request = RecordedRequest {
1638            id: "test-1".to_string(),
1639            protocol: Protocol::Http,
1640            timestamp: chrono::Utc::now(),
1641            method: "GET".to_string(),
1642            path: "/api/users".to_string(),
1643            query_params: None,
1644            headers: r#"{"content-type":"application/json"}"#.to_string(),
1645            body: None,
1646            body_encoding: "utf-8".to_string(),
1647            status_code: Some(200),
1648            duration_ms: Some(45),
1649            client_ip: Some("127.0.0.1".to_string()),
1650            trace_id: None,
1651            span_id: None,
1652            tags: None,
1653        };
1654
1655        let response = RecordedResponse {
1656            request_id: "test-1".to_string(),
1657            status_code: 200,
1658            headers: r#"{"content-type":"application/json"}"#.to_string(),
1659            body: Some(r#"{"users":[]}"#.to_string()),
1660            body_encoding: "utf-8".to_string(),
1661            size_bytes: 12,
1662            timestamp: chrono::Utc::now(),
1663        };
1664
1665        let code = generator.generate_rust_test(&request, &response).unwrap();
1666
1667        assert!(code.contains("#[tokio::test]"));
1668        assert!(code.contains("async fn test_get_api_users()"));
1669        assert!(code.contains("reqwest::Client::new()"));
1670        assert!(code.contains("assert_eq!(response.status().as_u16(), 200)"));
1671    }
1672
1673    #[tokio::test]
1674    async fn test_generate_curl() {
1675        let database = RecorderDatabase::new_in_memory().await.unwrap();
1676        let config = TestGenerationConfig::default();
1677        let generator = TestGenerator::new(database, config);
1678
1679        let request = RecordedRequest {
1680            id: "test-2".to_string(),
1681            protocol: Protocol::Http,
1682            timestamp: chrono::Utc::now(),
1683            method: "POST".to_string(),
1684            path: "/api/users".to_string(),
1685            query_params: None,
1686            headers: r#"{"content-type":"application/json"}"#.to_string(),
1687            body: Some(r#"{"name":"John"}"#.to_string()),
1688            body_encoding: "utf-8".to_string(),
1689            status_code: Some(201),
1690            duration_ms: Some(80),
1691            client_ip: None,
1692            trace_id: None,
1693            span_id: None,
1694            tags: None,
1695        };
1696
1697        let response = RecordedResponse {
1698            request_id: "test-2".to_string(),
1699            status_code: 201,
1700            headers: r#"{}"#.to_string(),
1701            body: None,
1702            body_encoding: "utf-8".to_string(),
1703            size_bytes: 0,
1704            timestamp: chrono::Utc::now(),
1705        };
1706
1707        let code = generator.generate_curl(&request, &response).unwrap();
1708
1709        assert!(code.contains("curl -X POST"));
1710        assert!(code.contains("/api/users"));
1711        assert!(code.contains("-H 'content-type: application/json'"));
1712        assert!(code.contains(r#"-d '{"name":"John"}'"#));
1713    }
1714
1715    #[tokio::test]
1716    async fn test_generate_http_file() {
1717        let database = RecorderDatabase::new_in_memory().await.unwrap();
1718        let config = TestGenerationConfig::default();
1719        let generator = TestGenerator::new(database, config);
1720
1721        let request = RecordedRequest {
1722            id: "test-3".to_string(),
1723            protocol: Protocol::Http,
1724            timestamp: chrono::Utc::now(),
1725            method: "DELETE".to_string(),
1726            path: "/api/users/123".to_string(),
1727            query_params: None,
1728            headers: r#"{}"#.to_string(),
1729            body: None,
1730            body_encoding: "utf-8".to_string(),
1731            status_code: Some(204),
1732            duration_ms: Some(30),
1733            client_ip: None,
1734            trace_id: None,
1735            span_id: None,
1736            tags: None,
1737        };
1738
1739        let response = RecordedResponse {
1740            request_id: "test-3".to_string(),
1741            status_code: 204,
1742            headers: r#"{}"#.to_string(),
1743            body: None,
1744            body_encoding: "utf-8".to_string(),
1745            size_bytes: 0,
1746            timestamp: chrono::Utc::now(),
1747        };
1748
1749        let code = generator.generate_http_file(&request, &response).unwrap();
1750
1751        assert!(code.contains("### DELETE /api/users/123"));
1752        assert!(code.contains("DELETE http://localhost:3000/api/users/123"));
1753    }
1754
1755    #[test]
1756    fn test_llm_config_defaults() {
1757        let config = LlmConfig::default();
1758        assert_eq!(config.provider, "ollama");
1759        assert_eq!(config.model, "llama2");
1760        assert_eq!(config.temperature, 0.3);
1761    }
1762
1763    #[test]
1764    fn test_test_format_variants() {
1765        assert_eq!(TestFormat::RustReqwest, TestFormat::RustReqwest);
1766        assert_ne!(TestFormat::RustReqwest, TestFormat::Curl);
1767        assert_ne!(TestFormat::PythonPytest, TestFormat::JavaScriptJest);
1768    }
1769}