"""Layer 1 — Transport contracts: error mapping, params, auth, and 204 handling."""

from __future__ import annotations

import httpx
import pytest
import respx

from kanoniv.client.client import KanonivAsyncClient, KanonivClient
from kanoniv.exceptions import (
    AuthenticationError,
    ConflictError,
    ForbiddenError,
    KanonivError,
    NotFoundError,
    RateLimitError,
    ServerError,
    ValidationError,
)

from .conftest import API_KEY, BASE_URL


# ---------------------------------------------------------------------------
# Error mapping: every status code → correct exception class
# ---------------------------------------------------------------------------

class TestErrorMapping:
    """Every HTTP error status maps to the correct SDK exception."""

    @pytest.mark.parametrize(
        "status_code,exc_class",
        [
            (400, ValidationError),
            (401, AuthenticationError),
            (403, ForbiddenError),
            (404, NotFoundError),
            (409, ConflictError),
            (429, RateLimitError),
            (500, ServerError),
            (502, ServerError),
            (503, ServerError),
        ],
    )
    def test_status_maps_to_exception(
        self, mock_api: respx.Router, client: KanonivClient,
        status_code: int, exc_class: type,
    ):
        mock_api.get("/v1/stats").mock(
            return_value=httpx.Response(status_code, json={"error": "boom"}),
        )
        with pytest.raises(exc_class):
            client.stats()

    def test_unknown_4xx_raises_base(self, mock_api: respx.Router, client: KanonivClient):
        mock_api.get("/v1/stats").mock(
            return_value=httpx.Response(418, json={"error": "teapot"}),
        )
        with pytest.raises(KanonivError) as exc_info:
            client.stats()
        assert exc_info.value.status_code == 418


# ---------------------------------------------------------------------------
# Exception attributes
# ---------------------------------------------------------------------------

class TestExceptionAttributes:
    """Exceptions carry status_code, body, and message."""

    def test_status_code_preserved(self, mock_api: respx.Router, client: KanonivClient):
        mock_api.get("/v1/stats").mock(
            return_value=httpx.Response(404, json={"error": "not found"}),
        )
        with pytest.raises(NotFoundError) as exc_info:
            client.stats()
        assert exc_info.value.status_code == 404

    def test_json_body_preserved(self, mock_api: respx.Router, client: KanonivClient):
        body = {"error": "bad input", "details": ["field required"]}
        mock_api.get("/v1/stats").mock(
            return_value=httpx.Response(400, json=body),
        )
        with pytest.raises(ValidationError) as exc_info:
            client.stats()
        assert exc_info.value.body == body

    def test_text_body_preserved(self, mock_api: respx.Router, client: KanonivClient):
        mock_api.get("/v1/stats").mock(
            return_value=httpx.Response(500, text="Internal Server Error"),
        )
        with pytest.raises(ServerError) as exc_info:
            client.stats()
        assert exc_info.value.body == "Internal Server Error"

    def test_message_from_error_key(self, mock_api: respx.Router, client: KanonivClient):
        mock_api.get("/v1/stats").mock(
            return_value=httpx.Response(400, json={"error": "email is required"}),
        )
        with pytest.raises(ValidationError) as exc_info:
            client.stats()
        assert "email is required" in str(exc_info.value)


# ---------------------------------------------------------------------------
# Parameter cleaning
# ---------------------------------------------------------------------------

class TestCleanParams:
    """None params are stripped; present params preserved."""

    def test_none_params_stripped(self, mock_api: respx.Router, client: KanonivClient):
        route = mock_api.get("/v1/entities").mock(
            return_value=httpx.Response(200, json={"data": []}),
        )
        client.entities.search(q="alice", entity_type=None)
        request = route.calls.last.request
        assert "q=alice" in str(request.url)
        assert "entity_type" not in str(request.url)

    def test_present_params_preserved(self, mock_api: respx.Router, client: KanonivClient):
        route = mock_api.get("/v1/entities").mock(
            return_value=httpx.Response(200, json={"data": []}),
        )
        client.entities.search(q="bob", entity_type="customer", limit=10)
        url = str(route.calls.last.request.url)
        assert "q=bob" in url
        assert "entity_type=customer" in url
        assert "limit=10" in url


# ---------------------------------------------------------------------------
# 204 No Content → None
# ---------------------------------------------------------------------------

class TestStatusCode204:
    """204 No Content returns None."""

    def test_204_returns_none(self, mock_api: respx.Router, client: KanonivClient):
        mock_api.delete("/v1/sources/src-1").mock(
            return_value=httpx.Response(204),
        )
        result = client.sources.delete("src-1")
        assert result is None


# ---------------------------------------------------------------------------
# Auth headers
# ---------------------------------------------------------------------------

class TestAuthHeaders:
    """Authentication headers are sent correctly."""

    def test_api_key_header(self, mock_api: respx.Router):
        route = mock_api.get("/v1/stats").mock(
            return_value=httpx.Response(200, json={"total": 0}),
        )
        c = KanonivClient(api_key="kn_secret", base_url=BASE_URL, max_retries=0)
        try:
            c.stats()
            assert route.calls.last.request.headers["X-API-Key"] == "kn_secret"
        finally:
            c.close()

    def test_api_key_precedence_over_access_token(self, mock_api: respx.Router):
        route = mock_api.get("/v1/stats").mock(
            return_value=httpx.Response(200, json={"total": 0}),
        )
        c = KanonivClient(
            api_key="kn_key", access_token="tok_bearer",
            base_url=BASE_URL, max_retries=0,
        )
        try:
            c.stats()
            headers = route.calls.last.request.headers
            assert headers["X-API-Key"] == "kn_key"
            assert "Authorization" not in headers
        finally:
            c.close()
