"""End-to-end ingest tests for SP28: 999 file → claim_acks rows.

Step 4.5 of the plan. Covers the two pass-joint paths:

* ``test_ingest_999_creates_link_rows_via_st02_join`` — D10 Pass 1
  (Batch.envelope.control_number == ST02). The fixture's ST02
  (``991102989``) matches a pre-seeded Batch's envelope so Pass 1
  resolves the claim by ``Claim.batch_id``. PCN mismatch on purpose.

* ``test_ingest_999_creates_link_rows_via_pcn_fallback`` — D10 Pass 2
  (Claim.patient_control_number). No Batch matches, so Pass 2 fires
  and resolves by PCN.

Both tests use the FastAPI test client to exercise the full
parse-999 → apply_999_acceptances → add_claim_ack chain end-to-end,
then verify the row exists in the DB and the response shape includes
the new ``claim_ack_links_count`` field.
"""
from __future__ import annotations

from datetime import datetime, timezone
from decimal import Decimal
from pathlib import Path

import pytest
from fastapi.testclient import TestClient

from cyclone import db
from cyclone.api import app
from cyclone.db import Batch, Claim, ClaimAck, ClaimState
from cyclone.store import store


GAINWELL_999 = (
    Path(__file__).parent / "fixtures" / "minimal_999_ik5_gainwell.txt"
)
ACCEPTED_999 = Path(__file__).parent / "fixtures" / "minimal_999.txt"


@pytest.fixture(autouse=True)
def clear_store():
    """Reset the module-level store before and after each test.

    The conftest's autouse fixtures set up the tmp DB; here we just
    make sure the in-memory store shim is clean too (no-op for the
    DB-backed store but matches the rest of the test suite's style).
    """
    with store._lock:
        store._batches.clear()
    yield
    with store._lock:
        store._batches.clear()


@pytest.fixture
def client() -> TestClient:
    return TestClient(app)


def _seed_batch_envelope(*, batch_id: str, envelope_control: str):
    """Insert one Batch row whose raw_result_json envelope carries
    ``envelope_control`` so D10 Pass 1 can resolve it via
    ``batch_envelope_index``."""
    with db.SessionLocal()() as s:
        b = Batch(
            id=batch_id,
            kind="837p",
            input_filename=f"{batch_id}.txt",
            parsed_at=datetime(2026, 7, 2, 12, 0, tzinfo=timezone.utc),
            raw_result_json={
                "envelope": {
                    "sender_id": "SUBMITTER",
                    "receiver_id": "RECEIVER",
                    "control_number": envelope_control,
                    "transaction_date": "2026-07-02",
                },
            },
        )
        s.add(b)
        s.commit()


def _seed_claim(*, claim_id: str, batch_id: str, patient_control_number: str):
    """Insert one Claim row tied to a pre-seeded batch."""
    with db.SessionLocal()() as s:
        c = Claim(
            id=claim_id,
            batch_id=batch_id,
            patient_control_number=patient_control_number,
            charge_amount=Decimal("100.00"),
            state=ClaimState.SUBMITTED,
        )
        s.add(c)
        s.commit()


def test_ingest_999_creates_link_rows_via_st02_join(client: TestClient):
    """D10 Pass 1: 999's AK2-2 set_control_number matches a pre-seeded
    Batch's envelope.control_number, so the auto-linker resolves
    the claim by Claim.batch_id (NOT by PCN).

    PCN is intentionally non-matching to prove Pass 1 won — if Pass 2
    had fired instead, the PCN lookup would have returned no claim
    and the row would have been an orphan.
    """
    # Seed a Batch whose envelope control number == the Gainwell
    # 999's AK2-2 set_control_number ("991102989").
    _seed_batch_envelope(batch_id="B-GAINWELL", envelope_control="991102989")
    # Seed a Claim in that batch, but with a PCN that does NOT match
    # "991102989" — Pass 2 must not fire.
    _seed_claim(
        claim_id="CLM-GW-1", batch_id="B-GAINWELL",
        patient_control_number="t991102989o1cA",
    )

    text = GAINWELL_999.read_text()
    resp = client.post(
        "/api/parse-999",
        files={"file": ("minimal_999_ik5_gainwell.txt", text, "text/plain")},
        headers={"Accept": "application/json"},
    )
    assert resp.status_code == 200, resp.text
    body = resp.json()
    # New response field from spec §4.
    assert "claim_ack_links_count" in body["ack"]
    assert body["ack"]["claim_ack_links_count"] == 1

    # The DB has exactly one ClaimAck row for this ingest — claim_id
    # resolves to CLM-GW-1 (Pass 1 win), set_control_number matches
    # the AK2-2, set_accept_reject_code is "A" (IK5 accepted).
    with db.SessionLocal()() as s:
        rows = (
            s.query(ClaimAck)
            .filter(ClaimAck.ack_kind == "999")
            .all()
        )
        assert len(rows) == 1
        assert rows[0].claim_id == "CLM-GW-1"
        assert rows[0].set_control_number == "991102989"
        assert rows[0].set_accept_reject_code == "A"
        assert rows[0].ak2_index == 0
        assert rows[0].linked_by == "auto"


def test_ingest_999_creates_link_rows_via_pcn_fallback(client: TestClient):
    """D10 Pass 2: no Batch's envelope matches the AK2-2, so the
    auto-linker falls back to Claim.patient_control_number."""
    # Seed a Batch whose envelope control number does NOT match.
    _seed_batch_envelope(batch_id="B-OTHER", envelope_control="999999999")
    # Seed a Claim in some other batch whose PCN equals the 999's
    # set_control_number ("0001" for the minimal accepted fixture).
    _seed_claim(
        claim_id="CLM-PCN-1", batch_id="B-OTHER",
        patient_control_number="0001",
    )

    text = ACCEPTED_999.read_text()
    resp = client.post(
        "/api/parse-999",
        files={"file": ("minimal_999.txt", text, "text/plain")},
        headers={"Accept": "application/json"},
    )
    assert resp.status_code == 200, resp.text
    body = resp.json()
    assert body["ack"]["claim_ack_links_count"] == 1

    with db.SessionLocal()() as s:
        rows = (
            s.query(ClaimAck)
            .filter(ClaimAck.ack_kind == "999")
            .all()
        )
        assert len(rows) == 1
        assert rows[0].claim_id == "CLM-PCN-1"
        assert rows[0].set_control_number == "0001"