# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """!add_photo: what lands in the album, and what is refused before it can.""" from __future__ import annotations from typing import TYPE_CHECKING from signal_bot.pack import TAGS_FILE from signal_bot.pack_custom import CUSTOM_FILE from signal_bot.pack_replies import ( AMBIGUOUS, DUPLICATE_ONLY, NEED_LABEL, NEED_PHOTO, NEW_TAG_HINT, REJECTED, TOO_MANY, ) from signal_bot.pack_store import ADDS_FILE from tests.conftest import SECOND_GROUP from tests.pack_add_fixture import ( JPEG_ONE, JPEG_TWO, NOT_MEDIA, FakeStripper, Wiring, _run, holder, ) if TYPE_CHECKING: from signal_bot.config import Config from signal_bot.pack_holder import PackHolder from signal_bot.store import Store from signal_bot.transport_fake import FakeTransport __all__ = ["holder"] # -------------------------------------------------------------------------- # The happy paths # -------------------------------------------------------------------------- async def test_a_photo_tagged_with_a_person_is_retrievable_at_once( holder: PackHolder, transport: FakeTransport, store: Store, config: Config ) -> None: transport.payloads["a1"] = JPEG_ONE before = len(holder.pack.matching(frozenset({"p1"}))) await _run(Wiring(holder, transport, store, config), "!dodaj alfa", attachments=("a1",)) assert len(holder.pack.matching(frozenset({"p1"}))) == before + 1 assert transport.texts[-1] == f"dodano · p1: {before + 1}" async def test_an_unknown_label_mints_a_custom_tag_and_says_how_to_reach_it( holder: PackHolder, transport: FakeTransport, store: Store, config: Config ) -> None: transport.payloads["a1"] = JPEG_ONE await _run(Wiring(holder, transport, store, config), "!dodaj wakacje", attachments=("a1",)) assert holder.pack.custom.resolve("wakacje") == "t1" assert NEW_TAG_HINT in transport.texts[-1] assert "dodano · t1: 1" in transport.texts[-1] async def test_two_labels_tag_both( holder: PackHolder, transport: FakeTransport, store: Store, config: Config ) -> None: transport.payloads["a1"] = JPEG_ONE await _run(Wiring(holder, transport, store, config), "!dodaj alfa wakacje", attachments=("a1",)) stored = holder.pack.matching(frozenset({"p1", "t1"})) assert len(stored) == 1 async def test_a_whole_batch_is_stored( holder: PackHolder, transport: FakeTransport, store: Store, config: Config ) -> None: """Picking three photos at once is the normal phone gesture.""" transport.payloads.update({"a1": JPEG_ONE, "a2": JPEG_TWO}) before = len(holder.pack.matching(frozenset({"p1"}))) await _run(Wiring(holder, transport, store, config), "!dodaj alfa", attachments=("a1", "a2")) assert len(holder.pack.matching(frozenset({"p1"}))) == before + 2 async def test_a_combination_trigger_tags_everybody_in_it( holder: PackHolder, transport: FakeTransport, store: Store, config: Config ) -> None: transport.payloads["a1"] = JPEG_ONE await _run(Wiring(holder, transport, store, config), "!dodaj oboje", attachments=("a1",)) assert len(holder.pack.matching(frozenset({"p1", "p2"}))) == 2 # -------------------------------------------------------------------------- # The refusals, none of which may write anything # -------------------------------------------------------------------------- async def test_no_label_stores_nothing( holder: PackHolder, transport: FakeTransport, store: Store, config: Config ) -> None: transport.payloads["a1"] = JPEG_ONE await _run(Wiring(holder, transport, store, config), "!dodaj", attachments=("a1",)) assert transport.texts[-1] == NEED_LABEL async def test_no_attachment_changes_nothing_on_disk( holder: PackHolder, transport: FakeTransport, store: Store, config: Config ) -> None: """The pending mint is discarded: custom.json must not appear at all.""" root = holder.pack.directory await _run(Wiring(holder, transport, store, config), "!dodaj piesek") assert transport.texts[-1] == NEED_PHOTO assert not (root / CUSTOM_FILE).exists() assert not (root / ADDS_FILE).exists() assert holder.pack.custom.keys == frozenset() async def test_too_many_labels_are_refused( holder: PackHolder, transport: FakeTransport, store: Store, config: Config ) -> None: transport.payloads["a1"] = JPEG_ONE labels = " ".join(f"etykieta{n}" for n in range(7)) await _run(Wiring(holder, transport, store, config), f"!dodaj {labels}", attachments=("a1",)) assert transport.texts[-1] == TOO_MANY async def test_an_either_or_combination_is_refused_rather_than_guessed( holder: PackHolder, transport: FakeTransport, store: Store, config: Config ) -> None: """ "!ktokolwiek" means two different answers; tagging cannot pick one.""" transport.payloads["a1"] = JPEG_ONE await _run(Wiring(holder, transport, store, config), "!dodaj ktokolwiek", attachments=("a1",)) assert transport.texts[-1] == AMBIGUOUS async def test_a_file_that_is_not_media_is_refused_on_its_bytes( holder: PackHolder, transport: FakeTransport, store: Store, config: Config ) -> None: transport.payloads["a1"] = NOT_MEDIA await _run(Wiring(holder, transport, store, config), "!dodaj alfa", attachments=("a1",)) assert transport.texts[-1] == REJECTED assert not (holder.pack.directory / CUSTOM_FILE).exists() async def test_a_file_the_stripper_refuses_is_not_stored( holder: PackHolder, transport: FakeTransport, store: Store, config: Config ) -> None: transport.payloads["a1"] = JPEG_ONE before = len(holder.pack.tags) await _run( Wiring(holder, transport, store, config), "!dodaj alfa", stripper=FakeStripper(accepts=False), attachments=("a1",), ) assert transport.texts[-1] == REJECTED assert len(holder.pack.tags) == before async def test_an_attachment_the_container_lost_is_refused( holder: PackHolder, transport: FakeTransport, store: Store, config: Config ) -> None: await _run(Wiring(holder, transport, store, config), "!dodaj alfa", attachments=("missing",)) assert transport.texts[-1] == REJECTED async def test_it_does_not_exist_in_another_group( holder: PackHolder, transport: FakeTransport, store: Store, config: Config ) -> None: transport.payloads["a1"] = JPEG_ONE await _run( Wiring(holder, transport, store, config), "!dodaj alfa", attachments=("a1",), group=SECOND_GROUP, ) assert transport.sent == [] assert transport.deleted == [] # -------------------------------------------------------------------------- # Duplicates # -------------------------------------------------------------------------- async def test_a_photo_already_in_the_album_is_reported_and_nothing_changes( holder: PackHolder, transport: FakeTransport, store: Store, config: Config ) -> None: transport.payloads["a1"] = JPEG_ONE await _run(Wiring(holder, transport, store, config), "!dodaj alfa", attachments=("a1",)) tags_before = (holder.pack.directory / TAGS_FILE).read_text(encoding="utf-8") transport.payloads["a2"] = JPEG_ONE await _run(Wiring(holder, transport, store, config), "!dodaj beta", attachments=("a2",)) assert transport.texts[-1] == DUPLICATE_ONLY assert (holder.pack.directory / TAGS_FILE).read_text(encoding="utf-8") == tags_before async def test_a_duplicate_only_message_mints_nothing( holder: PackHolder, transport: FakeTransport, store: Store, config: Config ) -> None: transport.payloads["a1"] = JPEG_ONE await _run(Wiring(holder, transport, store, config), "!dodaj alfa", attachments=("a1",)) transport.payloads["a2"] = JPEG_ONE await _run(Wiring(holder, transport, store, config), "!dodaj zupelnienowe", attachments=("a2",)) assert holder.pack.custom.resolve("zupelnienowe") is None async def test_a_mixed_batch_stores_the_new_ones_and_counts_the_rest( holder: PackHolder, transport: FakeTransport, store: Store, config: Config ) -> None: transport.payloads["a1"] = JPEG_ONE await _run(Wiring(holder, transport, store, config), "!dodaj alfa", attachments=("a1",)) before = len(holder.pack.matching(frozenset({"p2"}))) transport.payloads.update({"a2": JPEG_ONE, "a3": JPEG_TWO}) await _run(Wiring(holder, transport, store, config), "!dodaj beta", attachments=("a2", "a3")) assert "1 już było" in transport.texts[-1] assert len(holder.pack.matching(frozenset({"p2"}))) == before + 1 async def test_the_same_photo_twice_in_one_message_counts_once( holder: PackHolder, transport: FakeTransport, store: Store, config: Config ) -> None: transport.payloads.update({"a1": JPEG_ONE, "a2": JPEG_ONE}) before = len(holder.pack.matching(frozenset({"p1"}))) await _run(Wiring(holder, transport, store, config), "!dodaj alfa", attachments=("a1", "a2")) assert len(holder.pack.matching(frozenset({"p1"}))) == before + 1 assert "1 już było" in transport.texts[-1]