"""Routing a classified document to the workflow that handles it.

The router itself is a lookup, so most of what is worth testing is the wiring
around it — that the inbox now decides from a classification rather than an
`if`, and that the two guarantees the old `if` carried survived the change:

  * a captioned bank statement is never opened, and
  * a document nothing recognises still reaches the reading workflow.

The second is the one a refactor quietly breaks. A router that returns nothing
for `unknown` would leave every unrecognised document nowhere at all, and the
symptom is silence.
"""

from __future__ import annotations

from pathlib import Path

import pytest

from app.documents import registry, router
from app.documents.classifier import UNKNOWN, Classification, Method
from app.workflow import catalogue
from app.workflow.bank_statement_intake import BANK_STATEMENT_INTAKE
from app.workflow.document_intake import DOCUMENT_INTAKE


class TestRouting:
    def test_a_bank_statement_goes_to_the_workflow_that_never_reads_it(self):
        destination = router.route(Classification("bank_statement", confidence=0.95))

        assert destination.workflow == BANK_STATEMENT_INTAKE.name
        assert destination.reads_document is False

    def test_a_type_with_no_workflow_of_its_own_goes_to_the_reading_workflow(self):
        # A receipt is an attachment: nothing to extract, nothing to specialise.
        # The default handles it adequately, which is what a default is for.
        destination = router.route(Classification("receipt", confidence=0.9))

        assert destination.workflow == DOCUMENT_INTAKE.name

    def test_a_type_that_has_claimed_a_workflow_goes_to_it(self):
        for slug, expected in (
            ("salary_slip", "salary_slip_intake"),
            ("cnic_front", "identity_intake"),
            ("property_document", "property_intake"),
            ("tax_certificate", "tax_document_intake"),
        ):
            assert router.route(Classification(slug, confidence=0.9)).workflow == expected

    def test_an_unknown_document_still_goes_somewhere(self):
        """The failure a refactor introduces and nothing reports.

        A document nothing recognised is the ordinary case the reading workflow
        exists for. Routing it to nothing would lose it silently.
        """
        destination = router.route(
            Classification(UNKNOWN, reason="nothing matched", method=Method.NONE)
        )

        assert destination.workflow == DOCUMENT_INTAKE.name

    def test_an_unknown_document_carries_the_reason_forward(self):
        destination = router.route(Classification(UNKNOWN, reason="scored too close"))

        assert "scored too close" in destination.reason

    def test_a_type_the_registry_does_not_have_falls_back_rather_than_raising(self):
        # Not reachable from the classifier, which only returns registry slugs.
        # Handled anyway: the alternative is a KeyError inside a daemon loop
        # over somebody's document.
        destination = router.route(Classification("a_type_from_the_future", confidence=0.99))

        assert destination.workflow == DOCUMENT_INTAKE.name
        assert "not a type this agent knows" in destination.reason

    def test_sensitivity_travels_with_the_route(self):
        assert router.route(Classification("bank_statement", confidence=0.9)).sensitive is True
        assert router.route(Classification("receipt", confidence=0.9)).sensitive is False


class TestEveryRouteIsReal:
    def test_every_workflow_named_in_the_registry_exists(self):
        """A type routed to a workflow nobody defined fails when a real document
        arrives, which is the worst possible time to find out.

        Checked against the catalogue rather than a list written here, so
        registering a workflow and making it reachable are the same act.
        """
        for name in router.workflows():
            assert catalogue.get(name) is not None, f"'{name}' is routed to but not defined"

    def test_every_defined_workflow_is_reachable(self):
        """The other direction, and the one that goes stale quietly.

        A workflow nothing routes to is dead code that still passes its own
        tests. `document_intake` is exempt: it is the default, so it is reached
        by everything the registry does not specialise.
        """
        routed = set(router.workflows())

        for name in catalogue.names():
            if name == DOCUMENT_INTAKE.name:
                continue

            assert name in routed, f"'{name}' is defined but nothing routes to it"

    def test_every_filing_type_routes_somewhere(self):
        for filing in registry.filing_types():
            destination = router.route(Classification(filing.slug, confidence=0.9))

            assert destination.workflow


class TestTheInboxRoutesRatherThanBranching:
    """The rewire, exercised through the real `_process_inbox`."""

    def _container(self, tmp_path):
        from app.whatsapp.inbox import InboxFilter
        from app.intake import InMemoryOutbox
        from app.whatsapp.pending import InMemoryPendingPdfs
        from app.whatsapp.webhook import InboundQueue
        from tests.intake_support import RecordingCms

        started: list = []
        reads: list = []

        class FakeContainer:
            inbound_queue = InboundQueue()
            incoming_dir = tmp_path
            inbox = InboxFilter()
            pending_pdfs = InMemoryPendingPdfs()
            # No classification model. The stage is off unless one is
            # configured, which is the state of every deployment today.
            model = None

            # Since ADR-0010 a document does not proceed until the CMS has a
            # record of it, so a container without these processes nothing.
            cms = RecordingCms()
            intake_outbox = InMemoryOutbox()

            class whatsapp:
                @staticmethod
                def download_media(media, destination):
                    # Written to match the suffix, because the envelope check
                    # rejects a file that does not begin the way its type must.
                    destination.write_bytes(
                        b"\xff\xd8\xff\xe0 jpeg"
                        if destination.suffix.lower() in {".jpg", ".jpeg"}
                        else b"%PDF-1.4 statement"
                    )

                    return destination

            class engine:
                @staticmethod
                def start(workflow, context):
                    started.append((workflow.name, context))

                    class Run:
                        id = "r1"

                        class state:
                            value = "awaiting_approval"

                    return Run()

        return FakeContainer, started, reads

    def _message(
        self,
        *,
        text="",
        filename="statement.jpg",
        message_id="evo-1",
        mime_type="image/jpeg",
    ):
        """A photograph by default.

        These tests are about what the router does with a caption, and only an
        image still has one that is read: a PDF's caption is deliberately
        ignored, because a forwarded PDF never has one to begin with. The PDF
        path is covered in TestAPdfWaitsToBeNamed.
        """
        from app.whatsapp.messages import InboundMessage, MediaReference, MessageType

        return InboundMessage(
            provider_message_id=message_id,
            sender="923049637232",
            type=MessageType.DOCUMENT,
            text=text,
            media=MediaReference(handle="h", mime_type=mime_type, filename=filename),
        )

    def test_a_captioned_bank_statement_still_reaches_the_forwarding_workflow(self, tmp_path):
        from app import __main__ as entry

        container, started, _ = self._container(tmp_path)
        container.inbound_queue.put(self._message(text="file 1420"))

        entry._process_inbox(container)

        # The V1 convention: a caption naming a client and no type is a
        # forwarded bank statement. Preserved through the rewire.
        assert [name for name, _ in started] == [BANK_STATEMENT_INTAKE.name]

    def test_a_caption_naming_the_type_reaches_the_same_workflow(self, tmp_path):
        from app import __main__ as entry

        container, started, _ = self._container(tmp_path)
        container.inbound_queue.put(self._message(text="Bank Statement for file 1420"))

        entry._process_inbox(container)

        assert [name for name, _ in started] == [BANK_STATEMENT_INTAKE.name]

    def test_a_document_naming_nothing_still_reaches_the_reader(self, tmp_path):
        from app import __main__ as entry

        container, started, _ = self._container(tmp_path)
        container.inbound_queue.put(self._message(text="here you go", filename="scan001.pdf"))

        entry._process_inbox(container)

        assert [name for name, _ in started] == [DOCUMENT_INTAKE.name]

    def test_a_salary_slip_named_in_the_caption_reaches_its_own_workflow(self, tmp_path):
        from app import __main__ as entry

        container, started, _ = self._container(tmp_path)
        container.inbound_queue.put(self._message(text="Salary Slip July"))

        entry._process_inbox(container)

        assert [name for name, _ in started] == ["salary_slip_intake"]

    def test_the_workflow_is_told_what_the_document_already_is(self, tmp_path):
        """An extraction workflow must not re-decide the type.

        Re-classifying inside the workflow would let it disagree with the router
        that chose it, and the document would be filed as something other than
        what it was routed as.
        """
        from app import __main__ as entry

        container, started, _ = self._container(tmp_path)
        container.inbound_queue.put(self._message(text="Salary Slip for file 1420"))

        entry._process_inbox(container)

        _, context = started[0]

        assert context["document_type"] == "salary_slip"
        assert context["classification"]["method"] == "caption"
        # The caption named a client too, and that travels with it — a person
        # saying who it belongs to beats anything printed on the page.
        assert context["file_number"] == "1420"

    def test_classification_downloads_nothing(self, tmp_path):
        """The ordering that makes `reads_document` enforceable.

        If the pipeline had to fetch and read a document to decide what it is,
        it would have read every bank statement before discovering it must not.
        Caption and filename are both already in the message.
        """
        from app import __main__ as entry

        container, started, _ = self._container(tmp_path)
        container.inbound_queue.put(self._message(text="Bank Statement", filename="s.pdf"))

        entry._process_inbox(container)

        # One file on disk — written by the forwarding workflow's own download,
        # after the routing decision, not before it.
        assert len(list(tmp_path.iterdir())) == 1
