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