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                    // Check for status code override header
1787                    let status_override = headers
1788                        .get("X-Mockforge-Response-Status")
1789                        .and_then(|v| v.to_str().ok())
1790                        .and_then(|s| s.parse::<u16>().ok());
1791
1792                    // Check for scenario header
1793                    let scenario = headers
1794                        .get("X-Mockforge-Scenario")
1795                        .and_then(|v| v.to_str().ok())
1796                        .map(|s| s.to_string())
1797                        .or_else(|| std::env::var("MOCKFORGE_HTTP_SCENARIO").ok());
1798
1799                    // Fallback to standard response generation
1800                    let (status, response) = route
1801                        .mock_response_with_status_and_scenario_and_override(
1802                            scenario.as_deref(),
1803                            status_override,
1804                        );
1805                    (
1806                        axum::http::StatusCode::from_u16(status)
1807                            .unwrap_or(axum::http::StatusCode::OK),
1808                        Json(response),
1809                    )
1810                }
1811            };
1812
1813            match route.method.as_str() {
1814                "GET" => {
1815                    router = router.route(&route.path, get(handler));
1816                }
1817                "POST" => {
1818                    router = router.route(&route.path, post(handler));
1819                }
1820                "PUT" => {
1821                    router = router.route(&route.path, put(handler));
1822                }
1823                "DELETE" => {
1824                    router = router.route(&route.path, delete(handler));
1825                }
1826                "PATCH" => {
1827                    router = router.route(&route.path, patch(handler));
1828                }
1829                _ => tracing::warn!("Unsupported HTTP method for MockAI: {}", route.method),
1830            }
1831        }
1832
1833        router
1834    }
1835}
1836
1837// Note: templating helpers are now in core::templating (shared across modules)
1838
1839/// Extract multipart form data from request body bytes
1840/// Returns (form_fields, file_paths) where file_paths maps field names to stored file paths
1841async fn extract_multipart_from_bytes(
1842    body: &axum::body::Bytes,
1843    headers: &HeaderMap,
1844) -> Result<(HashMap<String, Value>, HashMap<String, String>)> {
1845    // Get boundary from Content-Type header
1846    let boundary = headers
1847        .get(axum::http::header::CONTENT_TYPE)
1848        .and_then(|v| v.to_str().ok())
1849        .and_then(|ct| {
1850            ct.split(';').find_map(|part| {
1851                let part = part.trim();
1852                if part.starts_with("boundary=") {
1853                    Some(part.strip_prefix("boundary=").unwrap_or("").trim_matches('"'))
1854                } else {
1855                    None
1856                }
1857            })
1858        })
1859        .ok_or_else(|| Error::generic("Missing boundary in Content-Type header"))?;
1860
1861    let mut fields = HashMap::new();
1862    let mut files = HashMap::new();
1863
1864    // Parse multipart data using bytes directly (not string conversion)
1865    // Multipart format: --boundary\r\n...\r\n--boundary\r\n...\r\n--boundary--\r\n
1866    let boundary_prefix = format!("--{}", boundary).into_bytes();
1867    let boundary_line = format!("\r\n--{}\r\n", boundary).into_bytes();
1868    let end_boundary = format!("\r\n--{}--\r\n", boundary).into_bytes();
1869
1870    // Find all boundary positions
1871    let mut pos = 0;
1872    let mut parts = Vec::new();
1873
1874    // Skip initial boundary if present
1875    if body.starts_with(&boundary_prefix) {
1876        if let Some(first_crlf) = body.iter().position(|&b| b == b'\r') {
1877            pos = first_crlf + 2; // Skip --boundary\r\n
1878        }
1879    }
1880
1881    // Find all middle boundaries
1882    while let Some(boundary_pos) = body[pos..]
1883        .windows(boundary_line.len())
1884        .position(|window| window == boundary_line.as_slice())
1885    {
1886        let actual_pos = pos + boundary_pos;
1887        if actual_pos > pos {
1888            parts.push((pos, actual_pos));
1889        }
1890        pos = actual_pos + boundary_line.len();
1891    }
1892
1893    // Find final boundary
1894    if let Some(end_pos) = body[pos..]
1895        .windows(end_boundary.len())
1896        .position(|window| window == end_boundary.as_slice())
1897    {
1898        let actual_end = pos + end_pos;
1899        if actual_end > pos {
1900            parts.push((pos, actual_end));
1901        }
1902    } else if pos < body.len() {
1903        // No final boundary found, treat rest as last part
1904        parts.push((pos, body.len()));
1905    }
1906
1907    // Process each part
1908    for (start, end) in parts {
1909        let part_data = &body[start..end];
1910
1911        // Find header/body separator (CRLF CRLF)
1912        let separator = b"\r\n\r\n";
1913        if let Some(sep_pos) =
1914            part_data.windows(separator.len()).position(|window| window == separator)
1915        {
1916            let header_bytes = &part_data[..sep_pos];
1917            let body_start = sep_pos + separator.len();
1918            let body_data = &part_data[body_start..];
1919
1920            // Parse headers (assuming UTF-8)
1921            let header_str = String::from_utf8_lossy(header_bytes);
1922            let mut field_name = None;
1923            let mut filename = None;
1924
1925            for header_line in header_str.lines() {
1926                if header_line.starts_with("Content-Disposition:") {
1927                    // Extract field name
1928                    if let Some(name_start) = header_line.find("name=\"") {
1929                        let name_start = name_start + 6;
1930                        if let Some(name_end) = header_line[name_start..].find('"') {
1931                            field_name =
1932                                Some(header_line[name_start..name_start + name_end].to_string());
1933                        }
1934                    }
1935
1936                    // Extract filename if present
1937                    if let Some(file_start) = header_line.find("filename=\"") {
1938                        let file_start = file_start + 10;
1939                        if let Some(file_end) = header_line[file_start..].find('"') {
1940                            filename =
1941                                Some(header_line[file_start..file_start + file_end].to_string());
1942                        }
1943                    }
1944                }
1945            }
1946
1947            if let Some(name) = field_name {
1948                if let Some(file) = filename {
1949                    // This is a file upload - store to temp directory
1950                    let temp_dir = std::env::temp_dir().join("mockforge-uploads");
1951                    std::fs::create_dir_all(&temp_dir).map_err(|e| {
1952                        Error::generic(format!("Failed to create temp directory: {}", e))
1953                    })?;
1954
1955                    let file_path = temp_dir.join(format!("{}_{}", uuid::Uuid::new_v4(), file));
1956                    std::fs::write(&file_path, body_data)
1957                        .map_err(|e| Error::generic(format!("Failed to write file: {}", e)))?;
1958
1959                    let file_path_str = file_path.to_string_lossy().to_string();
1960                    files.insert(name.clone(), file_path_str.clone());
1961                    fields.insert(name, Value::String(file_path_str));
1962                } else {
1963                    // This is a regular form field - try to parse as UTF-8 string
1964                    // Trim trailing CRLF
1965                    let body_str = body_data
1966                        .strip_suffix(b"\r\n")
1967                        .or_else(|| body_data.strip_suffix(b"\n"))
1968                        .unwrap_or(body_data);
1969
1970                    if let Ok(field_value) = String::from_utf8(body_str.to_vec()) {
1971                        fields.insert(name, Value::String(field_value.trim().to_string()));
1972                    } else {
1973                        // Non-UTF-8 field value - store as base64 encoded string
1974                        use base64::{engine::general_purpose, Engine as _};
1975                        fields.insert(
1976                            name,
1977                            Value::String(general_purpose::STANDARD.encode(body_str)),
1978                        );
1979                    }
1980                }
1981            }
1982        }
1983    }
1984
1985    Ok((fields, files))
1986}
1987
1988static LAST_ERRORS: Lazy<Mutex<VecDeque<Value>>> =
1989    Lazy::new(|| Mutex::new(VecDeque::with_capacity(20)));
1990
1991/// Record last validation error for Admin UI inspection
1992pub fn record_validation_error(v: &Value) {
1993    if let Ok(mut q) = LAST_ERRORS.lock() {
1994        if q.len() >= 20 {
1995            q.pop_front();
1996        }
1997        q.push_back(v.clone());
1998    }
1999    // If mutex is poisoned, we silently fail - validation errors are informational only
2000}
2001
2002/// Get most recent validation error
2003pub fn get_last_validation_error() -> Option<Value> {
2004    LAST_ERRORS.lock().ok()?.back().cloned()
2005}
2006
2007/// Get recent validation errors (most recent last)
2008pub fn get_validation_errors() -> Vec<Value> {
2009    LAST_ERRORS.lock().map(|q| q.iter().cloned().collect()).unwrap_or_default()
2010}
2011
2012/// Coerce a parameter `value` into the expected JSON type per `schema` where reasonable.
2013/// Applies only to param contexts (not request bodies). Conservative conversions:
2014/// - integer/number: parse from string; arrays: split comma-separated strings and coerce items
2015/// - boolean: parse true/false (case-insensitive) from string
2016fn coerce_value_for_schema(value: &Value, schema: &openapiv3::Schema) -> Value {
2017    // Basic coercion: try to parse strings as appropriate types
2018    match value {
2019        Value::String(s) => {
2020            // Check if schema expects an array and we have a comma-separated string
2021            if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(array_type)) =
2022                &schema.schema_kind
2023            {
2024                if s.contains(',') {
2025                    // Split comma-separated string into array
2026                    let parts: Vec<&str> = s.split(',').map(|s| s.trim()).collect();
2027                    let mut array_values = Vec::new();
2028
2029                    for part in parts {
2030                        // Coerce each part based on array item type
2031                        if let Some(items_schema) = &array_type.items {
2032                            if let Some(items_schema_obj) = items_schema.as_item() {
2033                                let part_value = Value::String(part.to_string());
2034                                let coerced_part =
2035                                    coerce_value_for_schema(&part_value, items_schema_obj);
2036                                array_values.push(coerced_part);
2037                            } else {
2038                                // If items schema is a reference or not available, keep as string
2039                                array_values.push(Value::String(part.to_string()));
2040                            }
2041                        } else {
2042                            // No items schema defined, keep as string
2043                            array_values.push(Value::String(part.to_string()));
2044                        }
2045                    }
2046                    return Value::Array(array_values);
2047                }
2048            }
2049
2050            // Only coerce if the schema expects a different type
2051            match &schema.schema_kind {
2052                openapiv3::SchemaKind::Type(openapiv3::Type::String(_)) => {
2053                    // Schema expects string, keep as string
2054                    value.clone()
2055                }
2056                openapiv3::SchemaKind::Type(openapiv3::Type::Number(_)) => {
2057                    // Schema expects number, try to parse
2058                    if let Ok(n) = s.parse::<f64>() {
2059                        if let Some(num) = serde_json::Number::from_f64(n) {
2060                            return Value::Number(num);
2061                        }
2062                    }
2063                    value.clone()
2064                }
2065                openapiv3::SchemaKind::Type(openapiv3::Type::Integer(_)) => {
2066                    // Schema expects integer, try to parse
2067                    if let Ok(n) = s.parse::<i64>() {
2068                        if let Some(num) = serde_json::Number::from_f64(n as f64) {
2069                            return Value::Number(num);
2070                        }
2071                    }
2072                    value.clone()
2073                }
2074                openapiv3::SchemaKind::Type(openapiv3::Type::Boolean(_)) => {
2075                    // Schema expects boolean, try to parse
2076                    match s.to_lowercase().as_str() {
2077                        "true" | "1" | "yes" | "on" => Value::Bool(true),
2078                        "false" | "0" | "no" | "off" => Value::Bool(false),
2079                        _ => value.clone(),
2080                    }
2081                }
2082                _ => {
2083                    // Unknown schema type, keep as string
2084                    value.clone()
2085                }
2086            }
2087        }
2088        _ => value.clone(),
2089    }
2090}
2091
2092/// Apply style-aware coercion for query params
2093fn coerce_by_style(value: &Value, schema: &openapiv3::Schema, style: Option<&str>) -> Value {
2094    // Style-aware coercion for query parameters
2095    match value {
2096        Value::String(s) => {
2097            // Check if schema expects an array and we have a delimited string
2098            if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(array_type)) =
2099                &schema.schema_kind
2100            {
2101                let delimiter = match style {
2102                    Some("spaceDelimited") => " ",
2103                    Some("pipeDelimited") => "|",
2104                    Some("form") | None => ",", // Default to form style (comma-separated)
2105                    _ => ",",                   // Fallback to comma
2106                };
2107
2108                if s.contains(delimiter) {
2109                    // Split delimited string into array
2110                    let parts: Vec<&str> = s.split(delimiter).map(|s| s.trim()).collect();
2111                    let mut array_values = Vec::new();
2112
2113                    for part in parts {
2114                        // Coerce each part based on array item type
2115                        if let Some(items_schema) = &array_type.items {
2116                            if let Some(items_schema_obj) = items_schema.as_item() {
2117                                let part_value = Value::String(part.to_string());
2118                                let coerced_part =
2119                                    coerce_by_style(&part_value, items_schema_obj, style);
2120                                array_values.push(coerced_part);
2121                            } else {
2122                                // If items schema is a reference or not available, keep as string
2123                                array_values.push(Value::String(part.to_string()));
2124                            }
2125                        } else {
2126                            // No items schema defined, keep as string
2127                            array_values.push(Value::String(part.to_string()));
2128                        }
2129                    }
2130                    return Value::Array(array_values);
2131                }
2132            }
2133
2134            // Try to parse as number first
2135            if let Ok(n) = s.parse::<f64>() {
2136                if let Some(num) = serde_json::Number::from_f64(n) {
2137                    return Value::Number(num);
2138                }
2139            }
2140            // Try to parse as boolean
2141            match s.to_lowercase().as_str() {
2142                "true" | "1" | "yes" | "on" => return Value::Bool(true),
2143                "false" | "0" | "no" | "off" => return Value::Bool(false),
2144                _ => {}
2145            }
2146            // Keep as string
2147            value.clone()
2148        }
2149        _ => value.clone(),
2150    }
2151}
2152
2153/// Build a deepObject from query params like `name[prop]=val`
2154fn build_deep_object(name: &str, params: &Map<String, Value>) -> Option<Value> {
2155    let prefix = format!("{}[", name);
2156    let mut obj = Map::new();
2157    for (k, v) in params.iter() {
2158        if let Some(rest) = k.strip_prefix(&prefix) {
2159            if let Some(key) = rest.strip_suffix(']') {
2160                obj.insert(key.to_string(), v.clone());
2161            }
2162        }
2163    }
2164    if obj.is_empty() {
2165        None
2166    } else {
2167        Some(Value::Object(obj))
2168    }
2169}
2170
2171// Import the enhanced schema diff functionality
2172// use crate::schema_diff::{validation_diff, to_enhanced_422_json, ValidationError}; // Not currently used
2173
2174/// Generate an enhanced 422 response with detailed schema validation errors
2175/// This function provides comprehensive error information using the new schema diff utility
2176#[allow(clippy::too_many_arguments)]
2177fn generate_enhanced_422_response(
2178    validator: &OpenApiRouteRegistry,
2179    path_template: &str,
2180    method: &str,
2181    body: Option<&Value>,
2182    path_params: &Map<String, Value>,
2183    query_params: &Map<String, Value>,
2184    header_params: &Map<String, Value>,
2185    cookie_params: &Map<String, Value>,
2186) -> Value {
2187    let mut field_errors = Vec::new();
2188
2189    // Extract schema validation details if we have a route
2190    if let Some(route) = validator.get_route(path_template, method) {
2191        // Validate request body with detailed error collection
2192        if let Some(schema) = &route.operation.request_body {
2193            if let Some(value) = body {
2194                if let Some(content) =
2195                    schema.as_item().and_then(|rb| rb.content.get("application/json"))
2196                {
2197                    if let Some(_schema_ref) = &content.schema {
2198                        // Basic JSON validation - schema validation deferred
2199                        if serde_json::from_value::<Value>(value.clone()).is_err() {
2200                            field_errors.push(json!({
2201                                "path": "body",
2202                                "message": "invalid JSON"
2203                            }));
2204                        }
2205                    }
2206                }
2207            } else {
2208                field_errors.push(json!({
2209                    "path": "body",
2210                    "expected": "object",
2211                    "found": "missing",
2212                    "message": "Request body is required but not provided"
2213                }));
2214            }
2215        }
2216
2217        // Validate parameters with detailed error collection
2218        for param_ref in &route.operation.parameters {
2219            if let Some(param) = param_ref.as_item() {
2220                match param {
2221                    openapiv3::Parameter::Path { parameter_data, .. } => {
2222                        validate_parameter_detailed(
2223                            parameter_data,
2224                            path_params,
2225                            "path",
2226                            "path parameter",
2227                            &mut field_errors,
2228                        );
2229                    }
2230                    openapiv3::Parameter::Query { parameter_data, .. } => {
2231                        let deep_value = if Some("form") == Some("deepObject") {
2232                            build_deep_object(&parameter_data.name, query_params)
2233                        } else {
2234                            None
2235                        };
2236                        validate_parameter_detailed_with_deep(
2237                            parameter_data,
2238                            query_params,
2239                            "query",
2240                            "query parameter",
2241                            deep_value,
2242                            &mut field_errors,
2243                        );
2244                    }
2245                    openapiv3::Parameter::Header { parameter_data, .. } => {
2246                        validate_parameter_detailed(
2247                            parameter_data,
2248                            header_params,
2249                            "header",
2250                            "header parameter",
2251                            &mut field_errors,
2252                        );
2253                    }
2254                    openapiv3::Parameter::Cookie { parameter_data, .. } => {
2255                        validate_parameter_detailed(
2256                            parameter_data,
2257                            cookie_params,
2258                            "cookie",
2259                            "cookie parameter",
2260                            &mut field_errors,
2261                        );
2262                    }
2263                }
2264            }
2265        }
2266    }
2267
2268    // Return the detailed 422 error format
2269    json!({
2270        "error": "Schema validation failed",
2271        "details": field_errors,
2272        "method": method,
2273        "path": path_template,
2274        "timestamp": Utc::now().to_rfc3339(),
2275        "validation_type": "openapi_schema"
2276    })
2277}
2278
2279/// Helper function to validate a parameter
2280fn validate_parameter(
2281    parameter_data: &openapiv3::ParameterData,
2282    params_map: &Map<String, Value>,
2283    prefix: &str,
2284    aggregate: bool,
2285    errors: &mut Vec<String>,
2286    details: &mut Vec<Value>,
2287) {
2288    match params_map.get(&parameter_data.name) {
2289        Some(v) => {
2290            if let ParameterSchemaOrContent::Schema(s) = &parameter_data.format {
2291                if let Some(schema) = s.as_item() {
2292                    let coerced = coerce_value_for_schema(v, schema);
2293                    // Validate the coerced value against the schema
2294                    if let Err(validation_error) =
2295                        OpenApiSchema::new(schema.clone()).validate(&coerced)
2296                    {
2297                        let error_msg = validation_error.to_string();
2298                        errors.push(format!(
2299                            "{} parameter '{}' validation failed: {}",
2300                            prefix, parameter_data.name, error_msg
2301                        ));
2302                        if aggregate {
2303                            details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"schema_validation","message":error_msg}));
2304                        }
2305                    }
2306                }
2307            }
2308        }
2309        None => {
2310            if parameter_data.required {
2311                errors.push(format!(
2312                    "missing required {} parameter '{}'",
2313                    prefix, parameter_data.name
2314                ));
2315                details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"required","message":"Missing required parameter"}));
2316            }
2317        }
2318    }
2319}
2320
2321/// Helper function to validate a parameter with deep object support
2322#[allow(clippy::too_many_arguments)]
2323fn validate_parameter_with_deep_object(
2324    parameter_data: &openapiv3::ParameterData,
2325    params_map: &Map<String, Value>,
2326    prefix: &str,
2327    deep_value: Option<Value>,
2328    style: Option<&str>,
2329    aggregate: bool,
2330    errors: &mut Vec<String>,
2331    details: &mut Vec<Value>,
2332) {
2333    match deep_value.as_ref().or_else(|| params_map.get(&parameter_data.name)) {
2334        Some(v) => {
2335            if let ParameterSchemaOrContent::Schema(s) = &parameter_data.format {
2336                if let Some(schema) = s.as_item() {
2337                    let coerced = coerce_by_style(v, schema, style); // Use the actual style
2338                                                                     // Validate the coerced value against the schema
2339                    if let Err(validation_error) =
2340                        OpenApiSchema::new(schema.clone()).validate(&coerced)
2341                    {
2342                        let error_msg = validation_error.to_string();
2343                        errors.push(format!(
2344                            "{} parameter '{}' validation failed: {}",
2345                            prefix, parameter_data.name, error_msg
2346                        ));
2347                        if aggregate {
2348                            details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"schema_validation","message":error_msg}));
2349                        }
2350                    }
2351                }
2352            }
2353        }
2354        None => {
2355            if parameter_data.required {
2356                errors.push(format!(
2357                    "missing required {} parameter '{}'",
2358                    prefix, parameter_data.name
2359                ));
2360                details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"required","message":"Missing required parameter"}));
2361            }
2362        }
2363    }
2364}
2365
2366/// Helper function to validate a parameter with detailed error collection
2367fn validate_parameter_detailed(
2368    parameter_data: &openapiv3::ParameterData,
2369    params_map: &Map<String, Value>,
2370    location: &str,
2371    value_type: &str,
2372    field_errors: &mut Vec<Value>,
2373) {
2374    match params_map.get(&parameter_data.name) {
2375        Some(value) => {
2376            if let ParameterSchemaOrContent::Schema(schema) = &parameter_data.format {
2377                // Collect detailed validation errors for this parameter
2378                let details: Vec<Value> = Vec::new();
2379                let param_path = format!("{}.{}", location, parameter_data.name);
2380
2381                // Apply coercion before validation
2382                if let Some(schema_ref) = schema.as_item() {
2383                    let coerced_value = coerce_value_for_schema(value, schema_ref);
2384                    // Validate the coerced value against the schema
2385                    if let Err(validation_error) =
2386                        OpenApiSchema::new(schema_ref.clone()).validate(&coerced_value)
2387                    {
2388                        field_errors.push(json!({
2389                            "path": param_path,
2390                            "expected": "valid according to schema",
2391                            "found": coerced_value,
2392                            "message": validation_error.to_string()
2393                        }));
2394                    }
2395                }
2396
2397                for detail in details {
2398                    field_errors.push(json!({
2399                        "path": detail["path"],
2400                        "expected": detail["expected_type"],
2401                        "found": detail["value"],
2402                        "message": detail["message"]
2403                    }));
2404                }
2405            }
2406        }
2407        None => {
2408            if parameter_data.required {
2409                field_errors.push(json!({
2410                    "path": format!("{}.{}", location, parameter_data.name),
2411                    "expected": "value",
2412                    "found": "missing",
2413                    "message": format!("Missing required {} '{}'", value_type, parameter_data.name)
2414                }));
2415            }
2416        }
2417    }
2418}
2419
2420/// Helper function to validate a parameter with deep object support and detailed errors
2421fn validate_parameter_detailed_with_deep(
2422    parameter_data: &openapiv3::ParameterData,
2423    params_map: &Map<String, Value>,
2424    location: &str,
2425    value_type: &str,
2426    deep_value: Option<Value>,
2427    field_errors: &mut Vec<Value>,
2428) {
2429    match deep_value.as_ref().or_else(|| params_map.get(&parameter_data.name)) {
2430        Some(value) => {
2431            if let ParameterSchemaOrContent::Schema(schema) = &parameter_data.format {
2432                // Collect detailed validation errors for this parameter
2433                let details: Vec<Value> = Vec::new();
2434                let param_path = format!("{}.{}", location, parameter_data.name);
2435
2436                // Apply coercion before validation
2437                if let Some(schema_ref) = schema.as_item() {
2438                    let coerced_value = coerce_by_style(value, schema_ref, Some("form")); // Default to form style for now
2439                                                                                          // Validate the coerced value against the schema
2440                    if let Err(validation_error) =
2441                        OpenApiSchema::new(schema_ref.clone()).validate(&coerced_value)
2442                    {
2443                        field_errors.push(json!({
2444                            "path": param_path,
2445                            "expected": "valid according to schema",
2446                            "found": coerced_value,
2447                            "message": validation_error.to_string()
2448                        }));
2449                    }
2450                }
2451
2452                for detail in details {
2453                    field_errors.push(json!({
2454                        "path": detail["path"],
2455                        "expected": detail["expected_type"],
2456                        "found": detail["value"],
2457                        "message": detail["message"]
2458                    }));
2459                }
2460            }
2461        }
2462        None => {
2463            if parameter_data.required {
2464                field_errors.push(json!({
2465                    "path": format!("{}.{}", location, parameter_data.name),
2466                    "expected": "value",
2467                    "found": "missing",
2468                    "message": format!("Missing required {} '{}'", value_type, parameter_data.name)
2469                }));
2470            }
2471        }
2472    }
2473}
2474
2475/// Helper function to create an OpenAPI route registry from a file
2476pub async fn create_registry_from_file<P: AsRef<std::path::Path>>(
2477    path: P,
2478) -> Result<OpenApiRouteRegistry> {
2479    let spec = OpenApiSpec::from_file(path).await?;
2480    spec.validate()?;
2481    Ok(OpenApiRouteRegistry::new(spec))
2482}
2483
2484/// Helper function to create an OpenAPI route registry from JSON
2485pub fn create_registry_from_json(json: Value) -> Result<OpenApiRouteRegistry> {
2486    let spec = OpenApiSpec::from_json(json)?;
2487    spec.validate()?;
2488    Ok(OpenApiRouteRegistry::new(spec))
2489}
2490
2491#[cfg(test)]
2492mod tests {
2493    use super::*;
2494    use serde_json::json;
2495    use tempfile::TempDir;
2496
2497    #[tokio::test]
2498    async fn test_registry_creation() {
2499        let spec_json = json!({
2500            "openapi": "3.0.0",
2501            "info": {
2502                "title": "Test API",
2503                "version": "1.0.0"
2504            },
2505            "paths": {
2506                "/users": {
2507                    "get": {
2508                        "summary": "Get users",
2509                        "responses": {
2510                            "200": {
2511                                "description": "Success",
2512                                "content": {
2513                                    "application/json": {
2514                                        "schema": {
2515                                            "type": "array",
2516                                            "items": {
2517                                                "type": "object",
2518                                                "properties": {
2519                                                    "id": {"type": "integer"},
2520                                                    "name": {"type": "string"}
2521                                                }
2522                                            }
2523                                        }
2524                                    }
2525                                }
2526                            }
2527                        }
2528                    },
2529                    "post": {
2530                        "summary": "Create user",
2531                        "requestBody": {
2532                            "content": {
2533                                "application/json": {
2534                                    "schema": {
2535                                        "type": "object",
2536                                        "properties": {
2537                                            "name": {"type": "string"}
2538                                        },
2539                                        "required": ["name"]
2540                                    }
2541                                }
2542                            }
2543                        },
2544                        "responses": {
2545                            "201": {
2546                                "description": "Created",
2547                                "content": {
2548                                    "application/json": {
2549                                        "schema": {
2550                                            "type": "object",
2551                                            "properties": {
2552                                                "id": {"type": "integer"},
2553                                                "name": {"type": "string"}
2554                                            }
2555                                        }
2556                                    }
2557                                }
2558                            }
2559                        }
2560                    }
2561                },
2562                "/users/{id}": {
2563                    "get": {
2564                        "summary": "Get user by ID",
2565                        "parameters": [
2566                            {
2567                                "name": "id",
2568                                "in": "path",
2569                                "required": true,
2570                                "schema": {"type": "integer"}
2571                            }
2572                        ],
2573                        "responses": {
2574                            "200": {
2575                                "description": "Success",
2576                                "content": {
2577                                    "application/json": {
2578                                        "schema": {
2579                                            "type": "object",
2580                                            "properties": {
2581                                                "id": {"type": "integer"},
2582                                                "name": {"type": "string"}
2583                                            }
2584                                        }
2585                                    }
2586                                }
2587                            }
2588                        }
2589                    }
2590                }
2591            }
2592        });
2593
2594        let registry = create_registry_from_json(spec_json).unwrap();
2595
2596        // Test basic properties
2597        assert_eq!(registry.paths().len(), 2);
2598        assert!(registry.paths().contains(&"/users".to_string()));
2599        assert!(registry.paths().contains(&"/users/{id}".to_string()));
2600
2601        assert_eq!(registry.methods().len(), 2);
2602        assert!(registry.methods().contains(&"GET".to_string()));
2603        assert!(registry.methods().contains(&"POST".to_string()));
2604
2605        // Test route lookup
2606        let get_users_route = registry.get_route("/users", "GET").unwrap();
2607        assert_eq!(get_users_route.method, "GET");
2608        assert_eq!(get_users_route.path, "/users");
2609
2610        let post_users_route = registry.get_route("/users", "POST").unwrap();
2611        assert_eq!(post_users_route.method, "POST");
2612        assert!(post_users_route.operation.request_body.is_some());
2613
2614        // Test path parameter conversion
2615        let user_by_id_route = registry.get_route("/users/{id}", "GET").unwrap();
2616        assert_eq!(user_by_id_route.axum_path(), "/users/{id}");
2617    }
2618
2619    #[tokio::test]
2620    async fn test_validate_request_with_params_and_formats() {
2621        let spec_json = json!({
2622            "openapi": "3.0.0",
2623            "info": { "title": "Test API", "version": "1.0.0" },
2624            "paths": {
2625                "/users/{id}": {
2626                    "post": {
2627                        "parameters": [
2628                            { "name": "id", "in": "path", "required": true, "schema": {"type": "string"} },
2629                            { "name": "q",  "in": "query", "required": false, "schema": {"type": "integer"} }
2630                        ],
2631                        "requestBody": {
2632                            "content": {
2633                                "application/json": {
2634                                    "schema": {
2635                                        "type": "object",
2636                                        "required": ["email", "website"],
2637                                        "properties": {
2638                                            "email":   {"type": "string", "format": "email"},
2639                                            "website": {"type": "string", "format": "uri"}
2640                                        }
2641                                    }
2642                                }
2643                            }
2644                        },
2645                        "responses": {"200": {"description": "ok"}}
2646                    }
2647                }
2648            }
2649        });
2650
2651        let registry = create_registry_from_json(spec_json).unwrap();
2652        let mut path_params = Map::new();
2653        path_params.insert("id".to_string(), json!("abc"));
2654        let mut query_params = Map::new();
2655        query_params.insert("q".to_string(), json!(123));
2656
2657        // valid body
2658        let body = json!({"email":"a@b.co","website":"https://example.com"});
2659        assert!(registry
2660            .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&body))
2661            .is_ok());
2662
2663        // invalid email
2664        let bad_email = json!({"email":"not-an-email","website":"https://example.com"});
2665        assert!(registry
2666            .validate_request_with(
2667                "/users/{id}",
2668                "POST",
2669                &path_params,
2670                &query_params,
2671                Some(&bad_email)
2672            )
2673            .is_err());
2674
2675        // missing required path param
2676        let empty_path_params = Map::new();
2677        assert!(registry
2678            .validate_request_with(
2679                "/users/{id}",
2680                "POST",
2681                &empty_path_params,
2682                &query_params,
2683                Some(&body)
2684            )
2685            .is_err());
2686    }
2687
2688    #[tokio::test]
2689    async fn test_ref_resolution_for_params_and_body() {
2690        let spec_json = json!({
2691            "openapi": "3.0.0",
2692            "info": { "title": "Ref API", "version": "1.0.0" },
2693            "components": {
2694                "schemas": {
2695                    "EmailWebsite": {
2696                        "type": "object",
2697                        "required": ["email", "website"],
2698                        "properties": {
2699                            "email":   {"type": "string", "format": "email"},
2700                            "website": {"type": "string", "format": "uri"}
2701                        }
2702                    }
2703                },
2704                "parameters": {
2705                    "PathId": {"name": "id", "in": "path", "required": true, "schema": {"type": "string"}},
2706                    "QueryQ": {"name": "q",  "in": "query", "required": false, "schema": {"type": "integer"}}
2707                },
2708                "requestBodies": {
2709                    "CreateUser": {
2710                        "content": {
2711                            "application/json": {
2712                                "schema": {"$ref": "#/components/schemas/EmailWebsite"}
2713                            }
2714                        }
2715                    }
2716                }
2717            },
2718            "paths": {
2719                "/users/{id}": {
2720                    "post": {
2721                        "parameters": [
2722                            {"$ref": "#/components/parameters/PathId"},
2723                            {"$ref": "#/components/parameters/QueryQ"}
2724                        ],
2725                        "requestBody": {"$ref": "#/components/requestBodies/CreateUser"},
2726                        "responses": {"200": {"description": "ok"}}
2727                    }
2728                }
2729            }
2730        });
2731
2732        let registry = create_registry_from_json(spec_json).unwrap();
2733        let mut path_params = Map::new();
2734        path_params.insert("id".to_string(), json!("abc"));
2735        let mut query_params = Map::new();
2736        query_params.insert("q".to_string(), json!(7));
2737
2738        let body = json!({"email":"user@example.com","website":"https://example.com"});
2739        assert!(registry
2740            .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&body))
2741            .is_ok());
2742
2743        let bad = json!({"email":"nope","website":"https://example.com"});
2744        assert!(registry
2745            .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&bad))
2746            .is_err());
2747    }
2748
2749    #[tokio::test]
2750    async fn test_header_cookie_and_query_coercion() {
2751        let spec_json = json!({
2752            "openapi": "3.0.0",
2753            "info": { "title": "Params API", "version": "1.0.0" },
2754            "paths": {
2755                "/items": {
2756                    "get": {
2757                        "parameters": [
2758                            {"name": "X-Flag", "in": "header", "required": true, "schema": {"type": "boolean"}},
2759                            {"name": "session", "in": "cookie", "required": true, "schema": {"type": "string"}},
2760                            {"name": "ids", "in": "query", "required": false, "schema": {"type": "array", "items": {"type": "integer"}}}
2761                        ],
2762                        "responses": {"200": {"description": "ok"}}
2763                    }
2764                }
2765            }
2766        });
2767
2768        let registry = create_registry_from_json(spec_json).unwrap();
2769
2770        let path_params = Map::new();
2771        let mut query_params = Map::new();
2772        // comma-separated string for array should coerce
2773        query_params.insert("ids".to_string(), json!("1,2,3"));
2774        let mut header_params = Map::new();
2775        header_params.insert("X-Flag".to_string(), json!("true"));
2776        let mut cookie_params = Map::new();
2777        cookie_params.insert("session".to_string(), json!("abc123"));
2778
2779        assert!(registry
2780            .validate_request_with_all(
2781                "/items",
2782                "GET",
2783                &path_params,
2784                &query_params,
2785                &header_params,
2786                &cookie_params,
2787                None
2788            )
2789            .is_ok());
2790
2791        // Missing required cookie
2792        let empty_cookie = Map::new();
2793        assert!(registry
2794            .validate_request_with_all(
2795                "/items",
2796                "GET",
2797                &path_params,
2798                &query_params,
2799                &header_params,
2800                &empty_cookie,
2801                None
2802            )
2803            .is_err());
2804
2805        // Bad boolean header value (cannot coerce)
2806        let mut bad_header = Map::new();
2807        bad_header.insert("X-Flag".to_string(), json!("notabool"));
2808        assert!(registry
2809            .validate_request_with_all(
2810                "/items",
2811                "GET",
2812                &path_params,
2813                &query_params,
2814                &bad_header,
2815                &cookie_params,
2816                None
2817            )
2818            .is_err());
2819    }
2820
2821    #[tokio::test]
2822    async fn test_query_styles_space_pipe_deepobject() {
2823        let spec_json = json!({
2824            "openapi": "3.0.0",
2825            "info": { "title": "Query Styles API", "version": "1.0.0" },
2826            "paths": {"/search": {"get": {
2827                "parameters": [
2828                    {"name":"tags","in":"query","style":"spaceDelimited","schema":{"type":"array","items":{"type":"string"}}},
2829                    {"name":"ids","in":"query","style":"pipeDelimited","schema":{"type":"array","items":{"type":"integer"}}},
2830                    {"name":"filter","in":"query","style":"deepObject","schema":{"type":"object","properties":{"color":{"type":"string"}},"required":["color"]}}
2831                ],
2832                "responses": {"200": {"description":"ok"}}
2833            }} }
2834        });
2835
2836        let registry = create_registry_from_json(spec_json).unwrap();
2837
2838        let path_params = Map::new();
2839        let mut query = Map::new();
2840        query.insert("tags".into(), json!("alpha beta gamma"));
2841        query.insert("ids".into(), json!("1|2|3"));
2842        query.insert("filter[color]".into(), json!("red"));
2843
2844        assert!(registry
2845            .validate_request_with("/search", "GET", &path_params, &query, None)
2846            .is_ok());
2847    }
2848
2849    #[tokio::test]
2850    async fn test_oneof_anyof_allof_validation() {
2851        let spec_json = json!({
2852            "openapi": "3.0.0",
2853            "info": { "title": "Composite API", "version": "1.0.0" },
2854            "paths": {
2855                "/composite": {
2856                    "post": {
2857                        "requestBody": {
2858                            "content": {
2859                                "application/json": {
2860                                    "schema": {
2861                                        "allOf": [
2862                                            {"type": "object", "required": ["base"], "properties": {"base": {"type": "string"}}}
2863                                        ],
2864                                        "oneOf": [
2865                                            {"type": "object", "properties": {"a": {"type": "integer"}}, "required": ["a"], "not": {"required": ["b"]}},
2866                                            {"type": "object", "properties": {"b": {"type": "integer"}}, "required": ["b"], "not": {"required": ["a"]}}
2867                                        ],
2868                                        "anyOf": [
2869                                            {"type": "object", "properties": {"flag": {"type": "boolean"}}, "required": ["flag"]},
2870                                            {"type": "object", "properties": {"extra": {"type": "string"}}, "required": ["extra"]}
2871                                        ]
2872                                    }
2873                                }
2874                            }
2875                        },
2876                        "responses": {"200": {"description": "ok"}}
2877                    }
2878                }
2879            }
2880        });
2881
2882        let registry = create_registry_from_json(spec_json).unwrap();
2883        // valid: satisfies base via allOf, exactly one of a/b, and at least one of flag/extra
2884        let ok = json!({"base": "x", "a": 1, "flag": true});
2885        assert!(registry
2886            .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&ok))
2887            .is_ok());
2888
2889        // invalid oneOf: both a and b present
2890        let bad_oneof = json!({"base": "x", "a": 1, "b": 2, "flag": false});
2891        assert!(registry
2892            .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&bad_oneof))
2893            .is_err());
2894
2895        // invalid anyOf: none of flag/extra present
2896        let bad_anyof = json!({"base": "x", "a": 1});
2897        assert!(registry
2898            .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&bad_anyof))
2899            .is_err());
2900
2901        // invalid allOf: missing base
2902        let bad_allof = json!({"a": 1, "flag": true});
2903        assert!(registry
2904            .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&bad_allof))
2905            .is_err());
2906    }
2907
2908    #[tokio::test]
2909    async fn test_overrides_warn_mode_allows_invalid() {
2910        // Spec with a POST route expecting an integer query param
2911        let spec_json = json!({
2912            "openapi": "3.0.0",
2913            "info": { "title": "Overrides API", "version": "1.0.0" },
2914            "paths": {"/things": {"post": {
2915                "parameters": [{"name":"q","in":"query","required":true,"schema":{"type":"integer"}}],
2916                "responses": {"200": {"description":"ok"}}
2917            }}}
2918        });
2919
2920        let spec = OpenApiSpec::from_json(spec_json).unwrap();
2921        let mut overrides = HashMap::new();
2922        overrides.insert("POST /things".to_string(), ValidationMode::Warn);
2923        let registry = OpenApiRouteRegistry::new_with_options(
2924            spec,
2925            ValidationOptions {
2926                request_mode: ValidationMode::Enforce,
2927                aggregate_errors: true,
2928                validate_responses: false,
2929                overrides,
2930                admin_skip_prefixes: vec![],
2931                response_template_expand: false,
2932                validation_status: None,
2933            },
2934        );
2935
2936        // Invalid q (missing) should warn, not error
2937        let ok = registry.validate_request_with("/things", "POST", &Map::new(), &Map::new(), None);
2938        assert!(ok.is_ok());
2939    }
2940
2941    #[tokio::test]
2942    async fn test_admin_skip_prefix_short_circuit() {
2943        let spec_json = json!({
2944            "openapi": "3.0.0",
2945            "info": { "title": "Skip API", "version": "1.0.0" },
2946            "paths": {}
2947        });
2948        let spec = OpenApiSpec::from_json(spec_json).unwrap();
2949        let registry = OpenApiRouteRegistry::new_with_options(
2950            spec,
2951            ValidationOptions {
2952                request_mode: ValidationMode::Enforce,
2953                aggregate_errors: true,
2954                validate_responses: false,
2955                overrides: HashMap::new(),
2956                admin_skip_prefixes: vec!["/admin".into()],
2957                response_template_expand: false,
2958                validation_status: None,
2959            },
2960        );
2961
2962        // No route exists for this, but skip prefix means it is accepted
2963        let res = registry.validate_request_with_all(
2964            "/admin/__mockforge/health",
2965            "GET",
2966            &Map::new(),
2967            &Map::new(),
2968            &Map::new(),
2969            &Map::new(),
2970            None,
2971        );
2972        assert!(res.is_ok());
2973    }
2974
2975    #[test]
2976    fn test_path_conversion() {
2977        assert_eq!(OpenApiRouteRegistry::convert_path_to_axum("/users"), "/users");
2978        assert_eq!(OpenApiRouteRegistry::convert_path_to_axum("/users/{id}"), "/users/{id}");
2979        assert_eq!(
2980            OpenApiRouteRegistry::convert_path_to_axum("/users/{id}/posts/{postId}"),
2981            "/users/{id}/posts/{postId}"
2982        );
2983    }
2984
2985    #[test]
2986    fn test_validation_options_default() {
2987        let options = ValidationOptions::default();
2988        assert!(matches!(options.request_mode, ValidationMode::Enforce));
2989        assert!(options.aggregate_errors);
2990        assert!(!options.validate_responses);
2991        assert!(options.overrides.is_empty());
2992        assert!(options.admin_skip_prefixes.is_empty());
2993        assert!(!options.response_template_expand);
2994        assert!(options.validation_status.is_none());
2995    }
2996
2997    #[test]
2998    fn test_validation_mode_variants() {
2999        // Test that all variants can be created and compared
3000        let disabled = ValidationMode::Disabled;
3001        let warn = ValidationMode::Warn;
3002        let enforce = ValidationMode::Enforce;
3003        let default = ValidationMode::default();
3004
3005        // Test that default is Warn
3006        assert!(matches!(default, ValidationMode::Warn));
3007
3008        // Test that variants are distinct
3009        assert!(!matches!(disabled, ValidationMode::Warn));
3010        assert!(!matches!(warn, ValidationMode::Enforce));
3011        assert!(!matches!(enforce, ValidationMode::Disabled));
3012    }
3013
3014    #[test]
3015    fn test_registry_spec_accessor() {
3016        let spec_json = json!({
3017            "openapi": "3.0.0",
3018            "info": {
3019                "title": "Test API",
3020                "version": "1.0.0"
3021            },
3022            "paths": {}
3023        });
3024        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3025        let registry = OpenApiRouteRegistry::new(spec.clone());
3026
3027        // Test spec() accessor
3028        let accessed_spec = registry.spec();
3029        assert_eq!(accessed_spec.title(), "Test API");
3030    }
3031
3032    #[test]
3033    fn test_clone_for_validation() {
3034        let spec_json = json!({
3035            "openapi": "3.0.0",
3036            "info": {
3037                "title": "Test API",
3038                "version": "1.0.0"
3039            },
3040            "paths": {
3041                "/users": {
3042                    "get": {
3043                        "responses": {
3044                            "200": {
3045                                "description": "Success"
3046                            }
3047                        }
3048                    }
3049                }
3050            }
3051        });
3052        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3053        let registry = OpenApiRouteRegistry::new(spec);
3054
3055        // Test clone_for_validation
3056        let cloned = registry.clone_for_validation();
3057        assert_eq!(cloned.routes().len(), registry.routes().len());
3058        assert_eq!(cloned.spec().title(), registry.spec().title());
3059    }
3060
3061    #[test]
3062    fn test_with_custom_fixture_loader() {
3063        let temp_dir = TempDir::new().unwrap();
3064        let spec_json = json!({
3065            "openapi": "3.0.0",
3066            "info": {
3067                "title": "Test API",
3068                "version": "1.0.0"
3069            },
3070            "paths": {}
3071        });
3072        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3073        let registry = OpenApiRouteRegistry::new(spec);
3074        let original_routes_len = registry.routes().len();
3075
3076        // Test with_custom_fixture_loader
3077        let custom_loader =
3078            Arc::new(crate::CustomFixtureLoader::new(temp_dir.path().to_path_buf(), true));
3079        let registry_with_loader = registry.with_custom_fixture_loader(custom_loader);
3080
3081        // Verify the loader was set (we can't directly access it, but we can test it doesn't panic)
3082        assert_eq!(registry_with_loader.routes().len(), original_routes_len);
3083    }
3084
3085    #[test]
3086    fn test_get_route() {
3087        let spec_json = json!({
3088            "openapi": "3.0.0",
3089            "info": {
3090                "title": "Test API",
3091                "version": "1.0.0"
3092            },
3093            "paths": {
3094                "/users": {
3095                    "get": {
3096                        "operationId": "getUsers",
3097                        "responses": {
3098                            "200": {
3099                                "description": "Success"
3100                            }
3101                        }
3102                    },
3103                    "post": {
3104                        "operationId": "createUser",
3105                        "responses": {
3106                            "201": {
3107                                "description": "Created"
3108                            }
3109                        }
3110                    }
3111                }
3112            }
3113        });
3114        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3115        let registry = OpenApiRouteRegistry::new(spec);
3116
3117        // Test get_route for existing route
3118        let route = registry.get_route("/users", "GET");
3119        assert!(route.is_some());
3120        assert_eq!(route.unwrap().method, "GET");
3121        assert_eq!(route.unwrap().path, "/users");
3122
3123        // Test get_route for non-existent route
3124        let route = registry.get_route("/nonexistent", "GET");
3125        assert!(route.is_none());
3126
3127        // Test get_route for different method
3128        let route = registry.get_route("/users", "POST");
3129        assert!(route.is_some());
3130        assert_eq!(route.unwrap().method, "POST");
3131    }
3132
3133    #[test]
3134    fn test_get_routes_for_path() {
3135        let spec_json = json!({
3136            "openapi": "3.0.0",
3137            "info": {
3138                "title": "Test API",
3139                "version": "1.0.0"
3140            },
3141            "paths": {
3142                "/users": {
3143                    "get": {
3144                        "responses": {
3145                            "200": {
3146                                "description": "Success"
3147                            }
3148                        }
3149                    },
3150                    "post": {
3151                        "responses": {
3152                            "201": {
3153                                "description": "Created"
3154                            }
3155                        }
3156                    },
3157                    "put": {
3158                        "responses": {
3159                            "200": {
3160                                "description": "Success"
3161                            }
3162                        }
3163                    }
3164                },
3165                "/posts": {
3166                    "get": {
3167                        "responses": {
3168                            "200": {
3169                                "description": "Success"
3170                            }
3171                        }
3172                    }
3173                }
3174            }
3175        });
3176        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3177        let registry = OpenApiRouteRegistry::new(spec);
3178
3179        // Test get_routes_for_path with multiple methods
3180        let routes = registry.get_routes_for_path("/users");
3181        assert_eq!(routes.len(), 3);
3182        let methods: Vec<&str> = routes.iter().map(|r| r.method.as_str()).collect();
3183        assert!(methods.contains(&"GET"));
3184        assert!(methods.contains(&"POST"));
3185        assert!(methods.contains(&"PUT"));
3186
3187        // Test get_routes_for_path with single method
3188        let routes = registry.get_routes_for_path("/posts");
3189        assert_eq!(routes.len(), 1);
3190        assert_eq!(routes[0].method, "GET");
3191
3192        // Test get_routes_for_path with non-existent path
3193        let routes = registry.get_routes_for_path("/nonexistent");
3194        assert!(routes.is_empty());
3195    }
3196
3197    #[test]
3198    fn test_new_vs_new_with_options() {
3199        let spec_json = json!({
3200            "openapi": "3.0.0",
3201            "info": {
3202                "title": "Test API",
3203                "version": "1.0.0"
3204            },
3205            "paths": {}
3206        });
3207        let spec1 = OpenApiSpec::from_json(spec_json.clone()).unwrap();
3208        let spec2 = OpenApiSpec::from_json(spec_json).unwrap();
3209
3210        // Test new() - uses environment-based options
3211        let registry1 = OpenApiRouteRegistry::new(spec1);
3212        assert_eq!(registry1.spec().title(), "Test API");
3213
3214        // Test new_with_options() - uses explicit options
3215        let options = ValidationOptions {
3216            request_mode: ValidationMode::Disabled,
3217            aggregate_errors: false,
3218            validate_responses: true,
3219            overrides: HashMap::new(),
3220            admin_skip_prefixes: vec!["/admin".to_string()],
3221            response_template_expand: true,
3222            validation_status: Some(422),
3223        };
3224        let registry2 = OpenApiRouteRegistry::new_with_options(spec2, options);
3225        assert_eq!(registry2.spec().title(), "Test API");
3226    }
3227
3228    #[test]
3229    fn test_new_with_env_vs_new() {
3230        let spec_json = json!({
3231            "openapi": "3.0.0",
3232            "info": {
3233                "title": "Test API",
3234                "version": "1.0.0"
3235            },
3236            "paths": {}
3237        });
3238        let spec1 = OpenApiSpec::from_json(spec_json.clone()).unwrap();
3239        let spec2 = OpenApiSpec::from_json(spec_json).unwrap();
3240
3241        // Test new() calls new_with_env()
3242        let registry1 = OpenApiRouteRegistry::new(spec1);
3243
3244        // Test new_with_env() directly
3245        let registry2 = OpenApiRouteRegistry::new_with_env(spec2);
3246
3247        // Both should create valid registries
3248        assert_eq!(registry1.spec().title(), "Test API");
3249        assert_eq!(registry2.spec().title(), "Test API");
3250    }
3251
3252    #[test]
3253    fn test_validation_options_custom() {
3254        let options = ValidationOptions {
3255            request_mode: ValidationMode::Warn,
3256            aggregate_errors: false,
3257            validate_responses: true,
3258            overrides: {
3259                let mut map = HashMap::new();
3260                map.insert("getUsers".to_string(), ValidationMode::Disabled);
3261                map
3262            },
3263            admin_skip_prefixes: vec!["/admin".to_string(), "/internal".to_string()],
3264            response_template_expand: true,
3265            validation_status: Some(422),
3266        };
3267
3268        assert!(matches!(options.request_mode, ValidationMode::Warn));
3269        assert!(!options.aggregate_errors);
3270        assert!(options.validate_responses);
3271        assert_eq!(options.overrides.len(), 1);
3272        assert_eq!(options.admin_skip_prefixes.len(), 2);
3273        assert!(options.response_template_expand);
3274        assert_eq!(options.validation_status, Some(422));
3275    }
3276
3277    #[test]
3278    fn test_validation_mode_default_standalone() {
3279        let mode = ValidationMode::default();
3280        assert!(matches!(mode, ValidationMode::Warn));
3281    }
3282
3283    #[test]
3284    fn test_validation_mode_clone() {
3285        let mode1 = ValidationMode::Enforce;
3286        let mode2 = mode1.clone();
3287        assert!(matches!(mode1, ValidationMode::Enforce));
3288        assert!(matches!(mode2, ValidationMode::Enforce));
3289    }
3290
3291    #[test]
3292    fn test_validation_mode_debug() {
3293        let mode = ValidationMode::Disabled;
3294        let debug_str = format!("{:?}", mode);
3295        assert!(debug_str.contains("Disabled") || debug_str.contains("ValidationMode"));
3296    }
3297
3298    #[test]
3299    fn test_validation_options_clone() {
3300        let options1 = ValidationOptions {
3301            request_mode: ValidationMode::Warn,
3302            aggregate_errors: true,
3303            validate_responses: false,
3304            overrides: HashMap::new(),
3305            admin_skip_prefixes: vec![],
3306            response_template_expand: false,
3307            validation_status: None,
3308        };
3309        let options2 = options1.clone();
3310        assert!(matches!(options2.request_mode, ValidationMode::Warn));
3311        assert_eq!(options1.aggregate_errors, options2.aggregate_errors);
3312    }
3313
3314    #[test]
3315    fn test_validation_options_debug() {
3316        let options = ValidationOptions::default();
3317        let debug_str = format!("{:?}", options);
3318        assert!(debug_str.contains("ValidationOptions"));
3319    }
3320
3321    #[test]
3322    fn test_validation_options_with_all_fields() {
3323        let mut overrides = HashMap::new();
3324        overrides.insert("op1".to_string(), ValidationMode::Disabled);
3325        overrides.insert("op2".to_string(), ValidationMode::Warn);
3326
3327        let options = ValidationOptions {
3328            request_mode: ValidationMode::Enforce,
3329            aggregate_errors: false,
3330            validate_responses: true,
3331            overrides: overrides.clone(),
3332            admin_skip_prefixes: vec!["/admin".to_string(), "/internal".to_string()],
3333            response_template_expand: true,
3334            validation_status: Some(422),
3335        };
3336
3337        assert!(matches!(options.request_mode, ValidationMode::Enforce));
3338        assert!(!options.aggregate_errors);
3339        assert!(options.validate_responses);
3340        assert_eq!(options.overrides.len(), 2);
3341        assert_eq!(options.admin_skip_prefixes.len(), 2);
3342        assert!(options.response_template_expand);
3343        assert_eq!(options.validation_status, Some(422));
3344    }
3345
3346    #[test]
3347    fn test_openapi_route_registry_clone() {
3348        let spec_json = json!({
3349            "openapi": "3.0.0",
3350            "info": { "title": "Test API", "version": "1.0.0" },
3351            "paths": {}
3352        });
3353        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3354        let registry1 = OpenApiRouteRegistry::new(spec);
3355        let registry2 = registry1.clone();
3356        assert_eq!(registry1.spec().title(), registry2.spec().title());
3357    }
3358
3359    #[test]
3360    fn test_validation_mode_serialization() {
3361        let mode = ValidationMode::Enforce;
3362        let json = serde_json::to_string(&mode).unwrap();
3363        assert!(json.contains("Enforce") || json.contains("enforce"));
3364    }
3365
3366    #[test]
3367    fn test_validation_mode_deserialization() {
3368        let json = r#""Disabled""#;
3369        let mode: ValidationMode = serde_json::from_str(json).unwrap();
3370        assert!(matches!(mode, ValidationMode::Disabled));
3371    }
3372
3373    #[test]
3374    fn test_validation_options_default_values() {
3375        let options = ValidationOptions::default();
3376        assert!(matches!(options.request_mode, ValidationMode::Enforce));
3377        assert!(options.aggregate_errors);
3378        assert!(!options.validate_responses);
3379        assert!(options.overrides.is_empty());
3380        assert!(options.admin_skip_prefixes.is_empty());
3381        assert!(!options.response_template_expand);
3382        assert_eq!(options.validation_status, None);
3383    }
3384
3385    #[test]
3386    fn test_validation_mode_all_variants() {
3387        let disabled = ValidationMode::Disabled;
3388        let warn = ValidationMode::Warn;
3389        let enforce = ValidationMode::Enforce;
3390
3391        assert!(matches!(disabled, ValidationMode::Disabled));
3392        assert!(matches!(warn, ValidationMode::Warn));
3393        assert!(matches!(enforce, ValidationMode::Enforce));
3394    }
3395
3396    #[test]
3397    fn test_validation_options_with_overrides() {
3398        let mut overrides = HashMap::new();
3399        overrides.insert("operation1".to_string(), ValidationMode::Disabled);
3400        overrides.insert("operation2".to_string(), ValidationMode::Warn);
3401
3402        let options = ValidationOptions {
3403            request_mode: ValidationMode::Enforce,
3404            aggregate_errors: true,
3405            validate_responses: false,
3406            overrides,
3407            admin_skip_prefixes: vec![],
3408            response_template_expand: false,
3409            validation_status: None,
3410        };
3411
3412        assert_eq!(options.overrides.len(), 2);
3413        assert!(matches!(options.overrides.get("operation1"), Some(ValidationMode::Disabled)));
3414        assert!(matches!(options.overrides.get("operation2"), Some(ValidationMode::Warn)));
3415    }
3416
3417    #[test]
3418    fn test_validation_options_with_admin_skip_prefixes() {
3419        let options = ValidationOptions {
3420            request_mode: ValidationMode::Enforce,
3421            aggregate_errors: true,
3422            validate_responses: false,
3423            overrides: HashMap::new(),
3424            admin_skip_prefixes: vec![
3425                "/admin".to_string(),
3426                "/internal".to_string(),
3427                "/debug".to_string(),
3428            ],
3429            response_template_expand: false,
3430            validation_status: None,
3431        };
3432
3433        assert_eq!(options.admin_skip_prefixes.len(), 3);
3434        assert!(options.admin_skip_prefixes.contains(&"/admin".to_string()));
3435        assert!(options.admin_skip_prefixes.contains(&"/internal".to_string()));
3436        assert!(options.admin_skip_prefixes.contains(&"/debug".to_string()));
3437    }
3438
3439    #[test]
3440    fn test_validation_options_with_validation_status() {
3441        let options1 = ValidationOptions {
3442            request_mode: ValidationMode::Enforce,
3443            aggregate_errors: true,
3444            validate_responses: false,
3445            overrides: HashMap::new(),
3446            admin_skip_prefixes: vec![],
3447            response_template_expand: false,
3448            validation_status: Some(400),
3449        };
3450
3451        let options2 = ValidationOptions {
3452            request_mode: ValidationMode::Enforce,
3453            aggregate_errors: true,
3454            validate_responses: false,
3455            overrides: HashMap::new(),
3456            admin_skip_prefixes: vec![],
3457            response_template_expand: false,
3458            validation_status: Some(422),
3459        };
3460
3461        assert_eq!(options1.validation_status, Some(400));
3462        assert_eq!(options2.validation_status, Some(422));
3463    }
3464
3465    #[test]
3466    fn test_validate_request_with_disabled_mode() {
3467        // Test validation with disabled mode (lines 1001-1007)
3468        let spec_json = json!({
3469            "openapi": "3.0.0",
3470            "info": {"title": "Test API", "version": "1.0.0"},
3471            "paths": {
3472                "/users": {
3473                    "get": {
3474                        "responses": {"200": {"description": "OK"}}
3475                    }
3476                }
3477            }
3478        });
3479        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3480        let options = ValidationOptions {
3481            request_mode: ValidationMode::Disabled,
3482            ..Default::default()
3483        };
3484        let registry = OpenApiRouteRegistry::new_with_options(spec, options);
3485
3486        // Should pass validation when disabled (lines 1002-1003, 1005-1007)
3487        let result = registry.validate_request_with_all(
3488            "/users",
3489            "GET",
3490            &Map::new(),
3491            &Map::new(),
3492            &Map::new(),
3493            &Map::new(),
3494            None,
3495        );
3496        assert!(result.is_ok());
3497    }
3498
3499    #[test]
3500    fn test_validate_request_with_warn_mode() {
3501        // Test validation with warn mode (lines 1162-1166)
3502        let spec_json = json!({
3503            "openapi": "3.0.0",
3504            "info": {"title": "Test API", "version": "1.0.0"},
3505            "paths": {
3506                "/users": {
3507                    "post": {
3508                        "requestBody": {
3509                            "required": true,
3510                            "content": {
3511                                "application/json": {
3512                                    "schema": {
3513                                        "type": "object",
3514                                        "required": ["name"],
3515                                        "properties": {
3516                                            "name": {"type": "string"}
3517                                        }
3518                                    }
3519                                }
3520                            }
3521                        },
3522                        "responses": {"200": {"description": "OK"}}
3523                    }
3524                }
3525            }
3526        });
3527        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3528        let options = ValidationOptions {
3529            request_mode: ValidationMode::Warn,
3530            ..Default::default()
3531        };
3532        let registry = OpenApiRouteRegistry::new_with_options(spec, options);
3533
3534        // Should pass with warnings when body is missing (lines 1162-1166)
3535        let result = registry.validate_request_with_all(
3536            "/users",
3537            "POST",
3538            &Map::new(),
3539            &Map::new(),
3540            &Map::new(),
3541            &Map::new(),
3542            None, // Missing required body
3543        );
3544        assert!(result.is_ok()); // Warn mode doesn't fail
3545    }
3546
3547    #[test]
3548    fn test_validate_request_body_validation_error() {
3549        // Test request body validation error path (lines 1072-1091)
3550        let spec_json = json!({
3551            "openapi": "3.0.0",
3552            "info": {"title": "Test API", "version": "1.0.0"},
3553            "paths": {
3554                "/users": {
3555                    "post": {
3556                        "requestBody": {
3557                            "required": true,
3558                            "content": {
3559                                "application/json": {
3560                                    "schema": {
3561                                        "type": "object",
3562                                        "required": ["name"],
3563                                        "properties": {
3564                                            "name": {"type": "string"}
3565                                        }
3566                                    }
3567                                }
3568                            }
3569                        },
3570                        "responses": {"200": {"description": "OK"}}
3571                    }
3572                }
3573            }
3574        });
3575        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3576        let registry = OpenApiRouteRegistry::new(spec);
3577
3578        // Should fail validation when body is missing (lines 1088-1091)
3579        let result = registry.validate_request_with_all(
3580            "/users",
3581            "POST",
3582            &Map::new(),
3583            &Map::new(),
3584            &Map::new(),
3585            &Map::new(),
3586            None, // Missing required body
3587        );
3588        assert!(result.is_err());
3589    }
3590
3591    #[test]
3592    fn test_validate_request_body_schema_validation_error() {
3593        // Test request body schema validation error (lines 1038-1049)
3594        let spec_json = json!({
3595            "openapi": "3.0.0",
3596            "info": {"title": "Test API", "version": "1.0.0"},
3597            "paths": {
3598                "/users": {
3599                    "post": {
3600                        "requestBody": {
3601                            "required": true,
3602                            "content": {
3603                                "application/json": {
3604                                    "schema": {
3605                                        "type": "object",
3606                                        "required": ["name"],
3607                                        "properties": {
3608                                            "name": {"type": "string"}
3609                                        }
3610                                    }
3611                                }
3612                            }
3613                        },
3614                        "responses": {"200": {"description": "OK"}}
3615                    }
3616                }
3617            }
3618        });
3619        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3620        let registry = OpenApiRouteRegistry::new(spec);
3621
3622        // Should fail validation when body doesn't match schema (lines 1038-1049)
3623        let invalid_body = json!({}); // Missing required "name" field
3624        let result = registry.validate_request_with_all(
3625            "/users",
3626            "POST",
3627            &Map::new(),
3628            &Map::new(),
3629            &Map::new(),
3630            &Map::new(),
3631            Some(&invalid_body),
3632        );
3633        assert!(result.is_err());
3634    }
3635
3636    #[test]
3637    fn test_validate_request_body_referenced_schema_error() {
3638        // Test request body with referenced schema that can't be resolved (lines 1070-1076)
3639        let spec_json = json!({
3640            "openapi": "3.0.0",
3641            "info": {"title": "Test API", "version": "1.0.0"},
3642            "paths": {
3643                "/users": {
3644                    "post": {
3645                        "requestBody": {
3646                            "required": true,
3647                            "content": {
3648                                "application/json": {
3649                                    "schema": {
3650                                        "$ref": "#/components/schemas/NonExistentSchema"
3651                                    }
3652                                }
3653                            }
3654                        },
3655                        "responses": {"200": {"description": "OK"}}
3656                    }
3657                }
3658            },
3659            "components": {}
3660        });
3661        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3662        let registry = OpenApiRouteRegistry::new(spec);
3663
3664        // Should fail validation when schema reference can't be resolved (lines 1070-1076)
3665        let body = json!({"name": "test"});
3666        let result = registry.validate_request_with_all(
3667            "/users",
3668            "POST",
3669            &Map::new(),
3670            &Map::new(),
3671            &Map::new(),
3672            &Map::new(),
3673            Some(&body),
3674        );
3675        assert!(result.is_err());
3676    }
3677
3678    #[test]
3679    fn test_validate_request_body_referenced_request_body_error() {
3680        // Test request body with referenced request body that can't be resolved (lines 1081-1087)
3681        let spec_json = json!({
3682            "openapi": "3.0.0",
3683            "info": {"title": "Test API", "version": "1.0.0"},
3684            "paths": {
3685                "/users": {
3686                    "post": {
3687                        "requestBody": {
3688                            "$ref": "#/components/requestBodies/NonExistentRequestBody"
3689                        },
3690                        "responses": {"200": {"description": "OK"}}
3691                    }
3692                }
3693            },
3694            "components": {}
3695        });
3696        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3697        let registry = OpenApiRouteRegistry::new(spec);
3698
3699        // Should fail validation when request body reference can't be resolved (lines 1081-1087)
3700        let body = json!({"name": "test"});
3701        let result = registry.validate_request_with_all(
3702            "/users",
3703            "POST",
3704            &Map::new(),
3705            &Map::new(),
3706            &Map::new(),
3707            &Map::new(),
3708            Some(&body),
3709        );
3710        assert!(result.is_err());
3711    }
3712
3713    #[test]
3714    fn test_validate_request_body_provided_when_not_expected() {
3715        // Test body provided when not expected (lines 1092-1094)
3716        let spec_json = json!({
3717            "openapi": "3.0.0",
3718            "info": {"title": "Test API", "version": "1.0.0"},
3719            "paths": {
3720                "/users": {
3721                    "get": {
3722                        "responses": {"200": {"description": "OK"}}
3723                    }
3724                }
3725            }
3726        });
3727        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3728        let registry = OpenApiRouteRegistry::new(spec);
3729
3730        // Should accept body even when not expected (lines 1092-1094)
3731        let body = json!({"extra": "data"});
3732        let result = registry.validate_request_with_all(
3733            "/users",
3734            "GET",
3735            &Map::new(),
3736            &Map::new(),
3737            &Map::new(),
3738            &Map::new(),
3739            Some(&body),
3740        );
3741        // Should not error - just logs debug message
3742        assert!(result.is_ok());
3743    }
3744
3745    #[test]
3746    fn test_get_operation() {
3747        // Test get_operation method (lines 1196-1205)
3748        let spec_json = json!({
3749            "openapi": "3.0.0",
3750            "info": {"title": "Test API", "version": "1.0.0"},
3751            "paths": {
3752                "/users": {
3753                    "get": {
3754                        "operationId": "getUsers",
3755                        "summary": "Get users",
3756                        "responses": {"200": {"description": "OK"}}
3757                    }
3758                }
3759            }
3760        });
3761        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3762        let registry = OpenApiRouteRegistry::new(spec);
3763
3764        // Should return operation details (lines 1196-1205)
3765        let operation = registry.get_operation("/users", "GET");
3766        assert!(operation.is_some());
3767        assert_eq!(operation.unwrap().method, "GET");
3768
3769        // Should return None for non-existent route
3770        assert!(registry.get_operation("/nonexistent", "GET").is_none());
3771    }
3772
3773    #[test]
3774    fn test_extract_path_parameters() {
3775        // Test extract_path_parameters method (lines 1208-1223)
3776        let spec_json = json!({
3777            "openapi": "3.0.0",
3778            "info": {"title": "Test API", "version": "1.0.0"},
3779            "paths": {
3780                "/users/{id}": {
3781                    "get": {
3782                        "parameters": [
3783                            {
3784                                "name": "id",
3785                                "in": "path",
3786                                "required": true,
3787                                "schema": {"type": "string"}
3788                            }
3789                        ],
3790                        "responses": {"200": {"description": "OK"}}
3791                    }
3792                }
3793            }
3794        });
3795        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3796        let registry = OpenApiRouteRegistry::new(spec);
3797
3798        // Should extract path parameters (lines 1208-1223)
3799        let params = registry.extract_path_parameters("/users/123", "GET");
3800        assert_eq!(params.get("id"), Some(&"123".to_string()));
3801
3802        // Should return empty map for non-matching path
3803        let empty_params = registry.extract_path_parameters("/users", "GET");
3804        assert!(empty_params.is_empty());
3805    }
3806
3807    #[test]
3808    fn test_extract_path_parameters_multiple_params() {
3809        // Test extract_path_parameters with multiple path parameters
3810        let spec_json = json!({
3811            "openapi": "3.0.0",
3812            "info": {"title": "Test API", "version": "1.0.0"},
3813            "paths": {
3814                "/users/{userId}/posts/{postId}": {
3815                    "get": {
3816                        "parameters": [
3817                            {
3818                                "name": "userId",
3819                                "in": "path",
3820                                "required": true,
3821                                "schema": {"type": "string"}
3822                            },
3823                            {
3824                                "name": "postId",
3825                                "in": "path",
3826                                "required": true,
3827                                "schema": {"type": "string"}
3828                            }
3829                        ],
3830                        "responses": {"200": {"description": "OK"}}
3831                    }
3832                }
3833            }
3834        });
3835        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3836        let registry = OpenApiRouteRegistry::new(spec);
3837
3838        // Should extract multiple path parameters
3839        let params = registry.extract_path_parameters("/users/123/posts/456", "GET");
3840        assert_eq!(params.get("userId"), Some(&"123".to_string()));
3841        assert_eq!(params.get("postId"), Some(&"456".to_string()));
3842    }
3843
3844    #[test]
3845    fn test_validate_request_route_not_found() {
3846        // Test validation when route not found (lines 1171-1173)
3847        let spec_json = json!({
3848            "openapi": "3.0.0",
3849            "info": {"title": "Test API", "version": "1.0.0"},
3850            "paths": {
3851                "/users": {
3852                    "get": {
3853                        "responses": {"200": {"description": "OK"}}
3854                    }
3855                }
3856            }
3857        });
3858        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3859        let registry = OpenApiRouteRegistry::new(spec);
3860
3861        // Should return error when route not found (lines 1171-1173)
3862        let result = registry.validate_request_with_all(
3863            "/nonexistent",
3864            "GET",
3865            &Map::new(),
3866            &Map::new(),
3867            &Map::new(),
3868            &Map::new(),
3869            None,
3870        );
3871        assert!(result.is_err());
3872        assert!(result.unwrap_err().to_string().contains("not found"));
3873    }
3874
3875    #[test]
3876    fn test_validate_request_with_path_parameters() {
3877        // Test path parameter validation (lines 1101-1110)
3878        let spec_json = json!({
3879            "openapi": "3.0.0",
3880            "info": {"title": "Test API", "version": "1.0.0"},
3881            "paths": {
3882                "/users/{id}": {
3883                    "get": {
3884                        "parameters": [
3885                            {
3886                                "name": "id",
3887                                "in": "path",
3888                                "required": true,
3889                                "schema": {"type": "string", "minLength": 1}
3890                            }
3891                        ],
3892                        "responses": {"200": {"description": "OK"}}
3893                    }
3894                }
3895            }
3896        });
3897        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3898        let registry = OpenApiRouteRegistry::new(spec);
3899
3900        // Should pass validation with valid path parameter
3901        let mut path_params = Map::new();
3902        path_params.insert("id".to_string(), json!("123"));
3903        let result = registry.validate_request_with_all(
3904            "/users/{id}",
3905            "GET",
3906            &path_params,
3907            &Map::new(),
3908            &Map::new(),
3909            &Map::new(),
3910            None,
3911        );
3912        assert!(result.is_ok());
3913    }
3914
3915    #[test]
3916    fn test_validate_request_with_query_parameters() {
3917        // Test query parameter validation (lines 1111-1134)
3918        let spec_json = json!({
3919            "openapi": "3.0.0",
3920            "info": {"title": "Test API", "version": "1.0.0"},
3921            "paths": {
3922                "/users": {
3923                    "get": {
3924                        "parameters": [
3925                            {
3926                                "name": "page",
3927                                "in": "query",
3928                                "required": true,
3929                                "schema": {"type": "integer", "minimum": 1}
3930                            }
3931                        ],
3932                        "responses": {"200": {"description": "OK"}}
3933                    }
3934                }
3935            }
3936        });
3937        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3938        let registry = OpenApiRouteRegistry::new(spec);
3939
3940        // Should pass validation with valid query parameter
3941        let mut query_params = Map::new();
3942        query_params.insert("page".to_string(), json!(1));
3943        let result = registry.validate_request_with_all(
3944            "/users",
3945            "GET",
3946            &Map::new(),
3947            &query_params,
3948            &Map::new(),
3949            &Map::new(),
3950            None,
3951        );
3952        assert!(result.is_ok());
3953    }
3954
3955    #[test]
3956    fn test_validate_request_with_header_parameters() {
3957        // Test header parameter validation (lines 1135-1144)
3958        let spec_json = json!({
3959            "openapi": "3.0.0",
3960            "info": {"title": "Test API", "version": "1.0.0"},
3961            "paths": {
3962                "/users": {
3963                    "get": {
3964                        "parameters": [
3965                            {
3966                                "name": "X-API-Key",
3967                                "in": "header",
3968                                "required": true,
3969                                "schema": {"type": "string"}
3970                            }
3971                        ],
3972                        "responses": {"200": {"description": "OK"}}
3973                    }
3974                }
3975            }
3976        });
3977        let spec = OpenApiSpec::from_json(spec_json).unwrap();
3978        let registry = OpenApiRouteRegistry::new(spec);
3979
3980        // Should pass validation with valid header parameter
3981        let mut header_params = Map::new();
3982        header_params.insert("X-API-Key".to_string(), json!("secret-key"));
3983        let result = registry.validate_request_with_all(
3984            "/users",
3985            "GET",
3986            &Map::new(),
3987            &Map::new(),
3988            &header_params,
3989            &Map::new(),
3990            None,
3991        );
3992        assert!(result.is_ok());
3993    }
3994
3995    #[test]
3996    fn test_validate_request_with_cookie_parameters() {
3997        // Test cookie parameter validation (lines 1145-1154)
3998        let spec_json = json!({
3999            "openapi": "3.0.0",
4000            "info": {"title": "Test API", "version": "1.0.0"},
4001            "paths": {
4002                "/users": {
4003                    "get": {
4004                        "parameters": [
4005                            {
4006                                "name": "sessionId",
4007                                "in": "cookie",
4008                                "required": true,
4009                                "schema": {"type": "string"}
4010                            }
4011                        ],
4012                        "responses": {"200": {"description": "OK"}}
4013                    }
4014                }
4015            }
4016        });
4017        let spec = OpenApiSpec::from_json(spec_json).unwrap();
4018        let registry = OpenApiRouteRegistry::new(spec);
4019
4020        // Should pass validation with valid cookie parameter
4021        let mut cookie_params = Map::new();
4022        cookie_params.insert("sessionId".to_string(), json!("abc123"));
4023        let result = registry.validate_request_with_all(
4024            "/users",
4025            "GET",
4026            &Map::new(),
4027            &Map::new(),
4028            &Map::new(),
4029            &cookie_params,
4030            None,
4031        );
4032        assert!(result.is_ok());
4033    }
4034
4035    #[test]
4036    fn test_validate_request_no_errors_early_return() {
4037        // Test early return when no errors (lines 1158-1160)
4038        let spec_json = json!({
4039            "openapi": "3.0.0",
4040            "info": {"title": "Test API", "version": "1.0.0"},
4041            "paths": {
4042                "/users": {
4043                    "get": {
4044                        "responses": {"200": {"description": "OK"}}
4045                    }
4046                }
4047            }
4048        });
4049        let spec = OpenApiSpec::from_json(spec_json).unwrap();
4050        let registry = OpenApiRouteRegistry::new(spec);
4051
4052        // Should return early when no errors (lines 1158-1160)
4053        let result = registry.validate_request_with_all(
4054            "/users",
4055            "GET",
4056            &Map::new(),
4057            &Map::new(),
4058            &Map::new(),
4059            &Map::new(),
4060            None,
4061        );
4062        assert!(result.is_ok());
4063    }
4064
4065    #[test]
4066    fn test_validate_request_query_parameter_different_styles() {
4067        // Test query parameter validation with different styles (lines 1118-1123)
4068        let spec_json = json!({
4069            "openapi": "3.0.0",
4070            "info": {"title": "Test API", "version": "1.0.0"},
4071            "paths": {
4072                "/users": {
4073                    "get": {
4074                        "parameters": [
4075                            {
4076                                "name": "tags",
4077                                "in": "query",
4078                                "style": "pipeDelimited",
4079                                "schema": {
4080                                    "type": "array",
4081                                    "items": {"type": "string"}
4082                                }
4083                            }
4084                        ],
4085                        "responses": {"200": {"description": "OK"}}
4086                    }
4087                }
4088            }
4089        });
4090        let spec = OpenApiSpec::from_json(spec_json).unwrap();
4091        let registry = OpenApiRouteRegistry::new(spec);
4092
4093        // Should handle pipeDelimited style (lines 1118-1123)
4094        let mut query_params = Map::new();
4095        query_params.insert("tags".to_string(), json!(["tag1", "tag2"]));
4096        let result = registry.validate_request_with_all(
4097            "/users",
4098            "GET",
4099            &Map::new(),
4100            &query_params,
4101            &Map::new(),
4102            &Map::new(),
4103            None,
4104        );
4105        // Should not error on style handling
4106        assert!(result.is_ok() || result.is_err()); // Either is fine, just testing the path
4107    }
4108}