# Feature: dual-pipeline-signal-engine, Properties: Bayesian log-odds, entropy gate, EV_R
"""Property-based tests for the probabilistic pipeline math.

Feature: dual-pipeline-signal-engine

Tests three properties from the design specification:
1. Bayesian log-odds round-trip (Requirement 17.2)
2. Shannon entropy gate properties (Requirement 17.3)
3. EV_R monotonicity with P_up (Requirement 17.8)

Requirements: 6.3, 6.4, 6.5, 17.2, 17.3, 17.8
"""
from __future__ import annotations

import math

from hypothesis import given, settings
from hypothesis import strategies as st

from services.signal_engine.probabilistic import (
    _logit,
    _shannon_entropy,
    _sigmoid,
)

# ---------------------------------------------------------------------------
# Hypothesis strategies
# ---------------------------------------------------------------------------

# Prior probability in (0.01, 0.99) — avoids extreme clamping at boundaries
_prior_prob = st.floats(
    min_value=0.01, max_value=0.99, allow_nan=False, allow_infinity=False,
)

# Log-likelihood ratio values — bounded to avoid overflow in sigmoid
_log_lr = st.floats(
    min_value=-10.0, max_value=10.0, allow_nan=False, allow_infinity=False,
)

# List of log-LR values (1 to 10 signals)
_log_lr_list = st.lists(_log_lr, min_size=1, max_size=10)

# Probability in open interval (0, 1) for entropy tests
_open_prob = st.floats(
    min_value=1e-6, max_value=1.0 - 1e-6, allow_nan=False, allow_infinity=False,
)

# P_up values for EV_R monotonicity — two ordered values
_p_up_pair = st.tuples(
    st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
    st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
).filter(lambda pair: pair[0] < pair[1])

# Positive expected win in R-units
_e_win_r = st.floats(
    min_value=0.01, max_value=100.0, allow_nan=False, allow_infinity=False,
)


# ---------------------------------------------------------------------------
# Property 1: Bayesian log-odds round-trip
# Validates: Requirements 6.3, 17.2
# ---------------------------------------------------------------------------


@given(p_prior=_prior_prob, log_lrs=_log_lr_list)
@settings(max_examples=100)
def test_bayesian_log_odds_round_trip(
    p_prior: float, log_lrs: list[float],
) -> None:
    """**Validates: Requirements 6.3, 17.2**

    Converting P_prior to logit, adding Σ log(LR_i), and converting back
    via sigmoid SHALL produce a valid probability in [0, 1].

    logit(P_post) = logit(P_prior) + Σ log(LR_i)
    P_post = sigmoid(logit(P_post))

    The sigmoid implementation clamps extreme values to 0.0 / 1.0, so the
    result is in the closed interval [0, 1].
    """
    logit_prior = _logit(p_prior)
    sum_log_lr = sum(log_lrs)
    logit_posterior = logit_prior + sum_log_lr
    p_posterior = _sigmoid(logit_posterior)

    # Posterior must be a valid probability in [0, 1]
    assert 0.0 <= p_posterior <= 1.0, (
        f"Posterior {p_posterior} not in [0, 1]. "
        f"P_prior={p_prior}, logit_prior={logit_prior}, "
        f"Σ log_lr={sum_log_lr}, logit_posterior={logit_posterior}"
    )

    # For non-saturated posteriors, round-trip should hold:
    # sigmoid(logit(p)) ≈ p.  At saturation (0.0 or 1.0) the logit
    # clamps, so we only check the interior.
    if 1e-9 < p_posterior < 1.0 - 1e-9:
        round_trip = _sigmoid(_logit(p_posterior))
        assert math.isclose(round_trip, p_posterior, rel_tol=1e-6), (
            f"Round-trip failed: sigmoid(logit({p_posterior})) = {round_trip}"
        )


# ---------------------------------------------------------------------------
# Property 2: Shannon entropy gate properties
# Validates: Requirements 6.4, 17.3
# ---------------------------------------------------------------------------


@given(p=_open_prob)
@settings(max_examples=100)
def test_entropy_maximized_at_half(p: float) -> None:
    """**Validates: Requirements 6.4, 17.3**

    Shannon entropy H(p) SHALL be maximized at p = 0.5.
    For all p in (0, 1): H(0.5) >= H(p).
    """
    h_p = _shannon_entropy(p)
    h_half = _shannon_entropy(0.5)

    assert h_half >= h_p - 1e-12, (
        f"Entropy at 0.5 ({h_half}) should be >= entropy at {p} ({h_p})"
    )


def test_entropy_zero_at_boundaries() -> None:
    """**Validates: Requirements 6.4, 17.3**

    Shannon entropy SHALL equal 0.0 at p = 0.0 and p = 1.0.
    """
    assert _shannon_entropy(0.0) == 0.0, "H(0.0) should be 0.0"
    assert _shannon_entropy(1.0) == 0.0, "H(1.0) should be 0.0"


@given(p=_open_prob)
@settings(max_examples=100)
def test_entropy_symmetric_around_half(p: float) -> None:
    """**Validates: Requirements 6.4, 17.3**

    Shannon entropy SHALL be symmetric around 0.5: H(p) == H(1 - p).
    """
    h_p = _shannon_entropy(p)
    h_complement = _shannon_entropy(1.0 - p)

    assert math.isclose(h_p, h_complement, rel_tol=1e-9), (
        f"Entropy not symmetric: H({p}) = {h_p}, H({1.0 - p}) = {h_complement}"
    )


# ---------------------------------------------------------------------------
# Property 3: EV_R monotonicity with P_up
# Validates: Requirements 6.5, 17.8
# ---------------------------------------------------------------------------


@given(p_pair=_p_up_pair, e_win=_e_win_r)
@settings(max_examples=100)
def test_ev_r_monotonically_increasing_with_p_up(
    p_pair: tuple[float, float], e_win: float,
) -> None:
    """**Validates: Requirements 6.5, 17.8**

    EV_R = P_up · E[win_R] - (1 - P_up) · 1.0 SHALL be monotonically
    increasing with P_up for fixed E[win_R] > 0.

    For p1 < p2 and fixed E[win_R] > 0: EV_R(p2) >= EV_R(p1).
    """
    p1, p2 = p_pair

    # Compute EV_R directly using the formula (not _compute_ev_r which
    # derives E[win_R] from confluence signals)
    ev_r_1 = p1 * e_win - (1.0 - p1) * 1.0
    ev_r_2 = p2 * e_win - (1.0 - p2) * 1.0

    assert ev_r_2 >= ev_r_1 - 1e-12, (
        f"EV_R not monotonic: EV_R(p2={p2}) = {ev_r_2} < "
        f"EV_R(p1={p1}) = {ev_r_1} with E[win_R]={e_win}"
    )
