# 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
#
#     http://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 WsxHandler — WebSocket/WSX receive loop and dispatch."""

from __future__ import annotations

import asyncio
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from genro_asgi.wsx.handler import WsxHandler
from genro_asgi.wsx.protocol import build_wsx_message, parse_wsx_message
from genro_asgi.request import RequestRegistry


class FakeWebSocket:
    """Simulates an ASGI WebSocket for testing WsxHandler.

    Feeds pre-loaded messages via iter_text(), collects sent responses.
    """

    def __init__(self, messages: list[str]) -> None:
        self._messages = list(messages)
        self.sent: list[str] = []
        self.accepted = False
        self.closed = False
        self._receive = AsyncMock()
        self._send = AsyncMock()

    async def accept(self, subprotocol: str | None = None, headers: Any = None) -> None:
        self.accepted = True

    async def send_text(self, data: str) -> None:
        self.sent.append(data)

    async def close(self, code: int = 1000, reason: str = "") -> None:
        self.closed = True

    async def iter_text(self):  # noqa: ANN201
        for msg in self._messages:
            yield msg


def _make_scope(
    headers: list[tuple[bytes, bytes]] | None = None,
) -> dict[str, Any]:
    """Create a minimal WebSocket ASGI scope."""
    return {
        "type": "websocket",
        "asgi": {"version": "3.0"},
        "http_version": "1.1",
        "scheme": "ws",
        "path": "/",
        "query_string": b"",
        "root_path": "",
        "headers": headers or [],
        "client": ("127.0.0.1", 9000),
    }


def _make_server(handler_result: Any = {"ok": True}) -> MagicMock:
    """Create a mock AsgiServer with router and request_registry."""
    server = MagicMock()
    server.request_registry = RequestRegistry()

    # Router.node() returns a callable handler node.
    # The node must be a sync callable (not async) because smartasync
    # checks iscoroutinefunction at import time. With a MagicMock,
    # iscoroutinefunction returns False, so smartasync treats it as
    # sync and offloads to asyncio.to_thread — the return value
    # must be the result directly, not a coroutine.
    node = MagicMock()
    node.metadata = {}
    node.return_value = handler_result
    server.router.node.return_value = node

    # Dispatcher without auth middleware attributes —
    # use spec=[] to prevent MagicMock from auto-creating attributes
    dispatcher = MagicMock(spec=[])
    server.dispatcher = dispatcher

    return server


class TestWsxHandlerControl:
    """Tests for WSX control messages (_wsx/* paths)."""

    @pytest.mark.asyncio
    async def test_ping_pong(self) -> None:
        msg = build_wsx_message(id="ping-1", method="GET", path="_wsx/ping")
        server = _make_server()
        handler = WsxHandler(server)

        ws = FakeWebSocket([msg])
        with patch("genro_asgi.wsx.handler.WebSocket", return_value=ws):
            await handler(_make_scope(), AsyncMock(), AsyncMock())

        assert len(ws.sent) == 1
        response = parse_wsx_message(ws.sent[0])
        assert response["id"] == "ping-1"
        assert response["status"] == 200
        assert response["data"] == "pong"

    @pytest.mark.asyncio
    async def test_unknown_control_path(self) -> None:
        msg = build_wsx_message(id="ctrl-1", method="GET", path="_wsx/unknown")
        server = _make_server()
        handler = WsxHandler(server)

        ws = FakeWebSocket([msg])
        with patch("genro_asgi.wsx.handler.WebSocket", return_value=ws):
            await handler(_make_scope(), AsyncMock(), AsyncMock())

        assert len(ws.sent) == 1
        response = parse_wsx_message(ws.sent[0])
        assert response["id"] == "ctrl-1"
        assert response["status"] == 404


class TestWsxHandlerDispatch:
    """Tests for WSX message dispatch through router."""

    @pytest.mark.asyncio
    async def test_dispatch_simple_get(self) -> None:
        msg = build_wsx_message(id="req-1", method="GET", path="/api/hello")
        server = _make_server(handler_result={"greeting": "world"})
        handler = WsxHandler(server)

        ws = FakeWebSocket([msg])
        with patch("genro_asgi.wsx.handler.WebSocket", return_value=ws):
            await handler(_make_scope(), AsyncMock(), AsyncMock())

        # Wait for task completion
        await asyncio.sleep(0.05)

        assert len(ws.sent) >= 1
        response = parse_wsx_message(ws.sent[0])
        assert response["id"] == "req-1"
        # Debug: print response if it fails
        assert response["status"] == 200, f"Response: {response}"
        assert response["data"] == {"greeting": "world"}

    @pytest.mark.asyncio
    async def test_dispatch_with_query_params(self) -> None:
        msg = build_wsx_message(
            id="req-2", method="GET", path="/api/users",
            query={"limit": "10", "offset": "0"},
        )
        server = _make_server()
        handler = WsxHandler(server)

        ws = FakeWebSocket([msg])
        with patch("genro_asgi.wsx.handler.WebSocket", return_value=ws):
            await handler(_make_scope(), AsyncMock(), AsyncMock())

        await asyncio.sleep(0.05)

        # Verify router was called with the right path
        server.router.node.assert_called_once()
        call_args = server.router.node.call_args
        assert call_args[0][0] == "/api/users"

    @pytest.mark.asyncio
    async def test_non_wsx_message_ignored(self) -> None:
        server = _make_server()
        handler = WsxHandler(server)

        ws = FakeWebSocket(["not a WSX message", "plain text"])
        with patch("genro_asgi.wsx.handler.WebSocket", return_value=ws):
            await handler(_make_scope(), AsyncMock(), AsyncMock())

        assert len(ws.sent) == 0

    @pytest.mark.asyncio
    async def test_multiple_messages(self) -> None:
        msg1 = build_wsx_message(id="m1", method="GET", path="/api/a")
        msg2 = build_wsx_message(id="m2", method="GET", path="/api/b")
        server = _make_server()
        handler = WsxHandler(server)

        ws = FakeWebSocket([msg1, msg2])
        with patch("genro_asgi.wsx.handler.WebSocket", return_value=ws):
            await handler(_make_scope(), AsyncMock(), AsyncMock())

        await asyncio.sleep(0.05)

        assert len(ws.sent) == 2
        ids = {parse_wsx_message(s)["id"] for s in ws.sent}
        assert ids == {"m1", "m2"}

    @pytest.mark.asyncio
    async def test_mixed_control_and_dispatch(self) -> None:
        ping = build_wsx_message(id="p1", method="GET", path="_wsx/ping")
        req = build_wsx_message(id="r1", method="GET", path="/api/test")
        server = _make_server()
        handler = WsxHandler(server)

        ws = FakeWebSocket([ping, req])
        with patch("genro_asgi.wsx.handler.WebSocket", return_value=ws):
            await handler(_make_scope(), AsyncMock(), AsyncMock())

        await asyncio.sleep(0.05)

        assert len(ws.sent) == 2
        responses = {parse_wsx_message(s)["id"]: parse_wsx_message(s) for s in ws.sent}
        assert responses["p1"]["data"] == "pong"
        assert responses["r1"]["status"] == 200


class TestWsxHandlerAuth:
    """Tests for WebSocket authentication via handshake headers."""

    @pytest.mark.asyncio
    async def test_auth_from_headers(self) -> None:
        """Auth middleware in the chain is found and called."""
        server = _make_server()

        # Simulate an auth middleware in the chain
        auth_mw = MagicMock()
        auth_mw._auth_config = {"basic": {}}
        auth_mw._authenticate = MagicMock(
            return_value={"identity": "admin", "tags": ["superadmin"], "backend": "basic"}
        )
        server.dispatcher = auth_mw

        handler = WsxHandler(server)
        scope = _make_scope(headers=[
            (b"authorization", b"Basic YWRtaW46c2VjcmV0"),
        ])

        ws = FakeWebSocket([])
        with patch("genro_asgi.wsx.handler.WebSocket", return_value=ws):
            await handler(scope, AsyncMock(), AsyncMock())

        assert scope["auth"] == {"identity": "admin", "tags": ["superadmin"], "backend": "basic"}
        assert scope["_filters"]["auth_tags"] == ["superadmin"]

    @pytest.mark.asyncio
    async def test_no_auth_middleware(self) -> None:
        """No auth middleware — auth is None, tags empty."""
        server = _make_server()
        handler = WsxHandler(server)

        ws = FakeWebSocket([])
        scope = _make_scope()
        with patch("genro_asgi.wsx.handler.WebSocket", return_value=ws):
            await handler(scope, AsyncMock(), AsyncMock())

        assert scope["auth"] is None
        assert scope["_filters"]["auth_tags"] == []


class TestWsxHandlerRegistry:
    """Tests for connection registration/unregistration."""

    @pytest.mark.asyncio
    async def test_connection_registered_and_unregistered(self) -> None:
        server = _make_server()
        handler = WsxHandler(server)

        # Before connection
        assert len(handler.registry) == 0

        ws = FakeWebSocket([])
        with patch("genro_asgi.wsx.handler.WebSocket", return_value=ws):
            await handler(_make_scope(), AsyncMock(), AsyncMock())

        # After connection closes (iter_text exhausted)
        assert len(handler.registry) == 0  # Unregistered in finally


class TestWsxHandlerErrors:
    """Tests for error handling in dispatch."""

    @pytest.mark.asyncio
    async def test_handler_exception_returns_500(self) -> None:
        msg = build_wsx_message(id="err-1", method="GET", path="/api/fail")
        server = _make_server()

        # Make router.node() return a handler that raises (sync, see _make_server note)
        node = MagicMock()
        node.metadata = {}
        node.side_effect = RuntimeError("Something broke")
        server.router.node.return_value = node

        handler = WsxHandler(server)
        ws = FakeWebSocket([msg])
        with patch("genro_asgi.wsx.handler.WebSocket", return_value=ws):
            await handler(_make_scope(), AsyncMock(), AsyncMock())

        await asyncio.sleep(0.05)

        assert len(ws.sent) >= 1
        response = parse_wsx_message(ws.sent[0])
        assert response["id"] == "err-1"
        assert response["status"] == 500

    @pytest.mark.asyncio
    async def test_http_exception_returns_status_code(self) -> None:
        from genro_asgi.exceptions import HTTPNotFound

        msg = build_wsx_message(id="err-2", method="GET", path="/api/missing")
        server = _make_server()

        server.router.node.side_effect = HTTPNotFound("Not found")

        handler = WsxHandler(server)
        ws = FakeWebSocket([msg])
        with patch("genro_asgi.wsx.handler.WebSocket", return_value=ws):
            await handler(_make_scope(), AsyncMock(), AsyncMock())

        await asyncio.sleep(0.05)

        assert len(ws.sent) >= 1
        response = parse_wsx_message(ws.sent[0])
        assert response["id"] == "err-2"
        assert response["status"] == 404
