"""ITs for the my-sea spectator (invitee) views — Phase B of
[[my-sea-invite-voice-blueprint]].

An ACCEPTED invitee can VISIT the owner's my-sea as a read-only spectator,
deposit a token at a visitor gate to occupy seat 2C (+ open a 24h voice
window), and BYE out (freeing the seat). All access is gated on an ACCEPTED
SeaInvite for (owner, request.user).
"""

from datetime import timedelta

from django.test import TestCase
from django.urls import reverse
from django.utils import timezone

from apps.gameboard.models import MySeaDraw, SeaInvite
from apps.lyric.models import Token, User


def _owner_with_sig(email="owner@test.io", username="discoman"):
    owner = User.objects.create(email=email, username=username)
    # A sig id is required to build a MySeaDraw; any int is fine for the
    # spectator read-only path (no card lookup asserted here).
    return owner


class MySeaVisitGuardTest(TestCase):
    def setUp(self):
        self.owner = _owner_with_sig()
        self.bud = User.objects.create(email="bud@test.io", username="budster")
        self.invite = SeaInvite.objects.create(
            owner=self.owner, invitee=self.bud, invitee_email=self.bud.email,
            status=SeaInvite.ACCEPTED, accepted_at=timezone.now(),
        )
        self.url = reverse("my_sea_visit", args=[self.owner.id])

    def test_accepted_invitee_can_visit(self):
        self.client.force_login(self.bud)
        self.assertEqual(self.client.get(self.url).status_code, 200)

    def test_non_invitee_is_forbidden(self):
        stranger = User.objects.create(email="x@test.io", username="x")
        self.client.force_login(stranger)
        self.assertEqual(self.client.get(self.url).status_code, 403)

    def test_pending_invitee_is_auto_accepted_on_visit(self):
        # Accept-on-GET (user-spec 2026-05-29): following the bud-page sea-btn
        # cascade — or the @mailman post-attribution anchor — to the spectator
        # table implicitly accepts a still-pending invite, so the page renders
        # instead of 403ing the legitimately-invited user.
        self.invite.status = SeaInvite.PENDING
        self.invite.accepted_at = None
        self.invite.save()
        self.client.force_login(self.bud)
        self.assertEqual(self.client.get(self.url).status_code, 200)
        self.invite.refresh_from_db()
        self.assertEqual(self.invite.status, SeaInvite.ACCEPTED)
        self.assertIsNotNone(self.invite.accepted_at)

    def test_expired_pending_invitee_is_forbidden(self):
        # A lapsed PENDING invite (no deposit, past 24h) is NOT auto-accepted.
        self.invite.status = SeaInvite.PENDING
        self.invite.accepted_at = None
        self.invite.save()
        SeaInvite.objects.filter(pk=self.invite.pk).update(
            created_at=timezone.now() - timedelta(hours=48),
        )
        self.client.force_login(self.bud)
        self.assertEqual(self.client.get(self.url).status_code, 403)

    def test_left_invitee_is_forbidden(self):
        self.invite.status = SeaInvite.LEFT
        self.invite.left_at = timezone.now()
        self.invite.save()
        self.client.force_login(self.bud)
        self.assertEqual(self.client.get(self.url).status_code, 403)

    def test_owner_visiting_own_sea_redirects_to_my_sea(self):
        self.client.force_login(self.owner)
        resp = self.client.get(self.url)
        self.assertRedirects(resp, reverse("my_sea"), fetch_redirect_response=False)


class MySeaVisitContextTest(TestCase):
    def setUp(self):
        self.owner = _owner_with_sig()
        self.bud = User.objects.create(email="bud@test.io", username="budster")
        self.invite = SeaInvite.objects.create(
            owner=self.owner, invitee=self.bud, invitee_email=self.bud.email,
            status=SeaInvite.ACCEPTED, accepted_at=timezone.now(),
        )
        # Owner has drawn a hand → seat 1C is occupied + cards exist to view.
        MySeaDraw.objects.create(
            user=self.owner, spread="situation-action-outcome",
            significator_id=1, hand=[
                {"position": "lay", "card_id": 1, "reversed": False,
                 "polarity": "gravity"},
            ],
        )
        self.client.force_login(self.bud)
        self.url = reverse("my_sea_visit", args=[self.owner.id])

    def test_spectator_context_flags(self):
        ctx = self.client.get(self.url).context
        self.assertTrue(ctx["spectator"])
        self.assertFalse(ctx["is_owner"])
        self.assertEqual(ctx["owner"], self.owner)
        self.assertEqual(ctx["sea_invite"], self.invite)
        # Owner controls forced off on the spectator surface.
        self.assertFalse(ctx["sea_btn_active"])

    def test_not_present_shows_gate_view(self):
        content = self.client.get(self.url).content.decode()
        self.assertIn("GATE", content)
        self.assertIn(reverse("my_sea_visit_gate", args=[self.owner.id]), content)

    def test_present_shows_view_draw_and_seat2(self):
        self.invite.token_deposited_at = timezone.now()
        self.invite.voice_until = timezone.now() + timedelta(hours=24)
        self.invite.save()
        ctx = self.client.get(self.url).context
        self.assertTrue(ctx["sea_invite"].is_present)
        self.assertTrue(ctx["seat2_present"])
        self.assertIn("VIEW", self.client.get(self.url).content.decode())


class MySeaVisitInsertTokenTest(TestCase):
    def setUp(self):
        self.owner = _owner_with_sig()
        self.bud = User.objects.create(email="bud@test.io", username="budster")
        self.invite = SeaInvite.objects.create(
            owner=self.owner, invitee=self.bud, invitee_email=self.bud.email,
            status=SeaInvite.ACCEPTED, accepted_at=timezone.now(),
        )
        self.url = reverse("my_sea_visit_insert_token", args=[self.owner.id])

    def test_deposit_marks_present_and_opens_voice_window(self):
        self.client.force_login(self.bud)
        resp = self.client.post(self.url)
        self.invite.refresh_from_db()
        self.assertIsNotNone(self.invite.token_deposited_at)
        self.assertIsNotNone(self.invite.voice_until)
        self.assertTrue(self.invite.voice_active)
        self.assertTrue(self.invite.is_present)
        self.assertRedirects(
            resp, reverse("my_sea_visit", args=[self.owner.id]),
            fetch_redirect_response=False,
        )

    def test_non_invitee_cannot_deposit(self):
        stranger = User.objects.create(email="x@test.io", username="x")
        self.client.force_login(stranger)
        self.assertEqual(self.client.post(self.url).status_code, 403)

    def test_no_token_does_not_mark_present(self):
        # Strip the visitor of every usable token.
        self.bud.equipped_trinket = None
        self.bud.save(update_fields=["equipped_trinket"])
        Token.objects.filter(user=self.bud).delete()
        self.client.force_login(self.bud)
        self.client.post(self.url)
        self.invite.refresh_from_db()
        self.assertIsNone(self.invite.token_deposited_at)
        self.assertFalse(self.invite.is_present)


class MySeaVisitLeaveTest(TestCase):
    def setUp(self):
        self.owner = _owner_with_sig()
        self.bud = User.objects.create(email="bud@test.io", username="budster")
        self.invite = SeaInvite.objects.create(
            owner=self.owner, invitee=self.bud, invitee_email=self.bud.email,
            status=SeaInvite.ACCEPTED, accepted_at=timezone.now(),
            token_deposited_at=timezone.now(),
            voice_until=timezone.now() + timedelta(hours=24),
        )
        self.url = reverse("my_sea_visit_leave", args=[self.owner.id])

    def test_leave_sets_left_and_kills_voice(self):
        self.client.force_login(self.bud)
        resp = self.client.post(self.url)
        self.invite.refresh_from_db()
        self.assertEqual(self.invite.status, SeaInvite.LEFT)
        self.assertIsNotNone(self.invite.left_at)
        self.assertIsNone(self.invite.voice_until)
        self.assertFalse(self.invite.is_present)
        self.assertRedirects(
            resp, reverse("gameboard"), fetch_redirect_response=False,
        )

    def test_non_invitee_cannot_leave(self):
        stranger = User.objects.create(email="x@test.io", username="x")
        self.client.force_login(stranger)
        self.assertEqual(self.client.post(self.url).status_code, 403)


class MySeaVoiceContextTest(TestCase):
    """Phase C — the #id_voice_btn lights up (voice_active) for both the
    owner and the present invitee while the 24h voice window is open, keyed on
    mysea-<owner_id>."""

    def setUp(self):
        self.owner = _owner_with_sig()
        self.bud = User.objects.create(email="bud@test.io", username="budster")
        self.room_id = f"mysea-{self.owner.id}"

    def _present_invite(self, voice_future=True):
        return SeaInvite.objects.create(
            owner=self.owner, invitee=self.bud, invitee_email=self.bud.email,
            status=SeaInvite.ACCEPTED, accepted_at=timezone.now(),
            token_deposited_at=timezone.now(),
            voice_until=timezone.now() + (
                timedelta(hours=24) if voice_future else timedelta(hours=-1)
            ),
        )

    def test_spectator_voice_active_in_context(self):
        self._present_invite()
        self.client.force_login(self.bud)
        ctx = self.client.get(
            reverse("my_sea_visit", args=[self.owner.id])
        ).context
        self.assertTrue(ctx["voice_active"])
        self.assertEqual(ctx["voice_room_id"], self.room_id)

    def test_owner_voice_active_when_present_invitee_has_window(self):
        self._present_invite()
        self.client.force_login(self.owner)
        ctx = self.client.get(reverse("my_sea")).context
        self.assertTrue(ctx["voice_active"])
        self.assertEqual(ctx["voice_room_id"], self.room_id)

    def test_owner_voice_inactive_without_present_invitee(self):
        self.client.force_login(self.owner)
        ctx = self.client.get(reverse("my_sea")).context
        self.assertFalse(ctx["voice_active"])

    def test_owner_voice_inactive_when_window_lapsed(self):
        self._present_invite(voice_future=False)
        self.client.force_login(self.owner)
        ctx = self.client.get(reverse("my_sea")).context
        self.assertFalse(ctx["voice_active"])
