"""Tests for Breeden-Litzenberger cross-asset probability extraction (Rust)."""

import math
import pytest
import horizon as hz


def _bs_call(s, k, r, t, sigma):
    """Simplified Black-Scholes call price for testing."""
    d1 = (math.log(s / k) + (r + 0.5 * sigma**2) * t) / (sigma * math.sqrt(t))
    d2 = d1 - sigma * math.sqrt(t)
    return s * _ncdf(d1) - k * math.exp(-r * t) * _ncdf(d2)


def _ncdf(x):
    """Standard normal CDF."""
    return 0.5 * (1.0 + math.erf(x / math.sqrt(2.0)))


def _generate_bs_chain(s=100.0, r=0.0, t=1.0, sigma=0.3, k_range=(60, 140)):
    strikes = list(range(k_range[0], k_range[1] + 1))
    strikes_f = [float(k) for k in strikes]
    calls = [_bs_call(s, k, r, t, sigma) for k in strikes_f]
    return strikes_f, calls


class TestRiskNeutralDensity:
    def test_density_integrates_near_one(self):
        strikes, calls = _generate_bs_chain()
        rnd = hz.risk_neutral_density(strikes, calls, r=0.0, t=1.0)
        # CDF should end near 1.0
        last_cdf = rnd.cdf[-1] if rnd.cdf else 0.0
        assert abs(last_cdf - 1.0) < 0.15, f"CDF last = {last_cdf}"

    def test_mean_reasonable(self):
        strikes, calls = _generate_bs_chain(s=100.0)
        rnd = hz.risk_neutral_density(strikes, calls, r=0.0, t=1.0)
        # Mean should be near 100
        assert 70 < rnd.mean < 130

    def test_variance_positive(self):
        strikes, calls = _generate_bs_chain()
        rnd = hz.risk_neutral_density(strikes, calls)
        assert rnd.variance > 0

    def test_too_few_strikes(self):
        rnd = hz.risk_neutral_density([100.0, 110.0], [5.0, 2.0])
        assert len(rnd.densities) == 2

    def test_repr(self):
        strikes, calls = _generate_bs_chain()
        rnd = hz.risk_neutral_density(strikes, calls)
        assert "RiskNeutralDensity" in repr(rnd)


class TestImpliedProbability:
    def test_above_below_sum_to_one(self):
        strikes, calls = _generate_bs_chain()
        above = hz.implied_probability_above(strikes, calls, 100.0)
        below = hz.implied_probability_below(strikes, calls, 100.0)
        assert abs(above + below - 1.0) < 1e-10

    def test_above_decreases_with_threshold(self):
        strikes, calls = _generate_bs_chain()
        p80 = hz.implied_probability_above(strikes, calls, 80.0)
        p120 = hz.implied_probability_above(strikes, calls, 120.0)
        assert p80 > p120

    def test_between_bounded(self):
        strikes, calls = _generate_bs_chain()
        between = hz.implied_probability_between(strikes, calls, 90.0, 110.0)
        above90 = hz.implied_probability_above(strikes, calls, 90.0)
        assert between <= above90 + 1e-10
        assert between >= 0.0


class TestCrossAssetEdge:
    def test_positive_edge(self):
        e = hz.cross_asset_edge(0.7, 0.5, 0.01)
        assert e.raw_edge > 0.0
        assert e.adjusted_edge > 0.0
        assert e.confidence > 0.0

    def test_negative_edge(self):
        e = hz.cross_asset_edge(0.3, 0.5, 0.01)
        assert e.raw_edge < 0.0
        assert e.adjusted_edge < 0.0

    def test_no_edge(self):
        e = hz.cross_asset_edge(0.5, 0.5, 0.01)
        assert abs(e.raw_edge) < 1e-10
        assert abs(e.adjusted_edge) < 1e-10

    def test_confidence_monotonicity(self):
        # Larger edge → higher confidence
        e_small = hz.cross_asset_edge(0.51, 0.5, 0.01)
        e_large = hz.cross_asset_edge(0.6, 0.5, 0.01)
        assert e_large.confidence >= e_small.confidence

    def test_repr(self):
        e = hz.cross_asset_edge(0.7, 0.5)
        assert "CrossAssetEdge" in repr(e)


class TestRiskPremiumAdjustment:
    def test_blend(self):
        r = hz.risk_premium_adjustment(0.8, 0.5, 0.5)
        # 0.8 * 0.5 + 0.5 * 0.5 = 0.65
        assert abs(r - 0.65) < 1e-10

    def test_full_options(self):
        r = hz.risk_premium_adjustment(0.8, 0.5, 1.0)
        assert abs(r - 0.8) < 1e-10

    def test_full_market(self):
        r = hz.risk_premium_adjustment(0.8, 0.5, 0.0)
        assert abs(r - 0.5) < 1e-10

    def test_clamped(self):
        r = hz.risk_premium_adjustment(1.5, 0.5, 0.5)
        assert 0.0 <= r <= 1.0
