Skip to main content

mockforge_core/
openapi_routes.rs

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