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