"""Tests for mesh mDNS/DNS-SD discovery and manual peer list."""

from __future__ import annotations

import sys
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from radix_edge.mesh.discovery import ManualPeerList, MeshDiscovery, SERVICE_TYPE
from radix_edge.mesh.security import mesh_key_prefix


class TestManualPeerList:
    def test_parse_single_peer(self):
        pl = ManualPeerList("10.0.0.1:8080")
        assert len(pl.peers) == 1
        assert pl.peers[0] == {"host": "10.0.0.1", "port": 8080}

    def test_parse_multiple_peers(self):
        pl = ManualPeerList("10.0.0.1:8080,10.0.0.2:9090")
        assert len(pl.peers) == 2
        assert pl.peers[0] == {"host": "10.0.0.1", "port": 8080}
        assert pl.peers[1] == {"host": "10.0.0.2", "port": 9090}

    def test_parse_with_spaces(self):
        pl = ManualPeerList("  10.0.0.1:8080 , 10.0.0.2:9090  ")
        assert len(pl.peers) == 2

    def test_empty_string(self):
        pl = ManualPeerList("")
        assert len(pl.peers) == 0

    def test_whitespace_only(self):
        pl = ManualPeerList("   ")
        assert len(pl.peers) == 0

    def test_invalid_entry_missing_port(self):
        pl = ManualPeerList("10.0.0.1")
        assert len(pl.peers) == 0

    def test_invalid_port_non_numeric(self):
        pl = ManualPeerList("10.0.0.1:abc")
        assert len(pl.peers) == 0

    def test_mixed_valid_and_invalid(self):
        pl = ManualPeerList("10.0.0.1:8080,badentry,10.0.0.2:9090")
        assert len(pl.peers) == 2


class TestMeshDiscovery:
    def test_init_properties(self):
        d = MeshDiscovery("node-a", 8080, "my-key", gpu_count=2)
        assert d.node_id == "node-a"
        assert d.port == 8080
        assert d.gpu_count == 2
        assert d._key_prefix == mesh_key_prefix("my-key")
        assert d.discovered_peers == {}

    @pytest.mark.asyncio
    async def test_start_without_zeroconf_logs_warning(self):
        """When zeroconf is not installed, start() should not crash."""
        with patch("radix_edge.mesh.discovery.ZEROCONF_AVAILABLE", False):
            d = MeshDiscovery("node-a", 8080, "key")
            await d.start()  # Should not raise
            await d.stop()   # Should not raise

    @pytest.mark.asyncio
    async def test_stop_idempotent(self):
        d = MeshDiscovery("node-a", 8080, "key")
        await d.stop()
        await d.stop()  # Should not raise

    def test_discovered_peers_returns_copy(self):
        d = MeshDiscovery("node-a", 8080, "key")
        d._discovered["node-b"] = {"host": "10.0.0.2", "port": 8080}

        peers = d.discovered_peers
        peers["node-c"] = {"host": "10.0.0.3", "port": 8080}

        # Original should not be modified
        assert "node-c" not in d._discovered
        assert "node-b" in d._discovered

    def test_on_service_state_change_filters_different_key(self):
        """Peers with different mesh key prefix should be ignored."""
        d = MeshDiscovery("node-a", 8080, "key-alpha")

        # Mock the zeroconf and ServiceInfo
        mock_zc = MagicMock()
        mock_info = MagicMock()
        mock_info.properties = {
            b"node_id": b"node-b",
            b"mesh_key_prefix": b"different",
        }
        mock_info.parsed_addresses.return_value = ["10.0.0.2"]
        mock_info.port = 8080
        mock_zc.get_service_info.return_value = mock_info

        with patch("radix_edge.mesh.discovery.ZEROCONF_AVAILABLE", True):
            try:
                from zeroconf import ServiceStateChange
                d._on_service_state_change(mock_zc, SERVICE_TYPE, "node-b._radix-mesh._tcp.local.", ServiceStateChange.Added)
            except ImportError:
                # zeroconf not installed, simulate the state change enum
                d._on_service_state_change(mock_zc, SERVICE_TYPE, "node-b._radix-mesh._tcp.local.", "Added")

        assert "node-b" not in d.discovered_peers

    def test_on_service_state_change_skips_self(self):
        """Should not add self to discovered peers."""
        d = MeshDiscovery("node-a", 8080, "key-alpha")
        prefix = mesh_key_prefix("key-alpha")

        mock_zc = MagicMock()
        mock_info = MagicMock()
        mock_info.properties = {
            b"node_id": b"node-a",
            b"mesh_key_prefix": prefix.encode(),
        }
        mock_info.parsed_addresses.return_value = ["10.0.0.1"]
        mock_info.port = 8080
        mock_zc.get_service_info.return_value = mock_info

        with patch("radix_edge.mesh.discovery.ZEROCONF_AVAILABLE", True):
            try:
                from zeroconf import ServiceStateChange
                d._on_service_state_change(mock_zc, SERVICE_TYPE, "node-a._radix-mesh._tcp.local.", ServiceStateChange.Added)
            except ImportError:
                d._on_service_state_change(mock_zc, SERVICE_TYPE, "node-a._radix-mesh._tcp.local.", "Added")

        assert "node-a" not in d.discovered_peers

    def test_on_service_state_change_adds_matching_peer(self):
        """Should add a peer with matching key prefix."""
        d = MeshDiscovery("node-a", 8080, "key-alpha")
        prefix = mesh_key_prefix("key-alpha")

        mock_zc = MagicMock()
        mock_info = MagicMock()
        mock_info.properties = {
            b"node_id": b"node-b",
            b"mesh_key_prefix": prefix.encode(),
        }
        mock_info.parsed_addresses.return_value = ["10.0.0.2"]
        mock_info.port = 9090
        mock_zc.get_service_info.return_value = mock_info

        with patch("radix_edge.mesh.discovery.ZEROCONF_AVAILABLE", True):
            try:
                from zeroconf import ServiceStateChange
                d._on_service_state_change(mock_zc, SERVICE_TYPE, "node-b._radix-mesh._tcp.local.", ServiceStateChange.Added)
            except ImportError:
                d._on_service_state_change(mock_zc, SERVICE_TYPE, "node-b._radix-mesh._tcp.local.", "Added")

        assert "node-b" in d.discovered_peers
        assert d.discovered_peers["node-b"]["host"] == "10.0.0.2"
        assert d.discovered_peers["node-b"]["port"] == 9090
