"""Tests unitaires pour RabbitMQConsumer."""

from __future__ import annotations

import json
from unittest.mock import MagicMock, patch

import pytest
from pika.exceptions import AMQPConnectionError

from cezam_lib.cezam_shared.rabbitmq import RabbitMQConsumer
from cezam_lib.cezam_shared.exceptions import RabbitMQError, RetryableError, NonRetryableError


class TestRabbitMQConsumerInit:
    """Tests d'initialisation du consumer."""

    @patch("cezam_lib.cezam_shared.rabbitmq.pika.BlockingConnection")
    def test_init_success(self, mock_connection):
        """Le consumer se connecte correctement."""
        mock_channel = MagicMock()
        mock_connection.return_value.channel.return_value = mock_channel

        consumer = RabbitMQConsumer(
            host="localhost",
            port=5672,
            user="guest",
            password="guest",
        )

        assert consumer._connection is not None
        assert consumer._channel is not None
        mock_connection.assert_called_once()

    @patch("cezam_lib.cezam_shared.rabbitmq.pika.BlockingConnection")
    def test_init_connection_error(self, mock_connection):
        """Une erreur de connexion lève RabbitMQError."""
        mock_connection.side_effect = AMQPConnectionError("Connection refused")

        with pytest.raises(RabbitMQError) as exc_info:
            RabbitMQConsumer(
                host="localhost",
                port=5672,
                user="guest",
                password="guest",
            )

        assert "Échec de connexion à RabbitMQ" in str(exc_info.value)

    @patch("cezam_lib.cezam_shared.rabbitmq.pika.BlockingConnection")
    def test_init_with_vhost(self, mock_connection):
        """Le consumer accepte un vhost personnalisé."""
        mock_channel = MagicMock()
        mock_connection.return_value.channel.return_value = mock_channel

        consumer = RabbitMQConsumer(
            host="localhost",
            port=5672,
            user="guest",
            password="guest",
            vhost="/custom",
        )

        assert consumer._vhost == "/custom"


class TestRabbitMQConsumerConsume:
    """Tests de consommation de messages."""

    @patch("cezam_lib.cezam_shared.rabbitmq.pika.BlockingConnection")
    def test_consume_sets_prefetch_count(self, mock_connection):
        """consume() configure le prefetch_count."""
        mock_channel = MagicMock()
        mock_connection.return_value.channel.return_value = mock_channel
        mock_connection.return_value.is_closed = False
        mock_channel.is_closed = False

        consumer = RabbitMQConsumer(
            host="localhost",
            port=5672,
            user="guest",
            password="guest",
        )

        consumer._should_stop = True

        callback = MagicMock()
        consumer.consume(queue="test-queue", callback=callback, prefetch_count=5)

        mock_channel.basic_qos.assert_called_once_with(prefetch_count=5)

    @patch("cezam_lib.cezam_shared.rabbitmq.pika.BlockingConnection")
    def test_consume_registers_callback(self, mock_connection):
        """consume() enregistre le callback sur la queue."""
        mock_channel = MagicMock()
        mock_connection.return_value.channel.return_value = mock_channel
        mock_connection.return_value.is_closed = False
        mock_channel.is_closed = False
        mock_channel.basic_consume.return_value = "consumer-tag-123"

        consumer = RabbitMQConsumer(
            host="localhost",
            port=5672,
            user="guest",
            password="guest",
        )

        consumer._should_stop = True

        callback = MagicMock()
        consumer.consume(queue="test-queue", callback=callback)

        mock_channel.basic_consume.assert_called_once()
        call_kwargs = mock_channel.basic_consume.call_args.kwargs
        assert call_kwargs["queue"] == "test-queue"
        assert call_kwargs["auto_ack"] is False


class TestRabbitMQConsumerAckNack:
    """Tests de gestion ack/nack selon les exceptions."""

    def _create_consumer_and_trigger_message(
        self, mock_connection, callback, message_body, headers=None
    ):
        """Helper pour créer un consumer et simuler un message."""
        mock_channel = MagicMock()
        mock_connection.return_value.channel.return_value = mock_channel
        mock_connection.return_value.is_closed = False
        mock_channel.is_closed = False

        captured_on_message = None

        def capture_callback(**kwargs):
            nonlocal captured_on_message
            captured_on_message = kwargs["on_message_callback"]
            return "consumer-tag"

        mock_channel.basic_consume.side_effect = capture_callback

        consumer = RabbitMQConsumer(
            host="localhost",
            port=5672,
            user="guest",
            password="guest",
        )

        consumer._should_stop = True
        consumer.consume(queue="test-queue", callback=callback)

        mock_method = MagicMock()
        mock_method.delivery_tag = 42

        mock_properties = MagicMock()
        mock_properties.headers = headers

        body = json.dumps(message_body).encode("utf-8")

        consumer._should_stop = False
        captured_on_message(mock_channel, mock_method, mock_properties, body)

        return mock_channel

    @patch("cezam_lib.cezam_shared.rabbitmq.pika.BlockingConnection")
    def test_successful_callback_sends_ack(self, mock_connection):
        """Un callback réussi envoie un ack."""
        callback = MagicMock()

        mock_channel = self._create_consumer_and_trigger_message(
            mock_connection, callback, {"test": "data"}
        )

        callback.assert_called_once_with({"test": "data"})
        mock_channel.basic_ack.assert_called_once_with(delivery_tag=42)
        mock_channel.basic_nack.assert_not_called()

    @patch("cezam_lib.cezam_shared.rabbitmq.pika.BlockingConnection")
    def test_retryable_error_sends_nack_with_requeue(self, mock_connection):
        """Une RetryableError envoie un nack avec requeue=True."""
        callback = MagicMock(side_effect=RetryableError("Temporary failure"))

        mock_channel = self._create_consumer_and_trigger_message(
            mock_connection, callback, {"test": "data"}
        )

        mock_channel.basic_ack.assert_not_called()
        mock_channel.basic_nack.assert_called_once_with(delivery_tag=42, requeue=True)

    @patch("cezam_lib.cezam_shared.rabbitmq.pika.BlockingConnection")
    def test_non_retryable_error_sends_nack_without_requeue(self, mock_connection):
        """Une NonRetryableError envoie un nack avec requeue=False."""
        callback = MagicMock(side_effect=NonRetryableError("Permanent failure"))

        mock_channel = self._create_consumer_and_trigger_message(
            mock_connection, callback, {"test": "data"}
        )

        mock_channel.basic_ack.assert_not_called()
        mock_channel.basic_nack.assert_called_once_with(delivery_tag=42, requeue=False)

    @patch("cezam_lib.cezam_shared.rabbitmq.pika.BlockingConnection")
    def test_generic_exception_sends_nack_without_requeue(self, mock_connection):
        """Une exception générique envoie un nack avec requeue=False."""
        callback = MagicMock(side_effect=ValueError("Unexpected error"))

        mock_channel = self._create_consumer_and_trigger_message(
            mock_connection, callback, {"test": "data"}
        )

        mock_channel.basic_ack.assert_not_called()
        mock_channel.basic_nack.assert_called_once_with(delivery_tag=42, requeue=False)


class TestRabbitMQConsumerOTelExtraction:
    """Tests d'extraction du contexte OTel."""

    @patch("cezam_lib.cezam_shared.rabbitmq.otel_context")
    @patch("cezam_lib.cezam_shared.rabbitmq.extract")
    @patch("cezam_lib.cezam_shared.rabbitmq.pika.BlockingConnection")
    def test_extracts_otel_context_from_headers(
        self, mock_connection, mock_extract, mock_otel_context
    ):
        """Le contexte OTel est extrait des headers."""
        mock_channel = MagicMock()
        mock_connection.return_value.channel.return_value = mock_channel
        mock_connection.return_value.is_closed = False
        mock_channel.is_closed = False

        mock_ctx = MagicMock()
        mock_extract.return_value = mock_ctx
        mock_token = MagicMock()
        mock_otel_context.attach.return_value = mock_token

        captured_on_message = None

        def capture_callback(**kwargs):
            nonlocal captured_on_message
            captured_on_message = kwargs["on_message_callback"]
            return "consumer-tag"

        mock_channel.basic_consume.side_effect = capture_callback

        consumer = RabbitMQConsumer(
            host="localhost",
            port=5672,
            user="guest",
            password="guest",
        )

        callback = MagicMock()
        consumer._should_stop = True
        consumer.consume(queue="test-queue", callback=callback)

        consumer._should_stop = False

        mock_method = MagicMock()
        mock_method.delivery_tag = 1

        mock_properties = MagicMock()
        mock_properties.headers = {"traceparent": "00-trace-span-01"}

        body = json.dumps({"test": "data"}).encode("utf-8")
        captured_on_message(mock_channel, mock_method, mock_properties, body)

        mock_extract.assert_called_once_with({"traceparent": "00-trace-span-01"})
        mock_otel_context.attach.assert_called_once_with(mock_ctx)
        mock_otel_context.detach.assert_called_once_with(mock_token)

    @patch("cezam_lib.cezam_shared.rabbitmq.otel_context")
    @patch("cezam_lib.cezam_shared.rabbitmq.extract")
    @patch("cezam_lib.cezam_shared.rabbitmq.pika.BlockingConnection")
    def test_handles_missing_headers(
        self, mock_connection, mock_extract, mock_otel_context
    ):
        """Les headers manquants sont gérés correctement."""
        mock_channel = MagicMock()
        mock_connection.return_value.channel.return_value = mock_channel
        mock_connection.return_value.is_closed = False
        mock_channel.is_closed = False

        mock_ctx = MagicMock()
        mock_extract.return_value = mock_ctx
        mock_token = MagicMock()
        mock_otel_context.attach.return_value = mock_token

        captured_on_message = None

        def capture_callback(**kwargs):
            nonlocal captured_on_message
            captured_on_message = kwargs["on_message_callback"]
            return "consumer-tag"

        mock_channel.basic_consume.side_effect = capture_callback

        consumer = RabbitMQConsumer(
            host="localhost",
            port=5672,
            user="guest",
            password="guest",
        )

        callback = MagicMock()
        consumer._should_stop = True
        consumer.consume(queue="test-queue", callback=callback)

        consumer._should_stop = False

        mock_method = MagicMock()
        mock_method.delivery_tag = 1

        mock_properties = MagicMock()
        mock_properties.headers = None

        body = json.dumps({"test": "data"}).encode("utf-8")
        captured_on_message(mock_channel, mock_method, mock_properties, body)

        mock_extract.assert_called_once_with({})
        callback.assert_called_once()


class TestRabbitMQConsumerStop:
    """Tests d'arrêt du consumer."""

    @patch("cezam_lib.cezam_shared.rabbitmq.pika.BlockingConnection")
    def test_stop_cancels_consumer(self, mock_connection):
        """stop() annule le consumer."""
        mock_channel = MagicMock()
        mock_conn_instance = MagicMock()
        mock_conn_instance.channel.return_value = mock_channel
        mock_conn_instance.is_closed = False
        mock_channel.is_closed = False
        mock_connection.return_value = mock_conn_instance

        consumer = RabbitMQConsumer(
            host="localhost",
            port=5672,
            user="guest",
            password="guest",
        )

        consumer._consumer_tag = "test-tag"
        consumer.stop()

        mock_channel.basic_cancel.assert_called_once_with("test-tag")
        mock_conn_instance.close.assert_called_once()
        assert consumer._connection is None
        assert consumer._channel is None
        assert consumer._consumer_tag is None

    @patch("cezam_lib.cezam_shared.rabbitmq.pika.BlockingConnection")
    def test_context_manager_calls_stop(self, mock_connection):
        """Le context manager appelle stop()."""
        mock_channel = MagicMock()
        mock_conn_instance = MagicMock()
        mock_conn_instance.channel.return_value = mock_channel
        mock_conn_instance.is_closed = False
        mock_channel.is_closed = False
        mock_connection.return_value = mock_conn_instance

        with RabbitMQConsumer(
            host="localhost",
            port=5672,
            user="guest",
            password="guest",
        ) as consumer:
            assert consumer._connection is not None

        mock_conn_instance.close.assert_called_once()
