# 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 WsxRegistry and WsxConnectionInfo."""

from __future__ import annotations

from unittest.mock import AsyncMock

import pytest

from genro_asgi.wsx.registry import WsxConnectionInfo, WsxRegistry


def _make_ws() -> AsyncMock:
    """Create a mock WebSocket."""
    ws = AsyncMock()
    ws.send_text = AsyncMock()
    return ws


def _make_scope(client: tuple[str, int] = ("127.0.0.1", 9000)) -> dict:
    """Create a minimal WebSocket scope."""
    return {"type": "websocket", "client": client, "headers": []}


class TestWsxConnectionInfo:
    """Tests for WsxConnectionInfo."""

    def test_basic_attributes(self) -> None:
        ws = _make_ws()
        scope = _make_scope()
        auth = {"identity": "admin", "tags": ["superadmin"]}
        info = WsxConnectionInfo("conn-1", ws, scope, auth)

        assert info.connection_id == "conn-1"
        assert info.websocket is ws
        assert info.scope is scope
        assert info.auth is auth

    def test_identity_from_auth(self) -> None:
        info = WsxConnectionInfo(
            "conn-1", _make_ws(), _make_scope(),
            auth={"identity": "admin", "tags": []},
        )
        assert info.identity == "admin"

    def test_identity_none_when_no_auth(self) -> None:
        info = WsxConnectionInfo("conn-1", _make_ws(), _make_scope())
        assert info.identity is None

    def test_client_from_scope(self) -> None:
        info = WsxConnectionInfo(
            "conn-1", _make_ws(), _make_scope(("10.0.0.1", 4321)),
        )
        assert info.client == ("10.0.0.1", 4321)


class TestWsxRegistry:
    """Tests for WsxRegistry."""

    def test_empty_registry(self) -> None:
        reg = WsxRegistry()
        assert len(reg) == 0
        assert list(reg) == []

    def test_register_and_get(self) -> None:
        reg = WsxRegistry()
        ws = _make_ws()
        scope = _make_scope()
        auth = {"identity": "admin", "tags": ["superadmin"]}

        info = reg.register("conn-1", ws, scope, auth)
        assert info.connection_id == "conn-1"
        assert len(reg) == 1
        assert "conn-1" in reg

        retrieved = reg.get("conn-1")
        assert retrieved is info

    def test_unregister(self) -> None:
        reg = WsxRegistry()
        reg.register("conn-1", _make_ws(), _make_scope())

        info = reg.unregister("conn-1")
        assert info is not None
        assert info.connection_id == "conn-1"
        assert len(reg) == 0
        assert "conn-1" not in reg

    def test_unregister_unknown(self) -> None:
        reg = WsxRegistry()
        assert reg.unregister("nonexistent") is None

    def test_get_unknown(self) -> None:
        reg = WsxRegistry()
        assert reg.get("nonexistent") is None

    def test_contains(self) -> None:
        reg = WsxRegistry()
        reg.register("conn-1", _make_ws(), _make_scope())
        assert "conn-1" in reg
        assert "conn-2" not in reg

    def test_iter(self) -> None:
        reg = WsxRegistry()
        reg.register("conn-1", _make_ws(), _make_scope())
        reg.register("conn-2", _make_ws(), _make_scope())

        infos = list(reg)
        assert len(infos) == 2
        ids = {i.connection_id for i in infos}
        assert ids == {"conn-1", "conn-2"}

    def test_find_by_identity(self) -> None:
        reg = WsxRegistry()
        reg.register("conn-1", _make_ws(), _make_scope(), {"identity": "admin", "tags": []})
        reg.register("conn-2", _make_ws(), _make_scope(), {"identity": "user1", "tags": []})
        reg.register("conn-3", _make_ws(), _make_scope(), {"identity": "admin", "tags": []})

        admin_conns = reg.find_by_identity("admin")
        assert len(admin_conns) == 2
        assert {c.connection_id for c in admin_conns} == {"conn-1", "conn-3"}

    def test_find_by_identity_no_match(self) -> None:
        reg = WsxRegistry()
        reg.register("conn-1", _make_ws(), _make_scope(), {"identity": "admin", "tags": []})
        assert reg.find_by_identity("nonexistent") == []

    @pytest.mark.asyncio
    async def test_broadcast(self) -> None:
        reg = WsxRegistry()
        ws1 = _make_ws()
        ws2 = _make_ws()
        reg.register("conn-1", ws1, _make_scope())
        reg.register("conn-2", ws2, _make_scope())

        sent = await reg.broadcast({"msg": "hello"})
        assert sent == 2
        ws1.send_text.assert_called_once()
        ws2.send_text.assert_called_once()

        # Verify WSX format
        sent_msg = ws1.send_text.call_args[0][0]
        assert sent_msg.startswith("WSX://")
        assert '"_broadcast"' in sent_msg

    @pytest.mark.asyncio
    async def test_broadcast_with_exclude(self) -> None:
        reg = WsxRegistry()
        ws1 = _make_ws()
        ws2 = _make_ws()
        reg.register("conn-1", ws1, _make_scope())
        reg.register("conn-2", ws2, _make_scope())

        sent = await reg.broadcast({"msg": "hello"}, exclude="conn-1")
        assert sent == 1
        ws1.send_text.assert_not_called()
        ws2.send_text.assert_called_once()

    @pytest.mark.asyncio
    async def test_broadcast_handles_send_failure(self) -> None:
        reg = WsxRegistry()
        ws1 = _make_ws()
        ws1.send_text.side_effect = RuntimeError("Connection lost")
        ws2 = _make_ws()
        reg.register("conn-1", ws1, _make_scope())
        reg.register("conn-2", ws2, _make_scope())

        sent = await reg.broadcast({"msg": "hello"})
        assert sent == 1  # Only ws2 succeeded

    @pytest.mark.asyncio
    async def test_send_to(self) -> None:
        reg = WsxRegistry()
        ws1 = _make_ws()
        ws2 = _make_ws()
        ws3 = _make_ws()
        reg.register("conn-1", ws1, _make_scope(), {"identity": "admin", "tags": []})
        reg.register("conn-2", ws2, _make_scope(), {"identity": "user1", "tags": []})
        reg.register("conn-3", ws3, _make_scope(), {"identity": "admin", "tags": []})

        sent = await reg.send_to("admin", {"msg": "for admins"})
        assert sent == 2
        ws1.send_text.assert_called_once()
        ws2.send_text.assert_not_called()
        ws3.send_text.assert_called_once()

    def test_multiple_register_unregister(self) -> None:
        reg = WsxRegistry()
        reg.register("conn-1", _make_ws(), _make_scope())
        reg.register("conn-2", _make_ws(), _make_scope())
        reg.register("conn-3", _make_ws(), _make_scope())
        assert len(reg) == 3

        reg.unregister("conn-2")
        assert len(reg) == 2
        assert "conn-2" not in reg
        assert "conn-1" in reg
        assert "conn-3" in reg
