"""ITs for the gatekeeper invite_gamer view post-bud-btn refactor.

The legacy form-submit path (POST `invitee_email` + 302 redirect) still
works for any pre-existing caller; the new bud-btn slide-out POSTs
`recipient` (email OR username) w. Accept: application/json and gets
back {brief, recipient_display}. On registered recipients we auto-add
both directions of the buds graph (mirrors share_post per the Phase 2
spec) and spawn a Brief w. kind=GAME_INVITE pointing at the room.
"""
from django.test import TestCase
from django.urls import reverse

from apps.billboard.models import Brief
from apps.epic.models import Room, RoomInvite
from apps.lyric.models import User


class InviteGamerAjaxTest(TestCase):
    """Bud-btn flow: POST /room/<id>/gate/invite w. Accept: application/json."""

    def setUp(self):
        self.owner = User.objects.create(email="owner@test.io", username="owner")
        self.client.force_login(self.owner)
        self.alice = User.objects.create(email="alice@test.io", username="alice")
        self.room = Room.objects.create(name="Bingobango", owner=self.owner)

    def _invite(self, recipient):
        return self.client.post(
            reverse("epic:invite_gamer", args=[self.room.id]),
            data={"recipient": recipient},
            HTTP_ACCEPT="application/json",
        )

    def test_ajax_invite_returns_brief_payload(self):
        response = self._invite("alice@test.io")
        self.assertEqual(response.status_code, 200)
        body = response.json()
        self.assertIn("brief", body)
        self.assertIn("recipient_display", body)
        self.assertIsNotNone(body["brief"])

    def test_ajax_invite_creates_room_invite(self):
        self._invite("alice@test.io")
        self.assertTrue(RoomInvite.objects.filter(
            room=self.room, inviter=self.owner, invitee_email="alice@test.io",
        ).exists())

    def test_ajax_invite_resolves_username_to_email(self):
        """Username-typed recipient stores the resolved User's email."""
        self._invite("alice")
        self.assertTrue(RoomInvite.objects.filter(
            room=self.room, invitee_email="alice@test.io",
        ).exists())

    def test_ajax_invite_creates_brief_with_game_invite_kind_and_room_fk(self):
        self._invite("alice@test.io")
        brief = Brief.objects.get(owner=self.owner)
        self.assertEqual(brief.kind, Brief.KIND_GAME_INVITE)
        self.assertEqual(brief.room, self.room)
        self.assertIsNone(brief.post)
        self.assertTrue(brief.is_unread)

    def test_ajax_invite_brief_banner_dict_links_to_room_gatekeeper(self):
        body = self._invite("alice@test.io").json()
        self.assertEqual(
            body["brief"]["post_url"],
            reverse("epic:gatekeeper", args=[self.room.id]),
        )

    def test_ajax_invite_auto_adds_recipient_to_inviter_buds(self):
        self._invite("alice@test.io")
        self.assertIn(self.alice, self.owner.buds.all())

    def test_ajax_invite_auto_adds_inviter_to_recipient_buds_symmetric(self):
        """Per Phase 2 spec: shared events imply mutual buds graph link."""
        self._invite("alice@test.io")
        self.assertIn(self.owner, self.alice.buds.all())

    def test_ajax_invite_unregistered_email_creates_invite_no_buds_add(self):
        """Privacy + correctness: unregistered recipient still gets a
        RoomInvite (so they can accept after registration), but the
        inviter's buds list isn't touched (we don't auto-add a non-User)."""
        response = self._invite("ghost@test.io")
        self.assertEqual(response.status_code, 200)
        self.assertTrue(RoomInvite.objects.filter(
            room=self.room, invitee_email="ghost@test.io",
        ).exists())
        self.assertEqual(self.owner.buds.count(), 0)

    def test_ajax_invite_self_is_silent_noop(self):
        """Inviting yourself: brief=null, no RoomInvite, no buds touch."""
        response = self._invite("owner@test.io")
        self.assertEqual(response.status_code, 200)
        self.assertIsNone(response.json()["brief"])
        self.assertFalse(RoomInvite.objects.filter(room=self.room).exists())

    def test_ajax_invite_blank_recipient_is_silent_noop(self):
        response = self._invite("")
        self.assertEqual(response.status_code, 200)
        self.assertIsNone(response.json()["brief"])
        self.assertFalse(RoomInvite.objects.filter(room=self.room).exists())

    def test_ajax_invite_recipient_display_is_username_when_registered(self):
        body = self._invite("alice@test.io").json()
        self.assertEqual(body["recipient_display"], "alice")

    def test_ajax_invite_recipient_display_is_null_when_unregistered(self):
        body = self._invite("ghost@test.io").json()
        self.assertIsNone(body["recipient_display"])


class InviteGamerLegacyFormTest(TestCase):
    """Form-submit path (no Accept: application/json) preserved — older
    callers still get a 302 redirect to the gatekeeper. The legacy
    `invitee_email` field name is also still accepted for full backwards
    compat, even though the new bud-btn uses `recipient`."""

    def setUp(self):
        self.owner = User.objects.create(email="owner@test.io")
        self.client.force_login(self.owner)
        self.room = Room.objects.create(name="Test", owner=self.owner)

    def test_form_submit_with_invitee_email_redirects(self):
        response = self.client.post(
            reverse("epic:invite_gamer", args=[self.room.id]),
            data={"invitee_email": "alice@test.io"},
        )
        self.assertEqual(response.status_code, 302)
        self.assertEqual(
            response["Location"],
            reverse("epic:gatekeeper", args=[self.room.id]),
        )

    def test_form_submit_with_recipient_field_also_works(self):
        """The bud-btn field name (recipient) also works on form submit."""
        response = self.client.post(
            reverse("epic:invite_gamer", args=[self.room.id]),
            data={"recipient": "alice@test.io"},
        )
        self.assertEqual(response.status_code, 302)
        self.assertTrue(RoomInvite.objects.filter(
            room=self.room, invitee_email="alice@test.io",
        ).exists())


class InviteGamerAlreadyPresentTest(TestCase):
    """Duplicate-invite guard: a gatekeeper invite gets `already_present:
    true` in its JSON response when the recipient is either (a) already
    seated in the room or (b) has a pending RoomInvite. `recipient_user_id`
    is set only when seated — pending invitees have no visible
    `.gate-slot.filled` DOM element to highlight, so the FYI button just
    dismisses without easing in the flash class."""

    def setUp(self):
        from apps.epic.models import GateSlot, TableSeat
        self.GateSlot = GateSlot
        self.TableSeat = TableSeat
        self.owner = User.objects.create(email="owner@test.io", username="owner")
        self.client.force_login(self.owner)
        self.alice = User.objects.create(email="alice@test.io", username="alice")
        self.room = Room.objects.create(name="Bingo", owner=self.owner)

    def _invite(self, recipient):
        return self.client.post(
            reverse("epic:invite_gamer", args=[self.room.id]),
            data={"recipient": recipient},
            HTTP_ACCEPT="application/json",
        )

    def test_already_present_true_when_recipient_is_seated_via_gate_slot(self):
        """Gatekeeper phase: 'seated' means a GateSlot.FILLED w. gamer set."""
        self.GateSlot.objects.create(
            room=self.room, gamer=self.alice, slot_number=1,
            status=self.GateSlot.FILLED,
        )
        self.assertTrue(self._invite("alice@test.io").json()["already_present"])

    def test_already_present_true_when_recipient_has_table_seat(self):
        """Post-gatekeeper phase: TableSeat exists once slots transition."""
        self.TableSeat.objects.create(room=self.room, gamer=self.alice, slot_number=1)
        self.assertTrue(self._invite("alice@test.io").json()["already_present"])

    def test_recipient_user_id_carries_alice_id_when_seated(self):
        self.TableSeat.objects.create(room=self.room, gamer=self.alice, slot_number=1)
        body = self._invite("alice@test.io").json()
        self.assertEqual(body["recipient_user_id"], str(self.alice.id))

    def test_already_present_true_when_recipient_has_pending_room_invite(self):
        RoomInvite.objects.create(
            room=self.room, inviter=self.owner,
            invitee_email="alice@test.io", status=RoomInvite.PENDING,
        )
        self.assertTrue(self._invite("alice@test.io").json()["already_present"])

    def test_recipient_user_id_null_when_only_invited_not_seated(self):
        """Pending invites have no visible slot element to highlight."""
        RoomInvite.objects.create(
            room=self.room, inviter=self.owner,
            invitee_email="alice@test.io", status=RoomInvite.PENDING,
        )
        self.assertIsNone(self._invite("alice@test.io").json()["recipient_user_id"])

    def test_already_present_false_for_brand_new_recipient(self):
        self.assertFalse(self._invite("alice@test.io").json()["already_present"])

    def test_brief_is_null_on_duplicate(self):
        """No new Brief on duplicate — the client renders the error Brief
        from the already_present flag instead."""
        self.TableSeat.objects.create(room=self.room, gamer=self.alice, slot_number=1)
        self.assertIsNone(self._invite("alice@test.io").json()["brief"])

    def test_no_new_room_invite_on_duplicate(self):
        RoomInvite.objects.create(
            room=self.room, inviter=self.owner,
            invitee_email="alice@test.io", status=RoomInvite.PENDING,
        )
        self._invite("alice@test.io")
        self.assertEqual(
            RoomInvite.objects.filter(room=self.room, invitee_email="alice@test.io").count(),
            1,
        )

    def test_recipient_display_carries_username_on_duplicate(self):
        self.TableSeat.objects.create(room=self.room, gamer=self.alice, slot_number=1)
        self.assertEqual(
            self._invite("alice@test.io").json()["recipient_display"],
            "alice",
        )
