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