Skip to main content

mockforge_openapi/response/
mod.rs

1//! OpenAPI response generation and mocking
2//!
3//! This module provides functionality for generating mock responses
4//! based on OpenAPI specifications.
5
6mod ai_assisted;
7mod schema_based;
8
9use crate::OpenApiSpec;
10use async_trait::async_trait;
11use chrono;
12use mockforge_foundation::ai_response::{AiResponseConfig, RequestContext};
13use mockforge_foundation::error::Result;
14use mockforge_foundation::intelligent_behavior::Persona;
15use openapiv3::{Operation, ReferenceOr, Response, Responses, Schema};
16use rand::Rng;
17use serde_json::Value;
18use std::collections::HashMap;
19use uuid;
20
21/// Trait for AI response generation
22///
23/// This trait allows the HTTP layer to provide custom AI generation
24/// implementations without creating circular dependencies between crates.
25#[async_trait]
26pub trait AiGenerator: Send + Sync {
27    /// Generate an AI response from a prompt
28    ///
29    /// # Arguments
30    /// * `prompt` - The expanded prompt to send to the LLM
31    /// * `config` - The AI response configuration with temperature, max_tokens, etc.
32    ///
33    /// # Returns
34    /// A JSON value containing the generated response
35    async fn generate(&self, prompt: &str, config: &AiResponseConfig) -> Result<Value>;
36}
37
38/// Round 33 (#822) — Srikanth's "can we add those as negative response
39/// tests from mockforge server side". Off by default. When enabled,
40/// `ResponseGenerator::maybe_inject_response_violation` drops the
41/// first required field from every synthesized 2xx response so the
42/// caller's proxy / conformance pipeline can be tested against a
43/// known-bad-shape mockforge end-to-end.
44fn inject_response_violations_enabled() -> bool {
45    std::env::var("MOCKFORGE_INJECT_RESPONSE_VIOLATIONS")
46        .ok()
47        .map(|s| matches!(s.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
48        .unwrap_or(false)
49}
50
51/// Response generator for creating mock responses
52pub struct ResponseGenerator;
53
54impl ResponseGenerator {
55    /// Generate a mock response for an operation and status code
56    pub fn generate_response(
57        spec: &OpenApiSpec,
58        operation: &Operation,
59        status_code: u16,
60        content_type: Option<&str>,
61    ) -> Result<Value> {
62        Self::generate_response_with_expansion(spec, operation, status_code, content_type, true)
63    }
64
65    /// Generate a mock response for an operation and status code with token expansion control
66    pub fn generate_response_with_expansion(
67        spec: &OpenApiSpec,
68        operation: &Operation,
69        status_code: u16,
70        content_type: Option<&str>,
71        expand_tokens: bool,
72    ) -> Result<Value> {
73        Self::generate_response_with_expansion_and_mode(
74            spec,
75            operation,
76            status_code,
77            content_type,
78            expand_tokens,
79            None,
80            None,
81        )
82    }
83
84    /// Generate response with token expansion and selection mode
85    pub fn generate_response_with_expansion_and_mode(
86        spec: &OpenApiSpec,
87        operation: &Operation,
88        status_code: u16,
89        content_type: Option<&str>,
90        expand_tokens: bool,
91        selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
92        selector: Option<&crate::response_selection::ResponseSelector>,
93    ) -> Result<Value> {
94        Self::generate_response_with_expansion_and_mode_and_persona(
95            spec,
96            operation,
97            status_code,
98            content_type,
99            expand_tokens,
100            selection_mode,
101            selector,
102            None, // No persona by default
103        )
104    }
105
106    /// Generate response with token expansion, selection mode, and persona
107    #[allow(clippy::too_many_arguments)]
108    pub fn generate_response_with_expansion_and_mode_and_persona(
109        spec: &OpenApiSpec,
110        operation: &Operation,
111        status_code: u16,
112        content_type: Option<&str>,
113        expand_tokens: bool,
114        selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
115        selector: Option<&crate::response_selection::ResponseSelector>,
116        persona: Option<&Persona>,
117    ) -> Result<Value> {
118        Self::generate_response_with_scenario_and_mode_and_persona(
119            spec,
120            operation,
121            status_code,
122            content_type,
123            expand_tokens,
124            None, // No scenario by default
125            selection_mode,
126            selector,
127            persona,
128        )
129    }
130
131    /// Generate a mock response with scenario support
132    ///
133    /// This method allows selection of specific example scenarios from the OpenAPI spec.
134    /// Scenarios are defined using the standard OpenAPI `examples` field (not the singular `example`).
135    ///
136    /// # Arguments
137    /// * `spec` - The OpenAPI specification
138    /// * `operation` - The operation to generate a response for
139    /// * `status_code` - The HTTP status code
140    /// * `content_type` - Optional content type (e.g., "application/json")
141    /// * `expand_tokens` - Whether to expand template tokens like {{now}} and {{uuid}}
142    /// * `scenario` - Optional scenario name to select from the examples map
143    ///
144    /// # Example
145    /// ```yaml
146    /// responses:
147    ///   '200':
148    ///     content:
149    ///       application/json:
150    ///         examples:
151    ///           happy:
152    ///             value: { "status": "success", "message": "All good!" }
153    ///           error:
154    ///             value: { "status": "error", "message": "Something went wrong" }
155    /// ```
156    pub fn generate_response_with_scenario(
157        spec: &OpenApiSpec,
158        operation: &Operation,
159        status_code: u16,
160        content_type: Option<&str>,
161        expand_tokens: bool,
162        scenario: Option<&str>,
163    ) -> Result<Value> {
164        Self::generate_response_with_scenario_and_mode(
165            spec,
166            operation,
167            status_code,
168            content_type,
169            expand_tokens,
170            scenario,
171            None,
172            None,
173        )
174    }
175
176    /// Generate response with scenario support and selection mode
177    #[allow(clippy::too_many_arguments)]
178    pub fn generate_response_with_scenario_and_mode(
179        spec: &OpenApiSpec,
180        operation: &Operation,
181        status_code: u16,
182        content_type: Option<&str>,
183        expand_tokens: bool,
184        scenario: Option<&str>,
185        selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
186        selector: Option<&crate::response_selection::ResponseSelector>,
187    ) -> Result<Value> {
188        Self::generate_response_with_scenario_and_mode_and_persona(
189            spec,
190            operation,
191            status_code,
192            content_type,
193            expand_tokens,
194            scenario,
195            selection_mode,
196            selector,
197            None, // No persona by default
198        )
199    }
200
201    /// Generate response with scenario support, selection mode, and persona
202    #[allow(clippy::too_many_arguments)]
203    pub fn generate_response_with_scenario_and_mode_and_persona(
204        spec: &OpenApiSpec,
205        operation: &Operation,
206        status_code: u16,
207        content_type: Option<&str>,
208        expand_tokens: bool,
209        scenario: Option<&str>,
210        selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
211        selector: Option<&crate::response_selection::ResponseSelector>,
212        _persona: Option<&Persona>,
213    ) -> Result<Value> {
214        // Find the response for the status code
215        let response = Self::find_response_for_status(&operation.responses, status_code);
216
217        tracing::debug!(
218            "Finding response for status code {}: {:?}",
219            status_code,
220            if response.is_some() {
221                "found"
222            } else {
223                "not found"
224            }
225        );
226
227        let body = match response {
228            Some(response_ref) => match response_ref {
229                ReferenceOr::Item(response) => {
230                    tracing::debug!(
231                        "Using direct response item with {} content types",
232                        response.content.len()
233                    );
234                    Self::generate_from_response_with_scenario_and_mode(
235                        spec,
236                        response,
237                        content_type,
238                        expand_tokens,
239                        scenario,
240                        selection_mode,
241                        selector,
242                    )
243                }
244                ReferenceOr::Reference { reference } => {
245                    tracing::debug!("Resolving response reference: {}", reference);
246                    if let Some(resolved_response) = spec.get_response(reference) {
247                        tracing::debug!(
248                            "Resolved response reference with {} content types",
249                            resolved_response.content.len()
250                        );
251                        Self::generate_from_response_with_scenario_and_mode(
252                            spec,
253                            resolved_response,
254                            content_type,
255                            expand_tokens,
256                            scenario,
257                            selection_mode,
258                            selector,
259                        )
260                    } else {
261                        tracing::warn!("Response reference '{}' not found in spec", reference);
262                        Ok(Value::Object(serde_json::Map::new()))
263                    }
264                }
265            },
266            None => {
267                tracing::warn!(
268                    "No response found for status code {} in operation. Available status codes: {:?}",
269                    status_code,
270                    operation.responses.responses.keys().collect::<Vec<_>>()
271                );
272                Ok(Value::Object(serde_json::Map::new()))
273            }
274        };
275
276        // Round 33 (#822) — Srikanth's "can we add those as a negative
277        // response tests from mockforge server side" ask. When the
278        // operator opts in with `MOCKFORGE_INJECT_RESPONSE_VIOLATIONS`,
279        // drop the first declared required field from synthesized 2xx
280        // responses so the operator can test their proxy / conformance
281        // pipeline against a known-bad-shape mockforge end-to-end.
282        // Non-2xx responses are left alone because their shape is
283        // already the "negative" case for clients.
284        body.map(|b| Self::maybe_inject_response_violation(spec, operation, status_code, b))
285    }
286
287    /// Round 33 (#822) — drop the first required field from a 2xx
288    /// response body when the operator has opted into
289    /// `MOCKFORGE_INJECT_RESPONSE_VIOLATIONS`. No-op for non-2xx,
290    /// non-Object schemas, schemas without `required`, bodies that
291    /// already don't contain the field, and runs that don't have the
292    /// env var set.
293    fn maybe_inject_response_violation(
294        spec: &OpenApiSpec,
295        operation: &Operation,
296        status_code: u16,
297        body: Value,
298    ) -> Value {
299        if !inject_response_violations_enabled() {
300            return body;
301        }
302        if !(200..300).contains(&status_code) {
303            return body;
304        }
305        let Some(required_field) =
306            Self::first_required_field_for_status(spec, operation, status_code)
307        else {
308            return body;
309        };
310        match body {
311            Value::Object(mut map) => {
312                if map.remove(&required_field).is_some() {
313                    tracing::info!(
314                        "MOCKFORGE_INJECT_RESPONSE_VIOLATIONS dropped required field '{required_field}' from {status_code} response"
315                    );
316                }
317                Value::Object(map)
318            }
319            other => other,
320        }
321    }
322
323    /// Walk the response → content[application/json] → schema chain
324    /// for `status_code` and return the first declared required field,
325    /// resolving `$ref`s once for Response and once for Schema. Returns
326    /// `None` for refs we can't resolve, non-Object schemas, or
327    /// schemas without `required`.
328    fn first_required_field_for_status(
329        spec: &OpenApiSpec,
330        operation: &Operation,
331        status_code: u16,
332    ) -> Option<String> {
333        let response_ref = Self::find_response_for_status(&operation.responses, status_code)?;
334        let response: &Response = match response_ref {
335            ReferenceOr::Item(r) => r,
336            ReferenceOr::Reference { reference } => spec.get_response(reference)?,
337        };
338        let media = response.content.get("application/json")?;
339        let schema_ref = media.schema.as_ref()?;
340        let schema = match schema_ref {
341            ReferenceOr::Item(s) => s.clone(),
342            ReferenceOr::Reference { reference } => spec.resolve_schema_ref(reference)?,
343        };
344        if let openapiv3::SchemaKind::Type(openapiv3::Type::Object(obj)) = &schema.schema_kind {
345            obj.required.first().cloned()
346        } else {
347            None
348        }
349    }
350
351    /// Find response for a given status code
352    fn find_response_for_status(
353        responses: &Responses,
354        status_code: u16,
355    ) -> Option<&ReferenceOr<Response>> {
356        // First try exact match
357        if let Some(response) = responses.responses.get(&openapiv3::StatusCode::Code(status_code)) {
358            return Some(response);
359        }
360
361        // Try default response
362        if let Some(default_response) = &responses.default {
363            return Some(default_response);
364        }
365
366        None
367    }
368
369    /// Generate response from a Response object
370    #[allow(dead_code)]
371    fn generate_from_response(
372        spec: &OpenApiSpec,
373        response: &Response,
374        content_type: Option<&str>,
375        expand_tokens: bool,
376    ) -> Result<Value> {
377        Self::generate_from_response_with_scenario(
378            spec,
379            response,
380            content_type,
381            expand_tokens,
382            None,
383        )
384    }
385
386    /// Generate response from a Response object with scenario support
387    #[allow(dead_code)]
388    fn generate_from_response_with_scenario(
389        spec: &OpenApiSpec,
390        response: &Response,
391        content_type: Option<&str>,
392        expand_tokens: bool,
393        scenario: Option<&str>,
394    ) -> Result<Value> {
395        Self::generate_from_response_with_scenario_and_mode(
396            spec,
397            response,
398            content_type,
399            expand_tokens,
400            scenario,
401            None,
402            None,
403        )
404    }
405
406    /// Generate response from a Response object with scenario support and selection mode
407    fn generate_from_response_with_scenario_and_mode(
408        spec: &OpenApiSpec,
409        response: &Response,
410        content_type: Option<&str>,
411        expand_tokens: bool,
412        scenario: Option<&str>,
413        selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
414        selector: Option<&crate::response_selection::ResponseSelector>,
415    ) -> Result<Value> {
416        Self::generate_from_response_with_scenario_and_mode_and_persona(
417            spec,
418            response,
419            content_type,
420            expand_tokens,
421            scenario,
422            selection_mode,
423            selector,
424            None, // No persona by default
425        )
426    }
427
428    /// Generate response from a Response object with scenario support, selection mode, and persona
429    #[allow(clippy::too_many_arguments)]
430    #[allow(dead_code)]
431    fn generate_from_response_with_scenario_and_mode_and_persona(
432        spec: &OpenApiSpec,
433        response: &Response,
434        content_type: Option<&str>,
435        expand_tokens: bool,
436        scenario: Option<&str>,
437        selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
438        selector: Option<&crate::response_selection::ResponseSelector>,
439        persona: Option<&Persona>,
440    ) -> Result<Value> {
441        // If content_type is specified, look for that media type
442        if let Some(content_type) = content_type {
443            if let Some(media_type) = response.content.get(content_type) {
444                return Self::generate_from_media_type_with_scenario_and_mode_and_persona(
445                    spec,
446                    media_type,
447                    expand_tokens,
448                    scenario,
449                    selection_mode,
450                    selector,
451                    persona,
452                );
453            }
454        }
455
456        // If no content_type specified or not found, try common content types
457        let preferred_types = ["application/json", "application/xml", "text/plain"];
458
459        for content_type in &preferred_types {
460            if let Some(media_type) = response.content.get(*content_type) {
461                return Self::generate_from_media_type_with_scenario_and_mode_and_persona(
462                    spec,
463                    media_type,
464                    expand_tokens,
465                    scenario,
466                    selection_mode,
467                    selector,
468                    persona,
469                );
470            }
471        }
472
473        // If no suitable content type found, return the first available
474        if let Some((_, media_type)) = response.content.iter().next() {
475            return Self::generate_from_media_type_with_scenario_and_mode_and_persona(
476                spec,
477                media_type,
478                expand_tokens,
479                scenario,
480                selection_mode,
481                selector,
482                persona,
483            );
484        }
485
486        // No content found, return empty object
487        Ok(Value::Object(serde_json::Map::new()))
488    }
489
490    /// Generate response from a MediaType with optional scenario selection
491    #[allow(dead_code)]
492    fn generate_from_media_type(
493        spec: &OpenApiSpec,
494        media_type: &openapiv3::MediaType,
495        expand_tokens: bool,
496    ) -> Result<Value> {
497        Self::generate_from_media_type_with_scenario(spec, media_type, expand_tokens, None)
498    }
499
500    /// Generate response from a MediaType with scenario support and selection mode
501    #[allow(dead_code)]
502    fn generate_from_media_type_with_scenario(
503        spec: &OpenApiSpec,
504        media_type: &openapiv3::MediaType,
505        expand_tokens: bool,
506        scenario: Option<&str>,
507    ) -> Result<Value> {
508        Self::generate_from_media_type_with_scenario_and_mode(
509            spec,
510            media_type,
511            expand_tokens,
512            scenario,
513            None,
514            None,
515        )
516    }
517
518    /// Generate response from a MediaType with scenario support and selection mode (6 args)
519    #[allow(dead_code)]
520    fn generate_from_media_type_with_scenario_and_mode(
521        spec: &OpenApiSpec,
522        media_type: &openapiv3::MediaType,
523        expand_tokens: bool,
524        scenario: Option<&str>,
525        selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
526        selector: Option<&crate::response_selection::ResponseSelector>,
527    ) -> Result<Value> {
528        Self::generate_from_media_type_with_scenario_and_mode_and_persona(
529            spec,
530            media_type,
531            expand_tokens,
532            scenario,
533            selection_mode,
534            selector,
535            None, // No persona by default
536        )
537    }
538
539    /// Generate response from a MediaType with scenario support, selection mode, and persona
540    fn generate_from_media_type_with_scenario_and_mode_and_persona(
541        spec: &OpenApiSpec,
542        media_type: &openapiv3::MediaType,
543        expand_tokens: bool,
544        scenario: Option<&str>,
545        selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
546        selector: Option<&crate::response_selection::ResponseSelector>,
547        persona: Option<&Persona>,
548    ) -> Result<Value> {
549        // First, check if there's an explicit example
550        // CRITICAL: Always check examples first before falling back to schema generation
551        // This ensures GET requests use the correct response format from OpenAPI examples
552        if let Some(example) = &media_type.example {
553            tracing::debug!("Using explicit example from media type: {:?}", example);
554            // Expand templates in the example if enabled
555            if expand_tokens {
556                let expanded_example = Self::expand_templates(example);
557                return Ok(expanded_example);
558            } else {
559                return Ok(example.clone());
560            }
561        }
562
563        // Then check examples map - with scenario support and selection modes
564        // CRITICAL: Always use examples if available, even if query parameters are missing
565        // This fixes the bug where GET requests without query params return POST-style responses
566        if !media_type.examples.is_empty() {
567            use crate::response_selection::{ResponseSelectionMode, ResponseSelector};
568
569            tracing::debug!(
570                "Found {} examples in media type, available examples: {:?}",
571                media_type.examples.len(),
572                media_type.examples.keys().collect::<Vec<_>>()
573            );
574
575            // If a scenario is specified, try to find it first
576            if let Some(scenario_name) = scenario {
577                if let Some(example_ref) = media_type.examples.get(scenario_name) {
578                    tracing::debug!("Using scenario '{}' from examples map", scenario_name);
579                    match Self::extract_example_value_with_persona(
580                        spec,
581                        example_ref,
582                        expand_tokens,
583                        persona,
584                        media_type.schema.as_ref(),
585                    ) {
586                        Ok(value) => return Ok(value),
587                        Err(e) => {
588                            tracing::warn!(
589                                "Failed to extract example for scenario '{}': {}, falling back",
590                                scenario_name,
591                                e
592                            );
593                        }
594                    }
595                } else {
596                    tracing::warn!(
597                        "Scenario '{}' not found in examples, falling back based on selection mode",
598                        scenario_name
599                    );
600                }
601            }
602
603            // Determine selection mode
604            let mode = selection_mode.unwrap_or(ResponseSelectionMode::First);
605
606            // Get list of example names for selection
607            let example_names: Vec<String> = media_type.examples.keys().cloned().collect();
608
609            if example_names.is_empty() {
610                // No examples available, fall back to schema
611                tracing::warn!("Examples map is empty, falling back to schema generation");
612            } else if mode == ResponseSelectionMode::Scenario && scenario.is_some() {
613                // Scenario mode was requested but scenario not found, fall through to selection mode
614                tracing::debug!("Scenario not found, using selection mode: {:?}", mode);
615            } else {
616                // Use selection mode to choose an example
617                let selected_index = if let Some(sel) = selector {
618                    sel.select(&example_names)
619                } else {
620                    // Create temporary selector for this selection
621                    let temp_selector = ResponseSelector::new(mode);
622                    temp_selector.select(&example_names)
623                };
624
625                if let Some(example_name) = example_names.get(selected_index) {
626                    if let Some(example_ref) = media_type.examples.get(example_name) {
627                        tracing::debug!(
628                            "Using example '{}' from examples map (mode: {:?}, index: {})",
629                            example_name,
630                            mode,
631                            selected_index
632                        );
633                        match Self::extract_example_value_with_persona(
634                            spec,
635                            example_ref,
636                            expand_tokens,
637                            persona,
638                            media_type.schema.as_ref(),
639                        ) {
640                            Ok(value) => return Ok(value),
641                            Err(e) => {
642                                tracing::warn!(
643                                    "Failed to extract example '{}': {}, trying fallback",
644                                    example_name,
645                                    e
646                                );
647                            }
648                        }
649                    }
650                }
651            }
652
653            // Fall back to first example if selection failed
654            // This is critical - always use the first example if available, even if previous attempts failed
655            if let Some((example_name, example_ref)) = media_type.examples.iter().next() {
656                tracing::debug!(
657                    "Using first example '{}' from examples map as fallback",
658                    example_name
659                );
660                match Self::extract_example_value_with_persona(
661                    spec,
662                    example_ref,
663                    expand_tokens,
664                    persona,
665                    media_type.schema.as_ref(),
666                ) {
667                    Ok(value) => {
668                        tracing::debug!(
669                            "Successfully extracted fallback example '{}'",
670                            example_name
671                        );
672                        return Ok(value);
673                    }
674                    Err(e) => {
675                        tracing::error!(
676                            "Failed to extract fallback example '{}': {}, falling back to schema generation",
677                            example_name,
678                            e
679                        );
680                        // Continue to schema generation as last resort
681                    }
682                }
683            }
684        } else {
685            tracing::debug!("No examples found in media type, will use schema generation");
686        }
687
688        // Fall back to schema-based generation
689        // Pass persona through to schema generation for consistent data patterns
690        if let Some(schema_ref) = &media_type.schema {
691            Ok(Self::generate_example_from_schema_ref(spec, schema_ref, persona))
692        } else {
693            Ok(Value::Object(serde_json::Map::new()))
694        }
695    }
696
697    /// Extract value from an example reference
698    /// Optionally expands items arrays based on pagination metadata if persona is provided
699    #[allow(dead_code)]
700    fn extract_example_value(
701        spec: &OpenApiSpec,
702        example_ref: &ReferenceOr<openapiv3::Example>,
703        expand_tokens: bool,
704    ) -> Result<Value> {
705        Self::extract_example_value_with_persona(spec, example_ref, expand_tokens, None, None)
706    }
707
708    /// Extract value from an example reference with optional persona and schema for pagination expansion
709    fn extract_example_value_with_persona(
710        spec: &OpenApiSpec,
711        example_ref: &ReferenceOr<openapiv3::Example>,
712        expand_tokens: bool,
713        persona: Option<&Persona>,
714        schema_ref: Option<&ReferenceOr<Schema>>,
715    ) -> Result<Value> {
716        let mut value = match example_ref {
717            ReferenceOr::Item(example) => {
718                if let Some(v) = &example.value {
719                    tracing::debug!("Using example from examples map: {:?}", v);
720                    if expand_tokens {
721                        Self::expand_templates(v)
722                    } else {
723                        v.clone()
724                    }
725                } else {
726                    return Ok(Value::Object(serde_json::Map::new()));
727                }
728            }
729            ReferenceOr::Reference { reference } => {
730                // Resolve the example reference
731                if let Some(example) = spec.get_example(reference) {
732                    if let Some(v) = &example.value {
733                        tracing::debug!("Using resolved example reference: {:?}", v);
734                        if expand_tokens {
735                            Self::expand_templates(v)
736                        } else {
737                            v.clone()
738                        }
739                    } else {
740                        return Ok(Value::Object(serde_json::Map::new()));
741                    }
742                } else {
743                    tracing::warn!("Example reference '{}' not found", reference);
744                    return Ok(Value::Object(serde_json::Map::new()));
745                }
746            }
747        };
748
749        // Check for pagination mismatch and expand items array if needed
750        value = Self::expand_example_items_if_needed(spec, value, persona, schema_ref);
751
752        Ok(value)
753    }
754
755    /// Expand items array in example if pagination metadata suggests more items
756    /// Checks for common response structures: { data: { items: [...], total, limit } } or { items: [...], total, limit }
757    fn expand_example_items_if_needed(
758        _spec: &OpenApiSpec,
759        mut example: Value,
760        _persona: Option<&Persona>,
761        _schema_ref: Option<&ReferenceOr<Schema>>,
762    ) -> Value {
763        // Try to find items array and pagination metadata in the example
764        // Support both nested (data.items) and flat (items) structures
765        let has_nested_items = example
766            .get("data")
767            .and_then(|v| v.as_object())
768            .map(|obj| obj.contains_key("items"))
769            .unwrap_or(false);
770
771        let has_flat_items = example.get("items").is_some();
772
773        if !has_nested_items && !has_flat_items {
774            return example; // No items array found
775        }
776
777        // Extract pagination metadata
778        let total = example
779            .get("data")
780            .and_then(|d| d.get("total"))
781            .or_else(|| example.get("total"))
782            .and_then(|v| v.as_u64().or_else(|| v.as_i64().map(|i| i as u64)));
783
784        let limit = example
785            .get("data")
786            .and_then(|d| d.get("limit"))
787            .or_else(|| example.get("limit"))
788            .and_then(|v| v.as_u64().or_else(|| v.as_i64().map(|i| i as u64)));
789
790        // Get current items array
791        let items_array = example
792            .get("data")
793            .and_then(|d| d.get("items"))
794            .or_else(|| example.get("items"))
795            .and_then(|v| v.as_array())
796            .cloned();
797
798        if let (Some(total_val), Some(limit_val), Some(mut items)) = (total, limit, items_array) {
799            let current_count = items.len() as u64;
800            let expected_count = std::cmp::min(total_val, limit_val);
801            let max_items = 100; // Cap at reasonable maximum
802            let expected_count = std::cmp::min(expected_count, max_items);
803
804            // If items array is smaller than expected, expand it
805            if current_count < expected_count && !items.is_empty() {
806                tracing::debug!(
807                    "Expanding example items array: {} -> {} items (total={}, limit={})",
808                    current_count,
809                    expected_count,
810                    total_val,
811                    limit_val
812                );
813
814                // Use first item as template
815                let template = items[0].clone();
816                let additional_count = expected_count - current_count;
817
818                // Generate additional items
819                for i in 0..additional_count {
820                    let mut new_item = template.clone();
821                    // Use the centralized variation function
822                    let item_index = current_count + i + 1;
823                    Self::add_item_variation(&mut new_item, item_index);
824                    items.push(new_item);
825                }
826
827                // Update the items array in the example
828                if let Some(data_obj) = example.get_mut("data").and_then(|v| v.as_object_mut()) {
829                    data_obj.insert("items".to_string(), Value::Array(items));
830                } else if let Some(root_obj) = example.as_object_mut() {
831                    root_obj.insert("items".to_string(), Value::Array(items));
832                }
833            }
834        }
835
836        example
837    }
838
839    /// Generate example responses from OpenAPI examples
840    pub fn generate_from_examples(
841        response: &Response,
842        content_type: Option<&str>,
843    ) -> Result<Option<Value>> {
844        use openapiv3::ReferenceOr;
845
846        // If content_type is specified, look for examples in that media type
847        if let Some(content_type) = content_type {
848            if let Some(media_type) = response.content.get(content_type) {
849                // Check for single example first
850                if let Some(example) = &media_type.example {
851                    return Ok(Some(example.clone()));
852                }
853
854                // Check for multiple examples
855                for (_, example_ref) in &media_type.examples {
856                    if let ReferenceOr::Item(example) = example_ref {
857                        if let Some(value) = &example.value {
858                            return Ok(Some(value.clone()));
859                        }
860                    }
861                    // Reference resolution would require spec parameter to be added to this function
862                }
863            }
864        }
865
866        // If no content_type specified or not found, check all media types
867        for (_, media_type) in &response.content {
868            // Check for single example first
869            if let Some(example) = &media_type.example {
870                return Ok(Some(example.clone()));
871            }
872
873            // Check for multiple examples
874            for (_, example_ref) in &media_type.examples {
875                if let ReferenceOr::Item(example) = example_ref {
876                    if let Some(value) = &example.value {
877                        return Ok(Some(value.clone()));
878                    }
879                }
880                // Reference resolution would require spec parameter to be added to this function
881            }
882        }
883
884        Ok(None)
885    }
886
887    /// Expand templates like {{now}} and {{uuid}} in JSON values
888    fn expand_templates(value: &Value) -> Value {
889        match value {
890            Value::String(s) => {
891                let expanded = s
892                    .replace("{{now}}", &chrono::Utc::now().to_rfc3339())
893                    .replace("{{uuid}}", &uuid::Uuid::new_v4().to_string());
894                Value::String(expanded)
895            }
896            Value::Object(map) => {
897                let mut new_map = serde_json::Map::new();
898                for (key, val) in map {
899                    new_map.insert(key.clone(), Self::expand_templates(val));
900                }
901                Value::Object(new_map)
902            }
903            Value::Array(arr) => {
904                let new_arr: Vec<Value> = arr.iter().map(Self::expand_templates).collect();
905                Value::Array(new_arr)
906            }
907            _ => value.clone(),
908        }
909    }
910}
911
912/// Mock response data
913#[derive(Debug, Clone)]
914pub struct MockResponse {
915    /// HTTP status code
916    pub status_code: u16,
917    /// Response headers
918    pub headers: HashMap<String, String>,
919    /// Response body
920    pub body: Option<Value>,
921}
922
923impl MockResponse {
924    /// Create a new mock response
925    pub fn new(status_code: u16) -> Self {
926        Self {
927            status_code,
928            headers: HashMap::new(),
929            body: None,
930        }
931    }
932
933    /// Add a header to the response
934    pub fn with_header(mut self, name: String, value: String) -> Self {
935        self.headers.insert(name, value);
936        self
937    }
938
939    /// Set the response body
940    pub fn with_body(mut self, body: Value) -> Self {
941        self.body = Some(body);
942        self
943    }
944}
945
946/// OpenAPI security requirement wrapper
947#[derive(Debug, Clone)]
948pub struct OpenApiSecurityRequirement {
949    /// The security scheme name
950    pub scheme: String,
951    /// Required scopes (for OAuth2)
952    pub scopes: Vec<String>,
953}
954
955impl OpenApiSecurityRequirement {
956    /// Create a new security requirement
957    pub fn new(scheme: String, scopes: Vec<String>) -> Self {
958        Self { scheme, scopes }
959    }
960}
961
962/// OpenAPI operation wrapper with path context
963#[derive(Debug, Clone)]
964pub struct OpenApiOperation {
965    /// The HTTP method
966    pub method: String,
967    /// The path this operation belongs to
968    pub path: String,
969    /// The OpenAPI operation
970    pub operation: Operation,
971}
972
973impl OpenApiOperation {
974    /// Create a new OpenApiOperation
975    pub fn new(method: String, path: String, operation: Operation) -> Self {
976        Self {
977            method,
978            path,
979            operation,
980        }
981    }
982}
983
984#[cfg(test)]
985mod tests {
986    use super::*;
987    use openapiv3::ReferenceOr;
988    use serde_json::json;
989
990    // Mock AI generator for testing
991    struct MockAiGenerator {
992        response: Value,
993    }
994
995    #[async_trait]
996    impl AiGenerator for MockAiGenerator {
997        async fn generate(&self, _prompt: &str, _config: &AiResponseConfig) -> Result<Value> {
998            Ok(self.response.clone())
999        }
1000    }
1001
1002    #[test]
1003    fn generates_example_using_referenced_schemas() {
1004        let yaml = r#"
1005openapi: 3.0.3
1006info:
1007  title: Test API
1008  version: "1.0.0"
1009paths:
1010  /apiaries:
1011    get:
1012      responses:
1013        '200':
1014          description: ok
1015          content:
1016            application/json:
1017              schema:
1018                $ref: '#/components/schemas/Apiary'
1019components:
1020  schemas:
1021    Apiary:
1022      type: object
1023      properties:
1024        id:
1025          type: string
1026        hive:
1027          $ref: '#/components/schemas/Hive'
1028    Hive:
1029      type: object
1030      properties:
1031        name:
1032          type: string
1033        active:
1034          type: boolean
1035        "#;
1036
1037        let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("load spec");
1038        let path_item = spec
1039            .spec
1040            .paths
1041            .paths
1042            .get("/apiaries")
1043            .and_then(ReferenceOr::as_item)
1044            .expect("path item");
1045        let operation = path_item.get.as_ref().expect("GET operation");
1046
1047        let response =
1048            ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1049                .expect("generate response");
1050
1051        let obj = response.as_object().expect("response object");
1052        assert!(obj.contains_key("id"));
1053        let hive = obj.get("hive").and_then(|value| value.as_object()).expect("hive object");
1054        assert!(hive.contains_key("name"));
1055        assert!(hive.contains_key("active"));
1056    }
1057
1058    /// Round 33 (#822) — when `MOCKFORGE_INJECT_RESPONSE_VIOLATIONS`
1059    /// is unset, synthesized 2xx bodies are untouched. Single test
1060    /// path (no env mutation) so we don't race the rest of the suite.
1061    /// The injection helper is unit-tested directly below.
1062    #[test]
1063    fn inject_response_violations_off_by_default_leaves_required_fields() {
1064        let yaml = r#"
1065openapi: 3.0.3
1066info:
1067  title: t
1068  version: "1"
1069paths:
1070  /thing:
1071    get:
1072      responses:
1073        '200':
1074          description: ok
1075          content:
1076            application/json:
1077              schema:
1078                $ref: '#/components/schemas/Thing'
1079components:
1080  schemas:
1081    Thing:
1082      type: object
1083      required: [must_have]
1084      properties:
1085        must_have:
1086          type: string
1087        optional:
1088          type: string
1089        "#;
1090        let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("load");
1091        let path_item = spec
1092            .spec
1093            .paths
1094            .paths
1095            .get("/thing")
1096            .and_then(ReferenceOr::as_item)
1097            .expect("path");
1098        let op = path_item.get.as_ref().expect("op");
1099        let body =
1100            ResponseGenerator::generate_response(&spec, op, 200, Some("application/json")).unwrap();
1101        assert!(body.as_object().unwrap().contains_key("must_have"));
1102    }
1103
1104    /// Round 33 (#822) — `maybe_inject_response_violation` drops the
1105    /// first required field when the env flag would be on. Drive the
1106    /// inner helper directly so we don't have to flip a process env
1107    /// var. (The helper short-circuits on the env check before calling
1108    /// `first_required_field_for_status`, so we sidestep it by reusing
1109    /// the body-mutation portion.)
1110    #[test]
1111    fn first_required_field_for_status_finds_required_in_referenced_schema() {
1112        let yaml = r#"
1113openapi: 3.0.3
1114info:
1115  title: t
1116  version: "1"
1117paths:
1118  /thing:
1119    get:
1120      responses:
1121        '200':
1122          description: ok
1123          content:
1124            application/json:
1125              schema:
1126                $ref: '#/components/schemas/Thing'
1127components:
1128  schemas:
1129    Thing:
1130      type: object
1131      required: [comment, location]
1132      properties:
1133        comment:
1134          type: string
1135        location:
1136          type: string
1137        "#;
1138        let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("load");
1139        let path_item = spec
1140            .spec
1141            .paths
1142            .paths
1143            .get("/thing")
1144            .and_then(ReferenceOr::as_item)
1145            .expect("path");
1146        let op = path_item.get.as_ref().expect("op");
1147        let first = ResponseGenerator::first_required_field_for_status(&spec, op, 200);
1148        assert_eq!(first.as_deref(), Some("comment"), "first declared required field is picked");
1149    }
1150
1151    /// Round 33 (#822) — non-2xx responses and Object-schemas without
1152    /// any `required` array don't surface a field to drop.
1153    #[test]
1154    fn first_required_field_returns_none_when_no_required_or_non_2xx() {
1155        let yaml = r#"
1156openapi: 3.0.3
1157info:
1158  title: t
1159  version: "1"
1160paths:
1161  /thing:
1162    get:
1163      responses:
1164        '200':
1165          description: ok
1166          content:
1167            application/json:
1168              schema:
1169                type: object
1170                properties:
1171                  whatever:
1172                    type: string
1173        '500':
1174          description: err
1175          content:
1176            application/json:
1177              schema:
1178                type: object
1179                required: [code]
1180                properties:
1181                  code:
1182                    type: string
1183        "#;
1184        let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("load");
1185        let path_item = spec
1186            .spec
1187            .paths
1188            .paths
1189            .get("/thing")
1190            .and_then(ReferenceOr::as_item)
1191            .expect("path");
1192        let op = path_item.get.as_ref().expect("op");
1193        // 200 has no required → None.
1194        assert!(ResponseGenerator::first_required_field_for_status(&spec, op, 200).is_none());
1195        // 500 has required but the injection helper would short-circuit
1196        // on the 2xx check before consulting it; we still confirm the
1197        // schema walker itself returns the field if asked.
1198        assert_eq!(
1199            ResponseGenerator::first_required_field_for_status(&spec, op, 500).as_deref(),
1200            Some("code")
1201        );
1202    }
1203
1204    #[tokio::test]
1205    async fn test_generate_ai_response_with_generator() {
1206        let ai_config = AiResponseConfig {
1207            enabled: true,
1208            mode: mockforge_foundation::ai_response::AiResponseMode::Intelligent,
1209            prompt: Some("Generate a response for {{method}} {{path}}".to_string()),
1210            context: None,
1211            temperature: 0.7,
1212            max_tokens: 1000,
1213            schema: None,
1214            cache_enabled: true,
1215        };
1216        let context = RequestContext {
1217            method: "GET".to_string(),
1218            path: "/api/users".to_string(),
1219            path_params: HashMap::new(),
1220            query_params: HashMap::new(),
1221            headers: HashMap::new(),
1222            body: None,
1223            multipart_fields: HashMap::new(),
1224            multipart_files: HashMap::new(),
1225        };
1226        let mock_generator = MockAiGenerator {
1227            response: json!({"message": "Generated response"}),
1228        };
1229
1230        let result =
1231            ResponseGenerator::generate_ai_response(&ai_config, &context, Some(&mock_generator))
1232                .await;
1233
1234        assert!(result.is_ok());
1235        let value = result.unwrap();
1236        assert_eq!(value["message"], "Generated response");
1237    }
1238
1239    #[tokio::test]
1240    async fn test_generate_ai_response_without_generator() {
1241        let ai_config = AiResponseConfig {
1242            enabled: true,
1243            mode: mockforge_foundation::ai_response::AiResponseMode::Intelligent,
1244            prompt: Some("Generate a response for {{method}} {{path}}".to_string()),
1245            context: None,
1246            temperature: 0.7,
1247            max_tokens: 1000,
1248            schema: None,
1249            cache_enabled: true,
1250        };
1251        let context = RequestContext {
1252            method: "POST".to_string(),
1253            path: "/api/users".to_string(),
1254            path_params: HashMap::new(),
1255            query_params: HashMap::new(),
1256            headers: HashMap::new(),
1257            body: None,
1258            multipart_fields: HashMap::new(),
1259            multipart_files: HashMap::new(),
1260        };
1261
1262        let result = ResponseGenerator::generate_ai_response(&ai_config, &context, None).await;
1263
1264        // Without a generator, generate_ai_response returns an error
1265        assert!(result.is_err());
1266        let err = result.unwrap_err().to_string();
1267        assert!(
1268            err.contains("no AI generator configured"),
1269            "Expected 'no AI generator configured' error, got: {}",
1270            err
1271        );
1272    }
1273
1274    #[tokio::test]
1275    async fn test_generate_ai_response_no_prompt() {
1276        let ai_config = AiResponseConfig {
1277            enabled: true,
1278            mode: mockforge_foundation::ai_response::AiResponseMode::Intelligent,
1279            prompt: None,
1280            context: None,
1281            temperature: 0.7,
1282            max_tokens: 1000,
1283            schema: None,
1284            cache_enabled: true,
1285        };
1286        let context = RequestContext {
1287            method: "GET".to_string(),
1288            path: "/api/test".to_string(),
1289            path_params: HashMap::new(),
1290            query_params: HashMap::new(),
1291            headers: HashMap::new(),
1292            body: None,
1293            multipart_fields: HashMap::new(),
1294            multipart_files: HashMap::new(),
1295        };
1296
1297        let result = ResponseGenerator::generate_ai_response(&ai_config, &context, None).await;
1298
1299        assert!(result.is_err());
1300    }
1301
1302    #[test]
1303    fn test_generate_response_with_expansion() {
1304        let spec = OpenApiSpec::from_string(
1305            r#"openapi: 3.0.0
1306info:
1307  title: Test API
1308  version: 1.0.0
1309paths:
1310  /users:
1311    get:
1312      responses:
1313        '200':
1314          description: OK
1315          content:
1316            application/json:
1317              schema:
1318                type: object
1319                properties:
1320                  id:
1321                    type: integer
1322                  name:
1323                    type: string
1324"#,
1325            Some("yaml"),
1326        )
1327        .unwrap();
1328
1329        let operation = spec
1330            .spec
1331            .paths
1332            .paths
1333            .get("/users")
1334            .and_then(|p| p.as_item())
1335            .and_then(|p| p.get.as_ref())
1336            .unwrap();
1337
1338        let response = ResponseGenerator::generate_response_with_expansion(
1339            &spec,
1340            operation,
1341            200,
1342            Some("application/json"),
1343            true,
1344        )
1345        .unwrap();
1346
1347        assert!(response.is_object());
1348    }
1349
1350    #[test]
1351    fn test_generate_response_with_scenario() {
1352        let spec = OpenApiSpec::from_string(
1353            r#"openapi: 3.0.0
1354info:
1355  title: Test API
1356  version: 1.0.0
1357paths:
1358  /users:
1359    get:
1360      responses:
1361        '200':
1362          description: OK
1363          content:
1364            application/json:
1365              examples:
1366                happy:
1367                  value:
1368                    id: 1
1369                    name: "Happy User"
1370                sad:
1371                  value:
1372                    id: 2
1373                    name: "Sad User"
1374"#,
1375            Some("yaml"),
1376        )
1377        .unwrap();
1378
1379        let operation = spec
1380            .spec
1381            .paths
1382            .paths
1383            .get("/users")
1384            .and_then(|p| p.as_item())
1385            .and_then(|p| p.get.as_ref())
1386            .unwrap();
1387
1388        let response = ResponseGenerator::generate_response_with_scenario(
1389            &spec,
1390            operation,
1391            200,
1392            Some("application/json"),
1393            false,
1394            Some("happy"),
1395        )
1396        .unwrap();
1397
1398        assert_eq!(response["id"], 1);
1399        assert_eq!(response["name"], "Happy User");
1400    }
1401
1402    #[test]
1403    fn test_generate_response_with_referenced_response() {
1404        let spec = OpenApiSpec::from_string(
1405            r#"openapi: 3.0.0
1406info:
1407  title: Test API
1408  version: 1.0.0
1409paths:
1410  /users:
1411    get:
1412      responses:
1413        '200':
1414          $ref: '#/components/responses/UserResponse'
1415components:
1416  responses:
1417    UserResponse:
1418      description: User response
1419      content:
1420        application/json:
1421          schema:
1422            type: object
1423            properties:
1424              id:
1425                type: integer
1426              name:
1427                type: string
1428"#,
1429            Some("yaml"),
1430        )
1431        .unwrap();
1432
1433        let operation = spec
1434            .spec
1435            .paths
1436            .paths
1437            .get("/users")
1438            .and_then(|p| p.as_item())
1439            .and_then(|p| p.get.as_ref())
1440            .unwrap();
1441
1442        let response =
1443            ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1444                .unwrap();
1445
1446        assert!(response.is_object());
1447    }
1448
1449    #[test]
1450    fn test_generate_response_with_default_status() {
1451        let spec = OpenApiSpec::from_string(
1452            r#"openapi: 3.0.0
1453info:
1454  title: Test API
1455  version: 1.0.0
1456paths:
1457  /users:
1458    get:
1459      responses:
1460        '200':
1461          description: OK
1462        default:
1463          description: Error
1464          content:
1465            application/json:
1466              schema:
1467                type: object
1468                properties:
1469                  error:
1470                    type: string
1471"#,
1472            Some("yaml"),
1473        )
1474        .unwrap();
1475
1476        let operation = spec
1477            .spec
1478            .paths
1479            .paths
1480            .get("/users")
1481            .and_then(|p| p.as_item())
1482            .and_then(|p| p.get.as_ref())
1483            .unwrap();
1484
1485        // Use default response for 500 status
1486        let response =
1487            ResponseGenerator::generate_response(&spec, operation, 500, Some("application/json"))
1488                .unwrap();
1489
1490        assert!(response.is_object());
1491    }
1492
1493    #[test]
1494    fn test_generate_response_with_example_in_media_type() {
1495        let spec = OpenApiSpec::from_string(
1496            r#"openapi: 3.0.0
1497info:
1498  title: Test API
1499  version: 1.0.0
1500paths:
1501  /users:
1502    get:
1503      responses:
1504        '200':
1505          description: OK
1506          content:
1507            application/json:
1508              example:
1509                id: 1
1510                name: "Example User"
1511"#,
1512            Some("yaml"),
1513        )
1514        .unwrap();
1515
1516        let operation = spec
1517            .spec
1518            .paths
1519            .paths
1520            .get("/users")
1521            .and_then(|p| p.as_item())
1522            .and_then(|p| p.get.as_ref())
1523            .unwrap();
1524
1525        let response =
1526            ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1527                .unwrap();
1528
1529        assert_eq!(response["id"], 1);
1530        assert_eq!(response["name"], "Example User");
1531    }
1532
1533    #[test]
1534    fn test_generate_response_with_schema_example() {
1535        let spec = OpenApiSpec::from_string(
1536            r#"openapi: 3.0.0
1537info:
1538  title: Test API
1539  version: 1.0.0
1540paths:
1541  /users:
1542    get:
1543      responses:
1544        '200':
1545          description: OK
1546          content:
1547            application/json:
1548              schema:
1549                type: object
1550                example:
1551                  id: 42
1552                  name: "Schema Example"
1553                properties:
1554                  id:
1555                    type: integer
1556                  name:
1557                    type: string
1558"#,
1559            Some("yaml"),
1560        )
1561        .unwrap();
1562
1563        let operation = spec
1564            .spec
1565            .paths
1566            .paths
1567            .get("/users")
1568            .and_then(|p| p.as_item())
1569            .and_then(|p| p.get.as_ref())
1570            .unwrap();
1571
1572        let response =
1573            ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1574                .unwrap();
1575
1576        // Should use schema example if available
1577        assert!(response.is_object());
1578    }
1579
1580    #[test]
1581    fn test_generate_response_with_referenced_schema() {
1582        let spec = OpenApiSpec::from_string(
1583            r#"openapi: 3.0.0
1584info:
1585  title: Test API
1586  version: 1.0.0
1587paths:
1588  /users:
1589    get:
1590      responses:
1591        '200':
1592          description: OK
1593          content:
1594            application/json:
1595              schema:
1596                $ref: '#/components/schemas/User'
1597components:
1598  schemas:
1599    User:
1600      type: object
1601      properties:
1602        id:
1603          type: integer
1604        name:
1605          type: string
1606"#,
1607            Some("yaml"),
1608        )
1609        .unwrap();
1610
1611        let operation = spec
1612            .spec
1613            .paths
1614            .paths
1615            .get("/users")
1616            .and_then(|p| p.as_item())
1617            .and_then(|p| p.get.as_ref())
1618            .unwrap();
1619
1620        let response =
1621            ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1622                .unwrap();
1623
1624        assert!(response.is_object());
1625        assert!(response.get("id").is_some());
1626        assert!(response.get("name").is_some());
1627    }
1628
1629    #[test]
1630    fn test_generate_response_with_array_schema() {
1631        let spec = OpenApiSpec::from_string(
1632            r#"openapi: 3.0.0
1633info:
1634  title: Test API
1635  version: 1.0.0
1636paths:
1637  /users:
1638    get:
1639      responses:
1640        '200':
1641          description: OK
1642          content:
1643            application/json:
1644              schema:
1645                type: array
1646                items:
1647                  type: object
1648                  properties:
1649                    id:
1650                      type: integer
1651                    name:
1652                      type: string
1653"#,
1654            Some("yaml"),
1655        )
1656        .unwrap();
1657
1658        let operation = spec
1659            .spec
1660            .paths
1661            .paths
1662            .get("/users")
1663            .and_then(|p| p.as_item())
1664            .and_then(|p| p.get.as_ref())
1665            .unwrap();
1666
1667        let response =
1668            ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1669                .unwrap();
1670
1671        assert!(response.is_array());
1672    }
1673
1674    #[test]
1675    fn test_generate_response_with_different_content_types() {
1676        let spec = OpenApiSpec::from_string(
1677            r#"openapi: 3.0.0
1678info:
1679  title: Test API
1680  version: 1.0.0
1681paths:
1682  /users:
1683    get:
1684      responses:
1685        '200':
1686          description: OK
1687          content:
1688            application/json:
1689              schema:
1690                type: object
1691            text/plain:
1692              schema:
1693                type: string
1694"#,
1695            Some("yaml"),
1696        )
1697        .unwrap();
1698
1699        let operation = spec
1700            .spec
1701            .paths
1702            .paths
1703            .get("/users")
1704            .and_then(|p| p.as_item())
1705            .and_then(|p| p.get.as_ref())
1706            .unwrap();
1707
1708        // Test JSON content type
1709        let json_response =
1710            ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1711                .unwrap();
1712        assert!(json_response.is_object());
1713
1714        // Test text/plain content type
1715        let text_response =
1716            ResponseGenerator::generate_response(&spec, operation, 200, Some("text/plain"))
1717                .unwrap();
1718        assert!(text_response.is_string());
1719    }
1720
1721    #[test]
1722    fn test_generate_response_without_content_type() {
1723        let spec = OpenApiSpec::from_string(
1724            r#"openapi: 3.0.0
1725info:
1726  title: Test API
1727  version: 1.0.0
1728paths:
1729  /users:
1730    get:
1731      responses:
1732        '200':
1733          description: OK
1734          content:
1735            application/json:
1736              schema:
1737                type: object
1738                properties:
1739                  id:
1740                    type: integer
1741"#,
1742            Some("yaml"),
1743        )
1744        .unwrap();
1745
1746        let operation = spec
1747            .spec
1748            .paths
1749            .paths
1750            .get("/users")
1751            .and_then(|p| p.as_item())
1752            .and_then(|p| p.get.as_ref())
1753            .unwrap();
1754
1755        // No content type specified - should use first available
1756        let response = ResponseGenerator::generate_response(&spec, operation, 200, None).unwrap();
1757
1758        assert!(response.is_object());
1759    }
1760
1761    #[test]
1762    fn test_generate_response_with_no_content() {
1763        let spec = OpenApiSpec::from_string(
1764            r#"openapi: 3.0.0
1765info:
1766  title: Test API
1767  version: 1.0.0
1768paths:
1769  /users:
1770    delete:
1771      responses:
1772        '204':
1773          description: No Content
1774"#,
1775            Some("yaml"),
1776        )
1777        .unwrap();
1778
1779        let operation = spec
1780            .spec
1781            .paths
1782            .paths
1783            .get("/users")
1784            .and_then(|p| p.as_item())
1785            .and_then(|p| p.delete.as_ref())
1786            .unwrap();
1787
1788        let response = ResponseGenerator::generate_response(&spec, operation, 204, None).unwrap();
1789
1790        // Should return empty object for no content
1791        assert!(response.is_object());
1792        assert!(response.as_object().unwrap().is_empty());
1793    }
1794
1795    #[test]
1796    fn test_generate_response_with_expansion_disabled() {
1797        let spec = OpenApiSpec::from_string(
1798            r#"openapi: 3.0.0
1799info:
1800  title: Test API
1801  version: 1.0.0
1802paths:
1803  /users:
1804    get:
1805      responses:
1806        '200':
1807          description: OK
1808          content:
1809            application/json:
1810              schema:
1811                type: object
1812                properties:
1813                  id:
1814                    type: integer
1815                  name:
1816                    type: string
1817"#,
1818            Some("yaml"),
1819        )
1820        .unwrap();
1821
1822        let operation = spec
1823            .spec
1824            .paths
1825            .paths
1826            .get("/users")
1827            .and_then(|p| p.as_item())
1828            .and_then(|p| p.get.as_ref())
1829            .unwrap();
1830
1831        let response = ResponseGenerator::generate_response_with_expansion(
1832            &spec,
1833            operation,
1834            200,
1835            Some("application/json"),
1836            false, // No expansion
1837        )
1838        .unwrap();
1839
1840        assert!(response.is_object());
1841    }
1842
1843    #[test]
1844    fn test_generate_response_with_array_schema_referenced_items() {
1845        // Test array schema with referenced item schema (lines 1035-1046)
1846        let spec = OpenApiSpec::from_string(
1847            r#"openapi: 3.0.0
1848info:
1849  title: Test API
1850  version: 1.0.0
1851paths:
1852  /items:
1853    get:
1854      responses:
1855        '200':
1856          description: OK
1857          content:
1858            application/json:
1859              schema:
1860                type: array
1861                items:
1862                  $ref: '#/components/schemas/Item'
1863components:
1864  schemas:
1865    Item:
1866      type: object
1867      properties:
1868        id:
1869          type: string
1870        name:
1871          type: string
1872"#,
1873            Some("yaml"),
1874        )
1875        .unwrap();
1876
1877        let operation = spec
1878            .spec
1879            .paths
1880            .paths
1881            .get("/items")
1882            .and_then(|p| p.as_item())
1883            .and_then(|p| p.get.as_ref())
1884            .unwrap();
1885
1886        let response =
1887            ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1888                .unwrap();
1889
1890        // Should generate an array with items from referenced schema
1891        let arr = response.as_array().expect("response should be array");
1892        assert!(!arr.is_empty());
1893        if let Some(item) = arr.first() {
1894            let obj = item.as_object().expect("item should be object");
1895            assert!(obj.contains_key("id") || obj.contains_key("name"));
1896        }
1897    }
1898
1899    #[test]
1900    fn test_generate_response_with_array_schema_missing_reference() {
1901        // Test array schema with missing referenced item schema (line 1045)
1902        let spec = OpenApiSpec::from_string(
1903            r#"openapi: 3.0.0
1904info:
1905  title: Test API
1906  version: 1.0.0
1907paths:
1908  /items:
1909    get:
1910      responses:
1911        '200':
1912          description: OK
1913          content:
1914            application/json:
1915              schema:
1916                type: array
1917                items:
1918                  $ref: '#/components/schemas/NonExistentItem'
1919components:
1920  schemas: {}
1921"#,
1922            Some("yaml"),
1923        )
1924        .unwrap();
1925
1926        let operation = spec
1927            .spec
1928            .paths
1929            .paths
1930            .get("/items")
1931            .and_then(|p| p.as_item())
1932            .and_then(|p| p.get.as_ref())
1933            .unwrap();
1934
1935        let response =
1936            ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1937                .unwrap();
1938
1939        // Should generate an array with empty objects when reference not found
1940        let arr = response.as_array().expect("response should be array");
1941        assert!(!arr.is_empty());
1942    }
1943
1944    #[test]
1945    fn test_generate_response_with_array_example_and_pagination() {
1946        // Test array generation with pagination using example items (lines 1114-1126)
1947        let spec = OpenApiSpec::from_string(
1948            r#"openapi: 3.0.0
1949info:
1950  title: Test API
1951  version: 1.0.0
1952paths:
1953  /products:
1954    get:
1955      responses:
1956        '200':
1957          description: OK
1958          content:
1959            application/json:
1960              schema:
1961                type: array
1962                example: [{"id": 1, "name": "Product 1"}]
1963                items:
1964                  type: object
1965                  properties:
1966                    id:
1967                      type: integer
1968                    name:
1969                      type: string
1970"#,
1971            Some("yaml"),
1972        )
1973        .unwrap();
1974
1975        let operation = spec
1976            .spec
1977            .paths
1978            .paths
1979            .get("/products")
1980            .and_then(|p| p.as_item())
1981            .and_then(|p| p.get.as_ref())
1982            .unwrap();
1983
1984        let response =
1985            ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1986                .unwrap();
1987
1988        // Should generate an array using the example as template
1989        let arr = response.as_array().expect("response should be array");
1990        assert!(!arr.is_empty());
1991        if let Some(item) = arr.first() {
1992            let obj = item.as_object().expect("item should be object");
1993            assert!(obj.contains_key("id") || obj.contains_key("name"));
1994        }
1995    }
1996
1997    #[test]
1998    fn test_generate_response_with_missing_response_reference() {
1999        // Test response generation with missing response reference (lines 294-298)
2000        let spec = OpenApiSpec::from_string(
2001            r#"openapi: 3.0.0
2002info:
2003  title: Test API
2004  version: 1.0.0
2005paths:
2006  /users:
2007    get:
2008      responses:
2009        '200':
2010          $ref: '#/components/responses/NonExistentResponse'
2011components:
2012  responses: {}
2013"#,
2014            Some("yaml"),
2015        )
2016        .unwrap();
2017
2018        let operation = spec
2019            .spec
2020            .paths
2021            .paths
2022            .get("/users")
2023            .and_then(|p| p.as_item())
2024            .and_then(|p| p.get.as_ref())
2025            .unwrap();
2026
2027        let response =
2028            ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2029                .unwrap();
2030
2031        // Should return empty object when reference not found
2032        assert!(response.is_object());
2033        assert!(response.as_object().unwrap().is_empty());
2034    }
2035
2036    #[test]
2037    fn test_generate_response_with_no_response_for_status() {
2038        // Test response generation when no response found for status code (lines 302-310)
2039        let spec = OpenApiSpec::from_string(
2040            r#"openapi: 3.0.0
2041info:
2042  title: Test API
2043  version: 1.0.0
2044paths:
2045  /users:
2046    get:
2047      responses:
2048        '404':
2049          description: Not found
2050"#,
2051            Some("yaml"),
2052        )
2053        .unwrap();
2054
2055        let operation = spec
2056            .spec
2057            .paths
2058            .paths
2059            .get("/users")
2060            .and_then(|p| p.as_item())
2061            .and_then(|p| p.get.as_ref())
2062            .unwrap();
2063
2064        // Request status code 200 but only 404 is defined
2065        let response =
2066            ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2067                .unwrap();
2068
2069        // Should return empty object when no response found
2070        assert!(response.is_object());
2071        assert!(response.as_object().unwrap().is_empty());
2072    }
2073}