"""Tests for SSRF-validated redirect following (PR #1949).

Verifies:
- Manual redirect following with SSRF validation on each hop
- Redirect to internal IPs blocked
- Too many redirects raises ValueError
- Missing Location header stops redirect chain
- Both safe_get and safe_post
"""

import pytest
from unittest.mock import patch, MagicMock

import requests

from local_deep_research.security import ssrf_validator

from local_deep_research.security.safe_requests import (
    safe_get,
    safe_post,
    SafeSession,
    _REDIRECT_STATUS_CODES,
    _MAX_REDIRECTS,
    _resolve_redirect_method,
    MAX_RESPONSE_SIZE,
)


@pytest.fixture
def mock_validate_url():
    with patch.object(
        ssrf_validator,
        "validate_url",
        return_value=True,
    ) as m:
        yield m


def _make_response(status_code=200, headers=None, url="https://example.com"):
    resp = MagicMock()
    resp.status_code = status_code
    resp.headers = headers or {}
    resp.url = url
    return resp


class TestSafeGetRedirectFollowing:
    """Test redirect following in safe_get."""

    def test_no_redirects_when_disabled(self, mock_validate_url):
        """With allow_redirects=False, redirect responses are returned as-is."""
        redirect_resp = _make_response(302, {"Location": "https://other.com"})

        with patch(
            "local_deep_research.security.safe_requests.requests.get",
            return_value=redirect_resp,
        ):
            result = safe_get("https://example.com", allow_redirects=False)
            assert result.status_code == 302

    def test_follows_valid_redirect(self, mock_validate_url):
        """With allow_redirects=True, follows redirect to validated target."""
        redirect_resp = _make_response(
            302, {"Location": "https://other.com"}, "https://example.com"
        )
        final_resp = _make_response(200)

        with patch(
            "local_deep_research.security.safe_requests.requests.get",
            side_effect=[redirect_resp, final_resp],
        ):
            result = safe_get("https://example.com", allow_redirects=True)
            assert result.status_code == 200

    def test_blocks_redirect_to_internal_ip(self, mock_validate_url):
        """Redirect to internal IP raises ValueError."""
        redirect_resp = _make_response(
            302,
            {"Location": "http://169.254.169.254/metadata"},
            "https://example.com",
        )

        # First call validates initial URL (True), second validates redirect target (False)
        mock_validate_url.side_effect = [True, False]

        with patch(
            "local_deep_research.security.safe_requests.requests.get",
            return_value=redirect_resp,
        ):
            with pytest.raises(ValueError, match="Redirect target failed SSRF"):
                safe_get("https://example.com", allow_redirects=True)

    def test_too_many_redirects_raises(self, mock_validate_url):
        """More than _MAX_REDIRECTS raises ValueError."""
        redirect_resp = _make_response(
            301, {"Location": "https://example.com/loop"}, "https://example.com"
        )

        with patch(
            "local_deep_research.security.safe_requests.requests.get",
            return_value=redirect_resp,
        ):
            with pytest.raises(ValueError, match="Too many redirects"):
                safe_get("https://example.com", allow_redirects=True)

    def test_missing_location_stops_following(self, mock_validate_url):
        """Redirect without Location header stops the chain."""
        redirect_resp = _make_response(302, {})  # No Location header

        with patch(
            "local_deep_research.security.safe_requests.requests.get",
            return_value=redirect_resp,
        ):
            result = safe_get("https://example.com", allow_redirects=True)
            assert result.status_code == 302

    def test_all_redirect_status_codes_followed(self, mock_validate_url):
        """All status codes in _REDIRECT_STATUS_CODES trigger redirect following."""
        for code in _REDIRECT_STATUS_CODES:
            redirect_resp = _make_response(
                code, {"Location": "https://final.com"}, "https://example.com"
            )
            final_resp = _make_response(200)

            with patch(
                "local_deep_research.security.safe_requests.requests.get",
                side_effect=[redirect_resp, final_resp],
            ):
                result = safe_get("https://example.com", allow_redirects=True)
                assert result.status_code == 200, f"Failed for status {code}"

    def test_each_hop_validated(self, mock_validate_url):
        """Each redirect hop is validated against SSRF rules."""
        resp1 = _make_response(
            302, {"Location": "https://hop2.com"}, "https://example.com"
        )
        resp2 = _make_response(
            302, {"Location": "https://hop3.com"}, "https://hop2.com"
        )
        final = _make_response(200)

        with patch(
            "local_deep_research.security.safe_requests.requests.get",
            side_effect=[resp1, resp2, final],
        ):
            safe_get("https://example.com", allow_redirects=True)

        # Initial URL + 2 redirect targets = 3 validate_url calls
        assert mock_validate_url.call_count == 3
        validated_urls = [
            call.args[0] for call in mock_validate_url.call_args_list
        ]
        assert validated_urls == [
            "https://example.com",
            "https://hop2.com",
            "https://hop3.com",
        ]

    def test_default_allow_redirects_follows(self, mock_validate_url):
        """Without explicit allow_redirects, safe_get follows redirects (default True)."""
        redirect_resp = _make_response(
            302, {"Location": "https://other.com"}, "https://example.com"
        )
        final_resp = _make_response(200)

        with patch(
            "local_deep_research.security.safe_requests.requests.get",
            side_effect=[redirect_resp, final_resp],
        ) as mock_get:
            # No allow_redirects kwarg — should still follow
            result = safe_get("https://example.com")
            assert result.status_code == 200
            assert mock_get.call_count == 2

    def test_whitespace_stripped_from_location(self, mock_validate_url):
        """Whitespace in Location header is stripped before following."""
        redirect_resp = _make_response(
            302,
            {"Location": "  https://other.com  "},
            "https://example.com",
        )
        final_resp = _make_response(200)

        with patch(
            "local_deep_research.security.safe_requests.requests.get",
            side_effect=[redirect_resp, final_resp],
        ) as mock_get:
            safe_get("https://example.com", allow_redirects=True)
            second_url = mock_get.call_args_list[1][0][0]
            assert second_url == "https://other.com"


class TestSafePostRedirectFollowing:
    """Test redirect following in safe_post."""

    def test_follows_valid_redirect(self, mock_validate_url):
        """safe_post follows redirects when allow_redirects=True."""
        redirect_resp = _make_response(
            307, {"Location": "https://other.com"}, "https://example.com"
        )
        final_resp = _make_response(200)

        with patch(
            "local_deep_research.security.safe_requests.requests.post",
            side_effect=[redirect_resp, final_resp],
        ):
            result = safe_post("https://example.com", allow_redirects=True)
            assert result.status_code == 200

    def test_blocks_redirect_to_internal_ip(self, mock_validate_url):
        """safe_post blocks redirect to internal IP."""
        redirect_resp = _make_response(
            307, {"Location": "http://10.0.0.1/internal"}, "https://example.com"
        )

        mock_validate_url.side_effect = [True, False]

        with patch(
            "local_deep_research.security.safe_requests.requests.post",
            return_value=redirect_resp,
        ):
            with pytest.raises(ValueError, match="Redirect target failed SSRF"):
                safe_post("https://example.com", allow_redirects=True)

    def test_no_redirects_when_disabled(self, mock_validate_url):
        """With allow_redirects=False, POST redirect responses returned as-is."""
        redirect_resp = _make_response(307, {"Location": "https://other.com"})

        with patch(
            "local_deep_research.security.safe_requests.requests.post",
            return_value=redirect_resp,
        ):
            result = safe_post("https://example.com", allow_redirects=False)
            assert result.status_code == 307

    def test_too_many_redirects_raises(self, mock_validate_url):
        """More than _MAX_REDIRECTS raises ValueError (307 loop)."""
        redirect_resp = _make_response(
            307, {"Location": "https://example.com/loop"}, "https://example.com"
        )

        with patch(
            "local_deep_research.security.safe_requests.requests.post",
            return_value=redirect_resp,
        ):
            with pytest.raises(ValueError, match="Too many redirects"):
                safe_post("https://example.com", allow_redirects=True)

    def test_missing_location_stops_following(self, mock_validate_url):
        """Redirect without Location header stops the chain."""
        redirect_resp = _make_response(307, {})  # No Location header

        with patch(
            "local_deep_research.security.safe_requests.requests.post",
            return_value=redirect_resp,
        ):
            result = safe_post("https://example.com", allow_redirects=True)
            assert result.status_code == 307


class TestRedirectConstants:
    """Verify redirect-related constants."""

    def test_redirect_status_codes(self):
        assert 301 in _REDIRECT_STATUS_CODES
        assert 302 in _REDIRECT_STATUS_CODES
        assert 303 in _REDIRECT_STATUS_CODES
        assert 307 in _REDIRECT_STATUS_CODES
        assert 308 in _REDIRECT_STATUS_CODES
        assert 200 not in _REDIRECT_STATUS_CODES

    def test_max_redirects_is_reasonable(self):
        assert _MAX_REDIRECTS == 10


class TestResolveRedirectMethod:
    """Unit tests for _resolve_redirect_method helper."""

    def test_303_converts_post_to_get(self):
        assert _resolve_redirect_method("POST", 303) == "GET"

    def test_302_converts_post_to_get(self):
        assert _resolve_redirect_method("POST", 302) == "GET"

    def test_301_converts_post_to_get(self):
        assert _resolve_redirect_method("POST", 301) == "GET"

    def test_307_preserves_post(self):
        assert _resolve_redirect_method("POST", 307) == "POST"

    def test_308_preserves_post(self):
        assert _resolve_redirect_method("POST", 308) == "POST"

    def test_301_preserves_get(self):
        assert _resolve_redirect_method("GET", 301) == "GET"

    def test_302_preserves_put(self):
        assert _resolve_redirect_method("PUT", 302) == "PUT"

    def test_303_preserves_head(self):
        assert _resolve_redirect_method("HEAD", 303) == "HEAD"


class TestSafePostMethodConversion:
    """Test HTTP method conversion in safe_post redirect loop."""

    def test_post_302_converts_to_get(self, mock_validate_url):
        """POST with 302 redirect should convert to GET."""
        redirect_resp = _make_response(
            302, {"Location": "https://other.com"}, "https://example.com"
        )
        final_resp = _make_response(200)

        with (
            patch(
                "local_deep_research.security.safe_requests.requests.post",
                return_value=redirect_resp,
            ) as mock_post,
            patch(
                "local_deep_research.security.safe_requests.requests.get",
                return_value=final_resp,
            ) as mock_get,
        ):
            result = safe_post("https://example.com", allow_redirects=True)
            assert result.status_code == 200
            assert mock_post.call_count == 1
            assert mock_get.call_count == 1

    def test_post_303_converts_to_get(self, mock_validate_url):
        """POST with 303 redirect should convert to GET."""
        redirect_resp = _make_response(
            303, {"Location": "https://other.com"}, "https://example.com"
        )
        final_resp = _make_response(200)

        with (
            patch(
                "local_deep_research.security.safe_requests.requests.post",
                return_value=redirect_resp,
            ) as mock_post,
            patch(
                "local_deep_research.security.safe_requests.requests.get",
                return_value=final_resp,
            ) as mock_get,
        ):
            result = safe_post("https://example.com", allow_redirects=True)
            assert result.status_code == 200
            assert mock_post.call_count == 1
            assert mock_get.call_count == 1

    def test_post_307_preserves_method_and_body(self, mock_validate_url):
        """POST with 307 redirect should preserve method and body."""
        test_data = b"form-data"
        redirect_resp = _make_response(
            307, {"Location": "https://other.com"}, "https://example.com"
        )
        final_resp = _make_response(200)

        with patch(
            "local_deep_research.security.safe_requests.requests.post",
            side_effect=[redirect_resp, final_resp],
        ) as mock_post:
            result = safe_post(
                "https://example.com", data=test_data, allow_redirects=True
            )
            assert result.status_code == 200
            assert mock_post.call_count == 2
            # Verify body was forwarded on the redirect hop
            assert mock_post.call_args_list[1].kwargs.get("data") == test_data

    def test_post_308_preserves_method_and_body(self, mock_validate_url):
        """POST with 308 redirect should preserve method and body."""
        test_json = {"key": "value"}
        redirect_resp = _make_response(
            308, {"Location": "https://other.com"}, "https://example.com"
        )
        final_resp = _make_response(200)

        with patch(
            "local_deep_research.security.safe_requests.requests.post",
            side_effect=[redirect_resp, final_resp],
        ) as mock_post:
            result = safe_post(
                "https://example.com", json=test_json, allow_redirects=True
            )
            assert result.status_code == 200
            assert mock_post.call_count == 2
            assert mock_post.call_args_list[1].kwargs.get("json") == test_json

    def test_post_multi_hop_302_then_301(self, mock_validate_url):
        """POST→302→GET→301→GET: both hops use GET."""
        resp_302 = _make_response(
            302, {"Location": "https://hop2.com"}, "https://example.com"
        )
        resp_301 = _make_response(
            301, {"Location": "https://hop3.com"}, "https://hop2.com"
        )
        final_resp = _make_response(200)

        with (
            patch(
                "local_deep_research.security.safe_requests.requests.post",
                return_value=resp_302,
            ) as mock_post,
            patch(
                "local_deep_research.security.safe_requests.requests.get",
                side_effect=[resp_301, final_resp],
            ) as mock_get,
        ):
            result = safe_post("https://example.com", allow_redirects=True)
            assert result.status_code == 200
            assert mock_post.call_count == 1
            assert mock_get.call_count == 2


class TestSafeSessionRedirectFollowing:
    """Test redirect validation via SafeSession.send() override.

    SafeSession validates redirect targets in send(), which the requests
    library calls for each redirect hop via resolve_redirects().
    """

    def test_send_validates_redirect_target(self):
        """SafeSession.send() validates each URL against SSRF rules."""
        session = SafeSession()

        # Build a PreparedRequest pointing to an internal IP
        prep = requests.PreparedRequest()
        prep.prepare_url("http://169.254.169.254/metadata", {})
        prep.prepare_method("GET")

        with patch.object(
            ssrf_validator,
            "validate_url",
            return_value=False,
        ):
            with pytest.raises(ValueError, match="Redirect target failed"):
                session.send(prep)

    def test_send_allows_valid_url(self):
        """SafeSession.send() allows valid external URLs."""
        session = SafeSession()

        prep = requests.PreparedRequest()
        prep.prepare_url("https://example.com", {})
        prep.prepare_method("GET")

        with patch.object(
            ssrf_validator,
            "validate_url",
            return_value=True,
        ):
            with patch.object(
                requests.Session, "send", return_value=_make_response(200)
            ):
                result = session.send(prep)
                assert result.status_code == 200

    def test_send_blocks_localhost_redirect(self):
        """SafeSession.send() blocks redirect to localhost."""
        session = SafeSession()

        prep = requests.PreparedRequest()
        prep.prepare_url("http://127.0.0.1/admin", {})
        prep.prepare_method("GET")

        with patch.object(
            ssrf_validator,
            "validate_url",
            return_value=False,
        ):
            with pytest.raises(ValueError, match="SSRF"):
                session.send(prep)

    def test_send_respects_allow_localhost(self):
        """SafeSession.send() passes allow_localhost to validate_url."""
        session = SafeSession(allow_localhost=True)

        prep = requests.PreparedRequest()
        prep.prepare_url("http://localhost:8080/api", {})
        prep.prepare_method("GET")

        with patch.object(
            ssrf_validator,
            "validate_url",
            return_value=True,
        ) as mock_validate:
            with patch.object(
                requests.Session, "send", return_value=_make_response(200)
            ):
                session.send(prep)
                mock_validate.assert_called_once_with(
                    "http://localhost:8080/api",
                    allow_localhost=True,
                    allow_private_ips=False,
                )

    def test_send_respects_allow_private_ips(self):
        """SafeSession.send() passes allow_private_ips to validate_url."""
        session = SafeSession(allow_private_ips=True)

        prep = requests.PreparedRequest()
        prep.prepare_url("http://192.168.1.1/api", {})
        prep.prepare_method("GET")

        with patch.object(
            ssrf_validator,
            "validate_url",
            return_value=True,
        ) as mock_validate:
            with patch.object(
                requests.Session, "send", return_value=_make_response(200)
            ):
                session.send(prep)
                mock_validate.assert_called_once_with(
                    "http://192.168.1.1/api",
                    allow_localhost=False,
                    allow_private_ips=True,
                )

    def test_request_validates_initial_url(self):
        """SafeSession.request() validates the initial URL."""
        with patch.object(
            ssrf_validator,
            "validate_url",
            return_value=False,
        ):
            session = SafeSession()
            with pytest.raises(ValueError, match="SSRF"):
                session.request("GET", "http://169.254.169.254/metadata")

    def test_send_handles_none_url(self):
        """SafeSession.send() skips SSRF validation when URL is None.

        A PreparedRequest with url=None is unreachable in normal ``requests``
        flow — both ``request()`` and ``resolve_redirects()`` always populate
        the URL before calling ``send()``.  The ``if request.url`` guard is a
        defensive measure; this test documents that it does not raise.
        """
        session = SafeSession()

        prep = requests.PreparedRequest()
        # URL is None by default — unreachable in normal flow

        with patch.object(
            requests.Session, "send", return_value=_make_response(200)
        ):
            # Should not raise — None URL skips validation (defensive guard)
            result = session.send(prep)
            assert result.status_code == 200


class TestResponseCloseOnRedirect:
    """Verify response.close() is called on intermediate and error paths."""

    def test_intermediate_responses_closed_in_chain(self, mock_validate_url):
        """Each intermediate response is closed during a multi-hop redirect."""
        resp1 = _make_response(
            302, {"Location": "https://hop2.com"}, "https://example.com"
        )
        resp2 = _make_response(
            302, {"Location": "https://hop3.com"}, "https://hop2.com"
        )
        final = _make_response(200)

        with patch(
            "local_deep_research.security.safe_requests.requests.get",
            side_effect=[resp1, resp2, final],
        ):
            safe_get("https://example.com", allow_redirects=True)

        resp1.close.assert_called_once()
        resp2.close.assert_called_once()
        final.close.assert_not_called()

    def test_response_closed_on_ssrf_failure(self, mock_validate_url):
        """Response is closed when a redirect target fails SSRF validation."""
        redirect_resp = _make_response(
            302,
            {"Location": "http://169.254.169.254/metadata"},
            "https://example.com",
        )

        mock_validate_url.side_effect = [True, False]

        with patch(
            "local_deep_research.security.safe_requests.requests.get",
            return_value=redirect_resp,
        ):
            with pytest.raises(ValueError, match="Redirect target failed SSRF"):
                safe_get("https://example.com", allow_redirects=True)

        redirect_resp.close.assert_called_once()

    def test_response_closed_on_too_many_redirects(self, mock_validate_url):
        """Response is closed when too-many-redirects limit is exceeded."""
        redirect_resp = _make_response(
            301,
            {"Location": "https://example.com/loop"},
            "https://example.com",
        )

        with patch(
            "local_deep_research.security.safe_requests.requests.get",
            return_value=redirect_resp,
        ):
            with pytest.raises(ValueError, match="Too many redirects"):
                safe_get("https://example.com", allow_redirects=True)

        # The last response in the chain should be closed
        assert redirect_resp.close.called

    def test_response_closed_on_oversized_content_length(
        self, mock_validate_url
    ):
        """Response is closed when Content-Length exceeds MAX_RESPONSE_SIZE."""
        resp = _make_response(200)
        resp.headers = {"Content-Length": str(MAX_RESPONSE_SIZE + 1)}

        with patch(
            "local_deep_research.security.safe_requests.requests.get",
            return_value=resp,
        ):
            with pytest.raises(ValueError, match="Response too large"):
                safe_get("https://example.com")

        resp.close.assert_called_once()

    def test_post_response_closed_on_ssrf_failure(self, mock_validate_url):
        """safe_post closes response when redirect target fails SSRF validation."""
        redirect_resp = _make_response(
            307,
            {"Location": "http://169.254.169.254/metadata"},
            "https://example.com",
        )

        mock_validate_url.side_effect = [True, False]

        with patch(
            "local_deep_research.security.safe_requests.requests.post",
            return_value=redirect_resp,
        ):
            with pytest.raises(ValueError, match="Redirect target failed SSRF"):
                safe_post("https://example.com", allow_redirects=True)

        redirect_resp.close.assert_called_once()

    def test_post_response_closed_on_too_many_redirects(
        self, mock_validate_url
    ):
        """safe_post closes response when too-many-redirects limit is exceeded."""
        redirect_resp = _make_response(
            307,
            {"Location": "https://example.com/loop"},
            "https://example.com",
        )

        with patch(
            "local_deep_research.security.safe_requests.requests.post",
            return_value=redirect_resp,
        ):
            with pytest.raises(ValueError, match="Too many redirects"):
                safe_post("https://example.com", allow_redirects=True)

        assert redirect_resp.close.called

    def test_post_response_closed_on_oversized_content_length(
        self, mock_validate_url
    ):
        """safe_post closes response when Content-Length exceeds MAX_RESPONSE_SIZE."""
        resp = _make_response(200)
        resp.headers = {"Content-Length": str(MAX_RESPONSE_SIZE + 1)}

        with patch(
            "local_deep_research.security.safe_requests.requests.post",
            return_value=resp,
        ):
            with pytest.raises(ValueError, match="Response too large"):
                safe_post("https://example.com")

        resp.close.assert_called_once()


class TestPostBodyNotForwardedOnConversion:
    """POST body must not leak when method converts to GET on 302/303."""

    def test_post_302_then_307_keeps_get_no_body(self, mock_validate_url):
        """POST→302→GET→307→GET: body is dropped and method stays GET.

        The 302 converts POST to GET and drops the body. A subsequent 307
        preserves the (now-GET) method. The body must not reappear on any
        GET hop — this is the real invariant that ``data = None; json = None``
        in safe_post protects.
        """
        resp_302 = _make_response(
            302, {"Location": "https://hop2.com"}, "https://example.com"
        )
        resp_307 = _make_response(
            307, {"Location": "https://hop3.com"}, "https://hop2.com"
        )
        final_resp = _make_response(200)

        with (
            patch(
                "local_deep_research.security.safe_requests.requests.post",
                return_value=resp_302,
            ) as mock_post,
            patch(
                "local_deep_research.security.safe_requests.requests.get",
                side_effect=[resp_307, final_resp],
            ) as mock_get,
        ):
            safe_post(
                "https://example.com",
                data=b"secret",
                json={"password": "x"},
                allow_redirects=True,
            )

            # Only one POST (the initial request)
            assert mock_post.call_count == 1
            # Two GETs: the 302→GET hop and the 307→GET hop
            assert mock_get.call_count == 2

            # Neither GET hop should carry body kwargs
            for i, call in enumerate(mock_get.call_args_list):
                _, kwargs = call
                assert "data" not in kwargs, f"GET hop {i} leaked 'data'"
                assert "json" not in kwargs, f"GET hop {i} leaked 'json'"

    def test_post_303_drops_body(self, mock_validate_url):
        """On 303, the converted GET request must not carry data or json."""
        resp_303 = _make_response(
            303, {"Location": "https://other.com"}, "https://example.com"
        )
        final_resp = _make_response(200)

        with (
            patch(
                "local_deep_research.security.safe_requests.requests.post",
                return_value=resp_303,
            ),
            patch(
                "local_deep_research.security.safe_requests.requests.get",
                return_value=final_resp,
            ) as mock_get,
        ):
            safe_post(
                "https://example.com",
                data=b"secret",
                json={"password": "x"},
                allow_redirects=True,
            )

            # requests.get is called without data/json positional or keyword args
            assert mock_get.call_count == 1
            call_args, call_kwargs = mock_get.call_args
            # Positional: only the URL
            assert call_args == ("https://other.com",)
            assert "data" not in call_kwargs
            assert "json" not in call_kwargs


class TestSafePostPerHopValidation:
    """Verify validate_url is called for each hop in safe_post."""

    def test_each_hop_validated(self, mock_validate_url):
        """Each redirect hop in safe_post is validated against SSRF rules."""
        resp1 = _make_response(
            307, {"Location": "https://hop2.com"}, "https://example.com"
        )
        resp2 = _make_response(
            307, {"Location": "https://hop3.com"}, "https://hop2.com"
        )
        final = _make_response(200)

        with patch(
            "local_deep_research.security.safe_requests.requests.post",
            side_effect=[resp1, resp2, final],
        ):
            safe_post("https://example.com", allow_redirects=True)

        # Initial URL + 2 redirect targets = 3 validate_url calls
        assert mock_validate_url.call_count == 3
        validated_urls = [
            call.args[0] for call in mock_validate_url.call_args_list
        ]
        assert validated_urls == [
            "https://example.com",
            "https://hop2.com",
            "https://hop3.com",
        ]


class TestRelativeRedirectURL:
    """Test redirect with relative Location header."""

    def test_relative_location_resolved(self, mock_validate_url):
        """Location: /path should be resolved relative to the current URL."""
        redirect_resp = _make_response(
            302, {"Location": "/new-path"}, "https://example.com/old"
        )
        final_resp = _make_response(200)

        with patch(
            "local_deep_research.security.safe_requests.requests.get",
            side_effect=[redirect_resp, final_resp],
        ) as mock_get:
            safe_get("https://example.com/old", allow_redirects=True)

            # The second call should use the resolved absolute URL
            second_call_url = mock_get.call_args_list[1][0][0]
            assert second_call_url == "https://example.com/new-path"

    def test_response_url_none_fallback(self, mock_validate_url):
        """When response.url is None, current_url is used as the base."""
        redirect_resp = _make_response(302, {"Location": "/path"}, url=None)
        final_resp = _make_response(200)

        with patch(
            "local_deep_research.security.safe_requests.requests.get",
            side_effect=[redirect_resp, final_resp],
        ) as mock_get:
            safe_get("https://example.com/start", allow_redirects=True)

            second_call_url = mock_get.call_args_list[1][0][0]
            assert second_call_url == "https://example.com/path"


class TestSafeSessionContentLength:
    """Verify SafeSession.send() rejects oversized responses."""

    def test_send_rejects_oversized_response(self):
        """SafeSession.send() raises ValueError for oversized Content-Length."""
        session = SafeSession()

        prep = requests.PreparedRequest()
        prep.prepare_url("https://example.com", {})
        prep.prepare_method("GET")

        oversized_resp = _make_response(200)
        oversized_resp.headers = {"Content-Length": str(MAX_RESPONSE_SIZE + 1)}

        with patch.object(
            ssrf_validator,
            "validate_url",
            return_value=True,
        ):
            with patch.object(
                requests.Session, "send", return_value=oversized_resp
            ):
                with pytest.raises(ValueError, match="Response too large"):
                    session.send(prep)

        oversized_resp.close.assert_called_once()

    def test_send_allows_normal_response(self):
        """SafeSession.send() allows responses within size limit."""
        session = SafeSession()

        prep = requests.PreparedRequest()
        prep.prepare_url("https://example.com", {})
        prep.prepare_method("GET")

        normal_resp = _make_response(200)
        normal_resp.headers = {"Content-Length": "1024"}

        with patch.object(
            ssrf_validator,
            "validate_url",
            return_value=True,
        ):
            with patch.object(
                requests.Session, "send", return_value=normal_resp
            ):
                result = session.send(prep)
                assert result.status_code == 200


class TestNonWhitelistedStatusCodes:
    """Verify that non-whitelisted 3xx codes are NOT followed as redirects."""

    def test_304_not_followed(self, mock_validate_url):
        """304 Not Modified with Location header is returned as-is, not followed."""
        resp_304 = _make_response(
            304, {"Location": "https://other.com"}, "https://example.com"
        )

        with patch(
            "local_deep_research.security.safe_requests.requests.get",
            return_value=resp_304,
        ) as mock_get:
            result = safe_get("https://example.com", allow_redirects=True)
            assert result.status_code == 304
            # Only one request — the redirect was NOT followed
            assert mock_get.call_count == 1

    def test_300_not_followed(self, mock_validate_url):
        """300 Multiple Choices with Location header is returned as-is, not followed."""
        resp_300 = _make_response(
            300, {"Location": "https://other.com"}, "https://example.com"
        )

        with patch(
            "local_deep_research.security.safe_requests.requests.get",
            return_value=resp_300,
        ) as mock_get:
            result = safe_get("https://example.com", allow_redirects=True)
            assert result.status_code == 300
            assert mock_get.call_count == 1
