"""Tests for safe_requests module - SSRF-protected HTTP requests."""

import pytest
from unittest.mock import patch, MagicMock
import requests

from local_deep_research.security.safe_requests import (
    safe_get,
    safe_post,
    SafeSession,
    DEFAULT_TIMEOUT,
    MAX_RESPONSE_SIZE,
)


class TestSafeGetFunction:
    """Tests for safe_get function."""

    def test_valid_url_makes_request(self):
        """Should make request to valid external URL."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ):
            with patch(
                "local_deep_research.security.safe_requests.requests.get"
            ) as mock_get:
                mock_response = MagicMock()
                mock_response.headers = {}
                mock_get.return_value = mock_response

                response = safe_get("https://example.com")

                mock_get.assert_called_once()
                assert response == mock_response

    def test_rejects_invalid_url(self):
        """Should raise ValueError for URLs failing SSRF validation."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=False,
        ):
            with pytest.raises(ValueError, match="SSRF"):
                safe_get("http://127.0.0.1/admin")

    def test_uses_default_timeout(self):
        """Should use default timeout when not specified."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ):
            with patch(
                "local_deep_research.security.safe_requests.requests.get"
            ) as mock_get:
                mock_response = MagicMock()
                mock_response.headers = {}
                mock_get.return_value = mock_response

                safe_get("https://example.com")

                _, kwargs = mock_get.call_args
                assert kwargs["timeout"] == DEFAULT_TIMEOUT

    def test_custom_timeout(self):
        """Should use custom timeout when provided."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ):
            with patch(
                "local_deep_research.security.safe_requests.requests.get"
            ) as mock_get:
                mock_response = MagicMock()
                mock_response.headers = {}
                mock_get.return_value = mock_response

                safe_get("https://example.com", timeout=60)

                _, kwargs = mock_get.call_args
                assert kwargs["timeout"] == 60

    def test_disables_redirects_by_default(self):
        """Should disable redirects by default to prevent SSRF bypass."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ):
            with patch(
                "local_deep_research.security.safe_requests.requests.get"
            ) as mock_get:
                mock_response = MagicMock()
                mock_response.headers = {}
                mock_get.return_value = mock_response

                safe_get("https://example.com")

                _, kwargs = mock_get.call_args
                assert kwargs["allow_redirects"] is False

    def test_can_enable_redirects(self):
        """Should allow enabling redirects explicitly."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ):
            with patch(
                "local_deep_research.security.safe_requests.requests.get"
            ) as mock_get:
                mock_response = MagicMock()
                mock_response.headers = {}
                mock_get.return_value = mock_response

                safe_get("https://example.com", allow_redirects=True)

                _, kwargs = mock_get.call_args
                assert kwargs["allow_redirects"] is True

    def test_passes_params(self):
        """Should pass URL parameters."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ):
            with patch(
                "local_deep_research.security.safe_requests.requests.get"
            ) as mock_get:
                mock_response = MagicMock()
                mock_response.headers = {}
                mock_get.return_value = mock_response

                params = {"q": "test", "page": "1"}
                safe_get("https://example.com", params=params)

                args, _ = mock_get.call_args
                assert args == ("https://example.com",)
                _, kwargs = mock_get.call_args
                assert "params" not in kwargs or kwargs.get("params") == params

    def test_oversized_response_silently_passes(self):
        """Documents current behavior: oversized responses are NOT rejected.

        NOTE: This is a potential security bug - the ValueError raised for
        "Response too large" is caught by the same except block that handles
        int() parsing errors, making the size check ineffective.
        """
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ):
            with patch(
                "local_deep_research.security.safe_requests.requests.get"
            ) as mock_get:
                mock_response = MagicMock()
                # Configure headers as a dict-like object
                mock_response.headers = {
                    "Content-Length": str(MAX_RESPONSE_SIZE + 1)
                }
                mock_get.return_value = mock_response

                # BUG: This should raise ValueError but doesn't because
                # the except block catches both int() errors and the raised error
                result = safe_get("https://example.com")
                assert result == mock_response

    def test_accepts_response_within_limit(self):
        """Should accept response within size limit."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ):
            with patch(
                "local_deep_research.security.safe_requests.requests.get"
            ) as mock_get:
                mock_response = MagicMock()
                mock_response.headers = {"Content-Length": str(1024)}
                mock_get.return_value = mock_response

                response = safe_get("https://example.com")
                assert response == mock_response

    def test_handles_invalid_content_length(self):
        """Should ignore invalid Content-Length values."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ):
            with patch(
                "local_deep_research.security.safe_requests.requests.get"
            ) as mock_get:
                mock_response = MagicMock()
                mock_response.headers = {"Content-Length": "not-a-number"}
                mock_get.return_value = mock_response

                # Should not raise
                response = safe_get("https://example.com")
                assert response == mock_response

    def test_reraises_timeout(self):
        """Should re-raise timeout exceptions."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ):
            with patch(
                "local_deep_research.security.safe_requests.requests.get",
                side_effect=requests.Timeout("timeout"),
            ):
                with pytest.raises(requests.Timeout):
                    safe_get("https://example.com")

    def test_reraises_request_exception(self):
        """Should re-raise request exceptions."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ):
            with patch(
                "local_deep_research.security.safe_requests.requests.get",
                side_effect=requests.RequestException("connection error"),
            ):
                with pytest.raises(requests.RequestException):
                    safe_get("https://example.com")

    def test_allow_localhost_parameter(self):
        """Should pass allow_localhost to validate_url."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ) as mock_validate:
            with patch(
                "local_deep_research.security.safe_requests.requests.get"
            ) as mock_get:
                mock_response = MagicMock()
                mock_response.headers = {}
                mock_get.return_value = mock_response

                safe_get("http://localhost:8080", allow_localhost=True)

                mock_validate.assert_called_once_with(
                    "http://localhost:8080",
                    allow_localhost=True,
                    allow_private_ips=False,
                )

    def test_allow_private_ips_parameter(self):
        """Should pass allow_private_ips to validate_url."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ) as mock_validate:
            with patch(
                "local_deep_research.security.safe_requests.requests.get"
            ) as mock_get:
                mock_response = MagicMock()
                mock_response.headers = {}
                mock_get.return_value = mock_response

                safe_get("http://192.168.1.1", allow_private_ips=True)

                mock_validate.assert_called_once_with(
                    "http://192.168.1.1",
                    allow_localhost=False,
                    allow_private_ips=True,
                )


class TestSafePostFunction:
    """Tests for safe_post function."""

    def test_valid_url_makes_request(self):
        """Should make POST request to valid external URL."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ):
            with patch(
                "local_deep_research.security.safe_requests.requests.post"
            ) as mock_post:
                mock_response = MagicMock()
                mock_response.headers = {}
                mock_post.return_value = mock_response

                response = safe_post(
                    "https://example.com/api", json={"key": "value"}
                )

                mock_post.assert_called_once()
                assert response == mock_response

    def test_rejects_invalid_url(self):
        """Should raise ValueError for URLs failing SSRF validation."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=False,
        ):
            with pytest.raises(ValueError, match="SSRF"):
                safe_post("http://169.254.169.254/metadata")

    def test_passes_data_parameter(self):
        """Should pass data parameter for form data."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ):
            with patch(
                "local_deep_research.security.safe_requests.requests.post"
            ) as mock_post:
                mock_response = MagicMock()
                mock_response.headers = {}
                mock_post.return_value = mock_response

                safe_post("https://example.com", data="raw data")

                _, kwargs = mock_post.call_args
                assert kwargs.get("data") == "raw data"

    def test_passes_json_parameter(self):
        """Should pass json parameter for JSON data."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ):
            with patch(
                "local_deep_research.security.safe_requests.requests.post"
            ) as mock_post:
                mock_response = MagicMock()
                mock_response.headers = {}
                mock_post.return_value = mock_response

                json_data = {"key": "value"}
                safe_post("https://example.com", json=json_data)

                _, kwargs = mock_post.call_args
                assert kwargs.get("json") == json_data

    def test_uses_default_timeout(self):
        """Should use default timeout when not specified."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ):
            with patch(
                "local_deep_research.security.safe_requests.requests.post"
            ) as mock_post:
                mock_response = MagicMock()
                mock_response.headers = {}
                mock_post.return_value = mock_response

                safe_post("https://example.com")

                _, kwargs = mock_post.call_args
                assert kwargs["timeout"] == DEFAULT_TIMEOUT

    def test_disables_redirects_by_default(self):
        """Should disable redirects by default."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ):
            with patch(
                "local_deep_research.security.safe_requests.requests.post"
            ) as mock_post:
                mock_response = MagicMock()
                mock_response.headers = {}
                mock_post.return_value = mock_response

                safe_post("https://example.com")

                _, kwargs = mock_post.call_args
                assert kwargs["allow_redirects"] is False

    def test_oversized_response_silently_passes(self):
        """Documents current behavior: oversized responses are NOT rejected.

        NOTE: This is a potential security bug - same as in safe_get().
        """
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ):
            with patch(
                "local_deep_research.security.safe_requests.requests.post"
            ) as mock_post:
                mock_response = MagicMock()
                mock_response.headers = {
                    "Content-Length": str(MAX_RESPONSE_SIZE + 1)
                }
                mock_post.return_value = mock_response

                # BUG: This should raise ValueError but doesn't
                result = safe_post("https://example.com")
                assert result == mock_response

    def test_reraises_timeout(self):
        """Should re-raise timeout exceptions."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ):
            with patch(
                "local_deep_research.security.safe_requests.requests.post",
                side_effect=requests.Timeout("timeout"),
            ):
                with pytest.raises(requests.Timeout):
                    safe_post("https://example.com")


class TestSafeSession:
    """Tests for SafeSession class."""

    def test_init_default_values(self):
        """Should initialize with default security settings."""
        session = SafeSession()
        assert session.allow_localhost is False
        assert session.allow_private_ips is False

    def test_init_allow_localhost(self):
        """Should accept allow_localhost parameter."""
        session = SafeSession(allow_localhost=True)
        assert session.allow_localhost is True
        assert session.allow_private_ips is False

    def test_init_allow_private_ips(self):
        """Should accept allow_private_ips parameter."""
        session = SafeSession(allow_private_ips=True)
        assert session.allow_localhost is False
        assert session.allow_private_ips is True

    def test_request_validates_url(self):
        """Should validate URL before making request."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=False,
        ):
            session = SafeSession()
            with pytest.raises(ValueError, match="SSRF"):
                session.request("GET", "http://127.0.0.1/admin")

    def test_request_makes_call_on_valid_url(self):
        """Should make request when URL is valid."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ):
            with patch.object(requests.Session, "request") as mock_request:
                mock_response = MagicMock()
                mock_request.return_value = mock_response

                session = SafeSession()
                response = session.request("GET", "https://example.com")

                mock_request.assert_called_once()
                assert response == mock_response

    def test_request_uses_default_timeout(self):
        """Should set default timeout if not provided."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ):
            with patch.object(requests.Session, "request") as mock_request:
                mock_response = MagicMock()
                mock_request.return_value = mock_response

                session = SafeSession()
                session.request("GET", "https://example.com")

                _, kwargs = mock_request.call_args
                assert kwargs["timeout"] == DEFAULT_TIMEOUT

    def test_request_respects_custom_timeout(self):
        """Should respect custom timeout when provided."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ):
            with patch.object(requests.Session, "request") as mock_request:
                mock_response = MagicMock()
                mock_request.return_value = mock_response

                session = SafeSession()
                session.request("GET", "https://example.com", timeout=120)

                _, kwargs = mock_request.call_args
                assert kwargs["timeout"] == 120

    def test_context_manager(self):
        """Should work as context manager."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ):
            with SafeSession() as session:
                assert isinstance(session, SafeSession)

    def test_passes_allow_localhost_to_validate(self):
        """Should pass allow_localhost to validate_url."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ) as mock_validate:
            with patch.object(requests.Session, "request"):
                session = SafeSession(allow_localhost=True)
                session.request("GET", "http://localhost:8080")

                mock_validate.assert_called_once_with(
                    "http://localhost:8080",
                    allow_localhost=True,
                    allow_private_ips=False,
                )

    def test_passes_allow_private_ips_to_validate(self):
        """Should pass allow_private_ips to validate_url."""
        with patch(
            "local_deep_research.security.safe_requests.validate_url",
            return_value=True,
        ) as mock_validate:
            with patch.object(requests.Session, "request"):
                session = SafeSession(allow_private_ips=True)
                session.request("GET", "http://192.168.1.1")

                mock_validate.assert_called_once_with(
                    "http://192.168.1.1",
                    allow_localhost=False,
                    allow_private_ips=True,
                )


class TestConstants:
    """Tests for module constants."""

    def test_default_timeout_reasonable(self):
        """DEFAULT_TIMEOUT should be a reasonable value."""
        assert DEFAULT_TIMEOUT == 30
        assert isinstance(DEFAULT_TIMEOUT, int)

    def test_max_response_size_reasonable(self):
        """MAX_RESPONSE_SIZE should be a reasonable value (10MB)."""
        assert MAX_RESPONSE_SIZE == 10 * 1024 * 1024  # 10MB
        assert isinstance(MAX_RESPONSE_SIZE, int)
