Skip to main content

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