"""Tests for the extended GET /api/config/providers/{npi} (SP21 Task 1.6).

The endpoint gains two new top-level arrays for the drill-down panel:
``recent_claims`` (top 10 by submission date desc) and ``recent_activity``
(top 10 by ``ts`` desc, joined to claims by ``claim_id`` because
``ActivityEvent`` has no direct ``provider_npi`` column).

Existing SP9 fields (``label``, ``legal_name``, ``tax_id``,
``address_line1``, ``city``, ``state``, ``zip``, etc.) must remain
present — the new arrays are additive only.

The fixtures in ``fixtures/minimal_837p.txt`` and ``fixtures/minimal_835.txt``
pair up to a single claim with ``provider_npi='1993999998'``. That NPI is
NOT in the seeded provider set (Montrose/Delta/Salida → 1881068062/1851446637/
1467507269). For the tests we hit the seeded Montrose NPI, which is the
canonical SP9 fixture NPI. The arrays come back as empty lists — the
contract under test is the *shape* (array, ≤10) and the *backwards compat*
of the existing fields; the data-path itself is exercised by the existing
ingestion path that backs ``/api/claims``.
"""
from __future__ import annotations

from datetime import datetime, timezone
from pathlib import Path

import pytest
from fastapi.testclient import TestClient

from cyclone.api import app
from cyclone.providers import Provider
from cyclone.store import store

FIXTURE_837 = Path(__file__).parent / "fixtures" / "minimal_837p.txt"
FIXTURE_835 = Path(__file__).parent / "fixtures" / "minimal_835.txt"

# Montrose — one of the three providers that `ensure_clearhouse_seeded()`
# writes into the providers table. Using a seeded NPI means the endpoint
# won't 404; the seeded claims (provider_npi='1993999998') won't appear
# under this NPI, so recent_claims/activity are expected empty lists.
MONTROSE_NPI = "1881068062"

# NPI the minimal 837P fixture bills under. NOT in the default seed —
# registering it before ingest is required to pass R204 (NPI must exist
# in the providers table).
TEST_837_NPI = "1993999998"


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


@pytest.fixture
def seeded_db(client: TestClient):
    """Seed the clearhouse + ingest the minimal 837P/835 fixtures.

    Mirrors the Task 1.5 ``seeded_db`` pattern: seed → ingest → hand the
    client back so the test hits the same TestClient.

    The 837 fixture bills under ``TEST_837_NPI``; without first registering
    that provider the parser's R204 rule rejects the claim with HTTP 422.
    """
    store.ensure_clearhouse_seeded()
    test_provider = Provider(
        npi=TEST_837_NPI,
        label="Test Provider",
        legal_name="Test Provider Inc",
        tax_id="123456789",
        taxonomy_code="207R00000X",
        address_line1="123 Test St",
        city="Denver",
        state="CO",
        zip="80202",
        created_at=datetime.now(timezone.utc),
        updated_at=datetime.now(timezone.utc),
    )
    store.upsert_provider(test_provider)
    text_837 = FIXTURE_837.read_text()
    text_835 = FIXTURE_835.read_text()
    r837 = client.post(
        "/api/parse-837",
        files={"file": ("x.txt", text_837, "text/plain")},
        headers={"Accept": "application/json"},
    )
    assert r837.status_code == 200, r837.text
    r835 = client.post(
        "/api/parse-835",
        files={"file": ("era.txt", text_835, "text/plain")},
        headers={"Accept": "application/json"},
    )
    assert r835.status_code == 200, r835.text
    return client


def test_provider_detail_includes_recent_claims(seeded_db: TestClient):
    """The extended response gains a recent_claims array (top 10)."""
    resp = seeded_db.get(f"/api/config/providers/{MONTROSE_NPI}")
    assert resp.status_code == 200, resp.text
    data = resp.json()
    assert "recent_claims" in data
    assert isinstance(data["recent_claims"], list)
    assert len(data["recent_claims"]) <= 10


def test_provider_detail_includes_recent_activity(seeded_db: TestClient):
    """The extended response gains a recent_activity array (top 10)."""
    resp = seeded_db.get(f"/api/config/providers/{MONTROSE_NPI}")
    assert resp.status_code == 200, resp.text
    data = resp.json()
    assert "recent_activity" in data
    assert isinstance(data["recent_activity"], list)
    assert len(data["recent_activity"]) <= 10


def test_provider_detail_backwards_compat(seeded_db: TestClient):
    """All SP9 fields still present; new arrays don't break the contract.

    The Provider Pydantic model (backend/src/cyclone/providers.py)
    serializes snake_case fields — that's what the wire carries. The
    TS ``Provider`` interface in ``src/types/index.ts`` is the
    in-memory sample shape and intentionally diverges; the contract
    being verified here is the API's actual payload.
    """
    resp = seeded_db.get(f"/api/config/providers/{MONTROSE_NPI}")
    assert resp.status_code == 200, resp.text
    data = resp.json()
    for key in (
        "npi",
        "label",
        "legal_name",
        "tax_id",
        "taxonomy_code",
        "address_line1",
        "city",
        "state",
        "zip",
        "is_active",
    ):
        assert key in data, f"missing field {key}"


def test_provider_detail_includes_orphan_remit_received(seeded_db: TestClient):
    """Regression: the ``remit_received`` ActivityEvent is recorded at 835
    ingest with ``claim_id=None`` (``store.add`` lines 999-1003) — the
    remittance hasn't been matched to a claim yet. The original
    ``ActivityEvent.claim_id IN (claim_ids)`` filter misses it because
    the orphan's claim_id is NULL.

    Once reconciliation (auto or manual) populates
    ``Remittance.claim_id``, the activity filter must surface the event
    via the ``Remittance.claim_id IN (claim_ids)`` branch of the OR.
    Without that branch, a provider's activity feed appears to freeze
    the moment an 835 lands — the most common activity, invisible.

    Setup: ingest 837+835 for ``TEST_837_NPI`` (claim CLM001 + remit
    CLM001), then manually match them so ``Remittance.claim_id`` is
    populated. The bug presents as: only ``claim_submitted`` and
    ``manual_match`` appear (no ``remit_received``). The fix surfaces
    all three.
    """
    # Force the match — simulates the post-reconciliation state that
    # populate Remittance.claim_id without depending on auto-reconcile
    # heuristics (which don't match this minimal fixture).
    match_resp = seeded_db.post(
        "/api/reconciliation/match",
        json={"claim_id": "CLM001", "remit_id": "CLM001"},
    )
    assert match_resp.status_code == 200, match_resp.text

    resp = seeded_db.get(f"/api/config/providers/{TEST_837_NPI}")
    assert resp.status_code == 200, resp.text
    data = resp.json()
    kinds = {event["kind"] for event in data["recent_activity"]}
    assert "remit_received" in kinds, (
        f"expected remit_received in recent_activity (orphan remits "
        f"must surface via the Remittance join), got kinds={sorted(kinds)}"
    )
