"""Tests for GET /api/claims/{claim_id} (SP4 Task 2).

The endpoint is a thin wrapper over
``CycloneStore.get_claim_detail`` (added in Task 1, commit 423415a),
which already returns the spec-shaped dict or ``None``. The endpoint's
job is therefore narrow:

  * call the store,
  * translate ``None`` to a 404 with ``{"error": "Not found",
    "detail": "..."}``,
  * surface the dict verbatim on a hit.

These tests pin the endpoint contract: response shape on 200, the 404
shape (matching the existing ``/api/remittances/{id}`` convention),
and two payload-level checks the rest of the SP4 work depends on —
``stateHistory`` surfaces ``manual_match`` events, and a paired claim's
``matchedRemittance`` block is populated.
"""
from __future__ import annotations

from pathlib import Path

import pytest
from fastapi.testclient import TestClient

from cyclone.api import app
from cyclone.store import store as global_store

FIXTURE_837 = Path(__file__).parent / "fixtures" / "co_medicaid_837p.txt"
FIXTURE_835 = Path(__file__).parent / "fixtures" / "co_medicaid_835.txt"


@pytest.fixture(autouse=True)
def clear_store():
    with global_store._lock:
        global_store._batches.clear()
    yield
    with global_store._lock:
        global_store._batches.clear()


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


@pytest.fixture
def seeded_store():
    """One parsed 837P fixture batch — drives the list endpoint."""
    text_837 = FIXTURE_837.read_text()
    c = TestClient(app)
    c.post(
        "/api/parse-837",
        files={"file": ("x.txt", text_837, "text/plain")},
        headers={"Accept": "application/json"},
    )
    return c


@pytest.fixture
def seeded_store_with_pair():
    """837 + 835 fixture pair, no auto-match (different PCNs in each).

    The 837 subscriber ``member_id`` values (``P060946``, ``H582447``)
    intentionally don't match the 835 ``payer_claim_control_number``
    values (``t991102986o1c1d``, ``t991102986o1c2d``), so the auto-
    matcher in ``reconcile.run`` leaves both sides in the unmatched
    bucket — giving us a clean manual-match target.
    """
    text_837 = FIXTURE_837.read_text()
    text_835 = FIXTURE_835.read_text()
    c = TestClient(app)
    c.post(
        "/api/parse-837",
        files={"file": ("x.txt", text_837, "text/plain")},
        headers={"Accept": "application/json"},
    )
    c.post(
        "/api/parse-835",
        files={"file": ("era.txt", text_835, "text/plain")},
        headers={"Accept": "application/json"},
    )
    return c


JSON = {"Accept": "application/json"}


def test_get_claim_detail_happy_path(seeded_store):
    """A real claim returns 200 with all spec-mandated top-level keys."""
    cid = seeded_store.get("/api/claims", headers=JSON).json()["items"][0]["id"]
    resp = seeded_store.get(f"/api/claims/{cid}")
    assert resp.status_code == 200, resp.text
    body = resp.json()
    # All keys required by the spec §3.2 (excluding only the optional
    # `matchedRemittance` shape — that's covered in
    # `test_get_claim_detail_matched_remit_inlines_summary`).
    for k in (
        "id", "batchId", "state", "stateLabel", "billedAmount",
        "patientName", "providerNpi", "providerName", "payerName",
        "payerId", "submissionDate", "serviceDateFrom", "serviceDateTo",
        "parsedAt", "diagnoses", "serviceLines", "parties",
        "validation", "rawSegments", "matchedRemittance", "stateHistory",
    ):
        assert k in body, f"missing key {k!r}"
    # Spec §3.2: matchedRemittance is `null` for an unpaired claim
    # (the dominant UI render path). The matched-populated case is
    # covered separately in
    # test_get_claim_detail_matched_remit_inlines_summary.
    assert body["matchedRemittance"] is None
    assert body["id"] == cid
    # billedAmount is a number (not a Decimal string).
    assert isinstance(body["billedAmount"], (int, float))
    # stateHistory must contain the claim_submitted event seeded at
    # 837 ingest time — this is the baseline other history events are
    # appended to.
    kinds = [ev["kind"] for ev in body["stateHistory"]]
    assert "claim_submitted" in kinds, kinds


def test_get_claim_detail_404_for_missing(client):
    """Missing claim returns 404 (not 500) with the same envelope as
    ``get_remittance`` so the UI can render a single 404 path."""
    resp = client.get("/api/claims/DOES-NOT-EXIST")
    assert resp.status_code == 404
    body = resp.json()
    # FastAPI wraps the HTTPException(detail=dict) under .detail.
    detail = body.get("detail", body)
    assert detail.get("error") == "Not found"
    assert "DOES-NOT-EXIST" in detail.get("detail", "")


def test_get_claim_detail_state_history_includes_manual_match(seeded_store_with_pair):
    """After /api/reconciliation/match, stateHistory surfaces manual_match.

    The fixture pair leaves both sides in the unmatched bucket (837
    subscriber member_ids don't match 835 PCNs), so the manual-match
    API has clean work to do. After matching, the claim's
    ``stateHistory`` must include both the baseline ``claim_submitted``
    event (from 837 ingest) and the new ``manual_match`` event (from
    the match call), and the manual_match event must carry the
    ``remittanceId`` it was paired with.
    """
    unmatched = seeded_store_with_pair.get(
        "/api/reconciliation/unmatched", headers=JSON,
    ).json()
    if not unmatched["claims"] or not unmatched["remittances"]:
        pytest.fail(
            "co_medicaid_837p.txt + co_medicaid_835.txt fixtures no longer "
            "produce an unmatched pair; matched-remit tests need updating "
            "(see seeded_store_with_pair docstring for the expected invariant)"
        )
    claim_id = unmatched["claims"][0]["id"]
    remit_id = unmatched["remittances"][0]["id"]

    match_resp = seeded_store_with_pair.post(
        "/api/reconciliation/match",
        json={"claim_id": claim_id, "remit_id": remit_id},
        headers=JSON,
    )
    assert match_resp.status_code == 200, match_resp.text

    resp = seeded_store_with_pair.get(f"/api/claims/{claim_id}")
    assert resp.status_code == 200, resp.text
    body = resp.json()
    kinds = [ev["kind"] for ev in body["stateHistory"]]
    assert "manual_match" in kinds, body["stateHistory"]
    assert "claim_submitted" in kinds, body["stateHistory"]

    # The manual_match event must carry the remittanceId it paired
    # with — the UI uses it to deep-link the drawer to the matched
    # remit. The claim_submitted event must NOT carry a remittanceId.
    mm = next(ev for ev in body["stateHistory"] if ev["kind"] == "manual_match")
    assert mm["remittanceId"] == remit_id
    cs = next(ev for ev in body["stateHistory"] if ev["kind"] == "claim_submitted")
    assert cs["remittanceId"] is None


def test_get_claim_detail_matched_remit_inlines_summary(seeded_store_with_pair):
    """A paired claim surfaces a populated ``matchedRemittance`` block.

    The drawer renders this block at the top of the right column;
    the spec §3.2 requires ``id``, ``totalPaid`` (number), ``status``
    (one of ``received``/``reconciled``), and ``receivedAt`` (ISO
    ending in ``Z``). This pins the inlined shape so a UI refactor
    can't drift from it.
    """
    unmatched = seeded_store_with_pair.get(
        "/api/reconciliation/unmatched", headers=JSON,
    ).json()
    if not unmatched["claims"] or not unmatched["remittances"]:
        pytest.fail(
            "co_medicaid_837p.txt + co_medicaid_835.txt fixtures no longer "
            "produce an unmatched pair; matched-remit tests need updating "
            "(see seeded_store_with_pair docstring for the expected invariant)"
        )
    claim_id = unmatched["claims"][0]["id"]
    remit_id = unmatched["remittances"][0]["id"]
    seeded_store_with_pair.post(
        "/api/reconciliation/match",
        json={"claim_id": claim_id, "remit_id": remit_id},
        headers=JSON,
    )

    resp = seeded_store_with_pair.get(f"/api/claims/{claim_id}")
    assert resp.status_code == 200, resp.text
    mr = resp.json()["matchedRemittance"]
    assert mr is not None
    assert mr["id"] == remit_id
    assert isinstance(mr["totalPaid"], (int, float))
    assert mr["status"] in {"received", "reconciled"}
    # ISO timestamp must end in Z — the spec contract that lets the
    # UI render the time without a tz suffix shim.
    assert mr["receivedAt"].endswith("Z"), mr["receivedAt"]


def test_get_claim_detail_includes_line_reconciliation_slim_projection(seeded_store):
    """SP7 §5.2: claim detail exposes a `lineReconciliation` slim list
    parallel to `serviceLines`, indexed by line number. The values are
    strings (Decimals serialized) or nulls when the line is unmatched."""
    cid = seeded_store.get("/api/claims", headers=JSON).json()["items"][0]["id"]
    resp = seeded_store.get(f"/api/claims/{cid}")
    assert resp.status_code == 200, resp.text
    body = resp.json()
    assert "lineReconciliation" in body
    lr_list = body["lineReconciliation"]
    assert isinstance(lr_list, list)
    # No 835 was ingested in this fixture, so every line is unmatched
    # on the 835 side — the default for an unpaired claim.
    for row in lr_list:
        assert "lineNumber" in row
        assert "status" in row
        assert "paid" in row
        assert "adjustmentsSum" in row
    # One entry per service line.
    assert len(lr_list) == len(body["serviceLines"])