Skip to main content

mockforge_core/
openapi_routes.rs

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