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