import pytest

from app.services.infra.ssrf import UnsafeUrlError, assert_safe_url


@pytest.mark.parametrize("url", [
    "https://dummyjson.com/products",
    "https://example.com:8080/api",
    "https://api.example.co.uk/v1/products",
])
def test_public_urls_allowed(url, monkeypatch):
    # Patched so the suite does not depend on live DNS: an offline CI must
    # still be able to prove a public address is accepted.
    import app.services.infra.ssrf as mod
    monkeypatch.setattr(mod, "_resolve", lambda host: ["93.184.216.34"])
    assert assert_safe_url(url) == url


@pytest.mark.parametrize("url", [
    "https://127.0.0.1/admin",
    "https://127.0.0.1:8001/api/shopify/status",
    "https://localhost/",
    "https://[::1]/",
    "https://169.254.169.254/latest/meta-data/",   # cloud instance metadata
    "https://10.0.0.1/",
    "https://192.168.1.1/",
    "https://172.16.0.1/",
    "https://0.0.0.0/",
])
def test_internal_addresses_rejected(url):
    # https, deliberately: now that http is refused on scheme alone, an
    # http:// case here would never reach the resolved-address check --
    # the module's actual SSRF logic -- and the suite would go green while
    # that logic silently stopped being exercised.
    with pytest.raises(UnsafeUrlError):
        assert_safe_url(url)


@pytest.mark.parametrize("url", [
    "http://example.com/products",     # plaintext: the API key would travel in clear
    "file:///etc/passwd",
    "gopher://example.com/",
    "ftp://example.com/",
    "not-a-url",
    "",
    None,
])
def test_non_https_schemes_rejected(url):
    with pytest.raises(UnsafeUrlError):
        assert_safe_url(url)


def test_hostname_resolving_to_loopback_is_rejected(monkeypatch):
    # The load-bearing case. Validating the hostname STRING would let this
    # through, because "sneaky.example.com" looks entirely public.
    import app.services.infra.ssrf as mod
    monkeypatch.setattr(mod, "_resolve", lambda host: ["127.0.0.1"])
    with pytest.raises(UnsafeUrlError):
        assert_safe_url("https://sneaky.example.com/products")


def test_unresolvable_host_rejected(monkeypatch):
    import app.services.infra.ssrf as mod

    def boom(host):
        raise OSError("nodename nor servname provided")

    monkeypatch.setattr(mod, "_resolve", boom)
    with pytest.raises(UnsafeUrlError):
        assert_safe_url("https://does-not-exist.example/")


def test_error_names_the_rule():
    with pytest.raises(UnsafeUrlError) as ex:
        assert_safe_url("file:///etc/passwd")
    assert "scheme" in str(ex.value).lower()


def test_the_real_resolver_rejects_localhost():
    # Not patched: proves _resolve itself works against the hosts file, with
    # no network required. https, so this still reaches the address check
    # rather than being turned away on scheme alone.
    with pytest.raises(UnsafeUrlError):
        assert_safe_url("https://localhost/")


def test_a_host_resolving_to_both_public_and_private_is_rejected(monkeypatch):
    # An attacker only needs one private address in the set. Accepting on the
    # strength of the public one would defeat the whole guard.
    import app.services.infra.ssrf as mod
    monkeypatch.setattr(mod, "_resolve", lambda host: ["93.184.216.34", "10.0.0.5"])
    with pytest.raises(UnsafeUrlError):
        assert_safe_url("https://mixed.example.com/")


@pytest.mark.parametrize("url", [
    "https://[::1/x",                  # urlparse raises on a malformed authority
    "https://" + "a" * 300 + "/",      # getaddrinfo raises UnicodeError on a long label
])
def test_malformed_urls_are_rejected_not_raised(url):
    # A guard that crashes on bad input is a 500 where a 400 belongs, and
    # invites someone to wrap the caller in a bare except later.
    with pytest.raises(UnsafeUrlError):
        assert_safe_url(url)


@pytest.mark.parametrize("url", [
    "https://[::ffff:127.0.0.1]/",
    "https://[::ffff:169.254.169.254]/",
])
def test_ipv4_mapped_ipv6_is_rejected(url):
    with pytest.raises(UnsafeUrlError):
        assert_safe_url(url)
