"""Tests for the DalyClient request handling and error mapping."""

import httpx
import pytest

from dalysdk.client import DalyClient
from dalysdk.exceptions import (
    AuthenticationError,
    ConflictError,
    DalyAPIError,
    ForbiddenError,
    NotFoundError,
    RateLimitError,
    ServerError,
    TimeoutError,
    ValidationError,
)

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


AUTH_RESPONSE = {
    "access_token": "test-jwt-token",
    "token_type": "bearer",
    "expires_in": 86400,
}


def _mock_transport(responses: dict[str, httpx.Response]) -> httpx.MockTransport:
    """Create a mock transport that returns canned responses by path."""

    def handler(request: httpx.Request) -> httpx.Response:
        path = request.url.path

        # Auth endpoint always succeeds
        if path == "/auth/token":
            return httpx.Response(200, json=AUTH_RESPONSE)

        key = f"{request.method} {path}"
        if key in responses:
            return responses[key]

        return httpx.Response(404, json={"detail": f"No mock for {key}"})

    return httpx.MockTransport(handler)


def _make_client(responses: dict[str, httpx.Response]) -> DalyClient:
    """Build a DalyClient with mocked HTTP transport."""
    client = DalyClient(
        workspace_api_key="wk_test",
        user_api_key="uk_test",
    )
    client._base_url = "https://mock.dalyenergy.com"
    client._auth._base_url = "https://mock.dalyenergy.com"
    # Replace httpx client with mock transport
    client._http = httpx.Client(
        base_url="https://mock.dalyenergy.com",
        transport=_mock_transport(responses),
    )
    # Pre-set auth token to avoid real HTTP call in AuthManager
    import time

    client._auth._token = "test-jwt-token"
    client._auth._token_expiry = time.time() + 86400
    return client


# ---------------------------------------------------------------------------
# Request routing
# ---------------------------------------------------------------------------


class TestClientRequest:
    def test_get_returns_json(self):
        client = _make_client(
            {
                "GET /locations": httpx.Response(200, json=[{"id": 1, "name": "Site A"}]),
            }
        )
        result = client._request("GET", "/locations")
        assert result == [{"id": 1, "name": "Site A"}]
        client.close()

    def test_202_returns_json(self):
        client = _make_client(
            {
                "POST /energymodels": httpx.Response(
                    202,
                    json={"taskDetails": {"taskId": 9}, "energyModelId": 42},
                ),
            }
        )
        result = client._request("POST", "/energymodels", json={"blocks": []})
        assert result["taskDetails"]["taskId"] == 9
        assert result["energyModelId"] == 42
        client.close()

    def test_post_sends_json_body(self):
        client = _make_client(
            {
                "POST /locations": httpx.Response(200, json={"id": 2, "name": "Site B"}),
            }
        )
        result = client._request("POST", "/locations", json={"name": "Site B"})
        assert result["name"] == "Site B"
        client.close()

    def test_delete_returns_confirmation(self):
        client = _make_client(
            {
                "DELETE /locations/1": httpx.Response(200, json={"deleted": True}),
            }
        )
        result = client._request("DELETE", "/locations/1")
        assert result["deleted"] is True
        client.close()

    def test_204_returns_empty_dict(self):
        client = _make_client(
            {
                "DELETE /locations/1": httpx.Response(204),
            }
        )
        result = client._request("DELETE", "/locations/1")
        assert result == {}
        client.close()


# ---------------------------------------------------------------------------
# Error handling
# ---------------------------------------------------------------------------


class TestErrorHandling:
    def test_401_raises_authentication_error(self):
        client = _make_client(
            {
                "GET /fail": httpx.Response(401, json={"detail": "Bad token"}),
            }
        )

        # Patch httpx.post so the auth refresh after invalidation doesn't do a real call
        import dalysdk.auth as auth_module

        original_post = httpx.post

        def mock_post(url, **kwargs):
            return httpx.Response(
                200,
                json={
                    "access_token": "refreshed-token",
                    "expires_in": 86400,
                },
            )

        auth_module.httpx.post = mock_post
        try:
            # After the first 401, the client invalidates + retries.
            # The mock always returns 401 for /fail, so we still get AuthenticationError.
            with pytest.raises(AuthenticationError):
                client._request("GET", "/fail")
        finally:
            auth_module.httpx.post = original_post
            client.close()

    def test_403_raises_forbidden_error(self):
        client = _make_client(
            {
                "GET /forbidden": httpx.Response(403, json={"detail": "No access"}),
            }
        )
        with pytest.raises(ForbiddenError):
            client._request("GET", "/forbidden")
        client.close()

    def test_404_raises_not_found_error(self):
        client = _make_client(
            {
                "GET /locations/999": httpx.Response(404, json={"detail": "Not found"}),
            }
        )
        with pytest.raises(NotFoundError):
            client._request("GET", "/locations/999")
        client.close()

    def test_422_raises_validation_error(self):
        client = _make_client(
            {
                "POST /locations": httpx.Response(422, json={"detail": [{"msg": "field required"}]}),
            }
        )
        with pytest.raises(ValidationError):
            client._request("POST", "/locations", json={})
        client.close()

    def test_500_raises_server_error(self):
        client = _make_client(
            {
                "GET /crash": httpx.Response(500, json={"detail": "Internal error"}),
            }
        )
        with pytest.raises(ServerError):
            client._request("GET", "/crash")
        client.close()

    def test_409_raises_conflict_error(self):
        client = _make_client(
            {
                "PUT /locations/1": httpx.Response(409, json={"detail": "Duplicate name"}),
            }
        )
        with pytest.raises(ConflictError) as exc_info:
            client._request("PUT", "/locations/1", json={"name": "dup"})
        assert exc_info.value.status_code == 409
        assert exc_info.value.detail == "Duplicate name"
        client.close()

    def test_429_raises_rate_limit_error(self):
        client = _make_client(
            {
                "GET /locations": httpx.Response(
                    429,
                    json={"detail": "Too many requests"},
                    headers={"Retry-After": "30"},
                ),
            }
        )
        with pytest.raises(RateLimitError) as exc_info:
            client._request("GET", "/locations")
        assert exc_info.value.status_code == 429
        assert exc_info.value.retry_after == 30.0
        client.close()

    def test_429_without_retry_after_header(self):
        client = _make_client(
            {
                "GET /locations": httpx.Response(429, json={"detail": "Slow down"}),
            }
        )
        with pytest.raises(RateLimitError) as exc_info:
            client._request("GET", "/locations")
        assert exc_info.value.retry_after is None
        client.close()

    def test_timeout_raises_timeout_error(self):
        """Verify that httpx timeout exceptions are wrapped in TimeoutError."""

        def _timeout_transport(request: httpx.Request) -> httpx.Response:
            if request.url.path == "/auth/token":
                return httpx.Response(200, json=AUTH_RESPONSE)
            raise httpx.ReadTimeout("Connection timed out")

        client = DalyClient(
            workspace_api_key="wk_test",
            user_api_key="uk_test",
        )
        client._base_url = "https://mock.dalyenergy.com"
        client._auth._base_url = "https://mock.dalyenergy.com"
        client._http = httpx.Client(
            base_url="https://mock.dalyenergy.com",
            transport=httpx.MockTransport(_timeout_transport),
        )
        import time

        client._auth._token = "test-jwt-token"
        client._auth._token_expiry = time.time() + 86400

        with pytest.raises(TimeoutError) as exc_info:
            client._request("GET", "/slow")
        assert "timed out" in exc_info.value.detail.lower()
        assert exc_info.value.status_code == 0
        client.close()

    def test_timeout_error_is_daly_api_error(self):
        """TimeoutError should be catchable as DalyAPIError."""

        def _timeout_transport(request: httpx.Request) -> httpx.Response:
            if request.url.path == "/auth/token":
                return httpx.Response(200, json=AUTH_RESPONSE)
            raise httpx.ConnectTimeout("Connection timed out")

        client = DalyClient(
            workspace_api_key="wk_test",
            user_api_key="uk_test",
        )
        client._base_url = "https://mock.dalyenergy.com"
        client._auth._base_url = "https://mock.dalyenergy.com"
        client._http = httpx.Client(
            base_url="https://mock.dalyenergy.com",
            transport=httpx.MockTransport(_timeout_transport),
        )
        import time

        client._auth._token = "test-jwt-token"
        client._auth._token_expiry = time.time() + 86400

        with pytest.raises(DalyAPIError):
            client._request("GET", "/slow")
        client.close()

    def test_unexpected_status_raises_base_error(self):
        client = _make_client(
            {
                "GET /teapot": httpx.Response(418, json={"detail": "I'm a teapot"}),
            }
        )
        with pytest.raises(DalyAPIError):
            client._request("GET", "/teapot")
        client.close()


# ---------------------------------------------------------------------------
# Context manager
# ---------------------------------------------------------------------------


class TestContextManager:
    def test_context_manager_closes_client(self):
        client = _make_client({})
        with client:
            pass
        # After exiting, the HTTP client should be closed
        assert client._http.is_closed
