# Standard Library
from unittest.mock import Mock, patch

# Alliance Auth (Core)
from allianceauth.eveonline.models import EveFactionInfo
from allianceauth.utils.testing import NoSocketsTestCase

from incursions.models.incursion import Incursion, IncursionsConfig, Webhook
from incursions.models.waitlist import Waitlist
from incursions.tasks import (
    incursion_boss_spawned, incursion_ended, incursion_established, incursion_mobilizing, incursion_withdrawing,
    waitlist_state,
)


def _webhook(*, high: bool, low: bool, null: bool) -> Webhook:
    return Webhook.objects.create(
        url="https://discord.example.com/hook", name="hook",
        security_high=high, security_low=low, security_null=null,
    )


class TestIncursionWebhookTasks(NoSocketsTestCase):
    def setUp(self) -> None:
        self.faction = EveFactionInfo.objects.create(faction_id=500019, faction_name="Sansha's Nation")

    def _incursion(self, *, is_high_sec: bool, is_low_sec: bool, is_null_sec: bool) -> Incursion:
        incursion = Incursion.objects.create(faction=self.faction)
        # Patch the security band properties directly rather than building real
        # SolarSystem security_status combinations for every permutation.
        patcher_high = patch.object(Incursion, "is_high_sec", new_callable=lambda: property(lambda self: is_high_sec))
        patcher_low = patch.object(Incursion, "is_low_sec", new_callable=lambda: property(lambda self: is_low_sec))
        patcher_null = patch.object(Incursion, "is_null_sec", new_callable=lambda: property(lambda self: is_null_sec))
        for patcher in (patcher_high, patcher_low, patcher_null):
            patcher.start()
            self.addCleanup(patcher.stop)
        return incursion

    @patch("incursions.tasks.embed_established_addendum")
    @patch("incursions.tasks.embed_established")
    def test_incursion_established_fans_out_to_matching_high_sec_webhooks(
        self, mock_embed: Mock, mock_addendum: Mock,
    ) -> None:
        incursion = self._incursion(is_high_sec=True, is_low_sec=False, is_null_sec=False)
        matching = _webhook(high=True, low=False, null=False)
        IncursionsConfig.get_solo().status_webhooks.add(matching)

        with patch.object(Webhook, "send_embed") as mock_send:
            incursion_established(incursion.pk)

        self.assertEqual(mock_send.call_count, 2)
        mock_embed.assert_called_once_with(incursion)
        mock_addendum.assert_called_once_with(incursion)

    @patch("incursions.tasks.embed_established_addendum")
    @patch("incursions.tasks.embed_established")
    def test_incursion_established_skips_non_matching_webhook(
        self, mock_embed: Mock, mock_addendum: Mock,
    ) -> None:
        # Each condition is a plain `==` between the incursion's security-band
        # bool and the webhook's flag, so e.g. (False == False) still "matches"
        # on a later elif. Every flag must genuinely disagree with the
        # incursion's bands for none of the three branches to fire.
        incursion = self._incursion(is_high_sec=True, is_low_sec=False, is_null_sec=False)
        non_matching = _webhook(high=False, low=True, null=True)
        IncursionsConfig.get_solo().status_webhooks.add(non_matching)

        with patch.object(Webhook, "send_embed") as mock_send:
            incursion_established(incursion.pk)

        mock_send.assert_not_called()

    @patch("incursions.tasks.embed_mobilizing")
    def test_incursion_mobilizing_fans_out_to_matching_low_sec_webhook(self, mock_embed: Mock) -> None:
        incursion = self._incursion(is_high_sec=False, is_low_sec=True, is_null_sec=False)
        matching = _webhook(high=False, low=True, null=False)
        IncursionsConfig.get_solo().status_webhooks.add(matching)

        with patch.object(Webhook, "send_embed") as mock_send:
            incursion_mobilizing(incursion.pk)

        mock_send.assert_called_once_with(embed=mock_embed.return_value)

    @patch("incursions.tasks.embed_withdrawing")
    def test_incursion_withdrawing_fans_out_to_matching_null_sec_webhook(self, mock_embed: Mock) -> None:
        incursion = self._incursion(is_high_sec=False, is_low_sec=False, is_null_sec=True)
        matching = _webhook(high=False, low=False, null=True)
        IncursionsConfig.get_solo().status_webhooks.add(matching)

        with patch.object(Webhook, "send_embed") as mock_send:
            incursion_withdrawing(incursion.pk)

        mock_send.assert_called_once_with(embed=mock_embed.return_value)

    @patch("incursions.tasks.embed_ended")
    def test_incursion_ended_fans_out_to_matching_webhook(self, mock_embed: Mock) -> None:
        incursion = self._incursion(is_high_sec=True, is_low_sec=False, is_null_sec=False)
        matching = _webhook(high=True, low=False, null=False)
        IncursionsConfig.get_solo().status_webhooks.add(matching)

        with patch.object(Webhook, "send_embed") as mock_send:
            incursion_ended(incursion.pk)

        mock_send.assert_called_once_with(embed=mock_embed.return_value)

    @patch("incursions.tasks.embed_boss_spawned")
    def test_incursion_boss_spawned_fans_out_to_matching_webhook(self, mock_embed: Mock) -> None:
        incursion = self._incursion(is_high_sec=True, is_low_sec=False, is_null_sec=False)
        matching = _webhook(high=True, low=False, null=False)
        IncursionsConfig.get_solo().status_webhooks.add(matching)

        with patch.object(Webhook, "send_embed") as mock_send:
            incursion_boss_spawned(incursion.pk)

        mock_send.assert_called_once_with(embed=mock_embed.return_value)

    @patch("incursions.tasks.embed_waitlist_state")
    def test_waitlist_state_fans_out_over_waitlist_webhooks_when_open(self, mock_embed: Mock) -> None:
        waitlist = Waitlist.get_solo()
        webhook_one = _webhook(high=True, low=True, null=True)
        webhook_two = _webhook(high=True, low=True, null=True)
        IncursionsConfig.get_solo().waitlist_webhooks.add(webhook_one, webhook_two)

        with patch.object(Webhook, "send_embed") as mock_send:
            waitlist_state(waitlist.pk, True)

        self.assertEqual(mock_send.call_count, 2)
        # embed built fresh per webhook, not cached/reused across the loop.
        self.assertEqual(mock_embed.call_count, 2)
        mock_embed.assert_called_with(waitlist=waitlist, is_open=True)

    @patch("incursions.tasks.embed_waitlist_state")
    def test_waitlist_state_fans_out_over_waitlist_webhooks_when_closed(self, mock_embed: Mock) -> None:
        waitlist = Waitlist.get_solo()
        webhook_one = _webhook(high=True, low=True, null=True)
        IncursionsConfig.get_solo().waitlist_webhooks.add(webhook_one)

        with patch.object(Webhook, "send_embed") as mock_send:
            waitlist_state(waitlist.pk, False)

        mock_send.assert_called_once_with(embed=mock_embed.return_value)
        mock_embed.assert_called_once_with(waitlist=waitlist, is_open=False)
