rustapi_core/
router.rs

1//! Router implementation using radix tree (matchit)
2//!
3//! This module provides HTTP routing functionality for RustAPI. Routes are
4//! registered using path patterns and HTTP method handlers.
5//!
6//! # Path Patterns
7//!
8//! Routes support dynamic path parameters using `{param}` syntax:
9//!
10//! - `/users` - Static path
11//! - `/users/{id}` - Single parameter
12//! - `/users/{user_id}/posts/{post_id}` - Multiple parameters
13//!
14//! # Example
15//!
16//! ```rust,ignore
17//! use rustapi_core::{Router, get, post, put, delete};
18//!
19//! async fn list_users() -> &'static str { "List users" }
20//! async fn get_user() -> &'static str { "Get user" }
21//! async fn create_user() -> &'static str { "Create user" }
22//! async fn update_user() -> &'static str { "Update user" }
23//! async fn delete_user() -> &'static str { "Delete user" }
24//!
25//! let router = Router::new()
26//!     .route("/users", get(list_users).post(create_user))
27//!     .route("/users/{id}", get(get_user).put(update_user).delete(delete_user));
28//! ```
29//!
30//! # Method Chaining
31//!
32//! Multiple HTTP methods can be registered for the same path using method chaining:
33//!
34//! ```rust,ignore
35//! .route("/users", get(list).post(create))
36//! .route("/users/{id}", get(show).put(update).delete(destroy))
37//! ```
38//!
39//! # Route Conflict Detection
40//!
41//! The router detects conflicting routes at registration time and provides
42//! helpful error messages with resolution guidance.
43
44use crate::handler::{into_boxed_handler, BoxedHandler, Handler};
45use crate::path_params::PathParams;
46use http::{Extensions, Method};
47use matchit::Router as MatchitRouter;
48use rustapi_openapi::Operation;
49use std::collections::HashMap;
50use std::sync::Arc;
51
52/// Information about a registered route for conflict detection
53#[derive(Debug, Clone)]
54pub struct RouteInfo {
55    /// The original path pattern (e.g., "/users/{id}")
56    pub path: String,
57    /// The HTTP methods registered for this path
58    pub methods: Vec<Method>,
59}
60
61/// Error returned when a route conflict is detected
62#[derive(Debug, Clone)]
63pub struct RouteConflictError {
64    /// The path that was being registered
65    pub new_path: String,
66    /// The HTTP method that conflicts
67    pub method: Option<Method>,
68    /// The existing path that conflicts
69    pub existing_path: String,
70    /// Detailed error message from the underlying router
71    pub details: String,
72}
73
74impl std::fmt::Display for RouteConflictError {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        writeln!(
77            f,
78            "\n╭─────────────────────────────────────────────────────────────╮"
79        )?;
80        writeln!(
81            f,
82            "│                    ROUTE CONFLICT DETECTED                   │"
83        )?;
84        writeln!(
85            f,
86            "╰─────────────────────────────────────────────────────────────╯"
87        )?;
88        writeln!(f)?;
89        writeln!(f, "  Conflicting routes:")?;
90        writeln!(f, "    → Existing: {}", self.existing_path)?;
91        writeln!(f, "    → New:      {}", self.new_path)?;
92        writeln!(f)?;
93        if let Some(ref method) = self.method {
94            writeln!(f, "  HTTP Method: {}", method)?;
95            writeln!(f)?;
96        }
97        writeln!(f, "  Details: {}", self.details)?;
98        writeln!(f)?;
99        writeln!(f, "  How to resolve:")?;
100        writeln!(f, "    1. Use different path patterns for each route")?;
101        writeln!(
102            f,
103            "    2. If paths must be similar, ensure parameter names differ"
104        )?;
105        writeln!(
106            f,
107            "    3. Consider using different HTTP methods if appropriate"
108        )?;
109        writeln!(f)?;
110        writeln!(f, "  Example:")?;
111        writeln!(f, "    Instead of:")?;
112        writeln!(f, "      .route(\"/users/{{id}}\", get(handler1))")?;
113        writeln!(f, "      .route(\"/users/{{user_id}}\", get(handler2))")?;
114        writeln!(f)?;
115        writeln!(f, "    Use:")?;
116        writeln!(f, "      .route(\"/users/{{id}}\", get(handler1))")?;
117        writeln!(f, "      .route(\"/users/{{id}}/profile\", get(handler2))")?;
118        Ok(())
119    }
120}
121
122impl std::error::Error for RouteConflictError {}
123
124/// HTTP method router for a single path
125pub struct MethodRouter {
126    handlers: HashMap<Method, BoxedHandler>,
127    pub(crate) operations: HashMap<Method, Operation>,
128}
129
130impl Clone for MethodRouter {
131    fn clone(&self) -> Self {
132        Self {
133            handlers: self.handlers.clone(),
134            operations: self.operations.clone(),
135        }
136    }
137}
138
139impl MethodRouter {
140    /// Create a new empty method router
141    pub fn new() -> Self {
142        Self {
143            handlers: HashMap::new(),
144            operations: HashMap::new(),
145        }
146    }
147
148    /// Add a handler for a specific method
149    fn on(mut self, method: Method, handler: BoxedHandler, operation: Operation) -> Self {
150        self.handlers.insert(method.clone(), handler);
151        self.operations.insert(method, operation);
152        self
153    }
154
155    /// Get handler for a method
156    pub(crate) fn get_handler(&self, method: &Method) -> Option<&BoxedHandler> {
157        self.handlers.get(method)
158    }
159
160    /// Get allowed methods for 405 response
161    pub(crate) fn allowed_methods(&self) -> Vec<Method> {
162        self.handlers.keys().cloned().collect()
163    }
164
165    /// Create from pre-boxed handlers (internal use)
166    pub(crate) fn from_boxed(handlers: HashMap<Method, BoxedHandler>) -> Self {
167        Self {
168            handlers,
169            operations: HashMap::new(), // Operations lost when using raw boxed handlers for now
170        }
171    }
172
173    /// Insert a pre-boxed handler and its OpenAPI operation (internal use).
174    ///
175    /// Panics if the same method is inserted twice for the same path.
176    pub(crate) fn insert_boxed_with_operation(
177        &mut self,
178        method: Method,
179        handler: BoxedHandler,
180        operation: Operation,
181    ) {
182        if self.handlers.contains_key(&method) {
183            panic!(
184                "Duplicate handler for method {} on the same path",
185                method.as_str()
186            );
187        }
188
189        self.handlers.insert(method.clone(), handler);
190        self.operations.insert(method, operation);
191    }
192    /// Add a GET handler
193    pub fn get<H, T>(self, handler: H) -> Self
194    where
195        H: Handler<T>,
196        T: 'static,
197    {
198        let mut op = Operation::new();
199        H::update_operation(&mut op);
200        self.on(Method::GET, into_boxed_handler(handler), op)
201    }
202
203    /// Add a POST handler
204    pub fn post<H, T>(self, handler: H) -> Self
205    where
206        H: Handler<T>,
207        T: 'static,
208    {
209        let mut op = Operation::new();
210        H::update_operation(&mut op);
211        self.on(Method::POST, into_boxed_handler(handler), op)
212    }
213
214    /// Add a PUT handler
215    pub fn put<H, T>(self, handler: H) -> Self
216    where
217        H: Handler<T>,
218        T: 'static,
219    {
220        let mut op = Operation::new();
221        H::update_operation(&mut op);
222        self.on(Method::PUT, into_boxed_handler(handler), op)
223    }
224
225    /// Add a PATCH handler
226    pub fn patch<H, T>(self, handler: H) -> Self
227    where
228        H: Handler<T>,
229        T: 'static,
230    {
231        let mut op = Operation::new();
232        H::update_operation(&mut op);
233        self.on(Method::PATCH, into_boxed_handler(handler), op)
234    }
235
236    /// Add a DELETE handler
237    pub fn delete<H, T>(self, handler: H) -> Self
238    where
239        H: Handler<T>,
240        T: 'static,
241    {
242        let mut op = Operation::new();
243        H::update_operation(&mut op);
244        self.on(Method::DELETE, into_boxed_handler(handler), op)
245    }
246}
247
248impl Default for MethodRouter {
249    fn default() -> Self {
250        Self::new()
251    }
252}
253
254/// Create a GET route handler
255pub fn get<H, T>(handler: H) -> MethodRouter
256where
257    H: Handler<T>,
258    T: 'static,
259{
260    let mut op = Operation::new();
261    H::update_operation(&mut op);
262    MethodRouter::new().on(Method::GET, into_boxed_handler(handler), op)
263}
264
265/// Create a POST route handler
266pub fn post<H, T>(handler: H) -> MethodRouter
267where
268    H: Handler<T>,
269    T: 'static,
270{
271    let mut op = Operation::new();
272    H::update_operation(&mut op);
273    MethodRouter::new().on(Method::POST, into_boxed_handler(handler), op)
274}
275
276/// Create a PUT route handler
277pub fn put<H, T>(handler: H) -> MethodRouter
278where
279    H: Handler<T>,
280    T: 'static,
281{
282    let mut op = Operation::new();
283    H::update_operation(&mut op);
284    MethodRouter::new().on(Method::PUT, into_boxed_handler(handler), op)
285}
286
287/// Create a PATCH route handler
288pub fn patch<H, T>(handler: H) -> MethodRouter
289where
290    H: Handler<T>,
291    T: 'static,
292{
293    let mut op = Operation::new();
294    H::update_operation(&mut op);
295    MethodRouter::new().on(Method::PATCH, into_boxed_handler(handler), op)
296}
297
298/// Create a DELETE route handler
299pub fn delete<H, T>(handler: H) -> MethodRouter
300where
301    H: Handler<T>,
302    T: 'static,
303{
304    let mut op = Operation::new();
305    H::update_operation(&mut op);
306    MethodRouter::new().on(Method::DELETE, into_boxed_handler(handler), op)
307}
308
309/// Main router
310pub struct Router {
311    inner: MatchitRouter<MethodRouter>,
312    state: Arc<Extensions>,
313    /// Track registered routes for conflict detection
314    registered_routes: HashMap<String, RouteInfo>,
315    /// Store MethodRouters for nesting support (keyed by matchit path)
316    method_routers: HashMap<String, MethodRouter>,
317    /// Track state type IDs for merging (type name -> whether it's set)
318    /// This is a workaround since Extensions doesn't support iteration
319    state_type_ids: Vec<std::any::TypeId>,
320}
321
322impl Router {
323    /// Create a new router
324    pub fn new() -> Self {
325        Self {
326            inner: MatchitRouter::new(),
327            state: Arc::new(Extensions::new()),
328            registered_routes: HashMap::new(),
329            method_routers: HashMap::new(),
330            state_type_ids: Vec::new(),
331        }
332    }
333
334    /// Add a route
335    pub fn route(mut self, path: &str, method_router: MethodRouter) -> Self {
336        // Convert {param} style to :param for matchit
337        let matchit_path = convert_path_params(path);
338
339        // Get the methods being registered
340        let methods: Vec<Method> = method_router.handlers.keys().cloned().collect();
341
342        // Store a clone of the MethodRouter for nesting support
343        self.method_routers
344            .insert(matchit_path.clone(), method_router.clone());
345
346        match self.inner.insert(matchit_path.clone(), method_router) {
347            Ok(_) => {
348                // Track the registered route
349                self.registered_routes.insert(
350                    matchit_path.clone(),
351                    RouteInfo {
352                        path: path.to_string(),
353                        methods,
354                    },
355                );
356            }
357            Err(e) => {
358                // Remove the method_router we just added since registration failed
359                self.method_routers.remove(&matchit_path);
360
361                // Find the existing conflicting route
362                let existing_path = self
363                    .find_conflicting_route(&matchit_path)
364                    .map(|info| info.path.clone())
365                    .unwrap_or_else(|| "<unknown>".to_string());
366
367                let conflict_error = RouteConflictError {
368                    new_path: path.to_string(),
369                    method: methods.first().cloned(),
370                    existing_path,
371                    details: e.to_string(),
372                };
373
374                panic!("{}", conflict_error);
375            }
376        }
377        self
378    }
379
380    /// Find a conflicting route by checking registered routes
381    fn find_conflicting_route(&self, matchit_path: &str) -> Option<&RouteInfo> {
382        // Try to find an exact match first
383        if let Some(info) = self.registered_routes.get(matchit_path) {
384            return Some(info);
385        }
386
387        // Try to find a route that would conflict (same structure but different param names)
388        let normalized_new = normalize_path_for_comparison(matchit_path);
389
390        for (registered_path, info) in &self.registered_routes {
391            let normalized_existing = normalize_path_for_comparison(registered_path);
392            if normalized_new == normalized_existing {
393                return Some(info);
394            }
395        }
396
397        None
398    }
399
400    /// Add application state
401    pub fn state<S: Clone + Send + Sync + 'static>(mut self, state: S) -> Self {
402        let type_id = std::any::TypeId::of::<S>();
403        let extensions = Arc::make_mut(&mut self.state);
404        extensions.insert(state);
405        if !self.state_type_ids.contains(&type_id) {
406            self.state_type_ids.push(type_id);
407        }
408        self
409    }
410
411    /// Check if state of a given type exists
412    pub fn has_state<S: 'static>(&self) -> bool {
413        self.state_type_ids.contains(&std::any::TypeId::of::<S>())
414    }
415
416    /// Get state type IDs (for testing and debugging)
417    pub fn state_type_ids(&self) -> &[std::any::TypeId] {
418        &self.state_type_ids
419    }
420
421    /// Nest another router under a prefix
422    ///
423    /// All routes from the nested router will be registered with the prefix
424    /// prepended to their paths. State from the nested router is merged into
425    /// the parent router (parent state takes precedence for type conflicts).
426    ///
427    /// # State Merging
428    ///
429    /// When nesting routers with state:
430    /// - If the parent router has state of type T, it is preserved (parent wins)
431    /// - If only the nested router has state of type T, it is added to the parent
432    /// - State type tracking is merged to enable proper conflict detection
433    ///
434    /// Note: Due to limitations of `http::Extensions`, automatic state merging
435    /// requires using the `merge_state` method for specific types.
436    ///
437    /// # Example
438    ///
439    /// ```rust,ignore
440    /// use rustapi_core::{Router, get};
441    ///
442    /// async fn list_users() -> &'static str { "List users" }
443    /// async fn get_user() -> &'static str { "Get user" }
444    ///
445    /// let users_router = Router::new()
446    ///     .route("/", get(list_users))
447    ///     .route("/{id}", get(get_user));
448    ///
449    /// let app = Router::new()
450    ///     .nest("/api/users", users_router);
451    ///
452    /// // Routes are now:
453    /// // GET /api/users/
454    /// // GET /api/users/{id}
455    /// ```
456    pub fn nest(mut self, prefix: &str, router: Router) -> Self {
457        // 1. Normalize the prefix
458        let normalized_prefix = normalize_prefix(prefix);
459
460        // 2. Merge state type IDs from nested router
461        // Parent state takes precedence - we only track types, actual values
462        // are handled by merge_state calls or by the user adding state to parent
463        for type_id in &router.state_type_ids {
464            if !self.state_type_ids.contains(type_id) {
465                self.state_type_ids.push(*type_id);
466            }
467        }
468
469        // 3. Collect routes from the nested router before consuming it
470        // We need to iterate over registered_routes and get the corresponding MethodRouters
471        let nested_routes: Vec<(String, RouteInfo, MethodRouter)> = router
472            .registered_routes
473            .into_iter()
474            .filter_map(|(matchit_path, route_info)| {
475                router
476                    .method_routers
477                    .get(&matchit_path)
478                    .map(|mr| (matchit_path, route_info, mr.clone()))
479            })
480            .collect();
481
482        // 4. Register each nested route with the prefix
483        for (matchit_path, route_info, method_router) in nested_routes {
484            // Build the prefixed path
485            // The matchit_path already has the :param format
486            // The route_info.path has the {param} format
487            let prefixed_matchit_path = if matchit_path == "/" {
488                normalized_prefix.clone()
489            } else {
490                format!("{}{}", normalized_prefix, matchit_path)
491            };
492
493            let prefixed_display_path = if route_info.path == "/" {
494                normalized_prefix.clone()
495            } else {
496                format!("{}{}", normalized_prefix, route_info.path)
497            };
498
499            // Store the MethodRouter for future nesting
500            self.method_routers
501                .insert(prefixed_matchit_path.clone(), method_router.clone());
502
503            // Try to insert into the matchit router
504            match self
505                .inner
506                .insert(prefixed_matchit_path.clone(), method_router)
507            {
508                Ok(_) => {
509                    // Track the registered route
510                    self.registered_routes.insert(
511                        prefixed_matchit_path,
512                        RouteInfo {
513                            path: prefixed_display_path,
514                            methods: route_info.methods,
515                        },
516                    );
517                }
518                Err(e) => {
519                    // Remove the method_router we just added since registration failed
520                    self.method_routers.remove(&prefixed_matchit_path);
521
522                    // Find the existing conflicting route
523                    let existing_path = self
524                        .find_conflicting_route(&prefixed_matchit_path)
525                        .map(|info| info.path.clone())
526                        .unwrap_or_else(|| "<unknown>".to_string());
527
528                    let conflict_error = RouteConflictError {
529                        new_path: prefixed_display_path,
530                        method: route_info.methods.first().cloned(),
531                        existing_path,
532                        details: e.to_string(),
533                    };
534
535                    panic!("{}", conflict_error);
536                }
537            }
538        }
539
540        self
541    }
542
543    /// Merge state from another router into this one
544    ///
545    /// This method allows explicit state merging when nesting routers.
546    /// Parent state takes precedence - if the parent already has state of type S,
547    /// the nested state is ignored.
548    ///
549    /// # Example
550    ///
551    /// ```rust,ignore
552    /// #[derive(Clone)]
553    /// struct DbPool(String);
554    ///
555    /// let nested = Router::new().state(DbPool("nested".to_string()));
556    /// let parent = Router::new()
557    ///     .merge_state::<DbPool>(&nested); // Adds DbPool from nested
558    /// ```
559    pub fn merge_state<S: Clone + Send + Sync + 'static>(mut self, other: &Router) -> Self {
560        let type_id = std::any::TypeId::of::<S>();
561
562        // Parent wins - only merge if parent doesn't have this state type
563        if !self.state_type_ids.contains(&type_id) {
564            // Try to get the state from the other router
565            if let Some(state) = other.state.get::<S>() {
566                let extensions = Arc::make_mut(&mut self.state);
567                extensions.insert(state.clone());
568                self.state_type_ids.push(type_id);
569            }
570        }
571
572        self
573    }
574
575    /// Match a request and return the handler + params
576    pub(crate) fn match_route(&self, path: &str, method: &Method) -> RouteMatch<'_> {
577        match self.inner.at(path) {
578            Ok(matched) => {
579                let method_router = matched.value;
580
581                if let Some(handler) = method_router.get_handler(method) {
582                    // Use stack-optimized PathParams (avoids heap allocation for ≤4 params)
583                    let params: PathParams = matched
584                        .params
585                        .iter()
586                        .map(|(k, v)| (k.to_string(), v.to_string()))
587                        .collect();
588
589                    RouteMatch::Found { handler, params }
590                } else {
591                    RouteMatch::MethodNotAllowed {
592                        allowed: method_router.allowed_methods(),
593                    }
594                }
595            }
596            Err(_) => RouteMatch::NotFound,
597        }
598    }
599
600    /// Get shared state
601    pub(crate) fn state_ref(&self) -> Arc<Extensions> {
602        self.state.clone()
603    }
604
605    /// Get registered routes (for testing and debugging)
606    pub fn registered_routes(&self) -> &HashMap<String, RouteInfo> {
607        &self.registered_routes
608    }
609
610    /// Get method routers (for OpenAPI integration during nesting)
611    pub fn method_routers(&self) -> &HashMap<String, MethodRouter> {
612        &self.method_routers
613    }
614}
615
616impl Default for Router {
617    fn default() -> Self {
618        Self::new()
619    }
620}
621
622/// Result of route matching
623pub(crate) enum RouteMatch<'a> {
624    Found {
625        handler: &'a BoxedHandler,
626        params: PathParams,
627    },
628    NotFound,
629    MethodNotAllowed {
630        allowed: Vec<Method>,
631    },
632}
633
634/// Convert {param} style to :param for matchit
635fn convert_path_params(path: &str) -> String {
636    let mut result = String::with_capacity(path.len());
637
638    for ch in path.chars() {
639        match ch {
640            '{' => {
641                result.push(':');
642            }
643            '}' => {
644                // Skip closing brace
645            }
646            _ => {
647                result.push(ch);
648            }
649        }
650    }
651
652    result
653}
654
655/// Normalize a path for conflict comparison by replacing parameter names with a placeholder
656fn normalize_path_for_comparison(path: &str) -> String {
657    let mut result = String::with_capacity(path.len());
658    let mut in_param = false;
659
660    for ch in path.chars() {
661        match ch {
662            ':' => {
663                in_param = true;
664                result.push_str(":_");
665            }
666            '/' => {
667                in_param = false;
668                result.push('/');
669            }
670            _ if in_param => {
671                // Skip parameter name characters
672            }
673            _ => {
674                result.push(ch);
675            }
676        }
677    }
678
679    result
680}
681
682/// Normalize a prefix for router nesting.
683///
684/// Ensures the prefix:
685/// - Starts with exactly one leading slash
686/// - Has no trailing slash (unless it's just "/")
687/// - Has no double slashes
688///
689/// # Examples
690///
691/// ```ignore
692/// assert_eq!(normalize_prefix("api"), "/api");
693/// assert_eq!(normalize_prefix("/api"), "/api");
694/// assert_eq!(normalize_prefix("/api/"), "/api");
695/// assert_eq!(normalize_prefix("//api//"), "/api");
696/// assert_eq!(normalize_prefix(""), "/");
697/// ```
698pub(crate) fn normalize_prefix(prefix: &str) -> String {
699    // Handle empty string
700    if prefix.is_empty() {
701        return "/".to_string();
702    }
703
704    // Split by slashes and filter out empty segments (handles multiple slashes)
705    let segments: Vec<&str> = prefix.split('/').filter(|s| !s.is_empty()).collect();
706
707    // If no segments after filtering, return root
708    if segments.is_empty() {
709        return "/".to_string();
710    }
711
712    // Build the normalized prefix with leading slash
713    let mut result = String::with_capacity(prefix.len() + 1);
714    for segment in segments {
715        result.push('/');
716        result.push_str(segment);
717    }
718
719    result
720}
721
722#[cfg(test)]
723mod tests {
724    use super::*;
725
726    #[test]
727    fn test_convert_path_params() {
728        assert_eq!(convert_path_params("/users/{id}"), "/users/:id");
729        assert_eq!(
730            convert_path_params("/users/{user_id}/posts/{post_id}"),
731            "/users/:user_id/posts/:post_id"
732        );
733        assert_eq!(convert_path_params("/static/path"), "/static/path");
734    }
735
736    #[test]
737    fn test_normalize_path_for_comparison() {
738        assert_eq!(normalize_path_for_comparison("/users/:id"), "/users/:_");
739        assert_eq!(
740            normalize_path_for_comparison("/users/:user_id"),
741            "/users/:_"
742        );
743        assert_eq!(
744            normalize_path_for_comparison("/users/:id/posts/:post_id"),
745            "/users/:_/posts/:_"
746        );
747        assert_eq!(
748            normalize_path_for_comparison("/static/path"),
749            "/static/path"
750        );
751    }
752
753    #[test]
754    fn test_normalize_prefix() {
755        // Basic cases
756        assert_eq!(normalize_prefix("api"), "/api");
757        assert_eq!(normalize_prefix("/api"), "/api");
758        assert_eq!(normalize_prefix("/api/"), "/api");
759        assert_eq!(normalize_prefix("api/"), "/api");
760
761        // Multiple segments
762        assert_eq!(normalize_prefix("api/v1"), "/api/v1");
763        assert_eq!(normalize_prefix("/api/v1"), "/api/v1");
764        assert_eq!(normalize_prefix("/api/v1/"), "/api/v1");
765
766        // Edge cases: empty and root
767        assert_eq!(normalize_prefix(""), "/");
768        assert_eq!(normalize_prefix("/"), "/");
769
770        // Multiple slashes
771        assert_eq!(normalize_prefix("//api"), "/api");
772        assert_eq!(normalize_prefix("api//v1"), "/api/v1");
773        assert_eq!(normalize_prefix("//api//v1//"), "/api/v1");
774        assert_eq!(normalize_prefix("///"), "/");
775    }
776
777    #[test]
778    #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
779    fn test_route_conflict_detection() {
780        async fn handler1() -> &'static str {
781            "handler1"
782        }
783        async fn handler2() -> &'static str {
784            "handler2"
785        }
786
787        let _router = Router::new()
788            .route("/users/{id}", get(handler1))
789            .route("/users/{user_id}", get(handler2)); // This should panic
790    }
791
792    #[test]
793    fn test_no_conflict_different_paths() {
794        async fn handler1() -> &'static str {
795            "handler1"
796        }
797        async fn handler2() -> &'static str {
798            "handler2"
799        }
800
801        let router = Router::new()
802            .route("/users/{id}", get(handler1))
803            .route("/users/{id}/profile", get(handler2));
804
805        assert_eq!(router.registered_routes().len(), 2);
806    }
807
808    #[test]
809    fn test_route_info_tracking() {
810        async fn handler() -> &'static str {
811            "handler"
812        }
813
814        let router = Router::new().route("/users/{id}", get(handler));
815
816        let routes = router.registered_routes();
817        assert_eq!(routes.len(), 1);
818
819        let info = routes.get("/users/:id").unwrap();
820        assert_eq!(info.path, "/users/{id}");
821        assert_eq!(info.methods.len(), 1);
822        assert_eq!(info.methods[0], Method::GET);
823    }
824
825    #[test]
826    fn test_basic_router_nesting() {
827        async fn list_users() -> &'static str {
828            "list users"
829        }
830        async fn get_user() -> &'static str {
831            "get user"
832        }
833
834        let users_router = Router::new()
835            .route("/", get(list_users))
836            .route("/{id}", get(get_user));
837
838        let app = Router::new().nest("/api/users", users_router);
839
840        let routes = app.registered_routes();
841        assert_eq!(routes.len(), 2);
842
843        // Check that routes are registered with prefix
844        assert!(routes.contains_key("/api/users"));
845        assert!(routes.contains_key("/api/users/:id"));
846
847        // Check display paths
848        let list_info = routes.get("/api/users").unwrap();
849        assert_eq!(list_info.path, "/api/users");
850
851        let get_info = routes.get("/api/users/:id").unwrap();
852        assert_eq!(get_info.path, "/api/users/{id}");
853    }
854
855    #[test]
856    fn test_nested_route_matching() {
857        async fn handler() -> &'static str {
858            "handler"
859        }
860
861        let users_router = Router::new().route("/{id}", get(handler));
862
863        let app = Router::new().nest("/api/users", users_router);
864
865        // Test that the route can be matched
866        match app.match_route("/api/users/123", &Method::GET) {
867            RouteMatch::Found { params, .. } => {
868                assert_eq!(params.get("id"), Some(&"123".to_string()));
869            }
870            _ => panic!("Route should be found"),
871        }
872    }
873
874    #[test]
875    fn test_nested_route_matching_multiple_params() {
876        async fn handler() -> &'static str {
877            "handler"
878        }
879
880        let posts_router = Router::new().route("/{user_id}/posts/{post_id}", get(handler));
881
882        let app = Router::new().nest("/api", posts_router);
883
884        // Test that multiple parameters are correctly extracted
885        match app.match_route("/api/42/posts/100", &Method::GET) {
886            RouteMatch::Found { params, .. } => {
887                assert_eq!(params.get("user_id"), Some(&"42".to_string()));
888                assert_eq!(params.get("post_id"), Some(&"100".to_string()));
889            }
890            _ => panic!("Route should be found"),
891        }
892    }
893
894    #[test]
895    fn test_nested_route_matching_static_path() {
896        async fn handler() -> &'static str {
897            "handler"
898        }
899
900        let health_router = Router::new().route("/health", get(handler));
901
902        let app = Router::new().nest("/api/v1", health_router);
903
904        // Test that static paths are correctly matched
905        match app.match_route("/api/v1/health", &Method::GET) {
906            RouteMatch::Found { params, .. } => {
907                assert!(params.is_empty(), "Static path should have no params");
908            }
909            _ => panic!("Route should be found"),
910        }
911    }
912
913    #[test]
914    fn test_nested_route_not_found() {
915        async fn handler() -> &'static str {
916            "handler"
917        }
918
919        let users_router = Router::new().route("/users", get(handler));
920
921        let app = Router::new().nest("/api", users_router);
922
923        // Test that non-existent paths return NotFound
924        match app.match_route("/api/posts", &Method::GET) {
925            RouteMatch::NotFound => {
926                // Expected
927            }
928            _ => panic!("Route should not be found"),
929        }
930
931        // Test that wrong prefix returns NotFound
932        match app.match_route("/v2/users", &Method::GET) {
933            RouteMatch::NotFound => {
934                // Expected
935            }
936            _ => panic!("Route with wrong prefix should not be found"),
937        }
938    }
939
940    #[test]
941    fn test_nested_route_method_not_allowed() {
942        async fn handler() -> &'static str {
943            "handler"
944        }
945
946        let users_router = Router::new().route("/users", get(handler));
947
948        let app = Router::new().nest("/api", users_router);
949
950        // Test that wrong method returns MethodNotAllowed
951        match app.match_route("/api/users", &Method::POST) {
952            RouteMatch::MethodNotAllowed { allowed } => {
953                assert!(allowed.contains(&Method::GET));
954                assert!(!allowed.contains(&Method::POST));
955            }
956            _ => panic!("Should return MethodNotAllowed"),
957        }
958    }
959
960    #[test]
961    fn test_nested_route_multiple_methods() {
962        async fn get_handler() -> &'static str {
963            "get"
964        }
965        async fn post_handler() -> &'static str {
966            "post"
967        }
968
969        // Create a method router with both GET and POST
970        let get_router = get(get_handler);
971        let post_router = post(post_handler);
972        let mut combined = MethodRouter::new();
973        for (method, handler) in get_router.handlers {
974            combined.handlers.insert(method, handler);
975        }
976        for (method, handler) in post_router.handlers {
977            combined.handlers.insert(method, handler);
978        }
979
980        let users_router = Router::new().route("/users", combined);
981        let app = Router::new().nest("/api", users_router);
982
983        // Both GET and POST should work
984        match app.match_route("/api/users", &Method::GET) {
985            RouteMatch::Found { .. } => {}
986            _ => panic!("GET should be found"),
987        }
988
989        match app.match_route("/api/users", &Method::POST) {
990            RouteMatch::Found { .. } => {}
991            _ => panic!("POST should be found"),
992        }
993
994        // DELETE should return MethodNotAllowed with GET and POST in allowed
995        match app.match_route("/api/users", &Method::DELETE) {
996            RouteMatch::MethodNotAllowed { allowed } => {
997                assert!(allowed.contains(&Method::GET));
998                assert!(allowed.contains(&Method::POST));
999            }
1000            _ => panic!("DELETE should return MethodNotAllowed"),
1001        }
1002    }
1003
1004    #[test]
1005    fn test_nested_router_prefix_normalization() {
1006        async fn handler() -> &'static str {
1007            "handler"
1008        }
1009
1010        // Test various prefix formats
1011        let router1 = Router::new().route("/test", get(handler));
1012        let app1 = Router::new().nest("api", router1);
1013        assert!(app1.registered_routes().contains_key("/api/test"));
1014
1015        let router2 = Router::new().route("/test", get(handler));
1016        let app2 = Router::new().nest("/api/", router2);
1017        assert!(app2.registered_routes().contains_key("/api/test"));
1018
1019        let router3 = Router::new().route("/test", get(handler));
1020        let app3 = Router::new().nest("//api//", router3);
1021        assert!(app3.registered_routes().contains_key("/api/test"));
1022    }
1023
1024    #[test]
1025    fn test_state_tracking() {
1026        #[derive(Clone)]
1027        struct MyState(String);
1028
1029        let router = Router::new().state(MyState("test".to_string()));
1030
1031        assert!(router.has_state::<MyState>());
1032        assert!(!router.has_state::<String>());
1033    }
1034
1035    #[test]
1036    fn test_state_merge_nested_only() {
1037        #[derive(Clone, PartialEq, Debug)]
1038        struct NestedState(String);
1039
1040        async fn handler() -> &'static str {
1041            "handler"
1042        }
1043
1044        // Create a router with state to use as source for merging
1045        let state_source = Router::new().state(NestedState("nested".to_string()));
1046
1047        let nested = Router::new().route("/test", get(handler));
1048
1049        let parent = Router::new()
1050            .nest("/api", nested)
1051            .merge_state::<NestedState>(&state_source);
1052
1053        // Parent should now have the nested state
1054        assert!(parent.has_state::<NestedState>());
1055
1056        // Verify the state value
1057        let state = parent.state.get::<NestedState>().unwrap();
1058        assert_eq!(state.0, "nested");
1059    }
1060
1061    #[test]
1062    fn test_state_merge_parent_wins() {
1063        #[derive(Clone, PartialEq, Debug)]
1064        struct SharedState(String);
1065
1066        async fn handler() -> &'static str {
1067            "handler"
1068        }
1069
1070        // Create a router with state to use as source for merging
1071        let state_source = Router::new().state(SharedState("nested".to_string()));
1072
1073        let nested = Router::new().route("/test", get(handler));
1074
1075        let parent = Router::new()
1076            .state(SharedState("parent".to_string()))
1077            .nest("/api", nested)
1078            .merge_state::<SharedState>(&state_source);
1079
1080        // Parent should still have its own state (parent wins)
1081        assert!(parent.has_state::<SharedState>());
1082
1083        // Verify the state value is from parent
1084        let state = parent.state.get::<SharedState>().unwrap();
1085        assert_eq!(state.0, "parent");
1086    }
1087
1088    #[test]
1089    fn test_state_type_ids_merged_on_nest() {
1090        #[derive(Clone)]
1091        struct NestedState(String);
1092
1093        async fn handler() -> &'static str {
1094            "handler"
1095        }
1096
1097        let nested = Router::new()
1098            .route("/test", get(handler))
1099            .state(NestedState("nested".to_string()));
1100
1101        let parent = Router::new().nest("/api", nested);
1102
1103        // Parent should track the nested state type ID
1104        assert!(parent
1105            .state_type_ids()
1106            .contains(&std::any::TypeId::of::<NestedState>()));
1107    }
1108
1109    #[test]
1110    #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
1111    fn test_nested_route_conflict_with_existing_route() {
1112        async fn handler1() -> &'static str {
1113            "handler1"
1114        }
1115        async fn handler2() -> &'static str {
1116            "handler2"
1117        }
1118
1119        // Create a parent router with an existing route
1120        let parent = Router::new().route("/api/users/{id}", get(handler1));
1121
1122        // Create a nested router with a conflicting route
1123        let nested = Router::new().route("/{user_id}", get(handler2));
1124
1125        // This should panic because /api/users/{id} conflicts with /api/users/{user_id}
1126        let _app = parent.nest("/api/users", nested);
1127    }
1128
1129    #[test]
1130    #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
1131    fn test_nested_route_conflict_same_path_different_param_names() {
1132        async fn handler1() -> &'static str {
1133            "handler1"
1134        }
1135        async fn handler2() -> &'static str {
1136            "handler2"
1137        }
1138
1139        // Create two nested routers with same path structure but different param names
1140        let nested1 = Router::new().route("/{id}", get(handler1));
1141        let nested2 = Router::new().route("/{user_id}", get(handler2));
1142
1143        // Nest both under the same prefix - should conflict
1144        let _app = Router::new()
1145            .nest("/api/users", nested1)
1146            .nest("/api/users", nested2);
1147    }
1148
1149    #[test]
1150    fn test_nested_route_conflict_error_contains_both_paths() {
1151        use std::panic::{catch_unwind, AssertUnwindSafe};
1152
1153        async fn handler1() -> &'static str {
1154            "handler1"
1155        }
1156        async fn handler2() -> &'static str {
1157            "handler2"
1158        }
1159
1160        let result = catch_unwind(AssertUnwindSafe(|| {
1161            let parent = Router::new().route("/api/users/{id}", get(handler1));
1162            let nested = Router::new().route("/{user_id}", get(handler2));
1163            let _app = parent.nest("/api/users", nested);
1164        }));
1165
1166        assert!(result.is_err(), "Should have panicked due to conflict");
1167
1168        if let Err(panic_info) = result {
1169            if let Some(msg) = panic_info.downcast_ref::<String>() {
1170                assert!(
1171                    msg.contains("ROUTE CONFLICT DETECTED"),
1172                    "Error should contain 'ROUTE CONFLICT DETECTED'"
1173                );
1174                assert!(
1175                    msg.contains("Existing:") && msg.contains("New:"),
1176                    "Error should contain both 'Existing:' and 'New:' labels"
1177                );
1178                assert!(
1179                    msg.contains("How to resolve:"),
1180                    "Error should contain resolution guidance"
1181                );
1182            }
1183        }
1184    }
1185
1186    #[test]
1187    fn test_nested_routes_no_conflict_different_prefixes() {
1188        async fn handler1() -> &'static str {
1189            "handler1"
1190        }
1191        async fn handler2() -> &'static str {
1192            "handler2"
1193        }
1194
1195        // Create two nested routers with same internal paths but different prefixes
1196        let nested1 = Router::new().route("/{id}", get(handler1));
1197        let nested2 = Router::new().route("/{id}", get(handler2));
1198
1199        // Nest under different prefixes - should NOT conflict
1200        let app = Router::new()
1201            .nest("/api/users", nested1)
1202            .nest("/api/posts", nested2);
1203
1204        assert_eq!(app.registered_routes().len(), 2);
1205        assert!(app.registered_routes().contains_key("/api/users/:id"));
1206        assert!(app.registered_routes().contains_key("/api/posts/:id"));
1207    }
1208
1209    // **Feature: router-nesting, Property 4: Multiple Router Composition**
1210    // Tests for nesting multiple routers under different prefixes
1211    // **Validates: Requirements 1.5**
1212
1213    #[test]
1214    fn test_multiple_router_composition_all_routes_registered() {
1215        async fn users_list() -> &'static str {
1216            "users list"
1217        }
1218        async fn users_get() -> &'static str {
1219            "users get"
1220        }
1221        async fn posts_list() -> &'static str {
1222            "posts list"
1223        }
1224        async fn posts_get() -> &'static str {
1225            "posts get"
1226        }
1227        async fn comments_list() -> &'static str {
1228            "comments list"
1229        }
1230
1231        // Create multiple sub-routers with different routes
1232        let users_router = Router::new()
1233            .route("/", get(users_list))
1234            .route("/{id}", get(users_get));
1235
1236        let posts_router = Router::new()
1237            .route("/", get(posts_list))
1238            .route("/{id}", get(posts_get));
1239
1240        let comments_router = Router::new().route("/", get(comments_list));
1241
1242        // Nest all routers under different prefixes
1243        let app = Router::new()
1244            .nest("/api/users", users_router)
1245            .nest("/api/posts", posts_router)
1246            .nest("/api/comments", comments_router);
1247
1248        // Verify all routes are registered (2 + 2 + 1 = 5 routes)
1249        let routes = app.registered_routes();
1250        assert_eq!(routes.len(), 5, "Should have 5 routes registered");
1251
1252        // Verify users routes
1253        assert!(
1254            routes.contains_key("/api/users"),
1255            "Should have /api/users route"
1256        );
1257        assert!(
1258            routes.contains_key("/api/users/:id"),
1259            "Should have /api/users/:id route"
1260        );
1261
1262        // Verify posts routes
1263        assert!(
1264            routes.contains_key("/api/posts"),
1265            "Should have /api/posts route"
1266        );
1267        assert!(
1268            routes.contains_key("/api/posts/:id"),
1269            "Should have /api/posts/:id route"
1270        );
1271
1272        // Verify comments routes
1273        assert!(
1274            routes.contains_key("/api/comments"),
1275            "Should have /api/comments route"
1276        );
1277    }
1278
1279    #[test]
1280    fn test_multiple_router_composition_no_interference() {
1281        async fn users_handler() -> &'static str {
1282            "users"
1283        }
1284        async fn posts_handler() -> &'static str {
1285            "posts"
1286        }
1287        async fn admin_handler() -> &'static str {
1288            "admin"
1289        }
1290
1291        // Create routers with same internal structure but different prefixes
1292        let users_router = Router::new()
1293            .route("/list", get(users_handler))
1294            .route("/{id}", get(users_handler));
1295
1296        let posts_router = Router::new()
1297            .route("/list", get(posts_handler))
1298            .route("/{id}", get(posts_handler));
1299
1300        let admin_router = Router::new()
1301            .route("/list", get(admin_handler))
1302            .route("/{id}", get(admin_handler));
1303
1304        // Nest all routers
1305        let app = Router::new()
1306            .nest("/api/v1/users", users_router)
1307            .nest("/api/v1/posts", posts_router)
1308            .nest("/admin", admin_router);
1309
1310        // Verify all routes are registered (2 + 2 + 2 = 6 routes)
1311        let routes = app.registered_routes();
1312        assert_eq!(routes.len(), 6, "Should have 6 routes registered");
1313
1314        // Verify each prefix group has its routes
1315        assert!(routes.contains_key("/api/v1/users/list"));
1316        assert!(routes.contains_key("/api/v1/users/:id"));
1317        assert!(routes.contains_key("/api/v1/posts/list"));
1318        assert!(routes.contains_key("/api/v1/posts/:id"));
1319        assert!(routes.contains_key("/admin/list"));
1320        assert!(routes.contains_key("/admin/:id"));
1321
1322        // Verify routes are matchable and don't interfere with each other
1323        match app.match_route("/api/v1/users/list", &Method::GET) {
1324            RouteMatch::Found { params, .. } => {
1325                assert!(params.is_empty(), "Static path should have no params");
1326            }
1327            _ => panic!("Should find /api/v1/users/list"),
1328        }
1329
1330        match app.match_route("/api/v1/posts/123", &Method::GET) {
1331            RouteMatch::Found { params, .. } => {
1332                assert_eq!(params.get("id"), Some(&"123".to_string()));
1333            }
1334            _ => panic!("Should find /api/v1/posts/123"),
1335        }
1336
1337        match app.match_route("/admin/456", &Method::GET) {
1338            RouteMatch::Found { params, .. } => {
1339                assert_eq!(params.get("id"), Some(&"456".to_string()));
1340            }
1341            _ => panic!("Should find /admin/456"),
1342        }
1343    }
1344
1345    #[test]
1346    fn test_multiple_router_composition_with_multiple_methods() {
1347        async fn get_handler() -> &'static str {
1348            "get"
1349        }
1350        async fn post_handler() -> &'static str {
1351            "post"
1352        }
1353        async fn put_handler() -> &'static str {
1354            "put"
1355        }
1356
1357        // Create routers with multiple HTTP methods
1358        // Combine GET and POST for users root
1359        let get_router = get(get_handler);
1360        let post_router = post(post_handler);
1361        let mut users_root_combined = MethodRouter::new();
1362        for (method, handler) in get_router.handlers {
1363            users_root_combined.handlers.insert(method, handler);
1364        }
1365        for (method, handler) in post_router.handlers {
1366            users_root_combined.handlers.insert(method, handler);
1367        }
1368
1369        // Combine GET and PUT for users/{id}
1370        let get_router2 = get(get_handler);
1371        let put_router = put(put_handler);
1372        let mut users_id_combined = MethodRouter::new();
1373        for (method, handler) in get_router2.handlers {
1374            users_id_combined.handlers.insert(method, handler);
1375        }
1376        for (method, handler) in put_router.handlers {
1377            users_id_combined.handlers.insert(method, handler);
1378        }
1379
1380        let users_router = Router::new()
1381            .route("/", users_root_combined)
1382            .route("/{id}", users_id_combined);
1383
1384        // Combine GET and POST for posts root
1385        let get_router3 = get(get_handler);
1386        let post_router2 = post(post_handler);
1387        let mut posts_root_combined = MethodRouter::new();
1388        for (method, handler) in get_router3.handlers {
1389            posts_root_combined.handlers.insert(method, handler);
1390        }
1391        for (method, handler) in post_router2.handlers {
1392            posts_root_combined.handlers.insert(method, handler);
1393        }
1394
1395        let posts_router = Router::new().route("/", posts_root_combined);
1396
1397        // Nest routers
1398        let app = Router::new()
1399            .nest("/users", users_router)
1400            .nest("/posts", posts_router);
1401
1402        // Verify routes are registered
1403        let routes = app.registered_routes();
1404        assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1405
1406        // Verify methods are preserved for users routes
1407        let users_root = routes.get("/users").unwrap();
1408        assert!(users_root.methods.contains(&Method::GET));
1409        assert!(users_root.methods.contains(&Method::POST));
1410
1411        let users_id = routes.get("/users/:id").unwrap();
1412        assert!(users_id.methods.contains(&Method::GET));
1413        assert!(users_id.methods.contains(&Method::PUT));
1414
1415        // Verify methods are preserved for posts routes
1416        let posts_root = routes.get("/posts").unwrap();
1417        assert!(posts_root.methods.contains(&Method::GET));
1418        assert!(posts_root.methods.contains(&Method::POST));
1419
1420        // Verify route matching works for all methods
1421        match app.match_route("/users", &Method::GET) {
1422            RouteMatch::Found { .. } => {}
1423            _ => panic!("GET /users should be found"),
1424        }
1425        match app.match_route("/users", &Method::POST) {
1426            RouteMatch::Found { .. } => {}
1427            _ => panic!("POST /users should be found"),
1428        }
1429        match app.match_route("/users/123", &Method::PUT) {
1430            RouteMatch::Found { .. } => {}
1431            _ => panic!("PUT /users/123 should be found"),
1432        }
1433    }
1434
1435    #[test]
1436    fn test_multiple_router_composition_deep_nesting() {
1437        async fn handler() -> &'static str {
1438            "handler"
1439        }
1440
1441        // Create nested routers at different depth levels
1442        let deep_router = Router::new().route("/action", get(handler));
1443
1444        let mid_router = Router::new().route("/info", get(handler));
1445
1446        let shallow_router = Router::new().route("/status", get(handler));
1447
1448        // Nest at different depths
1449        let app = Router::new()
1450            .nest("/api/v1/resources/items", deep_router)
1451            .nest("/api/v1/resources", mid_router)
1452            .nest("/api", shallow_router);
1453
1454        // Verify all routes are registered
1455        let routes = app.registered_routes();
1456        assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1457
1458        assert!(routes.contains_key("/api/v1/resources/items/action"));
1459        assert!(routes.contains_key("/api/v1/resources/info"));
1460        assert!(routes.contains_key("/api/status"));
1461
1462        // Verify all routes are matchable
1463        match app.match_route("/api/v1/resources/items/action", &Method::GET) {
1464            RouteMatch::Found { .. } => {}
1465            _ => panic!("Should find deep route"),
1466        }
1467        match app.match_route("/api/v1/resources/info", &Method::GET) {
1468            RouteMatch::Found { .. } => {}
1469            _ => panic!("Should find mid route"),
1470        }
1471        match app.match_route("/api/status", &Method::GET) {
1472            RouteMatch::Found { .. } => {}
1473            _ => panic!("Should find shallow route"),
1474        }
1475    }
1476}
1477
1478#[cfg(test)]
1479mod property_tests {
1480    use super::*;
1481    use proptest::prelude::*;
1482    use std::panic::{catch_unwind, AssertUnwindSafe};
1483
1484    // **Feature: router-nesting, Property 2: Prefix Normalization**
1485    //
1486    // For any prefix string (with or without leading/trailing slashes), the normalized
1487    // prefix should start with exactly one slash and have no trailing slash, and all
1488    // nested routes should have properly formed paths without double slashes.
1489    //
1490    // **Validates: Requirements 1.2, 1.3**
1491    proptest! {
1492        #![proptest_config(ProptestConfig::with_cases(100))]
1493
1494        /// Property: Normalized prefix always starts with exactly one slash
1495        ///
1496        /// For any input prefix, the normalized result should always start with
1497        /// exactly one leading slash.
1498        #[test]
1499        fn prop_normalized_prefix_starts_with_single_slash(
1500            // Generate prefix with optional leading slashes
1501            leading_slashes in prop::collection::vec(Just('/'), 0..5),
1502            segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 0..4),
1503            trailing_slashes in prop::collection::vec(Just('/'), 0..5),
1504        ) {
1505            // Build the input prefix
1506            let mut prefix = String::new();
1507            for _ in &leading_slashes {
1508                prefix.push('/');
1509            }
1510            for (i, segment) in segments.iter().enumerate() {
1511                if i > 0 {
1512                    prefix.push('/');
1513                }
1514                prefix.push_str(segment);
1515            }
1516            for _ in &trailing_slashes {
1517                prefix.push('/');
1518            }
1519
1520            let normalized = normalize_prefix(&prefix);
1521
1522            // Property 1: Always starts with exactly one slash
1523            prop_assert!(
1524                normalized.starts_with('/'),
1525                "Normalized prefix '{}' should start with '/', input was '{}'",
1526                normalized, prefix
1527            );
1528
1529            // Property 2: No double slashes at the start
1530            prop_assert!(
1531                !normalized.starts_with("//"),
1532                "Normalized prefix '{}' should not start with '//', input was '{}'",
1533                normalized, prefix
1534            );
1535        }
1536
1537        /// Property: Normalized prefix has no trailing slash (unless root)
1538        ///
1539        /// For any input prefix with non-empty segments, the normalized result
1540        /// should have no trailing slash.
1541        #[test]
1542        fn prop_normalized_prefix_no_trailing_slash(
1543            segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
1544            trailing_slashes in prop::collection::vec(Just('/'), 0..5),
1545        ) {
1546            // Build the input prefix with segments
1547            let mut prefix = String::from("/");
1548            for (i, segment) in segments.iter().enumerate() {
1549                if i > 0 {
1550                    prefix.push('/');
1551                }
1552                prefix.push_str(segment);
1553            }
1554            for _ in &trailing_slashes {
1555                prefix.push('/');
1556            }
1557
1558            let normalized = normalize_prefix(&prefix);
1559
1560            // Property: No trailing slash when there are segments
1561            prop_assert!(
1562                !normalized.ends_with('/'),
1563                "Normalized prefix '{}' should not end with '/', input was '{}'",
1564                normalized, prefix
1565            );
1566        }
1567
1568        /// Property: Normalized prefix has no double slashes
1569        ///
1570        /// For any input prefix, the normalized result should never contain
1571        /// consecutive slashes.
1572        #[test]
1573        fn prop_normalized_prefix_no_double_slashes(
1574            // Generate prefix with random slashes between segments
1575            segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
1576            extra_slashes in prop::collection::vec(0..4usize, 1..4),
1577        ) {
1578            // Build the input prefix with extra slashes between segments
1579            let mut prefix = String::from("/");
1580            for (i, segment) in segments.iter().enumerate() {
1581                if i > 0 {
1582                    // Add extra slashes between segments
1583                    let num_slashes = extra_slashes.get(i).copied().unwrap_or(1);
1584                    for _ in 0..=num_slashes {
1585                        prefix.push('/');
1586                    }
1587                }
1588                prefix.push_str(segment);
1589            }
1590
1591            let normalized = normalize_prefix(&prefix);
1592
1593            // Property: No consecutive slashes
1594            prop_assert!(
1595                !normalized.contains("//"),
1596                "Normalized prefix '{}' should not contain '//', input was '{}'",
1597                normalized, prefix
1598            );
1599        }
1600
1601        /// Property: Prefix normalization preserves segment content
1602        ///
1603        /// For any input prefix, all non-empty segments should be preserved
1604        /// in the normalized output in the same order.
1605        #[test]
1606        fn prop_normalized_prefix_preserves_segments(
1607            segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..4),
1608        ) {
1609            // Build the input prefix
1610            let prefix = format!("/{}", segments.join("/"));
1611
1612            let normalized = normalize_prefix(&prefix);
1613
1614            // Extract segments from normalized prefix
1615            let normalized_segments: Vec<&str> = normalized
1616                .split('/')
1617                .filter(|s| !s.is_empty())
1618                .collect();
1619
1620            prop_assert_eq!(
1621                segments.len(),
1622                normalized_segments.len(),
1623                "Segment count should be preserved"
1624            );
1625
1626            for (original, normalized_seg) in segments.iter().zip(normalized_segments.iter()) {
1627                prop_assert_eq!(
1628                    original, normalized_seg,
1629                    "Segment content should be preserved"
1630                );
1631            }
1632        }
1633
1634        /// Property: Empty or slash-only input normalizes to root
1635        ///
1636        /// For any input that contains only slashes or is empty, the normalized
1637        /// result should be exactly "/".
1638        #[test]
1639        fn prop_empty_or_slashes_normalize_to_root(
1640            num_slashes in 0..10usize,
1641        ) {
1642            let prefix: String = std::iter::repeat('/').take(num_slashes).collect();
1643
1644            let normalized = normalize_prefix(&prefix);
1645
1646            prop_assert_eq!(
1647                normalized, "/",
1648                "Empty or slash-only prefix '{}' should normalize to '/'",
1649                prefix
1650            );
1651        }
1652    }
1653
1654    // **Feature: router-nesting, Property 3: HTTP Method Preservation**
1655    //
1656    // For any router with routes having multiple HTTP methods, cloning the MethodRouter
1657    // should preserve all method handlers for each route.
1658    //
1659    // **Validates: Requirements 1.4**
1660    proptest! {
1661        #![proptest_config(ProptestConfig::with_cases(100))]
1662
1663        /// Property: Cloning a MethodRouter preserves all HTTP method handlers
1664        ///
1665        /// For any combination of HTTP methods registered on a MethodRouter,
1666        /// cloning should preserve all handlers and their associated methods.
1667        #[test]
1668        fn prop_method_router_clone_preserves_methods(
1669            // Generate a random subset of HTTP methods to register
1670            use_get in any::<bool>(),
1671            use_post in any::<bool>(),
1672            use_put in any::<bool>(),
1673            use_patch in any::<bool>(),
1674            use_delete in any::<bool>(),
1675        ) {
1676            // Ensure at least one method is selected
1677            prop_assume!(use_get || use_post || use_put || use_patch || use_delete);
1678
1679            // Build a MethodRouter with the selected methods
1680            let mut method_router = MethodRouter::new();
1681            let mut expected_methods: Vec<Method> = Vec::new();
1682
1683            async fn handler() -> &'static str { "handler" }
1684
1685            if use_get {
1686                method_router = get(handler);
1687                expected_methods.push(Method::GET);
1688            }
1689
1690            if use_post {
1691                let post_router = post(handler);
1692                for (method, handler) in post_router.handlers {
1693                    method_router.handlers.insert(method.clone(), handler);
1694                    if !expected_methods.contains(&method) {
1695                        expected_methods.push(method);
1696                    }
1697                }
1698            }
1699
1700            if use_put {
1701                let put_router = put(handler);
1702                for (method, handler) in put_router.handlers {
1703                    method_router.handlers.insert(method.clone(), handler);
1704                    if !expected_methods.contains(&method) {
1705                        expected_methods.push(method);
1706                    }
1707                }
1708            }
1709
1710            if use_patch {
1711                let patch_router = patch(handler);
1712                for (method, handler) in patch_router.handlers {
1713                    method_router.handlers.insert(method.clone(), handler);
1714                    if !expected_methods.contains(&method) {
1715                        expected_methods.push(method);
1716                    }
1717                }
1718            }
1719
1720            if use_delete {
1721                let delete_router = delete(handler);
1722                for (method, handler) in delete_router.handlers {
1723                    method_router.handlers.insert(method.clone(), handler);
1724                    if !expected_methods.contains(&method) {
1725                        expected_methods.push(method);
1726                    }
1727                }
1728            }
1729
1730            // Clone the MethodRouter
1731            let cloned_router = method_router.clone();
1732
1733            // Verify all methods are preserved in the clone
1734            let original_methods = method_router.allowed_methods();
1735            let cloned_methods = cloned_router.allowed_methods();
1736
1737            prop_assert_eq!(
1738                original_methods.len(),
1739                cloned_methods.len(),
1740                "Cloned router should have same number of methods"
1741            );
1742
1743            for method in &expected_methods {
1744                prop_assert!(
1745                    cloned_router.get_handler(method).is_some(),
1746                    "Cloned router should have handler for method {:?}",
1747                    method
1748                );
1749            }
1750
1751            // Verify handlers are accessible (not null/invalid)
1752            for method in &cloned_methods {
1753                prop_assert!(
1754                    cloned_router.get_handler(method).is_some(),
1755                    "Handler for {:?} should be accessible after clone",
1756                    method
1757                );
1758            }
1759        }
1760    }
1761
1762    // **Feature: router-nesting, Property 1: Route Registration with Prefix**
1763    //
1764    // For any router with routes and any valid prefix, nesting the router should
1765    // result in all routes being registered with the prefix prepended to their
1766    // original paths.
1767    //
1768    // **Validates: Requirements 1.1**
1769    proptest! {
1770        #![proptest_config(ProptestConfig::with_cases(100))]
1771
1772        /// Property: All nested routes are registered with prefix prepended
1773        ///
1774        /// For any router with routes and any valid prefix, nesting should result
1775        /// in all routes being registered with the prefix prepended.
1776        #[test]
1777        fn prop_nested_routes_have_prefix(
1778            // Generate prefix segments
1779            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1780            // Generate route path segments
1781            route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1782            has_param in any::<bool>(),
1783        ) {
1784            async fn handler() -> &'static str { "handler" }
1785
1786            // Build the prefix
1787            let prefix = format!("/{}", prefix_segments.join("/"));
1788
1789            // Build the route path
1790            let mut route_path = format!("/{}", route_segments.join("/"));
1791            if has_param {
1792                route_path.push_str("/{id}");
1793            }
1794
1795            // Create nested router and nest it
1796            let nested_router = Router::new().route(&route_path, get(handler));
1797            let app = Router::new().nest(&prefix, nested_router);
1798
1799            // Build expected prefixed path (matchit format)
1800            let expected_matchit_path = if has_param {
1801                format!("{}/{}/:id", prefix, route_segments.join("/"))
1802            } else {
1803                format!("{}/{}", prefix, route_segments.join("/"))
1804            };
1805
1806            let routes = app.registered_routes();
1807
1808            // Property: The prefixed route should exist
1809            prop_assert!(
1810                routes.contains_key(&expected_matchit_path),
1811                "Expected route '{}' not found. Available routes: {:?}",
1812                expected_matchit_path,
1813                routes.keys().collect::<Vec<_>>()
1814            );
1815
1816            // Property: The route info should have the correct display path
1817            let route_info = routes.get(&expected_matchit_path).unwrap();
1818            let expected_display_path = format!("{}{}", prefix, route_path);
1819            prop_assert_eq!(
1820                &route_info.path, &expected_display_path,
1821                "Display path should be prefix + original path"
1822            );
1823        }
1824
1825        /// Property: Number of routes is preserved after nesting
1826        ///
1827        /// For any router with N routes, nesting should result in exactly N routes
1828        /// being registered in the parent router (assuming no conflicts).
1829        #[test]
1830        fn prop_route_count_preserved_after_nesting(
1831            // Generate number of routes (1-3 to keep test fast)
1832            num_routes in 1..4usize,
1833            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1834        ) {
1835            async fn handler() -> &'static str { "handler" }
1836
1837            let prefix = format!("/{}", prefix_segments.join("/"));
1838
1839            // Create nested router with multiple routes
1840            let mut nested_router = Router::new();
1841            for i in 0..num_routes {
1842                let path = format!("/route{}", i);
1843                nested_router = nested_router.route(&path, get(handler));
1844            }
1845
1846            let app = Router::new().nest(&prefix, nested_router);
1847
1848            prop_assert_eq!(
1849                app.registered_routes().len(),
1850                num_routes,
1851                "Number of routes should be preserved after nesting"
1852            );
1853        }
1854
1855        /// Property: Nested routes are matchable
1856        ///
1857        /// For any nested route, a request to the prefixed path should match.
1858        #[test]
1859        fn prop_nested_routes_are_matchable(
1860            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1861            route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1862        ) {
1863            async fn handler() -> &'static str { "handler" }
1864
1865            let prefix = format!("/{}", prefix_segments.join("/"));
1866            let route_path = format!("/{}", route_segments.join("/"));
1867
1868            let nested_router = Router::new().route(&route_path, get(handler));
1869            let app = Router::new().nest(&prefix, nested_router);
1870
1871            // Build the full path to match
1872            let full_path = format!("{}{}", prefix, route_path);
1873
1874            // Property: The route should be matchable
1875            match app.match_route(&full_path, &Method::GET) {
1876                RouteMatch::Found { .. } => {
1877                    // Success - route was found
1878                }
1879                RouteMatch::NotFound => {
1880                    prop_assert!(false, "Route '{}' should be found but got NotFound", full_path);
1881                }
1882                RouteMatch::MethodNotAllowed { .. } => {
1883                    prop_assert!(false, "Route '{}' should be found but got MethodNotAllowed", full_path);
1884                }
1885            }
1886        }
1887    }
1888
1889    // **Feature: router-nesting, Property 9: State Merging**
1890    //
1891    // For any nested router with state, that state should be accessible via the
1892    // State extractor in handlers after nesting (assuming no type conflict with parent).
1893    //
1894    // **Validates: Requirements 3.1, 3.3**
1895    proptest! {
1896        #![proptest_config(ProptestConfig::with_cases(100))]
1897
1898        /// Property: State type IDs are merged from nested router
1899        ///
1900        /// For any nested router with state, the parent router should track
1901        /// the state type IDs after nesting.
1902        #[test]
1903        fn prop_state_type_ids_merged(
1904            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1905            has_nested_state in any::<bool>(),
1906        ) {
1907            #[derive(Clone)]
1908            struct TestState(i32);
1909
1910            async fn handler() -> &'static str { "handler" }
1911
1912            let prefix = format!("/{}", prefix_segments.join("/"));
1913
1914            let mut nested = Router::new().route("/test", get(handler));
1915            if has_nested_state {
1916                nested = nested.state(TestState(42));
1917            }
1918
1919            let parent = Router::new().nest(&prefix, nested);
1920
1921            // Property: If nested had state, parent should track the type ID
1922            if has_nested_state {
1923                prop_assert!(
1924                    parent.state_type_ids().contains(&std::any::TypeId::of::<TestState>()),
1925                    "Parent should track nested state type ID"
1926                );
1927            }
1928        }
1929
1930        /// Property: State merging adds nested state to parent
1931        ///
1932        /// For any nested router with state that the parent doesn't have,
1933        /// merge_state should add that state to the parent.
1934        #[test]
1935        fn prop_merge_state_adds_nested_state(
1936            state_value in any::<i32>(),
1937        ) {
1938            #[derive(Clone, PartialEq, Debug)]
1939            struct UniqueState(i32);
1940
1941            // Create a source router with state
1942            let source = Router::new().state(UniqueState(state_value));
1943
1944            // Create a parent without this state type
1945            let parent = Router::new().merge_state::<UniqueState>(&source);
1946
1947            // Property: Parent should now have the state
1948            prop_assert!(
1949                parent.has_state::<UniqueState>(),
1950                "Parent should have state after merge"
1951            );
1952
1953            // Property: State value should match
1954            let merged_state = parent.state.get::<UniqueState>().unwrap();
1955            prop_assert_eq!(
1956                merged_state.0, state_value,
1957                "Merged state value should match source"
1958            );
1959        }
1960    }
1961
1962    // **Feature: router-nesting, Property 10: State Precedence**
1963    //
1964    // For any parent and nested router both having state of the same type,
1965    // the parent's state value should be preserved after nesting.
1966    //
1967    // **Validates: Requirements 3.2**
1968    proptest! {
1969        #![proptest_config(ProptestConfig::with_cases(100))]
1970
1971        /// Property: Parent state takes precedence over nested state
1972        ///
1973        /// For any parent and nested router both having state of the same type,
1974        /// the parent's state value should be preserved after merge_state.
1975        #[test]
1976        fn prop_parent_state_takes_precedence(
1977            parent_value in any::<i32>(),
1978            nested_value in any::<i32>(),
1979        ) {
1980            // Ensure values are different to make the test meaningful
1981            prop_assume!(parent_value != nested_value);
1982
1983            #[derive(Clone, PartialEq, Debug)]
1984            struct SharedState(i32);
1985
1986            // Create source router with nested state
1987            let source = Router::new().state(SharedState(nested_value));
1988
1989            // Create parent with its own state
1990            let parent = Router::new()
1991                .state(SharedState(parent_value))
1992                .merge_state::<SharedState>(&source);
1993
1994            // Property: Parent should still have state
1995            prop_assert!(
1996                parent.has_state::<SharedState>(),
1997                "Parent should have state"
1998            );
1999
2000            // Property: Parent's state value should be preserved (parent wins)
2001            let final_state = parent.state.get::<SharedState>().unwrap();
2002            prop_assert_eq!(
2003                final_state.0, parent_value,
2004                "Parent state value should be preserved, not overwritten by nested"
2005            );
2006        }
2007
2008        /// Property: State precedence is consistent regardless of merge order
2009        ///
2010        /// For any parent with state, merging from a source with the same type
2011        /// should always preserve the parent's value.
2012        #[test]
2013        fn prop_state_precedence_consistent(
2014            parent_value in any::<i32>(),
2015            source1_value in any::<i32>(),
2016            source2_value in any::<i32>(),
2017        ) {
2018            #[derive(Clone, PartialEq, Debug)]
2019            struct ConsistentState(i32);
2020
2021            // Create multiple source routers
2022            let source1 = Router::new().state(ConsistentState(source1_value));
2023            let source2 = Router::new().state(ConsistentState(source2_value));
2024
2025            // Create parent with its own state and merge from multiple sources
2026            let parent = Router::new()
2027                .state(ConsistentState(parent_value))
2028                .merge_state::<ConsistentState>(&source1)
2029                .merge_state::<ConsistentState>(&source2);
2030
2031            // Property: Parent's original state should be preserved
2032            let final_state = parent.state.get::<ConsistentState>().unwrap();
2033            prop_assert_eq!(
2034                final_state.0, parent_value,
2035                "Parent state should be preserved after multiple merges"
2036            );
2037        }
2038    }
2039
2040    // **Feature: phase4-ergonomics-v1, Property 1: Route Conflict Detection**
2041    //
2042    // For any two routes with the same path and HTTP method registered on the same
2043    // RustApi instance, the system should detect the conflict and report an error
2044    // at startup time.
2045    //
2046    // **Validates: Requirements 1.2**
2047    proptest! {
2048        #![proptest_config(ProptestConfig::with_cases(100))]
2049
2050        /// Property: Routes with identical path structure but different parameter names conflict
2051        ///
2052        /// For any valid path with parameters, registering two routes with the same
2053        /// structure but different parameter names should be detected as a conflict.
2054        #[test]
2055        fn prop_same_structure_different_param_names_conflict(
2056            // Generate valid path segments
2057            segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
2058            // Generate two different parameter names
2059            param1 in "[a-z][a-z0-9]{0,5}",
2060            param2 in "[a-z][a-z0-9]{0,5}",
2061        ) {
2062            // Ensure param names are different
2063            prop_assume!(param1 != param2);
2064
2065            // Build two paths with same structure but different param names
2066            let mut path1 = String::from("/");
2067            let mut path2 = String::from("/");
2068
2069            for segment in &segments {
2070                path1.push_str(segment);
2071                path1.push('/');
2072                path2.push_str(segment);
2073                path2.push('/');
2074            }
2075
2076            path1.push('{');
2077            path1.push_str(&param1);
2078            path1.push('}');
2079
2080            path2.push('{');
2081            path2.push_str(&param2);
2082            path2.push('}');
2083
2084            // Try to register both routes - should panic
2085            let result = catch_unwind(AssertUnwindSafe(|| {
2086                async fn handler1() -> &'static str { "handler1" }
2087                async fn handler2() -> &'static str { "handler2" }
2088
2089                let _router = Router::new()
2090                    .route(&path1, get(handler1))
2091                    .route(&path2, get(handler2));
2092            }));
2093
2094            prop_assert!(
2095                result.is_err(),
2096                "Routes '{}' and '{}' should conflict but didn't",
2097                path1, path2
2098            );
2099        }
2100
2101        /// Property: Routes with different path structures don't conflict
2102        ///
2103        /// For any two paths with different structures (different number of segments
2104        /// or different static segments), they should not conflict.
2105        #[test]
2106        fn prop_different_structures_no_conflict(
2107            // Generate different path segments for two routes
2108            segments1 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2109            segments2 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2110            // Optional parameter at the end
2111            has_param1 in any::<bool>(),
2112            has_param2 in any::<bool>(),
2113        ) {
2114            // Build two paths
2115            let mut path1 = String::from("/");
2116            let mut path2 = String::from("/");
2117
2118            for segment in &segments1 {
2119                path1.push_str(segment);
2120                path1.push('/');
2121            }
2122            path1.pop(); // Remove trailing slash
2123
2124            for segment in &segments2 {
2125                path2.push_str(segment);
2126                path2.push('/');
2127            }
2128            path2.pop(); // Remove trailing slash
2129
2130            if has_param1 {
2131                path1.push_str("/{id}");
2132            }
2133
2134            if has_param2 {
2135                path2.push_str("/{id}");
2136            }
2137
2138            // Normalize paths for comparison
2139            let norm1 = normalize_path_for_comparison(&convert_path_params(&path1));
2140            let norm2 = normalize_path_for_comparison(&convert_path_params(&path2));
2141
2142            // Only test if paths are actually different
2143            prop_assume!(norm1 != norm2);
2144
2145            // Try to register both routes - should succeed
2146            let result = catch_unwind(AssertUnwindSafe(|| {
2147                async fn handler1() -> &'static str { "handler1" }
2148                async fn handler2() -> &'static str { "handler2" }
2149
2150                let router = Router::new()
2151                    .route(&path1, get(handler1))
2152                    .route(&path2, get(handler2));
2153
2154                router.registered_routes().len()
2155            }));
2156
2157            prop_assert!(
2158                result.is_ok(),
2159                "Routes '{}' and '{}' should not conflict but did",
2160                path1, path2
2161            );
2162
2163            if let Ok(count) = result {
2164                prop_assert_eq!(count, 2, "Should have registered 2 routes");
2165            }
2166        }
2167
2168        /// Property: Conflict error message contains both route paths
2169        ///
2170        /// When a conflict is detected, the error message should include both
2171        /// the existing route path and the new conflicting route path.
2172        #[test]
2173        fn prop_conflict_error_contains_both_paths(
2174            // Generate a valid path segment
2175            segment in "[a-z][a-z0-9]{1,5}",
2176            param1 in "[a-z][a-z0-9]{1,5}",
2177            param2 in "[a-z][a-z0-9]{1,5}",
2178        ) {
2179            prop_assume!(param1 != param2);
2180
2181            let path1 = format!("/{}/{{{}}}", segment, param1);
2182            let path2 = format!("/{}/{{{}}}", segment, param2);
2183
2184            let result = catch_unwind(AssertUnwindSafe(|| {
2185                async fn handler1() -> &'static str { "handler1" }
2186                async fn handler2() -> &'static str { "handler2" }
2187
2188                let _router = Router::new()
2189                    .route(&path1, get(handler1))
2190                    .route(&path2, get(handler2));
2191            }));
2192
2193            prop_assert!(result.is_err(), "Should have panicked due to conflict");
2194
2195            // Check that the panic message contains useful information
2196            if let Err(panic_info) = result {
2197                if let Some(msg) = panic_info.downcast_ref::<String>() {
2198                    prop_assert!(
2199                        msg.contains("ROUTE CONFLICT DETECTED"),
2200                        "Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
2201                        msg
2202                    );
2203                    prop_assert!(
2204                        msg.contains("Existing:") && msg.contains("New:"),
2205                        "Error should contain both 'Existing:' and 'New:' labels, got: {}",
2206                        msg
2207                    );
2208                    prop_assert!(
2209                        msg.contains("How to resolve:"),
2210                        "Error should contain resolution guidance, got: {}",
2211                        msg
2212                    );
2213                }
2214            }
2215        }
2216
2217        /// Property: Exact duplicate paths conflict
2218        ///
2219        /// Registering the exact same path twice should always be detected as a conflict.
2220        #[test]
2221        fn prop_exact_duplicate_paths_conflict(
2222            // Generate valid path segments
2223            segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
2224            has_param in any::<bool>(),
2225        ) {
2226            // Build a path
2227            let mut path = String::from("/");
2228
2229            for segment in &segments {
2230                path.push_str(segment);
2231                path.push('/');
2232            }
2233            path.pop(); // Remove trailing slash
2234
2235            if has_param {
2236                path.push_str("/{id}");
2237            }
2238
2239            // Try to register the same path twice - should panic
2240            let result = catch_unwind(AssertUnwindSafe(|| {
2241                async fn handler1() -> &'static str { "handler1" }
2242                async fn handler2() -> &'static str { "handler2" }
2243
2244                let _router = Router::new()
2245                    .route(&path, get(handler1))
2246                    .route(&path, get(handler2));
2247            }));
2248
2249            prop_assert!(
2250                result.is_err(),
2251                "Registering path '{}' twice should conflict but didn't",
2252                path
2253            );
2254        }
2255    }
2256
2257    // **Feature: router-nesting, Property 5: Nested Route Matching**
2258    //
2259    // For any nested route and a request with a matching path and method,
2260    // the router should return the correct handler.
2261    //
2262    // **Validates: Requirements 2.1**
2263    proptest! {
2264        #![proptest_config(ProptestConfig::with_cases(100))]
2265
2266        /// Property: Nested routes with path parameters are correctly matched
2267        ///
2268        /// For any nested route with path parameters, a request to the prefixed path
2269        /// with valid parameter values should match and return Found.
2270        #[test]
2271        fn prop_nested_route_with_params_matches(
2272            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2273            route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 0..2),
2274            param_value in "[a-z0-9]{1,10}",
2275        ) {
2276            async fn handler() -> &'static str { "handler" }
2277
2278            let prefix = format!("/{}", prefix_segments.join("/"));
2279            let route_path = if route_segments.is_empty() {
2280                "/{id}".to_string()
2281            } else {
2282                format!("/{}/{{id}}", route_segments.join("/"))
2283            };
2284
2285            let nested_router = Router::new().route(&route_path, get(handler));
2286            let app = Router::new().nest(&prefix, nested_router);
2287
2288            // Build the full path to match with actual parameter value
2289            let full_path = if route_segments.is_empty() {
2290                format!("{}/{}", prefix, param_value)
2291            } else {
2292                format!("{}/{}/{}", prefix, route_segments.join("/"), param_value)
2293            };
2294
2295            // Property: The route should be matched
2296            match app.match_route(&full_path, &Method::GET) {
2297                RouteMatch::Found { params, .. } => {
2298                    // Verify the parameter was extracted
2299                    prop_assert!(
2300                        params.contains_key("id"),
2301                        "Should have 'id' parameter, got: {:?}",
2302                        params
2303                    );
2304                    prop_assert_eq!(
2305                        params.get("id").unwrap(),
2306                        &param_value,
2307                        "Parameter value should match"
2308                    );
2309                }
2310                RouteMatch::NotFound => {
2311                    prop_assert!(false, "Route '{}' should be found but got NotFound", full_path);
2312                }
2313                RouteMatch::MethodNotAllowed { .. } => {
2314                    prop_assert!(false, "Route '{}' should be found but got MethodNotAllowed", full_path);
2315                }
2316            }
2317        }
2318
2319        /// Property: Nested routes match correct HTTP method
2320        ///
2321        /// For any nested route registered with a specific HTTP method, only requests
2322        /// with that method should return Found.
2323        #[test]
2324        fn prop_nested_route_matches_correct_method(
2325            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
2326            route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
2327            use_get in any::<bool>(),
2328        ) {
2329            async fn handler() -> &'static str { "handler" }
2330
2331            let prefix = format!("/{}", prefix_segments.join("/"));
2332            let route_path = format!("/{}", route_segments.join("/"));
2333
2334            // Register with either GET or POST
2335            let method_router = if use_get { get(handler) } else { post(handler) };
2336            let nested_router = Router::new().route(&route_path, method_router);
2337            let app = Router::new().nest(&prefix, nested_router);
2338
2339            let full_path = format!("{}{}", prefix, route_path);
2340            let registered_method = if use_get { Method::GET } else { Method::POST };
2341            let other_method = if use_get { Method::POST } else { Method::GET };
2342
2343            // Property: Registered method should match
2344            match app.match_route(&full_path, &registered_method) {
2345                RouteMatch::Found { .. } => {
2346                    // Success
2347                }
2348                other => {
2349                    prop_assert!(false, "Route should be found for registered method, got: {:?}",
2350                        match other {
2351                            RouteMatch::NotFound => "NotFound",
2352                            RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
2353                            _ => "Found",
2354                        }
2355                    );
2356                }
2357            }
2358
2359            // Property: Other method should return MethodNotAllowed
2360            match app.match_route(&full_path, &other_method) {
2361                RouteMatch::MethodNotAllowed { allowed } => {
2362                    prop_assert!(
2363                        allowed.contains(&registered_method),
2364                        "Allowed methods should contain {:?}",
2365                        registered_method
2366                    );
2367                }
2368                other => {
2369                    prop_assert!(false, "Route should return MethodNotAllowed for other method, got: {:?}",
2370                        match other {
2371                            RouteMatch::NotFound => "NotFound",
2372                            RouteMatch::Found { .. } => "Found",
2373                            _ => "MethodNotAllowed",
2374                        }
2375                    );
2376                }
2377            }
2378        }
2379    }
2380
2381    // **Feature: router-nesting, Property 6: Path Parameter Extraction**
2382    //
2383    // For any nested route with path parameters and a matching request,
2384    // the extracted parameters should have the correct names and values.
2385    //
2386    // **Validates: Requirements 2.2**
2387    proptest! {
2388        #![proptest_config(ProptestConfig::with_cases(100))]
2389
2390        /// Property: Single path parameter is correctly extracted from nested route
2391        ///
2392        /// For any nested route with a single path parameter, the parameter name
2393        /// and value should be correctly extracted.
2394        #[test]
2395        fn prop_single_param_extraction(
2396            prefix in "[a-z][a-z0-9]{1,5}",
2397            param_name in "[a-z][a-z0-9]{1,5}",
2398            param_value in "[a-z0-9]{1,10}",
2399        ) {
2400            async fn handler() -> &'static str { "handler" }
2401
2402            let prefix = format!("/{}", prefix);
2403            let route_path = format!("/{{{}}}", param_name);
2404
2405            let nested_router = Router::new().route(&route_path, get(handler));
2406            let app = Router::new().nest(&prefix, nested_router);
2407
2408            let full_path = format!("{}/{}", prefix, param_value);
2409
2410            match app.match_route(&full_path, &Method::GET) {
2411                RouteMatch::Found { params, .. } => {
2412                    prop_assert!(
2413                        params.contains_key(&param_name),
2414                        "Should have '{}' parameter, got: {:?}",
2415                        param_name, params
2416                    );
2417                    prop_assert_eq!(
2418                        params.get(&param_name).unwrap(),
2419                        &param_value,
2420                        "Parameter '{}' value should be '{}'",
2421                        param_name, param_value
2422                    );
2423                }
2424                _ => {
2425                    prop_assert!(false, "Route should be found");
2426                }
2427            }
2428        }
2429
2430        /// Property: Multiple path parameters are correctly extracted from nested route
2431        ///
2432        /// For any nested route with multiple path parameters, all parameters
2433        /// should be correctly extracted with their names and values.
2434        #[test]
2435        fn prop_multiple_params_extraction(
2436            prefix in "[a-z][a-z0-9]{1,5}",
2437            param1_name in "[a-z]{1,5}",
2438            param1_value in "[a-z0-9]{1,10}",
2439            param2_name in "[a-z]{1,5}",
2440            param2_value in "[a-z0-9]{1,10}",
2441        ) {
2442            // Ensure param names are different
2443            prop_assume!(param1_name != param2_name);
2444
2445            async fn handler() -> &'static str { "handler" }
2446
2447            let prefix = format!("/{}", prefix);
2448            let route_path = format!("/{{{}}}/items/{{{}}}", param1_name, param2_name);
2449
2450            let nested_router = Router::new().route(&route_path, get(handler));
2451            let app = Router::new().nest(&prefix, nested_router);
2452
2453            let full_path = format!("{}/{}/items/{}", prefix, param1_value, param2_value);
2454
2455            match app.match_route(&full_path, &Method::GET) {
2456                RouteMatch::Found { params, .. } => {
2457                    // Check first parameter
2458                    prop_assert!(
2459                        params.contains_key(&param1_name),
2460                        "Should have '{}' parameter, got: {:?}",
2461                        param1_name, params
2462                    );
2463                    prop_assert_eq!(
2464                        params.get(&param1_name).unwrap(),
2465                        &param1_value,
2466                        "Parameter '{}' value should be '{}'",
2467                        param1_name, param1_value
2468                    );
2469
2470                    // Check second parameter
2471                    prop_assert!(
2472                        params.contains_key(&param2_name),
2473                        "Should have '{}' parameter, got: {:?}",
2474                        param2_name, params
2475                    );
2476                    prop_assert_eq!(
2477                        params.get(&param2_name).unwrap(),
2478                        &param2_value,
2479                        "Parameter '{}' value should be '{}'",
2480                        param2_name, param2_value
2481                    );
2482                }
2483                _ => {
2484                    prop_assert!(false, "Route should be found");
2485                }
2486            }
2487        }
2488
2489        /// Property: Path parameters preserve special characters in values
2490        ///
2491        /// For any nested route with path parameters, parameter values containing
2492        /// URL-safe special characters should be preserved correctly.
2493        #[test]
2494        fn prop_param_value_preservation(
2495            prefix in "[a-z]{1,5}",
2496            // Generate values with alphanumeric and some special chars
2497            param_value in "[a-zA-Z0-9_-]{1,15}",
2498        ) {
2499            async fn handler() -> &'static str { "handler" }
2500
2501            let prefix = format!("/{}", prefix);
2502            let route_path = "/{id}".to_string();
2503
2504            let nested_router = Router::new().route(&route_path, get(handler));
2505            let app = Router::new().nest(&prefix, nested_router);
2506
2507            let full_path = format!("{}/{}", prefix, param_value);
2508
2509            match app.match_route(&full_path, &Method::GET) {
2510                RouteMatch::Found { params, .. } => {
2511                    prop_assert_eq!(
2512                        params.get("id").unwrap(),
2513                        &param_value,
2514                        "Parameter value should be preserved exactly"
2515                    );
2516                }
2517                _ => {
2518                    prop_assert!(false, "Route should be found");
2519                }
2520            }
2521        }
2522    }
2523
2524    // **Feature: router-nesting, Property 7: Not Found Response**
2525    //
2526    // For any request path that doesn't match any registered route (nested or otherwise),
2527    // the router should return NotFound.
2528    //
2529    // **Validates: Requirements 2.3**
2530    proptest! {
2531        #![proptest_config(ProptestConfig::with_cases(100))]
2532
2533        /// Property: Unregistered paths return NotFound
2534        ///
2535        /// For any path that doesn't match any registered route, the router
2536        /// should return NotFound.
2537        #[test]
2538        fn prop_unregistered_path_returns_not_found(
2539            prefix in "[a-z][a-z0-9]{1,5}",
2540            route_segment in "[a-z][a-z0-9]{1,5}",
2541            unregistered_segment in "[a-z][a-z0-9]{6,10}",
2542        ) {
2543            // Ensure segments are different
2544            prop_assume!(route_segment != unregistered_segment);
2545
2546            async fn handler() -> &'static str { "handler" }
2547
2548            let prefix = format!("/{}", prefix);
2549            let route_path = format!("/{}", route_segment);
2550
2551            let nested_router = Router::new().route(&route_path, get(handler));
2552            let app = Router::new().nest(&prefix, nested_router);
2553
2554            // Try to match an unregistered path
2555            let unregistered_path = format!("{}/{}", prefix, unregistered_segment);
2556
2557            match app.match_route(&unregistered_path, &Method::GET) {
2558                RouteMatch::NotFound => {
2559                    // Success - this is expected
2560                }
2561                RouteMatch::Found { .. } => {
2562                    prop_assert!(false, "Path '{}' should not be found", unregistered_path);
2563                }
2564                RouteMatch::MethodNotAllowed { .. } => {
2565                    prop_assert!(false, "Path '{}' should return NotFound, not MethodNotAllowed", unregistered_path);
2566                }
2567            }
2568        }
2569
2570        /// Property: Wrong prefix returns NotFound
2571        ///
2572        /// For any nested route, a request with a different prefix should return NotFound.
2573        #[test]
2574        fn prop_wrong_prefix_returns_not_found(
2575            prefix1 in "[a-z][a-z0-9]{1,5}",
2576            prefix2 in "[a-z][a-z0-9]{6,10}",
2577            route_segment in "[a-z][a-z0-9]{1,5}",
2578        ) {
2579            // Ensure prefixes are different
2580            prop_assume!(prefix1 != prefix2);
2581
2582            async fn handler() -> &'static str { "handler" }
2583
2584            let prefix = format!("/{}", prefix1);
2585            let route_path = format!("/{}", route_segment);
2586
2587            let nested_router = Router::new().route(&route_path, get(handler));
2588            let app = Router::new().nest(&prefix, nested_router);
2589
2590            // Try to match with wrong prefix
2591            let wrong_prefix_path = format!("/{}/{}", prefix2, route_segment);
2592
2593            match app.match_route(&wrong_prefix_path, &Method::GET) {
2594                RouteMatch::NotFound => {
2595                    // Success - this is expected
2596                }
2597                _ => {
2598                    prop_assert!(false, "Path '{}' with wrong prefix should return NotFound", wrong_prefix_path);
2599                }
2600            }
2601        }
2602
2603        /// Property: Partial path match returns NotFound
2604        ///
2605        /// For any nested route with multiple segments, a request matching only
2606        /// part of the path should return NotFound.
2607        #[test]
2608        fn prop_partial_path_returns_not_found(
2609            prefix in "[a-z][a-z0-9]{1,5}",
2610            segment1 in "[a-z][a-z0-9]{1,5}",
2611            segment2 in "[a-z][a-z0-9]{1,5}",
2612        ) {
2613            async fn handler() -> &'static str { "handler" }
2614
2615            let prefix = format!("/{}", prefix);
2616            let route_path = format!("/{}/{}", segment1, segment2);
2617
2618            let nested_router = Router::new().route(&route_path, get(handler));
2619            let app = Router::new().nest(&prefix, nested_router);
2620
2621            // Try to match only the first segment (partial path)
2622            let partial_path = format!("{}/{}", prefix, segment1);
2623
2624            match app.match_route(&partial_path, &Method::GET) {
2625                RouteMatch::NotFound => {
2626                    // Success - partial path should not match
2627                }
2628                _ => {
2629                    prop_assert!(false, "Partial path '{}' should return NotFound", partial_path);
2630                }
2631            }
2632        }
2633    }
2634
2635    // **Feature: router-nesting, Property 8: Method Not Allowed Response**
2636    //
2637    // For any request to a valid path but with an unregistered HTTP method,
2638    // the router should return MethodNotAllowed with the list of allowed methods.
2639    //
2640    // **Validates: Requirements 2.4**
2641    proptest! {
2642        #![proptest_config(ProptestConfig::with_cases(100))]
2643
2644        /// Property: Unregistered method returns MethodNotAllowed with allowed methods
2645        ///
2646        /// For any nested route registered with specific methods, a request with
2647        /// an unregistered method should return MethodNotAllowed with the correct
2648        /// list of allowed methods.
2649        #[test]
2650        fn prop_unregistered_method_returns_method_not_allowed(
2651            prefix in "[a-z][a-z0-9]{1,5}",
2652            route_segment in "[a-z][a-z0-9]{1,5}",
2653        ) {
2654            async fn handler() -> &'static str { "handler" }
2655
2656            let prefix = format!("/{}", prefix);
2657            let route_path = format!("/{}", route_segment);
2658
2659            // Register only GET
2660            let nested_router = Router::new().route(&route_path, get(handler));
2661            let app = Router::new().nest(&prefix, nested_router);
2662
2663            let full_path = format!("{}{}", prefix, route_path);
2664
2665            // Try POST on a GET-only route
2666            match app.match_route(&full_path, &Method::POST) {
2667                RouteMatch::MethodNotAllowed { allowed } => {
2668                    prop_assert!(
2669                        allowed.contains(&Method::GET),
2670                        "Allowed methods should contain GET, got: {:?}",
2671                        allowed
2672                    );
2673                    prop_assert!(
2674                        !allowed.contains(&Method::POST),
2675                        "Allowed methods should not contain POST"
2676                    );
2677                }
2678                RouteMatch::Found { .. } => {
2679                    prop_assert!(false, "POST should not be found on GET-only route");
2680                }
2681                RouteMatch::NotFound => {
2682                    prop_assert!(false, "Path exists, should return MethodNotAllowed not NotFound");
2683                }
2684            }
2685        }
2686
2687        /// Property: Multiple registered methods are all returned in allowed list
2688        ///
2689        /// For any nested route registered with multiple methods, the MethodNotAllowed
2690        /// response should include all registered methods.
2691        #[test]
2692        fn prop_multiple_methods_in_allowed_list(
2693            prefix in "[a-z][a-z0-9]{1,5}",
2694            route_segment in "[a-z][a-z0-9]{1,5}",
2695            use_get in any::<bool>(),
2696            use_post in any::<bool>(),
2697            use_put in any::<bool>(),
2698        ) {
2699            // Ensure at least one method is registered
2700            prop_assume!(use_get || use_post || use_put);
2701
2702            async fn handler() -> &'static str { "handler" }
2703
2704            let prefix = format!("/{}", prefix);
2705            let route_path = format!("/{}", route_segment);
2706
2707            // Build method router with selected methods
2708            let mut method_router = MethodRouter::new();
2709            let mut expected_methods: Vec<Method> = Vec::new();
2710
2711            if use_get {
2712                let get_router = get(handler);
2713                for (method, h) in get_router.handlers {
2714                    method_router.handlers.insert(method.clone(), h);
2715                    expected_methods.push(method);
2716                }
2717            }
2718            if use_post {
2719                let post_router = post(handler);
2720                for (method, h) in post_router.handlers {
2721                    method_router.handlers.insert(method.clone(), h);
2722                    expected_methods.push(method);
2723                }
2724            }
2725            if use_put {
2726                let put_router = put(handler);
2727                for (method, h) in put_router.handlers {
2728                    method_router.handlers.insert(method.clone(), h);
2729                    expected_methods.push(method);
2730                }
2731            }
2732
2733            let nested_router = Router::new().route(&route_path, method_router);
2734            let app = Router::new().nest(&prefix, nested_router);
2735
2736            let full_path = format!("{}{}", prefix, route_path);
2737
2738            // Try DELETE (which we never register)
2739            match app.match_route(&full_path, &Method::DELETE) {
2740                RouteMatch::MethodNotAllowed { allowed } => {
2741                    // All registered methods should be in allowed list
2742                    for method in &expected_methods {
2743                        prop_assert!(
2744                            allowed.contains(method),
2745                            "Allowed methods should contain {:?}, got: {:?}",
2746                            method, allowed
2747                        );
2748                    }
2749                    // DELETE should not be in allowed list
2750                    prop_assert!(
2751                        !allowed.contains(&Method::DELETE),
2752                        "Allowed methods should not contain DELETE"
2753                    );
2754                }
2755                RouteMatch::Found { .. } => {
2756                    prop_assert!(false, "DELETE should not be found");
2757                }
2758                RouteMatch::NotFound => {
2759                    prop_assert!(false, "Path exists, should return MethodNotAllowed not NotFound");
2760                }
2761            }
2762        }
2763    }
2764
2765    // **Feature: router-nesting, Property 12: Conflict Detection**
2766    //
2767    // For any nested route that conflicts with an existing route (same path structure),
2768    // the router should detect and report the conflict with both route paths.
2769    //
2770    // **Validates: Requirements 5.1, 5.3**
2771
2772    // **Feature: router-nesting, Property 4: Multiple Router Composition**
2773    //
2774    // For any set of routers with non-overlapping route structures nested under
2775    // different prefixes, all routes should be registered without conflicts.
2776    //
2777    // **Validates: Requirements 1.5**
2778    proptest! {
2779        #![proptest_config(ProptestConfig::with_cases(100))]
2780
2781        /// Property: Multiple routers nested under different prefixes register all routes
2782        ///
2783        /// For any set of routers with routes nested under different prefixes,
2784        /// all routes should be registered and the total count should equal the
2785        /// sum of routes from all nested routers.
2786        #[test]
2787        fn prop_multiple_routers_all_routes_registered(
2788            // Generate 2-3 different prefixes
2789            prefix1_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2790            prefix2_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2791            // Generate route counts for each router (1-3 routes each)
2792            num_routes1 in 1..4usize,
2793            num_routes2 in 1..4usize,
2794        ) {
2795            // Build prefixes
2796            let prefix1 = format!("/{}", prefix1_segments.join("/"));
2797            let prefix2 = format!("/{}", prefix2_segments.join("/"));
2798
2799            // Ensure prefixes are different
2800            prop_assume!(prefix1 != prefix2);
2801
2802            async fn handler() -> &'static str { "handler" }
2803
2804            // Create first router with routes
2805            let mut router1 = Router::new();
2806            for i in 0..num_routes1 {
2807                let path = format!("/route1_{}", i);
2808                router1 = router1.route(&path, get(handler));
2809            }
2810
2811            // Create second router with routes
2812            let mut router2 = Router::new();
2813            for i in 0..num_routes2 {
2814                let path = format!("/route2_{}", i);
2815                router2 = router2.route(&path, get(handler));
2816            }
2817
2818            // Nest both routers
2819            let app = Router::new()
2820                .nest(&prefix1, router1)
2821                .nest(&prefix2, router2);
2822
2823            let routes = app.registered_routes();
2824
2825            // Property: Total route count should equal sum of all nested routes
2826            let expected_count = num_routes1 + num_routes2;
2827            prop_assert_eq!(
2828                routes.len(),
2829                expected_count,
2830                "Should have {} routes ({}+{}), got {}",
2831                expected_count, num_routes1, num_routes2, routes.len()
2832            );
2833
2834            // Property: All routes from router1 should be registered with prefix1
2835            for i in 0..num_routes1 {
2836                let expected_path = format!("{}/route1_{}", prefix1, i);
2837                let matchit_path = convert_path_params(&expected_path);
2838                prop_assert!(
2839                    routes.contains_key(&matchit_path),
2840                    "Route '{}' should be registered",
2841                    expected_path
2842                );
2843            }
2844
2845            // Property: All routes from router2 should be registered with prefix2
2846            for i in 0..num_routes2 {
2847                let expected_path = format!("{}/route2_{}", prefix2, i);
2848                let matchit_path = convert_path_params(&expected_path);
2849                prop_assert!(
2850                    routes.contains_key(&matchit_path),
2851                    "Route '{}' should be registered",
2852                    expected_path
2853                );
2854            }
2855        }
2856
2857        /// Property: Multiple routers with same internal routes don't interfere
2858        ///
2859        /// For any set of routers with identical internal route structures nested
2860        /// under different prefixes, all routes should be independently matchable.
2861        #[test]
2862        fn prop_multiple_routers_no_interference(
2863            prefix1 in "[a-z][a-z0-9]{1,5}",
2864            prefix2 in "[a-z][a-z0-9]{1,5}",
2865            route_segment in "[a-z][a-z0-9]{1,5}",
2866            param_value1 in "[a-z0-9]{1,10}",
2867            param_value2 in "[a-z0-9]{1,10}",
2868        ) {
2869            // Ensure prefixes are different
2870            prop_assume!(prefix1 != prefix2);
2871
2872            let prefix1 = format!("/{}", prefix1);
2873            let prefix2 = format!("/{}", prefix2);
2874
2875            async fn handler() -> &'static str { "handler" }
2876
2877            // Create two routers with identical internal structure
2878            let router1 = Router::new()
2879                .route(&format!("/{}", route_segment), get(handler))
2880                .route("/{id}", get(handler));
2881
2882            let router2 = Router::new()
2883                .route(&format!("/{}", route_segment), get(handler))
2884                .route("/{id}", get(handler));
2885
2886            // Nest both routers
2887            let app = Router::new()
2888                .nest(&prefix1, router1)
2889                .nest(&prefix2, router2);
2890
2891            // Property: Routes under prefix1 should be matchable
2892            let path1_static = format!("{}/{}", prefix1, route_segment);
2893            match app.match_route(&path1_static, &Method::GET) {
2894                RouteMatch::Found { params, .. } => {
2895                    prop_assert!(params.is_empty(), "Static path should have no params");
2896                }
2897                _ => {
2898                    prop_assert!(false, "Route '{}' should be found", path1_static);
2899                }
2900            }
2901
2902            let path1_param = format!("{}/{}", prefix1, param_value1);
2903            match app.match_route(&path1_param, &Method::GET) {
2904                RouteMatch::Found { params, .. } => {
2905                    prop_assert_eq!(
2906                        params.get("id"),
2907                        Some(&param_value1.to_string()),
2908                        "Parameter should be extracted correctly"
2909                    );
2910                }
2911                _ => {
2912                    prop_assert!(false, "Route '{}' should be found", path1_param);
2913                }
2914            }
2915
2916            // Property: Routes under prefix2 should be matchable independently
2917            let path2_static = format!("{}/{}", prefix2, route_segment);
2918            match app.match_route(&path2_static, &Method::GET) {
2919                RouteMatch::Found { params, .. } => {
2920                    prop_assert!(params.is_empty(), "Static path should have no params");
2921                }
2922                _ => {
2923                    prop_assert!(false, "Route '{}' should be found", path2_static);
2924                }
2925            }
2926
2927            let path2_param = format!("{}/{}", prefix2, param_value2);
2928            match app.match_route(&path2_param, &Method::GET) {
2929                RouteMatch::Found { params, .. } => {
2930                    prop_assert_eq!(
2931                        params.get("id"),
2932                        Some(&param_value2.to_string()),
2933                        "Parameter should be extracted correctly"
2934                    );
2935                }
2936                _ => {
2937                    prop_assert!(false, "Route '{}' should be found", path2_param);
2938                }
2939            }
2940        }
2941
2942        /// Property: Multiple routers preserve HTTP methods independently
2943        ///
2944        /// For any set of routers with different HTTP methods nested under different
2945        /// prefixes, each route should preserve its own set of allowed methods.
2946        #[test]
2947        fn prop_multiple_routers_preserve_methods(
2948            prefix1 in "[a-z][a-z0-9]{1,5}",
2949            prefix2 in "[a-z][a-z0-9]{1,5}",
2950            route_segment in "[a-z][a-z0-9]{1,5}",
2951            router1_use_get in any::<bool>(),
2952            router1_use_post in any::<bool>(),
2953            router2_use_get in any::<bool>(),
2954            router2_use_put in any::<bool>(),
2955        ) {
2956            // Ensure at least one method per router
2957            prop_assume!(router1_use_get || router1_use_post);
2958            prop_assume!(router2_use_get || router2_use_put);
2959            // Ensure prefixes are different
2960            prop_assume!(prefix1 != prefix2);
2961
2962            let prefix1 = format!("/{}", prefix1);
2963            let prefix2 = format!("/{}", prefix2);
2964            let route_path = format!("/{}", route_segment);
2965
2966            async fn handler() -> &'static str { "handler" }
2967
2968            // Build router1 with selected methods
2969            let mut method_router1 = MethodRouter::new();
2970            let mut expected_methods1: Vec<Method> = Vec::new();
2971            if router1_use_get {
2972                let get_router = get(handler);
2973                for (method, h) in get_router.handlers {
2974                    method_router1.handlers.insert(method.clone(), h);
2975                    expected_methods1.push(method);
2976                }
2977            }
2978            if router1_use_post {
2979                let post_router = post(handler);
2980                for (method, h) in post_router.handlers {
2981                    method_router1.handlers.insert(method.clone(), h);
2982                    expected_methods1.push(method);
2983                }
2984            }
2985
2986            // Build router2 with selected methods
2987            let mut method_router2 = MethodRouter::new();
2988            let mut expected_methods2: Vec<Method> = Vec::new();
2989            if router2_use_get {
2990                let get_router = get(handler);
2991                for (method, h) in get_router.handlers {
2992                    method_router2.handlers.insert(method.clone(), h);
2993                    expected_methods2.push(method);
2994                }
2995            }
2996            if router2_use_put {
2997                let put_router = put(handler);
2998                for (method, h) in put_router.handlers {
2999                    method_router2.handlers.insert(method.clone(), h);
3000                    expected_methods2.push(method);
3001                }
3002            }
3003
3004            let router1 = Router::new().route(&route_path, method_router1);
3005            let router2 = Router::new().route(&route_path, method_router2);
3006
3007            let app = Router::new()
3008                .nest(&prefix1, router1)
3009                .nest(&prefix2, router2);
3010
3011            let full_path1 = format!("{}{}", prefix1, route_path);
3012            let full_path2 = format!("{}{}", prefix2, route_path);
3013
3014            // Property: Router1's methods should be preserved
3015            for method in &expected_methods1 {
3016                match app.match_route(&full_path1, method) {
3017                    RouteMatch::Found { .. } => {}
3018                    _ => {
3019                        prop_assert!(false, "Method {:?} should be found for {}", method, full_path1);
3020                    }
3021                }
3022            }
3023
3024            // Property: Router2's methods should be preserved
3025            for method in &expected_methods2 {
3026                match app.match_route(&full_path2, method) {
3027                    RouteMatch::Found { .. } => {}
3028                    _ => {
3029                        prop_assert!(false, "Method {:?} should be found for {}", method, full_path2);
3030                    }
3031                }
3032            }
3033
3034            // Property: Methods not registered should return MethodNotAllowed
3035            if !expected_methods1.contains(&Method::DELETE) {
3036                match app.match_route(&full_path1, &Method::DELETE) {
3037                    RouteMatch::MethodNotAllowed { allowed } => {
3038                        for method in &expected_methods1 {
3039                            prop_assert!(
3040                                allowed.contains(method),
3041                                "Allowed methods for {} should contain {:?}",
3042                                full_path1, method
3043                            );
3044                        }
3045                    }
3046                    _ => {
3047                        prop_assert!(false, "DELETE should return MethodNotAllowed for {}", full_path1);
3048                    }
3049                }
3050            }
3051        }
3052
3053        /// Property: Three or more routers can be composed without conflicts
3054        ///
3055        /// For any set of three routers nested under different prefixes,
3056        /// all routes should be registered without conflicts.
3057        #[test]
3058        fn prop_three_routers_composition(
3059            prefix1 in "[a-z]{1,3}",
3060            prefix2 in "[a-z]{4,6}",
3061            prefix3 in "[a-z]{7,9}",
3062            num_routes in 1..3usize,
3063        ) {
3064            let prefix1 = format!("/{}", prefix1);
3065            let prefix2 = format!("/{}", prefix2);
3066            let prefix3 = format!("/{}", prefix3);
3067
3068            async fn handler() -> &'static str { "handler" }
3069
3070            // Create three routers with same number of routes
3071            let mut router1 = Router::new();
3072            let mut router2 = Router::new();
3073            let mut router3 = Router::new();
3074
3075            for i in 0..num_routes {
3076                let path = format!("/item{}", i);
3077                router1 = router1.route(&path, get(handler));
3078                router2 = router2.route(&path, get(handler));
3079                router3 = router3.route(&path, get(handler));
3080            }
3081
3082            // Nest all three routers
3083            let app = Router::new()
3084                .nest(&prefix1, router1)
3085                .nest(&prefix2, router2)
3086                .nest(&prefix3, router3);
3087
3088            let routes = app.registered_routes();
3089
3090            // Property: Total route count should be 3 * num_routes
3091            let expected_count = 3 * num_routes;
3092            prop_assert_eq!(
3093                routes.len(),
3094                expected_count,
3095                "Should have {} routes, got {}",
3096                expected_count, routes.len()
3097            );
3098
3099            // Property: All routes should be matchable
3100            for i in 0..num_routes {
3101                let path1 = format!("{}/item{}", prefix1, i);
3102                let path2 = format!("{}/item{}", prefix2, i);
3103                let path3 = format!("{}/item{}", prefix3, i);
3104
3105                match app.match_route(&path1, &Method::GET) {
3106                    RouteMatch::Found { .. } => {}
3107                    _ => prop_assert!(false, "Route '{}' should be found", path1),
3108                }
3109                match app.match_route(&path2, &Method::GET) {
3110                    RouteMatch::Found { .. } => {}
3111                    _ => prop_assert!(false, "Route '{}' should be found", path2),
3112                }
3113                match app.match_route(&path3, &Method::GET) {
3114                    RouteMatch::Found { .. } => {}
3115                    _ => prop_assert!(false, "Route '{}' should be found", path3),
3116                }
3117            }
3118        }
3119    }
3120    proptest! {
3121        #![proptest_config(ProptestConfig::with_cases(100))]
3122
3123        /// Property: Nested routes with same path structure but different param names conflict
3124        ///
3125        /// For any existing route with a parameter and a nested route that would create
3126        /// the same path structure with a different parameter name, the router should
3127        /// detect and report the conflict.
3128        #[test]
3129        fn prop_nested_route_conflict_different_param_names(
3130            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3131            route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 0..2),
3132            param1 in "[a-z][a-z0-9]{1,5}",
3133            param2 in "[a-z][a-z0-9]{1,5}",
3134        ) {
3135            // Ensure param names are different
3136            prop_assume!(param1 != param2);
3137
3138            async fn handler1() -> &'static str { "handler1" }
3139            async fn handler2() -> &'static str { "handler2" }
3140
3141            let prefix = format!("/{}", prefix_segments.join("/"));
3142
3143            // Build the existing route path (with param1)
3144            let existing_path = if route_segments.is_empty() {
3145                format!("{}/{{{}}}", prefix, param1)
3146            } else {
3147                format!("{}/{}/{{{}}}", prefix, route_segments.join("/"), param1)
3148            };
3149
3150            // Build the nested route path (with param2)
3151            let nested_path = if route_segments.is_empty() {
3152                format!("/{{{}}}", param2)
3153            } else {
3154                format!("/{}/{{{}}}", route_segments.join("/"), param2)
3155            };
3156
3157            // Try to create a conflict
3158            let result = catch_unwind(AssertUnwindSafe(|| {
3159                let parent = Router::new().route(&existing_path, get(handler1));
3160                let nested = Router::new().route(&nested_path, get(handler2));
3161                let _app = parent.nest(&prefix, nested);
3162            }));
3163
3164            // Property: Should detect conflict
3165            prop_assert!(
3166                result.is_err(),
3167                "Nested route '{}{}' should conflict with existing route '{}' but didn't",
3168                prefix, nested_path, existing_path
3169            );
3170
3171            // Property: Error message should contain conflict information
3172            if let Err(panic_info) = result {
3173                if let Some(msg) = panic_info.downcast_ref::<String>() {
3174                    prop_assert!(
3175                        msg.contains("ROUTE CONFLICT DETECTED"),
3176                        "Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
3177                        msg
3178                    );
3179                    prop_assert!(
3180                        msg.contains("Existing:") && msg.contains("New:"),
3181                        "Error should contain both 'Existing:' and 'New:' labels, got: {}",
3182                        msg
3183                    );
3184                }
3185            }
3186        }
3187
3188        /// Property: Nested routes with exact same path conflict
3189        ///
3190        /// For any existing route and a nested route that would create the exact
3191        /// same path, the router should detect and report the conflict.
3192        #[test]
3193        fn prop_nested_route_conflict_exact_same_path(
3194            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3195            route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3196        ) {
3197            async fn handler1() -> &'static str { "handler1" }
3198            async fn handler2() -> &'static str { "handler2" }
3199
3200            let prefix = format!("/{}", prefix_segments.join("/"));
3201            let route_path = format!("/{}", route_segments.join("/"));
3202
3203            // Build the full existing path
3204            let existing_path = format!("{}{}", prefix, route_path);
3205
3206            // Try to create a conflict by nesting a route that creates the same path
3207            let result = catch_unwind(AssertUnwindSafe(|| {
3208                let parent = Router::new().route(&existing_path, get(handler1));
3209                let nested = Router::new().route(&route_path, get(handler2));
3210                let _app = parent.nest(&prefix, nested);
3211            }));
3212
3213            // Property: Should detect conflict
3214            prop_assert!(
3215                result.is_err(),
3216                "Nested route '{}{}' should conflict with existing route '{}' but didn't",
3217                prefix, route_path, existing_path
3218            );
3219        }
3220
3221        /// Property: Nested routes under different prefixes don't conflict
3222        ///
3223        /// For any two nested routers with the same internal routes but different
3224        /// prefixes, they should not conflict.
3225        #[test]
3226        fn prop_nested_routes_different_prefixes_no_conflict(
3227            prefix1_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3228            prefix2_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3229            route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3230            has_param in any::<bool>(),
3231        ) {
3232            // Build prefixes
3233            let prefix1 = format!("/{}", prefix1_segments.join("/"));
3234            let prefix2 = format!("/{}", prefix2_segments.join("/"));
3235
3236            // Ensure prefixes are different
3237            prop_assume!(prefix1 != prefix2);
3238
3239            async fn handler1() -> &'static str { "handler1" }
3240            async fn handler2() -> &'static str { "handler2" }
3241
3242            // Build the route path
3243            let route_path = if has_param {
3244                format!("/{}/{{id}}", route_segments.join("/"))
3245            } else {
3246                format!("/{}", route_segments.join("/"))
3247            };
3248
3249            // Try to nest both routers - should NOT conflict
3250            let result = catch_unwind(AssertUnwindSafe(|| {
3251                let nested1 = Router::new().route(&route_path, get(handler1));
3252                let nested2 = Router::new().route(&route_path, get(handler2));
3253
3254                let app = Router::new()
3255                    .nest(&prefix1, nested1)
3256                    .nest(&prefix2, nested2);
3257
3258                app.registered_routes().len()
3259            }));
3260
3261            // Property: Should NOT conflict
3262            prop_assert!(
3263                result.is_ok(),
3264                "Routes under different prefixes '{}' and '{}' should not conflict",
3265                prefix1, prefix2
3266            );
3267
3268            if let Ok(count) = result {
3269                prop_assert_eq!(count, 2, "Should have registered 2 routes");
3270            }
3271        }
3272
3273        /// Property: Conflict error message contains resolution guidance
3274        ///
3275        /// When a nested route conflict is detected, the error message should
3276        /// include guidance on how to resolve the conflict.
3277        #[test]
3278        fn prop_nested_conflict_error_contains_guidance(
3279            prefix in "[a-z][a-z0-9]{1,5}",
3280            segment in "[a-z][a-z0-9]{1,5}",
3281            param1 in "[a-z][a-z0-9]{1,5}",
3282            param2 in "[a-z][a-z0-9]{1,5}",
3283        ) {
3284            prop_assume!(param1 != param2);
3285
3286            async fn handler1() -> &'static str { "handler1" }
3287            async fn handler2() -> &'static str { "handler2" }
3288
3289            let prefix = format!("/{}", prefix);
3290            let existing_path = format!("{}/{}/{{{}}}", prefix, segment, param1);
3291            let nested_path = format!("/{}/{{{}}}", segment, param2);
3292
3293            let result = catch_unwind(AssertUnwindSafe(|| {
3294                let parent = Router::new().route(&existing_path, get(handler1));
3295                let nested = Router::new().route(&nested_path, get(handler2));
3296                let _app = parent.nest(&prefix, nested);
3297            }));
3298
3299            prop_assert!(result.is_err(), "Should have detected conflict");
3300
3301            if let Err(panic_info) = result {
3302                if let Some(msg) = panic_info.downcast_ref::<String>() {
3303                    prop_assert!(
3304                        msg.contains("How to resolve:"),
3305                        "Error should contain 'How to resolve:' guidance, got: {}",
3306                        msg
3307                    );
3308                    prop_assert!(
3309                        msg.contains("Use different path patterns") ||
3310                        msg.contains("different path patterns"),
3311                        "Error should suggest using different path patterns, got: {}",
3312                        msg
3313                    );
3314                }
3315            }
3316        }
3317    }
3318}