from __future__ import annotations

import numpy as np

import pytest

pytest.importorskip("hypothesis")

from hypothesis import given, settings, strategies as st

from zen_fronts.selection_core import mc_rank_select_detailed


@settings(max_examples=25, deadline=None)
@given(
    N=st.integers(min_value=2, max_value=25),
    M=st.integers(min_value=1, max_value=8),
    seed=st.integers(min_value=0, max_value=10_000),
    n_samples=st.integers(min_value=8, max_value=40),
    percentile=st.floats(min_value=0.05, max_value=0.5, allow_nan=False, allow_infinity=False),
)
def test_mc_rank_select_detailed_invariants(
    N: int, M: int, seed: int, n_samples: int, percentile: float
) -> None:
    """Basic shape/type/bounds invariants for the detailed kernel.

    This is intentionally *schema-level*: it does not assert any particular
    ranking outcome, only that the output is internally consistent.
    """

    rng = np.random.default_rng(12345)
    mu = rng.normal(size=(N, M)).astype(np.float64)
    # Ensure strictly positive variances to avoid weird edge-cases in P² init.
    sigma2 = (0.001 + rng.random((N, M))).astype(np.float64)

    (
        winners,
        losers,
        win_count,
        lose_count,
        rank_sum,
        place,
        front,
        within,
        quality,
    ) = mc_rank_select_detailed(
        mu,
        sigma2,
        int(n_samples),
        percentile=float(percentile),
        seed=int(seed),
        # mode_i=1 uses pareto_core rank coordinates; it's the production default.
        mode_i=1,
        within_mode_i=0,
        quantiles_mode_i=2,
        quantiles_budget=50_000,
    )

    assert winners.ndim == 1 and losers.ndim == 1
    assert win_count.shape == (N,)
    assert lose_count.shape == (N,)
    assert rank_sum.shape == (N,)
    assert quality.shape == (N,)

    # k = ceil(percentile*N), clamped to [1..N]
    k = int(np.ceil(float(percentile) * float(N)))
    k = max(1, min(k, N))
    assert winners.shape[0] == k
    assert losers.shape[0] == k

    # Indices are in range
    assert np.all((0 <= winners) & (winners < N))
    assert np.all((0 <= losers) & (losers < N))
    # No duplicates inside each selection list
    assert np.unique(winners).shape[0] == winners.shape[0]
    assert np.unique(losers).shape[0] == losers.shape[0]

    # Count bounds
    assert np.all((0 <= win_count) & (win_count <= n_samples))
    assert np.all((0 <= lose_count) & (lose_count <= n_samples))
    assert np.all((0 <= rank_sum) & (rank_sum <= n_samples * (N - 1)))

    # Metric blocks: q25 <= median <= q75, std >= 0, values in [0..N-1]
    for block in (place, front, within):
        mean = np.asarray(block["mean"], dtype=np.float64)
        median = np.asarray(block["median"], dtype=np.float64)
        std = np.asarray(block["std"], dtype=np.float64)
        q25 = np.asarray(block["q25"], dtype=np.float64)
        q75 = np.asarray(block["q75"], dtype=np.float64)

        assert mean.shape == (N,)
        assert median.shape == (N,)
        assert std.shape == (N,)
        assert q25.shape == (N,)
        assert q75.shape == (N,)

        assert np.all(std >= 0.0)
        assert np.all(q25 <= median)
        assert np.all(median <= q75)

        # Values are ranks; allow tiny numeric noise.
        assert np.all(mean >= -1e-9)
        assert np.all(mean <= (N - 1) + 1e-9)
        assert np.all(q25 >= -1e-9)
        assert np.all(q75 <= (N - 1) + 1e-9)
