"""
Tests for arp_sdk.http module.

Uses respx to mock httpx transport-level requests and verify that HttpClient
correctly dispatches methods, handles errors, retries, and manages its lifecycle.
"""

import httpx
import pytest
import respx

from arp_sdk.errors import (
    ARPError,
    AuthError,
    NotFoundError,
    RateLimitError,
    ServerError,
    TimeoutError as ARPTimeoutError,
)
from arp_sdk.http import HttpClient


BASE_URL = "https://api.arp.test"
DEFAULT_HEADERS = {
    "Authorization": "Bearer test-key",
    "Accept": "application/json",
}


@pytest.fixture
def client():
    """Create an HttpClient with no retries (for non-retry tests)."""
    c = HttpClient(
        base_url=BASE_URL,
        headers=DEFAULT_HEADERS,
        timeout=5.0,
        max_retries=0,
    )
    yield c
    c.close()


@pytest.fixture
def retry_client():
    """Create an HttpClient that retries up to 2 times, for retry tests."""
    c = HttpClient(
        base_url=BASE_URL,
        headers=DEFAULT_HEADERS,
        timeout=5.0,
        max_retries=2,
    )
    yield c
    c.close()


# ──────────────────────────────────────────────
# Basic request methods
# ──────────────────────────────────────────────


class TestGetRequest:
    """GET requests should send the correct method and URL."""

    @respx.mock
    def test_get_sends_correct_method_and_url(self, client):
        route = respx.get(f"{BASE_URL}/v1/agents/abc").mock(
            return_value=httpx.Response(200, json={"id": "abc", "name": "Agent"})
        )
        result = client.get("/v1/agents/abc")
        assert route.called
        assert result == {"id": "abc", "name": "Agent"}

    @respx.mock
    def test_get_sends_query_params(self, client):
        route = respx.get(f"{BASE_URL}/v1/agents").mock(
            return_value=httpx.Response(200, json={"data": []})
        )
        result = client.get("/v1/agents", params={"page": 1, "limit": 10})
        assert route.called
        request = route.calls[0].request
        query = str(request.url.query)
        assert "page=1" in query
        assert "limit=10" in query

    @respx.mock
    def test_get_strips_none_params(self, client):
        route = respx.get(f"{BASE_URL}/v1/agents").mock(
            return_value=httpx.Response(200, json={"data": []})
        )
        client.get("/v1/agents", params={"page": 1, "status": None})
        request = route.calls[0].request
        query = str(request.url.query)
        assert "page=1" in query
        assert "status" not in query


class TestPostRequest:
    """POST requests should send body as JSON."""

    @respx.mock
    def test_post_sends_json_body(self, client):
        route = respx.post(f"{BASE_URL}/v1/agents").mock(
            return_value=httpx.Response(201, json={"id": "new-agent"})
        )
        body = {"name": "NewAgent", "description": "A new agent"}
        result = client.post("/v1/agents", body)
        assert route.called
        assert result == {"id": "new-agent"}
        request = route.calls[0].request
        assert b'"name"' in request.content
        assert b'"NewAgent"' in request.content


class TestPatchRequest:
    """PATCH requests should send body as JSON."""

    @respx.mock
    def test_patch_sends_json_body(self, client):
        route = respx.patch(f"{BASE_URL}/v1/agents/abc").mock(
            return_value=httpx.Response(200, json={"id": "abc", "name": "Updated"})
        )
        body = {"name": "Updated"}
        result = client.patch("/v1/agents/abc", body)
        assert route.called
        assert result == {"id": "abc", "name": "Updated"}
        request = route.calls[0].request
        assert b'"Updated"' in request.content


class TestDeleteRequest:
    """DELETE requests should return None."""

    @respx.mock
    def test_delete_returns_none(self, client):
        route = respx.delete(f"{BASE_URL}/v1/agents/abc").mock(
            return_value=httpx.Response(204)
        )
        result = client.delete("/v1/agents/abc")
        assert route.called
        assert result is None


# ──────────────────────────────────────────────
# HTTP status code handling
# ──────────────────────────────────────────────


class TestErrorResponses:
    """Non-2xx responses should raise the correct error type."""

    @respx.mock
    def test_401_raises_auth_error(self, client):
        respx.get(f"{BASE_URL}/v1/agents/abc").mock(
            return_value=httpx.Response(
                401,
                json={"message": "Unauthorized"},
                headers={"content-type": "application/json"},
            )
        )
        with pytest.raises(AuthError) as exc_info:
            client.get("/v1/agents/abc")
        assert exc_info.value.code == "AUTH_ERROR"
        assert "Unauthorized" in exc_info.value.message

    @respx.mock
    def test_403_raises_auth_error(self, client):
        respx.get(f"{BASE_URL}/v1/agents/abc").mock(
            return_value=httpx.Response(
                403,
                json={"message": "Forbidden"},
                headers={"content-type": "application/json"},
            )
        )
        with pytest.raises(AuthError):
            client.get("/v1/agents/abc")

    @respx.mock
    def test_404_raises_not_found_error(self, client):
        respx.get(f"{BASE_URL}/v1/agents/missing").mock(
            return_value=httpx.Response(
                404,
                json={"message": "Not found"},
                headers={"content-type": "application/json"},
            )
        )
        with pytest.raises(NotFoundError) as exc_info:
            client.get("/v1/agents/missing")
        assert exc_info.value.code == "NOT_FOUND"

    @respx.mock
    def test_429_raises_rate_limit_error_no_retry(self, client):
        """With max_retries=0, a 429 should raise immediately."""
        respx.get(f"{BASE_URL}/v1/agents").mock(
            return_value=httpx.Response(
                429,
                json={"message": "Too many requests", "retryAfter": 30},
                headers={"content-type": "application/json"},
            )
        )
        with pytest.raises(RateLimitError) as exc_info:
            client.get("/v1/agents")
        assert exc_info.value.code == "RATE_LIMITED"

    @respx.mock
    def test_500_raises_server_error_no_retry(self, client):
        """With max_retries=0, a 500 should raise immediately."""
        respx.get(f"{BASE_URL}/v1/agents").mock(
            return_value=httpx.Response(
                500,
                json={"message": "Internal server error"},
                headers={"content-type": "application/json"},
            )
        )
        with pytest.raises(ServerError) as exc_info:
            client.get("/v1/agents")
        assert exc_info.value.status_code == 500


class TestNoContentResponse:
    """204 No Content should return None."""

    @respx.mock
    def test_204_returns_none(self, client):
        respx.post(f"{BASE_URL}/v1/action").mock(
            return_value=httpx.Response(204)
        )
        result = client.post("/v1/action")
        assert result is None


# ──────────────────────────────────────────────
# Retry logic
# ──────────────────────────────────────────────


class TestRetryLogic:
    """Retryable status codes (429, 5xx) should trigger retries."""

    @respx.mock
    def test_429_retries_then_succeeds(self, retry_client):
        """A 429 followed by a 200 should succeed after retry."""
        route = respx.get(f"{BASE_URL}/v1/agents").mock(
            side_effect=[
                httpx.Response(
                    429,
                    json={"message": "Rate limited"},
                    headers={"content-type": "application/json", "retry-after": "0"},
                ),
                httpx.Response(200, json={"data": "ok"}),
            ]
        )
        result = retry_client.get("/v1/agents")
        assert result == {"data": "ok"}
        assert route.call_count == 2

    @respx.mock
    def test_500_retries_then_succeeds(self, retry_client):
        """A 500 followed by a 200 should succeed after retry."""
        route = respx.get(f"{BASE_URL}/v1/agents").mock(
            side_effect=[
                httpx.Response(
                    500,
                    json={"message": "Server error"},
                    headers={"content-type": "application/json", "retry-after": "0"},
                ),
                httpx.Response(200, json={"data": "recovered"}),
            ]
        )
        result = retry_client.get("/v1/agents")
        assert result == {"data": "recovered"}
        assert route.call_count == 2

    @respx.mock
    def test_retries_exhausted_raises(self, retry_client):
        """When all retries are exhausted, the error should be raised."""
        respx.get(f"{BASE_URL}/v1/agents").mock(
            side_effect=[
                httpx.Response(
                    500,
                    json={"message": "Down"},
                    headers={"content-type": "application/json", "retry-after": "0"},
                ),
                httpx.Response(
                    500,
                    json={"message": "Still down"},
                    headers={"content-type": "application/json", "retry-after": "0"},
                ),
                httpx.Response(
                    500,
                    json={"message": "Still down"},
                    headers={"content-type": "application/json", "retry-after": "0"},
                ),
            ]
        )
        with pytest.raises(ServerError):
            retry_client.get("/v1/agents")

    @respx.mock
    def test_non_retryable_status_does_not_retry(self, retry_client):
        """A 404 is not retryable, so it should raise immediately."""
        route = respx.get(f"{BASE_URL}/v1/agents/missing").mock(
            return_value=httpx.Response(
                404,
                json={"message": "Not found"},
                headers={"content-type": "application/json"},
            )
        )
        with pytest.raises(NotFoundError):
            retry_client.get("/v1/agents/missing")
        assert route.call_count == 1


# ──────────────────────────────────────────────
# Timeout handling
# ──────────────────────────────────────────────


class TestTimeoutHandling:
    """Timeouts should raise ARPTimeoutError."""

    @respx.mock
    def test_timeout_raises_timeout_error(self, client):
        respx.get(f"{BASE_URL}/v1/agents").mock(
            side_effect=httpx.ReadTimeout("read timeout")
        )
        with pytest.raises(ARPTimeoutError) as exc_info:
            client.get("/v1/agents")
        assert exc_info.value.code == "TIMEOUT"
        assert "timed out" in exc_info.value.message

    @respx.mock
    def test_connect_timeout_raises_timeout_error(self, client):
        respx.get(f"{BASE_URL}/v1/agents").mock(
            side_effect=httpx.ConnectTimeout("connect timeout")
        )
        with pytest.raises(ARPTimeoutError):
            client.get("/v1/agents")


# ──────────────────────────────────────────────
# Context manager
# ──────────────────────────────────────────────


class TestContextManager:
    """HttpClient should work as a context manager."""

    @respx.mock
    def test_context_manager_closes_client(self):
        respx.get(f"{BASE_URL}/v1/ping").mock(
            return_value=httpx.Response(200, json={"status": "ok"})
        )
        with HttpClient(
            base_url=BASE_URL,
            headers=DEFAULT_HEADERS,
            timeout=5.0,
            max_retries=0,
        ) as http:
            result = http.get("/v1/ping")
            assert result == {"status": "ok"}
        # After exiting context, the client is closed.
        # Attempting a new request should fail.
        # (httpx raises an error when using a closed client)

    @respx.mock
    def test_context_manager_returns_self(self):
        with HttpClient(
            base_url=BASE_URL,
            headers=DEFAULT_HEADERS,
            timeout=5.0,
            max_retries=0,
        ) as http:
            assert isinstance(http, HttpClient)


# ──────────────────────────────────────────────
# Network error handling
# ──────────────────────────────────────────────


class TestNetworkErrors:
    """Non-timeout HTTP errors should raise ARPError with NETWORK_ERROR code."""

    @respx.mock
    def test_network_error_raises_arp_error(self, client):
        respx.get(f"{BASE_URL}/v1/agents").mock(
            side_effect=httpx.ConnectError("Connection refused")
        )
        with pytest.raises(ARPError) as exc_info:
            client.get("/v1/agents")
        assert exc_info.value.code == "NETWORK_ERROR"


# ──────────────────────────────────────────────
# Response body parsing edge cases
# ──────────────────────────────────────────────


class TestResponseParsing:
    """Verify that error responses with various content types are handled."""

    @respx.mock
    def test_error_with_plain_text_body(self, client):
        respx.get(f"{BASE_URL}/v1/agents").mock(
            return_value=httpx.Response(
                500,
                text="Something went wrong",
                headers={"content-type": "text/plain"},
            )
        )
        with pytest.raises(ServerError) as exc_info:
            client.get("/v1/agents")
        # Plain text body cannot be parsed as JSON, falls back to default message
        assert exc_info.value.status_code == 500

    @respx.mock
    def test_error_with_empty_body(self, client):
        respx.get(f"{BASE_URL}/v1/agents").mock(
            return_value=httpx.Response(404, text="")
        )
        with pytest.raises(NotFoundError) as exc_info:
            client.get("/v1/agents")
        assert "404" in exc_info.value.message

    @respx.mock
    def test_error_with_json_content_type(self, client):
        respx.get(f"{BASE_URL}/v1/agents").mock(
            return_value=httpx.Response(
                400,
                json={"message": "Validation failed", "fieldErrors": {"name": ["required"]}},
                headers={"content-type": "application/json"},
            )
        )
        from arp_sdk.errors import ValidationError

        with pytest.raises(ValidationError) as exc_info:
            client.get("/v1/agents")
        assert exc_info.value.message == "Validation failed"
