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

# Django
from django.http import Http404
from django.test import RequestFactory
from django.utils import timezone

# 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.api import fleets as fleets_api
from incursions.models.waitlist import ActiveFleet, Fleet, FleetSquad, WaitlistCategory


class FakeAPI:
    def __init__(self) -> None:
        self.routes: dict[str, object] = {}

    def _register(self, path: str, **kwargs: object):
        def decorator(func):
            self.routes[path] = func
            return func
        return decorator

    def get(self, path: str, **kwargs: object):
        return self._register(path, **kwargs)

    def post(self, path: str, **kwargs: object):
        return self._register(path, **kwargs)

    def patch(self, path: str, **kwargs: object):
        return self._register(path, **kwargs)

    def delete(self, path: str, **kwargs: object):
        return self._register(path, **kwargs)


def _routes() -> dict[str, object]:
    api = FakeAPI()
    fleets_api.setup(api)
    return api.routes


def _user(**perms: bool) -> Mock:
    return Mock(has_perm=Mock(side_effect=lambda perm: perms.get(perm, False)))


def _wing(wing_id: int, squads: list) -> SimpleNamespace:
    return SimpleNamespace(id=wing_id, name=f"Wing {wing_id}", squads=squads)


def _squad(squad_id: int) -> SimpleNamespace:
    return SimpleNamespace(id=squad_id, name=f"Squad {squad_id}")


def _member(character_id: int, ship_type_id: int, role: str, wing_id: int = 1, squad_id: int = 1) -> SimpleNamespace:
    return SimpleNamespace(character_id=character_id, ship_type_id=ship_type_id, role=role, wing_id=wing_id, squad_id=squad_id)


class FleetsAPITestBase(NoSocketsTestCase):
    def setUp(self) -> None:
        self.factory = RequestFactory()
        self.routes = _routes()

        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)

        self.boss = EveCharacter.objects.create(character_id=9000, character_name="Fleet Boss")


class TestFleetStatus(FleetsAPITestBase):
    def setUp(self) -> None:
        super().setUp()
        self.view = self.routes["/fleet/status"]

    def test_denies_without_permission(self) -> None:
        request = self.factory.get("/fleet/status")
        request.user = _user()

        response = self.view(request)

        self.assertEqual(response, (403, {"error": "Permission denied"}))

    def test_returns_none_when_no_active_fleet(self) -> None:
        request = self.factory.get("/fleet/status")
        request.user = _user(**{"incursions.basic_waitlist": True})

        status_code, body = self.view(request)

        self.assertEqual(status_code, 200)
        self.assertIsNone(body.fleets)

    def test_returns_active_fleet(self) -> None:
        fleet = Fleet.objects.create(boss=self.boss, is_updating=True, open=True, last_updated=timezone.now())
        active_fleet = ActiveFleet.get_solo()
        active_fleet.fleet = fleet
        active_fleet.save(update_fields=["fleet"])
        request = self.factory.get("/fleet/status")
        request.user = _user(**{"incursions.basic_waitlist": True})

        status_code, body = self.view(request)

        self.assertEqual(status_code, 200)
        self.assertEqual(body.fleets[0].id, fleet.pk)


class TestFleetInfo(FleetsAPITestBase):
    def setUp(self) -> None:
        super().setUp()
        self.view = self.routes["/fleet/info"]

    def test_denies_without_permission(self) -> None:
        request = self.factory.get("/fleet/info")
        request.user = _user()

        response = self.view(request, character_id=self.boss.character_id)

        self.assertEqual(response, (403, {"error": "Permission denied"}))

    @patch("incursions.api.fleets.get_all_characters_from_user")
    def test_denies_when_character_not_owned_by_user(self, mock_all_chars: Mock) -> None:
        mock_all_chars.return_value = []
        request = self.factory.get("/fleet/info")
        request.user = _user(**{"incursions.waitlist_manage_waitlist": True})

        response = self.view(request, character_id=self.boss.character_id)

        self.assertEqual(response, (403, {"error": "Permission denied"}))

    @patch("incursions.api.fleets.get_characters_character_id_fleet")
    @patch("incursions.api.fleets.get_all_characters_from_user")
    def test_raises_404_when_no_esi_fleet(self, mock_all_chars: Mock, mock_get_fleet: Mock) -> None:
        mock_all_chars.return_value = [self.boss]
        mock_get_fleet.return_value = None
        request = self.factory.get("/fleet/info")
        request.user = _user(**{"incursions.waitlist_manage_waitlist": True})

        with self.assertRaises(Http404):
            self.view(request, character_id=self.boss.character_id)

    @patch("incursions.api.fleets.Fleet.get_fleets_fleet_id_wings")
    @patch("incursions.api.fleets.get_characters_character_id_fleet")
    @patch("incursions.api.fleets.get_all_characters_from_user")
    def test_creates_fleet_and_returns_wings(
        self, mock_all_chars: Mock, mock_get_fleet: Mock, mock_get_wings: Mock,
    ) -> None:
        mock_all_chars.return_value = [self.boss]
        mock_get_fleet.return_value = SimpleNamespace(fleet_id=42)
        mock_get_wings.return_value = [_wing(1, [_squad(10), _squad(11)])]
        request = self.factory.get("/fleet/info")
        request.user = _user(**{"incursions.waitlist_manage_waitlist": True})

        status_code, body = self.view(request, character_id=self.boss.character_id)

        self.assertEqual(status_code, 200)
        self.assertEqual(body.fleet_id, 42)
        self.assertEqual(len(body.wings[0].squads), 2)
        fleet = Fleet.objects.get(pk=42)
        self.assertEqual(fleet.boss, self.boss)
        self.assertTrue(fleet.is_updating)

    @patch("incursions.api.fleets.Fleet.get_fleets_fleet_id_wings")
    @patch("incursions.api.fleets.get_characters_character_id_fleet")
    @patch("incursions.api.fleets.get_all_characters_from_user")
    @patch("incursions.api.fleets.EveCharacter.objects.create_character")
    def test_creates_missing_boss_character(
        self, mock_create_character: Mock, mock_all_chars: Mock, mock_get_fleet: Mock, mock_get_wings: Mock,
    ) -> None:
        # NOTE: the ownership check a few lines above (`EveCharacter.objects.get(...)
        # not in get_all_characters_from_user(...)`) performs the *same* lookup
        # unconditionally first. In real traffic, if that lookup were to raise
        # DoesNotExist it would propagate uncaught (500) before ever reaching this
        # try/except - so this fallback is unreachable outside of a test that,
        # like this one, makes the two identical calls behave differently.
        mock_create_character.return_value = self.boss
        mock_all_chars.return_value = [self.boss]
        mock_get_fleet.return_value = SimpleNamespace(fleet_id=43)
        mock_get_wings.return_value = []

        with patch(
            "incursions.api.fleets.EveCharacter.objects.get",
            side_effect=[self.boss, EveCharacter.DoesNotExist],
        ):
            request = self.factory.get("/fleet/info")
            request.user = _user(**{"incursions.waitlist_manage_waitlist": True})
            self.view(request, character_id=self.boss.character_id)

        mock_create_character.assert_called_once_with(character_id=self.boss.character_id)


class TestFleetComposition(FleetsAPITestBase):
    def setUp(self) -> None:
        super().setUp()
        self.view = self.routes["/fleet/fleetcomp"]
        self.fleet = Fleet.objects.create(boss=self.boss, is_updating=True, open=True, last_updated=timezone.now())
        active_fleet = ActiveFleet.get_solo()
        active_fleet.fleet = self.fleet
        active_fleet.save(update_fields=["fleet"])

    def test_denies_without_permission(self) -> None:
        request = self.factory.get("/fleet/fleetcomp")
        request.user = _user()

        response = self.view(request, character_id=self.boss.character_id)

        self.assertEqual(response, (403, {"error": "Permission denied"}))

    def test_returns_404_when_no_active_fleet(self) -> None:
        active_fleet = ActiveFleet.get_solo()
        active_fleet.fleet = None
        active_fleet.save(update_fields=["fleet"])
        request = self.factory.get("/fleet/fleetcomp")
        request.user = _user(**{"incursions.waitlist_fleet_view": True})

        response = self.view(request, character_id=self.boss.character_id)

        self.assertEqual(response, (404, {"error": "No active fleet found"}))

    @patch("incursions.api.fleets.Fleet.get_fleets_fleet_id_wings")
    @patch("incursions.api.fleets.Fleet.get_fleet_members")
    def test_builds_wing_squad_member_hierarchy(self, mock_get_members: Mock, mock_get_wings: Mock) -> None:
        pilot = EveCharacter.objects.create(character_id=9500, character_name="Squaddie")
        mock_get_wings.return_value = [_wing(1, [_squad(10)])]
        mock_get_members.return_value = [
            _member(self.boss.character_id, self.ship.id, role="fleet_commander", wing_id=1, squad_id=10),
            _member(pilot.character_id, self.ship.id, role="squad_member", wing_id=1, squad_id=10),
        ]
        request = self.factory.get("/fleet/fleetcomp")
        request.user = _user(**{"incursions.waitlist_fleet_view": True})

        status_code, body = self.view(request, character_id=self.boss.character_id)

        self.assertEqual(status_code, 200)
        self.assertEqual(body.boss.character_id, self.boss.character_id)
        self.assertEqual(len(body.members), 2)
        squad = body.wings[0].squads[0]
        self.assertEqual(len(squad.members), 1)
        self.assertEqual(squad.members[0].character_id, pilot.character_id)

    @patch("incursions.api.fleets.EveCharacter.objects.create_character")
    @patch("incursions.api.fleets.Fleet.get_fleets_fleet_id_wings")
    @patch("incursions.api.fleets.Fleet.get_fleet_members")
    def test_returns_503_for_unknown_character(
        self, mock_get_members: Mock, mock_get_wings: Mock, mock_create_character: Mock,
    ) -> None:
        mock_get_wings.return_value = [_wing(1, [_squad(10)])]
        mock_get_members.return_value = [_member(555555, self.ship.id, role="squad_member", wing_id=1, squad_id=10)]
        request = self.factory.get("/fleet/fleetcomp")
        request.user = _user(**{"incursions.waitlist_fleet_view": True})

        response = self.view(request, character_id=self.boss.character_id)

        self.assertEqual(response, (503, {"error": "Unknown Character  found, please try again later."}))
        mock_create_character.assert_called_once_with(character_id=555555)

    @patch("incursions.api.fleets.Fleet.get_fleets_fleet_id_wings")
    @patch("incursions.api.fleets.Fleet.get_fleet_members")
    def test_returns_503_for_unknown_ship(self, mock_get_members: Mock, mock_get_wings: Mock) -> None:
        mock_get_wings.return_value = [_wing(1, [_squad(10)])]
        mock_get_members.return_value = [_member(self.boss.character_id, 999999, role="squad_member", wing_id=1, squad_id=10)]
        request = self.factory.get("/fleet/fleetcomp")
        request.user = _user(**{"incursions.waitlist_fleet_view": True})

        response = self.view(request, character_id=self.boss.character_id)

        self.assertEqual(response, (503, {"error": "Unknown Ship found, please try again later"}))


class TestFleetMembers(FleetsAPITestBase):
    def setUp(self) -> None:
        super().setUp()
        self.view = self.routes["/fleet/members"]
        self.fleet = Fleet.objects.create(boss=self.boss, is_updating=True, open=True, last_updated=timezone.now())
        active_fleet = ActiveFleet.get_solo()
        active_fleet.fleet = self.fleet
        active_fleet.save(update_fields=["fleet"])
        self.category = WaitlistCategory.objects.create(name="SPONGE")

    def test_denies_without_permission(self) -> None:
        request = self.factory.get("/fleet/members")
        request.user = _user()

        response = self.view(request, character_id=self.boss.character_id)

        self.assertEqual(response, (403, {"error": "Permission denied"}))

    def test_returns_404_when_no_active_fleet(self) -> None:
        active_fleet = ActiveFleet.get_solo()
        active_fleet.fleet = None
        active_fleet.save(update_fields=["fleet"])
        request = self.factory.get("/fleet/members")
        request.user = _user(**{"incursions.waitlist_fleet_view": True})

        response = self.view(request, character_id=self.boss.character_id)

        self.assertEqual(response, (404, {"error": "No active fleet found"}))

    @patch("incursions.api.fleets.Fleet.get_fleets_fleet_id_wings")
    @patch("incursions.api.fleets.Fleet.get_fleet_members")
    def test_skips_member_missing_fleet_squad(self, mock_get_members: Mock, mock_get_wings: Mock) -> None:
        # No FleetSquad row exists for squad_id=10 -> should be skipped, not error.
        mock_get_wings.return_value = [_wing(1, [])]
        mock_get_members.return_value = [_member(self.boss.character_id, self.ship.id, role="squad_member", wing_id=1, squad_id=10)]
        request = self.factory.get("/fleet/members")
        request.user = _user(**{"incursions.waitlist_fleet_view": True})

        status_code, body = self.view(request, character_id=self.boss.character_id)

        self.assertEqual(status_code, 200)
        self.assertEqual(body.members, [])

    @patch("incursions.api.fleets.Fleet.get_fleets_fleet_id_wings")
    @patch("incursions.api.fleets.Fleet.get_fleet_members")
    def test_returns_member_with_squad_category(self, mock_get_members: Mock, mock_get_wings: Mock) -> None:
        FleetSquad.objects.create(fleet=self.fleet, category=self.category, wing_id=1, squad_id=10)
        mock_get_wings.return_value = [_wing(1, [_squad(10)])]
        mock_get_members.return_value = [_member(self.boss.character_id, self.ship.id, role="squad_member", wing_id=1, squad_id=10)]
        request = self.factory.get("/fleet/members")
        request.user = _user(**{"incursions.waitlist_fleet_view": True})

        status_code, body = self.view(request, character_id=self.boss.character_id)

        self.assertEqual(status_code, 200)
        self.assertEqual(body.members[0].wl_category, "SPONGE")


class TestRegisterFleet(FleetsAPITestBase):
    def setUp(self) -> None:
        super().setUp()
        self.view = self.routes["/fleet/register"]
        self.category = WaitlistCategory.objects.create(name="SPONGE")

    def _payload(self, **kwargs) -> fleets_api.RegisterRequest:
        defaults = {
            "character_id": self.boss.character_id,
            "fleet_id": 50,
            "assignments": {str(self.category.pk): (1, 2)},
        }
        defaults.update(kwargs)
        return fleets_api.RegisterRequest(**defaults)

    def test_denies_without_permission(self) -> None:
        request = self.factory.post("/fleet/register")
        request.user = _user()

        response = self.view(request, self._payload())

        self.assertEqual(response, (403, {"error": "Permission denied"}))

    @patch("incursions.api.fleets.get_all_characters_from_user")
    def test_denies_when_character_not_owned(self, mock_all_chars: Mock) -> None:
        mock_all_chars.return_value = []
        request = self.factory.post("/fleet/register")
        request.user = _user(**{"incursions.waitlist_manage_waitlist": True})

        response = self.view(request, self._payload())

        self.assertEqual(response, (403, {"error": "Permission denied"}))

    @patch("incursions.api.fleets.get_all_characters_from_user")
    def test_registers_fleet_and_assigns_squads(self, mock_all_chars: Mock) -> None:
        mock_all_chars.return_value = [self.boss]
        request = self.factory.post("/fleet/register")
        request.user = _user(**{"incursions.waitlist_manage_waitlist": True})

        response = self.view(request, self._payload())

        self.assertEqual(response, (200, {"message": "OK"}))
        fleet = Fleet.objects.get(pk=50)
        self.assertEqual(fleet.boss, self.boss)
        self.assertEqual(ActiveFleet.get_solo().fleet, fleet)
        squad = FleetSquad.objects.get(fleet=fleet, category=self.category)
        self.assertEqual((squad.wing_id, squad.squad_id), (1, 2))


class TestCloseFleet(FleetsAPITestBase):
    def setUp(self) -> None:
        super().setUp()
        self.view = self.routes["/fleet/close"]

    def test_denies_without_permission(self) -> None:
        request = self.factory.post("/fleet/close")
        request.user = _user()

        response = self.view(request, boss_character_id=self.boss.character_id)

        self.assertEqual(response, (403, {"error": "Permission denied"}))

    def test_returns_404_when_no_active_fleet(self) -> None:
        request = self.factory.post("/fleet/close")
        request.user = _user(**{"incursions.waitlist_manage_waitlist": True})

        response = self.view(request, boss_character_id=self.boss.character_id)

        self.assertEqual(response, (404, {"error": "No active fleet found"}))

    @patch("incursions.api.fleets.kick_all_fleet_members")
    def test_closes_fleet_and_kicks_members(self, mock_kick: Mock) -> None:
        fleet = Fleet.objects.create(boss=self.boss, is_updating=True, open=True, last_updated=timezone.now())
        active_fleet = ActiveFleet.get_solo()
        active_fleet.fleet = fleet
        active_fleet.save(update_fields=["fleet"])
        mock_kick.return_value = 5
        request = self.factory.post("/fleet/close")
        request.user = _user(**{"incursions.waitlist_manage_waitlist": True})

        response = self.view(request, boss_character_id=self.boss.character_id)

        self.assertEqual(response, (200, "Kicked 5 Fleet Members"))
        fleet.refresh_from_db()
        self.assertFalse(fleet.open)
        self.assertFalse(fleet.is_updating)
        mock_kick.assert_called_once_with(boss_character_id=self.boss.character_id, fleet_id=fleet.pk)
