# Standard Library
from unittest.mock import Mock, patch

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

# Alliance Auth (External Libs)
from eve_sde.models import Constellation, SolarSystem

from incursions.models.incursion import Focus, Incursion
from incursions.models.waitlist import Waitlist
from incursions.signals import incursion as incursion_signal_handler


class TestIncursionSignal(NoSocketsTestCase):
    def setUp(self) -> None:
        self.faction = EveFactionInfo.objects.create(faction_id=500019, faction_name="Sansha's Nation")
        constellation = Constellation.objects.create(id=20000001, name="Test Constellation")
        self.high_sec_system = SolarSystem.objects.create(
            id=30000142, name="Jita", constellation=constellation, security_status=0.9,
        )
        self.low_sec_system = SolarSystem.objects.create(
            id=30000144, name="Low Sec System", constellation=constellation, security_status=0.3,
        )

    @patch("incursions.signals.incursion_established.delay")
    def test_creation_triggers_incursion_established_task(self, mock_delay: Mock) -> None:
        incursion = Incursion.objects.create(faction=self.faction)

        mock_delay.assert_called_once_with(incursion.pk)

    @patch("incursions.signals.incursion_established.delay")
    @patch("incursions.signals.INCURSIONS_AUTO_HIGHSEC_FOCUS", True)
    def test_high_sec_creation_assigns_focus_when_setting_enabled(self, mock_delay: Mock) -> None:
        incursion = Incursion.objects.create(faction=self.faction, staging_solar_system=self.high_sec_system)

        self.assertEqual(Focus.get_solo().incursion_id, incursion.pk)

    @patch("incursions.signals.incursion_established.delay")
    @patch("incursions.signals.INCURSIONS_AUTO_HIGHSEC_FOCUS", False)
    def test_high_sec_creation_skips_focus_when_setting_disabled(self, mock_delay: Mock) -> None:
        Incursion.objects.create(faction=self.faction, staging_solar_system=self.high_sec_system)

        self.assertIsNone(Focus.get_solo().incursion)

    @patch("incursions.signals.incursion_established.delay")
    @patch("incursions.signals.INCURSIONS_AUTO_HIGHSEC_FOCUS", True)
    def test_low_sec_creation_does_not_assign_focus(self, mock_delay: Mock) -> None:
        Incursion.objects.create(faction=self.faction, staging_solar_system=self.low_sec_system)

        self.assertIsNone(Focus.get_solo().incursion)

    @patch("incursions.signals.incursion_established.delay")
    @patch("incursions.signals.INCURSIONS_AUTO_HIGHSEC_FOCUS", True)
    def test_focus_not_reassigned_when_already_pointing_at_incursion(self, mock_delay: Mock) -> None:
        incursion = Incursion.objects.create(faction=self.faction, staging_solar_system=self.high_sec_system)
        focus = Focus.get_solo()
        focus.incursion = incursion
        focus.save(update_fields=["incursion"])

        # Directly re-invoke the receiver's "created" branch to exercise the
        # already-focused no-op without needing a second real INSERT.
        with patch.object(Focus, "save") as mock_focus_save:
            incursion_signal_handler(sender=Incursion, instance=incursion, created=True)

        mock_focus_save.assert_not_called()

    @patch("incursions.signals.incursion_mobilizing.delay")
    def test_state_transition_to_mobilizing_triggers_task(self, mock_delay: Mock) -> None:
        incursion = Incursion.objects.create(faction=self.faction)

        incursion.state = Incursion.States.MOBILIZING
        incursion.save(update_fields=["state"])

        mock_delay.assert_called_once_with(incursion.pk)

    @patch("incursions.signals.incursion_withdrawing.delay")
    def test_state_transition_to_withdrawing_triggers_task(self, mock_delay: Mock) -> None:
        incursion = Incursion.objects.create(faction=self.faction, state=Incursion.States.MOBILIZING)

        incursion.state = Incursion.States.WITHDRAWING
        incursion.save(update_fields=["state"])

        mock_delay.assert_called_once_with(incursion.pk)

    @patch("incursions.signals.incursion_ended.delay")
    def test_state_transition_to_ended_triggers_task(self, mock_delay: Mock) -> None:
        incursion = Incursion.objects.create(faction=self.faction, state=Incursion.States.WITHDRAWING)

        incursion.state = Incursion.States.ENDED
        incursion.save(update_fields=["state"])

        mock_delay.assert_called_once_with(incursion.pk)

    @patch("incursions.signals.incursion_ended.delay")
    @patch("incursions.signals.incursion_withdrawing.delay")
    @patch("incursions.signals.incursion_mobilizing.delay")
    def test_state_not_in_update_fields_triggers_no_task(
        self, mock_mobilizing: Mock, mock_withdrawing: Mock, mock_ended: Mock,
    ) -> None:
        incursion = Incursion.objects.create(faction=self.faction)

        incursion.state = Incursion.States.MOBILIZING  # In-memory only, not persisted.
        incursion.save(update_fields=["type"])

        mock_mobilizing.assert_not_called()
        mock_withdrawing.assert_not_called()
        mock_ended.assert_not_called()

    @patch("incursions.signals.incursion_boss_spawned.delay")
    def test_has_boss_true_in_update_fields_triggers_boss_spawned_task(self, mock_delay: Mock) -> None:
        incursion = Incursion.objects.create(faction=self.faction, has_boss=False)

        incursion.has_boss = True
        incursion.save(update_fields=["has_boss"])

        mock_delay.assert_called_once_with(incursion.pk)

    @patch("incursions.signals.incursion_boss_spawned.delay")
    def test_has_boss_false_in_update_fields_does_not_trigger_task(self, mock_delay: Mock) -> None:
        incursion = Incursion.objects.create(faction=self.faction, has_boss=True)

        incursion.has_boss = False
        incursion.save(update_fields=["has_boss"])

        mock_delay.assert_not_called()


class TestWaitlistSignal(NoSocketsTestCase):
    @patch("incursions.signals.waitlist_state.delay")
    def test_is_open_in_update_fields_triggers_waitlist_state_task(self, mock_delay: Mock) -> None:
        waitlist = Waitlist.get_solo()

        waitlist.is_open = True
        waitlist.save(update_fields=["is_open"])

        mock_delay.assert_called_once_with(waitlist.pk, True)

    @patch("incursions.signals.waitlist_state.delay")
    def test_is_open_closed_value_is_passed_through(self, mock_delay: Mock) -> None:
        waitlist = Waitlist.get_solo()
        waitlist.is_open = True
        waitlist.save(update_fields=["is_open"])
        mock_delay.reset_mock()

        waitlist.is_open = False
        waitlist.save(update_fields=["is_open"])

        mock_delay.assert_called_once_with(waitlist.pk, False)

    @patch("incursions.signals.waitlist_state.delay")
    def test_is_open_not_in_update_fields_is_noop(self, mock_delay: Mock) -> None:
        waitlist = Waitlist.get_solo()

        waitlist.name = "Renamed Waitlist"
        waitlist.save(update_fields=["name"])

        mock_delay.assert_not_called()
