"""Tests property-based pour MinIOClient."""

import io
from typing import Any
from unittest.mock import MagicMock, patch

import pytest
from hypothesis import given, settings, assume
from hypothesis import strategies as st
from minio.error import S3Error
from pydantic import BaseModel

from cezam_lib.cezam_shared import MinIOClient, MinIOError


json_primitives = st.one_of(
    st.none(),
    st.booleans(),
    st.integers(min_value=-(2**53), max_value=2**53),
    st.floats(allow_nan=False, allow_infinity=False),
    st.text(max_size=100),
)


def json_values() -> st.SearchStrategy[Any]:
    """Génère des valeurs JSON valides (récursif)."""
    return st.recursive(
        json_primitives,
        lambda children: st.one_of(
            st.lists(children, max_size=10),
            st.dictionaries(st.text(min_size=1, max_size=20), children, max_size=10),
        ),
        max_leaves=50,
    )


json_objects = st.dictionaries(
    st.text(min_size=1, max_size=20),
    json_values(),
    min_size=1,
    max_size=10,
)


class DynamicModel(BaseModel):
    """Modèle Pydantic dynamique pour les tests."""

    name: str
    value: int
    tags: list[str] = []
    metadata: dict[str, Any] = {}


pydantic_models = st.builds(
    DynamicModel,
    name=st.text(min_size=1, max_size=50),
    value=st.integers(min_value=-1000000, max_value=1000000),
    tags=st.lists(st.text(min_size=1, max_size=20), max_size=5),
    metadata=st.dictionaries(
        st.text(min_size=1, max_size=20),
        st.one_of(st.text(max_size=50), st.integers(), st.booleans()),
        max_size=5,
    ),
)


class TestJsonRoundTrip:
    """Tests property-based pour le round-trip JSON."""

    @given(data=json_objects)
    @settings(max_examples=100)
    def test_dict_roundtrip_preserves_data(self, data: dict[str, Any]) -> None:
        """Écrire puis lire un dict doit retourner un objet équivalent."""
        captured_bytes: bytes | None = None

        def capture_put_object(
            bucket_name: str,
            object_name: str,
            data: io.BytesIO,
            length: int,
            content_type: str,
        ) -> None:
            nonlocal captured_bytes
            captured_bytes = data.read()

        def mock_get_object(bucket_name: str, object_name: str) -> MagicMock:
            response = MagicMock()
            response.read.return_value = captured_bytes
            return response

        with patch("cezam_lib.cezam_shared.minio_client.Minio") as mock_minio:
            mock_client = MagicMock()
            mock_minio.return_value = mock_client
            mock_client.put_object.side_effect = capture_put_object
            mock_client.get_object.side_effect = mock_get_object

            client = MinIOClient(
                endpoint="localhost:9000",
                access_key="access",
                secret_key="secret",
                bucket="test-bucket",
            )

            path = "test/data.json"
            client.put_json(path, data)
            result = client.get_json(path)

            assert result == data

    @given(model=pydantic_models)
    @settings(max_examples=100)
    def test_pydantic_model_roundtrip_preserves_data(
        self, model: DynamicModel
    ) -> None:
        """Écrire puis lire un modèle Pydantic doit retourner un objet équivalent."""
        captured_bytes: bytes | None = None

        def capture_put_object(
            bucket_name: str,
            object_name: str,
            data: io.BytesIO,
            length: int,
            content_type: str,
        ) -> None:
            nonlocal captured_bytes
            captured_bytes = data.read()

        def mock_get_object(bucket_name: str, object_name: str) -> MagicMock:
            response = MagicMock()
            response.read.return_value = captured_bytes
            return response

        with patch("cezam_lib.cezam_shared.minio_client.Minio") as mock_minio:
            mock_client = MagicMock()
            mock_minio.return_value = mock_client
            mock_client.put_object.side_effect = capture_put_object
            mock_client.get_object.side_effect = mock_get_object

            client = MinIOClient(
                endpoint="localhost:9000",
                access_key="access",
                secret_key="secret",
                bucket="test-bucket",
            )

            path = "test/model.json"
            client.put_json(path, model)
            result = client.get_json(path)

            expected = model.model_dump()
            assert result == expected

    @given(data=json_objects)
    @settings(max_examples=50)
    def test_roundtrip_preserves_unicode(self, data: dict[str, Any]) -> None:
        """Le round-trip doit préserver les caractères Unicode."""
        captured_bytes: bytes | None = None

        def capture_put_object(
            bucket_name: str,
            object_name: str,
            data: io.BytesIO,
            length: int,
            content_type: str,
        ) -> None:
            nonlocal captured_bytes
            captured_bytes = data.read()

        def mock_get_object(bucket_name: str, object_name: str) -> MagicMock:
            response = MagicMock()
            response.read.return_value = captured_bytes
            return response

        with patch("cezam_lib.cezam_shared.minio_client.Minio") as mock_minio:
            mock_client = MagicMock()
            mock_minio.return_value = mock_client
            mock_client.put_object.side_effect = capture_put_object
            mock_client.get_object.side_effect = mock_get_object

            client = MinIOClient(
                endpoint="localhost:9000",
                access_key="access",
                secret_key="secret",
                bucket="test-bucket",
            )

            path = "test/unicode.json"
            client.put_json(path, data)
            result = client.get_json(path)

            assert result == data


class TestErrorHandlingConsistency:
    """Tests property-based pour la gestion des erreurs."""

    s3_error_codes = st.sampled_from([
        "AccessDenied",
        "InternalError",
        "ServiceUnavailable",
        "SlowDown",
        "RequestTimeout",
        "BucketNotEmpty",
        "InvalidBucketName",
    ])

    paths = st.text(min_size=1, max_size=100).filter(lambda x: x.strip() != "")

    @given(error_code=s3_error_codes, path=paths)
    @settings(max_examples=50)
    def test_put_json_s3_error_raises_minio_error(
        self, error_code: str, path: str
    ) -> None:
        """Toute erreur S3 lors de put_json doit lever MinIOError."""
        with patch("cezam_lib.cezam_shared.minio_client.Minio") as mock_minio:
            mock_client = MagicMock()
            mock_minio.return_value = mock_client
            mock_client.put_object.side_effect = S3Error(
                code=error_code,
                message=f"Error: {error_code}",
                resource=f"/bucket/{path}",
                request_id="test-request-id",
                host_id="test-host-id",
                response=MagicMock(),
            )

            client = MinIOClient(
                endpoint="localhost:9000",
                access_key="access",
                secret_key="secret",
                bucket="test-bucket",
            )

            with pytest.raises(MinIOError) as exc_info:
                client.put_json(path, {"key": "value"})

            assert path in str(exc_info.value)

    @given(error_code=s3_error_codes, path=paths)
    @settings(max_examples=50)
    def test_get_json_s3_error_raises_minio_error(
        self, error_code: str, path: str
    ) -> None:
        """Toute erreur S3 lors de get_json doit lever MinIOError."""
        with patch("cezam_lib.cezam_shared.minio_client.Minio") as mock_minio:
            mock_client = MagicMock()
            mock_minio.return_value = mock_client
            mock_client.get_object.side_effect = S3Error(
                code=error_code,
                message=f"Error: {error_code}",
                resource=f"/bucket/{path}",
                request_id="test-request-id",
                host_id="test-host-id",
                response=MagicMock(),
            )

            client = MinIOClient(
                endpoint="localhost:9000",
                access_key="access",
                secret_key="secret",
                bucket="test-bucket",
            )

            with pytest.raises(MinIOError) as exc_info:
                client.get_json(path)

            assert path in str(exc_info.value)

    @given(path=paths)
    @settings(max_examples=50)
    def test_get_json_not_found_raises_minio_error_with_path(self, path: str) -> None:
        """Un fichier inexistant doit lever MinIOError avec le chemin."""
        with patch("cezam_lib.cezam_shared.minio_client.Minio") as mock_minio:
            mock_client = MagicMock()
            mock_minio.return_value = mock_client
            mock_client.get_object.side_effect = S3Error(
                code="NoSuchKey",
                message="The specified key does not exist",
                resource=f"/bucket/{path}",
                request_id="test-request-id",
                host_id="test-host-id",
                response=MagicMock(),
            )

            client = MinIOClient(
                endpoint="localhost:9000",
                access_key="access",
                secret_key="secret",
                bucket="test-bucket",
            )

            with pytest.raises(MinIOError) as exc_info:
                client.get_json(path)

            assert path in str(exc_info.value)
            assert "inexistant" in str(exc_info.value).lower()

    @given(error_code=s3_error_codes, path=paths)
    @settings(max_examples=50)
    def test_exists_s3_error_raises_minio_error(
        self, error_code: str, path: str
    ) -> None:
        """Toute erreur S3 (sauf NoSuchKey) lors de exists doit lever MinIOError."""
        with patch("cezam_lib.cezam_shared.minio_client.Minio") as mock_minio:
            mock_client = MagicMock()
            mock_minio.return_value = mock_client
            mock_client.stat_object.side_effect = S3Error(
                code=error_code,
                message=f"Error: {error_code}",
                resource=f"/bucket/{path}",
                request_id="test-request-id",
                host_id="test-host-id",
                response=MagicMock(),
            )

            client = MinIOClient(
                endpoint="localhost:9000",
                access_key="access",
                secret_key="secret",
                bucket="test-bucket",
            )

            with pytest.raises(MinIOError) as exc_info:
                client.exists(path)

            assert path in str(exc_info.value)

    @given(error_code=s3_error_codes, prefix=paths)
    @settings(max_examples=50)
    def test_list_prefix_s3_error_raises_minio_error(
        self, error_code: str, prefix: str
    ) -> None:
        """Toute erreur S3 lors de list_prefix doit lever MinIOError."""
        with patch("cezam_lib.cezam_shared.minio_client.Minio") as mock_minio:
            mock_client = MagicMock()
            mock_minio.return_value = mock_client
            mock_client.list_objects.side_effect = S3Error(
                code=error_code,
                message=f"Error: {error_code}",
                resource=f"/bucket/{prefix}",
                request_id="test-request-id",
                host_id="test-host-id",
                response=MagicMock(),
            )

            client = MinIOClient(
                endpoint="localhost:9000",
                access_key="access",
                secret_key="secret",
                bucket="test-bucket",
            )

            with pytest.raises(MinIOError) as exc_info:
                client.list_prefix(prefix)

            assert prefix in str(exc_info.value)

    generic_exceptions = st.sampled_from([
        ConnectionError("Connection refused"),
        TimeoutError("Operation timed out"),
        OSError("Network unreachable"),
        RuntimeError("Unexpected error"),
    ])

    @given(exception=generic_exceptions, path=paths)
    @settings(max_examples=30)
    def test_generic_exception_wrapped_in_minio_error(
        self, exception: Exception, path: str
    ) -> None:
        """Toute exception générique doit être encapsulée dans MinIOError."""
        with patch("cezam_lib.cezam_shared.minio_client.Minio") as mock_minio:
            mock_client = MagicMock()
            mock_minio.return_value = mock_client
            mock_client.put_object.side_effect = exception

            client = MinIOClient(
                endpoint="localhost:9000",
                access_key="access",
                secret_key="secret",
                bucket="test-bucket",
            )

            with pytest.raises(MinIOError) as exc_info:
                client.put_json(path, {"key": "value"})

            assert path in str(exc_info.value)

    @given(path=paths)
    @settings(max_examples=30)
    def test_connection_error_raises_minio_error_on_init(self, path: str) -> None:
        """Une erreur de connexion à l'initialisation doit lever MinIOError."""
        assume(len(path) > 0)

        with patch("cezam_lib.cezam_shared.minio_client.Minio") as mock_minio:
            mock_minio.side_effect = ConnectionError("Connection refused")

            with pytest.raises(MinIOError) as exc_info:
                MinIOClient(
                    endpoint="localhost:9000",
                    access_key="access",
                    secret_key="secret",
                    bucket="test-bucket",
                )

            assert "connexion" in str(exc_info.value).lower()
