Skip to main content

mockforge_openapi/
openapi_routes.rs

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