"""TDD: apply_999_rejections moves claims to REJECTED on 999 AK5 R/E.

Plan: docs/superpowers/plans/2026-06-20-cyclone-workflow-automation.md
Task 3 (T3).
"""
from __future__ import annotations

from datetime import date, datetime, timezone

import pytest

from cyclone import db
from cyclone.db import Batch, Claim, ClaimState
from cyclone.inbox_state import apply_999_rejections
from cyclone.parsers.models import BatchSummary, Envelope
from cyclone.parsers.models_999 import (
    AcknowledgmentHeader,
    FunctionalGroupAck,
    ParseResult999,
    SetAcceptReject,
    SetFunctionalGroupResponse,
)


@pytest.fixture(autouse=True)
def _setup(tmp_path, monkeypatch):
    monkeypatch.setenv("CYCLONE_DB_URL", f"sqlite:///{tmp_path}/inbox.db")
    db._reset_for_tests()
    db.init_db()


def _seed_batch() -> None:
    with db.SessionLocal()() as s:
        s.add(Batch(
            id="B-1", kind="837p", input_filename="x.txt",
            parsed_at=datetime.now(timezone.utc),
            totals_json={"total_claims": 1},
            validation_json={"passed": True, "warnings": [], "errors": []},
            raw_result_json={"_": "stub"},
        ))
        s.commit()


def _add_claim(*, claim_id: str, pcn: str, state: ClaimState = ClaimState.SUBMITTED) -> None:
    _seed_batch()
    with db.SessionLocal()() as s:
        s.add(Claim(
            id=claim_id, batch_id="B-1",
            patient_control_number=pcn, state=state,
        ))
        s.commit()


def _make_999(*, claim_refs: list[tuple[str, str]]) -> ParseResult999:
    """claim_refs: list of (set_control_number, accept_code)."""
    set_responses = [
        SetFunctionalGroupResponse(
            ak2=AcknowledgmentHeader(
                functional_id_code="837", group_control_number=ref,
            ),
            set_control_number=ref,
            transaction_set_identifier="837",
            segment_errors=[],
            set_accept_reject=SetAcceptReject(code=code),  # type: ignore[arg-type]
        )
        for ref, code in claim_refs
    ]
    return ParseResult999(
        envelope=Envelope(
            sender_id="RECEIVER", receiver_id="SUBMITTER",
            control_number="000000001", transaction_date=date(2024, 1, 1),
            implementation_guide="005010X231A1",
        ),
        functional_group_acks=[
            FunctionalGroupAck(
                ak1=AcknowledgmentHeader(functional_id_code="HC", group_control_number="0001"),
                received_count=len(set_responses), accepted_count=0,
                rejected_count=len(set_responses), ack_code="R",
            ),
        ],
        set_responses=set_responses,
        summary=BatchSummary(
            input_file="x.txt", total_claims=len(set_responses),
            passed=0, failed=len(set_responses),
        ),
    )


def _lookup_factory(session):
    """Build a claim_lookup closure that uses the given session.

    The session must be the same one passed to apply_999_rejections so
    that the returned Claim is attached to that session's transaction.
    """
    def _lookup(pcn: str):
        return session.query(Claim).filter_by(patient_control_number=pcn).first()
    return _lookup


def test_rejection_moves_submitted_claim_to_rejected_state():
    _add_claim(claim_id="CLP-1", pcn="PCN-001")

    with db.SessionLocal()() as s:
        result = apply_999_rejections(
            s,
            _make_999(claim_refs=[("PCN-001", "R")]),
            claim_lookup=_lookup_factory(s),
        )
        s.commit()

    assert result.matched == ["CLP-1"]
    assert result.orphans == []
    with db.SessionLocal()() as s:
        # Migration 0014: composite PK — claim is in batch "B-1"; use
        # filter-by-id since the test has only one claim with this id.
        c = s.query(Claim).filter(Claim.id == "CLP-1").first()
        assert c.state == ClaimState.REJECTED
        assert c.rejected_at is not None
        assert "999" in (c.rejection_reason or "")


def test_accepted_set_leaves_claim_alone():
    _add_claim(claim_id="CLP-1", pcn="PCN-002")

    with db.SessionLocal()() as s:
        result = apply_999_rejections(
            s,
            _make_999(claim_refs=[("PCN-002", "A")]),
            claim_lookup=_lookup_factory(s),
        )
        s.commit()

    assert result.matched == []
    with db.SessionLocal()() as s:
        # Migration 0014: composite PK — see test_rejection_moves...
        c = s.query(Claim).filter(Claim.id == "CLP-1").first()
        assert c.state == ClaimState.SUBMITTED
        assert c.rejected_at is None
        assert c.rejection_reason is None


def test_unknown_pcn_recorded_as_orphan_not_error():
    with db.SessionLocal()() as s:
        result = apply_999_rejections(
            s,
            _make_999(claim_refs=[("PCN-NONEXISTENT", "R")]),
            claim_lookup=_lookup_factory(s),
        )
        s.commit()

    assert result.matched == []
    assert result.orphans == ["PCN-NONEXISTENT"]


def test_already_rejected_claim_is_idempotent():
    _add_claim(claim_id="CLP-1", pcn="PCN-003", state=ClaimState.REJECTED)

    with db.SessionLocal()() as s:
        result = apply_999_rejections(
            s,
            _make_999(claim_refs=[("PCN-003", "R")]),
            claim_lookup=_lookup_factory(s),
        )
        s.commit()

    assert result.matched == []   # no-op
    with db.SessionLocal()() as s:
        # Migration 0014: composite PK — see test_rejection_moves...
        c = s.query(Claim).filter(Claim.id == "CLP-1").first()
        assert c.state == ClaimState.REJECTED
