"""Tests for MCPGateway SDK HTTP client."""

from __future__ import annotations

from typing import Any
from unittest.mock import AsyncMock, MagicMock

import httpx
import pytest

from mcpgateway_sdk._config import SDKConfig
from mcpgateway_sdk._exceptions import GatewayError, NotFoundError
from mcpgateway_sdk._http import HTTPClient


def _make_response(
    status_code: int,
    json_body: dict[str, Any] | None = None,
    content: bytes = b"",
    is_success: bool | None = None,
) -> MagicMock:
    """Create a mock httpx.Response."""
    resp = MagicMock()
    resp.status_code = status_code
    if is_success is None:
        resp.is_success = 200 <= status_code < 300
    else:
        resp.is_success = is_success
    if json_body is not None:
        resp.json.return_value = json_body
        resp.content = content or b"{}"
    else:
        resp.json.side_effect = ValueError("No JSON")
        resp.content = content
    return resp


@pytest.fixture
def config() -> SDKConfig:
    return SDKConfig(api_key="test-api-key", url="https://gw.example.com")


@pytest.fixture
def client(config: SDKConfig) -> HTTPClient:
    http = HTTPClient(config)
    # Replace the real httpx.AsyncClient with a mock
    http._client = AsyncMock()
    return http


class TestRequest:
    """Tests for HTTPClient.request method."""

    async def test_get_sends_authorization_header(self, client: HTTPClient) -> None:
        # Arrange
        client._client.request = AsyncMock(return_value=_make_response(200, json_body={"ok": True}))

        # Act
        await client.request("GET", "/api/v1/test")

        # Assert
        call_kwargs = client._client.request.call_args
        headers = call_kwargs.kwargs.get("headers") or call_kwargs[1].get("headers")
        assert headers["Authorization"] == "Bearer test-api-key"

    async def test_get_returns_parsed_json(self, client: HTTPClient) -> None:
        # Arrange
        expected = {"items": [1, 2, 3], "total": 3}
        client._client.request = AsyncMock(return_value=_make_response(200, json_body=expected))

        # Act
        result = await client.request("GET", "/api/v1/items")

        # Assert
        assert result == expected

    async def test_404_raises_not_found_error(self, client: HTTPClient) -> None:
        # Arrange
        client._client.request = AsyncMock(
            return_value=_make_response(404, json_body={"detail": "Resource not found"})
        )

        # Act & Assert
        with pytest.raises(NotFoundError) as exc_info:
            await client.request("GET", "/api/v1/missing")
        assert exc_info.value.message == "Resource not found"

    async def test_500_raises_gateway_error(self, client: HTTPClient) -> None:
        # Arrange
        client._client.request = AsyncMock(
            return_value=_make_response(500, json_body={"detail": "Internal error"})
        )

        # Act & Assert
        with pytest.raises(GatewayError) as exc_info:
            await client.request("GET", "/api/v1/broken")
        assert exc_info.value.status_code == 500
        assert exc_info.value.message == "Internal error"

    async def test_204_returns_none(self, client: HTTPClient) -> None:
        # Arrange
        client._client.request = AsyncMock(return_value=_make_response(204, content=b""))

        # Act
        result = await client.request("DELETE", "/api/v1/items/1")

        # Assert
        assert result is None

    async def test_error_with_non_json_body(self, client: HTTPClient) -> None:
        # Arrange
        resp = _make_response(502, content=b"Bad Gateway")
        resp.is_success = False
        resp.json.side_effect = ValueError("Not JSON")
        client._client.request = AsyncMock(return_value=resp)

        # Act & Assert
        with pytest.raises(GatewayError) as exc_info:
            await client.request("GET", "/api/v1/test")
        assert "Bad Gateway" in exc_info.value.message

    async def test_post_sends_json_body(self, client: HTTPClient) -> None:
        # Arrange
        client._client.request = AsyncMock(
            return_value=_make_response(201, json_body={"id": "abc123"})
        )
        payload = {"name": "test", "value": 42}

        # Act
        result = await client.request("POST", "/api/v1/items", json=payload)

        # Assert
        call_kwargs = client._client.request.call_args
        assert call_kwargs.kwargs.get("json") == payload
        assert result == {"id": "abc123"}

    async def test_custom_headers_merged(self, client: HTTPClient) -> None:
        # Arrange
        client._client.request = AsyncMock(return_value=_make_response(200, json_body={}))

        # Act
        await client.request("GET", "/api/v1/test", headers={"X-Custom": "value"})

        # Assert
        call_kwargs = client._client.request.call_args
        headers = call_kwargs.kwargs.get("headers") or call_kwargs[1].get("headers")
        assert headers["Authorization"] == "Bearer test-api-key"
        assert headers["X-Custom"] == "value"


class TestRequestRaw:
    """Tests for HTTPClient.request_raw method."""

    async def test_returns_bytes_on_success(self, client: HTTPClient) -> None:
        # Arrange
        raw_content = b"\x89PNG\r\n\x1a\n"
        resp = _make_response(200, content=raw_content)
        resp.is_success = True
        client._client.request = AsyncMock(return_value=resp)

        # Act
        result = await client.request_raw("GET", "/api/v1/export")

        # Assert
        assert result == raw_content
        assert isinstance(result, bytes)

    async def test_raises_on_error(self, client: HTTPClient) -> None:
        # Arrange
        client._client.request = AsyncMock(
            return_value=_make_response(404, json_body={"detail": "Not found"})
        )

        # Act & Assert
        with pytest.raises(NotFoundError):
            await client.request_raw("GET", "/api/v1/missing")

    async def test_sends_authorization_header(self, client: HTTPClient) -> None:
        # Arrange
        resp = _make_response(200, content=b"data")
        resp.is_success = True
        client._client.request = AsyncMock(return_value=resp)

        # Act
        await client.request_raw("GET", "/api/v1/export")

        # Assert
        call_kwargs = client._client.request.call_args
        headers = call_kwargs.kwargs.get("headers") or call_kwargs[1].get("headers")
        assert headers["Authorization"] == "Bearer test-api-key"


class TestNetworkErrorWrapping:
    """Tests that httpx network errors are wrapped in GatewayError."""

    async def test_timeout_raises_gateway_error(self, client: HTTPClient) -> None:
        # Arrange
        client._client.request = AsyncMock(
            side_effect=httpx.ReadTimeout("timed out")
        )

        # Act & Assert
        with pytest.raises(GatewayError) as exc_info:
            await client.request("GET", "/api/v1/test")
        assert exc_info.value.status_code == 0
        assert "timed out" in exc_info.value.message.lower()

    async def test_connect_error_raises_gateway_error(self, client: HTTPClient) -> None:
        # Arrange
        client._client.request = AsyncMock(
            side_effect=httpx.ConnectError("Connection refused")
        )

        # Act & Assert
        with pytest.raises(GatewayError) as exc_info:
            await client.request("GET", "/api/v1/test")
        assert exc_info.value.status_code == 0
        assert "Cannot connect" in exc_info.value.message

    async def test_generic_http_error_raises_gateway_error(self, client: HTTPClient) -> None:
        # Arrange
        client._client.request = AsyncMock(
            side_effect=httpx.HTTPError("something broke")
        )

        # Act & Assert
        with pytest.raises(GatewayError) as exc_info:
            await client.request("GET", "/api/v1/test")
        assert exc_info.value.status_code == 0
        assert "Network error" in exc_info.value.message

    async def test_timeout_on_request_raw(self, client: HTTPClient) -> None:
        # Arrange
        client._client.request = AsyncMock(
            side_effect=httpx.ReadTimeout("timed out")
        )

        # Act & Assert
        with pytest.raises(GatewayError) as exc_info:
            await client.request_raw("GET", "/api/v1/export")
        assert exc_info.value.status_code == 0
        assert "timed out" in exc_info.value.message.lower()

    async def test_connect_error_on_request_raw(self, client: HTTPClient) -> None:
        # Arrange
        client._client.request = AsyncMock(
            side_effect=httpx.ConnectError("Connection refused")
        )

        # Act & Assert
        with pytest.raises(GatewayError) as exc_info:
            await client.request_raw("GET", "/api/v1/export")
        assert exc_info.value.status_code == 0
        assert "Cannot connect" in exc_info.value.message

    async def test_generic_http_error_on_request_raw(self, client: HTTPClient) -> None:
        # Arrange
        client._client.request = AsyncMock(
            side_effect=httpx.HTTPError("something broke")
        )

        # Act & Assert
        with pytest.raises(GatewayError) as exc_info:
            await client.request_raw("GET", "/api/v1/export")
        assert exc_info.value.status_code == 0
        assert "Network error" in exc_info.value.message


class TestClose:
    """Tests for HTTPClient.close method."""

    async def test_close_calls_aclose(self, client: HTTPClient) -> None:
        # Arrange
        client._client.aclose = AsyncMock()

        # Act
        await client.close()

        # Assert
        client._client.aclose.assert_awaited_once()
