"""Tests for the SQLAlchemy-backed CycloneStore.

Public API is preserved from the in-memory version: add / get_batch /
iter_claims / iter_remittances / distinct_providers / recent_activity.
New methods: list_unmatched / manual_match / manual_unmatch.
"""

from __future__ import annotations

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

import pytest

from cyclone import db
from cyclone.store import BatchRecord, CycloneStore, store
from cyclone.parsers.models import (
    Address,
    BatchSummary,
    BillingProvider,
    ClaimHeader,
    ClaimOutput,
    Envelope,
    Payer,
    ParseResult,
    Subscriber,
    ValidationReport,
)


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


def _make_claim_837(claim_id: str = "CLM-1", charge: str = "124.00") -> ClaimOutput:
    return ClaimOutput(
        claim_id=claim_id,
        control_number="0001",
        transaction_date=date(2026, 6, 19),
        billing_provider=BillingProvider(
            name="Test", npi="1234567890",
        ),
        # Member_id doubles as patient_control_number in the DB. Derive it
        # from the claim id so two claims in the same batch are easy to tell
        # apart in test assertions (no longer required by a unique constraint
        # — see migration 0003 — but still a useful invariant for tests).
        subscriber=Subscriber(
            first_name="Jane", last_name="Doe", member_id=f"M-{claim_id}",
        ),
        payer=Payer(name="Test Payer", id="P1"),
        claim=ClaimHeader(
            claim_id=claim_id, total_charge=Decimal(charge),
            frequency_code="1", place_of_service="11",
        ),
        diagnoses=[],
        service_lines=[],
        validation=ValidationReport(passed=True, errors=[], warnings=[]),
        raw_segments=[],
    )


def _make_result_837() -> ParseResult:
    return ParseResult(
        envelope=Envelope(
            sender_id="S", receiver_id="R", control_number="0001",
            transaction_date=date(2026, 6, 19),
        ),
        claims=[_make_claim_837("CLM-1"), _make_claim_837("CLM-2", "88.50")],
        summary=BatchSummary(
            input_file="test.txt", control_number="0001",
            transaction_date=date(2026, 6, 19),
            total_claims=2, passed=2, failed=0,
        ),
    )


def _make_batch_record(kind: str = "837p", result=None, batch_id: str = "b-uuid-1"):
    return BatchRecord(
        id=batch_id, kind=kind, input_filename="test.txt",
        parsed_at=datetime(2026, 6, 19, 12, 0, tzinfo=timezone.utc),
        result=result or _make_result_837(),
    )


def test_module_singleton():
    assert isinstance(store, CycloneStore)


@pytest.mark.asyncio
async def test_add_837_publishes_claim_written_and_activity_recorded_events():
    """store.add() with event_bus= should publish one claim_written event
    per inserted claim and one activity_recorded event per activity row."""
    from cyclone.pubsub import EventBus

    bus = EventBus()
    claim_q = bus.subscribe(["claim_written"])
    activity_q = bus.subscribe(["activity_recorded"])

    s = CycloneStore()
    rec = _make_batch_record()
    s.add(rec, event_bus=bus)

    # Two claims + two claim_submitted activity events expected.
    import asyncio
    claims = []
    for _ in range(2):
        claims.append(await asyncio.wait_for(claim_q.__anext__(), timeout=0.5))
    activities = []
    for _ in range(2):
        activities.append(await asyncio.wait_for(activity_q.__anext__(), timeout=0.5))

    # Each event has _kind and a payload with the expected shape.
    for ev in claims:
        assert ev["_kind"] == "claim_written"
        assert "id" in ev
    for ev in activities:
        assert ev["_kind"] == "activity_recorded"
        assert ev["kind"] == "claim_submitted"
        assert "claimId" in ev


def test_add_835_publishes_remittance_written_and_activity_recorded_events():
    """835 batches publish remittance_written events (one per remit) and
    activity_recorded events (one per activity row)."""
    import asyncio
    from cyclone.pubsub import EventBus
    from cyclone.store import BatchRecord835

    # Reuse the 835 result helper from test_store_reconcile.py.
    from test_store_reconcile import _make_835_result, _make_remit_with_cas

    bus = EventBus()
    remit_q = bus.subscribe(["remittance_written"])
    activity_q = bus.subscribe(["activity_recorded"])

    cp = _make_remit_with_cas(remit_id="CLP-PUB", pcn="PCN-PUB")
    rec = BatchRecord835(
        id="b-835-1", kind="835", input_filename="test835.txt",
        parsed_at=datetime(2026, 6, 19, 12, 0, tzinfo=timezone.utc),
        result=_make_835_result([cp]),
    )
    CycloneStore().add(rec, event_bus=bus)

    loop = asyncio.new_event_loop()
    try:
        remits = []
        for _ in range(1):
            remits.append(loop.run_until_complete(
                asyncio.wait_for(remit_q.__anext__(), timeout=0.5)
            ))
        activities = []
        for _ in range(1):
            activities.append(loop.run_until_complete(
                asyncio.wait_for(activity_q.__anext__(), timeout=0.5)
            ))
    finally:
        loop.close()

    for ev in remits:
        assert ev["_kind"] == "remittance_written"
        assert ev["id"] == "PCN-PUB"
    for ev in activities:
        assert ev["_kind"] == "activity_recorded"
        assert ev["kind"] == "remit_received"
        assert ev["remittanceId"] == "PCN-PUB"


def test_add_837_persists_batch_and_claims():
    s = CycloneStore()
    rec = _make_batch_record()
    s.add(rec)
    # New session confirms persistence.
    with db.SessionLocal()() as session:
        from cyclone.db import Batch, Claim, ClaimState
        b = session.get(Batch, "b-uuid-1")
        assert b is not None
        assert b.kind == "837p"
        claims = session.query(Claim).all()
        assert len(claims) == 2
        assert all(c.state == ClaimState.SUBMITTED for c in claims)


def test_iter_claims_returns_dicts():
    s = CycloneStore()
    s.add(_make_batch_record())
    rows = s.iter_claims()
    assert len(rows) == 2
    assert all("id" in r and "state" in r for r in rows)


def test_persistence_across_session():
    s1 = CycloneStore()
    s1.add(_make_batch_record(batch_id="b-x"))

    s2 = CycloneStore()  # fresh instance, same engine
    loaded = s2.get_batch("b-x")
    assert loaded is not None
    assert loaded["kind"] == "837p"


def test_find_existing_batch_for_claim_returns_none_for_unknown():
    """Unknown claim_id -> None."""
    from cyclone import store as store_mod  # noqa: F401
    assert store_mod.find_existing_batch_for_claim("nope") is None


def test_find_existing_batch_for_claim_returns_batch_id():
    """Known claim_id -> batch_id of the holding batch."""
    from cyclone import db as _db
    from cyclone.db import Batch, Claim
    from datetime import datetime, timezone

    with _db.SessionLocal()() as s:
        s.add(Batch(
            id="B1", kind="837p", input_filename="x.txt",
            parsed_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
            raw_result_json={},
        ))
        s.add(Claim(id="CLM-A", batch_id="B1", patient_control_number="M1"))
        s.commit()

    from cyclone import store as store_mod  # noqa: F401
    assert store_mod.find_existing_batch_for_claim("CLM-A") == "B1"


def test_find_existing_batch_for_remit_returns_none_for_unknown():
    """Unknown remit id -> None."""
    from cyclone import store as store_mod  # noqa: F401
    assert store_mod.find_existing_batch_for_remit("nope") is None


def test_find_existing_batch_for_remit_returns_batch_id():
    """Known remit id -> batch_id of the holding batch."""
    from cyclone import db as _db
    from cyclone.db import Batch, Remittance
    from datetime import datetime, timezone

    with _db.SessionLocal()() as s:
        s.add(Batch(
            id="B2", kind="835", input_filename="y.txt",
            parsed_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
            raw_result_json={},
        ))
        s.add(Remittance(
            id="CLP-A", batch_id="B2",
            payer_claim_control_number="CLP-A",
            status_code="1",
            received_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
        ))
        s.commit()

    from cyclone import store as store_mod  # noqa: F401
    assert store_mod.find_existing_batch_for_remit("CLP-A") == "B2"