import pytest
from unittest.mock import Mock, patch
from celery.result import AsyncResult

from lts_celery_utils.utils import reliable_send_task


class TestReliableSendTask:
    """Tests for reliable_send_task function."""

    def test_successful_send_on_first_attempt(self):
        """Test that task is sent successfully on first attempt."""
        mock_app = Mock()
        mock_result = Mock(spec=AsyncResult)
        mock_app.send_task.return_value = mock_result

        result = reliable_send_task(
            task_name="my.task",
            args=(1, 2),
            kwargs={"key": "value"},
            queue="default",
            app=mock_app,
        )

        assert result == mock_result
        mock_app.send_task.assert_called_once_with(
            "my.task",
            args=(1, 2),
            kwargs={"key": "value"},
            queue="default",
            delivery_mode=2,
        )

    def test_retries_on_failure_then_succeeds(self):
        """Test that task is retried and eventually succeeds."""
        mock_app = Mock()
        mock_result = Mock(spec=AsyncResult)

        # Fail twice, succeed on third attempt
        mock_app.send_task.side_effect = [
            Exception("Connection error"),
            Exception("Connection error"),
            mock_result,
        ]

        with patch("lts_celery_utils.utils.time.sleep") as mock_sleep:
            result = reliable_send_task(
                task_name="my.task",
                max_retries=3,
                app=mock_app,
            )

        assert result == mock_result
        assert mock_app.send_task.call_count == 3
        # Verify exponential backoff: sleep(1) then sleep(2)
        mock_sleep.assert_any_call(1.0)
        mock_sleep.assert_any_call(2.0)

    def test_raises_exception_after_max_retries(self):
        """Test that exception is raised after max retries are exhausted."""
        mock_app = Mock()
        mock_app.send_task.side_effect = Exception("Connection error")

        with patch("lts_celery_utils.utils.time.sleep"):
            with pytest.raises(Exception, match="Connection error"):
                reliable_send_task(
                    task_name="my.task",
                    max_retries=3,
                    app=mock_app,
                )

        assert mock_app.send_task.call_count == 3

    def test_max_retries_validation(self):
        """Test that ValueError is raised for invalid max_retries."""
        mock_app = Mock()

        with pytest.raises(ValueError, match="max_retries must be >= 1"):
            reliable_send_task(
                task_name="my.task",
                max_retries=0,
                app=mock_app,
            )

        with pytest.raises(ValueError, match="max_retries must be >= 1"):
            reliable_send_task(
                task_name="my.task",
                max_retries=-1,
                app=mock_app,
            )

    def test_args_and_kwargs_conversion(self):
        """Test that args and kwargs are properly converted."""
        mock_app = Mock()
        mock_result = Mock(spec=AsyncResult)
        mock_app.send_task.return_value = mock_result

        # Test with list args and dict kwargs
        reliable_send_task(
            task_name="my.task",
            args=[1, 2, 3],
            kwargs={"a": 1, "b": 2},
            app=mock_app,
        )

        mock_app.send_task.assert_called_once()
        call_args = mock_app.send_task.call_args
        assert call_args[1]["args"] == (1, 2, 3)
        assert call_args[1]["kwargs"] == {"a": 1, "b": 2}

    def test_none_args_and_kwargs(self):
        """Test that None args and kwargs are handled correctly."""
        mock_app = Mock()
        mock_result = Mock(spec=AsyncResult)
        mock_app.send_task.return_value = mock_result

        reliable_send_task(
            task_name="my.task",
            args=None,
            kwargs=None,
            app=mock_app,
        )

        mock_app.send_task.assert_called_once()
        call_args = mock_app.send_task.call_args
        assert call_args[1]["args"] == ()
        assert call_args[1]["kwargs"] == {}

    def test_custom_delivery_mode(self):
        """Test that custom delivery_mode is passed correctly."""
        mock_app = Mock()
        mock_result = Mock(spec=AsyncResult)
        mock_app.send_task.return_value = mock_result

        reliable_send_task(
            task_name="my.task",
            delivery_mode=1,
            app=mock_app,
        )

        mock_app.send_task.assert_called_once()
        call_args = mock_app.send_task.call_args
        assert call_args[1]["delivery_mode"] == 1

    def test_custom_retry_backoff_base(self):
        """Test exponential backoff with custom base."""
        mock_app = Mock()
        mock_app.send_task.side_effect = [
            Exception("Error"),
            Exception("Error"),
            Mock(spec=AsyncResult),
        ]

        with patch("lts_celery_utils.utils.time.sleep") as mock_sleep:
            reliable_send_task(
                task_name="my.task",
                max_retries=3,
                retry_backoff_base=3.0,
                app=mock_app,
            )

        # First retry: 3^0 = 1, second retry: 3^1 = 3
        mock_sleep.assert_any_call(1.0)
        mock_sleep.assert_any_call(3.0)

    def test_zero_retry_backoff_base_no_sleep(self):
        """Test that sleep is skipped when retry_backoff_base is 0."""
        mock_app = Mock()
        mock_app.send_task.side_effect = [
            Exception("Error"),
            Exception("Error"),
            Mock(spec=AsyncResult),
        ]

        with patch("lts_celery_utils.utils.time.sleep") as mock_sleep:
            reliable_send_task(
                task_name="my.task",
                max_retries=3,
                retry_backoff_base=0,
                app=mock_app,
            )

        mock_sleep.assert_not_called()

    def test_uses_current_app_by_default(self):
        """Test that current_app is used when app is not provided."""
        with patch("lts_celery_utils.utils.current_app") as mock_current_app:
            mock_result = Mock(spec=AsyncResult)
            mock_current_app.send_task.return_value = mock_result

            result = reliable_send_task(
                task_name="my.task",
                args=(1,),
            )

            assert result == mock_result
            mock_current_app.send_task.assert_called_once()

    def test_queue_parameter_none(self):
        """Test that queue=None is passed correctly."""
        mock_app = Mock()
        mock_result = Mock(spec=AsyncResult)
        mock_app.send_task.return_value = mock_result

        reliable_send_task(
            task_name="my.task",
            queue=None,
            app=mock_app,
        )

        mock_app.send_task.assert_called_once()
        call_args = mock_app.send_task.call_args
        assert call_args[1]["queue"] is None

    def test_single_retry_failure(self):
        """Test with max_retries=1 (no retries, just one attempt)."""
        mock_app = Mock()
        mock_app.send_task.side_effect = Exception("Connection error")

        with pytest.raises(Exception, match="Connection error"):
            reliable_send_task(
                task_name="my.task",
                max_retries=1,
                app=mock_app,
            )

        assert mock_app.send_task.call_count == 1

    def test_multiple_different_exceptions(self):
        """Test that different exception types are handled."""
        mock_app = Mock()
        mock_app.send_task.side_effect = RuntimeError("First error")

        with patch("lts_celery_utils.utils.time.sleep"):
            with pytest.raises(RuntimeError, match="First error"):
                reliable_send_task(
                    task_name="my.task",
                    max_retries=2,
                    app=mock_app,
                )

    def test_returns_async_result(self):
        """Test that the return value is an AsyncResult."""
        mock_app = Mock()
        mock_result = Mock(spec=AsyncResult)
        mock_app.send_task.return_value = mock_result

        result = reliable_send_task(
            task_name="my.task",
            app=mock_app,
        )

        # Verify it's the mock we set up
        # (in real usage, it would be an AsyncResult)
        assert result == mock_result
