# Standard Library
from unittest.mock import Mock, patch

# Django
from django.test import RequestFactory
from django.utils.timezone import now

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

from incursions.api import announcements as announcements_api
from incursions.models.waitlist import Announcement


class FakeAPI:
    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 put(self, path: str, **kwargs: object):
        return self._register("PUT", path, **kwargs)

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


def _routes() -> dict[tuple[str, str], object]:
    api = FakeAPI()
    announcements_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 AnnouncementsAPITestBase(NoSocketsTestCase):
    def setUp(self) -> None:
        self.factory = RequestFactory()
        self.routes = _routes()
        self.author = EveCharacter.objects.create(character_id=8000, character_name="Author")


class TestListAnnouncements(AnnouncementsAPITestBase):
    def setUp(self) -> None:
        super().setUp()
        self.view = self.routes[("GET", "/announcements")]

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

        response = self.view(request)

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

    def test_returns_only_active_announcements(self) -> None:
        active = Announcement.objects.create(message="active", created_by=self.author)
        Announcement.objects.create(
            message="revoked", created_by=self.author, revoked_by=self.author, revoked_at=now(),
        )
        request = self.factory.get("/announcements")
        request.user = _user(**{"incursions.basic_waitlist": True})

        status_code, announcements = self.view(request)

        self.assertEqual(status_code, 200)
        self.assertIn(active.pk, [a.pk for a in announcements])
        self.assertNotIn("revoked", [a.message for a in announcements])


class TestCreateAnnouncement(AnnouncementsAPITestBase):
    def setUp(self) -> None:
        super().setUp()
        self.view = self.routes[("POST", "/announcements")]

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

        response = self.view(request, announcements_api.RequestPayloadSchema(message="hi", is_alert=False, pages=None))

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

    @patch("incursions.api.announcements.sse_client.submit")
    @patch("incursions.api.announcements.get_main_character_from_user")
    def test_creates_announcement_and_submits_sse_event(self, mock_main_char: Mock, mock_submit: Mock) -> None:
        mock_main_char.return_value = self.author
        request = self.factory.post("/announcements")
        request.user = _user(**{"incursions.waitlist_announcements_manage": True})

        status_code, body = self.view(
            request, announcements_api.RequestPayloadSchema(message="hello world", is_alert=True, pages="home"),
        )

        self.assertEqual(status_code, 200)
        announcement = Announcement.objects.get(pk=body["id"])
        self.assertEqual(announcement.message, "hello world")
        self.assertTrue(announcement.is_alert)
        self.assertEqual(announcement.pages, "home")
        self.assertEqual(announcement.created_by, self.author)
        mock_submit.assert_called_once()


class TestUpdateAnnouncement(AnnouncementsAPITestBase):
    def setUp(self) -> None:
        super().setUp()
        self.view = self.routes[("PUT", "/announcements/{announcement_id}")]
        self.announcement = Announcement.objects.create(message="original", created_by=self.author)

    def test_denies_without_permission(self) -> None:
        request = self.factory.put(f"/announcements/{self.announcement.pk}")
        request.user = _user()

        response = self.view(
            request, self.announcement.pk,
            announcements_api.RequestPayloadSchema(message="new", is_alert=False, pages=None),
        )

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

    @patch("incursions.api.announcements.sse_client.submit")
    @patch("incursions.api.announcements.get_main_character_from_user")
    def test_updates_announcement_fields(self, mock_main_char: Mock, mock_submit: Mock) -> None:
        mock_main_char.return_value = self.author
        request = self.factory.put(f"/announcements/{self.announcement.pk}")
        request.user = _user(**{"incursions.waitlist_announcements_manage": True})

        status_code, body = self.view(
            request, self.announcement.pk,
            announcements_api.RequestPayloadSchema(message="updated", is_alert=True, pages="waitlist"),
        )

        self.assertEqual(status_code, 200)
        self.announcement.refresh_from_db()
        self.assertEqual(self.announcement.message, "updated")
        self.assertTrue(self.announcement.is_alert)
        self.assertEqual(self.announcement.pages, "waitlist")
        mock_submit.assert_called_once()

    @patch("incursions.api.announcements.get_main_character_from_user")
    def test_raises_when_announcement_missing(self, mock_main_char: Mock) -> None:
        mock_main_char.return_value = self.author
        request = self.factory.put("/announcements/999999")
        request.user = _user(**{"incursions.waitlist_announcements_manage": True})

        with self.assertRaises(Announcement.DoesNotExist):
            self.view(
                request, 999999,
                announcements_api.RequestPayloadSchema(message="new", is_alert=False, pages=None),
            )


class TestRevokeAnnouncement(AnnouncementsAPITestBase):
    def setUp(self) -> None:
        super().setUp()
        self.view = self.routes[("DELETE", "/announcements/{announcement_id}")]
        self.announcement = Announcement.objects.create(message="original", created_by=self.author)

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

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

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

    @patch("incursions.api.announcements.sse_client.submit")
    @patch("incursions.api.announcements.get_main_character_from_user")
    def test_revokes_announcement(self, mock_main_char: Mock, mock_submit: Mock) -> None:
        mock_main_char.return_value = self.author
        request = self.factory.delete(f"/announcements/{self.announcement.pk}")
        request.user = _user(**{"incursions.waitlist_announcements_manage": True})

        status_code, body = self.view(request, self.announcement.pk)

        self.assertEqual(status_code, 200)
        self.announcement.refresh_from_db()
        self.assertIsNotNone(self.announcement.revoked_at)
        self.assertEqual(self.announcement.revoked_by, self.author)
        mock_submit.assert_called_once()

    @patch("incursions.api.announcements.get_main_character_from_user")
    def test_raises_when_announcement_missing_or_already_revoked(self, mock_main_char: Mock) -> None:
        mock_main_char.return_value = self.author
        request = self.factory.delete("/announcements/999999")
        request.user = _user(**{"incursions.waitlist_announcements_manage": True})

        with self.assertRaises(Announcement.DoesNotExist):
            self.view(request, 999999)
