from __future__ import annotations

import numpy as np

import pytest

pytest.importorskip("hypothesis")

from hypothesis import given, settings, strategies as st

from zen_fronts import ZenFronts
from zen_fronts.selection import SelectionConfig
from zen_fronts.selection.schema import validate_selection_stats


def _sampler(rng: np.random.Generator) -> dict[str, float]:
    return {"x1": float(rng.random()), "x2": float(rng.random())}


@settings(max_examples=12, deadline=None)
@given(
    seed=st.integers(min_value=0, max_value=10_000),
    pop=st.integers(min_value=4, max_value=12),
    extra_epochs=st.integers(min_value=1, max_value=5),
)
def test_tombstone_selection_snapshot_is_frozen(seed: int, pop: int, extra_epochs: int) -> None:
    """Deleting a point must not mutate its last persisted selection snapshot.

    Invariants:
      * params(pid) becomes unavailable (KeyError)
      * info(pid)["selection"] stays identical across future refresh() calls
      * the deleted pid is never again included in refresh() active ids
    """

    zf = ZenFronts(
        crits={"f1": "min", "f2": "min"},
        selection=SelectionConfig(
            n_samples=30,
            percentile=0.25,
            seed=int(seed),
            quantiles_mode_i=2,
            collect_stats=True,
        ),
        sampler=_sampler,
    )

    ids = zf.add_random_point(int(pop))

    # Make everybody ready with explicit stats (avoid dependence on any online filter).
    for pid in ids:
        p = zf.params(pid)
        zf.update_crits(
            pid,
            {
                "f1": {"mu": float(p["x1"]), "trend": 0.0, "sigma2": 0.01},
                "f2": {"mu": float(p["x2"]), "trend": 0.0, "sigma2": 0.01},
            },
            t=0.0,
        )

    losers = zf.refresh(now=0.0)
    assert len(losers) >= 1

    victim = int(losers[0])
    snap0 = zf.info(victim)["selection"]
    assert snap0 is not None
    validate_selection_stats(snap0)

    zf.delete_point(victim)
    assert victim not in zf.points

    # Tombstone must not have params.
    try:
        zf.params(victim)
        assert False, "expected KeyError for tombstone params"
    except KeyError:
        pass

    # Future refresh cycles must not touch tombstone snapshot.
    for e in range(1, int(extra_epochs) + 1):
        for pid in list(zf.points.keys()):
            p = zf.params(pid)
            zf.update_crits(
                pid,
                {
                    "f1": {"mu": float(p["x1"]), "trend": 0.0, "sigma2": 0.01},
                    "f2": {"mu": float(p["x2"]), "trend": 0.0, "sigma2": 0.01},
                },
                t=float(e),
            )

        zf.refresh(now=float(e))
        snap_now = zf.info(victim)["selection"]
        assert snap_now == snap0
