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