"""Tests for llmhosts.ports — adaptive port detection."""

from __future__ import annotations

import socket
from unittest.mock import AsyncMock, patch

import pytest

from llmhosts.ports import DEFAULT_UP_PORT, find_available_port, is_port_free, start_ipv6_bridge


@pytest.mark.unit
class TestIsPortFree:
    """Connect-based port availability checking."""

    def test_free_port(self) -> None:
        """A port with nothing listening is free."""
        # Use an OS-assigned port that we immediately close
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.bind(("127.0.0.1", 0))
            _, port = s.getsockname()
        # Socket closed — port should be free
        assert is_port_free(port) is True

    def test_occupied_port(self) -> None:
        """A port with a listening socket is not free."""
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            s.bind(("127.0.0.1", 0))
            s.listen(1)
            _, port = s.getsockname()
            assert is_port_free(port) is False

    def test_connect_refused_means_free(self) -> None:
        """ConnectionRefusedError on connect means nothing is listening."""
        with patch("llmhosts.ports.socket.socket") as mock_sock:
            mock_instance = mock_sock.return_value.__enter__.return_value
            mock_instance.connect.side_effect = ConnectionRefusedError
            assert is_port_free(99999) is True

    def test_connect_success_means_occupied(self) -> None:
        """Successful connect means something is listening."""
        with patch("llmhosts.ports.socket.socket") as mock_sock:
            mock_instance = mock_sock.return_value.__enter__.return_value
            mock_instance.connect.return_value = None
            assert is_port_free(99999) is False


@pytest.mark.unit
class TestFindAvailablePort:
    """Adaptive port scanning."""

    def test_preferred_port_free(self) -> None:
        """Returns preferred port when it's available."""
        with patch("llmhosts.ports.is_port_free", return_value=True):
            assert find_available_port(11400) == 11400

    def test_auto_increment_on_conflict(self) -> None:
        """Scans upward when preferred is taken."""

        def _free_after_two(port: int) -> bool:
            return port >= 11402

        with patch("llmhosts.ports.is_port_free", side_effect=_free_after_two):
            assert find_available_port(11400) == 11402

    def test_fallback_to_zero_when_all_taken(self) -> None:
        """Returns 0 (OS-assigned) when all scanned ports are taken."""
        with patch("llmhosts.ports.is_port_free", return_value=False):
            assert find_available_port(11400) == 0

    def test_default_port_constant(self) -> None:
        """DEFAULT_UP_PORT is 11400."""
        assert DEFAULT_UP_PORT == 11400


@pytest.mark.unit
class TestIPv6Bridge:
    """IPv6 loopback bridge for Windows anti-squatting."""

    async def test_returns_none_on_non_windows(self) -> None:
        """Bridge is only needed on Windows."""
        with patch("llmhosts.ports.platform.system", return_value="Linux"):
            result = await start_ipv6_bridge(11400)
            assert result is None

    async def test_returns_none_on_macos(self) -> None:
        """Bridge is only needed on Windows."""
        with patch("llmhosts.ports.platform.system", return_value="Darwin"):
            result = await start_ipv6_bridge(11400)
            assert result is None

    async def test_returns_server_on_windows(self) -> None:
        """Bridge starts on Windows when port is available."""
        with patch("llmhosts.ports.platform.system", return_value="Windows"):
            mock_server = AsyncMock()
            with patch("llmhosts.ports.asyncio.start_server", return_value=mock_server):
                result = await start_ipv6_bridge(11400)
                assert result is mock_server

    async def test_handles_oserror_gracefully(self) -> None:
        """Returns None if IPv6 port is already taken."""
        with (
            patch("llmhosts.ports.platform.system", return_value="Windows"),
            patch("llmhosts.ports.asyncio.start_server", side_effect=OSError("Address in use")),
        ):
            result = await start_ipv6_bridge(11400)
            assert result is None
