"""
Tests for security/ip_ranges.py

Tests cover:
- PRIVATE_IP_RANGES constant contains expected networks
- Private IP detection works correctly
- IPv4 and IPv6 address validation
"""

import ipaddress


def _ip_is_private(ip_str: str) -> bool:
    """Module-level helper used across the IPv6-transition-prefix test
    classes. (TestPrivateIPDetection has its own copy that returns
    False for invalid IPs; this one is for callers that pass only
    well-formed addresses.)"""
    from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

    ip = ipaddress.ip_address(ip_str)
    return any(ip in network for network in PRIVATE_IP_RANGES)


class TestPrivateIPRanges:
    """Tests for PRIVATE_IP_RANGES constant."""

    def test_private_ip_ranges_is_list(self):
        """PRIVATE_IP_RANGES should be a list."""
        from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

        assert isinstance(PRIVATE_IP_RANGES, list)

    def test_private_ip_ranges_not_empty(self):
        """PRIVATE_IP_RANGES should not be empty."""
        from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

        assert len(PRIVATE_IP_RANGES) > 0

    def test_private_ip_ranges_contains_ip_networks(self):
        """All entries should be ip_network objects."""
        from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

        for network in PRIVATE_IP_RANGES:
            assert isinstance(
                network, (ipaddress.IPv4Network, ipaddress.IPv6Network)
            )

    def test_contains_loopback_ipv4(self):
        """Should contain IPv4 loopback (127.0.0.0/8)."""
        from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

        loopback = ipaddress.ip_network("127.0.0.0/8")
        assert loopback in PRIVATE_IP_RANGES

    def test_contains_loopback_ipv6(self):
        """Should contain IPv6 loopback (::1/128)."""
        from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

        loopback = ipaddress.ip_network("::1/128")
        assert loopback in PRIVATE_IP_RANGES

    def test_contains_rfc1918_class_a(self):
        """Should contain RFC1918 Class A private (10.0.0.0/8)."""
        from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

        private_a = ipaddress.ip_network("10.0.0.0/8")
        assert private_a in PRIVATE_IP_RANGES

    def test_contains_rfc1918_class_b(self):
        """Should contain RFC1918 Class B private (172.16.0.0/12)."""
        from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

        private_b = ipaddress.ip_network("172.16.0.0/12")
        assert private_b in PRIVATE_IP_RANGES

    def test_contains_rfc1918_class_c(self):
        """Should contain RFC1918 Class C private (192.168.0.0/16)."""
        from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

        private_c = ipaddress.ip_network("192.168.0.0/16")
        assert private_c in PRIVATE_IP_RANGES

    def test_contains_cgnat(self):
        """Should contain CGNAT range (100.64.0.0/10)."""
        from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

        cgnat = ipaddress.ip_network("100.64.0.0/10")
        assert cgnat in PRIVATE_IP_RANGES

    def test_contains_link_local_ipv4(self):
        """Should contain IPv4 link-local (169.254.0.0/16)."""
        from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

        link_local = ipaddress.ip_network("169.254.0.0/16")
        assert link_local in PRIVATE_IP_RANGES

    def test_contains_link_local_ipv6(self):
        """Should contain IPv6 link-local (fe80::/10)."""
        from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

        link_local = ipaddress.ip_network("fe80::/10")
        assert link_local in PRIVATE_IP_RANGES

    def test_contains_ipv6_unique_local(self):
        """Should contain IPv6 unique local (fc00::/7)."""
        from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

        unique_local = ipaddress.ip_network("fc00::/7")
        assert unique_local in PRIVATE_IP_RANGES

    def test_contains_ipv4_unspecified(self):
        """Should contain 0.0.0.0/8 ('this' network — IPv4 unspecified).
        Linux routes connect() to 0.0.0.0 to local host."""
        from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

        unspecified_v4 = ipaddress.ip_network("0.0.0.0/8")
        assert unspecified_v4 in PRIVATE_IP_RANGES

    def test_contains_ipv6_unspecified(self):
        """Should contain ::/128 (IPv6 unspecified). Linux routes
        connect() to [::] to local host (same semantics as 0.0.0.0).
        Added in PR #3873 after empirical bypass discovery."""
        from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

        unspecified_v6 = ipaddress.ip_network("::/128")
        assert unspecified_v6 in PRIVATE_IP_RANGES

    def test_contains_6to4_prefix(self):
        """Should contain 2002::/16 (6to4 transition prefix). Wraps
        private IPv4 destinations on hosts with sit0 routes."""
        from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

        sixto4 = ipaddress.ip_network("2002::/16")
        assert sixto4 in PRIVATE_IP_RANGES

    def test_contains_nat64_prefix(self):
        """Should contain 64:ff9b::/96 (NAT64 well-known prefix)."""
        from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

        nat64 = ipaddress.ip_network("64:ff9b::/96")
        assert nat64 in PRIVATE_IP_RANGES

    def test_contains_nat64_local_use_prefix(self):
        """Should contain 64:ff9b:1::/48 (RFC 8215 NAT64 local-use prefix)
        — same SSRF threat class as the well-known prefix; missing it is
        the exact bypass paid out as a HackerOne bounty against
        ssrf_filter."""
        from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

        nat64_local = ipaddress.ip_network("64:ff9b:1::/48")
        assert nat64_local in PRIVATE_IP_RANGES

    def test_nat64_prefixes_constant_exposes_both(self):
        """NAT64_PREFIXES must contain exactly the two NAT64 prefixes —
        used by validators to identify which deny entries the
        security.allow_nat64 env carve-out should skip."""
        from local_deep_research.security.ip_ranges import NAT64_PREFIXES

        assert ipaddress.ip_network("64:ff9b::/96") in NAT64_PREFIXES
        assert ipaddress.ip_network("64:ff9b:1::/48") in NAT64_PREFIXES
        assert len(NAT64_PREFIXES) == 2

    def test_contains_teredo_prefix(self):
        """Should contain 2001::/32 (Teredo)."""
        from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

        teredo = ipaddress.ip_network("2001::/32")
        assert teredo in PRIVATE_IP_RANGES

    def test_contains_ipv6_discard_prefix(self):
        """Should contain 100::/64 (RFC 6666 discard)."""
        from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

        discard = ipaddress.ip_network("100::/64")
        assert discard in PRIVATE_IP_RANGES

    def test_contains_ipv4_compatible_ipv6_prefix(self):
        """Should contain ::/96 (RFC 4291 IPv4-Compatible IPv6 — DEPRECATED).
        Same SSRF threat class as the transition prefixes: embeds an IPv4
        address in the low 32 bits and is routable on hosts with ::/96
        routes configured. [::169.254.169.254] would otherwise reach IMDS."""
        from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

        ipv4_compat = ipaddress.ip_network("::/96")
        assert ipv4_compat in PRIVATE_IP_RANGES


class TestPrivateIPDetection:
    """Tests for using PRIVATE_IP_RANGES to detect private IPs."""

    def _is_private(self, ip_str: str) -> bool:
        """Helper to check if IP is in private ranges."""
        from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

        try:
            ip = ipaddress.ip_address(ip_str)
            return any(ip in network for network in PRIVATE_IP_RANGES)
        except ValueError:
            return False

    def test_localhost_is_private(self):
        """127.0.0.1 should be detected as private."""
        assert self._is_private("127.0.0.1") is True

    def test_localhost_ipv6_is_private(self):
        """::1 should be detected as private."""
        assert self._is_private("::1") is True

    def test_10_network_is_private(self):
        """10.x.x.x addresses should be detected as private."""
        assert self._is_private("10.0.0.1") is True
        assert self._is_private("10.255.255.255") is True

    def test_172_16_network_is_private(self):
        """172.16-31.x.x addresses should be detected as private."""
        assert self._is_private("172.16.0.1") is True
        assert self._is_private("172.31.255.255") is True

    def test_172_32_is_not_private(self):
        """172.32.x.x should NOT be detected as private."""
        assert self._is_private("172.32.0.1") is False

    def test_192_168_network_is_private(self):
        """192.168.x.x addresses should be detected as private."""
        assert self._is_private("192.168.0.1") is True
        assert self._is_private("192.168.255.255") is True

    def test_cgnat_is_private(self):
        """100.64.x.x CGNAT addresses should be detected as private."""
        assert self._is_private("100.64.0.1") is True
        assert self._is_private("100.127.255.255") is True

    def test_link_local_is_private(self):
        """169.254.x.x link-local addresses should be detected as private."""
        assert self._is_private("169.254.1.1") is True

    def test_public_ip_is_not_private(self):
        """Public IPs should NOT be detected as private."""
        assert self._is_private("8.8.8.8") is False
        assert self._is_private("1.1.1.1") is False
        assert self._is_private("93.184.216.34") is False  # example.com

    def test_ipv6_public_is_not_private(self):
        """Public IPv6 addresses should NOT be detected as private."""
        assert self._is_private("2001:4860:4860::8888") is False  # Google DNS


class TestIPv6TransitionPrefixesAntiCollision:
    """Anti-regression: confirm the new IPv6 transition-prefix entries
    do NOT swallow legitimate global IPv6 allocations at their boundaries.

    Each prefix is precisely scoped at the bit level:
      - 2001::/32 fixes the second hextet to 0x0000 (Teredo only).
      - 2002::/16 fixes the first hextet to 0x2002 (6to4 only).
      - 64:ff9b::/96 fixes the first 96 bits (the well-known NAT64 prefix
        only; RFC 8215 local-use 64:ff9b:1::/48 must NOT match).
      - 100::/64 fixes the first 64 bits (RFC 6666 discard only; the rest
        of 100::/8 is unallocated, not discard).
    """

    _is_private = staticmethod(_ip_is_private)

    def test_google_dns_v6_not_blocked(self):
        """2001:4860:4860::8888 — second hextet 0x4860, outside 2001::/32."""
        assert self._is_private("2001:4860:4860::8888") is False

    def test_cloudflare_dns_v6_not_blocked(self):
        """2606:4700:4700::1111 — first hextet 0x2606, far from 2001/2002."""
        assert self._is_private("2606:4700:4700::1111") is False

    def test_documentation_prefix_v6_not_blocked(self):
        """2001:db8::/32 (RFC 3849) — second hextet 0x0db8, outside Teredo."""
        assert self._is_private("2001:db8::1") is False

    def test_root_server_v6_not_blocked(self):
        """2001:500::/30 (root-server allocation) — second hextet 0x0500."""
        assert self._is_private("2001:500:88::1") is False

    def test_he_tunnelbroker_v6_not_blocked(self):
        """2001:470::/32 (Hurricane Electric) — second hextet 0x0470."""
        assert self._is_private("2001:470:1f04::1") is False

    def test_neighbor_above_6to4_not_blocked(self):
        """2003::/16 (Deutsche Telekom) sits adjacent to 2002::/16."""
        assert self._is_private("2003::1") is False

    def test_neighbor_below_6to4_not_blocked(self):
        """2001:ffff::1 — last address of 2001:: space, not in 2002::/16."""
        assert self._is_private("2001:ffff::1") is False

    # NOTE: RFC 8215's 64:ff9b:1::/48 (NAT64 local-use) IS now blocked —
    # see TestPrivateIPRanges::test_contains_nat64_local_use_prefix. It
    # is the same SSRF threat class as the well-known /96 and missing
    # it has been paid out as a HackerOne bounty against ssrf_filter.

    def test_ipv6_discard_prefix_neighbor_not_blocked(self):
        """100:1::/64 — second hextet 0x0001, outside the /64 discard
        block. The surrounding 100::/8 is reserved-unallocated, not
        discard, so we must not over-block it."""
        assert self._is_private("100:1::1") is False


class TestIPv6TransitionPrefixesPositiveDetection:
    """Confirm the new transition prefixes detect their full address space,
    including embedded private-IPv4 wraps relevant to SSRF."""

    _is_private = staticmethod(_ip_is_private)

    def test_6to4_wraps_loopback(self):
        """[2002:7f00:1::] — 6to4 wrap of 127.0.0.1."""
        assert self._is_private("2002:7f00:1::") is True

    def test_6to4_wraps_rfc1918_class_a(self):
        """[2002:0a00:1::] — 6to4 wrap of 10.0.0.1."""
        assert self._is_private("2002:0a00:1::") is True

    def test_6to4_wraps_rfc1918_class_b(self):
        """[2002:ac10:1::] — 6to4 wrap of 172.16.0.1."""
        assert self._is_private("2002:ac10:1::") is True

    def test_6to4_wraps_rfc1918_class_c(self):
        """[2002:c0a8:101::] — 6to4 wrap of 192.168.1.1."""
        assert self._is_private("2002:c0a8:101::") is True

    def test_6to4_wraps_aws_metadata(self):
        """[2002:a9fe:a9fe::] — 6to4 wrap of 169.254.169.254 (AWS IMDS).
        High-value SSRF target; must be caught by the prefix block."""
        assert self._is_private("2002:a9fe:a9fe::") is True

    def test_6to4_upper_boundary(self):
        """Last address in 2002::/16."""
        assert (
            self._is_private("2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff") is True
        )

    def test_nat64_wraps_loopback(self):
        """[64:ff9b::7f00:1] — NAT64 wrap of 127.0.0.1."""
        assert self._is_private("64:ff9b::7f00:1") is True

    def test_nat64_wraps_rfc1918_class_a(self):
        """[64:ff9b::a00:1] — NAT64 wrap of 10.0.0.1."""
        assert self._is_private("64:ff9b::a00:1") is True

    def test_nat64_wraps_rfc1918_class_b(self):
        """[64:ff9b::ac10:1] — NAT64 wrap of 172.16.0.1."""
        assert self._is_private("64:ff9b::ac10:1") is True

    def test_nat64_wraps_aws_metadata(self):
        """[64:ff9b::a9fe:a9fe] — NAT64 wrap of 169.254.169.254 (AWS IMDS)."""
        assert self._is_private("64:ff9b::a9fe:a9fe") is True

    def test_teredo_lower_boundary(self):
        """2001:0:0:0:0:0:0:0 — first address in 2001::/32."""
        assert self._is_private("2001::") is True

    def test_teredo_upper_boundary(self):
        """2001:0:ffff:ffff:ffff:ffff:ffff:ffff — last address in 2001::/32."""
        assert self._is_private("2001:0:ffff:ffff:ffff:ffff:ffff:ffff") is True

    def test_discard_prefix_upper_boundary(self):
        """Last address in 100::/64."""
        assert self._is_private("100::ffff:ffff:ffff:ffff") is True


class TestIPRangesUsedByValidators:
    """Tests to verify PRIVATE_IP_RANGES is correctly imported by validators."""

    def test_ssrf_validator_uses_shared_ranges(self):
        """SSRF validator should import BLOCKED_IP_RANGES from ip_ranges."""
        from local_deep_research.security.ssrf_validator import (
            BLOCKED_IP_RANGES,
        )
        from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

        # Should be the same object (imported, not copied)
        assert BLOCKED_IP_RANGES is PRIVATE_IP_RANGES

    def test_notification_validator_uses_shared_ranges(self):
        """Notification validator should use shared ranges."""
        from local_deep_research.security.notification_validator import (
            NotificationURLValidator,
        )
        from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES

        # Should be the same object
        assert NotificationURLValidator.PRIVATE_IP_RANGES is PRIVATE_IP_RANGES
