# Standard Library
from types import SimpleNamespace
from unittest.mock import Mock, patch

# Django
from django.utils.timezone import now

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

# Alliance Auth (External Libs)
from eve_sde.models import ItemCategory, ItemGroup, ItemType

from incursions.models.waitlist import (
    Fitting, Fleet, FleetActivity, ImplantSet, Waitlist, WaitlistCategory, WaitlistEntry, WaitlistEntryFit,
)
from incursions.tasks import update_fleet


def _fleet_member(character_id: int, ship_type_id: int) -> SimpleNamespace:
    return SimpleNamespace(character_id=character_id, ship_type_id=ship_type_id, role="squad_member")


class TestUpdateFleet(NoSocketsTestCase):
    def setUp(self) -> None:
        self.boss = EveCharacter.objects.create(character_id=1000, character_name="Fleet Boss")
        self.fleet = Fleet.objects.create(boss=self.boss, is_updating=True, open=True, last_updated=now())

        category = ItemCategory.objects.create(id=6, name="Ship", name_en="Ship", published=True)
        group = ItemGroup.objects.create(id=600, name="Cruiser", name_en="Cruiser", category=category, published=True)
        self.ship = ItemType.objects.create(id=601, name="Sample Cruiser", name_en="Sample Cruiser", group=group, published=True)

    def _make_waitlist_fit(self, character: EveCharacter) -> WaitlistEntryFit:
        waitlist_category = WaitlistCategory.objects.get_or_create(name="SPONGE")[0]
        fitting = Fitting.objects.create(ship=self.ship, dna=f"{self.ship.id}::{character.pk}")
        implants = ImplantSet.objects.create(implants=f"[{character.pk}]")
        entry = WaitlistEntry.objects.create(waitlist=Waitlist.get_solo(), main_character=character)
        return WaitlistEntryFit.objects.create(
            character=character, waitlist_entry=entry, fit=fitting, implant_set=implants,
            category=waitlist_category, tags="",
        )

    @patch("incursions.tasks.sse_client.submit")
    @patch("incursions.tasks.Fleet.get_fleet_members")
    def test_no_op_when_not_updating(self, mock_get_members: Mock, mock_submit: Mock) -> None:
        self.fleet.is_updating = False
        self.fleet.save(update_fields=["is_updating"])

        update_fleet(self.fleet.pk)

        mock_get_members.assert_not_called()
        mock_submit.assert_not_called()

    @patch("incursions.tasks.logger.error")
    @patch("incursions.tasks.Fleet.get_fleet_members", side_effect=RuntimeError("esi down"))
    def test_esi_failure_flips_is_updating_off(self, mock_get_members: Mock, mock_error: Mock) -> None:
        update_fleet(self.fleet.pk)

        self.fleet.refresh_from_db()
        self.assertFalse(self.fleet.is_updating)
        mock_error.assert_called_once()

    @patch("incursions.tasks.sse_client.submit")
    @patch("incursions.tasks.INCURSIONS_MINIMUM_FLEET_SIZE", 1)
    @patch("incursions.tasks.Fleet.get_fleet_members")
    def test_waitlist_entries_cleaned_up_for_members_who_fit(self, mock_get_members: Mock, mock_submit: Mock) -> None:
        member_character = EveCharacter.objects.create(character_id=2000, character_name="Fit Pilot")
        fit = self._make_waitlist_fit(member_character)
        mock_get_members.return_value = [_fleet_member(member_character.character_id, self.ship.id)]

        update_fleet(self.fleet.pk)

        self.assertFalse(WaitlistEntryFit.objects.filter(pk=fit.pk).exists())
        self.assertFalse(WaitlistEntry.objects.filter(pk=fit.waitlist_entry_id).exists())

    @patch("incursions.tasks.sse_client.submit")
    @patch("incursions.tasks.Fleet.get_fleet_members")
    def test_below_minimum_fleet_size_skips_comp_update(self, mock_get_members: Mock, mock_submit: Mock) -> None:
        member_character = EveCharacter.objects.create(character_id=2001, character_name="Solo Pilot")
        fit = self._make_waitlist_fit(member_character)
        mock_get_members.return_value = [_fleet_member(member_character.character_id, self.ship.id)]

        # Default INCURSIONS_MINIMUM_FLEET_SIZE (5) is not met by a 1-member fleet.
        update_fleet(self.fleet.pk)

        # The waitlist-cleanup atomic block runs before the size check, so the fit
        # is still removed, but no FleetActivity/SSE happens for the comp update.
        self.assertFalse(WaitlistEntryFit.objects.filter(pk=fit.pk).exists())
        self.assertFalse(FleetActivity.objects.filter(fleet=self.fleet).exists())
        mock_submit.assert_not_called()

    @patch("incursions.tasks.sse_client.submit")
    @patch("incursions.tasks.INCURSIONS_MINIMUM_FLEET_SIZE", 1)
    @patch("incursions.tasks.Fleet.get_fleet_members")
    def test_missing_evecharacter_is_skipped_without_error(self, mock_get_members: Mock, mock_submit: Mock) -> None:
        known_character = EveCharacter.objects.create(character_id=2002, character_name="Known Pilot")
        mock_get_members.return_value = [
            _fleet_member(999999, self.ship.id),  # Not present in the DB.
            _fleet_member(known_character.character_id, self.ship.id),
        ]

        update_fleet(self.fleet.pk)

        self.assertEqual(FleetActivity.objects.filter(fleet=self.fleet).count(), 1)
        self.assertTrue(FleetActivity.objects.filter(fleet=self.fleet, character=known_character).exists())

    @patch("incursions.tasks.sse_client.submit")
    @patch("incursions.tasks.INCURSIONS_MINIMUM_FLEET_SIZE", 1)
    @patch("incursions.tasks.Fleet.get_fleet_members")
    def test_new_fleet_member_triggers_comp_sse(self, mock_get_members: Mock, mock_submit: Mock) -> None:
        member_character = EveCharacter.objects.create(character_id=2003, character_name="New Pilot")
        mock_get_members.return_value = [_fleet_member(member_character.character_id, self.ship.id)]

        update_fleet(self.fleet.pk)

        self.assertTrue(FleetActivity.objects.filter(fleet=self.fleet, character=member_character, has_left=False).exists())
        self.assertTrue(mock_submit.called)
        submitted_events = mock_submit.call_args.args[0]
        self.assertEqual(submitted_events[0].topic, "fleet_comp")

    @patch("incursions.tasks.sse_client.submit")
    @patch("incursions.tasks.INCURSIONS_MINIMUM_FLEET_SIZE", 1)
    @patch("incursions.tasks.Fleet.get_fleet_members")
    def test_departed_member_triggers_comp_sse(self, mock_get_members: Mock, mock_submit: Mock) -> None:
        remaining_character = EveCharacter.objects.create(character_id=2004, character_name="Remaining Pilot")
        departed_character = EveCharacter.objects.create(character_id=2005, character_name="Departed Pilot")
        FleetActivity.objects.create(
            fleet=self.fleet, character=departed_character, ship=self.ship, last_seen=now(), has_left=False,
        )
        mock_get_members.return_value = [_fleet_member(remaining_character.character_id, self.ship.id)]

        update_fleet(self.fleet.pk)

        departed_activity = FleetActivity.objects.get(fleet=self.fleet, character=departed_character)
        self.assertTrue(departed_activity.has_left)
        self.assertTrue(mock_submit.called)

    @patch("incursions.tasks.sse_client.submit")
    @patch("incursions.tasks.INCURSIONS_MINIMUM_FLEET_SIZE", 1)
    @patch("incursions.tasks.Fleet.get_fleet_members")
    def test_no_sse_submitted_when_nothing_changed(self, mock_get_members: Mock, mock_submit: Mock) -> None:
        member_character = EveCharacter.objects.create(character_id=2006, character_name="Steady Pilot")
        FleetActivity.objects.create(
            fleet=self.fleet, character=member_character, ship=self.ship, last_seen=now(), has_left=False,
        )
        mock_get_members.return_value = [_fleet_member(member_character.character_id, self.ship.id)]

        update_fleet(self.fleet.pk)

        mock_submit.assert_not_called()
