"""High-value tests for security/ssrf_validator.py - SSRF Prevention."""

import socket
from unittest.mock import patch

import pytest


from local_deep_research.security.ssrf_validator import (
    is_ip_blocked,
    validate_url,
    get_safe_url,
    ALLOWED_SCHEMES,
    ALWAYS_BLOCKED_METADATA_IPS,
)


# ---------------------------------------------------------------------------
# is_ip_blocked - loopback / RFC1918 / CGNAT / link-local / IPv6
# ---------------------------------------------------------------------------


class TestIsIpBlockedLoopback:
    """Loopback address blocking."""

    def test_ipv4_loopback_blocked_by_default(self):
        assert is_ip_blocked("127.0.0.1") is True

    def test_ipv4_loopback_127_0_0_2_blocked(self):
        assert is_ip_blocked("127.0.0.2") is True

    def test_ipv6_loopback_blocked_by_default(self):
        assert is_ip_blocked("::1") is True

    def test_ipv4_loopback_allowed_with_flag(self):
        assert is_ip_blocked("127.0.0.1", allow_localhost=True) is False

    def test_ipv6_loopback_allowed_with_flag(self):
        assert is_ip_blocked("::1", allow_localhost=True) is False


class TestIsIpBlockedRFC1918:
    """RFC 1918 private ranges."""

    def test_10_network_blocked(self):
        assert is_ip_blocked("10.0.0.1") is True

    def test_172_16_blocked(self):
        assert is_ip_blocked("172.16.0.1") is True

    def test_192_168_blocked(self):
        assert is_ip_blocked("192.168.1.1") is True

    def test_10_network_allowed_with_private_flag(self):
        assert is_ip_blocked("10.0.0.1", allow_private_ips=True) is False

    def test_172_16_allowed_with_private_flag(self):
        assert is_ip_blocked("172.16.0.1", allow_private_ips=True) is False

    def test_192_168_allowed_with_private_flag(self):
        assert is_ip_blocked("192.168.1.1", allow_private_ips=True) is False


class TestIsIpBlockedSpecialRanges:
    """CGNAT, link-local, IPv6 private ranges."""

    def test_cgnat_blocked(self):
        assert is_ip_blocked("100.64.0.1") is True

    def test_link_local_blocked(self):
        assert is_ip_blocked("169.254.1.1") is True

    def test_ipv6_unique_local_blocked(self):
        assert is_ip_blocked("fc00::1") is True

    def test_ipv6_link_local_blocked(self):
        assert is_ip_blocked("fe80::1") is True

    def test_cgnat_allowed_with_private_flag(self):
        assert is_ip_blocked("100.64.0.1", allow_private_ips=True) is False

    def test_link_local_allowed_with_private_flag(self):
        assert is_ip_blocked("169.254.1.1", allow_private_ips=True) is False

    def test_loopback_also_allowed_with_private_flag(self):
        assert is_ip_blocked("127.0.0.1", allow_private_ips=True) is False


class TestIsIpBlockedCloudMetadata:
    """Cloud-provider metadata endpoints always blocked under all flags."""

    @pytest.mark.parametrize("ip", sorted(ALWAYS_BLOCKED_METADATA_IPS))
    def test_metadata_ip_blocked_default(self, ip):
        assert is_ip_blocked(ip) is True

    @pytest.mark.parametrize("ip", sorted(ALWAYS_BLOCKED_METADATA_IPS))
    def test_metadata_ip_blocked_with_allow_localhost(self, ip):
        assert is_ip_blocked(ip, allow_localhost=True) is True

    @pytest.mark.parametrize("ip", sorted(ALWAYS_BLOCKED_METADATA_IPS))
    def test_metadata_ip_blocked_with_allow_private_ips(self, ip):
        assert is_ip_blocked(ip, allow_private_ips=True) is True


class TestIsIpBlockedIPv4Mapped:
    """IPv4-mapped IPv6 address unwrapping."""

    def test_ipv4_mapped_loopback_blocked(self):
        assert is_ip_blocked("::ffff:127.0.0.1") is True

    def test_ipv4_mapped_private_blocked(self):
        assert is_ip_blocked("::ffff:10.0.0.1") is True

    def test_ipv4_mapped_aws_metadata_blocked(self):
        assert is_ip_blocked("::ffff:169.254.169.254") is True

    def test_ipv4_mapped_public_not_blocked(self):
        assert is_ip_blocked("::ffff:8.8.8.8") is False


class TestIsIpBlockedPublicAndInvalid:
    """Public IPs and invalid input."""

    def test_public_ip_not_blocked(self):
        assert is_ip_blocked("8.8.8.8") is False

    def test_another_public_ip(self):
        assert is_ip_blocked("1.1.1.1") is False

    def test_invalid_ip_returns_false(self):
        assert is_ip_blocked("not-an-ip") is False

    def test_empty_string_returns_false(self):
        assert is_ip_blocked("") is False


# ---------------------------------------------------------------------------
# validate_url
# ---------------------------------------------------------------------------


class TestValidateUrlScheme:
    """Scheme validation."""

    def test_ftp_scheme_rejected(self):
        assert validate_url("ftp://example.com/file") is False

    def test_file_scheme_rejected(self):
        assert validate_url("file:///etc/passwd") is False

    def test_javascript_scheme_rejected(self):
        assert validate_url("javascript:alert(1)") is False


class TestValidateUrlHostname:
    """Hostname and IP-based URL validation."""

    def test_missing_hostname_rejected(self):
        assert validate_url("http://") is False

    def test_ip_loopback_url_blocked(self):
        assert validate_url("http://127.0.0.1/admin") is False

    def test_ip_private_url_blocked(self):
        assert validate_url("http://192.168.1.1/") is False

    def test_aws_metadata_url_blocked(self):
        assert validate_url("http://169.254.169.254/latest/meta-data/") is False

    @patch("local_deep_research.security.ssrf_validator.socket.getaddrinfo")
    def test_hostname_resolving_to_public_ip_allowed(self, mock_dns):
        mock_dns.return_value = [
            (socket.AF_INET, socket.SOCK_STREAM, 0, "", ("93.184.216.34", 0))
        ]
        assert validate_url("http://example.com") is True

    @patch("local_deep_research.security.ssrf_validator.socket.getaddrinfo")
    def test_hostname_resolving_to_private_ip_blocked(self, mock_dns):
        mock_dns.return_value = [
            (socket.AF_INET, socket.SOCK_STREAM, 0, "", ("10.0.0.1", 0))
        ]
        assert validate_url("http://internal.corp") is False

    @patch("local_deep_research.security.ssrf_validator.socket.getaddrinfo")
    def test_dns_failure_blocks_url(self, mock_dns):
        mock_dns.side_effect = socket.gaierror("Name resolution failed")
        assert validate_url("http://nonexistent.test") is False


class TestValidateUrlFlagPassthrough:
    """Flag pass-through to is_ip_blocked."""

    @patch("local_deep_research.security.ssrf_validator.socket.getaddrinfo")
    def test_allow_localhost_passes_loopback(self, mock_dns):
        mock_dns.return_value = [
            (socket.AF_INET, socket.SOCK_STREAM, 0, "", ("127.0.0.1", 0))
        ]
        assert validate_url("http://127.0.0.1/", allow_localhost=True) is True

    @patch("local_deep_research.security.ssrf_validator.socket.getaddrinfo")
    def test_allow_private_ips_passes_private(self, mock_dns):
        mock_dns.return_value = [
            (socket.AF_INET, socket.SOCK_STREAM, 0, "", ("10.0.0.1", 0))
        ]
        assert validate_url("http://10.0.0.1/", allow_private_ips=True) is True

    @patch("local_deep_research.security.ssrf_validator.socket.getaddrinfo")
    def test_multi_address_dns_one_private_blocked(self, mock_dns):
        mock_dns.return_value = [
            (socket.AF_INET, socket.SOCK_STREAM, 0, "", ("93.184.216.34", 0)),
            (socket.AF_INET, socket.SOCK_STREAM, 0, "", ("10.0.0.1", 0)),
        ]
        assert validate_url("http://dual.example.com") is False


class TestIsIpBlockedZeroNetwork:
    """0.0.0.0/8 range blocking."""

    def test_0001_blocked(self):
        assert is_ip_blocked("0.0.0.1") is True

    def test_0001_blocked_even_with_allow_private(self):
        """0.0.0.0/8 is NOT in PRIVATE_RANGES, so it stays blocked."""
        assert is_ip_blocked("0.0.0.1", allow_private_ips=True) is True


class TestValidateUrlNoBypass:
    """Verify validate_url performs real validation (no test bypass)."""

    def test_ftp_scheme_blocked(self):
        assert validate_url("ftp://evil.com") is False

    def test_https_public_url_allowed(self):
        with patch("socket.getaddrinfo") as mock_dns:
            mock_dns.return_value = [(2, 1, 6, "", ("93.184.216.34", 0))]
            assert validate_url("https://example.com") is True


# ---------------------------------------------------------------------------
# get_safe_url
# ---------------------------------------------------------------------------


class TestGetSafeUrl:
    """get_safe_url() wrapper."""

    def test_none_returns_default(self):
        assert get_safe_url(None, "fallback") == "fallback"

    def test_empty_returns_default(self):
        assert get_safe_url("", "fallback") == "fallback"

    def test_safe_url_passes_through(self):
        """Exercises real validate_url end-to-end. Mocking only DNS means
        a regression in scheme/IP/host-extraction logic would surface
        here, not just whichever code path the mock previously covered.
        """
        with patch(
            "local_deep_research.security.ssrf_validator.socket.getaddrinfo",
            return_value=[(2, 1, 6, "", ("93.184.216.34", 0))],
        ):
            assert get_safe_url("http://example.com") == "http://example.com"

    def test_unsafe_url_returns_default(self):
        """Real-validator pass-through: the IP literal 10.0.0.5 is
        rejected without DNS, so removing the RFC1918 entry from
        PRIVATE_IP_RANGES would cause this test to fail.
        """
        assert get_safe_url("http://10.0.0.5/", "safe") == "safe"

    def test_none_url_none_default(self):
        assert get_safe_url(None) is None


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------


class TestConstants:
    def test_allowed_schemes_http_https(self):
        assert ALLOWED_SCHEMES == {"http", "https"}

    def test_always_blocked_metadata_ips_membership(self):
        """Lock in the exact membership of the always-blocked set so a
        future contributor accidentally removing an IP fails loudly."""
        assert ALWAYS_BLOCKED_METADATA_IPS == frozenset(
            {
                "169.254.169.254",
                "169.254.170.2",
                "169.254.170.23",
                "169.254.0.23",
                "100.100.100.200",
            }
        )
