"""Regression tests for SP27 Task 13b: ``total`` on the list endpoints
must reflect the full DB population, not a 100-row sample.

Before the fix, both ``/api/claims`` and ``/api/remittances`` computed
``total`` via::

    total = len(list(store.iter_<X>(**filters)))

``iter_<X>`` defaults to ``limit=100``, so the returned list was capped
at 100 regardless of how many rows the DB actually held. The frontend
then rendered ``data.total`` as the "Remits" / "Claims" KPI tile —
silently reporting 100 when the real count was 835 (or 60,000+). The
page also showed 100 rows, looked complete, and the bug stayed hidden.

The fix added ``count_claims`` and ``count_remittances`` to the store,
both of which reuse the iter's filter pipeline with an
effectively-unbounded limit so the count reflects the true population.

These tests cover:

* Module-level: ``count_*`` returns the right cardinality with each
  filter dimension (none, status, payer substring, date range,
  batch_id).
* Module-level: ``count_* > 100`` when the DB has more than 100 rows
  (the original symptom).
* HTTP: ``/api/claims?limit=25`` reports ``total`` matching the full
  DB count, not the page size.
* HTTP: ``/api/remittances`` ditto.
"""

from __future__ import annotations

from datetime import date, datetime, timedelta, timezone
from decimal import Decimal

import pytest
from fastapi.testclient import TestClient

from cyclone import db
from cyclone.api import app
from cyclone.db import Batch, Claim, ClaimState, Remittance
from cyclone.store import store as global_store


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


def _seed_batch(s, batch_id: str, parsed_at: datetime, kind: str = "837p") -> None:
    s.add(Batch(
        id=batch_id,
        kind=kind,
        input_filename=f"{batch_id}.edi",
        parsed_at=parsed_at,
        totals_json={"total_claims": 0},
        validation_json={"passed": True, "warnings": [], "errors": []},
        raw_result_json={"_": "stub"},
    ))


def _seed_claim(
    s,
    claim_id: str,
    batch_id: str,
    *,
    payer_name: str = "Colorado Medicaid",
    state: ClaimState = ClaimState.SUBMITTED,
    service_date: date = date(2026, 6, 1),
) -> None:
    s.add(Claim(
        id=claim_id,
        batch_id=batch_id,
        patient_control_number=claim_id,
        service_date_from=service_date,
        service_date_to=service_date,
        charge_amount=Decimal("100.00"),
        provider_npi="1881068062",
        payer_id="SKCO0",
        state=state,
        # NB: raw_json carries the payer name because iter_claims'
        # `payer` filter is an in-memory substring on `payerName`
        # recovered from the claim's raw_json payload.
        raw_json={"payer": {"name": payer_name}},
    ))


def _seed_remit(
    s,
    remit_id: str,
    batch_id: str,
    *,
    received_at: datetime,
    payer_name: str = "Colorado Medicaid",
    claim_id: str | None = None,
    total_paid: Decimal = Decimal("100.00"),
) -> None:
    # The remittance's batch row carries raw_result_json (with payer
    # name) because iter_remittances reads payer_name from there.
    s.add(Batch(
        id=batch_id,
        kind="835",
        input_filename=f"{remit_id}.edi",
        parsed_at=received_at,
        totals_json={"total_claims": 1},
        validation_json={"passed": True, "warnings": [], "errors": []},
        raw_result_json={"payer": {"name": payer_name}},
    ))
    s.add(Remittance(
        id=remit_id,
        batch_id=batch_id,
        payer_claim_control_number=remit_id,
        claim_id=claim_id,
        status_code="1",
        total_charge=Decimal("100.00"),
        total_paid=total_paid,
        adjustment_amount=Decimal("0"),
        received_at=received_at,
    ))


# --------------------------------------------------------------------------- #
# Module-level: count_claims
# --------------------------------------------------------------------------- #


def test_count_claims_zero_when_empty():
    assert global_store.count_claims() == 0


def test_count_claims_unfiltered_returns_full_population():
    """Regression: with 150 claims seeded and default iter limit=100,
    the old ``len(list(iter_claims()))`` returned 100. count_claims
    must return 150."""
    base = datetime(2026, 6, 1, 12, 0, tzinfo=timezone.utc)
    with db.SessionLocal()() as s:
        _seed_batch(s, "b-claims-pop", base)
        for i in range(150):
            _seed_claim(s, f"CLM-{i:04d}", "b-claims-pop")
        s.commit()

    assert global_store.count_claims() == 150


def test_count_claims_filters_by_status():
    base = datetime(2026, 6, 1, 12, 0, tzinfo=timezone.utc)
    with db.SessionLocal()() as s:
        _seed_batch(s, "b-status", base)
        for i in range(5):
            _seed_claim(s, f"CLM-SUB-{i}", "b-status", state=ClaimState.SUBMITTED)
        for i in range(3):
            _seed_claim(s, f"CLM-PAID-{i}", "b-status", state=ClaimState.PAID)
        s.commit()

    assert global_store.count_claims(status="submitted") == 5
    assert global_store.count_claims(status="paid") == 3


def test_count_claims_filters_by_payer_substring():
    """The `payer` filter is a case-insensitive substring on payerName
    recovered from raw_json — make sure count_claims applies it the
    same way iter_claims does."""
    base = datetime(2026, 6, 1, 12, 0, tzinfo=timezone.utc)
    with db.SessionLocal()() as s:
        _seed_batch(s, "b-payer", base)
        for i in range(4):
            _seed_claim(s, f"CLM-CO-{i}", "b-payer", payer_name="Colorado Medicaid")
        for i in range(2):
            _seed_claim(s, f"CLM-AZ-{i}", "b-payer", payer_name="Arizona Medicaid")
        s.commit()

    assert global_store.count_claims() == 6
    assert global_store.count_claims(payer="Colorado") == 4
    assert global_store.count_claims(payer="Arizona") == 2
    assert global_store.count_claims(payer="colorado") == 4  # case-insensitive


def test_count_claims_filters_by_provider_npi():
    base = datetime(2026, 6, 1, 12, 0, tzinfo=timezone.utc)
    with db.SessionLocal()() as s:
        _seed_batch(s, "b-npi", base)
        for i in range(3):
            _seed_claim(s, f"CLM-A-{i}", "b-npi")
        # Switch the rest to a different NPI via direct insert.
        for i in range(7):
            s.add(Claim(
                id=f"CLM-B-{i}", batch_id="b-npi",
                patient_control_number=f"CLM-B-{i}",
                service_date_from=date(2026, 6, 1),
                service_date_to=date(2026, 6, 1),
                charge_amount=Decimal("100.00"),
                provider_npi="9999999999",
                payer_id="SKCO0",
                state=ClaimState.SUBMITTED,
            ))
        s.commit()

    assert global_store.count_claims(provider_npi="1881068062") == 3
    assert global_store.count_claims(provider_npi="9999999999") == 7


# --------------------------------------------------------------------------- #
# Module-level: count_remittances
# --------------------------------------------------------------------------- #


def test_count_remittances_zero_when_empty():
    assert global_store.count_remittances() == 0


def test_count_remittances_unfiltered_returns_full_population():
    """Regression: same shape as test_count_claims_unfiltered — the
    old ``len(list(iter_remittances()))`` was capped at 100."""
    base = datetime(2026, 6, 1, 12, 0, tzinfo=timezone.utc)
    with db.SessionLocal()() as s:
        _seed_batch(s, "b-remit-pop", base)
        for i in range(120):
            _seed_remit(
                s, f"RMT-{i:04d}", f"b-{i:04d}",
                received_at=base + timedelta(minutes=i),
            )
        s.commit()

    assert global_store.count_remittances() == 120


def test_count_remittances_filters_by_claim_id():
    base = datetime(2026, 6, 1, 12, 0, tzinfo=timezone.utc)
    with db.SessionLocal()() as s:
        _seed_batch(s, "b-cid", base)
        _seed_remit(s, "RMT-A", "b-cid-A", received_at=base, claim_id="CLM-1")
        _seed_remit(s, "RMT-B", "b-cid-B", received_at=base, claim_id="CLM-2")
        _seed_remit(s, "RMT-C", "b-cid-C", received_at=base, claim_id=None)
        s.commit()

    assert global_store.count_remittances() == 3
    assert global_store.count_remittances(claim_id="CLM-1") == 1
    assert global_store.count_remittances(claim_id="CLM-2") == 1
    assert global_store.count_remittances(claim_id="CLM-missing") == 0


def test_count_remittances_filters_by_payer_exact_match():
    """iter_remittances' `payer` filter is an exact match (not a
    substring). Verify count_remittances mirrors that."""
    base = datetime(2026, 6, 1, 12, 0, tzinfo=timezone.utc)
    with db.SessionLocal()() as s:
        _seed_batch(s, "b-rpayer", base)
        for i in range(4):
            _seed_remit(
                s, f"RMT-CO-{i}", f"b-co-{i}",
                received_at=base, payer_name="Colorado Medicaid",
            )
        for i in range(2):
            _seed_remit(
                s, f"RMT-AZ-{i}", f"b-az-{i}",
                received_at=base, payer_name="Arizona Medicaid",
            )
        s.commit()

    assert global_store.count_remittances(payer="Colorado Medicaid") == 4
    assert global_store.count_remittances(payer="Arizona Medicaid") == 2
    assert global_store.count_remittances(payer="colorado medicaid") == 0  # case-sensitive exact


# --------------------------------------------------------------------------- #
# HTTP regression: list endpoint `total` reflects full population
# --------------------------------------------------------------------------- #


def test_api_claims_total_reflects_full_population(client: TestClient):
    """Bug repro: seed 150 claims, hit /api/claims?limit=25, assert
    ``total == 150`` (not 25, not 100)."""
    base = datetime(2026, 6, 1, 12, 0, tzinfo=timezone.utc)
    with db.SessionLocal()() as s:
        _seed_batch(s, "b-http-claims", base)
        for i in range(150):
            _seed_claim(s, f"CLM-HTTP-{i:04d}", "b-http-claims")
        s.commit()

    resp = client.get("/api/claims", params={"limit": 25})
    assert resp.status_code == 200, resp.text
    body = resp.json()
    assert body["total"] == 150, body
    assert body["returned"] == 25
    assert body["has_more"] is True


def test_api_remittances_total_reflects_full_population(client: TestClient):
    """Bug repro: seed 120 remits, hit /api/remittances?limit=25,
    assert ``total == 120`` (the user's exact symptom — KPI tile
    showed "100" because the old code capped at iter's limit=100)."""
    base = datetime(2026, 6, 1, 12, 0, tzinfo=timezone.utc)
    with db.SessionLocal()() as s:
        _seed_batch(s, "b-http-remit", base)
        for i in range(120):
            _seed_remit(
                s, f"RMT-HTTP-{i:04d}", f"b-h-{i:04d}",
                received_at=base + timedelta(minutes=i),
            )
        s.commit()

    resp = client.get("/api/remittances", params={"limit": 25})
    assert resp.status_code == 200, resp.text
    body = resp.json()
    assert body["total"] == 120, body
    assert body["returned"] == 25
    assert body["has_more"] is True


def test_api_remittances_total_zero_when_empty(client: TestClient):
    """Empty DB → total=0. (Sanity check the count path doesn't break
    for the no-data case.)"""
    resp = client.get("/api/remittances")
    assert resp.status_code == 200
    body = resp.json()
    assert body == {"items": [], "total": 0, "returned": 0, "has_more": False}


def test_api_remittances_total_respects_filters(client: TestClient):
    """Filter by payer → total narrows correctly (covers the in-memory
    payer filter on the count path)."""
    base = datetime(2026, 6, 1, 12, 0, tzinfo=timezone.utc)
    with db.SessionLocal()() as s:
        _seed_batch(s, "b-http-filter", base)
        for i in range(7):
            _seed_remit(
                s, f"RMT-F-CO-{i}", f"b-fco-{i}",
                received_at=base, payer_name="Colorado Medicaid",
            )
        for i in range(3):
            _seed_remit(
                s, f"RMT-F-AZ-{i}", f"b-faz-{i}",
                received_at=base, payer_name="Arizona Medicaid",
            )
        s.commit()

    resp = client.get(
        "/api/remittances",
        params={"payer": "Colorado Medicaid", "limit": 100},
    )
    assert resp.status_code == 200
    body = resp.json()
    assert body["total"] == 7
    assert body["returned"] == 7
    assert body["has_more"] is False