"""
Tests for HTTP client functionality.

This module provides comprehensive tests for:
- HTTP constants and configuration
- URL validation and security checks
- IP blocking functionality
- HttpResponse properties and methods
- Exception classes
- MockHttpClient behavior
"""

import pytest

from torivers_sdk.testing.mocks import MockHttpClient
from torivers_sdk.tools.http import (
    ALLOWED_SCHEMES,
    BLOCKED_NETWORKS,
    DEFAULT_TIMEOUT,
    HTTP_METHODS,
    MAX_REQUEST_SIZE,
    MAX_RESPONSE_SIZE,
    MAX_TIMEOUT,
    MIN_TIMEOUT,
    HttpBlockedIPError,
    HttpClient,
    HttpConnectionError,
    HttpError,
    HttpInvalidUrlError,
    HttpProxyClient,
    HttpRateLimitError,
    HttpRequestTooLargeError,
    HttpResponse,
    HttpResponseTooLargeError,
    HttpSchemeNotAllowedError,
    HttpStatusError,
    HttpTimeoutError,
    is_blocked_ip,
    validate_timeout,
    validate_url,
)

# =============================================================================
# Constants Tests
# =============================================================================


class TestHttpConstants:
    """Test suite for HTTP constants."""

    def test_blocked_networks_contains_private_ranges(self) -> None:
        """Test that blocked networks includes private IP ranges."""
        assert "10.0.0.0/8" in BLOCKED_NETWORKS
        assert "172.16.0.0/12" in BLOCKED_NETWORKS
        assert "192.168.0.0/16" in BLOCKED_NETWORKS

    def test_blocked_networks_contains_loopback(self) -> None:
        """Test that blocked networks includes loopback."""
        assert "127.0.0.0/8" in BLOCKED_NETWORKS

    def test_blocked_networks_contains_link_local(self) -> None:
        """Test that blocked networks includes link-local."""
        assert "169.254.0.0/16" in BLOCKED_NETWORKS

    def test_blocked_networks_contains_cloud_metadata(self) -> None:
        """Test that blocked networks includes cloud metadata endpoints."""
        assert "169.254.169.254/32" in BLOCKED_NETWORKS

    def test_blocked_networks_contains_ipv6(self) -> None:
        """Test that blocked networks includes IPv6 ranges."""
        assert "::1/128" in BLOCKED_NETWORKS
        assert "fc00::/7" in BLOCKED_NETWORKS
        assert "fe80::/10" in BLOCKED_NETWORKS

    def test_size_limits(self) -> None:
        """Test size limit constants."""
        assert MAX_REQUEST_SIZE == 1 * 1024 * 1024  # 1 MB
        assert MAX_RESPONSE_SIZE == 10 * 1024 * 1024  # 10 MB

    def test_timeout_constants(self) -> None:
        """Test timeout constants."""
        assert DEFAULT_TIMEOUT == 30.0
        assert MIN_TIMEOUT == 1.0
        assert MAX_TIMEOUT == 300.0

    def test_allowed_schemes(self) -> None:
        """Test allowed URL schemes."""
        assert "https" in ALLOWED_SCHEMES
        assert "http" not in ALLOWED_SCHEMES

    def test_http_methods(self) -> None:
        """Test HTTP method constants."""
        assert "GET" in HTTP_METHODS
        assert "POST" in HTTP_METHODS
        assert "PUT" in HTTP_METHODS
        assert "DELETE" in HTTP_METHODS
        assert "PATCH" in HTTP_METHODS
        assert "HEAD" in HTTP_METHODS
        assert "OPTIONS" in HTTP_METHODS


# =============================================================================
# is_blocked_ip Tests
# =============================================================================


class TestIsBlockedIp:
    """Test suite for is_blocked_ip function."""

    def test_private_class_a_blocked(self) -> None:
        """Test that Class A private IPs are blocked."""
        assert is_blocked_ip("10.0.0.1") is True
        assert is_blocked_ip("10.255.255.255") is True

    def test_private_class_b_blocked(self) -> None:
        """Test that Class B private IPs are blocked."""
        assert is_blocked_ip("172.16.0.1") is True
        assert is_blocked_ip("172.31.255.255") is True

    def test_private_class_c_blocked(self) -> None:
        """Test that Class C private IPs are blocked."""
        assert is_blocked_ip("192.168.0.1") is True
        assert is_blocked_ip("192.168.255.255") is True

    def test_loopback_blocked(self) -> None:
        """Test that loopback addresses are blocked."""
        assert is_blocked_ip("127.0.0.1") is True
        assert is_blocked_ip("127.255.255.255") is True

    def test_link_local_blocked(self) -> None:
        """Test that link-local addresses are blocked."""
        assert is_blocked_ip("169.254.0.1") is True
        assert is_blocked_ip("169.254.255.255") is True

    def test_cloud_metadata_blocked(self) -> None:
        """Test that cloud metadata endpoint is blocked."""
        assert is_blocked_ip("169.254.169.254") is True

    def test_ipv6_loopback_blocked(self) -> None:
        """Test that IPv6 loopback is blocked."""
        assert is_blocked_ip("::1") is True

    def test_public_ips_allowed(self) -> None:
        """Test that public IPs are allowed."""
        assert is_blocked_ip("8.8.8.8") is False
        assert is_blocked_ip("1.1.1.1") is False
        assert is_blocked_ip("93.184.216.34") is False

    def test_invalid_ip_blocked(self) -> None:
        """Test that invalid IPs are treated as blocked."""
        assert is_blocked_ip("invalid") is True
        assert is_blocked_ip("") is True
        assert is_blocked_ip("256.256.256.256") is True


# =============================================================================
# validate_url Tests
# =============================================================================


class TestValidateUrl:
    """Test suite for validate_url function."""

    def test_valid_https_url(self) -> None:
        """Test validation of valid HTTPS URL."""
        scheme, host, port = validate_url("https://api.example.com/path")
        assert scheme == "https"
        assert host == "api.example.com"
        assert port == 443

    def test_valid_https_url_with_port(self) -> None:
        """Test validation of HTTPS URL with custom port."""
        scheme, host, port = validate_url("https://api.example.com:8443/path")
        assert scheme == "https"
        assert host == "api.example.com"
        assert port == 8443

    def test_http_url_rejected(self) -> None:
        """Test that HTTP URLs are rejected."""
        with pytest.raises(HttpSchemeNotAllowedError) as exc_info:
            validate_url("http://example.com")
        assert exc_info.value.scheme == "http"
        assert "https" in exc_info.value.allowed_schemes

    def test_empty_url_rejected(self) -> None:
        """Test that empty URLs are rejected."""
        with pytest.raises(HttpInvalidUrlError):
            validate_url("")

    def test_none_url_rejected(self) -> None:
        """Test that None URLs are rejected."""
        with pytest.raises(HttpInvalidUrlError):
            validate_url(None)  # type: ignore

    def test_url_without_scheme_rejected(self) -> None:
        """Test that URLs without scheme are rejected."""
        with pytest.raises(HttpInvalidUrlError):
            validate_url("example.com/path")

    def test_url_without_host_rejected(self) -> None:
        """Test that URLs without host are rejected."""
        with pytest.raises(HttpInvalidUrlError):
            validate_url("https:///path")


# =============================================================================
# validate_timeout Tests
# =============================================================================


class TestValidateTimeout:
    """Test suite for validate_timeout function."""

    def test_valid_timeout(self) -> None:
        """Test that valid timeouts are unchanged."""
        assert validate_timeout(30.0) == 30.0
        assert validate_timeout(60.0) == 60.0

    def test_timeout_clamped_to_min(self) -> None:
        """Test that low timeouts are clamped to minimum."""
        assert validate_timeout(0.5) == MIN_TIMEOUT
        assert validate_timeout(0.0) == MIN_TIMEOUT

    def test_timeout_clamped_to_max(self) -> None:
        """Test that high timeouts are clamped to maximum."""
        assert validate_timeout(600.0) == MAX_TIMEOUT
        assert validate_timeout(1000.0) == MAX_TIMEOUT

    def test_boundary_values(self) -> None:
        """Test timeout boundary values."""
        assert validate_timeout(MIN_TIMEOUT) == MIN_TIMEOUT
        assert validate_timeout(MAX_TIMEOUT) == MAX_TIMEOUT


# =============================================================================
# HttpResponse Tests
# =============================================================================


class TestHttpResponse:
    """Test suite for HttpResponse dataclass."""

    def test_basic_response(self) -> None:
        """Test basic response creation."""
        response = HttpResponse(
            status_code=200,
            headers={"Content-Type": "application/json"},
            body=b'{"key": "value"}',
            url="https://api.example.com/data",
        )
        assert response.status_code == 200
        assert response.url == "https://api.example.com/data"

    def test_text_property(self) -> None:
        """Test text property decodes body."""
        response = HttpResponse(
            status_code=200,
            headers={},
            body=b"Hello, World!",
            url="https://example.com",
        )
        assert response.text == "Hello, World!"

    def test_json_method(self) -> None:
        """Test json method parses body."""
        response = HttpResponse(
            status_code=200,
            headers={},
            body=b'{"key": "value"}',
            url="https://example.com",
        )
        data = response.json()
        assert data == {"key": "value"}

    def test_ok_property_for_success(self) -> None:
        """Test ok property for success status codes."""
        for code in [200, 201, 204, 299]:
            response = HttpResponse(status_code=code, headers={}, body=b"", url="")
            assert response.ok is True

    def test_ok_property_for_errors(self) -> None:
        """Test ok property for error status codes."""
        for code in [400, 404, 500, 502]:
            response = HttpResponse(status_code=code, headers={}, body=b"", url="")
            assert response.ok is False

    def test_is_redirect_property(self) -> None:
        """Test is_redirect property."""
        for code in [301, 302, 307, 308]:
            response = HttpResponse(status_code=code, headers={}, body=b"", url="")
            assert response.is_redirect is True

        response = HttpResponse(status_code=200, headers={}, body=b"", url="")
        assert response.is_redirect is False

    def test_is_client_error_property(self) -> None:
        """Test is_client_error property."""
        for code in [400, 401, 403, 404, 422]:
            response = HttpResponse(status_code=code, headers={}, body=b"", url="")
            assert response.is_client_error is True

        response = HttpResponse(status_code=200, headers={}, body=b"", url="")
        assert response.is_client_error is False

    def test_is_server_error_property(self) -> None:
        """Test is_server_error property."""
        for code in [500, 502, 503, 504]:
            response = HttpResponse(status_code=code, headers={}, body=b"", url="")
            assert response.is_server_error is True

        response = HttpResponse(status_code=200, headers={}, body=b"", url="")
        assert response.is_server_error is False

    def test_content_type_property(self) -> None:
        """Test content_type property."""
        response = HttpResponse(
            status_code=200,
            headers={"Content-Type": "application/json"},
            body=b"",
            url="",
        )
        assert response.content_type == "application/json"

    def test_content_type_lowercase_header(self) -> None:
        """Test content_type with lowercase header."""
        response = HttpResponse(
            status_code=200,
            headers={"content-type": "text/html"},
            body=b"",
            url="",
        )
        assert response.content_type == "text/html"

    def test_content_length_from_header(self) -> None:
        """Test content_length from header."""
        response = HttpResponse(
            status_code=200,
            headers={"Content-Length": "100"},
            body=b"x" * 50,
            url="",
        )
        assert response.content_length == 100

    def test_content_length_from_body(self) -> None:
        """Test content_length from body when header missing."""
        response = HttpResponse(
            status_code=200,
            headers={},
            body=b"Hello",
            url="",
        )
        assert response.content_length == 5

    def test_raise_for_status_success(self) -> None:
        """Test raise_for_status with success code."""
        response = HttpResponse(status_code=200, headers={}, body=b"", url="")
        response.raise_for_status()  # Should not raise

    def test_raise_for_status_client_error(self) -> None:
        """Test raise_for_status with client error."""
        response = HttpResponse(
            status_code=404, headers={}, body=b"", url="https://example.com"
        )
        with pytest.raises(HttpStatusError) as exc_info:
            response.raise_for_status()
        assert exc_info.value.status_code == 404

    def test_raise_for_status_server_error(self) -> None:
        """Test raise_for_status with server error."""
        response = HttpResponse(
            status_code=500, headers={}, body=b"", url="https://example.com"
        )
        with pytest.raises(HttpStatusError) as exc_info:
            response.raise_for_status()
        assert exc_info.value.status_code == 500

    def test_elapsed_ms_default(self) -> None:
        """Test elapsed_ms default value."""
        response = HttpResponse(status_code=200, headers={}, body=b"", url="")
        assert response.elapsed_ms == 0.0

    def test_request_method_default(self) -> None:
        """Test request_method default value."""
        response = HttpResponse(status_code=200, headers={}, body=b"", url="")
        assert response.request_method == "GET"


# =============================================================================
# Exception Tests
# =============================================================================


class TestHttpError:
    """Test suite for HttpError base class."""

    def test_basic_error(self) -> None:
        """Test basic error creation."""
        error = HttpError("Something went wrong")
        assert str(error) == "Something went wrong"
        assert error.code is None
        assert error.url is None
        assert error.status_code is None

    def test_error_with_all_attributes(self) -> None:
        """Test error with all attributes."""
        error = HttpError(
            "Failed request",
            code="error_code",
            url="https://example.com",
            status_code=500,
        )
        assert error.code == "error_code"
        assert error.url == "https://example.com"
        assert error.status_code == 500


class TestHttpInvalidUrlError:
    """Test suite for HttpInvalidUrlError."""

    def test_basic_error(self) -> None:
        """Test basic invalid URL error."""
        error = HttpInvalidUrlError("Invalid URL format")
        assert "Invalid URL format" in str(error)
        assert error.code == "invalid_url"


class TestHttpSchemeNotAllowedError:
    """Test suite for HttpSchemeNotAllowedError."""

    def test_error_message(self) -> None:
        """Test error message includes scheme."""
        error = HttpSchemeNotAllowedError(
            url="http://example.com",
            scheme="http",
            allowed_schemes=["https"],
        )
        assert "http" in str(error)
        assert "https" in str(error)
        assert error.scheme == "http"
        assert error.code == "scheme_not_allowed"


class TestHttpBlockedIPError:
    """Test suite for HttpBlockedIPError."""

    def test_error_message(self) -> None:
        """Test error message includes IP."""
        error = HttpBlockedIPError(url="https://evil.com", ip="127.0.0.1")
        assert "127.0.0.1" in str(error)
        assert error.ip == "127.0.0.1"
        assert error.code == "blocked_ip"


class TestHttpRequestTooLargeError:
    """Test suite for HttpRequestTooLargeError."""

    def test_error_message(self) -> None:
        """Test error message includes sizes."""
        error = HttpRequestTooLargeError(
            size_bytes=2 * 1024 * 1024,
            max_size_bytes=1 * 1024 * 1024,
        )
        assert "2048.00 KB" in str(error)
        assert "1024.00 KB" in str(error)
        assert error.code == "request_too_large"


class TestHttpResponseTooLargeError:
    """Test suite for HttpResponseTooLargeError."""

    def test_error_message(self) -> None:
        """Test error message includes sizes."""
        error = HttpResponseTooLargeError(
            size_bytes=20 * 1024 * 1024,
            max_size_bytes=10 * 1024 * 1024,
        )
        assert "20.00 MB" in str(error)
        assert "10.00 MB" in str(error)
        assert error.code == "response_too_large"


class TestHttpTimeoutError:
    """Test suite for HttpTimeoutError."""

    def test_error_message(self) -> None:
        """Test error message includes timeout."""
        error = HttpTimeoutError(url="https://example.com", timeout=30.0)
        assert "30" in str(error)
        assert error.timeout == 30.0
        assert error.code == "timeout"


class TestHttpConnectionError:
    """Test suite for HttpConnectionError."""

    def test_basic_error(self) -> None:
        """Test basic connection error."""
        error = HttpConnectionError(url="https://example.com")
        assert "connect" in str(error).lower()
        assert error.code == "connection_error"

    def test_error_with_reason(self) -> None:
        """Test connection error with reason."""
        error = HttpConnectionError(
            url="https://example.com",
            reason="DNS resolution failed",
        )
        assert "DNS resolution failed" in str(error)


class TestHttpStatusError:
    """Test suite for HttpStatusError."""

    def test_basic_error(self) -> None:
        """Test basic status error."""
        error = HttpStatusError(
            message="Not Found",
            status_code=404,
            url="https://example.com",
        )
        assert error.status_code == 404
        assert error.code == "status_error"

    def test_is_client_error_property(self) -> None:
        """Test is_client_error property."""
        error = HttpStatusError("Not Found", status_code=404)
        assert error.is_client_error is True
        assert error.is_server_error is False

    def test_is_server_error_property(self) -> None:
        """Test is_server_error property."""
        error = HttpStatusError("Internal Error", status_code=500)
        assert error.is_client_error is False
        assert error.is_server_error is True


class TestHttpRateLimitError:
    """Test suite for HttpRateLimitError."""

    def test_basic_error(self) -> None:
        """Test basic rate limit error."""
        error = HttpRateLimitError(url="https://api.example.com")
        assert "rate limit" in str(error).lower()
        assert error.status_code == 429
        assert error.code == "rate_limit"

    def test_error_with_retry_after(self) -> None:
        """Test rate limit error with retry after."""
        error = HttpRateLimitError(
            url="https://api.example.com",
            retry_after=60.0,
        )
        assert "60" in str(error)
        assert error.retry_after == 60.0


# =============================================================================
# HttpClient Tests
# =============================================================================


class TestHttpClient:
    """Test suite for HttpClient abstract class."""

    def test_get_default_raises_runtime_error(self) -> None:
        """Test that get_default raises RuntimeError when env vars not set."""
        import os

        # Ensure env vars are not set
        env = os.environ.copy()
        os.environ.pop("TORIVERS_HTTP_PROXY_URL", None)
        os.environ.pop("TORIVERS_HTTP_TOKEN", None)
        try:
            with pytest.raises(RuntimeError) as exc_info:
                HttpClient.get_default()
            assert "runtime environment" in str(exc_info.value)
            assert "MockHttpClient" in str(exc_info.value)
        finally:
            os.environ.update(env)

    def test_get_default_returns_proxy_client(self) -> None:
        """Test that get_default returns HttpProxyClient when env vars are set."""
        import os

        os.environ["TORIVERS_HTTP_PROXY_URL"] = "http://localhost:8001"
        os.environ["TORIVERS_HTTP_TOKEN"] = "test-token"
        try:
            client = HttpClient.get_default()
            assert isinstance(client, HttpProxyClient)
        finally:
            os.environ.pop("TORIVERS_HTTP_PROXY_URL", None)
            os.environ.pop("TORIVERS_HTTP_TOKEN", None)


class TestHttpProxyClient:
    """Test suite for HttpProxyClient."""

    def _make_client(self) -> HttpProxyClient:
        return HttpProxyClient(base_url="http://localhost:8001", token="test-token")

    @pytest.mark.asyncio
    async def test_https_enforced(self) -> None:
        """Verify HTTP URLs raise HttpSchemeNotAllowedError client-side."""
        client = self._make_client()
        with pytest.raises(HttpSchemeNotAllowedError):
            await client.get("http://insecure.example.com/data")

    @pytest.mark.asyncio
    async def test_request_serialization(self) -> None:
        """Verify correct POST payload sent to proxy."""
        from unittest.mock import AsyncMock, patch

        import httpx

        client = self._make_client()

        mock_response = httpx.Response(
            200,
            json={
                "status_code": 200,
                "headers": {"content-type": "application/json"},
                "body": "eyJ0ZXN0IjogdHJ1ZX0=",  # base64('{"test": true}')
                "url": "https://api.example.com/data",
                "elapsed_ms": 50.0,
            },
            request=httpx.Request("POST", "http://localhost:8001/api/http/request"),
        )

        with patch.object(client, "_get_client") as mock_get:
            mock_http = AsyncMock()
            mock_http.post = AsyncMock(return_value=mock_response)
            mock_get.return_value = mock_http

            resp = await client.get(
                "https://api.example.com/data",
                headers={"Accept": "application/json"},
            )

            assert resp.status_code == 200
            assert resp.json() == {"test": True}
            mock_http.post.assert_called_once()
            call_kwargs = mock_http.post.call_args
            payload = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json")
            assert payload["method"] == "GET"
            assert payload["url"] == "https://api.example.com/data"

    @pytest.mark.asyncio
    async def test_response_deserialization(self) -> None:
        """Verify base64 body decoded correctly into HttpResponse."""
        import base64
        from unittest.mock import AsyncMock, patch

        import httpx

        client = self._make_client()
        raw_body = b"Hello, World!"
        encoded = base64.b64encode(raw_body).decode("ascii")

        mock_response = httpx.Response(
            200,
            json={
                "status_code": 200,
                "headers": {"content-type": "text/plain"},
                "body": encoded,
                "url": "https://example.com",
                "elapsed_ms": 10.0,
            },
            request=httpx.Request("POST", "http://localhost:8001/api/http/request"),
        )

        with patch.object(client, "_get_client") as mock_get:
            mock_http = AsyncMock()
            mock_http.post = AsyncMock(return_value=mock_response)
            mock_get.return_value = mock_http

            resp = await client.get("https://example.com")
            assert resp.body == raw_body
            assert resp.text == "Hello, World!"
            assert resp.status_code == 200

    @pytest.mark.asyncio
    async def test_error_code_mapping(self) -> None:
        """Verify proxy error codes map to correct SDK exceptions."""
        from unittest.mock import AsyncMock, patch

        import httpx

        client = self._make_client()

        error_cases = [
            ("blocked_ip", HttpBlockedIPError),
            ("rate_limit_exceeded", HttpRateLimitError),
            ("timeout", HttpTimeoutError),
        ]

        for error_code, expected_exc in error_cases:
            mock_response = httpx.Response(
                (
                    403
                    if error_code == "blocked_ip"
                    else 429 if error_code == "rate_limit_exceeded" else 504
                ),
                json={"error": "test error", "code": error_code},
                request=httpx.Request("POST", "http://localhost:8001/api/http/request"),
            )

            with patch.object(client, "_get_client") as mock_get:
                mock_http = AsyncMock()
                mock_http.post = AsyncMock(return_value=mock_response)
                mock_get.return_value = mock_http

                with pytest.raises(expected_exc):
                    await client.get("https://example.com")

    @pytest.mark.asyncio
    async def test_error_detail_mapping(self) -> None:
        """Verify FastAPI-style detail payloads are handled."""
        from unittest.mock import AsyncMock, patch

        import httpx

        client = self._make_client()
        mock_response = httpx.Response(
            429,
            json={"detail": {"code": "rate_limit_exceeded", "error": "too many"}},
            request=httpx.Request("POST", "http://localhost:8001/api/http/request"),
        )

        with patch.object(client, "_get_client") as mock_get:
            mock_http = AsyncMock()
            mock_http.post = AsyncMock(return_value=mock_response)
            mock_get.return_value = mock_http

            with pytest.raises(HttpRateLimitError):
                await client.get("https://example.com")

    @pytest.mark.asyncio
    async def test_token_error_maps_to_runtime_error(self) -> None:
        """Verify auth errors bubble as runtime auth failures."""
        from unittest.mock import AsyncMock, patch

        import httpx

        client = self._make_client()
        mock_response = httpx.Response(
            401,
            json={"detail": "token_invalid"},
            request=httpx.Request("POST", "http://localhost:8001/api/http/request"),
        )

        with patch.object(client, "_get_client") as mock_get:
            mock_http = AsyncMock()
            mock_http.post = AsyncMock(return_value=mock_response)
            mock_get.return_value = mock_http

            with pytest.raises(RuntimeError):
                await client.get("https://example.com")


# =============================================================================
# MockHttpClient Tests
# =============================================================================


class TestMockHttpClient:
    """Test suite for MockHttpClient."""

    @pytest.mark.asyncio
    async def test_basic_get_request(self) -> None:
        """Test basic GET request."""
        http = MockHttpClient()
        response = await http.get("https://api.example.com/data")
        assert response.status_code == 200
        assert response.url == "https://api.example.com/data"

    @pytest.mark.asyncio
    async def test_get_with_custom_response(self) -> None:
        """Test GET with custom response."""
        http = MockHttpClient()
        http.add_response(
            "https://api.example.com/data",
            HttpResponse(
                status_code=201,
                headers={"X-Custom": "header"},
                body=b'{"result": "success"}',
                url="https://api.example.com/data",
            ),
        )
        response = await http.get("https://api.example.com/data")
        assert response.status_code == 201
        assert response.json() == {"result": "success"}

    @pytest.mark.asyncio
    async def test_post_request(self) -> None:
        """Test POST request."""
        http = MockHttpClient()
        response = await http.post(
            "https://api.example.com/data",
            json={"key": "value"},
        )
        assert response.status_code == 200
        assert len(http.post_calls) == 1

    @pytest.mark.asyncio
    async def test_put_request(self) -> None:
        """Test PUT request."""
        http = MockHttpClient()
        response = await http.put(
            "https://api.example.com/data",
            data=b"content",
        )
        assert response.status_code == 200
        assert len(http.put_calls) == 1

    @pytest.mark.asyncio
    async def test_delete_request(self) -> None:
        """Test DELETE request."""
        http = MockHttpClient()
        response = await http.delete("https://api.example.com/data")
        assert response.status_code == 200
        assert len(http.delete_calls) == 1

    @pytest.mark.asyncio
    async def test_patch_request(self) -> None:
        """Test PATCH request."""
        http = MockHttpClient()
        response = await http.patch(
            "https://api.example.com/data",
            json={"update": "value"},
        )
        assert response.status_code == 200
        assert len(http.patch_calls) == 1

    @pytest.mark.asyncio
    async def test_call_tracking(self) -> None:
        """Test call tracking."""
        http = MockHttpClient()
        await http.get("https://api.example.com/1")
        await http.post("https://api.example.com/2")
        await http.get("https://api.example.com/3")

        assert len(http.calls) == 3
        assert len(http.get_calls) == 2
        assert len(http.post_calls) == 1

    @pytest.mark.asyncio
    async def test_queued_responses(self) -> None:
        """Test response queue functionality."""
        http = MockHttpClient()
        http.add_queued_response(
            HttpResponse(status_code=200, headers={}, body=b"first", url="")
        )
        http.add_queued_response(
            HttpResponse(status_code=201, headers={}, body=b"second", url="")
        )
        http.add_queued_response(
            HttpResponse(status_code=202, headers={}, body=b"third", url="")
        )

        r1 = await http.get("https://example.com/a")
        r2 = await http.get("https://example.com/b")
        r3 = await http.get("https://example.com/c")

        assert r1.status_code == 200
        assert r2.status_code == 201
        assert r3.status_code == 202

    @pytest.mark.asyncio
    async def test_url_validation_https_required(self) -> None:
        """Test URL validation rejects HTTP."""
        http = MockHttpClient(validate_urls=True)
        with pytest.raises(HttpSchemeNotAllowedError):
            await http.get("http://insecure.example.com")

    @pytest.mark.asyncio
    async def test_blocked_ip_simulation(self) -> None:
        """Test blocked IP simulation."""
        http = MockHttpClient(validate_urls=True)
        http.simulate_ip("evil.example.com", "127.0.0.1")

        with pytest.raises(HttpBlockedIPError) as exc_info:
            await http.get("https://evil.example.com")
        assert exc_info.value.ip == "127.0.0.1"

    @pytest.mark.asyncio
    async def test_request_size_limit(self) -> None:
        """Test request size limit enforcement."""
        http = MockHttpClient(
            enforce_size_limits=True,
            max_request_size=1024,
        )
        large_data = b"x" * 2048

        with pytest.raises(HttpRequestTooLargeError):
            await http.post("https://api.example.com", data=large_data)

    @pytest.mark.asyncio
    async def test_request_size_limit_with_dict(self) -> None:
        """Test request size limit with dict data."""
        http = MockHttpClient(
            enforce_size_limits=True,
            max_request_size=100,
        )
        large_dict = {"data": "x" * 200}

        with pytest.raises(HttpRequestTooLargeError):
            await http.post("https://api.example.com", data=large_dict)


class TestMockHttpClientAssertions:
    """Test suite for MockHttpClient assertion methods."""

    @pytest.mark.asyncio
    async def test_assert_called(self) -> None:
        """Test assert_called method."""
        http = MockHttpClient()
        await http.get("https://example.com")
        http.assert_called()
        http.assert_called("GET")

    @pytest.mark.asyncio
    async def test_assert_called_fails_when_not_called(self) -> None:
        """Test assert_called fails when not called."""
        http = MockHttpClient()
        with pytest.raises(AssertionError):
            http.assert_called()

    @pytest.mark.asyncio
    async def test_assert_not_called(self) -> None:
        """Test assert_not_called method."""
        http = MockHttpClient()
        http.assert_not_called()
        http.assert_not_called("POST")

    @pytest.mark.asyncio
    async def test_assert_not_called_fails_when_called(self) -> None:
        """Test assert_not_called fails when called."""
        http = MockHttpClient()
        await http.get("https://example.com")
        with pytest.raises(AssertionError):
            http.assert_not_called()

    @pytest.mark.asyncio
    async def test_assert_call_count(self) -> None:
        """Test assert_call_count method."""
        http = MockHttpClient()
        await http.get("https://example.com/1")
        await http.get("https://example.com/2")
        await http.post("https://example.com/3")

        http.assert_call_count(3)
        http.assert_call_count(2, "GET")
        http.assert_call_count(1, "POST")

    @pytest.mark.asyncio
    async def test_assert_call_count_fails_on_mismatch(self) -> None:
        """Test assert_call_count fails on mismatch."""
        http = MockHttpClient()
        await http.get("https://example.com")
        with pytest.raises(AssertionError):
            http.assert_call_count(2)

    @pytest.mark.asyncio
    async def test_assert_called_with_url(self) -> None:
        """Test assert_called_with_url method."""
        http = MockHttpClient()
        await http.get("https://api.example.com/users")
        await http.post("https://api.example.com/data")

        http.assert_called_with_url("https://api.example.com/users")
        http.assert_called_with_url("https://api.example.com/users", "GET")

    @pytest.mark.asyncio
    async def test_assert_called_with_url_fails(self) -> None:
        """Test assert_called_with_url fails for uncalled URL."""
        http = MockHttpClient()
        await http.get("https://example.com")

        with pytest.raises(AssertionError):
            http.assert_called_with_url("https://other.com")

    @pytest.mark.asyncio
    async def test_get_last_call(self) -> None:
        """Test get_last_call method."""
        http = MockHttpClient()
        await http.get("https://example.com/1")
        await http.post("https://example.com/2")
        await http.get("https://example.com/3")

        last = http.get_last_call()
        assert last is not None
        assert last["url"] == "https://example.com/3"

        last_get = http.get_last_call("GET")
        assert last_get is not None
        assert last_get["url"] == "https://example.com/3"

        last_post = http.get_last_call("POST")
        assert last_post is not None
        assert last_post["url"] == "https://example.com/2"

    @pytest.mark.asyncio
    async def test_get_last_call_returns_none_when_empty(self) -> None:
        """Test get_last_call returns None when no calls."""
        http = MockHttpClient()
        assert http.get_last_call() is None
        assert http.get_last_call("GET") is None

    @pytest.mark.asyncio
    async def test_clear_calls(self) -> None:
        """Test clear_calls method."""
        http = MockHttpClient()
        await http.get("https://example.com")
        assert len(http.calls) == 1

        http.clear_calls()
        assert len(http.calls) == 0

    @pytest.mark.asyncio
    async def test_clear_responses(self) -> None:
        """Test clear_responses method."""
        http = MockHttpClient()
        http.add_response(
            "https://example.com",
            HttpResponse(status_code=201, headers={}, body=b"", url=""),
        )
        http.add_queued_response(
            HttpResponse(status_code=202, headers={}, body=b"", url="")
        )

        http.clear_responses()

        # Should now use default response
        response = await http.get("https://example.com")
        assert response.status_code == 200


class TestMockHttpClientResponseMethods:
    """Test suite for MockHttpClient response configuration."""

    @pytest.mark.asyncio
    async def test_set_default_response(self) -> None:
        """Test set_default_response method."""
        http = MockHttpClient()
        http.set_default_response(
            HttpResponse(
                status_code=418,
                headers={"X-Teapot": "true"},
                body=b"I'm a teapot",
                url="",
            )
        )

        response = await http.get("https://any-url.com")
        assert response.status_code == 418
        assert response.text == "I'm a teapot"

    @pytest.mark.asyncio
    async def test_add_queued_responses(self) -> None:
        """Test add_queued_responses method."""
        http = MockHttpClient()
        http.add_queued_responses(
            [
                HttpResponse(status_code=200, headers={}, body=b"1", url=""),
                HttpResponse(status_code=201, headers={}, body=b"2", url=""),
            ]
        )

        r1 = await http.get("https://example.com")
        r2 = await http.get("https://example.com")

        assert r1.status_code == 200
        assert r2.status_code == 201

    @pytest.mark.asyncio
    async def test_response_includes_request_method(self) -> None:
        """Test that response includes request method."""
        http = MockHttpClient()

        get_response = await http.get("https://example.com")
        assert get_response.request_method == "GET"

        post_response = await http.post("https://example.com")
        assert post_response.request_method == "POST"

    @pytest.mark.asyncio
    async def test_timeout_recorded_in_call(self) -> None:
        """Test that timeout is recorded in calls."""
        http = MockHttpClient()
        await http.get("https://example.com", timeout=60.0)

        last_call = http.get_last_call()
        assert last_call is not None
        assert last_call["timeout"] == 60.0
