"""
Tests de la Story 2.17 : Gestion d'Erreurs Graceful (Zero HTTP 500)
Valide tous les Acceptance Criteria de la gestion centralisée des erreurs.
"""
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from fastapi.responses import JSONResponse
from starlette.requests import Request

from middleware.error_handler import ErrorHandlingMiddleware, format_error_response


# ---------------------------------------------------------------------------
# Helpers / fixtures locales
# ---------------------------------------------------------------------------

def _make_app_with_middleware(*routes) -> FastAPI:
    """Crée une mini-app FastAPI avec ErrorHandlingMiddleware et les routes fournies."""
    app = FastAPI()
    app.add_middleware(ErrorHandlingMiddleware)
    for route in routes:
        app.add_api_route(route["path"], route["endpoint"], methods=route.get("methods", ["GET"]))
    return app


# ---------------------------------------------------------------------------
# AC1 / AC2 : Gestionnaire global + Format JSON structuré
# ---------------------------------------------------------------------------

class TestGlobalExceptionHandler:
    """AC1 : Toutes les exceptions non capturées → gestionnaire global."""

    def test_unhandled_exception_returns_500_internal_error(self):
        """AC4 : Exception inattendue → INTERNAL_ERROR 500, stack trace masquée."""
        async def _crash(request: Request):
            result = 1 / 0  # ZeroDivisionError délibéré

        app = _make_app_with_middleware({"path": "/crash", "endpoint": _crash})
        client = TestClient(app, raise_server_exceptions=False)
        response = client.get("/crash")

        assert response.status_code == 500
        body = response.json()
        # AC2 : format structuré obligatoire
        assert "error" in body
        assert "message" in body
        assert "details" in body
        # AC3 : code d'erreur standard
        assert body["error"] == "INTERNAL_ERROR"
        # AC5 : message en français
        assert "inattendue" in body["message"].lower() or "produite" in body["message"].lower()
        # AC6 : zéro stack trace dans la réponse
        assert "Traceback" not in response.text
        assert "ZeroDivisionError" not in response.text

    def test_unhandled_exception_includes_request_id(self):
        """AC2 : details contient request_id."""
        async def _crash(request: Request):
            raise RuntimeError("boom")

        app = _make_app_with_middleware({"path": "/crash", "endpoint": _crash})
        client = TestClient(app, raise_server_exceptions=False)
        response = client.get("/crash")

        body = response.json()
        assert "details" in body
        assert "request_id" in body["details"]


# ---------------------------------------------------------------------------
# AC2 : Format JSON structuré {error, message, details}
# ---------------------------------------------------------------------------

class TestStructuredErrorFormat:
    """AC2 : Toutes les erreurs retournent {error, message, details}."""

    def test_format_error_response_always_includes_details(self):
        """format_error_response inclut toujours details même si vide."""
        response = format_error_response(
            status_code=400,
            message="Erreur de test",
            error_code="INVALID_FORMAT",
            request_id="test-123",
        )
        body = response.body
        import json
        data = json.loads(body)
        assert "error" in data
        assert "message" in data
        assert "details" in data
        assert data["details"]["request_id"] == "test-123"

    def test_format_error_response_no_extra_fields(self):
        """La réponse ne contient pas de champs supplémentaires non spécifiés."""
        response = format_error_response(
            status_code=500,
            message="Erreur interne",
            error_code="INTERNAL_ERROR",
            request_id="abc",
        )
        import json
        data = json.loads(response.body)
        allowed_keys = {"error", "message", "details"}
        assert set(data.keys()) == allowed_keys


# ---------------------------------------------------------------------------
# AC3 : Codes d'erreur standards
# ---------------------------------------------------------------------------

class TestStandardErrorCodes:
    """AC3 : Seuls les codes architecturaux sont utilisés."""

    ALLOWED_CODES = {
        "INVALID_FORMAT", "QUOTA_EXCEEDED", "UNAUTHORIZED",
        "FORBIDDEN", "FILE_TOO_LARGE", "PROVIDER_ERROR", "INTERNAL_ERROR",
        # Codes étendus acceptables
        "NOT_FOUND", "METHOD_NOT_ALLOWED", "SERVICE_UNAVAILABLE",
        "VALIDATION_ERROR",
    }

    def test_invalid_format_code_used_for_400(self):
        from middleware.error_handler import _map_http_status_to_code
        assert _map_http_status_to_code(400) == "INVALID_FORMAT"

    def test_quota_exceeded_code_used_for_429(self):
        from middleware.error_handler import _map_http_status_to_code
        assert _map_http_status_to_code(429) == "QUOTA_EXCEEDED"

    def test_unauthorized_code_used_for_401(self):
        from middleware.error_handler import _map_http_status_to_code
        assert _map_http_status_to_code(401) == "UNAUTHORIZED"

    def test_forbidden_code_used_for_403(self):
        from middleware.error_handler import _map_http_status_to_code
        assert _map_http_status_to_code(403) == "FORBIDDEN"

    def test_file_too_large_code_used_for_413(self):
        from middleware.error_handler import _map_http_status_to_code
        assert _map_http_status_to_code(413) == "FILE_TOO_LARGE"

    def test_provider_error_code_used_for_502(self):
        from middleware.error_handler import _map_http_status_to_code
        assert _map_http_status_to_code(502) == "PROVIDER_ERROR"

    def test_internal_error_code_used_for_500(self):
        from middleware.error_handler import _map_http_status_to_code
        assert _map_http_status_to_code(500) == "INTERNAL_ERROR"


# ---------------------------------------------------------------------------
# AC4 : Masquage des détails techniques
# ---------------------------------------------------------------------------

class TestTechnicalDetailsMasking:
    """AC4 : Stack traces et messages internes jamais exposés au client."""

    def test_attribute_error_not_leaked(self):
        """AttributeError interne → INTERNAL_ERROR, message générique."""
        async def _crash(request: Request):
            obj = None
            obj.does_not_exist  # AttributeError

        app = _make_app_with_middleware({"path": "/crash", "endpoint": _crash})
        client = TestClient(app, raise_server_exceptions=False)
        response = client.get("/crash")

        assert response.status_code == 500
        assert "AttributeError" not in response.text
        assert "does_not_exist" not in response.text
        assert response.json()["error"] == "INTERNAL_ERROR"

    def test_key_error_not_leaked(self):
        """KeyError interne → INTERNAL_ERROR, clé non exposée."""
        async def _crash(request: Request):
            d = {}
            return d["secret_key"]

        app = _make_app_with_middleware({"path": "/crash", "endpoint": _crash})
        client = TestClient(app, raise_server_exceptions=False)
        response = client.get("/crash")

        assert "secret_key" not in response.text
        assert "KeyError" not in response.text

    def test_http_404_format(self):
        """AC4 : HTTPException 404 retourne format structuré, pas de stack trace."""
        from fastapi import HTTPException

        async def _not_found(request: Request):
            raise HTTPException(status_code=404, detail="File not found")

        app = FastAPI()
        from starlette.exceptions import HTTPException as StarletteHTTPException
        from middleware.error_handler import format_error_response

        @app.exception_handler(StarletteHTTPException)
        async def http_exc(request, exc):
            return format_error_response(
                status_code=exc.status_code,
                message=str(exc.detail) if hasattr(exc, "detail") else "Ressource introuvable.",
                request_id=getattr(request.state, "request_id", "unknown"),
            )

        app.add_api_route("/not-found", _not_found, methods=["GET"])
        client = TestClient(app, raise_server_exceptions=False)
        response = client.get("/not-found")

        assert response.status_code == 404
        body = response.json()
        assert "error" in body
        assert "message" in body
        assert "details" in body
        assert "Traceback" not in response.text


# ---------------------------------------------------------------------------
# AC5 : Messages en français
# ---------------------------------------------------------------------------

class TestFrenchMessages:
    """AC5 : Messages d'erreur destinés à l'utilisateur en français."""

    def test_internal_error_message_is_french(self):
        """Le message générique pour INTERNAL_ERROR est en français."""
        async def _crash(request: Request):
            raise Exception("crash")

        app = _make_app_with_middleware({"path": "/crash", "endpoint": _crash})
        client = TestClient(app, raise_server_exceptions=False)
        response = client.get("/crash")

        message = response.json().get("message", "")
        # Doit contenir du texte français, pas "An unexpected error"
        assert any(
            word in message.lower()
            for word in ["erreur", "inattendue", "produite", "réessayez", "veuillez"]
        ), f"Message attendu en français, obtenu : {message!r}"

    def test_validation_error_message_is_french(self):
        """format_error_response avec message français est transmis tel quel."""
        response = format_error_response(
            status_code=400,
            message="Erreur de validation des données transmises.",
            error_code="INVALID_FORMAT",
            request_id="x",
        )
        import json
        body = json.loads(response.body)
        assert "Erreur" in body["message"]


# ---------------------------------------------------------------------------
# AC6 : Zéro stack trace dans les réponses API
# ---------------------------------------------------------------------------

class TestZeroStackTrace:
    """AC6 : Aucune trace d'exécution exposée dans les réponses API."""

    def test_no_traceback_in_500_response(self):
        """500 response ne contient pas de Traceback."""
        async def _crash(request: Request):
            raise ValueError("deep internal error with secrets")

        app = _make_app_with_middleware({"path": "/crash", "endpoint": _crash})
        client = TestClient(app, raise_server_exceptions=False)
        response = client.get("/crash")

        assert "Traceback" not in response.text
        assert "ValueError" not in response.text
        assert "deep internal error with secrets" not in response.text

    def test_no_file_path_in_500_response(self):
        """Les chemins de fichiers internes ne sont pas exposés."""
        async def _crash(request: Request):
            open("/this/path/does/not/exist.txt")

        app = _make_app_with_middleware({"path": "/crash", "endpoint": _crash})
        client = TestClient(app, raise_server_exceptions=False)
        response = client.get("/crash")

        assert "/this/path/does/not/exist.txt" not in response.text
        assert "FileNotFoundError" not in response.text
