from __future__ import annotations

import pytest

from generate.construction_affordances import (
    ConstructionProposal,
    all_diagnostic_families,
    lookup_by_organ,
    lookup_by_relation_type,
    lookup_family,
    make_proposal,
    propose_construction,
)
from generate.kernel_facts import SourceSpan


def test_catalog_entries_are_diagnostic_only_and_serving_forbidden() -> None:
    families = all_diagnostic_families()
    assert len(families) == 4
    for family in families:
        assert family.diagnostic_only is True
        assert family.serving_allowed is False


def test_catalog_ordering_is_deterministic() -> None:
    families = all_diagnostic_families()
    ids = [f.family_id for f in families]
    assert ids == sorted(ids)
    assert ids == [
        "binding.quantity_entity",
        "partition.percent_partition",
        "proportional_change.decrease_to_fraction",
        "state_change.unary_delta",
    ]


def test_lookups_correctness() -> None:
    quantity_entity = lookup_family("binding.quantity_entity")
    assert quantity_entity is not None
    assert lookup_by_organ("quantity_entity_binding") is quantity_entity
    assert lookup_by_relation_type("quantity_entity") is quantity_entity

    fraction_family = lookup_family("proportional_change.decrease_to_fraction")
    assert fraction_family is not None
    assert lookup_by_organ("fraction_decrease") is fraction_family
    assert lookup_by_relation_type("decrease_to_fraction") is fraction_family

    partition_family = lookup_family("partition.percent_partition")
    assert partition_family is not None
    assert lookup_by_organ("percent_partition") is partition_family
    assert lookup_by_relation_type("percent_of") is partition_family

    unary_delta = lookup_family("state_change.unary_delta")
    assert unary_delta is not None
    assert lookup_by_organ("unary_delta_transition") is unary_delta
    assert lookup_by_relation_type("unary_delta") is unary_delta

    assert lookup_family("invalid_family_id") is None
    assert lookup_by_organ("invalid_organ") is None
    assert lookup_by_relation_type("invalid_relation") is None


@pytest.mark.parametrize(
    ("family_id", "relation_type", "candidate_organ", "required_roles"),
    (
        (
            "binding.quantity_entity",
            "quantity_entity",
            "quantity_entity_binding",
            {
                "quantity",
                "entity",
                "quantity_kind",
                "provenance_span",
                "local_binding_relation",
            },
        ),
        (
            "proportional_change.decrease_to_fraction",
            "decrease_to_fraction",
            "fraction_decrease",
            {"base_quantity", "scale", "state_entity", "transition"},
        ),
        (
            "partition.percent_partition",
            "percent_of",
            "percent_partition",
            {"whole", "part", "scale"},
        ),
        (
            "state_change.unary_delta",
            "unary_delta",
            "unary_delta_transition",
            {"action_cue", "delta_quantity", "changed_object", "direction"},
        ),
    ),
)
def test_propose_construction_is_preassessment_and_catalog_backed(
    family_id: str,
    relation_type: str,
    candidate_organ: str,
    required_roles: set[str],
) -> None:
    span = SourceSpan("surface cue", 0, 11)

    proposal = propose_construction(family_id, (span,))

    assert proposal.family_id == family_id
    assert proposal.relation_type == relation_type
    assert proposal.candidate_organ == candidate_organ
    assert proposal.evidence_spans == (span,)
    assert proposal.status == "proposed"
    assert proposal.missing_roles == ()
    assert proposal.active_hazards == ()
    assert proposal.diagnostic_only is True
    assert proposal.serving_allowed is False
    assert {role.role for role in proposal.role_obligations if role.required} == required_roles


@pytest.mark.parametrize(
    "family_id",
    (
        "binding.quantity_entity",
        "proportional_change.decrease_to_fraction",
        "partition.percent_partition",
        "state_change.unary_delta",
    ),
)
def test_make_proposal_rejects_migrated_proposal_first_families(
    family_id: str,
) -> None:
    span = SourceSpan("surface cue", 0, 11)

    with pytest.raises(
        ValueError,
        match=rf"{family_id} is proposal-first; use propose_construction before assessment",
    ):
        make_proposal(
            family_id=family_id,
            evidence_spans=(span,),
            assessment_runnable=True,
            missing_roles=(),
            active_hazards=(),
        )


def test_make_proposal_still_rejects_unknown_family_ids() -> None:
    span = SourceSpan("surface cue", 0, 11)

    with pytest.raises(KeyError):
        propose_construction("invalid_family_id", (span,))


def test_construction_proposal_cannot_carry_assessment_authority() -> None:
    span = SourceSpan("test text", 0, 9)
    ConstructionProposal(
        family_id="proportional_change.decrease_to_fraction",
        relation_type="decrease_to_fraction",
        candidate_organ="fraction_decrease",
        evidence_spans=(span,),
        status="proposed",
    )

    for status in ("partial", "closed", "refused"):
        with pytest.raises(ValueError, match="must remain 'proposed'"):
            ConstructionProposal(
                family_id="proportional_change.decrease_to_fraction",
                relation_type="decrease_to_fraction",
                candidate_organ="fraction_decrease",
                evidence_spans=(span,),
                status=status,  # type: ignore[arg-type]
            )

    assert "missing_roles" not in ConstructionProposal.__dataclass_fields__
    assert "active_hazards" not in ConstructionProposal.__dataclass_fields__

    with pytest.raises(TypeError, match="unexpected keyword argument 'missing_roles'"):
        ConstructionProposal(
            family_id="proportional_change.decrease_to_fraction",
            relation_type="decrease_to_fraction",
            candidate_organ="fraction_decrease",
            evidence_spans=(span,),
            status="proposed",
            missing_roles=("base_quantity_unbound",),
        )
