"""The document registry — one table, and adding a type is one entry in it.

Most of these are structural: they assert the registry is internally consistent
and agrees with the CMS, because those are the two ways it can be wrong without
anything failing until a real document arrives.

The refinement tests are the interesting ones. A refinement must never compete
with the filing type it belongs to, and the reason is concrete: `sale deed` is a
marker for `property_document`, so a `sale_deed` type scored against the same
text at the same time would tie, the margin rule would refuse both, and a
document that classified correctly for months would start coming back unknown.
"""

from __future__ import annotations

import pytest

from app.documents import registry
from app.ocr.classification import UNKNOWN, classify
from app.runtime import preflight


class TestTheRegistryIsConsistent:
    def test_every_filing_slug_is_unique(self):
        slugs = [t.slug for t in registry.filing_types()]

        assert len(slugs) == len(set(slugs))

    def test_every_refinement_files_as_a_type_that_exists(self):
        """The one way a refinement can be quietly useless.

        A refinement whose `files_as` names nothing would classify a document
        and then submit a filing type the CMS has never heard of.
        """
        filing = set(registry.filing_slugs())

        for refinement in registry.REFINEMENTS:
            assert refinement.files_as in filing, refinement.slug

    def test_no_refinement_collides_with_a_filing_slug(self):
        # Both appear in a proposal, and a reviewer reading "property_document"
        # should never have to wonder which of the two it meant.
        filing = set(registry.filing_slugs())

        for refinement in registry.REFINEMENTS:
            assert refinement.slug not in filing, refinement.slug

    def test_every_refinement_carries_at_least_one_marker(self):
        # A refinement with no markers can never be chosen, so it is a line of
        # config that looks like a feature and is not.
        for refinement in registry.REFINEMENTS:
            assert refinement.markers, refinement.slug

    def test_other_is_offered_for_filing_but_never_classified_into(self):
        """`other` is a decision a person makes, not a recognition.

        It must exist as a filing type — a reviewer needs somewhere to put a
        document the system does not know — but it must carry no markers, or an
        empty document would score against it and win.
        """
        other = registry.get("other")

        assert other is not None
        assert other.markers == ()
        assert "other" not in registry.markers()

    def test_a_type_that_must_not_be_read_says_so(self):
        # The property that keeps ADR-0002's guarantee out of every workflow's
        # memory: a bank statement is never opened, and that is recorded once.
        assert registry.get("bank_statement").reads_document is False
        assert registry.get("salary_slip").reads_document is True


class TestRefinement:
    def test_it_names_what_a_property_document_actually_is(self):
        found = registry.refine("property_document", "SALE DEED\nPlot No 42, Khasra 118")

        assert found is not None
        assert found.slug == "sale_deed"
        assert found.label == "Sale Deed"

    def test_it_tells_a_rent_agreement_from_a_sale_deed(self):
        found = registry.refine("property_document", "LEASE AGREEMENT between the parties")

        assert found is not None
        assert found.slug == "rent_agreement"

    def test_it_returns_nothing_when_nothing_more_precise_can_be_said(self):
        # The common case, and not a failure. "Property document" is a complete
        # answer when the text does not say which kind.
        assert registry.refine("property_document", "Mutation record, Registrar office") is None

    def test_a_type_with_no_refinements_refines_to_nothing(self):
        assert registry.refine("utility_bill", "K-ELECTRIC Bill Month June") is None

    def test_refining_never_changes_the_filing_type(self):
        """The guarantee that makes fine classification safe.

        Whatever the refinement, the document is still filed under the coarse
        type the reviewer's dropdown and the client's filters know about.
        """
        for refinement in registry.REFINEMENTS:
            assert registry.get(refinement.files_as) is not None


class TestTheRuleEngineIsUnchangedByTheRegistry:
    """Phase 1 moved the markers; it must not have moved a decision.

    These duplicate assertions in test_classification.py on purpose. That file
    tests the classifier; this one tests that relocating its data into the
    registry left it alone — and if the two ever disagree, this says which
    change caused it.
    """

    @pytest.mark.parametrize(
        "text,expected",
        [
            ("SALARY SLIP\nGross Salary 150,000\nNet Pay 138,000", "salary_slip"),
            ("STATEMENT OF ACCOUNT\nClosing Balance 45,000\nOpening Balance 30,000", "bank_statement"),
            ("NATIONAL IDENTITY CARD\nNADRA\nIdentity Number 35202-1234567-1", "cnic_front"),
            ("FEDERAL BOARD OF REVENUE\nNotice u/s 114\nCommissioner", "fbr_notice"),
        ],
    )
    def test_known_documents_still_classify_the_same_way(self, text, expected):
        assert classify(text).document_type == expected

    def test_a_document_that_is_nothing_is_still_unknown(self):
        assert classify("Dear Sir, please find attached the file. Regards.").document_type is UNKNOWN

    def test_adding_specific_types_did_not_break_the_general_one(self):
        """The regression the refinement design exists to prevent.

        A sale deed is a property document. Before refinements, `sale deed` was
        a marker for `property_document` and nothing else; a `sale_deed` filing
        type would have tied with it and produced `unknown`.
        """
        result = classify("SALE DEED\nPlot No 42\nRegistrar of Properties")

        assert result.document_type == "property_document"
        assert result.document_type != UNKNOWN


class TestThePreflightAgreementCheck:
    """The two vocabularies are separately deployed and can disagree."""

    class _Cms:
        def __init__(self, offered):
            self._offered = offered

        def document_types(self):
            return [{"value": slug, "label": slug} for slug in self._offered]

    def test_an_installation_with_every_type_passes(self):
        check = preflight.check_document_types(self._Cms(registry.filing_slugs()))

        assert check.outcome is preflight.Outcome.OK

    def test_a_missing_type_degrades_and_names_it(self):
        offered = [s for s in registry.filing_slugs() if s != "tax_certificate"]

        check = preflight.check_document_types(self._Cms(offered))

        # Degraded, not fatal: one missing type breaks the documents that
        # classify as it, and refusing to start would break all the others too.
        assert check.outcome is preflight.Outcome.DEGRADED
        assert "tax_certificate" in check.detail

    def test_extra_types_on_the_cms_side_are_not_a_problem(self):
        # A person can file anything by hand that this never recognises.
        check = preflight.check_document_types(
            self._Cms([*registry.filing_slugs(), "something_a_human_added"])
        )

        assert check.outcome is preflight.Outcome.OK

    def test_an_unreadable_list_degrades_rather_than_claiming_agreement(self):
        class Broken:
            def document_types(self):
                raise RuntimeError("HTTP 500")

        check = preflight.check_document_types(Broken())

        assert check.outcome is preflight.Outcome.DEGRADED
        assert "unverified" in check.detail
