# Standard Library
from datetime import datetime, timezone
from unittest.mock import Mock, patch

# Django
from django.test import RequestFactory

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

from incursions.api import bans as bans_api
from incursions.api.schema import AllianceSchema, CharacterSchema, CorporationSchema
from incursions.models.waitlist import Ban


class FakeAPI:
    """Keys routes by (method, path): bans.py registers PATCH and DELETE (and
    GET/POST) on some identical paths, so a path-only key would silently let
    the second registration overwrite the first.
    """

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

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

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

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

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

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


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


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


class BansAPITestBase(NoSocketsTestCase):
    def setUp(self) -> None:
        self.factory = RequestFactory()
        self.routes = _routes()
        self.issuer = EveCharacter.objects.create(character_id=8000, character_name="Issuer")


class TestListBans(BansAPITestBase):
    def setUp(self) -> None:
        super().setUp()
        self.view = self.routes[("GET", "/bans")]
        self.character = EveCharacter.objects.create(character_id=8100, character_name="Banned Pilot")

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

        response = self.view(request)

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

    def test_returns_only_unrevoked_bans(self) -> None:
        active_ban = Ban.objects.create(
            entity_type=Ban.entity_choices.CHARACTER, entity_character=self.character,
            reason="internal", issued_by=self.issuer,
        )
        Ban.objects.create(
            entity_type=Ban.entity_choices.CHARACTER, entity_character=self.character,
            reason="old", issued_by=self.issuer, revoked_at=datetime.now(timezone.utc), revoked_by=self.issuer,
        )
        request = self.factory.get("/bans")
        request.user = _user(**{"incursions.waitlist_bans_view": True})

        status_code, bans = self.view(request)

        self.assertEqual(status_code, 200)
        self.assertEqual([b.pk for b in bans], [active_ban.pk])


class TestCreateBan(BansAPITestBase):
    def setUp(self) -> None:
        super().setUp()
        self.view = self.routes[("POST", "/bans")]
        # NOTE: create_ban assigns entity_character_id = payload.entity_character.character_id
        # (the EVE-side ID) directly as the Django FK id, instead of using the
        # resolved EveCharacter row's actual pk. That's only correct by
        # coincidence when a row's local pk matches its EVE ID, so these rows
        # are deliberately created with an explicit matching `id` to exercise
        # the endpoint without tripping the FK constraint. In a real database
        # (rows synced from ESI over time, local pk assigned by autoincrement)
        # this endpoint would raise an IntegrityError / reference the wrong
        # row for essentially every ban creation - a real bug worth flagging
        # alongside the previously-known api/statistics.py one.
        self.character = EveCharacter.objects.create(id=8200, character_id=8200, character_name="Target Pilot")
        self.corporation = EveCorporationInfo.objects.create(
            id=8300, corporation_id=8300, corporation_name="Target Corp", corporation_ticker="TGT",
        )
        self.alliance = EveAllianceInfo.objects.create(
            id=8400, alliance_id=8400, alliance_name="Target Alliance", alliance_ticker="TGTA",
        )

    def test_denies_without_permission(self) -> None:
        request = self.factory.post("/bans")
        request.user = _user()
        payload = bans_api.UpdateBanSchema(
            id=1, entity_type=Ban.entity_choices.CHARACTER, entity_character=None,
            entity_corporation=None, entity_alliance=None, reason="x", public_reason=None, revoked_at=None,
        )

        response = self.view(request, payload)

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

    @patch("incursions.api.bans.get_main_character_from_user")
    def test_creates_character_ban(self, mock_main: Mock) -> None:
        mock_main.return_value = self.issuer
        request = self.factory.post("/bans")
        request.user = _user(**{"incursions.waitlist_bans_manage": True})
        payload = bans_api.UpdateBanSchema(
            id=1, entity_type=Ban.entity_choices.CHARACTER,
            entity_character=CharacterSchema(character_id=self.character.character_id, character_name=self.character.character_name),
            entity_corporation=None, entity_alliance=None,
            reason="bad actor", public_reason="banned", revoked_at=None,
        )

        response = self.view(request, payload)

        self.assertEqual(response, (200, {"status": "Ban created"}))
        ban = Ban.objects.get(pk=1)
        self.assertEqual(ban.entity_character, self.character)
        self.assertIsNone(ban.entity_corporation)
        self.assertEqual(ban.issued_by, self.issuer)
        self.assertIsNone(ban.revoked_by)

    @patch("incursions.api.bans.get_main_character_from_user")
    def test_creates_corporation_ban(self, mock_main: Mock) -> None:
        mock_main.return_value = self.issuer
        request = self.factory.post("/bans")
        request.user = _user(**{"incursions.waitlist_bans_manage": True})
        payload = bans_api.UpdateBanSchema(
            id=2, entity_type=Ban.entity_choices.CORPORATION, entity_character=None,
            entity_corporation=CorporationSchema(
                corporation_id=self.corporation.corporation_id, corporation_name=self.corporation.corporation_name,
            ),
            entity_alliance=None, reason="bad corp", public_reason=None, revoked_at=None,
        )

        self.view(request, payload)

        ban = Ban.objects.get(pk=2)
        self.assertEqual(ban.entity_corporation, self.corporation)

    @patch("incursions.api.bans.get_main_character_from_user")
    def test_creates_alliance_ban(self, mock_main: Mock) -> None:
        mock_main.return_value = self.issuer
        request = self.factory.post("/bans")
        request.user = _user(**{"incursions.waitlist_bans_manage": True})
        payload = bans_api.UpdateBanSchema(
            id=3, entity_type=Ban.entity_choices.ALLIANCE, entity_character=None, entity_corporation=None,
            entity_alliance=AllianceSchema(alliance_id=self.alliance.alliance_id, alliance_name=self.alliance.alliance_name),
            reason="bad alliance", public_reason=None, revoked_at=None,
        )

        self.view(request, payload)

        ban = Ban.objects.get(pk=3)
        self.assertEqual(ban.entity_alliance, self.alliance)

    @patch("incursions.api.bans.get_main_character_from_user")
    def test_creates_pre_revoked_ban(self, mock_main: Mock) -> None:
        mock_main.return_value = self.issuer
        revoked_at = datetime.now(timezone.utc)
        request = self.factory.post("/bans")
        request.user = _user(**{"incursions.waitlist_bans_manage": True})
        payload = bans_api.UpdateBanSchema(
            id=4, entity_type=Ban.entity_choices.CHARACTER,
            entity_character=CharacterSchema(character_id=self.character.character_id, character_name=self.character.character_name),
            entity_corporation=None, entity_alliance=None,
            reason="temp", public_reason=None, revoked_at=revoked_at,
        )

        self.view(request, payload)

        ban = Ban.objects.get(pk=4)
        self.assertEqual(ban.revoked_by, self.issuer)


class TestCharacterHistory(BansAPITestBase):
    def setUp(self) -> None:
        super().setUp()
        self.view = self.routes[("GET", "/bans/{character_id}")]
        self.character = EveCharacter.objects.create(character_id=8500, character_name="History Pilot")

    def test_denies_without_permission(self) -> None:
        request = self.factory.get(f"/bans/{self.character.character_id}")
        request.user = _user()

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

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

    def test_returns_404_for_unknown_character(self) -> None:
        request = self.factory.get("/bans/999999")
        request.user = _user(**{"incursions.waitlist_bans_view": True})

        response = self.view(request, 999999)

        self.assertEqual(response, (404, {"error": "Character not found"}))

    def test_returns_character_ban_history(self) -> None:
        Ban.objects.create(
            entity_type=Ban.entity_choices.CHARACTER, entity_character=self.character,
            reason="strike one", issued_by=self.issuer,
        )
        request = self.factory.get(f"/bans/{self.character.character_id}")
        request.user = _user(**{"incursions.waitlist_bans_manage": True})

        status_code, bans = self.view(request, self.character.character_id)

        self.assertEqual(status_code, 200)
        self.assertEqual(len(bans), 1)


class TestUpdateBan(BansAPITestBase):
    def setUp(self) -> None:
        super().setUp()
        self.view = self.routes[("PATCH", "/bans/{ban_id}")]
        self.character = EveCharacter.objects.create(character_id=8600, character_name="Ban Target")
        self.ban = Ban.objects.create(
            entity_type=Ban.entity_choices.CHARACTER, entity_character=self.character,
            reason="original", issued_by=self.issuer,
        )

    def _payload(self, **kwargs) -> bans_api.BanSchema:
        defaults = dict(
            id=self.ban.pk, entity_name="Ban Target", entity_type=Ban.entity_choices.CHARACTER,
            entity_character=None, entity_corporation=None, entity_alliance=None,
            issued_at=datetime.now(timezone.utc), issued_by=None,
            reason="updated reason", public_reason="public", revoked_at=None, revoked_by=None,
        )
        defaults.update(kwargs)
        return bans_api.BanSchema(**defaults)

    def test_denies_without_permission(self) -> None:
        request = self.factory.patch(f"/bans/{self.ban.pk}")
        request.user = _user()

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

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

    def test_returns_404_when_ban_missing(self) -> None:
        request = self.factory.patch("/bans/999999")
        request.user = _user(**{"incursions.waitlist_bans_manage": True})

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

        self.assertEqual(response, (404, {"error": "Ban not found"}))

    @patch("incursions.api.bans.get_main_character_from_user")
    def test_updates_ban_reason(self, mock_main: Mock) -> None:
        mock_main.return_value = self.issuer
        request = self.factory.patch(f"/bans/{self.ban.pk}")
        request.user = _user(**{"incursions.waitlist_bans_manage": True})

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

        self.assertEqual(response, (200, {"status": "Ban updated"}))
        self.ban.refresh_from_db()
        self.assertEqual(self.ban.reason, "updated reason")
        self.assertEqual(self.ban.public_reason, "public")
        self.assertIsNone(self.ban.revoked_by)


class TestRevokeBan(BansAPITestBase):
    def setUp(self) -> None:
        super().setUp()
        self.view = self.routes[("DELETE", "/bans/{ban_id}")]
        self.character = EveCharacter.objects.create(character_id=8700, character_name="Ban Target")
        self.ban = Ban.objects.create(
            entity_type=Ban.entity_choices.CHARACTER, entity_character=self.character,
            reason="original", issued_by=self.issuer,
        )

    def test_denies_without_permission(self) -> None:
        request = self.factory.delete(f"/bans/{self.ban.pk}")
        request.user = _user()

        response = self.view(request, self.ban.pk)

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

    def test_returns_404_when_ban_missing(self) -> None:
        request = self.factory.delete("/bans/999999")
        request.user = _user(**{"incursions.waitlist_bans_manage": True})

        response = self.view(request, 999999)

        self.assertEqual(response, (404, {"error": "Ban not found"}))

    @patch("incursions.api.bans.get_main_character_from_user")
    def test_revokes_ban(self, mock_main: Mock) -> None:
        mock_main.return_value = self.issuer
        request = self.factory.delete(f"/bans/{self.ban.pk}")
        request.user = _user(**{"incursions.waitlist_bans_manage": True})

        response = self.view(request, self.ban.pk)

        self.assertEqual(response, (200, {"status": "Ban revoked"}))
        self.ban.refresh_from_db()
        self.assertIsNotNone(self.ban.revoked_at)
        self.assertEqual(self.ban.revoked_by, self.issuer)
