# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """The !papież command: a random pope, captioned and linked.""" from __future__ import annotations import base64 from pathlib import Path from typing import TYPE_CHECKING import httpx import pytest from signal_bot.decorators import DISPLAY_MARGIN_SECONDS, cooldown from signal_bot.http_client import ImageFetcher from signal_bot.models import parse_envelope from signal_bot.popes_command import ( COMMAND_NAME, article_url, caption_for, random_pope, register_pope_command, ) from signal_bot.portraits import PortraitCache from signal_bot.registry import CommandContext, CommandRefusedError, CommandRegistry from signal_bot.versioning import POPES from tests.conftest import WATCHED_GROUP, envelope if TYPE_CHECKING: from signal_bot.config import Config from signal_bot.store import Store from signal_bot.transport_fake import FakeTransport IMAGE = b"portrait-bytes" ALIASES = {COMMAND_NAME: ["!papież", "!papiez", "!pope"]} def _cache(tmp_path: Path) -> PortraitCache: def handler(request: httpx.Request) -> httpx.Response: if "commons" in str(request.url): page = {"imageinfo": [{"thumburl": "https://cdn/portrait.jpg"}]} return httpx.Response(200, json={"query": {"pages": [page]}}) return httpx.Response(200, content=IMAGE) fetcher = ImageFetcher(httpx.AsyncClient(transport=httpx.MockTransport(handler))) return PortraitCache(fetcher, "https://commons.example/w/api.php", tmp_path) def _ctx(transport: FakeTransport, store: Store, config: Config, text: str) -> CommandContext: message = parse_envelope(envelope(text=text)) assert message is not None return CommandContext( message=message, group_id=WATCHED_GROUP, transport=transport, store=store, config=config, command_name=COMMAND_NAME, now=100.0, day="2026-08-30", ) @pytest.mark.parametrize("trigger", ["!papież", "!papiez", "!pope"]) def test_every_alias_resolves(trigger: str) -> None: registry = CommandRegistry() register_pope_command(registry, _cache(Path("/nonexistent")), aliases=ALIASES) command = registry.resolve(trigger) assert command is not None assert command.name == COMMAND_NAME def test_the_real_alias_file_carries_the_command() -> None: registry = CommandRegistry() register_pope_command(registry, _cache(Path("/nonexistent"))) # The Polish spelling is the one kuhy asked for; folding makes the rest # of the keyboard layouts work, but this one must be listed outright. assert registry.resolve("!papież") is not None async def test_it_posts_a_portrait_with_the_caption_attached( tmp_path: Path, transport: FakeTransport, store: Store, config: Config ) -> None: registry = CommandRegistry() register_pope_command(registry, _cache(tmp_path), aliases=ALIASES) command = registry.resolve("!papież") assert command is not None await command.handler(_ctx(transport, store, config, "!papież")) sent = transport.sent[0] assert sent.recipient == WATCHED_GROUP assert sent.image_base64 is not None assert base64.b64decode(sent.image_base64) == IMAGE # One message, not two: the caption rides in the same payload. assert len(transport.sent) == 1 assert "papież nr " in sent.caption assert "https://pl.wikipedia.org/wiki/" in sent.caption async def test_the_cooldown_decorator_applies( tmp_path: Path, transport: FakeTransport, store: Store, config: Config ) -> None: registry = CommandRegistry() register_pope_command( registry, _cache(tmp_path), decorate=cooldown(10, "WAIT {seconds}"), aliases=ALIASES ) store.record_use(WATCHED_GROUP, "uuid-alice", COMMAND_NAME, 100.0, "2026-08-30") command = registry.resolve("!pope") assert command is not None with pytest.raises(CommandRefusedError): await command.handler(_ctx(transport, store, config, "!pope")) assert transport.texts == [f"WAIT {10 + DISPLAY_MARGIN_SECONDS}"] def test_the_caption_names_the_pope_in_both_languages() -> None: pope = POPES[263] assert pope.name == "Jan Paweł II" assert caption_for(pope) == ( "Jan Paweł II — Ioannes Paulus Secundus\n" "papież nr 264 z 267, pontyfikat 1978-2005\n" "https://pl.wikipedia.org/wiki/Jan_Pawe%C5%82_II" ) def test_the_latin_name_never_contradicts_the_polish_number() -> None: """Both names come from the same table row, so they cannot disagree. la.wikipedia titles #154 "Stephanus X" while every other source calls him the ninth, and "Stefan IX — Stephanus X" in one caption reads as a bug. The Latin column of Poczet papieży says "Stephanus Nonus". """ assert POPES[153].name == "Stefan IX" assert POPES[153].latin == "Stephanus Nonus" assert POPES[266].latin == "Leo Decimus Quartus" def test_the_url_uses_the_title_so_disambiguated_popes_still_resolve() -> None: telesfor = POPES[7] assert telesfor.name == "Telesfor" # Half the early popes live at " (papież)"; name alone 404s. assert article_url(telesfor) == "https://pl.wikipedia.org/wiki/Telesfor_%28papie%C5%BC%29" def test_the_draw_actually_varies() -> None: # 0.0.3 shipped a !gekon that posted one identical photo forever because # nobody drew twice. Twenty draws from 267 popes repeating is a 1-in-267^19 # event; anything that pins the choice fails here immediately. drawn = {random_pope().ordinal for _ in range(20)} assert len(drawn) > 1