"""Tests for ledgrab.utils.net_classify."""

import pytest

from ledgrab.utils.net_classify import (
    HostCategory,
    classify_ip,
    is_blocked_for_ssrf,
    is_local_for_http_default,
    is_loopback,
)


@pytest.mark.parametrize(
    "host,expected",
    [
        ("127.0.0.1", HostCategory.LOOPBACK),
        ("::1", HostCategory.LOOPBACK),
        ("10.0.0.5", HostCategory.PRIVATE),
        ("192.168.1.1", HostCategory.PRIVATE),
        ("172.16.0.5", HostCategory.PRIVATE),
        ("fd00::1", HostCategory.PRIVATE),  # ULA
        ("169.254.1.1", HostCategory.LINK_LOCAL),
        ("fe80::1", HostCategory.LINK_LOCAL),
        ("0.0.0.0", HostCategory.UNSPECIFIED),
        ("::", HostCategory.UNSPECIFIED),
        ("224.0.0.1", HostCategory.MULTICAST),
        ("ff00::1", HostCategory.MULTICAST),
        # ``240.0.0.0/4`` (class E) is labelled both ``is_private`` and
        # ``is_reserved`` by Python; we keep PRIVATE first which is the
        # stricter SSRF policy.
        ("240.0.0.1", HostCategory.PRIVATE),
        ("8.8.8.8", HostCategory.PUBLIC),
        ("1.1.1.1", HostCategory.PUBLIC),
        ("2606:4700:4700::1111", HostCategory.PUBLIC),
        ("not-an-ip", HostCategory.UNPARSEABLE),
        ("", HostCategory.UNPARSEABLE),
        ("example.com", HostCategory.UNPARSEABLE),
    ],
)
def test_classify_ip(host: str, expected: HostCategory) -> None:
    assert classify_ip(host) is expected


# ---------------------------------------------------------------------------
# SSRF block list — must include EVERY non-public category, including
# unparseable inputs. Regression guard: if anyone narrows this set we lose
# SSRF protection.
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
    "host",
    [
        "127.0.0.1",
        "::1",
        "10.0.0.5",
        "192.168.1.1",
        "172.16.0.5",
        "fd00::1",
        "169.254.1.1",
        "fe80::1",
        "0.0.0.0",
        "::",
        "224.0.0.1",
        "ff00::1",
        "240.0.0.1",
        "not-an-ip",  # unparseable → blocked
        "",  # unparseable → blocked
    ],
)
def test_is_blocked_for_ssrf_blocks_non_public(host: str) -> None:
    assert is_blocked_for_ssrf(host) is True


@pytest.mark.parametrize(
    "host",
    ["8.8.8.8", "1.1.1.1", "2606:4700:4700::1111"],
)
def test_is_blocked_for_ssrf_allows_public(host: str) -> None:
    assert is_blocked_for_ssrf(host) is False


# ---------------------------------------------------------------------------
# LAN-default policy — narrower than SSRF: we infer ``http://`` for loopback
# / private / link-local / unspecified, NOT for multicast / reserved /
# unparseable (those should fall through to ``https://`` or to caller-side
# heuristics like mDNS suffix matching).
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
    "host",
    ["127.0.0.1", "::1", "10.0.0.5", "192.168.1.1", "fe80::1", "0.0.0.0"],
)
def test_is_local_for_http_default_true(host: str) -> None:
    assert is_local_for_http_default(host) is True


@pytest.mark.parametrize(
    "host",
    ["8.8.8.8", "224.0.0.1", "not-an-ip", ""],
)
def test_is_local_for_http_default_false(host: str) -> None:
    assert is_local_for_http_default(host) is False


# ---------------------------------------------------------------------------
# Loopback predicate — accepts both literals and the auth module's textual
# placeholders (localhost, testclient).
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
    "host",
    [
        "127.0.0.1",
        "127.0.0.5",
        "::1",
        "localhost",
        "LOCALHOST",
        "testclient",
        "[::1]",
        "fe80::1%eth0",  # ← link-local with zone — must NOT match
    ],
)
def test_is_loopback_recognises_loopback(host: str) -> None:
    expected = host != "fe80::1%eth0"
    assert is_loopback(host) is expected


@pytest.mark.parametrize(
    "host",
    ["8.8.8.8", "10.0.0.5", "example.com", "", None],
)
def test_is_loopback_rejects_other(host) -> None:
    assert is_loopback(host) is False


# ─── validate_lan_host ────────────────────────────────────────────


from ledgrab.utils.net_classify import validate_lan_host  # noqa: E402


@pytest.mark.parametrize(
    "host",
    [
        "127.0.0.1",
        "10.0.0.5",
        "192.168.1.50",
        "172.16.0.10",
        "fe80::1",
        "fc00::1",
        "0.0.0.0",
        # IPv6 with brackets and zone id
        "[fe80::1%eth0]",
        # Plain hostnames pass through (mDNS / bare-label)
        "bulb.local",
        "office-strip",
        "controller.lan",
        "",  # empty also passes (caller handles validation upstream)
    ],
)
def test_validate_lan_host_accepts_local(host) -> None:
    """Loopback / private / link-local / hostnames must not raise."""
    validate_lan_host(host)


@pytest.mark.parametrize(
    "host",
    [
        "1.1.1.1",  # Cloudflare DNS (routable public)
        "8.8.8.8",  # Google DNS
        "224.0.0.1",  # multicast — not a LAN device address
        "2606:4700:4700::1111",  # Cloudflare DNS IPv6
    ],
)
def test_validate_lan_host_rejects_public_or_multicast(host) -> None:
    """Reject genuinely-public addresses and multicast targets.

    Note: Python's :func:`ipaddress.ip_address(...).is_private` treats
    RFC6890 ranges (documentation 192.0.2/24, former class E 240/4, etc.)
    as private, so those are accepted as "local-ish" by our classifier --
    which is the correct policy for LedGrab: those ranges aren't routable
    public addresses, and refusing them would be unhelpful theatre.
    """
    with pytest.raises(ValueError, match="LedGrab is LAN-only"):
        validate_lan_host(host)
