"""ITs for the @mailman "Acceptances & rejections" log — Phase A of
[[my-sea-invite-voice-blueprint]].

Mirrors `apps.billboard.tests.integrated.test_tax` (the @taxman ledger): a
reserved system-author user (`mailman`) authors the interactive invite log
Lines via `apps.billboard.mail.log_sea_invite`.

This file grows in A4 with the `log_sea_invite` Post/Line/Brief tests; A2
lands only the reserved-username + idempotency contract for `mailman`,
mirroring `test_tax.TaxmanReservedUsernameTest`.
"""

from django.test import TestCase

from apps.billboard.mail import INVITE_TEMPLATE, log_sea_invite
from apps.billboard.models import (
    Brief,
    Line,
    MAIL_ACCEPTANCE_POST_TITLE,
    Post,
)
from apps.gameboard.models import SeaInvite
from apps.lyric.models import (
    User,
    get_or_create_mailman,
    is_reserved_username,
)


class LogSeaInviteTest(TestCase):
    """`log_sea_invite` appends one interactive Line + spawns a Brief on the
    invitee's single "Acceptances & rejections" Post, and links the Line back
    to the SeaInvite (powering the OK/BYE render in A5)."""

    def setUp(self):
        self.owner = User.objects.create(
            email="owner@test.io", username="discoman",
        )
        self.invitee = User.objects.create(
            email="bud@test.io", username="budster",
        )
        self.invite = SeaInvite.objects.create(
            owner=self.owner,
            invitee=self.invitee,
            invitee_email=self.invitee.email,
        )

    def test_creates_post_line_brief_on_invitee(self):
        post, line, brief = log_sea_invite(self.invite)
        # Post owned by the INVITEE (it's their notification surface)
        self.assertEqual(post.owner, self.invitee)
        self.assertEqual(post.kind, Post.KIND_MAIL_ACCEPTANCE)
        self.assertEqual(post.title, MAIL_ACCEPTANCE_POST_TITLE)
        # Line authored by @mailman + admin_solicited (survives the guard)
        self.assertEqual(line.post, post)
        self.assertEqual(line.author, get_or_create_mailman())
        self.assertTrue(line.admin_solicited)
        # Brief points at the Post + Line w. correct kind
        self.assertEqual(brief.owner, self.invitee)
        self.assertEqual(brief.post, post)
        self.assertEqual(brief.line, line)
        self.assertEqual(brief.kind, Brief.KIND_MAIL_ACCEPTANCE)
        self.assertEqual(brief.title, MAIL_ACCEPTANCE_POST_TITLE)

    def test_links_line_to_invite_both_directions(self):
        _, line, _ = log_sea_invite(self.invite)
        self.invite.refresh_from_db()
        self.assertEqual(self.invite.line, line)
        # OneToOne reverse accessor used by post.html (A5)
        self.assertEqual(line.sea_invite, self.invite)

    def test_prose_interpolates_owner_handle_and_default_possessive(self):
        _, line, _ = log_sea_invite(self.invite)
        self.assertIn("@discoman", line.text)
        # pluralism (default) possessive = "their"
        self.assertIn("their drawing table", line.text)
        self.assertIn("expire 24h", line.text)

    def test_possessive_follows_owner_pronouns(self):
        self.owner.pronouns = "misogyny"  # he/him/his
        self.owner.save()
        _, line, _ = log_sea_invite(self.invite)
        self.assertIn("his drawing table", line.text)

    def test_line_survives_post_save_guard(self):
        _, line, _ = log_sea_invite(self.invite)
        self.assertTrue(Line.objects.filter(pk=line.pk).exists())

    def test_two_inviters_share_invitees_one_post(self):
        other_owner = User.objects.create(
            email="other@test.io", username="amigo",
        )
        other_invite = SeaInvite.objects.create(
            owner=other_owner, invitee=self.invitee,
            invitee_email=self.invitee.email,
        )
        log_sea_invite(self.invite)
        log_sea_invite(other_invite)
        posts = Post.objects.filter(
            owner=self.invitee, kind=Post.KIND_MAIL_ACCEPTANCE,
        )
        self.assertEqual(posts.count(), 1)
        self.assertEqual(posts.first().lines.count(), 2)

    def test_unregistered_invitee_creates_no_log(self):
        # An unregistered recipient has no per-user Post surface yet — linking
        # on registration is deferred. log_sea_invite no-ops to (None,None,None).
        invite = SeaInvite.objects.create(
            owner=self.owner, invitee=None,
            invitee_email="stranger@nowhere.io",
        )
        self.assertEqual(log_sea_invite(invite), (None, None, None))

    def test_template_uses_listen_hook(self):
        self.assertTrue(INVITE_TEMPLATE.startswith("Listen!"))

    def test_line_wraps_owner_handle_in_post_attribution_anchor(self):
        """Bud landing page sprint 2026-05-27 — the inline OK/BYE block
        migrates onto bud.html; the @mailman Line now carries an
        `<a class="post-attribution">` around the owner's handle whose
        href routes to the owner's per-bud landing page."""
        _, line, _ = log_sea_invite(self.invite)
        self.assertIn('class="post-attribution"', line.text)
        self.assertIn(
            f'href="/billboard/buds/{self.owner.id}/"', line.text,
        )
        # The anchor wraps ONLY the at_handle (not the surrounding prose)
        self.assertIn(">@discoman</a>", line.text)


class MailAcceptanceKindTest(TestCase):
    """The new MAIL_ACCEPTANCE kind is registered on both Post + Brief."""

    def test_post_kind_registered(self):
        self.assertEqual(Post.KIND_MAIL_ACCEPTANCE, "mail_acceptance")
        kinds = dict(Post.KIND_CHOICES)
        self.assertIn(Post.KIND_MAIL_ACCEPTANCE, kinds)

    def test_brief_kind_registered(self):
        self.assertEqual(Brief.KIND_MAIL_ACCEPTANCE, "mail_acceptance")
        kinds = dict(Brief.KIND_CHOICES)
        self.assertIn(Brief.KIND_MAIL_ACCEPTANCE, kinds)

    def test_post_title_constant(self):
        self.assertEqual(MAIL_ACCEPTANCE_POST_TITLE, "Acceptances & rejections")


class MailAcceptanceGuardTest(TestCase):
    """post_save guard nukes any Line saved on a MAIL_ACCEPTANCE Post w.o.
    admin_solicited=True — same defense-in-depth as NOTE_UNLOCK / TAX_LEDGER.
    `log_sea_invite` sets admin_solicited=True so legitimate invite Lines
    survive."""

    def setUp(self):
        self.user = User.objects.create(email="guard_mail@test.io")
        self.mailman = get_or_create_mailman()
        self.post = Post.objects.create(
            owner=self.user,
            kind=Post.KIND_MAIL_ACCEPTANCE,
            title=MAIL_ACCEPTANCE_POST_TITLE,
        )

    def test_unsolicited_line_on_mail_acceptance_gets_deleted(self):
        Line.objects.create(
            post=self.post, text="impostor", author=self.mailman,
            admin_solicited=False,
        )
        self.assertEqual(self.post.lines.count(), 0)

    def test_solicited_line_on_mail_acceptance_survives(self):
        Line.objects.create(
            post=self.post, text="legit", author=self.mailman,
            admin_solicited=True,
        )
        self.assertEqual(self.post.lines.count(), 1)


class MailmanReservedUsernameTest(TestCase):
    """`mailman` joins `adman` + `taxman` as a reserved system-author handle."""

    def test_mailman_is_reserved(self):
        self.assertTrue(is_reserved_username("mailman"))
        self.assertTrue(is_reserved_username("MAILMAN"))  # case-insensitive

    def test_get_or_create_mailman_is_idempotent(self):
        a = get_or_create_mailman()
        b = get_or_create_mailman()
        self.assertEqual(a.pk, b.pk)
        self.assertEqual(a.email, "mailman@earthmanrpg.local")

    def test_mailman_is_not_searchable(self):
        # System users never surface in bud / recipient autocomplete.
        self.assertFalse(get_or_create_mailman().searchable)

    def test_existing_username_owner_is_not_blocked(self):
        # A user who already holds a name isn't blocked from re-saving it.
        u = User.objects.create(email="m@test.io", username="mailman_fan")
        self.assertFalse(is_reserved_username("mailman_fan", current_user=u))
