"""Asking the sender for the one thing that was missing.

This is the only message this software has ever sent. Until now `MessageSender`
existed and nothing called it, which means the risk being introduced is not a
missed notification — it is a loop. A retried workflow, a restarted daemon or a
redelivered webhook turning one question into forty, overnight, from a business
number that WhatsApp can ban for exactly that, without appeal.

So the tests that matter here are about restraint, not delivery.
"""

from __future__ import annotations

from app.__main__ import _ask_for_missing_information, _first_notify
from app.whatsapp.notifications import missing_information


class FakeRun:
    def __init__(self, outputs):
        self.steps = [type("Step", (), {"output": o})() for o in outputs]


class FakeSender:
    def __init__(self, ok=True, raises=False):
        self.sent = []
        self._ok = ok
        self._raises = raises

    def send(self, message):
        if self._raises:
            raise RuntimeError("provider is down")

        self.sent.append(message)

        return type("Result", (), {"ok": self._ok, "error": None if self._ok else "refused"})()


class FakeContainer:
    def __init__(self, sender):
        self.message_sender = sender


class TestWhenToAsk:
    def test_it_asks_when_the_cms_says_to(self):
        sender = FakeSender()
        run = FakeRun([{"notify": {"reference": "DOC-1001", "required": "File number or CNIC"}}])

        _ask_for_missing_information(FakeContainer(sender), run, "923001234567")

        assert len(sender.sent) == 1
        assert "DOC-1001" in sender.sent[0].text

    def test_it_stays_silent_when_the_cms_does_not(self):
        """The default, and by far the common case.

        Almost every document is identified. A notifier that spoke unless told
        not to would message the owner about every CNIC photo they ever sent.
        """
        sender = FakeSender()

        _ask_for_missing_information(FakeContainer(sender), FakeRun([{"proposal_id": 7}]), "923001234567")

        assert sender.sent == []

    def test_a_null_notify_is_silence_not_a_message(self):
        # The CMS sends notify: null on every response after the first. Reading
        # that as "something to send" is how once becomes twice.
        sender = FakeSender()

        _ask_for_missing_information(FakeContainer(sender), FakeRun([{"notify": None}]), "923001234567")

        assert sender.sent == []

    def test_a_run_with_no_steps_sends_nothing(self):
        sender = FakeSender()

        _ask_for_missing_information(FakeContainer(sender), FakeRun([]), "923001234567")

        assert sender.sent == []


class TestNotSendingTwice:
    def test_the_decision_is_not_made_on_this_side(self):
        """The whole design, in one assertion.

        This module has no memory of what it has sent and needs none: the CMS
        stamps the row as it hands out the instruction, so a second response for
        the same document carries no instruction at all. Nothing here has to
        remember anything across a restart — which is exactly what a daemon is
        worst at.
        """
        sender = FakeSender()
        container = FakeContainer(sender)
        instruction = {"notify": {"reference": "DOC-1001", "required": "File number or CNIC"}}

        # First response carries it; every later one does not.
        _ask_for_missing_information(container, FakeRun([instruction]), "923001234567")
        _ask_for_missing_information(container, FakeRun([{"notify": None}]), "923001234567")
        _ask_for_missing_information(container, FakeRun([{"notify": None}]), "923001234567")

        assert len(sender.sent) == 1

    def test_a_provider_failure_does_not_become_a_retry_storm(self):
        # Swallowed, not raised. The document is already a proposal on the
        # Waiting page; failing the workflow would lose real work over a message,
        # and a raised exception here is what a retry loop is built from.
        sender = FakeSender(raises=True)

        _ask_for_missing_information(FakeContainer(sender), FakeRun([
            {"notify": {"reference": "DOC-1001", "required": "File number or CNIC"}}
        ]), "923001234567")

        assert sender.sent == []

    def test_a_refusal_is_reported_but_not_raised(self):
        sender = FakeSender(ok=False)

        _ask_for_missing_information(FakeContainer(sender), FakeRun([
            {"notify": {"reference": "DOC-1002", "required": "File number or CNIC"}}
        ]), "923001234567")

        assert len(sender.sent) == 1


class TestTheMessage:
    def test_it_asks_for_the_reference_back(self):
        """The constraint that makes a reply usable.

        A self-chat has no threading. When "35201-1234567-1" arrives an hour
        later, nothing in it says which document it answers, and the only ways to
        guess are "the newest" or "the oldest waiting" — both wrong the moment
        two documents are waiting, and being wrong files one client's bank
        statement onto another client's record.
        """
        message = missing_information("923001234567", "DOC-1001", "File number or CNIC")

        assert "DOC-1001" in message.text
        # The example shows the exact shape rather than describing it, because
        # somebody is reading this on a phone.
        assert "DOC-1001 35201-1234567-1" in message.text

    def test_it_offers_the_cms_as_well_as_a_reply(self):
        # Not everyone holding the intake phone is the person who can look up a
        # file number.
        message = missing_information("923001234567", "DOC-1001", "File number or CNIC")

        assert "Waiting for Information" in message.text

    def test_it_says_what_is_needed(self):
        message = missing_information("923001234567", "DOC-1001", "File number or CNIC")

        assert "File number or CNIC" in message.text

    def test_it_goes_to_the_number_that_sent_it(self):
        # The self-chat rule leaves exactly one possible recipient, and sending
        # anywhere else would be reaching outside the one conversation this
        # software is allowed to touch.
        message = missing_information("923049637232", "DOC-1001", "File number or CNIC")

        assert message.recipient == "923049637232"


class TestReadingTheInstruction:
    def test_it_finds_the_instruction_wherever_in_the_run_it_sits(self):
        run = FakeRun([{"proposal_id": 1}, {"notify": {"reference": "DOC-1009"}}])

        assert _first_notify(run)["reference"] == "DOC-1009"

    def test_a_malformed_output_is_ignored_rather_than_raising(self):
        # This reads a field absent from almost every response. An exception
        # here would fail a workflow whose real work already succeeded.
        for output in [None, "not a dict", {"notify": "not a dict"}, {}]:
            assert _first_notify(FakeRun([output])) is None
