"""Property-based tests for v3 macro and competitive layers.

Validates:
- Property 13: Noisy-OR normalized exposure is bounded in [0, 1]
- Property 14: Competitive LLR is clamped to [-1.25, 1.25]
- Property 15: Graph attenuation is zero beyond max distance

Requirements: 9.1, 9.2, 10.3, 10.4, 10.5
"""
from __future__ import annotations

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

from services.aggregation.interpolation import (
    compute_normalized_macro_exposure,
)
from services.aggregation.signal_propagation import (
    compute_competitive_llr,
    compute_shrunk_correlation,
)

# ---------------------------------------------------------------------------
# Strategies
# ---------------------------------------------------------------------------

unit_floats = st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False)
llr_source_values = st.floats(min_value=-5.0, max_value=5.0, allow_nan=False, allow_infinity=False)
distances_valid = st.integers(min_value=1, max_value=3)
distances_beyond = st.integers(min_value=4, max_value=10)


# ---------------------------------------------------------------------------
# Feature: math-core-v3-engine, Property 13: Noisy-OR normalized exposure
#   is bounded in [0, 1]
# ---------------------------------------------------------------------------
# **Validates: Requirements 9.1, 9.2**


@settings(max_examples=100)
@given(
    geo=unit_floats,
    supply=unit_floats,
    commodity=unit_floats,
    sector=unit_floats,
)
def test_property_13_noisy_or_exposure_bounded(
    geo: float,
    supply: float,
    commodity: float,
    sector: float,
) -> None:
    """Property 13: Noisy-OR normalized exposure is bounded in [0, 1].

    For any overlap values O_k in [0, 1] for each dimension (geo, supply,
    commodity, sector) with fixed positive weights, the normalized macro
    exposure E_macro SHALL be in [0.0, 1.0], reaching exactly 1.0 when
    all O_k = 1.0. (Use tier="regional" with dampener=1.0 for this
    property test)
    """
    overlaps = {
        "geo": geo,
        "supply": supply,
        "commodity": commodity,
        "sector": sector,
    }
    e_macro = compute_normalized_macro_exposure(overlaps, tier="regional")

    # Must be bounded in [0, 1] with regional dampener = 1.0
    assert 0.0 <= e_macro <= 1.0, (
        f"E_macro={e_macro} out of bounds [0, 1] for overlaps={overlaps}"
    )


@settings(max_examples=1)
@given(st.just(None))
def test_property_13_max_exposure_is_one(_: None) -> None:
    """Property 13 (edge): E_macro reaches exactly 1.0 when all O_k = 1.0."""
    overlaps = {"geo": 1.0, "supply": 1.0, "commodity": 1.0, "sector": 1.0}
    e_macro = compute_normalized_macro_exposure(overlaps, tier="regional")
    assert e_macro == 1.0, f"Expected 1.0 when all overlaps=1.0, got {e_macro}"


# ---------------------------------------------------------------------------
# Feature: math-core-v3-engine, Property 14: Competitive LLR is clamped
#   to [-1.25, 1.25]
# ---------------------------------------------------------------------------
# **Validates: Requirements 10.3, 10.4, 10.5**


@settings(max_examples=100)
@given(
    llr_source=llr_source_values,
    rho_rolling=unit_floats,
    n_observations=st.integers(min_value=1, max_value=500),
    same_sector=st.booleans(),
    d_network=distances_valid,
    pattern_confidence=unit_floats,
)
def test_property_14_competitive_llr_clamped(
    llr_source: float,
    rho_rolling: float,
    n_observations: int,
    same_sector: bool,
    d_network: int,
    pattern_confidence: float,
) -> None:
    """Property 14: Competitive LLR is clamped to [-1.25, 1.25].

    For any source LLR, shrunk correlation (non-negative), graph distance
    (1-3), and pattern confidence in [0,1], the computed competitive LLR
    SHALL be in [-1.25, 1.25].
    """
    rho_effective = compute_shrunk_correlation(
        rho_rolling=rho_rolling,
        n_observations=n_observations,
        same_sector=same_sector,
    )

    # rho_effective should be non-negative per definition
    assert rho_effective >= 0.0, f"rho_effective={rho_effective} is negative"

    llr_competitive = compute_competitive_llr(
        llr_source=llr_source,
        rho_effective=rho_effective,
        d_network=d_network,
        pattern_confidence=pattern_confidence,
    )

    assert -1.25 <= llr_competitive <= 1.25, (
        f"Competitive LLR={llr_competitive} out of bounds [-1.25, 1.25] "
        f"for llr_source={llr_source}, rho_effective={rho_effective}, "
        f"d_network={d_network}, pattern_confidence={pattern_confidence}"
    )


# ---------------------------------------------------------------------------
# Feature: math-core-v3-engine, Property 15: Graph attenuation is zero
#   beyond max distance
# ---------------------------------------------------------------------------
# **Validates: Requirements 10.4, 10.5**


@settings(max_examples=100)
@given(
    llr_source=llr_source_values,
    rho_effective=unit_floats,
    d_network=distances_beyond,
    pattern_confidence=unit_floats,
)
def test_property_15_zero_attenuation_beyond_max_distance(
    llr_source: float,
    rho_effective: float,
    d_network: int,
    pattern_confidence: float,
) -> None:
    """Property 15: Graph attenuation is zero beyond max distance.

    For any inputs where graph distance > 3, the computed attenuation
    SHALL be 0.0, producing zero competitive LLR regardless of other
    parameters.
    """
    llr_competitive = compute_competitive_llr(
        llr_source=llr_source,
        rho_effective=rho_effective,
        d_network=d_network,
        pattern_confidence=pattern_confidence,
    )

    assert llr_competitive == 0.0, (
        f"Expected 0.0 for d_network={d_network} > 3, got {llr_competitive}"
    )
