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