Skip to main content

mockforge_core/
openapi_routes.rs

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