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