Skip to main content

mockforge_openapi/
openapi_routes.rs

1//! OpenAPI-based route generation for MockForge
2//!
3//! The authoritative `OpenApiRouteRegistry` and all `build_router_*` methods
4//! are defined in this file. Sub-modules provide additional utilities:
5//! - `builder`: Helper functions for building routers from specs
6//! - `generation`: Route generation utilities
7//! - `validation`: Request/response validation types and logic
8//!
9//! Note: `registry` sub-module contains an abandoned partial refactoring with a
10//! duplicate `OpenApiRouteRegistry` type. Use the one from this module.
11
12pub mod builder;
13pub mod generation;
14#[doc(hidden)]
15pub mod registry;
16pub mod validation;
17
18use crate::response::AiGenerator;
19use crate::response_rewriter::ResponseRewriter;
20use crate::{OpenApiOperation, OpenApiRoute, OpenApiSchema, OpenApiSpec};
21use axum::extract::{DefaultBodyLimit, Path as AxumPath, RawQuery};
22use axum::http::HeaderMap;
23use axum::response::IntoResponse;
24use axum::routing::*;
25use axum::{Json, Router};
26pub use builder::*;
27use chrono::Utc;
28pub use generation::*;
29use mockforge_foundation::ai_response::RequestContext;
30use mockforge_foundation::error::{Error, Result};
31use mockforge_foundation::latency::LatencyInjector;
32use mockforge_foundation::response_generation_trace::ResponseGenerationTrace;
33use mockforge_foundation::schema_diff::validation_diff;
34use once_cell::sync::Lazy;
35use openapiv3::ParameterSchemaOrContent;
36use serde_json::{json, Map, Value};
37use std::collections::{HashMap, HashSet, VecDeque};
38use std::sync::{Arc, Mutex};
39use tracing;
40pub use validation::*;
41
42/// OpenAPI route registry that manages generated routes
43#[derive(Clone)]
44pub struct OpenApiRouteRegistry {
45    /// The OpenAPI specification
46    spec: Arc<OpenApiSpec>,
47    /// Generated routes
48    routes: Vec<OpenApiRoute>,
49    /// Validation options
50    options: ValidationOptions,
51    /// Custom fixture loader (optional)
52    custom_fixture_loader: Option<Arc<crate::custom_fixture::CustomFixtureLoader>>,
53}
54
55/// Validation mode for request/response validation
56#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, Default)]
57pub enum ValidationMode {
58    /// Validation is disabled (no checks performed)
59    Disabled,
60    /// Validation warnings are logged but do not fail requests
61    #[default]
62    Warn,
63    /// Validation failures return error responses
64    Enforce,
65}
66
67/// Options for configuring validation behavior
68#[derive(Debug, Clone)]
69pub struct ValidationOptions {
70    /// Validation mode for incoming requests
71    pub request_mode: ValidationMode,
72    /// Whether to aggregate multiple validation errors into a single response
73    pub aggregate_errors: bool,
74    /// Whether to validate outgoing responses against schemas
75    pub validate_responses: bool,
76    /// Per-operation validation mode overrides (operation ID -> mode)
77    pub overrides: HashMap<String, ValidationMode>,
78    /// Skip validation for request paths starting with any of these prefixes
79    pub admin_skip_prefixes: Vec<String>,
80    /// Expand templating tokens in responses/examples after generation
81    pub response_template_expand: bool,
82    /// HTTP status code to return for validation failures (e.g., 400 or 422)
83    pub validation_status: Option<u16>,
84}
85
86impl Default for ValidationOptions {
87    fn default() -> Self {
88        Self {
89            request_mode: ValidationMode::Enforce,
90            aggregate_errors: true,
91            validate_responses: false,
92            overrides: HashMap::new(),
93            admin_skip_prefixes: Vec::new(),
94            response_template_expand: false,
95            validation_status: None,
96        }
97    }
98}
99
100/// Shared context for all route handlers, encapsulating optional features.
101///
102/// Each `build_router_*` variant constructs a `RouterContext` with the appropriate
103/// features enabled, then delegates to `build_router_with_context`.
104#[derive(Clone)]
105pub struct RouterContext {
106    /// Custom fixture loader (highest priority response source)
107    pub custom_fixture_loader: Option<Arc<crate::custom_fixture::CustomFixtureLoader>>,
108    /// Latency injector (per-operation-tag latency simulation)
109    pub latency_injector: Option<LatencyInjector>,
110    /// Failure injector (per-tag fault injection)
111    pub failure_injector: Option<mockforge_foundation::failure_injection::FailureInjector>,
112    /// Response-body mutation hook — used for template token expansion
113    /// and override rule application. Core's concrete impl is
114    /// [`crate::openapi_rewriter::CoreResponseRewriter`].
115    pub response_rewriter: Option<Arc<dyn ResponseRewriter>>,
116    /// Whether override application is active (gates the
117    /// [`ResponseRewriter::apply_overrides`] call).
118    pub overrides_enabled: bool,
119    /// AI response generator
120    pub ai_generator: Option<Arc<dyn AiGenerator + Send + Sync>>,
121    /// MockAI intelligent behavior handle (type-erased via
122    /// [`mockforge_foundation::intelligent_behavior::MockAiBehavior`] so
123    /// the OpenAPI router doesn't depend on core's concrete `MockAI`).
124    pub mockai: Option<
125        Arc<
126            tokio::sync::RwLock<
127                dyn mockforge_foundation::intelligent_behavior::MockAiBehavior + Send + Sync,
128            >,
129        >,
130    >,
131    /// Enable full validation (422 enhanced responses, response validation, trace)
132    pub enable_full_validation: bool,
133    /// Enable template token expansion
134    pub enable_template_expand: bool,
135    /// Whether to add /openapi.json endpoint
136    pub add_spec_endpoint: bool,
137}
138
139impl Default for RouterContext {
140    fn default() -> Self {
141        Self {
142            custom_fixture_loader: None,
143            latency_injector: None,
144            failure_injector: None,
145            response_rewriter: None,
146            overrides_enabled: false,
147            ai_generator: None,
148            mockai: None,
149            enable_full_validation: false,
150            enable_template_expand: false,
151            add_spec_endpoint: true,
152        }
153    }
154}
155
156/// Maximum body bytes the OpenAPI router accepts. Axum's `DefaultBodyLimit`
157/// is 2 MiB out of the box; for an HTTP **mock** that's far too low —
158/// users routinely send fixture-sized JSON, multipart uploads, or
159/// chunked-transfer test payloads in the tens of MB. When the body
160/// exceeds the limit, axum's `Bytes` / `Option<Json<Value>>` extractors
161/// truncate the body and the handler runs without ever consuming the
162/// rest of the request, so hyper sends the response and SSL Close
163/// Notify *while the client is still uploading* — which is exactly the
164/// "200 OK before all chunk requests arrived" behaviour Srikanth caught
165/// on the 10 MB chunked PCAP for Issue #79.
166///
167/// Configurable via `MOCKFORGE_HTTP_BODY_LIMIT_MB`. Default 50 MiB,
168/// which covers the realistic mock-traffic range without giving an
169/// untrusted client an unlimited memory-fill vector.
170fn openapi_body_limit_bytes() -> usize {
171    const DEFAULT_MB: usize = 50;
172    std::env::var("MOCKFORGE_HTTP_BODY_LIMIT_MB")
173        .ok()
174        .and_then(|v| v.parse::<usize>().ok())
175        .unwrap_or(DEFAULT_MB)
176        .saturating_mul(1024 * 1024)
177}
178
179impl OpenApiRouteRegistry {
180    /// Create a new registry from an OpenAPI spec
181    pub fn new(spec: OpenApiSpec) -> Self {
182        Self::new_with_env(spec)
183    }
184
185    /// Create a new registry from an OpenAPI spec with environment-based validation options
186    ///
187    /// Options are read from environment variables:
188    /// - `MOCKFORGE_REQUEST_VALIDATION`: "off"/"warn"/"enforce" (default: "enforce")
189    /// - `MOCKFORGE_AGGREGATE_ERRORS`: "1"/"true" to aggregate errors (default: true)
190    /// - `MOCKFORGE_RESPONSE_VALIDATION`: "1"/"true" to validate responses (default: false)
191    /// - `MOCKFORGE_RESPONSE_TEMPLATE_EXPAND`: "1"/"true" to expand templates (default: false)
192    /// - `MOCKFORGE_VALIDATION_STATUS`: HTTP status code for validation failures (optional)
193    pub fn new_with_env(spec: OpenApiSpec) -> Self {
194        Self::new_with_env_and_persona(spec, None)
195    }
196
197    /// Create a new registry from an OpenAPI spec with environment-based validation options and persona
198    pub fn new_with_env_and_persona(
199        spec: OpenApiSpec,
200        persona: Option<Arc<mockforge_foundation::intelligent_behavior::Persona>>,
201    ) -> Self {
202        tracing::debug!("Creating OpenAPI route registry");
203        let spec = Arc::new(spec);
204        let routes = Self::generate_routes_with_persona(&spec, persona);
205        let options = ValidationOptions {
206            request_mode: match std::env::var("MOCKFORGE_REQUEST_VALIDATION")
207                .unwrap_or_else(|_| "enforce".into())
208                .to_ascii_lowercase()
209                .as_str()
210            {
211                "off" | "disable" | "disabled" => ValidationMode::Disabled,
212                "warn" | "warning" => ValidationMode::Warn,
213                _ => ValidationMode::Enforce,
214            },
215            aggregate_errors: std::env::var("MOCKFORGE_AGGREGATE_ERRORS")
216                .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
217                .unwrap_or(true),
218            validate_responses: std::env::var("MOCKFORGE_RESPONSE_VALIDATION")
219                .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
220                .unwrap_or(false),
221            overrides: HashMap::new(),
222            admin_skip_prefixes: Vec::new(),
223            response_template_expand: std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
224                .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
225                .unwrap_or(false),
226            validation_status: std::env::var("MOCKFORGE_VALIDATION_STATUS")
227                .ok()
228                .and_then(|s| s.parse::<u16>().ok()),
229        };
230        Self {
231            spec,
232            routes,
233            options,
234            custom_fixture_loader: None,
235        }
236    }
237
238    /// Construct with explicit options
239    pub fn new_with_options(spec: OpenApiSpec, options: ValidationOptions) -> Self {
240        Self::new_with_options_and_persona(spec, options, None)
241    }
242
243    /// Construct with explicit options and persona
244    pub fn new_with_options_and_persona(
245        spec: OpenApiSpec,
246        options: ValidationOptions,
247        persona: Option<Arc<mockforge_foundation::intelligent_behavior::Persona>>,
248    ) -> Self {
249        tracing::debug!("Creating OpenAPI route registry with custom options");
250        let spec = Arc::new(spec);
251        let routes = Self::generate_routes_with_persona(&spec, persona);
252        Self {
253            spec,
254            routes,
255            options,
256            custom_fixture_loader: None,
257        }
258    }
259
260    /// Set custom fixture loader
261    pub fn with_custom_fixture_loader(
262        mut self,
263        loader: Arc<crate::custom_fixture::CustomFixtureLoader>,
264    ) -> Self {
265        self.custom_fixture_loader = Some(loader);
266        self
267    }
268
269    /// Clone this registry for validation purposes (creates an independent copy)
270    ///
271    /// This is useful when you need a separate registry instance for validation
272    /// that won't interfere with the main registry's state.
273    pub fn clone_for_validation(&self) -> Self {
274        OpenApiRouteRegistry {
275            spec: self.spec.clone(),
276            routes: self.routes.clone(),
277            options: self.options.clone(),
278            custom_fixture_loader: self.custom_fixture_loader.clone(),
279        }
280    }
281
282    /// Generate routes from the OpenAPI specification with optional persona
283    fn generate_routes_with_persona(
284        spec: &Arc<OpenApiSpec>,
285        persona: Option<Arc<mockforge_foundation::intelligent_behavior::Persona>>,
286    ) -> Vec<OpenApiRoute> {
287        let mut routes = Vec::new();
288
289        let all_paths_ops = spec.all_paths_and_operations();
290        tracing::debug!("Generating routes from OpenAPI spec with {} paths", all_paths_ops.len());
291
292        for (path, operations) in all_paths_ops {
293            tracing::debug!("Processing path: {}", path);
294            for (method, operation) in operations {
295                routes.push(OpenApiRoute::from_operation_with_persona(
296                    &method,
297                    path.clone(),
298                    &operation,
299                    spec.clone(),
300                    persona.clone(),
301                ));
302            }
303        }
304
305        tracing::debug!("Generated {} total routes from OpenAPI spec", routes.len());
306        routes
307    }
308
309    /// Get all routes
310    pub fn routes(&self) -> &[OpenApiRoute] {
311        &self.routes
312    }
313
314    /// Get the OpenAPI specification
315    pub fn spec(&self) -> &OpenApiSpec {
316        &self.spec
317    }
318
319    /// Normalize an Axum path for dedup by replacing all `{param}` with `{_}`.
320    /// This ensures paths like `/func/{period}` and `/func/{date}` are treated as duplicates,
321    /// since Axum/matchit treats all path parameters as equivalent for routing.
322    fn normalize_path_for_dedup(path: &str) -> String {
323        let mut result = String::with_capacity(path.len());
324        let mut in_brace = false;
325        for ch in path.chars() {
326            if ch == '{' {
327                in_brace = true;
328                result.push_str("{_}");
329            } else if ch == '}' {
330                in_brace = false;
331            } else if !in_brace {
332                result.push(ch);
333            }
334        }
335        result
336    }
337
338    /// Returns deduplicated routes with their resolved Axum-compatible paths.
339    ///
340    /// Handles path validation, canonical param-name resolution (to prevent matchit panics
341    /// when two OpenAPI paths differ only in param names), and duplicate detection.
342    /// This is the shared preamble extracted from all `build_router_*` variants.
343    fn deduplicated_routes(&self) -> Vec<(String, &OpenApiRoute)> {
344        let mut result = Vec::new();
345        let mut registered_routes: HashSet<(String, String)> = HashSet::new();
346        let mut canonical_paths: HashMap<String, String> = HashMap::new();
347
348        for route in &self.routes {
349            if !route.is_valid_axum_path() {
350                tracing::warn!(
351                    "Skipping route with unsupported path syntax: {} {}",
352                    route.method,
353                    route.path
354                );
355                continue;
356            }
357            let axum_path = route.axum_path();
358            let normalized = Self::normalize_path_for_dedup(&axum_path);
359            let axum_path = canonical_paths
360                .entry(normalized.clone())
361                .or_insert_with(|| axum_path.clone())
362                .clone();
363            let route_key = (route.method.clone(), normalized);
364            if !registered_routes.insert(route_key) {
365                tracing::debug!(
366                    "Skipping duplicate route: {} {} (axum path: {})",
367                    route.method,
368                    route.path,
369                    axum_path
370                );
371                continue;
372            }
373            result.push((axum_path, route));
374        }
375        result
376    }
377
378    /// Register a handler on a router for the given HTTP method.
379    ///
380    /// Shared epilogue extracted from all `build_router_*` variants.
381    fn route_for_method<H, T>(router: Router, path: &str, method: &str, handler: H) -> Router
382    where
383        H: axum::handler::Handler<T, ()>,
384        T: 'static,
385    {
386        match method {
387            "GET" => router.route(path, get(handler)),
388            "POST" => router.route(path, post(handler)),
389            "PUT" => router.route(path, put(handler)),
390            "DELETE" => router.route(path, delete(handler)),
391            "PATCH" => router.route(path, patch(handler)),
392            "HEAD" => router.route(path, head(handler)),
393            "OPTIONS" => router.route(path, options(handler)),
394            _ => router,
395        }
396    }
397
398    /// Build an Axum router from the OpenAPI spec (simplified)
399    pub fn build_router(self) -> Router {
400        let ctx = RouterContext {
401            custom_fixture_loader: self.custom_fixture_loader.clone(),
402            enable_full_validation: true,
403            enable_template_expand: true,
404            add_spec_endpoint: true,
405            ..Default::default()
406        };
407        self.build_router_with_context(ctx)
408    }
409
410    /// Build an Axum router using a shared RouterContext.
411    ///
412    /// This is the unified router builder that all `build_router_*` variants
413    /// delegate to. The RouterContext controls which features are active.
414    fn build_router_with_context(self, ctx: RouterContext) -> Router {
415        let mut router = Router::new();
416        tracing::debug!("Building router from {} routes", self.routes.len());
417
418        let deduped = self.deduplicated_routes();
419        let ctx = Arc::new(ctx);
420        for (axum_path, route) in &deduped {
421            tracing::debug!("Adding route: {} {}", route.method, route.path);
422            let operation = route.operation.clone();
423            let method = route.method.clone();
424            let path_template = route.path.clone();
425            let validator = self.clone_for_validation();
426            let route_clone = (*route).clone();
427            let ctx = ctx.clone();
428
429            // Extract tags from operation for latency and failure injection
430            let mut operation_tags = operation.tags.clone();
431            if let Some(operation_id) = &operation.operation_id {
432                operation_tags.push(operation_id.clone());
433            }
434
435            // Unified handler: fixture -> failure -> latency -> scenario/override -> mock -> validate -> expand -> overrides -> trace -> response
436            let handler = move |AxumPath(path_params): AxumPath<HashMap<String, String>>,
437                                RawQuery(raw_query): RawQuery,
438                                headers: HeaderMap,
439                                body: axum::body::Bytes| async move {
440                tracing::debug!("Handling OpenAPI request: {} {}", method, path_template);
441
442                // (a) Check for custom fixture first (highest priority)
443                if let Some(ref loader) = ctx.custom_fixture_loader {
444                    use crate::request_fingerprint::RequestFingerprint;
445                    use axum::http::{Method, Uri};
446
447                    // Reconstruct the full path from template and params
448                    let mut request_path = path_template.clone();
449                    for (key, value) in &path_params {
450                        request_path = request_path.replace(&format!("{{{}}}", key), value);
451                    }
452
453                    // Normalize the path to match fixture normalization
454                    let normalized_request_path =
455                        crate::custom_fixture::CustomFixtureLoader::normalize_path(&request_path);
456
457                    // Build query string
458                    let query_string =
459                        raw_query.as_ref().map(|q| q.to_string()).unwrap_or_default();
460
461                    // Create URI for fingerprint
462                    // IMPORTANT: Use normalized path to match fixture paths
463                    let uri_str = if query_string.is_empty() {
464                        normalized_request_path.clone()
465                    } else {
466                        format!("{}?{}", normalized_request_path, query_string)
467                    };
468
469                    if let Ok(uri) = uri_str.parse::<Uri>() {
470                        let http_method =
471                            Method::from_bytes(method.as_bytes()).unwrap_or(Method::GET);
472                        let body_slice = if body.is_empty() {
473                            None
474                        } else {
475                            Some(body.as_ref())
476                        };
477                        let fingerprint =
478                            RequestFingerprint::new(http_method, &uri, &headers, body_slice);
479
480                        // Debug logging for fixture matching
481                        tracing::debug!(
482                            "Checking fixture for {} {} (template: '{}', request_path: '{}', normalized: '{}', fingerprint.path: '{}')",
483                            method,
484                            path_template,
485                            path_template,
486                            request_path,
487                            normalized_request_path,
488                            fingerprint.path
489                        );
490
491                        if let Some(custom_fixture) = loader.load_fixture(&fingerprint) {
492                            tracing::debug!(
493                                "Using custom fixture for {} {}",
494                                method,
495                                path_template
496                            );
497
498                            // Apply delay if specified
499                            if custom_fixture.delay_ms > 0 {
500                                tokio::time::sleep(tokio::time::Duration::from_millis(
501                                    custom_fixture.delay_ms,
502                                ))
503                                .await;
504                            }
505
506                            // Convert response to JSON string if needed
507                            let response_body = if custom_fixture.response.is_string() {
508                                custom_fixture.response.as_str().unwrap().to_string()
509                            } else {
510                                serde_json::to_string(&custom_fixture.response)
511                                    .unwrap_or_else(|_| "{}".to_string())
512                            };
513
514                            // Parse response body as JSON
515                            let json_value: Value = serde_json::from_str(&response_body)
516                                .unwrap_or_else(|_| serde_json::json!({}));
517
518                            // Build response with status and JSON body
519                            let status = axum::http::StatusCode::from_u16(custom_fixture.status)
520                                .unwrap_or(axum::http::StatusCode::OK);
521
522                            let mut response = (status, Json(json_value)).into_response();
523
524                            // Add custom headers to response
525                            let response_headers = response.headers_mut();
526                            for (key, value) in &custom_fixture.headers {
527                                if let (Ok(header_name), Ok(header_value)) = (
528                                    axum::http::HeaderName::from_bytes(key.as_bytes()),
529                                    axum::http::HeaderValue::from_str(value),
530                                ) {
531                                    response_headers.insert(header_name, header_value);
532                                }
533                            }
534
535                            // Ensure content-type is set if not already present
536                            if !custom_fixture.headers.contains_key("content-type") {
537                                response_headers.insert(
538                                    axum::http::header::CONTENT_TYPE,
539                                    axum::http::HeaderValue::from_static("application/json"),
540                                );
541                            }
542
543                            return response;
544                        }
545                    }
546                }
547
548                // (b) Failure injection (if configured)
549                if let Some(ref failure_injector) = ctx.failure_injector {
550                    if let Some((status_code, error_message)) =
551                        failure_injector.process_request(&operation_tags)
552                    {
553                        let payload = serde_json::json!({
554                            "error": error_message,
555                            "injected_failure": true
556                        });
557                        let body_bytes = serde_json::to_vec(&payload)
558                            .unwrap_or_else(|_| br#"{"error":"injected failure"}"#.to_vec());
559                        return axum::http::Response::builder()
560                            .status(
561                                axum::http::StatusCode::from_u16(status_code)
562                                    .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR),
563                            )
564                            .header(axum::http::header::CONTENT_TYPE, "application/json")
565                            .body(axum::body::Body::from(body_bytes))
566                            .expect("Response builder should create valid response");
567                    }
568                }
569
570                // (c) Latency injection (if configured)
571                if let Some(ref injector) = ctx.latency_injector {
572                    if let Err(e) = injector.inject_latency(&operation_tags).await {
573                        tracing::warn!("Failed to inject latency: {}", e);
574                    }
575                }
576
577                // (d) Scenario/status override from headers
578                let scenario = headers
579                    .get("X-Mockforge-Scenario")
580                    .and_then(|v| v.to_str().ok())
581                    .map(|s| s.to_string())
582                    .or_else(|| std::env::var("MOCKFORGE_HTTP_SCENARIO").ok());
583
584                let status_override = headers
585                    .get("X-Mockforge-Response-Status")
586                    .and_then(|v| v.to_str().ok())
587                    .and_then(|s| s.parse::<u16>().ok());
588
589                // (e) Generate mock response for this request with scenario support
590                let (selected_status, mock_response) = route_clone
591                    .mock_response_with_status_and_scenario_and_override(
592                        scenario.as_deref(),
593                        status_override,
594                    );
595
596                // (f) Validation (if full validation is enabled)
597                if ctx.enable_full_validation {
598                    // Build params maps
599                    let mut path_map = Map::new();
600                    for (k, v) in &path_params {
601                        path_map.insert(k.clone(), Value::String(v.clone()));
602                    }
603
604                    // Query
605                    let mut query_map = Map::new();
606                    if let Some(ref q) = raw_query {
607                        for (k, v) in url::form_urlencoded::parse(q.as_bytes()) {
608                            query_map.insert(k.to_string(), Value::String(v.to_string()));
609                        }
610                    }
611
612                    // Headers: only capture those declared on this operation
613                    let mut header_map = Map::new();
614                    for p_ref in &operation.parameters {
615                        if let Some(openapiv3::Parameter::Header { parameter_data, .. }) =
616                            p_ref.as_item()
617                        {
618                            let name_lc = parameter_data.name.to_ascii_lowercase();
619                            if let Ok(hn) = axum::http::HeaderName::from_bytes(name_lc.as_bytes()) {
620                                if let Some(val) = headers.get(hn) {
621                                    if let Ok(s) = val.to_str() {
622                                        header_map.insert(
623                                            parameter_data.name.clone(),
624                                            Value::String(s.to_string()),
625                                        );
626                                    }
627                                }
628                            }
629                        }
630                    }
631
632                    // Cookies: parse Cookie header
633                    let mut cookie_map = Map::new();
634                    if let Some(val) = headers.get(axum::http::header::COOKIE) {
635                        if let Ok(s) = val.to_str() {
636                            for part in s.split(';') {
637                                let part = part.trim();
638                                if let Some((k, v)) = part.split_once('=') {
639                                    cookie_map.insert(k.to_string(), Value::String(v.to_string()));
640                                }
641                            }
642                        }
643                    }
644
645                    // Check if this is a multipart request
646                    let is_multipart = headers
647                        .get(axum::http::header::CONTENT_TYPE)
648                        .and_then(|v| v.to_str().ok())
649                        .map(|ct| ct.starts_with("multipart/form-data"))
650                        .unwrap_or(false);
651
652                    // Extract multipart data if applicable
653                    #[allow(unused_assignments)]
654                    let mut multipart_fields = HashMap::new();
655                    let mut _multipart_files = HashMap::new();
656                    let mut body_json: Option<Value> = None;
657
658                    if is_multipart {
659                        // For multipart requests, extract fields and files
660                        match extract_multipart_from_bytes(&body, &headers).await {
661                            Ok((fields, files)) => {
662                                multipart_fields = fields;
663                                _multipart_files = files;
664                                // Also create a JSON representation for validation
665                                let mut body_obj = Map::new();
666                                for (k, v) in &multipart_fields {
667                                    body_obj.insert(k.clone(), v.clone());
668                                }
669                                if !body_obj.is_empty() {
670                                    body_json = Some(Value::Object(body_obj));
671                                }
672                            }
673                            Err(e) => {
674                                tracing::warn!("Failed to parse multipart data: {}", e);
675                            }
676                        }
677                    } else {
678                        // Body: try JSON when present
679                        body_json = if !body.is_empty() {
680                            serde_json::from_slice(&body).ok()
681                        } else {
682                            None
683                        };
684                    }
685
686                    if let Err(e) = validator.validate_request_with_all(
687                        &path_template,
688                        &method,
689                        &path_map,
690                        &query_map,
691                        &header_map,
692                        &cookie_map,
693                        body_json.as_ref(),
694                    ) {
695                        // Choose status: prefer options.validation_status, fallback to env, else 400
696                        let status_code =
697                            validator.options.validation_status.unwrap_or_else(|| {
698                                std::env::var("MOCKFORGE_VALIDATION_STATUS")
699                                    .ok()
700                                    .and_then(|s| s.parse::<u16>().ok())
701                                    .unwrap_or(400)
702                            });
703
704                        let payload = if status_code == 422 {
705                            // For 422 responses, use enhanced schema validation with detailed errors
706                            generate_enhanced_422_response(
707                                &validator,
708                                &path_template,
709                                &method,
710                                body_json.as_ref(),
711                                &path_map,
712                                &query_map,
713                                &header_map,
714                                &cookie_map,
715                            )
716                        } else {
717                            // For other status codes, use generic error format
718                            let msg = format!("{}", e);
719                            let detail_val = serde_json::from_str::<Value>(&msg)
720                                .unwrap_or(serde_json::json!(msg));
721                            json!({
722                                "error": "request validation failed",
723                                "detail": detail_val,
724                                "method": method,
725                                "path": path_template,
726                                "timestamp": Utc::now().to_rfc3339(),
727                            })
728                        };
729
730                        record_validation_error(&payload);
731                        let status = axum::http::StatusCode::from_u16(status_code)
732                            .unwrap_or(axum::http::StatusCode::BAD_REQUEST);
733
734                        // Serialize payload with fallback for serialization errors
735                        let body_bytes = serde_json::to_vec(&payload)
736                            .unwrap_or_else(|_| br#"{"error":"Serialization failed"}"#.to_vec());
737
738                        return axum::http::Response::builder()
739                            .status(status)
740                            .header(axum::http::header::CONTENT_TYPE, "application/json")
741                            .body(axum::body::Body::from(body_bytes))
742                            .expect("Response builder should create valid response with valid headers and body");
743                    }
744                }
745
746                // (g) Template expansion (if enabled via context or env var).
747                //
748                // The `MOCKFORGE_RESPONSE_TEMPLATE_EXPAND` env var is tri-state:
749                //   * unset  -> fall back to context/options flags
750                //   * "true" / "1" -> force expansion on
751                //   * "false" / anything else -> force expansion OFF
752                // Treating it as a forcing override (not just OR) lets tests and
753                // ad-hoc operator overrides disable token expansion explicitly.
754                let mut final_response = mock_response.clone();
755                let env_expand: Option<bool> = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
756                    .ok()
757                    .map(|v| v == "1" || v.eq_ignore_ascii_case("true"));
758                let expand = match env_expand {
759                    Some(v) => v,
760                    None => {
761                        ctx.enable_template_expand || validator.options.response_template_expand
762                    }
763                };
764                if expand {
765                    if let Some(ref rewriter) = ctx.response_rewriter {
766                        rewriter.expand_tokens(&mut final_response);
767                    }
768                }
769
770                // (h) Apply overrides if a rewriter is wired up and overrides are enabled.
771                if ctx.overrides_enabled {
772                    if let Some(ref rewriter) = ctx.response_rewriter {
773                        let op_tags =
774                            operation.operation_id.clone().map(|id| vec![id]).unwrap_or_default();
775                        rewriter.apply_overrides(
776                            &operation.operation_id.clone().unwrap_or_default(),
777                            &op_tags,
778                            &path_template,
779                            &mut final_response,
780                        );
781                    }
782                }
783
784                // (i) Response validation and trace (if full validation is enabled)
785                if ctx.enable_full_validation {
786                    // Optional response validation
787                    if validator.options.validate_responses {
788                        // Find the first 2xx response in the operation
789                        if let Some((status_code, _response)) = operation
790                            .responses
791                            .responses
792                            .iter()
793                            .filter_map(|(status, resp)| match status {
794                                openapiv3::StatusCode::Code(code)
795                                    if *code >= 200 && *code < 300 =>
796                                {
797                                    resp.as_item().map(|r| ((*code), r))
798                                }
799                                openapiv3::StatusCode::Range(range)
800                                    if *range >= 200 && *range < 300 =>
801                                {
802                                    resp.as_item().map(|r| (200, r))
803                                }
804                                _ => None,
805                            })
806                            .next()
807                        {
808                            // Basic response validation - check if response is valid JSON
809                            if serde_json::from_value::<Value>(final_response.clone()).is_err() {
810                                tracing::warn!(
811                                    "Response validation failed: invalid JSON for status {}",
812                                    status_code
813                                );
814                            }
815                        }
816                    }
817
818                    // Capture final payload and run schema validation for trace
819                    let mut trace = ResponseGenerationTrace::new();
820                    trace.set_final_payload(final_response.clone());
821
822                    // Extract response schema and run validation diff
823                    if let Some((_status_code, response_ref)) = operation
824                        .responses
825                        .responses
826                        .iter()
827                        .filter_map(|(status, resp)| match status {
828                            openapiv3::StatusCode::Code(code) if *code == selected_status => {
829                                resp.as_item().map(|r| ((*code), r))
830                            }
831                            openapiv3::StatusCode::Range(range)
832                                if *range >= 200 && *range < 300 =>
833                            {
834                                resp.as_item().map(|r| (200, r))
835                            }
836                            _ => None,
837                        })
838                        .next()
839                        .or_else(|| {
840                            // Fallback to first 2xx response
841                            operation
842                                .responses
843                                .responses
844                                .iter()
845                                .filter_map(|(status, resp)| match status {
846                                    openapiv3::StatusCode::Code(code)
847                                        if *code >= 200 && *code < 300 =>
848                                    {
849                                        resp.as_item().map(|r| ((*code), r))
850                                    }
851                                    _ => None,
852                                })
853                                .next()
854                        })
855                    {
856                        // response_ref is already a Response, not a ReferenceOr
857                        let response_item = response_ref;
858                        // Extract schema from application/json content
859                        if let Some(content) = response_item.content.get("application/json") {
860                            if let Some(schema_ref) = &content.schema {
861                                // Convert OpenAPI schema to JSON Schema Value
862                                if let Some(schema) = schema_ref.as_item() {
863                                    if let Ok(schema_json) = serde_json::to_value(schema) {
864                                        // Run validation diff
865                                        let validation_errors =
866                                            validation_diff(&schema_json, &final_response);
867                                        trace.set_schema_validation_diff(validation_errors);
868                                    }
869                                }
870                            }
871                        }
872                    }
873
874                    // Store trace in response extensions for later retrieval by logging middleware
875                    let mut response = Json(final_response).into_response();
876                    response.extensions_mut().insert(trace);
877                    *response.status_mut() = axum::http::StatusCode::from_u16(selected_status)
878                        .unwrap_or(axum::http::StatusCode::OK);
879                    return response;
880                }
881
882                // (j) Return response (non-full-validation path)
883                let mut response = Json(final_response).into_response();
884                *response.status_mut() = axum::http::StatusCode::from_u16(selected_status)
885                    .unwrap_or(axum::http::StatusCode::OK);
886                response
887            };
888
889            router = Self::route_for_method(router, axum_path, &route.method, handler);
890        }
891
892        // Add OpenAPI documentation endpoint if configured
893        if ctx.add_spec_endpoint {
894            let spec_json = serde_json::to_value(&self.spec.spec).unwrap_or(Value::Null);
895            router = router.route("/openapi.json", get(move || async move { Json(spec_json) }));
896        }
897
898        // Issue #79 — raise the body-size cap above axum's 2 MiB default so
899        // large chunked uploads don't get truncated mid-extraction. See
900        // `openapi_body_limit_bytes` for the rationale.
901        router.layer(DefaultBodyLimit::max(openapi_body_limit_bytes()))
902    }
903
904    /// Build an Axum router from the OpenAPI spec with latency injection support
905    pub fn build_router_with_latency(self, latency_injector: LatencyInjector) -> Router {
906        self.build_router_with_injectors(latency_injector, None)
907    }
908
909    /// Build an Axum router from the OpenAPI spec with both latency and failure injection support
910    pub fn build_router_with_injectors(
911        self,
912        latency_injector: LatencyInjector,
913        failure_injector: Option<mockforge_foundation::failure_injection::FailureInjector>,
914    ) -> Router {
915        self.build_router_with_injectors_and_overrides(
916            latency_injector,
917            failure_injector,
918            None,
919            false,
920        )
921    }
922
923    /// Build an Axum router from the OpenAPI spec with latency, failure
924    /// injection, and a response-rewriter hook (typically wrapping
925    /// core's `Overrides` + `templating::expand_tokens`).
926    pub fn build_router_with_injectors_and_overrides(
927        self,
928        latency_injector: LatencyInjector,
929        failure_injector: Option<mockforge_foundation::failure_injection::FailureInjector>,
930        response_rewriter: Option<Arc<dyn ResponseRewriter>>,
931        overrides_enabled: bool,
932    ) -> Router {
933        let ctx = RouterContext {
934            custom_fixture_loader: self.custom_fixture_loader.clone(),
935            latency_injector: Some(latency_injector),
936            failure_injector,
937            response_rewriter,
938            overrides_enabled,
939            enable_full_validation: true,
940            enable_template_expand: true,
941            add_spec_endpoint: true,
942            ..Default::default()
943        };
944        self.build_router_with_context(ctx)
945    }
946
947    /// Get route by path and method
948    pub fn get_route(&self, path: &str, method: &str) -> Option<&OpenApiRoute> {
949        self.routes.iter().find(|route| route.path == path && route.method == method)
950    }
951
952    /// Get all routes for a specific path
953    pub fn get_routes_for_path(&self, path: &str) -> Vec<&OpenApiRoute> {
954        self.routes.iter().filter(|route| route.path == path).collect()
955    }
956
957    /// Validate request against OpenAPI spec (legacy body-only)
958    pub fn validate_request(&self, path: &str, method: &str, body: Option<&Value>) -> Result<()> {
959        self.validate_request_with(path, method, &Map::new(), &Map::new(), body)
960    }
961
962    /// Validate request against OpenAPI spec with path/query params
963    pub fn validate_request_with(
964        &self,
965        path: &str,
966        method: &str,
967        path_params: &Map<String, Value>,
968        query_params: &Map<String, Value>,
969        body: Option<&Value>,
970    ) -> Result<()> {
971        self.validate_request_with_all(
972            path,
973            method,
974            path_params,
975            query_params,
976            &Map::new(),
977            &Map::new(),
978            body,
979        )
980    }
981
982    /// Validate request against OpenAPI spec with path/query/header/cookie params
983    #[allow(clippy::too_many_arguments)]
984    pub fn validate_request_with_all(
985        &self,
986        path: &str,
987        method: &str,
988        path_params: &Map<String, Value>,
989        query_params: &Map<String, Value>,
990        header_params: &Map<String, Value>,
991        cookie_params: &Map<String, Value>,
992        body: Option<&Value>,
993    ) -> Result<()> {
994        // Skip validation for any configured admin prefixes
995        for pref in &self.options.admin_skip_prefixes {
996            if !pref.is_empty() && path.starts_with(pref) {
997                return Ok(());
998            }
999        }
1000        // Runtime env overrides
1001        let env_mode = std::env::var("MOCKFORGE_REQUEST_VALIDATION").ok().map(|v| {
1002            match v.to_ascii_lowercase().as_str() {
1003                "off" | "disable" | "disabled" => ValidationMode::Disabled,
1004                "warn" | "warning" => ValidationMode::Warn,
1005                _ => ValidationMode::Enforce,
1006            }
1007        });
1008        let aggregate = std::env::var("MOCKFORGE_AGGREGATE_ERRORS")
1009            .ok()
1010            .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1011            .unwrap_or(self.options.aggregate_errors);
1012        // Per-route runtime overrides via JSON env var
1013        let env_overrides: Option<Map<String, Value>> =
1014            std::env::var("MOCKFORGE_VALIDATION_OVERRIDES_JSON")
1015                .ok()
1016                .and_then(|s| serde_json::from_str::<Value>(&s).ok())
1017                .and_then(|v| v.as_object().cloned());
1018        // Response validation is handled in HTTP layer now
1019        let mut effective_mode = env_mode.unwrap_or(self.options.request_mode.clone());
1020        // Apply runtime overrides first if present
1021        if let Some(map) = &env_overrides {
1022            if let Some(v) = map.get(&format!("{} {}", method, path)) {
1023                if let Some(m) = v.as_str() {
1024                    effective_mode = match m {
1025                        "off" => ValidationMode::Disabled,
1026                        "warn" => ValidationMode::Warn,
1027                        _ => ValidationMode::Enforce,
1028                    };
1029                }
1030            }
1031        }
1032        // Then static options overrides
1033        if let Some(override_mode) = self.options.overrides.get(&format!("{} {}", method, path)) {
1034            effective_mode = override_mode.clone();
1035        }
1036        if matches!(effective_mode, ValidationMode::Disabled) {
1037            return Ok(());
1038        }
1039        if let Some(route) = self.get_route(path, method) {
1040            if matches!(effective_mode, ValidationMode::Disabled) {
1041                return Ok(());
1042            }
1043            let mut errors: Vec<String> = Vec::new();
1044            let mut details: Vec<Value> = Vec::new();
1045            // Validate request body if required
1046            if let Some(schema) = &route.operation.request_body {
1047                if let Some(value) = body {
1048                    // First resolve the request body reference if it's a reference
1049                    let request_body = match schema {
1050                        openapiv3::ReferenceOr::Item(rb) => Some(rb),
1051                        openapiv3::ReferenceOr::Reference { reference } => {
1052                            // Try to resolve request body reference through spec
1053                            self.spec
1054                                .spec
1055                                .components
1056                                .as_ref()
1057                                .and_then(|components| {
1058                                    components.request_bodies.get(
1059                                        reference.trim_start_matches("#/components/requestBodies/"),
1060                                    )
1061                                })
1062                                .and_then(|rb_ref| rb_ref.as_item())
1063                        }
1064                    };
1065
1066                    if let Some(rb) = request_body {
1067                        if let Some(content) = rb.content.get("application/json") {
1068                            if let Some(schema_ref) = &content.schema {
1069                                // Resolve schema reference and validate
1070                                match schema_ref {
1071                                    openapiv3::ReferenceOr::Item(schema) => {
1072                                        // Direct schema - validate immediately
1073                                        if let Err(validation_error) =
1074                                            OpenApiSchema::new(schema.clone()).validate(value)
1075                                        {
1076                                            let error_msg = validation_error.to_string();
1077                                            errors.push(format!(
1078                                                "body validation failed: {}",
1079                                                error_msg
1080                                            ));
1081                                            if aggregate {
1082                                                details.push(serde_json::json!({"path":"body","code":"schema_validation","message":error_msg}));
1083                                            }
1084                                        }
1085                                    }
1086                                    openapiv3::ReferenceOr::Reference { reference } => {
1087                                        // Referenced schema - resolve and validate
1088                                        if let Some(resolved_schema_ref) =
1089                                            self.spec.get_schema(reference)
1090                                        {
1091                                            if let Err(validation_error) = OpenApiSchema::new(
1092                                                resolved_schema_ref.schema.clone(),
1093                                            )
1094                                            .validate(value)
1095                                            {
1096                                                let error_msg = validation_error.to_string();
1097                                                errors.push(format!(
1098                                                    "body validation failed: {}",
1099                                                    error_msg
1100                                                ));
1101                                                if aggregate {
1102                                                    details.push(serde_json::json!({"path":"body","code":"schema_validation","message":error_msg}));
1103                                                }
1104                                            }
1105                                        } else {
1106                                            // Schema reference couldn't be resolved
1107                                            errors.push(format!("body validation failed: could not resolve schema reference {}", reference));
1108                                            if aggregate {
1109                                                details.push(serde_json::json!({"path":"body","code":"reference_error","message":"Could not resolve schema reference"}));
1110                                            }
1111                                        }
1112                                    }
1113                                }
1114                            }
1115                        }
1116                    } else {
1117                        // Request body reference couldn't be resolved or no application/json content
1118                        errors.push("body validation failed: could not resolve request body or no application/json content".to_string());
1119                        if aggregate {
1120                            details.push(serde_json::json!({"path":"body","code":"reference_error","message":"Could not resolve request body reference"}));
1121                        }
1122                    }
1123                } else {
1124                    errors.push("body: Request body is required but not provided".to_string());
1125                    details.push(serde_json::json!({"path":"body","code":"required","message":"Request body is required"}));
1126                }
1127            } else if body.is_some() {
1128                // No body expected but provided — not an error by default, but log it
1129                tracing::debug!("Body provided for operation without requestBody; accepting");
1130            }
1131
1132            // Validate path/query parameters
1133            for p_ref in &route.operation.parameters {
1134                if let Some(p) = p_ref.as_item() {
1135                    match p {
1136                        openapiv3::Parameter::Path { parameter_data, .. } => {
1137                            validate_parameter(
1138                                parameter_data,
1139                                path_params,
1140                                "path",
1141                                aggregate,
1142                                &mut errors,
1143                                &mut details,
1144                            );
1145                        }
1146                        openapiv3::Parameter::Query {
1147                            parameter_data,
1148                            style,
1149                            ..
1150                        } => {
1151                            // For deepObject style, reconstruct nested value from keys like name[prop]
1152                            // e.g., filter[name]=John&filter[age]=30 -> {"name":"John","age":"30"}
1153                            let deep_value = if matches!(style, openapiv3::QueryStyle::DeepObject) {
1154                                let prefix_bracket = format!("{}[", parameter_data.name);
1155                                let mut obj = Map::new();
1156                                for (key, val) in query_params.iter() {
1157                                    if let Some(rest) = key.strip_prefix(&prefix_bracket) {
1158                                        if let Some(prop) = rest.strip_suffix(']') {
1159                                            obj.insert(prop.to_string(), val.clone());
1160                                        }
1161                                    }
1162                                }
1163                                if obj.is_empty() {
1164                                    None
1165                                } else {
1166                                    Some(Value::Object(obj))
1167                                }
1168                            } else {
1169                                None
1170                            };
1171                            let style_str = match style {
1172                                openapiv3::QueryStyle::Form => Some("form"),
1173                                openapiv3::QueryStyle::SpaceDelimited => Some("spaceDelimited"),
1174                                openapiv3::QueryStyle::PipeDelimited => Some("pipeDelimited"),
1175                                openapiv3::QueryStyle::DeepObject => Some("deepObject"),
1176                            };
1177                            validate_parameter_with_deep_object(
1178                                parameter_data,
1179                                query_params,
1180                                "query",
1181                                deep_value,
1182                                style_str,
1183                                aggregate,
1184                                &mut errors,
1185                                &mut details,
1186                            );
1187                        }
1188                        openapiv3::Parameter::Header { parameter_data, .. } => {
1189                            validate_parameter(
1190                                parameter_data,
1191                                header_params,
1192                                "header",
1193                                aggregate,
1194                                &mut errors,
1195                                &mut details,
1196                            );
1197                        }
1198                        openapiv3::Parameter::Cookie { parameter_data, .. } => {
1199                            validate_parameter(
1200                                parameter_data,
1201                                cookie_params,
1202                                "cookie",
1203                                aggregate,
1204                                &mut errors,
1205                                &mut details,
1206                            );
1207                        }
1208                    }
1209                }
1210            }
1211            if errors.is_empty() {
1212                return Ok(());
1213            }
1214            match effective_mode {
1215                ValidationMode::Disabled => Ok(()),
1216                ValidationMode::Warn => {
1217                    tracing::warn!("Request validation warnings: {:?}", errors);
1218                    Ok(())
1219                }
1220                ValidationMode::Enforce => Err(Error::validation(
1221                    serde_json::json!({"errors": errors, "details": details}).to_string(),
1222                )),
1223            }
1224        } else {
1225            Err(Error::internal(format!("Route {} {} not found in OpenAPI spec", method, path)))
1226        }
1227    }
1228
1229    // Legacy helper removed (mock + status selection happens in handler via route.mock_response_with_status)
1230
1231    /// Get all paths defined in the spec
1232    pub fn paths(&self) -> Vec<String> {
1233        let mut paths: Vec<String> = self.routes.iter().map(|route| route.path.clone()).collect();
1234        paths.sort();
1235        paths.dedup();
1236        paths
1237    }
1238
1239    /// Get all HTTP methods supported
1240    pub fn methods(&self) -> Vec<String> {
1241        let mut methods: Vec<String> =
1242            self.routes.iter().map(|route| route.method.clone()).collect();
1243        methods.sort();
1244        methods.dedup();
1245        methods
1246    }
1247
1248    /// Get operation details for a route
1249    pub fn get_operation(&self, path: &str, method: &str) -> Option<OpenApiOperation> {
1250        self.get_route(path, method).map(|route| {
1251            OpenApiOperation::from_operation(
1252                &route.method,
1253                route.path.clone(),
1254                &route.operation,
1255                &self.spec,
1256            )
1257        })
1258    }
1259
1260    /// Extract path parameters from a request path by matching against known routes
1261    pub fn extract_path_parameters(&self, path: &str, method: &str) -> HashMap<String, String> {
1262        for route in &self.routes {
1263            if route.method != method {
1264                continue;
1265            }
1266
1267            if let Some(params) = self.match_path_to_route(path, &route.path) {
1268                return params;
1269            }
1270        }
1271        HashMap::new()
1272    }
1273
1274    /// Match a request path against a route pattern and extract parameters
1275    fn match_path_to_route(
1276        &self,
1277        request_path: &str,
1278        route_pattern: &str,
1279    ) -> Option<HashMap<String, String>> {
1280        let mut params = HashMap::new();
1281
1282        // Split both paths into segments
1283        let request_segments: Vec<&str> = request_path.trim_start_matches('/').split('/').collect();
1284        let pattern_segments: Vec<&str> =
1285            route_pattern.trim_start_matches('/').split('/').collect();
1286
1287        if request_segments.len() != pattern_segments.len() {
1288            return None;
1289        }
1290
1291        for (req_seg, pat_seg) in request_segments.iter().zip(pattern_segments.iter()) {
1292            if pat_seg.starts_with('{') && pat_seg.ends_with('}') {
1293                // This is a parameter
1294                let param_name = &pat_seg[1..pat_seg.len() - 1];
1295                params.insert(param_name.to_string(), req_seg.to_string());
1296            } else if req_seg != pat_seg {
1297                // Static segment doesn't match
1298                return None;
1299            }
1300        }
1301
1302        Some(params)
1303    }
1304
1305    /// Convert OpenAPI path to Axum-compatible path
1306    /// This is a utility function for converting path parameters from {param} to :param format
1307    pub fn convert_path_to_axum(openapi_path: &str) -> String {
1308        // Axum v0.7+ uses {param} format, same as OpenAPI
1309        openapi_path.to_string()
1310    }
1311
1312    /// Build router with AI generator support
1313    pub fn build_router_with_ai(
1314        &self,
1315        ai_generator: Option<Arc<dyn AiGenerator + Send + Sync>>,
1316    ) -> Router {
1317        let mut router = Router::new();
1318        let deduped = self.deduplicated_routes();
1319        tracing::debug!("Building router with AI support from {} routes", self.routes.len());
1320
1321        for (axum_path, route) in &deduped {
1322            tracing::debug!("Adding AI-enabled route: {} {}", route.method, route.path);
1323
1324            let route_clone = (*route).clone();
1325            let ai_generator_clone = ai_generator.clone();
1326
1327            // Create async handler that extracts request data and builds context
1328            let handler = move |headers: HeaderMap, body: Option<Json<Value>>| {
1329                let route = route_clone.clone();
1330                let ai_generator = ai_generator_clone.clone();
1331
1332                async move {
1333                    tracing::debug!(
1334                        "Handling AI request for route: {} {}",
1335                        route.method,
1336                        route.path
1337                    );
1338
1339                    // Build request context
1340                    let mut context = RequestContext::new(route.method.clone(), route.path.clone());
1341
1342                    // Extract headers
1343                    context.headers = headers
1344                        .iter()
1345                        .map(|(k, v)| {
1346                            (k.to_string(), Value::String(v.to_str().unwrap_or("").to_string()))
1347                        })
1348                        .collect();
1349
1350                    // Extract body if present
1351                    context.body = body.map(|Json(b)| b);
1352
1353                    // Generate AI response if AI generator is available and route has AI config
1354                    let (status, response) = if let (Some(generator), Some(_ai_config)) =
1355                        (ai_generator, &route.ai_config)
1356                    {
1357                        route
1358                            .mock_response_with_status_async(&context, Some(generator.as_ref()))
1359                            .await
1360                    } else {
1361                        // No AI support, use static response
1362                        route.mock_response_with_status()
1363                    };
1364
1365                    (
1366                        axum::http::StatusCode::from_u16(status)
1367                            .unwrap_or(axum::http::StatusCode::OK),
1368                        Json(response),
1369                    )
1370                }
1371            };
1372
1373            router = Self::route_for_method(router, axum_path, &route.method, handler);
1374        }
1375
1376        // Issue #79 — same body-limit raise as `build_router_with_context`;
1377        // the AI handler also uses `Option<Json<Value>>` so axum's 2 MiB
1378        // default truncates large bodies and the handler responds early.
1379        router.layer(DefaultBodyLimit::max(openapi_body_limit_bytes()))
1380    }
1381
1382    /// Build router with MockAI (Behavioral Mock Intelligence) support
1383    ///
1384    /// This method integrates MockAI for intelligent, context-aware response generation,
1385    /// mutation detection, validation error generation, and pagination intelligence.
1386    ///
1387    /// # Arguments
1388    /// * `mockai` - Optional MockAI instance for intelligent behavior
1389    ///
1390    /// # Returns
1391    /// Axum router with MockAI-powered response generation
1392    pub fn build_router_with_mockai(
1393        &self,
1394        mockai: Option<
1395            Arc<
1396                tokio::sync::RwLock<
1397                    dyn mockforge_foundation::intelligent_behavior::MockAiBehavior + Send + Sync,
1398                >,
1399            >,
1400        >,
1401    ) -> Router {
1402        use mockforge_foundation::intelligent_behavior::Request as MockAIRequest;
1403
1404        let mut router = Router::new();
1405        let deduped = self.deduplicated_routes();
1406        tracing::debug!("Building router with MockAI support from {} routes", self.routes.len());
1407
1408        let custom_loader = self.custom_fixture_loader.clone();
1409        for (axum_path, route) in &deduped {
1410            tracing::debug!("Adding MockAI-enabled route: {} {}", route.method, route.path);
1411
1412            let route_clone = (*route).clone();
1413            let mockai_clone = mockai.clone();
1414            let custom_loader_clone = custom_loader.clone();
1415
1416            // Create async handler that processes requests through MockAI
1417            // Query params are extracted via Query extractor with HashMap
1418            // Note: Using Query<HashMap<String, String>> wrapped in Option to handle missing query params
1419            let handler = move |query: axum::extract::Query<HashMap<String, String>>,
1420                                headers: HeaderMap,
1421                                body: Option<Json<Value>>| {
1422                let route = route_clone.clone();
1423                let mockai = mockai_clone.clone();
1424
1425                async move {
1426                    tracing::info!(
1427                        "[FIXTURE DEBUG] Starting fixture check for {} {} (custom_loader available: {})",
1428                        route.method,
1429                        route.path,
1430                        custom_loader_clone.is_some()
1431                    );
1432
1433                    // Check for custom fixture first (highest priority, before MockAI)
1434                    if let Some(ref loader) = custom_loader_clone {
1435                        use crate::request_fingerprint::RequestFingerprint;
1436                        use axum::http::{Method, Uri};
1437
1438                        // Build query string from parsed query params
1439                        let query_string = if query.0.is_empty() {
1440                            String::new()
1441                        } else {
1442                            query
1443                                .0
1444                                .iter()
1445                                .map(|(k, v)| format!("{}={}", k, v))
1446                                .collect::<Vec<_>>()
1447                                .join("&")
1448                        };
1449
1450                        // Normalize the path to match fixture normalization
1451                        let normalized_request_path =
1452                            crate::custom_fixture::CustomFixtureLoader::normalize_path(&route.path);
1453
1454                        tracing::info!(
1455                            "[FIXTURE DEBUG] Path normalization: original='{}', normalized='{}'",
1456                            route.path,
1457                            normalized_request_path
1458                        );
1459
1460                        // Create URI for fingerprint
1461                        let uri_str = if query_string.is_empty() {
1462                            normalized_request_path.clone()
1463                        } else {
1464                            format!("{}?{}", normalized_request_path, query_string)
1465                        };
1466
1467                        tracing::info!(
1468                            "[FIXTURE DEBUG] URI construction: uri_str='{}', query_string='{}'",
1469                            uri_str,
1470                            query_string
1471                        );
1472
1473                        if let Ok(uri) = uri_str.parse::<Uri>() {
1474                            let http_method =
1475                                Method::from_bytes(route.method.as_bytes()).unwrap_or(Method::GET);
1476
1477                            // Convert body to bytes for fingerprint
1478                            let body_bytes =
1479                                body.as_ref().and_then(|Json(b)| serde_json::to_vec(b).ok());
1480                            let body_slice = body_bytes.as_deref();
1481
1482                            let fingerprint =
1483                                RequestFingerprint::new(http_method, &uri, &headers, body_slice);
1484
1485                            tracing::info!(
1486                                "[FIXTURE DEBUG] RequestFingerprint created: method='{}', path='{}', query='{}', body_hash={:?}",
1487                                fingerprint.method,
1488                                fingerprint.path,
1489                                fingerprint.query,
1490                                fingerprint.body_hash
1491                            );
1492
1493                            // Check what fixtures are available for this method
1494                            let available_fixtures = loader.has_fixture(&fingerprint);
1495                            tracing::info!(
1496                                "[FIXTURE DEBUG] Fixture check result: has_fixture={}",
1497                                available_fixtures
1498                            );
1499
1500                            if let Some(custom_fixture) = loader.load_fixture(&fingerprint) {
1501                                tracing::info!(
1502                                    "[FIXTURE DEBUG] ✅ FIXTURE MATCHED! Using custom fixture for {} {} (status: {}, path: '{}')",
1503                                    route.method,
1504                                    route.path,
1505                                    custom_fixture.status,
1506                                    custom_fixture.path
1507                                );
1508
1509                                // Apply delay if specified
1510                                if custom_fixture.delay_ms > 0 {
1511                                    tokio::time::sleep(tokio::time::Duration::from_millis(
1512                                        custom_fixture.delay_ms,
1513                                    ))
1514                                    .await;
1515                                }
1516
1517                                // Convert response to JSON string if needed
1518                                let response_body = if custom_fixture.response.is_string() {
1519                                    custom_fixture.response.as_str().unwrap().to_string()
1520                                } else {
1521                                    serde_json::to_string(&custom_fixture.response)
1522                                        .unwrap_or_else(|_| "{}".to_string())
1523                                };
1524
1525                                // Parse response body as JSON
1526                                let json_value: Value = serde_json::from_str(&response_body)
1527                                    .unwrap_or_else(|_| serde_json::json!({}));
1528
1529                                // Build response with status and JSON body
1530                                let status =
1531                                    axum::http::StatusCode::from_u16(custom_fixture.status)
1532                                        .unwrap_or(axum::http::StatusCode::OK);
1533
1534                                // Return as tuple (StatusCode, Json) to match handler signature
1535                                return (status, Json(json_value));
1536                            } else {
1537                                tracing::warn!(
1538                                    "[FIXTURE DEBUG] ❌ No fixture match found for {} {} (fingerprint.path='{}', normalized='{}')",
1539                                    route.method,
1540                                    route.path,
1541                                    fingerprint.path,
1542                                    normalized_request_path
1543                                );
1544                            }
1545                        } else {
1546                            tracing::warn!("[FIXTURE DEBUG] Failed to parse URI: '{}'", uri_str);
1547                        }
1548                    } else {
1549                        tracing::warn!(
1550                            "[FIXTURE DEBUG] Custom fixture loader not available for {} {}",
1551                            route.method,
1552                            route.path
1553                        );
1554                    }
1555
1556                    tracing::debug!(
1557                        "Handling MockAI request for route: {} {}",
1558                        route.method,
1559                        route.path
1560                    );
1561
1562                    // Query parameters are already parsed by Query extractor
1563                    let mockai_query = query.0;
1564
1565                    // If MockAI is enabled, use it to process the request
1566                    // CRITICAL FIX: Skip MockAI for GET, HEAD, and OPTIONS requests
1567                    // These are read-only operations and should use OpenAPI response generation
1568                    // MockAI's mutation analysis incorrectly treats GET requests as "Create" mutations
1569                    let method_upper = route.method.to_uppercase();
1570                    let should_use_mockai =
1571                        matches!(method_upper.as_str(), "POST" | "PUT" | "PATCH" | "DELETE");
1572
1573                    if should_use_mockai {
1574                        if let Some(mockai_arc) = mockai {
1575                            let mockai_guard = mockai_arc.read().await;
1576
1577                            // Build MockAI request
1578                            let mut mockai_headers = HashMap::new();
1579                            for (k, v) in headers.iter() {
1580                                mockai_headers
1581                                    .insert(k.to_string(), v.to_str().unwrap_or("").to_string());
1582                            }
1583
1584                            let mockai_request = MockAIRequest {
1585                                method: route.method.clone(),
1586                                path: route.path.clone(),
1587                                body: body.as_ref().map(|Json(b)| b.clone()),
1588                                query_params: mockai_query,
1589                                headers: mockai_headers,
1590                            };
1591
1592                            // Process request through MockAI
1593                            match mockai_guard.process_request(&mockai_request).await {
1594                                Ok(mockai_response) => {
1595                                    // Check if MockAI returned an empty object (signals to use OpenAPI generation)
1596                                    let is_empty = mockai_response.body.is_object()
1597                                        && mockai_response
1598                                            .body
1599                                            .as_object()
1600                                            .map(|obj| obj.is_empty())
1601                                            .unwrap_or(false);
1602
1603                                    if is_empty {
1604                                        tracing::debug!(
1605                                            "MockAI returned empty object for {} {}, falling back to OpenAPI response generation",
1606                                            route.method,
1607                                            route.path
1608                                        );
1609                                        // Fall through to standard OpenAPI response generation
1610                                    } else {
1611                                        // Use the status code from the OpenAPI spec rather than
1612                                        // MockAI's hardcoded 200, so that e.g. POST returning 201
1613                                        // is honored correctly.
1614                                        let spec_status = route.find_first_available_status_code();
1615                                        tracing::debug!(
1616                                            "MockAI generated response for {} {}, using spec status: {} (MockAI suggested: {})",
1617                                            route.method,
1618                                            route.path,
1619                                            spec_status,
1620                                            mockai_response.status_code
1621                                        );
1622                                        return (
1623                                            axum::http::StatusCode::from_u16(spec_status)
1624                                                .unwrap_or(axum::http::StatusCode::OK),
1625                                            Json(mockai_response.body),
1626                                        );
1627                                    }
1628                                }
1629                                Err(e) => {
1630                                    tracing::warn!(
1631                                        "MockAI processing failed for {} {}: {}, falling back to standard response",
1632                                        route.method,
1633                                        route.path,
1634                                        e
1635                                    );
1636                                    // Fall through to standard response generation
1637                                }
1638                            }
1639                        }
1640                    } else {
1641                        tracing::debug!(
1642                            "Skipping MockAI for {} request {} - using OpenAPI response generation",
1643                            method_upper,
1644                            route.path
1645                        );
1646                    }
1647
1648                    // Check for status code override header
1649                    let status_override = headers
1650                        .get("X-Mockforge-Response-Status")
1651                        .and_then(|v| v.to_str().ok())
1652                        .and_then(|s| s.parse::<u16>().ok());
1653
1654                    // Check for scenario header
1655                    let scenario = headers
1656                        .get("X-Mockforge-Scenario")
1657                        .and_then(|v| v.to_str().ok())
1658                        .map(|s| s.to_string())
1659                        .or_else(|| std::env::var("MOCKFORGE_HTTP_SCENARIO").ok());
1660
1661                    // Fallback to standard response generation
1662                    let (status, response) = route
1663                        .mock_response_with_status_and_scenario_and_override(
1664                            scenario.as_deref(),
1665                            status_override,
1666                        );
1667                    (
1668                        axum::http::StatusCode::from_u16(status)
1669                            .unwrap_or(axum::http::StatusCode::OK),
1670                        Json(response),
1671                    )
1672                }
1673            };
1674
1675            router = Self::route_for_method(router, axum_path, &route.method, handler);
1676        }
1677
1678        // Issue #79 — see `build_router_with_context`; same body-limit raise
1679        // for the MockAI router.
1680        router.layer(DefaultBodyLimit::max(openapi_body_limit_bytes()))
1681    }
1682}
1683
1684// Note: templating helpers are now in core::templating (shared across modules)
1685
1686/// Extract multipart form data from request body bytes
1687/// Returns (form_fields, file_paths) where file_paths maps field names to stored file paths
1688async fn extract_multipart_from_bytes(
1689    body: &axum::body::Bytes,
1690    headers: &HeaderMap,
1691) -> Result<(HashMap<String, Value>, HashMap<String, String>)> {
1692    // Get boundary from Content-Type header
1693    let boundary = headers
1694        .get(axum::http::header::CONTENT_TYPE)
1695        .and_then(|v| v.to_str().ok())
1696        .and_then(|ct| {
1697            ct.split(';').find_map(|part| {
1698                let part = part.trim();
1699                if part.starts_with("boundary=") {
1700                    Some(part.strip_prefix("boundary=").unwrap_or("").trim_matches('"'))
1701                } else {
1702                    None
1703                }
1704            })
1705        })
1706        .ok_or_else(|| Error::internal("Missing boundary in Content-Type header"))?;
1707
1708    let mut fields = HashMap::new();
1709    let mut files = HashMap::new();
1710
1711    // Parse multipart data using bytes directly (not string conversion)
1712    // Multipart format: --boundary\r\n...\r\n--boundary\r\n...\r\n--boundary--\r\n
1713    let boundary_prefix = format!("--{}", boundary).into_bytes();
1714    let boundary_line = format!("\r\n--{}\r\n", boundary).into_bytes();
1715    let end_boundary = format!("\r\n--{}--\r\n", boundary).into_bytes();
1716
1717    // Find all boundary positions
1718    let mut pos = 0;
1719    let mut parts = Vec::new();
1720
1721    // Skip initial boundary if present
1722    if body.starts_with(&boundary_prefix) {
1723        if let Some(first_crlf) = body.iter().position(|&b| b == b'\r') {
1724            pos = first_crlf + 2; // Skip --boundary\r\n
1725        }
1726    }
1727
1728    // Find all middle boundaries
1729    while let Some(boundary_pos) = body[pos..]
1730        .windows(boundary_line.len())
1731        .position(|window| window == boundary_line.as_slice())
1732    {
1733        let actual_pos = pos + boundary_pos;
1734        if actual_pos > pos {
1735            parts.push((pos, actual_pos));
1736        }
1737        pos = actual_pos + boundary_line.len();
1738    }
1739
1740    // Find final boundary
1741    if let Some(end_pos) = body[pos..]
1742        .windows(end_boundary.len())
1743        .position(|window| window == end_boundary.as_slice())
1744    {
1745        let actual_end = pos + end_pos;
1746        if actual_end > pos {
1747            parts.push((pos, actual_end));
1748        }
1749    } else if pos < body.len() {
1750        // No final boundary found, treat rest as last part
1751        parts.push((pos, body.len()));
1752    }
1753
1754    // Process each part
1755    for (start, end) in parts {
1756        let part_data = &body[start..end];
1757
1758        // Find header/body separator (CRLF CRLF)
1759        let separator = b"\r\n\r\n";
1760        if let Some(sep_pos) =
1761            part_data.windows(separator.len()).position(|window| window == separator)
1762        {
1763            let header_bytes = &part_data[..sep_pos];
1764            let body_start = sep_pos + separator.len();
1765            let body_data = &part_data[body_start..];
1766
1767            // Parse headers (assuming UTF-8)
1768            let header_str = String::from_utf8_lossy(header_bytes);
1769            let mut field_name = None;
1770            let mut filename = None;
1771
1772            for header_line in header_str.lines() {
1773                if header_line.starts_with("Content-Disposition:") {
1774                    // Extract field name
1775                    if let Some(name_start) = header_line.find("name=\"") {
1776                        let name_start = name_start + 6;
1777                        if let Some(name_end) = header_line[name_start..].find('"') {
1778                            field_name =
1779                                Some(header_line[name_start..name_start + name_end].to_string());
1780                        }
1781                    }
1782
1783                    // Extract filename if present
1784                    if let Some(file_start) = header_line.find("filename=\"") {
1785                        let file_start = file_start + 10;
1786                        if let Some(file_end) = header_line[file_start..].find('"') {
1787                            filename =
1788                                Some(header_line[file_start..file_start + file_end].to_string());
1789                        }
1790                    }
1791                }
1792            }
1793
1794            if let Some(name) = field_name {
1795                if let Some(file) = filename {
1796                    // This is a file upload - store to temp directory
1797                    let temp_dir = std::env::temp_dir().join("mockforge-uploads");
1798                    std::fs::create_dir_all(&temp_dir)
1799                        .map_err(|e| Error::io_with_context("temp directory", e.to_string()))?;
1800
1801                    let file_path = temp_dir.join(format!("{}_{}", uuid::Uuid::new_v4(), file));
1802                    std::fs::write(&file_path, body_data)
1803                        .map_err(|e| Error::io_with_context("file", e.to_string()))?;
1804
1805                    let file_path_str = file_path.to_string_lossy().to_string();
1806                    files.insert(name.clone(), file_path_str.clone());
1807                    fields.insert(name, Value::String(file_path_str));
1808                } else {
1809                    // This is a regular form field - try to parse as UTF-8 string
1810                    // Trim trailing CRLF
1811                    let body_str = body_data
1812                        .strip_suffix(b"\r\n")
1813                        .or_else(|| body_data.strip_suffix(b"\n"))
1814                        .unwrap_or(body_data);
1815
1816                    if let Ok(field_value) = String::from_utf8(body_str.to_vec()) {
1817                        fields.insert(name, Value::String(field_value.trim().to_string()));
1818                    } else {
1819                        // Non-UTF-8 field value - store as base64 encoded string
1820                        use base64::{engine::general_purpose, Engine as _};
1821                        fields.insert(
1822                            name,
1823                            Value::String(general_purpose::STANDARD.encode(body_str)),
1824                        );
1825                    }
1826                }
1827            }
1828        }
1829    }
1830
1831    Ok((fields, files))
1832}
1833
1834static LAST_ERRORS: Lazy<Mutex<VecDeque<Value>>> =
1835    Lazy::new(|| Mutex::new(VecDeque::with_capacity(20)));
1836
1837/// Record last validation error for Admin UI inspection
1838pub fn record_validation_error(v: &Value) {
1839    if let Ok(mut q) = LAST_ERRORS.lock() {
1840        if q.len() >= 20 {
1841            q.pop_front();
1842        }
1843        q.push_back(v.clone());
1844    }
1845    // If mutex is poisoned, we silently fail - validation errors are informational only
1846}
1847
1848/// Get most recent validation error
1849pub fn get_last_validation_error() -> Option<Value> {
1850    LAST_ERRORS.lock().ok()?.back().cloned()
1851}
1852
1853/// Get recent validation errors (most recent last)
1854pub fn get_validation_errors() -> Vec<Value> {
1855    LAST_ERRORS.lock().map(|q| q.iter().cloned().collect()).unwrap_or_default()
1856}
1857
1858/// Coerce a parameter `value` into the expected JSON type per `schema` where reasonable.
1859/// Applies only to param contexts (not request bodies). Conservative conversions:
1860/// - integer/number: parse from string; arrays: split comma-separated strings and coerce items
1861/// - boolean: parse true/false (case-insensitive) from string
1862fn coerce_value_for_schema(value: &Value, schema: &openapiv3::Schema) -> Value {
1863    // Basic coercion: try to parse strings as appropriate types
1864    match value {
1865        Value::String(s) => {
1866            // Check if schema expects an array and we have a comma-separated string
1867            if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(array_type)) =
1868                &schema.schema_kind
1869            {
1870                if s.contains(',') {
1871                    // Split comma-separated string into array
1872                    let parts: Vec<&str> = s.split(',').map(|s| s.trim()).collect();
1873                    let mut array_values = Vec::new();
1874
1875                    for part in parts {
1876                        // Coerce each part based on array item type
1877                        if let Some(items_schema) = &array_type.items {
1878                            if let Some(items_schema_obj) = items_schema.as_item() {
1879                                let part_value = Value::String(part.to_string());
1880                                let coerced_part =
1881                                    coerce_value_for_schema(&part_value, items_schema_obj);
1882                                array_values.push(coerced_part);
1883                            } else {
1884                                // If items schema is a reference or not available, keep as string
1885                                array_values.push(Value::String(part.to_string()));
1886                            }
1887                        } else {
1888                            // No items schema defined, keep as string
1889                            array_values.push(Value::String(part.to_string()));
1890                        }
1891                    }
1892                    return Value::Array(array_values);
1893                }
1894            }
1895
1896            // Only coerce if the schema expects a different type
1897            match &schema.schema_kind {
1898                openapiv3::SchemaKind::Type(openapiv3::Type::String(_)) => {
1899                    // Schema expects string, keep as string
1900                    value.clone()
1901                }
1902                openapiv3::SchemaKind::Type(openapiv3::Type::Number(_)) => {
1903                    // Schema expects number, try to parse
1904                    if let Ok(n) = s.parse::<f64>() {
1905                        if let Some(num) = serde_json::Number::from_f64(n) {
1906                            return Value::Number(num);
1907                        }
1908                    }
1909                    value.clone()
1910                }
1911                openapiv3::SchemaKind::Type(openapiv3::Type::Integer(_)) => {
1912                    // Schema expects integer, try to parse
1913                    if let Ok(n) = s.parse::<i64>() {
1914                        if let Some(num) = serde_json::Number::from_f64(n as f64) {
1915                            return Value::Number(num);
1916                        }
1917                    }
1918                    value.clone()
1919                }
1920                openapiv3::SchemaKind::Type(openapiv3::Type::Boolean(_)) => {
1921                    // Schema expects boolean, try to parse
1922                    match s.to_lowercase().as_str() {
1923                        "true" | "1" | "yes" | "on" => Value::Bool(true),
1924                        "false" | "0" | "no" | "off" => Value::Bool(false),
1925                        _ => value.clone(),
1926                    }
1927                }
1928                _ => {
1929                    // Unknown schema type, keep as string
1930                    value.clone()
1931                }
1932            }
1933        }
1934        _ => value.clone(),
1935    }
1936}
1937
1938/// Apply style-aware coercion for query params
1939fn coerce_by_style(value: &Value, schema: &openapiv3::Schema, style: Option<&str>) -> Value {
1940    // Style-aware coercion for query parameters
1941    match value {
1942        Value::String(s) => {
1943            // Check if schema expects an array and we have a delimited string
1944            if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(array_type)) =
1945                &schema.schema_kind
1946            {
1947                let delimiter = match style {
1948                    Some("spaceDelimited") => " ",
1949                    Some("pipeDelimited") => "|",
1950                    Some("form") | None => ",", // Default to form style (comma-separated)
1951                    _ => ",",                   // Fallback to comma
1952                };
1953
1954                if s.contains(delimiter) {
1955                    // Split delimited string into array
1956                    let parts: Vec<&str> = s.split(delimiter).map(|s| s.trim()).collect();
1957                    let mut array_values = Vec::new();
1958
1959                    for part in parts {
1960                        // Coerce each part based on array item type
1961                        if let Some(items_schema) = &array_type.items {
1962                            if let Some(items_schema_obj) = items_schema.as_item() {
1963                                let part_value = Value::String(part.to_string());
1964                                let coerced_part =
1965                                    coerce_by_style(&part_value, items_schema_obj, style);
1966                                array_values.push(coerced_part);
1967                            } else {
1968                                // If items schema is a reference or not available, keep as string
1969                                array_values.push(Value::String(part.to_string()));
1970                            }
1971                        } else {
1972                            // No items schema defined, keep as string
1973                            array_values.push(Value::String(part.to_string()));
1974                        }
1975                    }
1976                    return Value::Array(array_values);
1977                }
1978            }
1979
1980            // Try to parse as number first
1981            if let Ok(n) = s.parse::<f64>() {
1982                if let Some(num) = serde_json::Number::from_f64(n) {
1983                    return Value::Number(num);
1984                }
1985            }
1986            // Try to parse as boolean
1987            match s.to_lowercase().as_str() {
1988                "true" | "1" | "yes" | "on" => return Value::Bool(true),
1989                "false" | "0" | "no" | "off" => return Value::Bool(false),
1990                _ => {}
1991            }
1992            // Keep as string
1993            value.clone()
1994        }
1995        _ => value.clone(),
1996    }
1997}
1998
1999/// Build a deepObject from query params like `name[prop]=val`
2000fn build_deep_object(name: &str, params: &Map<String, Value>) -> Option<Value> {
2001    let prefix = format!("{}[", name);
2002    let mut obj = Map::new();
2003    for (k, v) in params.iter() {
2004        if let Some(rest) = k.strip_prefix(&prefix) {
2005            if let Some(key) = rest.strip_suffix(']') {
2006                obj.insert(key.to_string(), v.clone());
2007            }
2008        }
2009    }
2010    if obj.is_empty() {
2011        None
2012    } else {
2013        Some(Value::Object(obj))
2014    }
2015}
2016
2017// Import the enhanced schema diff functionality
2018// use mockforge_foundation::schema_diff::{validation_diff, to_enhanced_422_json, ValidationError}; // Not currently used
2019
2020/// Generate an enhanced 422 response with detailed schema validation errors
2021/// This function provides comprehensive error information using the new schema diff utility
2022#[allow(clippy::too_many_arguments)]
2023fn generate_enhanced_422_response(
2024    validator: &OpenApiRouteRegistry,
2025    path_template: &str,
2026    method: &str,
2027    body: Option<&Value>,
2028    path_params: &Map<String, Value>,
2029    query_params: &Map<String, Value>,
2030    header_params: &Map<String, Value>,
2031    cookie_params: &Map<String, Value>,
2032) -> Value {
2033    let mut field_errors = Vec::new();
2034
2035    // Extract schema validation details if we have a route
2036    if let Some(route) = validator.get_route(path_template, method) {
2037        // Validate request body with detailed error collection
2038        if let Some(schema) = &route.operation.request_body {
2039            if let Some(value) = body {
2040                if let Some(content) =
2041                    schema.as_item().and_then(|rb| rb.content.get("application/json"))
2042                {
2043                    if let Some(_schema_ref) = &content.schema {
2044                        // Basic JSON validation - schema validation deferred
2045                        if serde_json::from_value::<Value>(value.clone()).is_err() {
2046                            field_errors.push(json!({
2047                                "path": "body",
2048                                "message": "invalid JSON"
2049                            }));
2050                        }
2051                    }
2052                }
2053            } else {
2054                field_errors.push(json!({
2055                    "path": "body",
2056                    "expected": "object",
2057                    "found": "missing",
2058                    "message": "Request body is required but not provided"
2059                }));
2060            }
2061        }
2062
2063        // Validate parameters with detailed error collection
2064        for param_ref in &route.operation.parameters {
2065            if let Some(param) = param_ref.as_item() {
2066                match param {
2067                    openapiv3::Parameter::Path { parameter_data, .. } => {
2068                        validate_parameter_detailed(
2069                            parameter_data,
2070                            path_params,
2071                            "path",
2072                            "path parameter",
2073                            &mut field_errors,
2074                        );
2075                    }
2076                    openapiv3::Parameter::Query { parameter_data, .. } => {
2077                        let deep_value = if Some("form") == Some("deepObject") {
2078                            build_deep_object(&parameter_data.name, query_params)
2079                        } else {
2080                            None
2081                        };
2082                        validate_parameter_detailed_with_deep(
2083                            parameter_data,
2084                            query_params,
2085                            "query",
2086                            "query parameter",
2087                            deep_value,
2088                            &mut field_errors,
2089                        );
2090                    }
2091                    openapiv3::Parameter::Header { parameter_data, .. } => {
2092                        validate_parameter_detailed(
2093                            parameter_data,
2094                            header_params,
2095                            "header",
2096                            "header parameter",
2097                            &mut field_errors,
2098                        );
2099                    }
2100                    openapiv3::Parameter::Cookie { parameter_data, .. } => {
2101                        validate_parameter_detailed(
2102                            parameter_data,
2103                            cookie_params,
2104                            "cookie",
2105                            "cookie parameter",
2106                            &mut field_errors,
2107                        );
2108                    }
2109                }
2110            }
2111        }
2112    }
2113
2114    // Return the detailed 422 error format
2115    json!({
2116        "error": "Schema validation failed",
2117        "details": field_errors,
2118        "method": method,
2119        "path": path_template,
2120        "timestamp": Utc::now().to_rfc3339(),
2121        "validation_type": "openapi_schema"
2122    })
2123}
2124
2125/// Helper function to validate a parameter
2126fn validate_parameter(
2127    parameter_data: &openapiv3::ParameterData,
2128    params_map: &Map<String, Value>,
2129    prefix: &str,
2130    aggregate: bool,
2131    errors: &mut Vec<String>,
2132    details: &mut Vec<Value>,
2133) {
2134    match params_map.get(&parameter_data.name) {
2135        Some(v) => {
2136            if let ParameterSchemaOrContent::Schema(s) = &parameter_data.format {
2137                if let Some(schema) = s.as_item() {
2138                    let coerced = coerce_value_for_schema(v, schema);
2139                    // Validate the coerced value against the schema
2140                    if let Err(validation_error) =
2141                        OpenApiSchema::new(schema.clone()).validate(&coerced)
2142                    {
2143                        let error_msg = validation_error.to_string();
2144                        errors.push(format!(
2145                            "{} parameter '{}' validation failed: {}",
2146                            prefix, parameter_data.name, error_msg
2147                        ));
2148                        if aggregate {
2149                            details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"schema_validation","message":error_msg}));
2150                        }
2151                    }
2152                }
2153            }
2154        }
2155        None => {
2156            if parameter_data.required {
2157                errors.push(format!(
2158                    "missing required {} parameter '{}'",
2159                    prefix, parameter_data.name
2160                ));
2161                details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"required","message":"Missing required parameter"}));
2162            }
2163        }
2164    }
2165}
2166
2167/// Helper function to validate a parameter with deep object support
2168#[allow(clippy::too_many_arguments)]
2169fn validate_parameter_with_deep_object(
2170    parameter_data: &openapiv3::ParameterData,
2171    params_map: &Map<String, Value>,
2172    prefix: &str,
2173    deep_value: Option<Value>,
2174    style: Option<&str>,
2175    aggregate: bool,
2176    errors: &mut Vec<String>,
2177    details: &mut Vec<Value>,
2178) {
2179    match deep_value.as_ref().or_else(|| params_map.get(&parameter_data.name)) {
2180        Some(v) => {
2181            if let ParameterSchemaOrContent::Schema(s) = &parameter_data.format {
2182                if let Some(schema) = s.as_item() {
2183                    let coerced = coerce_by_style(v, schema, style); // Use the actual style
2184                                                                     // Validate the coerced value against the schema
2185                    if let Err(validation_error) =
2186                        OpenApiSchema::new(schema.clone()).validate(&coerced)
2187                    {
2188                        let error_msg = validation_error.to_string();
2189                        errors.push(format!(
2190                            "{} parameter '{}' validation failed: {}",
2191                            prefix, parameter_data.name, error_msg
2192                        ));
2193                        if aggregate {
2194                            details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"schema_validation","message":error_msg}));
2195                        }
2196                    }
2197                }
2198            }
2199        }
2200        None => {
2201            if parameter_data.required {
2202                errors.push(format!(
2203                    "missing required {} parameter '{}'",
2204                    prefix, parameter_data.name
2205                ));
2206                details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"required","message":"Missing required parameter"}));
2207            }
2208        }
2209    }
2210}
2211
2212/// Helper function to validate a parameter with detailed error collection
2213fn validate_parameter_detailed(
2214    parameter_data: &openapiv3::ParameterData,
2215    params_map: &Map<String, Value>,
2216    location: &str,
2217    value_type: &str,
2218    field_errors: &mut Vec<Value>,
2219) {
2220    match params_map.get(&parameter_data.name) {
2221        Some(value) => {
2222            if let ParameterSchemaOrContent::Schema(schema) = &parameter_data.format {
2223                // Collect detailed validation errors for this parameter
2224                let details: Vec<Value> = Vec::new();
2225                let param_path = format!("{}.{}", location, parameter_data.name);
2226
2227                // Apply coercion before validation
2228                if let Some(schema_ref) = schema.as_item() {
2229                    let coerced_value = coerce_value_for_schema(value, schema_ref);
2230                    // Validate the coerced value against the schema
2231                    if let Err(validation_error) =
2232                        OpenApiSchema::new(schema_ref.clone()).validate(&coerced_value)
2233                    {
2234                        field_errors.push(json!({
2235                            "path": param_path,
2236                            "expected": "valid according to schema",
2237                            "found": coerced_value,
2238                            "message": validation_error.to_string()
2239                        }));
2240                    }
2241                }
2242
2243                for detail in details {
2244                    field_errors.push(json!({
2245                        "path": detail["path"],
2246                        "expected": detail["expected_type"],
2247                        "found": detail["value"],
2248                        "message": detail["message"]
2249                    }));
2250                }
2251            }
2252        }
2253        None => {
2254            if parameter_data.required {
2255                field_errors.push(json!({
2256                    "path": format!("{}.{}", location, parameter_data.name),
2257                    "expected": "value",
2258                    "found": "missing",
2259                    "message": format!("Missing required {} '{}'", value_type, parameter_data.name)
2260                }));
2261            }
2262        }
2263    }
2264}
2265
2266/// Helper function to validate a parameter with deep object support and detailed errors
2267fn validate_parameter_detailed_with_deep(
2268    parameter_data: &openapiv3::ParameterData,
2269    params_map: &Map<String, Value>,
2270    location: &str,
2271    value_type: &str,
2272    deep_value: Option<Value>,
2273    field_errors: &mut Vec<Value>,
2274) {
2275    match deep_value.as_ref().or_else(|| params_map.get(&parameter_data.name)) {
2276        Some(value) => {
2277            if let ParameterSchemaOrContent::Schema(schema) = &parameter_data.format {
2278                // Collect detailed validation errors for this parameter
2279                let details: Vec<Value> = Vec::new();
2280                let param_path = format!("{}.{}", location, parameter_data.name);
2281
2282                // Apply coercion before validation
2283                if let Some(schema_ref) = schema.as_item() {
2284                    let coerced_value = coerce_by_style(value, schema_ref, Some("form")); // Default to form style for now
2285                                                                                          // Validate the coerced value against the schema
2286                    if let Err(validation_error) =
2287                        OpenApiSchema::new(schema_ref.clone()).validate(&coerced_value)
2288                    {
2289                        field_errors.push(json!({
2290                            "path": param_path,
2291                            "expected": "valid according to schema",
2292                            "found": coerced_value,
2293                            "message": validation_error.to_string()
2294                        }));
2295                    }
2296                }
2297
2298                for detail in details {
2299                    field_errors.push(json!({
2300                        "path": detail["path"],
2301                        "expected": detail["expected_type"],
2302                        "found": detail["value"],
2303                        "message": detail["message"]
2304                    }));
2305                }
2306            }
2307        }
2308        None => {
2309            if parameter_data.required {
2310                field_errors.push(json!({
2311                    "path": format!("{}.{}", location, parameter_data.name),
2312                    "expected": "value",
2313                    "found": "missing",
2314                    "message": format!("Missing required {} '{}'", value_type, parameter_data.name)
2315                }));
2316            }
2317        }
2318    }
2319}
2320
2321/// Helper function to create an OpenAPI route registry from a file
2322pub async fn create_registry_from_file<P: AsRef<std::path::Path>>(
2323    path: P,
2324) -> Result<OpenApiRouteRegistry> {
2325    let spec = OpenApiSpec::from_file(path).await?;
2326    spec.validate()?;
2327    Ok(OpenApiRouteRegistry::new(spec))
2328}
2329
2330/// Helper function to create an OpenAPI route registry from JSON
2331pub fn create_registry_from_json(json: Value) -> Result<OpenApiRouteRegistry> {
2332    let spec = OpenApiSpec::from_json(json)?;
2333    spec.validate()?;
2334    Ok(OpenApiRouteRegistry::new(spec))
2335}
2336
2337#[cfg(test)]
2338mod tests {
2339    use super::*;
2340    use serde_json::json;
2341    use tempfile::TempDir;
2342
2343    #[tokio::test]
2344    async fn test_registry_creation() {
2345        let spec_json = json!({
2346            "openapi": "3.0.0",
2347            "info": {
2348                "title": "Test API",
2349                "version": "1.0.0"
2350            },
2351            "paths": {
2352                "/users": {
2353                    "get": {
2354                        "summary": "Get users",
2355                        "responses": {
2356                            "200": {
2357                                "description": "Success",
2358                                "content": {
2359                                    "application/json": {
2360                                        "schema": {
2361                                            "type": "array",
2362                                            "items": {
2363                                                "type": "object",
2364                                                "properties": {
2365                                                    "id": {"type": "integer"},
2366                                                    "name": {"type": "string"}
2367                                                }
2368                                            }
2369                                        }
2370                                    }
2371                                }
2372                            }
2373                        }
2374                    },
2375                    "post": {
2376                        "summary": "Create user",
2377                        "requestBody": {
2378                            "content": {
2379                                "application/json": {
2380                                    "schema": {
2381                                        "type": "object",
2382                                        "properties": {
2383                                            "name": {"type": "string"}
2384                                        },
2385                                        "required": ["name"]
2386                                    }
2387                                }
2388                            }
2389                        },
2390                        "responses": {
2391                            "201": {
2392                                "description": "Created",
2393                                "content": {
2394                                    "application/json": {
2395                                        "schema": {
2396                                            "type": "object",
2397                                            "properties": {
2398                                                "id": {"type": "integer"},
2399                                                "name": {"type": "string"}
2400                                            }
2401                                        }
2402                                    }
2403                                }
2404                            }
2405                        }
2406                    }
2407                },
2408                "/users/{id}": {
2409                    "get": {
2410                        "summary": "Get user by ID",
2411                        "parameters": [
2412                            {
2413                                "name": "id",
2414                                "in": "path",
2415                                "required": true,
2416                                "schema": {"type": "integer"}
2417                            }
2418                        ],
2419                        "responses": {
2420                            "200": {
2421                                "description": "Success",
2422                                "content": {
2423                                    "application/json": {
2424                                        "schema": {
2425                                            "type": "object",
2426                                            "properties": {
2427                                                "id": {"type": "integer"},
2428                                                "name": {"type": "string"}
2429                                            }
2430                                        }
2431                                    }
2432                                }
2433                            }
2434                        }
2435                    }
2436                }
2437            }
2438        });
2439
2440        let registry = create_registry_from_json(spec_json).unwrap();
2441
2442        // Test basic properties
2443        assert_eq!(registry.paths().len(), 2);
2444        assert!(registry.paths().contains(&"/users".to_string()));
2445        assert!(registry.paths().contains(&"/users/{id}".to_string()));
2446
2447        assert_eq!(registry.methods().len(), 2);
2448        assert!(registry.methods().contains(&"GET".to_string()));
2449        assert!(registry.methods().contains(&"POST".to_string()));
2450
2451        // Test route lookup
2452        let get_users_route = registry.get_route("/users", "GET").unwrap();
2453        assert_eq!(get_users_route.method, "GET");
2454        assert_eq!(get_users_route.path, "/users");
2455
2456        let post_users_route = registry.get_route("/users", "POST").unwrap();
2457        assert_eq!(post_users_route.method, "POST");
2458        assert!(post_users_route.operation.request_body.is_some());
2459
2460        // Test path parameter conversion
2461        let user_by_id_route = registry.get_route("/users/{id}", "GET").unwrap();
2462        assert_eq!(user_by_id_route.axum_path(), "/users/{id}");
2463    }
2464
2465    #[tokio::test]
2466    async fn test_validate_request_with_params_and_formats() {
2467        let spec_json = json!({
2468            "openapi": "3.0.0",
2469            "info": { "title": "Test API", "version": "1.0.0" },
2470            "paths": {
2471                "/users/{id}": {
2472                    "post": {
2473                        "parameters": [
2474                            { "name": "id", "in": "path", "required": true, "schema": {"type": "string"} },
2475                            { "name": "q",  "in": "query", "required": false, "schema": {"type": "integer"} }
2476                        ],
2477                        "requestBody": {
2478                            "content": {
2479                                "application/json": {
2480                                    "schema": {
2481                                        "type": "object",
2482                                        "required": ["email", "website"],
2483                                        "properties": {
2484                                            "email":   {"type": "string", "format": "email"},
2485                                            "website": {"type": "string", "format": "uri"}
2486                                        }
2487                                    }
2488                                }
2489                            }
2490                        },
2491                        "responses": {"200": {"description": "ok"}}
2492                    }
2493                }
2494            }
2495        });
2496
2497        let registry = create_registry_from_json(spec_json).unwrap();
2498        let mut path_params = Map::new();
2499        path_params.insert("id".to_string(), json!("abc"));
2500        let mut query_params = Map::new();
2501        query_params.insert("q".to_string(), json!(123));
2502
2503        // valid body
2504        let body = json!({"email":"a@b.co","website":"https://example.com"});
2505        assert!(registry
2506            .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&body))
2507            .is_ok());
2508
2509        // invalid email
2510        let bad_email = json!({"email":"not-an-email","website":"https://example.com"});
2511        assert!(registry
2512            .validate_request_with(
2513                "/users/{id}",
2514                "POST",
2515                &path_params,
2516                &query_params,
2517                Some(&bad_email)
2518            )
2519            .is_err());
2520
2521        // missing required path param
2522        let empty_path_params = Map::new();
2523        assert!(registry
2524            .validate_request_with(
2525                "/users/{id}",
2526                "POST",
2527                &empty_path_params,
2528                &query_params,
2529                Some(&body)
2530            )
2531            .is_err());
2532    }
2533
2534    #[tokio::test]
2535    async fn test_ref_resolution_for_params_and_body() {
2536        let spec_json = json!({
2537            "openapi": "3.0.0",
2538            "info": { "title": "Ref API", "version": "1.0.0" },
2539            "components": {
2540                "schemas": {
2541                    "EmailWebsite": {
2542                        "type": "object",
2543                        "required": ["email", "website"],
2544                        "properties": {
2545                            "email":   {"type": "string", "format": "email"},
2546                            "website": {"type": "string", "format": "uri"}
2547                        }
2548                    }
2549                },
2550                "parameters": {
2551                    "PathId": {"name": "id", "in": "path", "required": true, "schema": {"type": "string"}},
2552                    "QueryQ": {"name": "q",  "in": "query", "required": false, "schema": {"type": "integer"}}
2553                },
2554                "requestBodies": {
2555                    "CreateUser": {
2556                        "content": {
2557                            "application/json": {
2558                                "schema": {"$ref": "#/components/schemas/EmailWebsite"}
2559                            }
2560                        }
2561                    }
2562                }
2563            },
2564            "paths": {
2565                "/users/{id}": {
2566                    "post": {
2567                        "parameters": [
2568                            {"$ref": "#/components/parameters/PathId"},
2569                            {"$ref": "#/components/parameters/QueryQ"}
2570                        ],
2571                        "requestBody": {"$ref": "#/components/requestBodies/CreateUser"},
2572                        "responses": {"200": {"description": "ok"}}
2573                    }
2574                }
2575            }
2576        });
2577
2578        let registry = create_registry_from_json(spec_json).unwrap();
2579        let mut path_params = Map::new();
2580        path_params.insert("id".to_string(), json!("abc"));
2581        let mut query_params = Map::new();
2582        query_params.insert("q".to_string(), json!(7));
2583
2584        let body = json!({"email":"user@example.com","website":"https://example.com"});
2585        assert!(registry
2586            .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&body))
2587            .is_ok());
2588
2589        let bad = json!({"email":"nope","website":"https://example.com"});
2590        assert!(registry
2591            .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&bad))
2592            .is_err());
2593    }
2594
2595    #[tokio::test]
2596    async fn test_header_cookie_and_query_coercion() {
2597        let spec_json = json!({
2598            "openapi": "3.0.0",
2599            "info": { "title": "Params API", "version": "1.0.0" },
2600            "paths": {
2601                "/items": {
2602                    "get": {
2603                        "parameters": [
2604                            {"name": "X-Flag", "in": "header", "required": true, "schema": {"type": "boolean"}},
2605                            {"name": "session", "in": "cookie", "required": true, "schema": {"type": "string"}},
2606                            {"name": "ids", "in": "query", "required": false, "schema": {"type": "array", "items": {"type": "integer"}}}
2607                        ],
2608                        "responses": {"200": {"description": "ok"}}
2609                    }
2610                }
2611            }
2612        });
2613
2614        let registry = create_registry_from_json(spec_json).unwrap();
2615
2616        let path_params = Map::new();
2617        let mut query_params = Map::new();
2618        // comma-separated string for array should coerce
2619        query_params.insert("ids".to_string(), json!("1,2,3"));
2620        let mut header_params = Map::new();
2621        header_params.insert("X-Flag".to_string(), json!("true"));
2622        let mut cookie_params = Map::new();
2623        cookie_params.insert("session".to_string(), json!("abc123"));
2624
2625        assert!(registry
2626            .validate_request_with_all(
2627                "/items",
2628                "GET",
2629                &path_params,
2630                &query_params,
2631                &header_params,
2632                &cookie_params,
2633                None
2634            )
2635            .is_ok());
2636
2637        // Missing required cookie
2638        let empty_cookie = Map::new();
2639        assert!(registry
2640            .validate_request_with_all(
2641                "/items",
2642                "GET",
2643                &path_params,
2644                &query_params,
2645                &header_params,
2646                &empty_cookie,
2647                None
2648            )
2649            .is_err());
2650
2651        // Bad boolean header value (cannot coerce)
2652        let mut bad_header = Map::new();
2653        bad_header.insert("X-Flag".to_string(), json!("notabool"));
2654        assert!(registry
2655            .validate_request_with_all(
2656                "/items",
2657                "GET",
2658                &path_params,
2659                &query_params,
2660                &bad_header,
2661                &cookie_params,
2662                None
2663            )
2664            .is_err());
2665    }
2666
2667    #[tokio::test]
2668    async fn test_query_styles_space_pipe_deepobject() {
2669        let spec_json = json!({
2670            "openapi": "3.0.0",
2671            "info": { "title": "Query Styles API", "version": "1.0.0" },
2672            "paths": {"/search": {"get": {
2673                "parameters": [
2674                    {"name":"tags","in":"query","style":"spaceDelimited","schema":{"type":"array","items":{"type":"string"}}},
2675                    {"name":"ids","in":"query","style":"pipeDelimited","schema":{"type":"array","items":{"type":"integer"}}},
2676                    {"name":"filter","in":"query","style":"deepObject","schema":{"type":"object","properties":{"color":{"type":"string"}},"required":["color"]}}
2677                ],
2678                "responses": {"200": {"description":"ok"}}
2679            }} }
2680        });
2681
2682        let registry = create_registry_from_json(spec_json).unwrap();
2683
2684        let path_params = Map::new();
2685        let mut query = Map::new();
2686        query.insert("tags".into(), json!("alpha beta gamma"));
2687        query.insert("ids".into(), json!("1|2|3"));
2688        query.insert("filter[color]".into(), json!("red"));
2689
2690        assert!(registry
2691            .validate_request_with("/search", "GET", &path_params, &query, None)
2692            .is_ok());
2693    }
2694
2695    #[tokio::test]
2696    async fn test_oneof_anyof_allof_validation() {
2697        let spec_json = json!({
2698            "openapi": "3.0.0",
2699            "info": { "title": "Composite API", "version": "1.0.0" },
2700            "paths": {
2701                "/composite": {
2702                    "post": {
2703                        "requestBody": {
2704                            "content": {
2705                                "application/json": {
2706                                    "schema": {
2707                                        "allOf": [
2708                                            {"type": "object", "required": ["base"], "properties": {"base": {"type": "string"}}}
2709                                        ],
2710                                        "oneOf": [
2711                                            {"type": "object", "properties": {"a": {"type": "integer"}}, "required": ["a"], "not": {"required": ["b"]}},
2712                                            {"type": "object", "properties": {"b": {"type": "integer"}}, "required": ["b"], "not": {"required": ["a"]}}
2713                                        ],
2714                                        "anyOf": [
2715                                            {"type": "object", "properties": {"flag": {"type": "boolean"}}, "required": ["flag"]},
2716                                            {"type": "object", "properties": {"extra": {"type": "string"}}, "required": ["extra"]}
2717                                        ]
2718                                    }
2719                                }
2720                            }
2721                        },
2722                        "responses": {"200": {"description": "ok"}}
2723                    }
2724                }
2725            }
2726        });
2727
2728        let registry = create_registry_from_json(spec_json).unwrap();
2729        // valid: satisfies base via allOf, exactly one of a/b, and at least one of flag/extra
2730        let ok = json!({"base": "x", "a": 1, "flag": true});
2731        assert!(registry
2732            .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&ok))
2733            .is_ok());
2734
2735        // invalid oneOf: both a and b present
2736        let bad_oneof = json!({"base": "x", "a": 1, "b": 2, "flag": false});
2737        assert!(registry
2738            .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&bad_oneof))
2739            .is_err());
2740
2741        // invalid anyOf: none of flag/extra present
2742        let bad_anyof = json!({"base": "x", "a": 1});
2743        assert!(registry
2744            .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&bad_anyof))
2745            .is_err());
2746
2747        // invalid allOf: missing base
2748        let bad_allof = json!({"a": 1, "flag": true});
2749        assert!(registry
2750            .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&bad_allof))
2751            .is_err());
2752    }
2753
2754    #[tokio::test]
2755    async fn test_overrides_warn_mode_allows_invalid() {
2756        // Spec with a POST route expecting an integer query param
2757        let spec_json = json!({
2758            "openapi": "3.0.0",
2759            "info": { "title": "Overrides API", "version": "1.0.0" },
2760            "paths": {"/things": {"post": {
2761                "parameters": [{"name":"q","in":"query","required":true,"schema":{"type":"integer"}}],
2762                "responses": {"200": {"description":"ok"}}
2763            }}}
2764        });
2765
2766        let spec = OpenApiSpec::from_json(spec_json).unwrap();
2767        let mut overrides = HashMap::new();
2768        overrides.insert("POST /things".to_string(), ValidationMode::Warn);
2769        let registry = OpenApiRouteRegistry::new_with_options(
2770            spec,
2771            ValidationOptions {
2772                request_mode: ValidationMode::Enforce,
2773                aggregate_errors: true,
2774                validate_responses: false,
2775                overrides,
2776                admin_skip_prefixes: vec![],
2777                response_template_expand: false,
2778                validation_status: None,
2779            },
2780        );
2781
2782        // Invalid q (missing) should warn, not error
2783        let ok = registry.validate_request_with("/things", "POST", &Map::new(), &Map::new(), None);
2784        assert!(ok.is_ok());
2785    }
2786
2787    #[tokio::test]
2788    async fn test_admin_skip_prefix_short_circuit() {
2789        let spec_json = json!({
2790            "openapi": "3.0.0",
2791            "info": { "title": "Skip API", "version": "1.0.0" },
2792            "paths": {}
2793        });
2794        let spec = OpenApiSpec::from_json(spec_json).unwrap();
2795        let registry = OpenApiRouteRegistry::new_with_options(
2796            spec,
2797            ValidationOptions {
2798                request_mode: ValidationMode::Enforce,
2799                aggregate_errors: true,
2800                validate_responses: false,
2801                overrides: HashMap::new(),
2802                admin_skip_prefixes: vec!["/admin".into()],
2803                response_template_expand: false,
2804                validation_status: None,
2805            },
2806        );
2807
2808        // No route exists for this, but skip prefix means it is accepted
2809        let res = registry.validate_request_with_all(
2810            "/admin/__mockforge/health",
2811            "GET",
2812            &Map::new(),
2813            &Map::new(),
2814            &Map::new(),
2815            &Map::new(),
2816            None,
2817        );
2818        assert!(res.is_ok());
2819    }
2820
2821    #[test]
2822    fn test_path_conversion() {
2823        assert_eq!(OpenApiRouteRegistry::convert_path_to_axum("/users"), "/users");
2824        assert_eq!(OpenApiRouteRegistry::convert_path_to_axum("/users/{id}"), "/users/{id}");
2825        assert_eq!(
2826            OpenApiRouteRegistry::convert_path_to_axum("/users/{id}/posts/{postId}"),
2827            "/users/{id}/posts/{postId}"
2828        );
2829    }
2830
2831    #[test]
2832    fn test_validation_options_default() {
2833        let options = ValidationOptions::default();
2834        assert!(matches!(options.request_mode, ValidationMode::Enforce));
2835        assert!(options.aggregate_errors);
2836        assert!(!options.validate_responses);
2837        assert!(options.overrides.is_empty());
2838        assert!(options.admin_skip_prefixes.is_empty());
2839        assert!(!options.response_template_expand);
2840        assert!(options.validation_status.is_none());
2841    }
2842
2843    #[test]
2844    fn test_validation_mode_variants() {
2845        // Test that all variants can be created and compared
2846        let disabled = ValidationMode::Disabled;
2847        let warn = ValidationMode::Warn;
2848        let enforce = ValidationMode::Enforce;
2849        let default = ValidationMode::default();
2850
2851        // Test that default is Warn
2852        assert!(matches!(default, ValidationMode::Warn));
2853
2854        // Test that variants are distinct
2855        assert!(!matches!(disabled, ValidationMode::Warn));
2856        assert!(!matches!(warn, ValidationMode::Enforce));
2857        assert!(!matches!(enforce, ValidationMode::Disabled));
2858    }
2859
2860    #[test]
2861    fn test_registry_spec_accessor() {
2862        let spec_json = json!({
2863            "openapi": "3.0.0",
2864            "info": {
2865                "title": "Test API",
2866                "version": "1.0.0"
2867            },
2868            "paths": {}
2869        });
2870        let spec = OpenApiSpec::from_json(spec_json).unwrap();
2871        let registry = OpenApiRouteRegistry::new(spec.clone());
2872
2873        // Test spec() accessor
2874        let accessed_spec = registry.spec();
2875        assert_eq!(accessed_spec.title(), "Test API");
2876    }
2877
2878    #[test]
2879    fn test_clone_for_validation() {
2880        let spec_json = json!({
2881            "openapi": "3.0.0",
2882            "info": {
2883                "title": "Test API",
2884                "version": "1.0.0"
2885            },
2886            "paths": {
2887                "/users": {
2888                    "get": {
2889                        "responses": {
2890                            "200": {
2891                                "description": "Success"
2892                            }
2893                        }
2894                    }
2895                }
2896            }
2897        });
2898        let spec = OpenApiSpec::from_json(spec_json).unwrap();
2899        let registry = OpenApiRouteRegistry::new(spec);
2900
2901        // Test clone_for_validation
2902        let cloned = registry.clone_for_validation();
2903        assert_eq!(cloned.routes().len(), registry.routes().len());
2904        assert_eq!(cloned.spec().title(), registry.spec().title());
2905    }
2906
2907    #[test]
2908    fn test_with_custom_fixture_loader() {
2909        let temp_dir = TempDir::new().unwrap();
2910        let spec_json = json!({
2911            "openapi": "3.0.0",
2912            "info": {
2913                "title": "Test API",
2914                "version": "1.0.0"
2915            },
2916            "paths": {}
2917        });
2918        let spec = OpenApiSpec::from_json(spec_json).unwrap();
2919        let registry = OpenApiRouteRegistry::new(spec);
2920        let original_routes_len = registry.routes().len();
2921
2922        // Test with_custom_fixture_loader
2923        let custom_loader = Arc::new(crate::custom_fixture::CustomFixtureLoader::new(
2924            temp_dir.path().to_path_buf(),
2925            true,
2926        ));
2927        let registry_with_loader = registry.with_custom_fixture_loader(custom_loader);
2928
2929        // Verify the loader was set (we can't directly access it, but we can test it doesn't panic)
2930        assert_eq!(registry_with_loader.routes().len(), original_routes_len);
2931    }
2932
2933    #[test]
2934    fn test_get_route() {
2935        let spec_json = json!({
2936            "openapi": "3.0.0",
2937            "info": {
2938                "title": "Test API",
2939                "version": "1.0.0"
2940            },
2941            "paths": {
2942                "/users": {
2943                    "get": {
2944                        "operationId": "getUsers",
2945                        "responses": {
2946                            "200": {
2947                                "description": "Success"
2948                            }
2949                        }
2950                    },
2951                    "post": {
2952                        "operationId": "createUser",
2953                        "responses": {
2954                            "201": {
2955                                "description": "Created"
2956                            }
2957                        }
2958                    }
2959                }
2960            }
2961        });
2962        let spec = OpenApiSpec::from_json(spec_json).unwrap();
2963        let registry = OpenApiRouteRegistry::new(spec);
2964
2965        // Test get_route for existing route
2966        let route = registry.get_route("/users", "GET");
2967        assert!(route.is_some());
2968        assert_eq!(route.unwrap().method, "GET");
2969        assert_eq!(route.unwrap().path, "/users");
2970
2971        // Test get_route for non-existent route
2972        let route = registry.get_route("/nonexistent", "GET");
2973        assert!(route.is_none());
2974
2975        // Test get_route for different method
2976        let route = registry.get_route("/users", "POST");
2977        assert!(route.is_some());
2978        assert_eq!(route.unwrap().method, "POST");
2979    }
2980
2981    #[test]
2982    fn test_get_routes_for_path() {
2983        let spec_json = json!({
2984            "openapi": "3.0.0",
2985            "info": {
2986                "title": "Test API",
2987                "version": "1.0.0"
2988            },
2989            "paths": {
2990                "/users": {
2991                    "get": {
2992                        "responses": {
2993                            "200": {
2994                                "description": "Success"
2995                            }
2996                        }
2997                    },
2998                    "post": {
2999                        "responses": {
3000                            "201": {
3001                                "description": "Created"
3002                            }
3003                        }
3004                    },
3005                    "put": {
3006                        "responses": {
3007                            "200": {
3008                                "description": "Success"
3009                            }
3010                        }
3011                    }
3012                },
3013                "/posts": {
3014                    "get": {
3015                        "responses": {
3016                            "200": {
3017                                "description": "Success"
3018                            }
3019                        }
3020                    }
3021                }
3022            }
3023        });
3024        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3025        let registry = OpenApiRouteRegistry::new(spec);
3026
3027        // Test get_routes_for_path with multiple methods
3028        let routes = registry.get_routes_for_path("/users");
3029        assert_eq!(routes.len(), 3);
3030        let methods: Vec<&str> = routes.iter().map(|r| r.method.as_str()).collect();
3031        assert!(methods.contains(&"GET"));
3032        assert!(methods.contains(&"POST"));
3033        assert!(methods.contains(&"PUT"));
3034
3035        // Test get_routes_for_path with single method
3036        let routes = registry.get_routes_for_path("/posts");
3037        assert_eq!(routes.len(), 1);
3038        assert_eq!(routes[0].method, "GET");
3039
3040        // Test get_routes_for_path with non-existent path
3041        let routes = registry.get_routes_for_path("/nonexistent");
3042        assert!(routes.is_empty());
3043    }
3044
3045    #[test]
3046    fn test_new_vs_new_with_options() {
3047        let spec_json = json!({
3048            "openapi": "3.0.0",
3049            "info": {
3050                "title": "Test API",
3051                "version": "1.0.0"
3052            },
3053            "paths": {}
3054        });
3055        let spec1 = OpenApiSpec::from_json(spec_json.clone()).unwrap();
3056        let spec2 = OpenApiSpec::from_json(spec_json).unwrap();
3057
3058        // Test new() - uses environment-based options
3059        let registry1 = OpenApiRouteRegistry::new(spec1);
3060        assert_eq!(registry1.spec().title(), "Test API");
3061
3062        // Test new_with_options() - uses explicit options
3063        let options = ValidationOptions {
3064            request_mode: ValidationMode::Disabled,
3065            aggregate_errors: false,
3066            validate_responses: true,
3067            overrides: HashMap::new(),
3068            admin_skip_prefixes: vec!["/admin".to_string()],
3069            response_template_expand: true,
3070            validation_status: Some(422),
3071        };
3072        let registry2 = OpenApiRouteRegistry::new_with_options(spec2, options);
3073        assert_eq!(registry2.spec().title(), "Test API");
3074    }
3075
3076    #[test]
3077    fn test_new_with_env_vs_new() {
3078        let spec_json = json!({
3079            "openapi": "3.0.0",
3080            "info": {
3081                "title": "Test API",
3082                "version": "1.0.0"
3083            },
3084            "paths": {}
3085        });
3086        let spec1 = OpenApiSpec::from_json(spec_json.clone()).unwrap();
3087        let spec2 = OpenApiSpec::from_json(spec_json).unwrap();
3088
3089        // Test new() calls new_with_env()
3090        let registry1 = OpenApiRouteRegistry::new(spec1);
3091
3092        // Test new_with_env() directly
3093        let registry2 = OpenApiRouteRegistry::new_with_env(spec2);
3094
3095        // Both should create valid registries
3096        assert_eq!(registry1.spec().title(), "Test API");
3097        assert_eq!(registry2.spec().title(), "Test API");
3098    }
3099
3100    #[test]
3101    fn test_validation_options_custom() {
3102        let options = ValidationOptions {
3103            request_mode: ValidationMode::Warn,
3104            aggregate_errors: false,
3105            validate_responses: true,
3106            overrides: {
3107                let mut map = HashMap::new();
3108                map.insert("getUsers".to_string(), ValidationMode::Disabled);
3109                map
3110            },
3111            admin_skip_prefixes: vec!["/admin".to_string(), "/internal".to_string()],
3112            response_template_expand: true,
3113            validation_status: Some(422),
3114        };
3115
3116        assert!(matches!(options.request_mode, ValidationMode::Warn));
3117        assert!(!options.aggregate_errors);
3118        assert!(options.validate_responses);
3119        assert_eq!(options.overrides.len(), 1);
3120        assert_eq!(options.admin_skip_prefixes.len(), 2);
3121        assert!(options.response_template_expand);
3122        assert_eq!(options.validation_status, Some(422));
3123    }
3124
3125    #[test]
3126    fn test_validation_mode_default_standalone() {
3127        let mode = ValidationMode::default();
3128        assert!(matches!(mode, ValidationMode::Warn));
3129    }
3130
3131    #[test]
3132    fn test_validation_mode_clone() {
3133        let mode1 = ValidationMode::Enforce;
3134        let mode2 = mode1.clone();
3135        assert!(matches!(mode1, ValidationMode::Enforce));
3136        assert!(matches!(mode2, ValidationMode::Enforce));
3137    }
3138
3139    #[test]
3140    fn test_validation_mode_debug() {
3141        let mode = ValidationMode::Disabled;
3142        let debug_str = format!("{:?}", mode);
3143        assert!(debug_str.contains("Disabled") || debug_str.contains("ValidationMode"));
3144    }
3145
3146    #[test]
3147    fn test_validation_options_clone() {
3148        let options1 = ValidationOptions {
3149            request_mode: ValidationMode::Warn,
3150            aggregate_errors: true,
3151            validate_responses: false,
3152            overrides: HashMap::new(),
3153            admin_skip_prefixes: vec![],
3154            response_template_expand: false,
3155            validation_status: None,
3156        };
3157        let options2 = options1.clone();
3158        assert!(matches!(options2.request_mode, ValidationMode::Warn));
3159        assert_eq!(options1.aggregate_errors, options2.aggregate_errors);
3160    }
3161
3162    #[test]
3163    fn test_validation_options_debug() {
3164        let options = ValidationOptions::default();
3165        let debug_str = format!("{:?}", options);
3166        assert!(debug_str.contains("ValidationOptions"));
3167    }
3168
3169    #[test]
3170    fn test_validation_options_with_all_fields() {
3171        let mut overrides = HashMap::new();
3172        overrides.insert("op1".to_string(), ValidationMode::Disabled);
3173        overrides.insert("op2".to_string(), ValidationMode::Warn);
3174
3175        let options = ValidationOptions {
3176            request_mode: ValidationMode::Enforce,
3177            aggregate_errors: false,
3178            validate_responses: true,
3179            overrides: overrides.clone(),
3180            admin_skip_prefixes: vec!["/admin".to_string(), "/internal".to_string()],
3181            response_template_expand: true,
3182            validation_status: Some(422),
3183        };
3184
3185        assert!(matches!(options.request_mode, ValidationMode::Enforce));
3186        assert!(!options.aggregate_errors);
3187        assert!(options.validate_responses);
3188        assert_eq!(options.overrides.len(), 2);
3189        assert_eq!(options.admin_skip_prefixes.len(), 2);
3190        assert!(options.response_template_expand);
3191        assert_eq!(options.validation_status, Some(422));
3192    }
3193
3194    #[test]
3195    fn test_openapi_route_registry_clone() {
3196        let spec_json = json!({
3197            "openapi": "3.0.0",
3198            "info": { "title": "Test API", "version": "1.0.0" },
3199            "paths": {}
3200        });
3201        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3202        let registry1 = OpenApiRouteRegistry::new(spec);
3203        let registry2 = registry1.clone();
3204        assert_eq!(registry1.spec().title(), registry2.spec().title());
3205    }
3206
3207    #[test]
3208    fn test_validation_mode_serialization() {
3209        let mode = ValidationMode::Enforce;
3210        let json = serde_json::to_string(&mode).unwrap();
3211        assert!(json.contains("Enforce") || json.contains("enforce"));
3212    }
3213
3214    #[test]
3215    fn test_validation_mode_deserialization() {
3216        let json = r#""Disabled""#;
3217        let mode: ValidationMode = serde_json::from_str(json).unwrap();
3218        assert!(matches!(mode, ValidationMode::Disabled));
3219    }
3220
3221    #[test]
3222    fn test_validation_options_default_values() {
3223        let options = ValidationOptions::default();
3224        assert!(matches!(options.request_mode, ValidationMode::Enforce));
3225        assert!(options.aggregate_errors);
3226        assert!(!options.validate_responses);
3227        assert!(options.overrides.is_empty());
3228        assert!(options.admin_skip_prefixes.is_empty());
3229        assert!(!options.response_template_expand);
3230        assert_eq!(options.validation_status, None);
3231    }
3232
3233    #[test]
3234    fn test_validation_mode_all_variants() {
3235        let disabled = ValidationMode::Disabled;
3236        let warn = ValidationMode::Warn;
3237        let enforce = ValidationMode::Enforce;
3238
3239        assert!(matches!(disabled, ValidationMode::Disabled));
3240        assert!(matches!(warn, ValidationMode::Warn));
3241        assert!(matches!(enforce, ValidationMode::Enforce));
3242    }
3243
3244    #[test]
3245    fn test_validation_options_with_overrides() {
3246        let mut overrides = HashMap::new();
3247        overrides.insert("operation1".to_string(), ValidationMode::Disabled);
3248        overrides.insert("operation2".to_string(), ValidationMode::Warn);
3249
3250        let options = ValidationOptions {
3251            request_mode: ValidationMode::Enforce,
3252            aggregate_errors: true,
3253            validate_responses: false,
3254            overrides,
3255            admin_skip_prefixes: vec![],
3256            response_template_expand: false,
3257            validation_status: None,
3258        };
3259
3260        assert_eq!(options.overrides.len(), 2);
3261        assert!(matches!(options.overrides.get("operation1"), Some(ValidationMode::Disabled)));
3262        assert!(matches!(options.overrides.get("operation2"), Some(ValidationMode::Warn)));
3263    }
3264
3265    #[test]
3266    fn test_validation_options_with_admin_skip_prefixes() {
3267        let options = ValidationOptions {
3268            request_mode: ValidationMode::Enforce,
3269            aggregate_errors: true,
3270            validate_responses: false,
3271            overrides: HashMap::new(),
3272            admin_skip_prefixes: vec![
3273                "/admin".to_string(),
3274                "/internal".to_string(),
3275                "/debug".to_string(),
3276            ],
3277            response_template_expand: false,
3278            validation_status: None,
3279        };
3280
3281        assert_eq!(options.admin_skip_prefixes.len(), 3);
3282        assert!(options.admin_skip_prefixes.contains(&"/admin".to_string()));
3283        assert!(options.admin_skip_prefixes.contains(&"/internal".to_string()));
3284        assert!(options.admin_skip_prefixes.contains(&"/debug".to_string()));
3285    }
3286
3287    #[test]
3288    fn test_validation_options_with_validation_status() {
3289        let options1 = ValidationOptions {
3290            request_mode: ValidationMode::Enforce,
3291            aggregate_errors: true,
3292            validate_responses: false,
3293            overrides: HashMap::new(),
3294            admin_skip_prefixes: vec![],
3295            response_template_expand: false,
3296            validation_status: Some(400),
3297        };
3298
3299        let options2 = ValidationOptions {
3300            request_mode: ValidationMode::Enforce,
3301            aggregate_errors: true,
3302            validate_responses: false,
3303            overrides: HashMap::new(),
3304            admin_skip_prefixes: vec![],
3305            response_template_expand: false,
3306            validation_status: Some(422),
3307        };
3308
3309        assert_eq!(options1.validation_status, Some(400));
3310        assert_eq!(options2.validation_status, Some(422));
3311    }
3312
3313    #[test]
3314    fn test_validate_request_with_disabled_mode() {
3315        // Test validation with disabled mode (lines 1001-1007)
3316        let spec_json = json!({
3317            "openapi": "3.0.0",
3318            "info": {"title": "Test API", "version": "1.0.0"},
3319            "paths": {
3320                "/users": {
3321                    "get": {
3322                        "responses": {"200": {"description": "OK"}}
3323                    }
3324                }
3325            }
3326        });
3327        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3328        let options = ValidationOptions {
3329            request_mode: ValidationMode::Disabled,
3330            ..Default::default()
3331        };
3332        let registry = OpenApiRouteRegistry::new_with_options(spec, options);
3333
3334        // Should pass validation when disabled (lines 1002-1003, 1005-1007)
3335        let result = registry.validate_request_with_all(
3336            "/users",
3337            "GET",
3338            &Map::new(),
3339            &Map::new(),
3340            &Map::new(),
3341            &Map::new(),
3342            None,
3343        );
3344        assert!(result.is_ok());
3345    }
3346
3347    #[test]
3348    fn test_validate_request_with_warn_mode() {
3349        // Test validation with warn mode (lines 1162-1166)
3350        let spec_json = json!({
3351            "openapi": "3.0.0",
3352            "info": {"title": "Test API", "version": "1.0.0"},
3353            "paths": {
3354                "/users": {
3355                    "post": {
3356                        "requestBody": {
3357                            "required": true,
3358                            "content": {
3359                                "application/json": {
3360                                    "schema": {
3361                                        "type": "object",
3362                                        "required": ["name"],
3363                                        "properties": {
3364                                            "name": {"type": "string"}
3365                                        }
3366                                    }
3367                                }
3368                            }
3369                        },
3370                        "responses": {"200": {"description": "OK"}}
3371                    }
3372                }
3373            }
3374        });
3375        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3376        let options = ValidationOptions {
3377            request_mode: ValidationMode::Warn,
3378            ..Default::default()
3379        };
3380        let registry = OpenApiRouteRegistry::new_with_options(spec, options);
3381
3382        // Should pass with warnings when body is missing (lines 1162-1166)
3383        let result = registry.validate_request_with_all(
3384            "/users",
3385            "POST",
3386            &Map::new(),
3387            &Map::new(),
3388            &Map::new(),
3389            &Map::new(),
3390            None, // Missing required body
3391        );
3392        assert!(result.is_ok()); // Warn mode doesn't fail
3393    }
3394
3395    #[test]
3396    fn test_validate_request_body_validation_error() {
3397        // Test request body validation error path (lines 1072-1091)
3398        let spec_json = json!({
3399            "openapi": "3.0.0",
3400            "info": {"title": "Test API", "version": "1.0.0"},
3401            "paths": {
3402                "/users": {
3403                    "post": {
3404                        "requestBody": {
3405                            "required": true,
3406                            "content": {
3407                                "application/json": {
3408                                    "schema": {
3409                                        "type": "object",
3410                                        "required": ["name"],
3411                                        "properties": {
3412                                            "name": {"type": "string"}
3413                                        }
3414                                    }
3415                                }
3416                            }
3417                        },
3418                        "responses": {"200": {"description": "OK"}}
3419                    }
3420                }
3421            }
3422        });
3423        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3424        let registry = OpenApiRouteRegistry::new(spec);
3425
3426        // Should fail validation when body is missing (lines 1088-1091)
3427        let result = registry.validate_request_with_all(
3428            "/users",
3429            "POST",
3430            &Map::new(),
3431            &Map::new(),
3432            &Map::new(),
3433            &Map::new(),
3434            None, // Missing required body
3435        );
3436        assert!(result.is_err());
3437    }
3438
3439    #[test]
3440    fn test_validate_request_body_schema_validation_error() {
3441        // Test request body schema validation error (lines 1038-1049)
3442        let spec_json = json!({
3443            "openapi": "3.0.0",
3444            "info": {"title": "Test API", "version": "1.0.0"},
3445            "paths": {
3446                "/users": {
3447                    "post": {
3448                        "requestBody": {
3449                            "required": true,
3450                            "content": {
3451                                "application/json": {
3452                                    "schema": {
3453                                        "type": "object",
3454                                        "required": ["name"],
3455                                        "properties": {
3456                                            "name": {"type": "string"}
3457                                        }
3458                                    }
3459                                }
3460                            }
3461                        },
3462                        "responses": {"200": {"description": "OK"}}
3463                    }
3464                }
3465            }
3466        });
3467        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3468        let registry = OpenApiRouteRegistry::new(spec);
3469
3470        // Should fail validation when body doesn't match schema (lines 1038-1049)
3471        let invalid_body = json!({}); // Missing required "name" field
3472        let result = registry.validate_request_with_all(
3473            "/users",
3474            "POST",
3475            &Map::new(),
3476            &Map::new(),
3477            &Map::new(),
3478            &Map::new(),
3479            Some(&invalid_body),
3480        );
3481        assert!(result.is_err());
3482    }
3483
3484    #[test]
3485    fn test_validate_request_body_referenced_schema_error() {
3486        // Test request body with referenced schema that can't be resolved (lines 1070-1076)
3487        let spec_json = json!({
3488            "openapi": "3.0.0",
3489            "info": {"title": "Test API", "version": "1.0.0"},
3490            "paths": {
3491                "/users": {
3492                    "post": {
3493                        "requestBody": {
3494                            "required": true,
3495                            "content": {
3496                                "application/json": {
3497                                    "schema": {
3498                                        "$ref": "#/components/schemas/NonExistentSchema"
3499                                    }
3500                                }
3501                            }
3502                        },
3503                        "responses": {"200": {"description": "OK"}}
3504                    }
3505                }
3506            },
3507            "components": {}
3508        });
3509        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3510        let registry = OpenApiRouteRegistry::new(spec);
3511
3512        // Should fail validation when schema reference can't be resolved (lines 1070-1076)
3513        let body = json!({"name": "test"});
3514        let result = registry.validate_request_with_all(
3515            "/users",
3516            "POST",
3517            &Map::new(),
3518            &Map::new(),
3519            &Map::new(),
3520            &Map::new(),
3521            Some(&body),
3522        );
3523        assert!(result.is_err());
3524    }
3525
3526    #[test]
3527    fn test_validate_request_body_referenced_request_body_error() {
3528        // Test request body with referenced request body that can't be resolved (lines 1081-1087)
3529        let spec_json = json!({
3530            "openapi": "3.0.0",
3531            "info": {"title": "Test API", "version": "1.0.0"},
3532            "paths": {
3533                "/users": {
3534                    "post": {
3535                        "requestBody": {
3536                            "$ref": "#/components/requestBodies/NonExistentRequestBody"
3537                        },
3538                        "responses": {"200": {"description": "OK"}}
3539                    }
3540                }
3541            },
3542            "components": {}
3543        });
3544        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3545        let registry = OpenApiRouteRegistry::new(spec);
3546
3547        // Should fail validation when request body reference can't be resolved (lines 1081-1087)
3548        let body = json!({"name": "test"});
3549        let result = registry.validate_request_with_all(
3550            "/users",
3551            "POST",
3552            &Map::new(),
3553            &Map::new(),
3554            &Map::new(),
3555            &Map::new(),
3556            Some(&body),
3557        );
3558        assert!(result.is_err());
3559    }
3560
3561    #[test]
3562    fn test_validate_request_body_provided_when_not_expected() {
3563        // Test body provided when not expected (lines 1092-1094)
3564        let spec_json = json!({
3565            "openapi": "3.0.0",
3566            "info": {"title": "Test API", "version": "1.0.0"},
3567            "paths": {
3568                "/users": {
3569                    "get": {
3570                        "responses": {"200": {"description": "OK"}}
3571                    }
3572                }
3573            }
3574        });
3575        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3576        let registry = OpenApiRouteRegistry::new(spec);
3577
3578        // Should accept body even when not expected (lines 1092-1094)
3579        let body = json!({"extra": "data"});
3580        let result = registry.validate_request_with_all(
3581            "/users",
3582            "GET",
3583            &Map::new(),
3584            &Map::new(),
3585            &Map::new(),
3586            &Map::new(),
3587            Some(&body),
3588        );
3589        // Should not error - just logs debug message
3590        assert!(result.is_ok());
3591    }
3592
3593    #[test]
3594    fn test_get_operation() {
3595        // Test get_operation method (lines 1196-1205)
3596        let spec_json = json!({
3597            "openapi": "3.0.0",
3598            "info": {"title": "Test API", "version": "1.0.0"},
3599            "paths": {
3600                "/users": {
3601                    "get": {
3602                        "operationId": "getUsers",
3603                        "summary": "Get users",
3604                        "responses": {"200": {"description": "OK"}}
3605                    }
3606                }
3607            }
3608        });
3609        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3610        let registry = OpenApiRouteRegistry::new(spec);
3611
3612        // Should return operation details (lines 1196-1205)
3613        let operation = registry.get_operation("/users", "GET");
3614        assert!(operation.is_some());
3615        assert_eq!(operation.unwrap().method, "GET");
3616
3617        // Should return None for non-existent route
3618        assert!(registry.get_operation("/nonexistent", "GET").is_none());
3619    }
3620
3621    #[test]
3622    fn test_extract_path_parameters() {
3623        // Test extract_path_parameters method (lines 1208-1223)
3624        let spec_json = json!({
3625            "openapi": "3.0.0",
3626            "info": {"title": "Test API", "version": "1.0.0"},
3627            "paths": {
3628                "/users/{id}": {
3629                    "get": {
3630                        "parameters": [
3631                            {
3632                                "name": "id",
3633                                "in": "path",
3634                                "required": true,
3635                                "schema": {"type": "string"}
3636                            }
3637                        ],
3638                        "responses": {"200": {"description": "OK"}}
3639                    }
3640                }
3641            }
3642        });
3643        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3644        let registry = OpenApiRouteRegistry::new(spec);
3645
3646        // Should extract path parameters (lines 1208-1223)
3647        let params = registry.extract_path_parameters("/users/123", "GET");
3648        assert_eq!(params.get("id"), Some(&"123".to_string()));
3649
3650        // Should return empty map for non-matching path
3651        let empty_params = registry.extract_path_parameters("/users", "GET");
3652        assert!(empty_params.is_empty());
3653    }
3654
3655    #[test]
3656    fn test_extract_path_parameters_multiple_params() {
3657        // Test extract_path_parameters with multiple path parameters
3658        let spec_json = json!({
3659            "openapi": "3.0.0",
3660            "info": {"title": "Test API", "version": "1.0.0"},
3661            "paths": {
3662                "/users/{userId}/posts/{postId}": {
3663                    "get": {
3664                        "parameters": [
3665                            {
3666                                "name": "userId",
3667                                "in": "path",
3668                                "required": true,
3669                                "schema": {"type": "string"}
3670                            },
3671                            {
3672                                "name": "postId",
3673                                "in": "path",
3674                                "required": true,
3675                                "schema": {"type": "string"}
3676                            }
3677                        ],
3678                        "responses": {"200": {"description": "OK"}}
3679                    }
3680                }
3681            }
3682        });
3683        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3684        let registry = OpenApiRouteRegistry::new(spec);
3685
3686        // Should extract multiple path parameters
3687        let params = registry.extract_path_parameters("/users/123/posts/456", "GET");
3688        assert_eq!(params.get("userId"), Some(&"123".to_string()));
3689        assert_eq!(params.get("postId"), Some(&"456".to_string()));
3690    }
3691
3692    #[test]
3693    fn test_validate_request_route_not_found() {
3694        // Test validation when route not found (lines 1171-1173)
3695        let spec_json = json!({
3696            "openapi": "3.0.0",
3697            "info": {"title": "Test API", "version": "1.0.0"},
3698            "paths": {
3699                "/users": {
3700                    "get": {
3701                        "responses": {"200": {"description": "OK"}}
3702                    }
3703                }
3704            }
3705        });
3706        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3707        let registry = OpenApiRouteRegistry::new(spec);
3708
3709        // Should return error when route not found (lines 1171-1173)
3710        let result = registry.validate_request_with_all(
3711            "/nonexistent",
3712            "GET",
3713            &Map::new(),
3714            &Map::new(),
3715            &Map::new(),
3716            &Map::new(),
3717            None,
3718        );
3719        assert!(result.is_err());
3720        assert!(result.unwrap_err().to_string().contains("not found"));
3721    }
3722
3723    #[test]
3724    fn test_validate_request_with_path_parameters() {
3725        // Test path parameter validation (lines 1101-1110)
3726        let spec_json = json!({
3727            "openapi": "3.0.0",
3728            "info": {"title": "Test API", "version": "1.0.0"},
3729            "paths": {
3730                "/users/{id}": {
3731                    "get": {
3732                        "parameters": [
3733                            {
3734                                "name": "id",
3735                                "in": "path",
3736                                "required": true,
3737                                "schema": {"type": "string", "minLength": 1}
3738                            }
3739                        ],
3740                        "responses": {"200": {"description": "OK"}}
3741                    }
3742                }
3743            }
3744        });
3745        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3746        let registry = OpenApiRouteRegistry::new(spec);
3747
3748        // Should pass validation with valid path parameter
3749        let mut path_params = Map::new();
3750        path_params.insert("id".to_string(), json!("123"));
3751        let result = registry.validate_request_with_all(
3752            "/users/{id}",
3753            "GET",
3754            &path_params,
3755            &Map::new(),
3756            &Map::new(),
3757            &Map::new(),
3758            None,
3759        );
3760        assert!(result.is_ok());
3761    }
3762
3763    #[test]
3764    fn test_validate_request_with_query_parameters() {
3765        // Test query parameter validation (lines 1111-1134)
3766        let spec_json = json!({
3767            "openapi": "3.0.0",
3768            "info": {"title": "Test API", "version": "1.0.0"},
3769            "paths": {
3770                "/users": {
3771                    "get": {
3772                        "parameters": [
3773                            {
3774                                "name": "page",
3775                                "in": "query",
3776                                "required": true,
3777                                "schema": {"type": "integer", "minimum": 1}
3778                            }
3779                        ],
3780                        "responses": {"200": {"description": "OK"}}
3781                    }
3782                }
3783            }
3784        });
3785        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3786        let registry = OpenApiRouteRegistry::new(spec);
3787
3788        // Should pass validation with valid query parameter
3789        let mut query_params = Map::new();
3790        query_params.insert("page".to_string(), json!(1));
3791        let result = registry.validate_request_with_all(
3792            "/users",
3793            "GET",
3794            &Map::new(),
3795            &query_params,
3796            &Map::new(),
3797            &Map::new(),
3798            None,
3799        );
3800        assert!(result.is_ok());
3801    }
3802
3803    #[test]
3804    fn test_validate_request_with_header_parameters() {
3805        // Test header parameter validation (lines 1135-1144)
3806        let spec_json = json!({
3807            "openapi": "3.0.0",
3808            "info": {"title": "Test API", "version": "1.0.0"},
3809            "paths": {
3810                "/users": {
3811                    "get": {
3812                        "parameters": [
3813                            {
3814                                "name": "X-API-Key",
3815                                "in": "header",
3816                                "required": true,
3817                                "schema": {"type": "string"}
3818                            }
3819                        ],
3820                        "responses": {"200": {"description": "OK"}}
3821                    }
3822                }
3823            }
3824        });
3825        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3826        let registry = OpenApiRouteRegistry::new(spec);
3827
3828        // Should pass validation with valid header parameter
3829        let mut header_params = Map::new();
3830        header_params.insert("X-API-Key".to_string(), json!("secret-key"));
3831        let result = registry.validate_request_with_all(
3832            "/users",
3833            "GET",
3834            &Map::new(),
3835            &Map::new(),
3836            &header_params,
3837            &Map::new(),
3838            None,
3839        );
3840        assert!(result.is_ok());
3841    }
3842
3843    #[test]
3844    fn test_validate_request_with_cookie_parameters() {
3845        // Test cookie parameter validation (lines 1145-1154)
3846        let spec_json = json!({
3847            "openapi": "3.0.0",
3848            "info": {"title": "Test API", "version": "1.0.0"},
3849            "paths": {
3850                "/users": {
3851                    "get": {
3852                        "parameters": [
3853                            {
3854                                "name": "sessionId",
3855                                "in": "cookie",
3856                                "required": true,
3857                                "schema": {"type": "string"}
3858                            }
3859                        ],
3860                        "responses": {"200": {"description": "OK"}}
3861                    }
3862                }
3863            }
3864        });
3865        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3866        let registry = OpenApiRouteRegistry::new(spec);
3867
3868        // Should pass validation with valid cookie parameter
3869        let mut cookie_params = Map::new();
3870        cookie_params.insert("sessionId".to_string(), json!("abc123"));
3871        let result = registry.validate_request_with_all(
3872            "/users",
3873            "GET",
3874            &Map::new(),
3875            &Map::new(),
3876            &Map::new(),
3877            &cookie_params,
3878            None,
3879        );
3880        assert!(result.is_ok());
3881    }
3882
3883    #[test]
3884    fn test_validate_request_no_errors_early_return() {
3885        // Test early return when no errors (lines 1158-1160)
3886        let spec_json = json!({
3887            "openapi": "3.0.0",
3888            "info": {"title": "Test API", "version": "1.0.0"},
3889            "paths": {
3890                "/users": {
3891                    "get": {
3892                        "responses": {"200": {"description": "OK"}}
3893                    }
3894                }
3895            }
3896        });
3897        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3898        let registry = OpenApiRouteRegistry::new(spec);
3899
3900        // Should return early when no errors (lines 1158-1160)
3901        let result = registry.validate_request_with_all(
3902            "/users",
3903            "GET",
3904            &Map::new(),
3905            &Map::new(),
3906            &Map::new(),
3907            &Map::new(),
3908            None,
3909        );
3910        assert!(result.is_ok());
3911    }
3912
3913    #[test]
3914    fn test_validate_request_query_parameter_different_styles() {
3915        // Test query parameter validation with different styles (lines 1118-1123)
3916        let spec_json = json!({
3917            "openapi": "3.0.0",
3918            "info": {"title": "Test API", "version": "1.0.0"},
3919            "paths": {
3920                "/users": {
3921                    "get": {
3922                        "parameters": [
3923                            {
3924                                "name": "tags",
3925                                "in": "query",
3926                                "style": "pipeDelimited",
3927                                "schema": {
3928                                    "type": "array",
3929                                    "items": {"type": "string"}
3930                                }
3931                            }
3932                        ],
3933                        "responses": {"200": {"description": "OK"}}
3934                    }
3935                }
3936            }
3937        });
3938        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3939        let registry = OpenApiRouteRegistry::new(spec);
3940
3941        // Should handle pipeDelimited style (lines 1118-1123)
3942        let mut query_params = Map::new();
3943        query_params.insert("tags".to_string(), json!(["tag1", "tag2"]));
3944        let result = registry.validate_request_with_all(
3945            "/users",
3946            "GET",
3947            &Map::new(),
3948            &query_params,
3949            &Map::new(),
3950            &Map::new(),
3951            None,
3952        );
3953        // Should not error on style handling
3954        assert!(result.is_ok() || result.is_err()); // Either is fine, just testing the path
3955    }
3956}