"""SP25: defensive read-side stub for synthetic ``<synthetic:orphan-reconcile>``
``Batch`` rows.

Background: ``CycloneStore.reconcile_orphan_st02s()`` (sp38) seeds ``Batch``
rows for orphan 999 ACK ST02s so the envelope index can resolve
``source_batch_id`` lookups. Those rows are inserted with
``raw_result_json=NULL`` because no source 837 file was ever parsed
for them. The read-side helper ``_row_to_record()`` in
``store/batches.py`` was written assuming the column is always
populated, so it raises a Pydantic ``ValidationError`` on ``summary:
Field required`` the moment it sees a synthetic row — which makes
``GET /api/batches?limit=5`` return HTTP 500.

This file pins the contract: a synthetic (or otherwise unparseable)
``Batch`` row MUST still hydrate to a typed ``BatchRecord`` with an
empty ``claims`` list and the ST02 echoed on ``result.summary.control_number``,
so the dashboard "Recent batches" widget can render them as
"synthetic: 0 claims" without the whole list blowing up.

Live state witnessed during the 2026-07-07 dashboard postmortem:
the sp38 reconcile pass on the production DB seeded 4 such rows
(SCN 991102986–989). Once the stub lands, ``/api/batches`` returns 200
instead of 500 and the dashboard "Recent batches" panel surfaces the
real underlying batches (the test runs).

Note: the rows inserted here are *ORM-level* fixtures (Batch rows with
NULL raw_result_json, used to exercise the read-path's defensive
deserialize). They are NOT synthetic EDI — the actual claim data for
the recover-ingest path lives under ``ingest/`` and is replayed by the
operator after this defensive stub ships.
"""
from __future__ import annotations

from datetime import datetime, timezone

from cyclone import db, store
from cyclone.db import Batch
from cyclone.store.records import BatchRecord837, BatchRecord835


def _now():
    return datetime.now(timezone.utc)


def _insert_synthetic(s, *, kind: str, st02: str, filename: str = "<synthetic:orphan-reconcile>"):
    bid = f"syn-{st02}"
    s.add(Batch(
        id=bid,
        kind=kind,
        input_filename=filename,
        parsed_at=_now(),
        transaction_set_control_number=st02,
        raw_result_json=None,
    ))
    s.commit()
    return bid


def test_row_to_record_handles_null_raw_json_837p():
    """837p synthetic row → stub BatchRecord837 with empty claims[] + SCN preserved."""
    s = db.SessionLocal()()
    try:
        _insert_synthetic(s, kind="837p", st02="991102986")
    finally:
        s.close()

    # No raw_result_json on the row → _row_to_record must NOT raise.
    rows = store.list_batches(limit=50)

    syn = [r for r in rows if r.id == "syn-991102986"]
    assert len(syn) == 1
    rec = syn[0]
    assert isinstance(rec, BatchRecord837)
    assert rec.result.claims == []
    assert rec.result.summary.total_claims == 0
    assert rec.result.summary.control_number == "991102986"
    assert rec.result.summary.passed == 0
    assert rec.result.summary.failed == 0


def test_row_to_record_handles_null_raw_json_835():
    """835 synthetic row → stub BatchRecord835 with empty claims[].claims."""
    s = db.SessionLocal()()
    try:
        _insert_synthetic(s, kind="835", st02="991102987")
    finally:
        s.close()

    rows = store.list_batches(limit=50)
    syn = [r for r in rows if r.id == "syn-991102987"]
    assert len(syn) == 1
    rec = syn[0]
    assert isinstance(rec, BatchRecord835)
    assert rec.result.claims == []
    # summary control_number echoes ST02.
    assert rec.result.summary.control_number == "991102987"


def test_row_to_record_handles_empty_string_raw_json():
    """raw_result_json='' (empty string) gets the same defensive treatment."""
    s = db.SessionLocal()()
    try:
        bid = "syn-empty"
        s.add(Batch(
            id=bid, kind="837p",
            input_filename="<synthetic:orphan-reconcile>",
            parsed_at=_now(),
            transaction_set_control_number="991102988",
            raw_result_json="",  # empty string instead of None
        ))
        s.commit()
    finally:
        s.close()

    rows = store.list_batches(limit=50)
    syn = [r for r in rows if r.id == "syn-empty"]
    assert len(syn) == 1
    rec = syn[0]
    assert rec.result.claims == []
    assert rec.result.summary.total_claims == 0


def test_row_to_record_normal_row_still_parses():
    """Regression guard: a well-formed raw_result_json still deserializes fully.

    Batch.raw_result_json is JSONText-backed, so we store a dict; the type
    decorator takes care of the JSON round-trip.
    """
    payload = {
        "envelope": {
            "sender_id": "11525703",
            "receiver_id": "RECEIVERID",
            "control_number": "000000001",
            "transaction_date": "2026-07-07",
            "transaction_time": "120000",
            "implementation_guide": "005010X222A1",
        },
        "claims": [],
        "summary": {
            "input_file": "single-claim.x12",
            "control_number": "000000001",
            "transaction_date": "2026-07-07",
            "total_claims": 1,
            "passed": 1,
            "failed": 0,
        },
    }
    s = db.SessionLocal()()
    try:
        s.add(Batch(
            id="syn-normal",
            kind="837p",
            input_filename="single-claim.x12",
            parsed_at=_now(),
            transaction_set_control_number="991102977",
            raw_result_json=payload,
        ))
        s.commit()
    finally:
        s.close()

    rows = store.list_batches(limit=50)
    norm = [r for r in rows if r.id == "syn-normal"]
    assert len(norm) == 1
    rec = norm[0]
    assert rec.result.summary.total_claims == 1
    assert rec.result.summary.passed == 1
