"""
Tests for foundation infrastructure: exceptions, response envelopes, and pagination.

These tests verify the cross-cutting plumbing that every endpoint depends on
before any domain-specific logic is involved.
"""

import pytest
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient

from app.core.exceptions import (
    AlreadyExistsException,
    AppException,
    ForbiddenException,
    NotFoundException,
    UnauthorizedException,
    ValidationException,
)
from app.core.pagination import MAX_PAGE_LIMIT, PaginationParams
from app.core.responses import ErrorDetail, ErrorResponse, PaginatedResponse, PaginationMeta, SuccessResponse
from app.main import app

# ── Exception Tests ───────────────────────────────────────────────────────────


class TestAppExceptions:
    """Verify each exception subclass carries correct status code and code."""

    def test_not_found_defaults(self) -> None:
        exc = NotFoundException()
        assert exc.status_code == 404
        assert exc.code == "NOT_FOUND"

    def test_not_found_custom_message(self) -> None:
        exc = NotFoundException(message="Patient not found")
        assert exc.message == "Patient not found"

    def test_already_exists(self) -> None:
        exc = AlreadyExistsException()
        assert exc.status_code == 409
        assert exc.code == "ALREADY_EXISTS"

    def test_unauthorized(self) -> None:
        exc = UnauthorizedException()
        assert exc.status_code == 401
        assert exc.code == "UNAUTHORIZED"

    def test_forbidden(self) -> None:
        exc = ForbiddenException()
        assert exc.status_code == 403
        assert exc.code == "FORBIDDEN"

    def test_validation(self) -> None:
        exc = ValidationException()
        assert exc.status_code == 422
        assert exc.code == "VALIDATION_ERROR"

    def test_all_are_app_exception_subclasses(self) -> None:
        subclasses = [
            NotFoundException,
            AlreadyExistsException,
            UnauthorizedException,
            ForbiddenException,
            ValidationException,
        ]
        for cls in subclasses:
            assert issubclass(cls, AppException)


# ── Response Envelope Tests ───────────────────────────────────────────────────


class TestResponseEnvelopes:
    """Verify response shapes serialize correctly."""

    def test_success_response_shape(self) -> None:
        resp = SuccessResponse[dict](data={"id": "abc"})
        assert resp.success is True
        assert resp.data == {"id": "abc"}

    def test_success_response_serialization(self) -> None:
        resp = SuccessResponse[str](data="hello")
        d = resp.model_dump()
        assert d["success"] is True
        assert d["data"] == "hello"

    def test_error_response_shape(self) -> None:
        resp = ErrorResponse(error=ErrorDetail(code="NOT_FOUND", message="Gone"))
        assert resp.success is False
        assert resp.error.code == "NOT_FOUND"
        assert resp.error.message == "Gone"

    def test_paginated_response_shape(self) -> None:
        meta = PaginationMeta(total=100, page=2, limit=10, pages=10)
        resp = PaginatedResponse[str](data=["a", "b"], meta=meta)
        assert resp.success is True
        assert len(resp.data) == 2
        assert resp.meta.pages == 10


# ── Pagination Params Tests ───────────────────────────────────────────────────


class TestPaginationParams:
    """Verify PaginationParams enforces bounds and computes offset correctly."""

    def test_default_values(self) -> None:
        params = PaginationParams(page=1, limit=20)
        assert params.page == 1
        assert params.limit == 20

    def test_offset_first_page(self) -> None:
        params = PaginationParams(page=1, limit=20)
        assert params.offset == 0

    def test_offset_second_page(self) -> None:
        params = PaginationParams(page=2, limit=20)
        assert params.offset == 20

    def test_offset_arbitrary_page(self) -> None:
        params = PaginationParams(page=5, limit=10)
        assert params.offset == 40

    def test_max_limit_constant(self) -> None:
        assert MAX_PAGE_LIMIT == 200


# ── Global Exception Handler Integration Tests ────────────────────────────────


@pytest.mark.anyio
class TestGlobalExceptionHandler:
    """Verify the global handler converts AppExceptions to ErrorResponse JSON."""

    async def test_not_found_returns_404_json(self) -> None:
        test_app = FastAPI()

        @test_app.get("/test")
        async def raise_not_found() -> None:
            raise NotFoundException(message="Test resource missing")

        from app.core.exceptions import AppException
        from app.main import app_exception_handler

        test_app.add_exception_handler(AppException, app_exception_handler)  # type: ignore[arg-type]

        async with AsyncClient(transport=ASGITransport(app=test_app), base_url="http://test") as client:
            response = await client.get("/test")

        assert response.status_code == 404
        body = response.json()
        assert body["success"] is False
        assert body["error"]["code"] == "NOT_FOUND"
        assert "Test resource missing" in body["error"]["message"]

    async def test_forbidden_returns_403_json(self) -> None:
        test_app = FastAPI()

        @test_app.get("/test")
        async def raise_forbidden() -> None:
            raise ForbiddenException()

        from app.core.exceptions import AppException
        from app.main import app_exception_handler

        test_app.add_exception_handler(AppException, app_exception_handler)  # type: ignore[arg-type]

        async with AsyncClient(transport=ASGITransport(app=test_app), base_url="http://test") as client:
            response = await client.get("/test")

        assert response.status_code == 403
        assert response.json()["error"]["code"] == "FORBIDDEN"

    async def test_health_endpoint_still_returns_200(self) -> None:
        async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
            response = await client.get("/health")
        assert response.status_code == 200
        assert response.json()["status"] == "ok"

    async def test_unknown_route_returns_404(self) -> None:
        async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
            response = await client.get("/this-route-does-not-exist")
        assert response.status_code == 404
