"""Tests for scripts/url_safety.py — SSRF guard + download size cap (issue #210).

Covers:
- is_safe_fetch_url(): scheme + resolved-IP validation. Rejects loopback,
  link-local (cloud metadata), RFC1918, file://, and hostnames that resolve
  to a private address. Accepts a normal public https URL.
- Call-site enforcement in vision_audit.download_image(): a loopback image
  src must be skipped with no request attempted, and an oversized response
  must be discarded rather than written to disk unbounded.

No real network calls — all DNS resolution and HTTP responses are mocked.
"""

import os
import socket
import sys
from unittest.mock import MagicMock

import pytest

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))

import vision_audit  # noqa: E402

try:
    import url_safety  # noqa: E402
except ImportError:
    url_safety = None


def _require_url_safety():
    if url_safety is None:
        pytest.fail("scripts/url_safety.py not found — SSRF guard not implemented yet")


def _cap_bytes():
    """Real cap post-fix; fallback matches the cap this test expects pre-fix."""
    if url_safety is not None:
        return url_safety.MAX_FETCH_BYTES
    return 25 * 1024 * 1024


def _fake_addrinfo(ip):
    return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (ip, 0))]


# ── is_safe_fetch_url ───────────────────────────────────────────────────────

class TestIsSafeFetchUrl:
    def test_rejects_loopback_literal(self):
        _require_url_safety()
        assert url_safety.is_safe_fetch_url("http://127.0.0.1/x") is False

    def test_rejects_link_local_metadata_ip(self):
        _require_url_safety()
        assert url_safety.is_safe_fetch_url("http://169.254.169.254/x") is False

    def test_rejects_rfc1918_literal(self):
        _require_url_safety()
        assert url_safety.is_safe_fetch_url("http://10.0.0.5/x") is False

    def test_rejects_file_scheme(self):
        _require_url_safety()
        assert url_safety.is_safe_fetch_url("file:///etc/passwd") is False

    def test_rejects_hostname_resolving_to_loopback(self, monkeypatch):
        _require_url_safety()
        monkeypatch.setattr(socket, "getaddrinfo", lambda *a, **kw: _fake_addrinfo("127.0.0.1"))
        assert url_safety.is_safe_fetch_url("http://evil.internal/x") is False

    def test_accepts_public_https_url(self, monkeypatch):
        _require_url_safety()
        monkeypatch.setattr(socket, "getaddrinfo", lambda *a, **kw: _fake_addrinfo("93.184.216.34"))
        assert url_safety.is_safe_fetch_url("https://images.example.com/x.png") is True


# ── Call-site enforcement: vision_audit.download_image ─────────────────────

class TestVisionAuditCallSite:
    def test_loopback_src_is_skipped_without_fetching(self, monkeypatch, tmp_path):
        """A page embedding <img src="http://127.0.0.1:8111/..."> must never
        reach requests.get — that's the SSRF probe this bug enables."""
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.iter_content.return_value = [b"P" * 200]
        mock_get = MagicMock(return_value=mock_resp)
        monkeypatch.setattr(vision_audit.requests, "get", mock_get)

        result = vision_audit.download_image(
            "http://127.0.0.1:8111/internal.png", output_dir=str(tmp_path)
        )

        assert result is None
        assert not mock_get.called

    def test_oversized_response_is_rejected(self, monkeypatch, tmp_path):
        """A response beyond the byte cap must be discarded, not written to
        disk in full."""
        monkeypatch.setattr(socket, "getaddrinfo", lambda *a, **kw: _fake_addrinfo("93.184.216.34"))

        cap = _cap_bytes()
        chunk = b"a" * (5 * 1024 * 1024)  # 5 MB
        n_chunks = (cap // len(chunk)) + 2  # comfortably exceed the cap
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.iter_content.return_value = [chunk] * n_chunks
        monkeypatch.setattr(vision_audit.requests, "get", MagicMock(return_value=mock_resp))

        result = vision_audit.download_image(
            "https://images.example.com/huge.png", output_dir=str(tmp_path)
        )

        assert result is None
        assert list(tmp_path.iterdir()) == []
