# Copyright 2025 Softwell S.r.l.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests for per-app middleware chains."""

from unittest.mock import AsyncMock, MagicMock

import pytest

from genro_asgi.server.dispatcher import Dispatcher
from genro_asgi.server.handler_executor import HandlerExecutor
from genro_asgi.server.server_config import ServerConfig


class TestParseAppOpts:
    """Tests for ServerConfig._parse_app_opts middleware extraction."""

    def _make_config(self) -> ServerConfig:
        """Create a minimal ServerConfig for testing _parse_app_opts."""
        config = object.__new__(ServerConfig)
        return config

    def test_parse_app_opts_extracts_middleware(self) -> None:
        """middleware key is extracted from kwargs into separate field."""
        config = self._make_config()
        app_opts = {
            "module": "api_app:Application",
            "middleware": {"session": True, "cache": True},
            "base_dir": "/some/path",
        }
        module_path, mw_config, kwargs = config._parse_app_opts("api", app_opts)

        assert module_path == "api_app:Application"
        assert mw_config == {"session": True, "cache": True}
        assert "middleware" not in kwargs
        assert kwargs["base_dir"] == "/some/path"

    def test_parse_app_opts_no_middleware(self) -> None:
        """Without middleware key, mw_config is None."""
        config = self._make_config()
        app_opts = {"module": "api_app:Application", "base_dir": "/path"}
        module_path, mw_config, kwargs = config._parse_app_opts("api", app_opts)

        assert mw_config is None
        assert kwargs["base_dir"] == "/path"


class TestHandlerExecutor:
    """Tests for HandlerExecutor ASGI app."""

    @pytest.mark.asyncio
    async def test_handler_executor_calls_node(self) -> None:
        """HandlerExecutor executes the handler node from scope and sends response."""
        mock_server = MagicMock()
        mock_request = MagicMock()
        mock_request.query = {}
        mock_response = AsyncMock()
        mock_request.response = mock_response
        mock_server.request_registry.current = mock_request

        mock_node = MagicMock(return_value="result_value")
        mock_node.metadata = {}

        executor = HandlerExecutor(mock_server)
        scope = {"_handler_node": mock_node}

        await executor(scope, None, None)

        mock_node.assert_called_once()
        mock_response.set_result.assert_called_once()
        mock_response.assert_awaited_once()


class TestResolveAppName:
    """Tests for Dispatcher._resolve_app_name."""

    def _make_dispatcher(self) -> Dispatcher:
        """Create Dispatcher with mock server."""
        mock_server = MagicMock()
        return Dispatcher(mock_server)

    def test_resolve_app_name_from_path(self) -> None:
        """/api/products/list -> 'api'."""
        dispatcher = self._make_dispatcher()
        assert dispatcher._resolve_app_name("/api/products/list") == "api"

    def test_resolve_app_name_root(self) -> None:
        """/ -> ''."""
        dispatcher = self._make_dispatcher()
        assert dispatcher._resolve_app_name("/") == ""


class TestDispatcherPerAppChain:
    """Tests for Dispatcher per-app chain delegation."""

    @pytest.mark.asyncio
    async def test_dispatcher_uses_per_app_chain(self) -> None:
        """Request to app with per-app chain uses that chain."""
        mock_server = MagicMock()
        mock_server.app_middleware_chains = {}

        mock_chain = AsyncMock()
        mock_server.app_middleware_chains["api"] = mock_chain

        mock_request = MagicMock()
        mock_request.path = "/api/test"
        mock_request.auth_tags = []
        mock_request.env_capabilities = []
        mock_request.query = {}

        mock_node = MagicMock()
        mock_node.metadata = {}
        mock_server.router.node.return_value = mock_node

        registry = MagicMock()
        registry.create = AsyncMock(return_value=mock_request)
        mock_server.request_registry = registry

        dispatcher = Dispatcher(mock_server)
        scope = {
            "type": "http",
            "headers": [],
            "auth": None,
            "auth_tags": [],
        }

        await dispatcher(scope, None, None)

        mock_chain.assert_awaited_once()
        assert scope["_handler_node"] is mock_node

    @pytest.mark.asyncio
    async def test_dispatcher_fallback_no_chain(self) -> None:
        """Request to app without per-app chain executes handler directly."""
        mock_server = MagicMock()
        mock_server.app_middleware_chains = {}

        mock_request = MagicMock()
        mock_request.path = "/public/index"
        mock_request.auth_tags = []
        mock_request.env_capabilities = []
        mock_request.query = {}
        mock_response = AsyncMock()
        mock_request.response = mock_response

        mock_node = MagicMock(return_value="result")
        mock_node.metadata = {}
        mock_server.router.node.return_value = mock_node

        registry = MagicMock()
        registry.create = AsyncMock(return_value=mock_request)
        mock_server.request_registry = registry

        dispatcher = Dispatcher(mock_server)
        scope = {
            "type": "http",
            "headers": [],
            "auth": None,
            "auth_tags": [],
        }

        await dispatcher(scope, None, None)

        mock_node.assert_called_once()
        mock_response.set_result.assert_called_once()
