"""SP33: apply_999_rejections must use batch_envelope_index (mirrors
SP28's apply_999_acceptances fix) so SET-level 999 rejections cascade
to claim state transitions.

The pre-SP33 bug: ``claim_lookup(sr.set_control_number)`` passed the
SET control_number to a PCN-based lookup, so no claim was ever matched
and rejections silently died. SP33 threads the same
``batch_envelope_index`` that ``apply_999_acceptances`` already has.
"""
from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import MagicMock

from cyclone.db import ClaimState
from cyclone.inbox_state import apply_999_rejections


def _mk_set_response(code: str = "R", scn: str = "991102994"):
    """Build a stand-in for ``ParseResult999.set_responses[0]``."""
    return SimpleNamespace(
        set_accept_reject=SimpleNamespace(code=code),
        set_control_number=scn,
        segment_errors=[],
    )


def _mk_claim(cid: str = "claim-x", state=ClaimState.SUBMITTED):
    c = MagicMock()
    c.id = cid
    c.state = state
    return c


def test_set_level_rejection_cascades_via_envelope_index():
    """When the SET-level 999 has set_control_number=991102994 and the
    batch_envelope_index maps it to a list of 3 claim_ids, all 3 should
    transition to REJECTED.
    """
    session = MagicMock()
    claims = [_mk_claim(f"c-{i}") for i in range(3)]
    session.query.return_value.filter.return_value.all.return_value = claims

    parsed = SimpleNamespace(set_responses=[_mk_set_response(code="R", scn="991102994")])
    index = {"991102994": [c.id for c in claims]}

    result = apply_999_rejections(
        session, parsed,
        claim_lookup=lambda _pcn: None,
        batch_envelope_index=index,
    )

    assert sorted(result.matched) == sorted(c.id for c in claims)
    assert result.orphans == []
    session.commit.assert_called_once()
    for c in claims:
        # ClaimState.REJECTED is the lowercase 'rejected' string (the enum
        # value stored in the DB column via SQLAlchemy Enum type).
        assert c.state == ClaimState.REJECTED


def test_set_level_rejection_without_index_still_uses_legacy_lookup():
    """Backwards compat: when batch_envelope_index is None, the function
    must fall back to claim_lookup (existing behavior preserved).
    """
    session = MagicMock()
    parsed = SimpleNamespace(set_responses=[_mk_set_response(code="R", scn="991102994")])
    legacy_claim = _mk_claim("legacy-claim")

    result = apply_999_rejections(
        session, parsed,
        claim_lookup=lambda scn: legacy_claim if scn == "991102994" else None,
    )

    assert result.matched == ["legacy-claim"]


def test_no_envelope_index_no_match_becomes_orphan():
    """Empty index + empty legacy lookup => the SET becomes an orphan."""
    session = MagicMock()
    parsed = SimpleNamespace(set_responses=[_mk_set_response(code="R", scn="991102999")])

    result = apply_999_rejections(
        session, parsed,
        claim_lookup=lambda _pcn: None,
        batch_envelope_index={},  # empty index
    )
    assert result.matched == []
    assert "991102999" in result.orphans


def test_accepted_999_does_not_reject():
    """An 'A' code must not transition any claim to REJECTED."""
    session = MagicMock()
    parsed = SimpleNamespace(set_responses=[_mk_set_response(code="A", scn="991102994")])

    result = apply_999_rejections(
        session, parsed,
        claim_lookup=lambda _pcn: None,
        batch_envelope_index={"991102994": ["claim-x"]},
    )
    assert result.matched == []
    session.query.assert_not_called()
    session.commit.assert_not_called()


def test_already_rejected_is_idempotent():
    """Pre-REJECTED claims in the index are left alone (no second commit)."""
    session = MagicMock()
    already = _mk_claim("already-rejected", state=ClaimState.REJECTED)
    session.query.return_value.filter.return_value.all.return_value = [already]

    parsed = SimpleNamespace(set_responses=[_mk_set_response(code="R", scn="991102994")])
    result = apply_999_rejections(
        session, parsed,
        claim_lookup=lambda _pcn: None,
        batch_envelope_index={"991102994": ["already-rejected"]},
    )
    assert result.matched == []
    assert already.state == ClaimState.REJECTED  # untouched
