"""Every document that arrives is registered with the CMS (ADR-0010).

The invariant these defend is not "the happy path works". It is that there is no
way to send something to this agent and have no record of it — including the
paths where the agent refuses the document, which is where the silence used to
be. A client who forwards a .docx got a counter nobody was watching; they now
get a row with a reason on it.

The second half is what happens when the CMS cannot be reached. Holding the
registration is a weakening of "nothing exists only inside Python", approved on
three conditions, and each of them is a test here: the document does not
proceed, the queue is bounded, and it is retried until it lands.
"""

from __future__ import annotations

import pytest

from app.intake import InMemoryOutbox
from app.whatsapp.inbox import InboxFilter
from app.whatsapp.messages import InboundMessage, MediaReference, MessageType
from app.whatsapp.pending import InMemoryPendingPdfs
from app.whatsapp.webhook import InboundQueue
from tests.intake_support import RecordingCms, UnreachableCms

OWNER = "923049637232"


@pytest.fixture
def container(tmp_path):
    class FakeContainer:
        inbound_queue = InboundQueue()
        incoming_dir = tmp_path
        inbox = InboxFilter()
        pending_pdfs = InMemoryPendingPdfs()
        model = None
        cms = RecordingCms()
        intake_outbox = InMemoryOutbox()

        class whatsapp:
            @staticmethod
            def download_media(media, destination):
                destination.write_bytes(
                    b"\xff\xd8\xff\xe0 jpeg"
                    if destination.suffix.lower() in {".jpg", ".jpeg"}
                    else b"%PDF-1.4 statement"
                )

                return destination

    return FakeContainer


def document(
    *,
    message_id="msg-1",
    filename="Account Statement.pdf",
    mime="application/pdf",
    size=69_100,
):
    return InboundMessage(
        provider_message_id=message_id,
        sender=OWNER,
        type=MessageType.DOCUMENT,
        text="",
        media=MediaReference(
            handle="h", mime_type=mime, filename=filename, size_bytes=size
        ),
    )


class TestNothingArrivesUnrecorded:
    def test_an_accepted_document_is_registered_before_it_is_read(self, container):
        from app import __main__ as entry

        path = entry._take_in(container, document())

        assert path is not None
        assert len(container.cms.registered) == 1

        sent = container.cms.registered[0]
        assert sent["source"] == "whatsapp"
        assert sent["provider_message_id"] == "msg-1"
        assert sent["original_filename"] == "Account Statement.pdf"
        assert "failure_reason" not in sent

    def test_a_type_the_agent_will_not_touch_is_still_recorded(self, container):
        from app import __main__ as entry

        refused = document(
            filename="contract.docx",
            mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        )

        assert entry._take_in(container, refused) is None

        # The whole point. Under the old behaviour this incremented a counter and
        # the sender heard nothing — there was no way to answer "I sent you a
        # file this morning" except to say no record exists.
        assert len(container.cms.registered) == 1
        assert "Only PDF, JPG and PNG" in container.cms.registered[0]["failure_reason"]

    def test_a_document_too_large_is_still_recorded(self, container):
        from app import __main__ as entry

        assert entry._take_in(container, document(size=40 * 1024 * 1024)) is None

        assert len(container.cms.registered) == 1
        assert "Larger than the CMS accepts" in container.cms.registered[0]["failure_reason"]

    def test_a_file_that_is_not_what_it_claims_is_still_recorded(self, container):
        from app import __main__ as entry

        class Liar(container.whatsapp):
            @staticmethod
            def download_media(media, destination):
                destination.write_bytes(b"MZ\x90\x00 this is an executable")

                return destination

        container.whatsapp = Liar

        assert entry._take_in(container, document()) is None

        assert len(container.cms.registered) == 1
        assert "does not begin like" in container.cms.registered[0]["failure_reason"]

    def test_a_download_that_fails_is_still_recorded(self, container):
        from app import __main__ as entry

        class Broken(container.whatsapp):
            @staticmethod
            def download_media(media, destination):
                raise RuntimeError("provider returned 502")

        container.whatsapp = Broken

        with pytest.raises(RuntimeError):
            entry._take_in(container, document())

        # Registered on the way out, so a download failure is a visible Failed
        # document rather than a log line and a counter.
        assert len(container.cms.registered) == 1
        assert "could not be downloaded" in container.cms.registered[0]["failure_reason"]


class TestWhenTheCmsCannotBeReached:
    def test_the_document_is_held_and_does_not_proceed(self, container, tmp_path):
        from app import __main__ as entry

        container.cms = UnreachableCms()

        assert entry._take_in(container, document()) is None

        # Held, not lost.
        assert len(container.intake_outbox) == 1

        # And crucially not processed. A document the CMS has never heard of must
        # not be read, proposed, or acknowledged to the sender — that is exactly
        # the invisible state this replaces.
        held = container.intake_outbox.oldest()
        assert held.provider_message_id == "msg-1"
        assert held.path is not None

    def test_a_refusal_is_held_too(self, container):
        from app import __main__ as entry

        container.cms = UnreachableCms()

        entry._take_in(container, document(filename="x.docx", mime="text/plain"))

        held = container.intake_outbox.oldest()
        assert held.failure_reason is not None
        assert held.path is None

    def test_it_stops_accepting_rather_than_hoarding(self, container):
        from app import __main__ as entry

        container.cms = UnreachableCms()
        container.intake_outbox = InMemoryOutbox(capacity=2)

        for index in range(4):
            entry._take_in(container, document(message_id=f"msg-{index}"))

        # The cap is what makes holding honest. Past it the agent refuses, so a
        # long outage is loud instead of turning into a thousand-document queue
        # nobody knew about.
        assert len(container.intake_outbox) == 2

    def test_the_queue_drains_once_the_cms_returns(self, container):
        from app import __main__ as entry

        container.cms = UnreachableCms()
        entry._take_in(container, document(message_id="held-1"))

        assert len(container.intake_outbox) == 1

        container.cms = RecordingCms()
        entry._drain_outbox(container)

        assert len(container.intake_outbox) == 0
        assert container.cms.registered[0]["provider_message_id"] == "held-1"

    def test_a_failed_retry_leaves_the_document_held(self, container):
        from app import __main__ as entry

        container.cms = UnreachableCms()
        entry._take_in(container, document())

        entry._drain_outbox(container)

        # Still there, with the attempt counted. Deleting on a failed attempt
        # would lose exactly the document this table exists to protect.
        assert len(container.intake_outbox) == 1
        assert container.intake_outbox.oldest().attempts == 1
        assert container.intake_outbox.oldest().last_error

    def test_one_document_is_retried_per_pass(self, container):
        from app import __main__ as entry

        container.cms = UnreachableCms()

        for index in range(3):
            entry._take_in(container, document(message_id=f"msg-{index}"))

        container.cms = UnreachableCms()
        entry._drain_outbox(container)

        # A CMS that is down should not be hit with the whole queue at once.
        assert container.cms.attempts == 1

    def test_an_arrival_whose_file_vanished_is_still_registered(self, container, tmp_path):
        from app import __main__ as entry

        container.cms = UnreachableCms()
        entry._take_in(container, document())

        # Cleaned up, or the deployment rebuilt, between holding and retrying.
        (tmp_path / "msg-1.pdf").unlink(missing_ok=True)

        container.cms = RecordingCms()
        entry._drain_outbox(container)

        # A record saying a document arrived and could not be kept beats no
        # record at all — the sender still gets an answer.
        assert len(container.intake_outbox) == 0
        assert container.cms.registered[0]["provider_message_id"] == "msg-1"

    def test_draining_an_empty_queue_does_nothing(self, container):
        from app import __main__ as entry

        entry._drain_outbox(container)

        assert container.cms.registered == []
