"""Tests for networking module."""

import socket

import pytest

from gaol.networking.dns import (
    InvalidDNSServerError,
    generate_resolv_conf,
    get_default_dns_config,
    parse_resolv_conf,
    validate_dns_server,
)
from gaol.networking.models import (
    BridgeConfig,
    DNSConfig,
    NetworkConfigV2,
    PortMapping,
    PortMappingList,
    Protocol,
)
from gaol.networking.ports import (
    find_available_port,
    is_port_available,
)


class TestProtocol:
    """Tests for Protocol enum."""

    def test_protocol_values(self) -> None:
        """Test protocol enum values."""
        assert Protocol.TCP.value == "tcp"
        assert Protocol.UDP.value == "udp"
        assert Protocol.BOTH.value == "both"


class TestPortMapping:
    """Tests for PortMapping model."""

    def test_basic_mapping(self) -> None:
        """Test basic port mapping creation."""
        mapping = PortMapping(host_port=8080, container_port=80)
        assert mapping.host_port == 8080
        assert mapping.container_port == 80
        assert mapping.protocol == Protocol.TCP
        assert mapping.host_ip == "0.0.0.0"

    def test_udp_mapping(self) -> None:
        """Test UDP port mapping."""
        mapping = PortMapping(
            host_port=53,
            container_port=53,
            protocol=Protocol.UDP,
        )
        assert mapping.protocol == Protocol.UDP

    def test_privileged_port(self) -> None:
        """Test privileged port detection."""
        mapping = PortMapping(host_port=80, container_port=80)
        assert mapping.is_privileged is True

        mapping2 = PortMapping(host_port=8080, container_port=80)
        assert mapping2.is_privileged is False

    def test_invalid_port_range_low(self) -> None:
        """Test port validation rejects port 0."""
        with pytest.raises(ValueError):
            PortMapping(host_port=0, container_port=80)

    def test_invalid_port_range_high(self) -> None:
        """Test port validation rejects port > 65535."""
        with pytest.raises(ValueError):
            PortMapping(host_port=70000, container_port=80)


class TestPortMappingList:
    """Tests for PortMappingList model."""

    def test_get_host_ports(self) -> None:
        """Test getting all host ports."""
        mappings = PortMappingList(
            mappings=[
                PortMapping(host_port=8080, container_port=80),
                PortMapping(host_port=8443, container_port=443),
                PortMapping(host_port=53, container_port=53, protocol=Protocol.UDP),
            ]
        )

        all_ports = mappings.get_host_ports()
        assert all_ports == {8080, 8443, 53}

        tcp_ports = mappings.get_host_ports(Protocol.TCP)
        assert tcp_ports == {8080, 8443}

        udp_ports = mappings.get_host_ports(Protocol.UDP)
        assert udp_ports == {53}

    def test_conflict_detection(self) -> None:
        """Test port conflict detection."""
        list1 = PortMappingList(
            mappings=[
                PortMapping(host_port=8080, container_port=80),
            ]
        )
        list2 = PortMappingList(
            mappings=[
                PortMapping(host_port=8080, container_port=8080),  # Conflict
                PortMapping(host_port=8081, container_port=80),  # No conflict
            ]
        )

        conflicts = list1.has_conflict(list2)
        assert len(conflicts) == 1
        assert conflicts[0] == (8080, Protocol.TCP)

    def test_no_conflict_different_protocol(self) -> None:
        """Test no conflict when protocols differ."""
        list1 = PortMappingList(
            mappings=[
                PortMapping(host_port=8080, container_port=80, protocol=Protocol.TCP),
            ]
        )
        list2 = PortMappingList(
            mappings=[
                PortMapping(host_port=8080, container_port=80, protocol=Protocol.UDP),
            ]
        )

        conflicts = list1.has_conflict(list2)
        assert len(conflicts) == 0

    def test_len_and_iter(self) -> None:
        """Test len and iter methods."""
        mappings = PortMappingList(
            mappings=[
                PortMapping(host_port=8080, container_port=80),
                PortMapping(host_port=8443, container_port=443),
            ]
        )
        assert len(mappings) == 2
        assert list(mappings) == mappings.mappings


class TestDNSConfig:
    """Tests for DNSConfig model."""

    def test_basic_config(self) -> None:
        """Test basic DNS config creation."""
        config = DNSConfig(nameservers=["8.8.8.8", "8.8.4.4"])
        assert config.nameservers == ["8.8.8.8", "8.8.4.4"]

    def test_to_resolv_conf(self) -> None:
        """Test resolv.conf generation."""
        config = DNSConfig(
            nameservers=["8.8.8.8"],
            search_domains=["example.com"],
            options=["ndots:5"],
        )
        resolv = config.to_resolv_conf()
        assert "nameserver 8.8.8.8" in resolv
        assert "search example.com" in resolv
        assert "options ndots:5" in resolv

    def test_invalid_nameserver(self) -> None:
        """Test validation rejects invalid nameserver."""
        with pytest.raises(ValueError):
            DNSConfig(nameservers=["not-an-ip"])


class TestBridgeConfig:
    """Tests for BridgeConfig model."""

    def test_default_bridge(self) -> None:
        """Test default bridge config."""
        config = BridgeConfig()
        assert config.name == "br0"
        assert config.nat_enabled is True

    def test_custom_bridge(self) -> None:
        """Test custom bridge config."""
        config = BridgeConfig(
            name="virbr0",
            ip_address="192.168.100.1/24",
            dhcp_enabled=True,
        )
        assert config.name == "virbr0"
        assert config.ip_address == "192.168.100.1/24"


class TestPortAvailability:
    """Tests for port availability functions."""

    def test_is_port_available_free_port(self) -> None:
        """Test port availability on a free port."""
        # Find a free port and test
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.bind(("127.0.0.1", 0))
            port = s.getsockname()[1]

        # Port should be available after socket is closed
        assert is_port_available(port, Protocol.TCP, "127.0.0.1")

    def test_is_port_available_used_port(self) -> None:
        """Test port availability on a used port."""
        # Bind a port and test
        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))
            port = s.getsockname()[1]
            s.listen(1)

            # Port should not be available while bound
            assert not is_port_available(port, Protocol.TCP, "127.0.0.1")

    def test_find_available_port(self) -> None:
        """Test finding an available port."""
        port = find_available_port(50000, 50100)
        assert port is not None
        assert 50000 <= port <= 50100


class TestDNSValidation:
    """Tests for DNS validation functions."""

    def test_validate_dns_server_valid(self) -> None:
        """Test valid DNS server validation."""
        assert validate_dns_server("8.8.8.8") is True
        assert validate_dns_server("2001:4860:4860::8888") is True

    def test_validate_dns_server_invalid(self) -> None:
        """Test invalid DNS server validation."""
        with pytest.raises(InvalidDNSServerError):
            validate_dns_server("not.an.ip")

        with pytest.raises(InvalidDNSServerError):
            validate_dns_server("0.0.0.0")

    def test_parse_resolv_conf(self) -> None:
        """Test parsing resolv.conf content."""
        content = """
# Comment
nameserver 8.8.8.8
nameserver 8.8.4.4
search example.com local
options ndots:5 timeout:2
"""
        config = parse_resolv_conf(content)
        assert config.nameservers == ["8.8.8.8", "8.8.4.4"]
        assert config.search_domains == ["example.com", "local"]
        assert config.options == ["ndots:5", "timeout:2"]

    def test_generate_resolv_conf(self) -> None:
        """Test generating resolv.conf content."""
        config = DNSConfig(
            nameservers=["1.1.1.1", "1.0.0.1"],
            search_domains=["example.com"],
        )
        content = generate_resolv_conf(config, include_comments=False)
        assert "nameserver 1.1.1.1" in content
        assert "nameserver 1.0.0.1" in content
        assert "search example.com" in content

    def test_get_default_dns_config(self) -> None:
        """Test getting default DNS config."""
        config = get_default_dns_config(provider="cloudflare")
        assert "1.1.1.1" in config.nameservers
        assert "1.0.0.1" in config.nameservers

        config_google = get_default_dns_config(provider="google")
        assert "8.8.8.8" in config_google.nameservers


class TestNetworkConfigV2:
    """Tests for enhanced network configuration."""

    def test_from_legacy(self) -> None:
        """Test converting from legacy NetworkConfig."""
        from gaol.models.container import NetworkConfig, NetworkMode

        legacy = NetworkConfig(
            mode=NetworkMode.SHARED,
            ports={8080: 80, 8443: 443},
            dns=["8.8.8.8"],
            hostname="myhost",
        )

        v2 = NetworkConfigV2.from_legacy(legacy)

        assert v2.mode == "shared"
        assert len(v2.ports.mappings) == 2
        assert v2.dns.nameservers == ["8.8.8.8"]
        assert v2.hostname == "myhost"

    def test_to_legacy_ports(self) -> None:
        """Test converting ports back to legacy format."""
        v2 = NetworkConfigV2(
            ports=PortMappingList(
                mappings=[
                    PortMapping(host_port=8080, container_port=80),
                    PortMapping(host_port=53, container_port=53, protocol=Protocol.UDP),
                ]
            )
        )

        legacy_ports = v2.to_legacy_ports()
        # Only TCP ports are included
        assert legacy_ports == {8080: 80}
