Skip to main content

mockforge_openapi/
openapi_routes.rs

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