from __future__ import annotations

from functools import lru_cache
from typing import TYPE_CHECKING, Any, Pattern

from litestar._asgi.routing_trie.types import PathParameterSentinel
from litestar.exceptions import MethodNotAllowedException, NotFoundException
from litestar.utils import normalize_path

__all__ = ("parse_node_handlers", "parse_path_params", "parse_path_to_route", "traverse_route_map")


if TYPE_CHECKING:
    from litestar._asgi.routing_trie.types import ASGIHandlerTuple, RouteTrieNode
    from litestar.types import ASGIApp, Method, RouteHandlerType
    from litestar.types.internal_types import PathParameterDefinition


def traverse_route_map(
    root_node: RouteTrieNode,
    path: str,
) -> tuple[RouteTrieNode, list[str], str]:
    """Traverses the application route mapping and retrieves the correct node for the request url.

    Args:
        root_node: The root trie node.
        path: The request's path.

    Raises:
        NotFoundException: If no correlating node is found.

    Returns:
        A tuple containing the target RouteMapNode and a list containing all path parameter values.
    """
    current_node = root_node
    path_params: list[str] = []
    path_components = [p for p in path.split("/") if p]

    for i, component in enumerate(path_components):
        if component in current_node.child_keys:
            current_node = current_node.children[component]
            continue

        if current_node.is_path_param_node:
            current_node = current_node.children[PathParameterSentinel]

            if current_node.is_path_type:
                path_params.append(normalize_path("/".join(path_components[i:])))
                break

            path_params.append(component)
            continue

        if i != len(path_components) - 1 or not current_node.children:
            raise NotFoundException()

    if not current_node.asgi_handlers:
        raise NotFoundException()

    return current_node, path_params, path


def parse_node_handlers(
    node: RouteTrieNode,
    method: Method | None,
) -> ASGIHandlerTuple:
    """Retrieve the handler tuple from the node.

    Args:
        node: The trie node to parse.
        method: The scope's method.

    Raises:
        KeyError: If no matching method is found.

    Returns:
        An ASGI Handler tuple.
    """

    if node.is_asgi:
        return node.asgi_handlers["asgi"]
    if method:
        return node.asgi_handlers[method]
    return node.asgi_handlers["websocket"]


@lru_cache(1024)
def parse_path_params(
    parameter_definitions: tuple[PathParameterDefinition, ...], path_param_values: tuple[str, ...]
) -> dict[str, Any]:
    """Parse path parameters into a dictionary of values.

    Args:
        parameter_definitions: The parameter definitions tuple from the route.
        path_param_values: The string values extracted from the url

    Raises:
        ValueError: If any of path parameters can not be parsed into a value.

    Returns:
        A dictionary of parsed path parameters.
    """
    return {
        param_definition.name: param_definition.parser(value) if param_definition.parser else value
        for param_definition, value in zip(parameter_definitions, path_param_values)
    }


def parse_path_to_route(
    method: Method | None,
    mount_paths_regex: Pattern | None,
    mount_routes: dict[str, RouteTrieNode],
    path: str,
    plain_routes: set[str],
    root_node: RouteTrieNode,
) -> tuple[ASGIApp, RouteHandlerType, str, dict[str, Any]]:
    """Given a scope object, retrieve the asgi_handlers and is_mount boolean values from correct trie node.

    Args:
        method: The scope's method, if any.
        root_node: The root trie node.
        path: The path to resolve scope instance.
        plain_routes: The set of plain routes.
        mount_routes: Mapping of mount routes to trie nodes.
        mount_paths_regex: A compiled regex to match the mount routes.

    Raises:
        MethodNotAllowedException: if no matching method is found.
        NotFoundException: If no correlating node is found or if path params can not be parsed into values according to the node definition.

    Returns:
        A tuple containing the stack of middlewares and the route handler that is wrapped by it.
    """

    try:
        if path in plain_routes:
            asgi_app, handler = parse_node_handlers(node=root_node.children[path], method=method)
            return asgi_app, handler, path, {}

        if mount_paths_regex and (match := mount_paths_regex.search(path)):
            mount_path = path[match.start() : match.end()]
            mount_node = mount_routes[mount_path]
            remaining_path = path[match.end() :]
            # since we allow regular handlers under static paths, we must validate that the request does not match
            # any such handler.
            if not mount_node.children or all(
                sub_route not in path for sub_route in mount_node.children  # type: ignore
            ):
                asgi_app, handler = parse_node_handlers(node=mount_node, method=method)
                remaining_path = remaining_path or "/"
                if not mount_node.is_static:
                    remaining_path = remaining_path if remaining_path.endswith("/") else f"{remaining_path}/"
                return asgi_app, handler, remaining_path, {}

        node, path_parameters, path = traverse_route_map(
            root_node=root_node,
            path=path,
        )
        asgi_app, handler = parse_node_handlers(node=node, method=method)
        key = method or ("asgi" if node.is_asgi else "websocket")
        parsed_path_parameters = parse_path_params(node.path_parameters[key], tuple(path_parameters))

        return (
            asgi_app,
            handler,
            path,
            parsed_path_parameters,
        )
    except KeyError as e:
        raise MethodNotAllowedException() from e
    except ValueError as e:
        raise NotFoundException() from e

litestar/_asgi/routing_trie/traversal.py
from __future__ import annotations

from functools import lru_cache
from typing import TYPE_CHECKING, Any, Pattern

from litestar._asgi.routing_trie.types import PathParameterSentinel
from litestar.exceptions import MethodNotAllowedException, NotFoundException
from litestar.utils import normalize_path

__all__ = ("parse_node_handlers", "parse_path_params", "parse_path_to_route", "traverse_route_map")


if TYPE_CHECKING:
    from litestar._asgi.routing_trie.types import ASGIHandlerTuple, RouteTrieNode
    from litestar.types import ASGIApp, Method, RouteHandlerType
    from litestar.types.internal_types import PathParameterDefinition


def traverse_route_map(
    root_node: RouteTrieNode,
    path: str,
) -> tuple[RouteTrieNode, list[str], str]:
    """Traverses the application route mapping and retrieves the correct node for the request url.

    Args:
        root_node: The root trie node.
        path: The request's path.

    Raises:
        NotFoundException: If no correlating node is found.

    Returns:
        A tuple containing the target RouteMapNode and a list containing all path parameter values.
    """
    current_node = root_node
    path_params: list[str] = []
    path_components = [p for p in path.split("/") if p]

    for i, component in enumerate(path_components):
        if component in current_node.child_keys:
            current_node = current_node.children[component]
            continue

        if current_node.is_path_param_node:
            current_node = current_node.children[PathParameterSentinel]

            if current_node.is_path_type:
                path_params.append(normalize_path("/".join(path_components[i:])))
                break

            path_params.append(component)
            continue

        raise NotFoundException()

    if not current_node.asgi_handlers:
        raise NotFoundException()

    return current_node, path_params, path


def parse_node_handlers(
    node: RouteTrieNode,
    method: Method | None,
) -> ASGIHandlerTuple:
    """Retrieve the handler tuple from the node.

    Args:
        node: The trie node to parse.
        method: The scope's method.

    Raises:
        KeyError: If no matching method is found.

    Returns:
        An ASGI Handler tuple.
    """

    if node.is_asgi:
        return node.asgi_handlers["asgi"]
    if method:
        return node.asgi_handlers[method]
    return node.asgi_handlers["websocket"]


@lru_cache(1024)
def parse_path_params(
    parameter_definitions: tuple[PathParameterDefinition, ...], path_param_values: tuple[str, ...]
) -> dict[str, Any]:
    """Parse path parameters into a dictionary of values.

    Args:
        parameter_definitions: The parameter definitions tuple from the route.
        path_param_values: The string values extracted from the url

    Raises:
        ValueError: If any of path parameters can not be parsed into a value.

    Returns:
        A dictionary of parsed path parameters.
    """
    return {
        param_definition.name: param_definition.parser(value) if param_definition.parser else value
        for param_definition, value in zip(parameter_definitions, path_param_values)
    }


def parse_path_to_route(
    method: Method | None,
    mount_paths_regex: Pattern | None,
    mount_routes: dict[str, RouteTrieNode],
    path: str,
    plain_routes: set[str],
    root_node: RouteTrieNode,
) -> tuple[ASGIApp, RouteHandlerType, str, dict[str, Any]]:
    """Given a scope object, retrieve the asgi_handlers and is_mount boolean values from correct trie node.

    Args:
        method: The scope's method, if any.
        root_node: The root trie node.
        path: The path to resolve scope instance.
        plain_routes: The set of plain routes.
        mount_routes: Mapping of mount routes to trie nodes.
        mount_paths_regex: A compiled regex to match the mount routes.

    Raises:
        MethodNotAllowedException: if no matching method is found.
        NotFoundException: If no correlating node is found or if path params can not be parsed into values according to the node definition.

    Returns:
        A tuple containing the stack of middlewares and the route handler that is wrapped by it.
    """

    try:
        if path in plain_routes:
            asgi_app, handler = parse_node_handlers(node=root_node.children[path], method=method)
            return asgi_app, handler, path, {}

        if mount_paths_regex and (match := mount_paths_regex.search(path)):
            mount_path = path[match.start() : match.end()]
            mount_node = mount_routes[mount_path]
            remaining_path = path[match.end() :]
            # since we allow regular handlers under static paths, we must validate that the request does not match
            # any such handler.
            if not mount_node.children or all(
                sub_route not in path for sub_route in mount_node.children  # type: ignore
            ):
                asgi_app, handler = parse_node_handlers(node=mount_node, method=method)
                remaining_path = remaining_path or "/"
                if not mount_node.is_static:
                    remaining_path = remaining_path if remaining_path.endswith("/") else f"{remaining_path}/"
                return asgi_app, handler, remaining_path, {}

        node, path_parameters, path = traverse_route_map(
            root_node=root_node,
            path=path,
        )
        asgi_app, handler = parse_node_handlers(node=node, method=method)
        key = method or ("asgi" if node.is_asgi else "websocket")
        parsed_path_parameters = parse_path_params(node.path_parameters[key], tuple(path_parameters))

        return (
            asgi_app,
            handler,
            path,
            parsed_path_parameters,
        )
    except KeyError as e:
        raise MethodNotAllowedException() from e
    except ValueError as e:
        raise NotFoundException() from e

litestar/_asgi/routing_trie/traversal.py
diff --git a/litestar/_asgi/routing_trie/traversal.py b/litestar/_asgi/routing_trie/traversal.py
index 3c182ffcb1..faa9e04996 100644
--- a/litestar/_asgi/routing_trie/traversal.py
+++ b/litestar/_asgi/routing_trie/traversal.py
@@ -51,8 +51,7 @@ def traverse_route_map(
             path_params.append(component)
             continue
 
-        if i != len(path_components) - 1 or not current_node.children:
-            raise NotFoundException()
+        raise NotFoundException()
 
     if not current_node.asgi_handlers:
         raise NotFoundException()
diff --git a/tests/e2e/test_routing/test_path_resolution.py b/tests/e2e/test_routing/test_path_resolution.py
index 507b4c7a03..7b91374847 100644
--- a/tests/e2e/test_routing/test_path_resolution.py
+++ b/tests/e2e/test_routing/test_path_resolution.py
@@ -3,7 +3,7 @@
 
 import pytest
 
-from litestar import Controller, MediaType, delete, get, post
+from litestar import Controller, MediaType, Router, delete, get, post
 from litestar.contrib.pydantic import _model_dump
 from litestar.status_codes import (
     HTTP_200_OK,
@@ -274,3 +274,29 @@ async def hello_world(name: str) -> str:
 
         response = client.get("/jon/bon/jovi")
         assert response.status_code == HTTP_404_NOT_FOUND
+
+
+def test_root_path_param_resolution_2() -> None:
+    # https://github.com/litestar-org/litestar/issues/1830#issuecomment-1642291149
+    @get("/{name:str}")
+    async def name_greeting(name: str) -> str:
+        return f"Hello, {name}!"
+
+    @get("/{age:int}")
+    async def age_greeting(name: str, age: int) -> str:
+        return f"Hello, {name}! {age} is a great age to be!"
+
+    age_router = Router("/{name:str}/age", route_handlers=[age_greeting])
+    name_router = Router("/name", route_handlers=[name_greeting, age_router])
+
+    with create_test_client(name_router) as client:
+        response = client.get("/name/jon")
+        assert response.status_code == HTTP_200_OK
+        assert response.text == "Hello, jon!"
+
+        response = client.get("/name/jon/age/42")
+        assert response.status_code == HTTP_200_OK
+        assert response.text == "Hello, jon! 42 is a great age to be!"
+
+        response = client.get("/name/jon/bon")
+        assert response.status_code == HTTP_404_NOT_FOUND

Bug: route with path parameters matches longer paths
### Description

A route registered for `/{param:str}`, in addition to matching `/foo` matches also `/foo/bar` (but not `/foo/bar/baz`).

### URL to code causing the issue

_No response_

### MCVE

```python
from litestar import Litestar, get


@get("/{name:str}")
async def hello_world(name: str) -> str:
    return f"Hello, {name}!"


app = Litestar([hello_world])
```


### Steps to reproduce

```bash
✗ http -b 'localhost:8000/jon'
Hello, jon!


✗ http -b 'localhost:8000/jon/bon'
Hello, jon!


✗ http -b 'localhost:8000/jon/bon/jovi'
{
    "detail": "Not Found",
    "status_code": 404
}
```
```


### Screenshots

```bash
"![SCREENSHOT_DESCRIPTION](SCREENSHOT_LINK.png)"
```


### Logs

_No response_

### Litestar Version

Main branch

### Platform

- [ ] Linux
- [ ] Mac
- [ ] Windows
- [ ] Other (Please specify in the description above)

<!-- POLAR PLEDGE BADGE START -->
## Funding
* If you would like to see an issue prioritized, make a pledge towards it!
* We receive the pledge once the issue is completed & verified

<a href="https://polar.sh/litestar-org/litestar/issues/1830">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/1830/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/1830/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->

