from unittest.mock import patch

import pytest
from django.apps import apps
from django.conf import settings

from health_check.contrib.celery_ping.apps import HealthCheckConfig
from health_check.contrib.celery_ping.backends import CeleryPingHealthCheck


class TestCeleryPingHealthCheck:
    CELERY_APP_CONTROL_PING = "health_check.contrib.celery_ping.backends.app.control.ping"
    CELERY_APP_CONTROL_INSPECT_ACTIVE_QUEUES = (
        "health_check.contrib.celery_ping.backends.app.control.inspect.active_queues"
    )

    @pytest.fixture
    def health_check(self):
        return CeleryPingHealthCheck()

    def test_check_status_doesnt_add_errors_when_ping_successful(self, health_check):
        celery_worker = "celery@4cc150a7b49b"

        with (
            patch(
                self.CELERY_APP_CONTROL_PING,
                return_value=[
                    {celery_worker: CeleryPingHealthCheck.CORRECT_PING_RESPONSE},
                    {f"{celery_worker}-2": CeleryPingHealthCheck.CORRECT_PING_RESPONSE},
                ],
            ),
            patch(
                self.CELERY_APP_CONTROL_INSPECT_ACTIVE_QUEUES,
                return_value={celery_worker: [{"name": queue.name} for queue in settings.CELERY_QUEUES]},
            ),
        ):
            health_check.check_status()

            assert not health_check.errors

    def test_check_status_reports_errors_if_ping_responses_are_incorrect(self, health_check):
        with patch(
            self.CELERY_APP_CONTROL_PING,
            return_value=[
                {"celery1@4cc150a7b49b": CeleryPingHealthCheck.CORRECT_PING_RESPONSE},
                {"celery2@4cc150a7b49b": {}},
                {"celery3@4cc150a7b49b": {"error": "pong"}},
            ],
        ):
            health_check.check_status()

            assert len(health_check.errors) == 2

    def test_check_status_adds_errors_when_ping_successfull_but_not_all_defined_queues_have_consumers(
        self,
        health_check,
    ):
        celery_worker = "celery@4cc150a7b49b"
        queues = list(settings.CELERY_QUEUES)

        with (
            patch(
                self.CELERY_APP_CONTROL_PING,
                return_value=[{celery_worker: CeleryPingHealthCheck.CORRECT_PING_RESPONSE}],
            ),
            patch(
                self.CELERY_APP_CONTROL_INSPECT_ACTIVE_QUEUES,
                return_value={celery_worker: [{"name": queues.pop().name}]},
            ),
        ):
            health_check.check_status()

            assert len(health_check.errors) == len(queues)

    @pytest.mark.parametrize(
        "exception_to_raise",
        [
            IOError,
            TimeoutError,
        ],
    )
    def test_check_status_add_error_when_io_error_raised_from_ping(self, exception_to_raise, health_check):
        with patch(self.CELERY_APP_CONTROL_PING, side_effect=exception_to_raise):
            health_check.check_status()

            assert len(health_check.errors) == 1
            assert "ioerror" in health_check.errors[0].message.lower()

    @pytest.mark.parametrize("exception_to_raise", [ValueError, SystemError, IndexError, MemoryError])
    def test_check_status_add_error_when_any_exception_raised_from_ping(self, exception_to_raise, health_check):
        with patch(self.CELERY_APP_CONTROL_PING, side_effect=exception_to_raise):
            health_check.check_status()

            assert len(health_check.errors) == 1
            assert health_check.errors[0].message.lower() == "unknown error"

    def test_check_status_when_raised_exception_notimplementederror(self, health_check):
        expected_error_message = "notimplementederror: make sure celery_result_backend is set"

        with patch(self.CELERY_APP_CONTROL_PING, side_effect=NotImplementedError):
            health_check.check_status()

            assert len(health_check.errors) == 1
            assert health_check.errors[0].message.lower() == expected_error_message

    @pytest.mark.parametrize("ping_result", [None, list()])
    def test_check_status_add_error_when_ping_result_failed(self, ping_result, health_check):
        with patch(self.CELERY_APP_CONTROL_PING, return_value=ping_result):
            health_check.check_status()

            assert len(health_check.errors) == 1
            assert "workers unavailable" in health_check.errors[0].message.lower()


class TestCeleryPingHealthCheckApps:
    def test_apps(self):
        assert HealthCheckConfig.name == "health_check.contrib.celery_ping"

        celery_ping = apps.get_app_config("celery_ping")
        assert celery_ping.name == "health_check.contrib.celery_ping"
