"""Pytest-native shared scaffolding for the round-trip test matrix.

This replaces the old ``RoundTripTestCase``/``do_tests`` inheritance
machinery. Each shared test (in ``test_statements.py``, ``test_attributes.py``,
``test_qnames.py``, ``test_examples.py``) builds a document and hands it to
the ``roundtrip`` fixture, which is parametrized over ``SHARED_TARGETS`` so
every case runs once per serialization format AND once under the
non-serializing ``model`` target.
"""

import io
import os

import pytest
from hypothesis import settings

from prov.model import ProvDocument

# Hypothesis profiles for the property-based round-trip tests
# (``test_property_roundtrip.py``). The local ``default`` profile is
# exploratory; the ``ci`` profile is bounded and deterministic so the property
# tests add a predictable, small amount of wall-clock time to the CI matrix.
# The active profile is chosen from the ``HYPOTHESIS_PROFILE`` env var (CI sets
# it to ``ci``); it defaults to ``default`` when unset.
settings.register_profile("default", max_examples=200, deadline=None)
settings.register_profile("ci", max_examples=50, deadline=None, derandomize=True)
settings.load_profile(os.environ.get("HYPOTHESIS_PROFILE", "default"))

# Formats that support a full serialize -> deserialize -> compare round trip.
ROUNDTRIP_FORMATS = ("json", "xml", "rdf")
# The full target axis: the round-trip formats PLUS a "model" target that
# constructs the document, exercises PROV-N generation, and checks the
# self-equality invariant WITHOUT serialization. The model axis preserves the
# coverage the old RoundTripModelTest provided.
SHARED_TARGETS = ("model", *ROUNDTRIP_FORMATS)


def roundtrip_document(doc: ProvDocument, fmt: str) -> ProvDocument:
    """Serialize ``doc`` to ``fmt`` and read it back."""
    with io.BytesIO() as stream:
        doc.serialize(destination=stream, format=fmt, indent=4)
        stream.seek(0)
        return ProvDocument.deserialize(source=stream, format=fmt)


@pytest.fixture(params=SHARED_TARGETS)
def fmt(request):
    """The target under test — "model" or a serialization format.

    Parametrizes every shared statement/attribute/qname/example case, so each
    runs once per round-trip format AND once under the non-serializing model
    target.
    """
    return request.param


@pytest.fixture
def roundtrip(fmt):
    """Return a ``_check(doc)`` callable that asserts ``doc`` survives its target.

    For a serialization format: serialize -> deserialize -> ``doc == reloaded``.
    For the "model" target: no serialization — force PROV-N generation (proving
    model support for the statement) and assert self-equality. On inequality the
    ``pytest_assertrepr_compare`` hook below renders which records differ.
    """

    def _check(doc: ProvDocument) -> ProvDocument:
        if fmt == "model":
            doc.get_provn()  # exercises PROV-N generation
            assert doc == doc  # self-equality invariant, no serialization
            return doc
        reloaded = roundtrip_document(doc, fmt)
        assert doc == reloaded  # readable diff via the hook below
        return reloaded

    return _check


def _document_record_strings(doc: ProvDocument) -> set[str]:
    """All record strings in ``doc``, including those inside named sub-bundles.

    ``ProvBundle.get_records()`` returns only the bundle's own records, but
    ``ProvDocument.__eq__`` also compares nested bundles, so a diff built from
    the top-level records alone is blind to bundle contents (it would render an
    empty diff for a document whose only records live in a sub-bundle). Fold
    each sub-bundle's records in, prefixed with the bundle identifier so records
    from different bundles cannot collide as equal strings. Documents nest
    bundles only one level, so no recursion is needed.
    """
    records = {str(r) for r in doc.get_records()}
    if doc.has_bundles():
        for bundle in doc.bundles:
            records.update(f"{bundle.identifier} | {r}" for r in bundle.get_records())
    return records


def pytest_assertrepr_compare(config, op, left, right):
    """Readable diff for ``assert doc == reloaded`` on two ProvDocuments."""
    if op != "==" or not (
        isinstance(left, ProvDocument) and isinstance(right, ProvDocument)
    ):
        return None
    left_recs = _document_record_strings(left)
    right_recs = _document_record_strings(right)
    only_left = sorted(left_recs - right_recs)
    only_right = sorted(right_recs - left_recs)
    return [
        "ProvDocument == ProvDocument failed:",
        f"  records: {len(left_recs)} left, {len(right_recs)} right",
        f"  in left only  ({len(only_left)}):",
        *(f"    - {r}" for r in only_left),
        f"  in right only ({len(only_right)}):",
        *(f"    + {r}" for r in only_right),
    ]
