"""Tests for characteristic function pricing."""

import math
import pytest

from horizon._horizon import (
    BinaryPrice,
    HestonParams,
    MertonParams,
    VGParams,
    binary_price_heston,
    binary_price_merton,
    binary_price_vg,
    implied_vol_from_binary,
)


# ===========================================================================
# HestonParams
# ===========================================================================


class TestHestonParams:
    def test_creation(self):
        params = HestonParams(v0=0.04, kappa=2.0, theta=0.04, sigma_v=0.3, rho=-0.7)
        assert params is not None

    def test_fields(self):
        params = HestonParams(v0=0.04, kappa=2.0, theta=0.04, sigma_v=0.3, rho=-0.7)
        assert params.v0 == pytest.approx(0.04)
        assert params.kappa == pytest.approx(2.0)
        assert params.theta == pytest.approx(0.04)
        assert params.sigma_v == pytest.approx(0.3)
        assert params.rho == pytest.approx(-0.7)

    def test_default_values(self):
        params = HestonParams()
        assert params.v0 == pytest.approx(0.04)
        assert params.kappa == pytest.approx(2.0)
        assert params.theta == pytest.approx(0.04)
        assert params.sigma_v == pytest.approx(0.3)
        assert params.rho == pytest.approx(-0.7)

    def test_custom_rho(self):
        params = HestonParams(rho=0.5)
        assert params.rho == pytest.approx(0.5)

    def test_repr(self):
        params = HestonParams()
        r = repr(params)
        assert "HestonParams" in r


# ===========================================================================
# MertonParams
# ===========================================================================


class TestMertonParams:
    def test_creation(self):
        params = MertonParams(jump_intensity=0.1, jump_mean=-0.05, jump_vol=0.1)
        assert params is not None

    def test_fields(self):
        params = MertonParams(jump_intensity=0.1, jump_mean=-0.05, jump_vol=0.1)
        assert params.jump_intensity == pytest.approx(0.1)
        assert params.jump_mean == pytest.approx(-0.05)
        assert params.jump_vol == pytest.approx(0.1)

    def test_default_values(self):
        params = MertonParams()
        assert params.jump_intensity == pytest.approx(0.1)
        assert params.jump_mean == pytest.approx(-0.1)
        assert params.jump_vol == pytest.approx(0.2)

    def test_repr(self):
        params = MertonParams()
        r = repr(params)
        assert "MertonParams" in r

    def test_zero_intensity(self):
        params = MertonParams(jump_intensity=0.0)
        assert params.jump_intensity == pytest.approx(0.0)


# ===========================================================================
# VGParams
# ===========================================================================


class TestVGParams:
    def test_creation(self):
        params = VGParams(sigma=0.2, theta=-0.1, nu=0.5)
        assert params is not None

    def test_fields(self):
        params = VGParams(sigma=0.2, theta=-0.1, nu=0.5)
        assert params.sigma == pytest.approx(0.2)
        assert params.theta == pytest.approx(-0.1)
        assert params.nu == pytest.approx(0.5)

    def test_default_values(self):
        params = VGParams()
        assert params.sigma == pytest.approx(0.2)
        assert params.theta == pytest.approx(-0.1)
        assert params.nu == pytest.approx(0.5)

    def test_repr(self):
        params = VGParams()
        r = repr(params)
        assert "VGParams" in r


# ===========================================================================
# binary_price_heston
# ===========================================================================


class TestBinaryPriceHeston:
    def test_basic(self):
        params = HestonParams(v0=0.04, kappa=2.0, theta=0.04, sigma_v=0.3, rho=-0.7)
        result = binary_price_heston(0.5, 0.25, params)
        assert isinstance(result, BinaryPrice)
        assert 0.0 <= result.price <= 1.0

    def test_result_fields(self):
        params = HestonParams(v0=0.04, kappa=2.0, theta=0.04, sigma_v=0.3, rho=-0.7)
        result = binary_price_heston(0.5, 0.25, params)
        assert hasattr(result, "price")
        assert math.isfinite(result.price)

    def test_atm_near_half(self):
        params = HestonParams(v0=0.04, kappa=2.0, theta=0.04, sigma_v=0.3, rho=0.0)
        result = binary_price_heston(1.0, 0.25, params, s0=1.0)
        assert 0.2 <= result.price <= 0.8

    def test_high_strike_near_one(self):
        # P(S_T < K) with K >> S0 should be high
        params = HestonParams(v0=0.04, kappa=2.0, theta=0.04, sigma_v=0.3, rho=-0.7)
        result = binary_price_heston(2.0, 0.25, params, s0=1.0)
        assert result.price > 0.5

    def test_low_strike_near_zero(self):
        # P(S_T < K) with K << S0 should be low
        params = HestonParams(v0=0.04, kappa=2.0, theta=0.04, sigma_v=0.3, rho=-0.7)
        result = binary_price_heston(0.1, 0.25, params, s0=1.0)
        assert result.price < 0.5

    def test_custom_r_and_s0(self):
        params = HestonParams(v0=0.04, kappa=2.0, theta=0.04, sigma_v=0.3, rho=-0.7)
        result = binary_price_heston(100.0, 0.5, params, r=0.05, s0=100.0)
        assert 0.0 <= result.price <= 1.0

    def test_longer_expiry_valid(self):
        params = HestonParams(v0=0.04, kappa=2.0, theta=0.04, sigma_v=0.3, rho=-0.7)
        r_short = binary_price_heston(1.0, 0.01, params, s0=1.0)
        r_long = binary_price_heston(1.0, 2.0, params, s0=1.0)
        assert 0.0 <= r_short.price <= 1.0
        assert 0.0 <= r_long.price <= 1.0

    def test_price_is_finite(self):
        params = HestonParams()
        result = binary_price_heston(1.0, 1.0, params, s0=1.0)
        assert math.isfinite(result.price)

    def test_has_model(self):
        params = HestonParams()
        result = binary_price_heston(1.0, 0.25, params, s0=1.0)
        assert hasattr(result, "model")

    def test_repr(self):
        params = HestonParams()
        result = binary_price_heston(1.0, 0.25, params, s0=1.0)
        r = repr(result)
        assert "BinaryPrice" in r

    def test_different_rho_different_price(self):
        p1 = HestonParams(rho=-0.9)
        p2 = HestonParams(rho=0.0)
        r1 = binary_price_heston(1.0, 0.5, p1, s0=1.0)
        r2 = binary_price_heston(1.0, 0.5, p2, s0=1.0)
        # Different rho should give different prices generally
        assert math.isfinite(r1.price)
        assert math.isfinite(r2.price)


# ===========================================================================
# binary_price_merton
# ===========================================================================


class TestBinaryPriceMerton:
    def test_basic(self):
        params = MertonParams(jump_intensity=0.1, jump_mean=-0.05, jump_vol=0.1)
        result = binary_price_merton(0.5, 0.25, 0.2, params)
        assert 0.0 <= result.price <= 1.0

    def test_no_jumps_near_bs(self):
        params = MertonParams(jump_intensity=0.0, jump_mean=0.0, jump_vol=0.01)
        result = binary_price_merton(1.0, 0.25, 0.2, params, s0=1.0)
        assert 0.2 <= result.price <= 0.8

    def test_with_custom_s0(self):
        params = MertonParams()
        result = binary_price_merton(100.0, 0.5, 0.2, params, s0=100.0)
        assert 0.0 <= result.price <= 1.0

    def test_price_finite(self):
        params = MertonParams()
        result = binary_price_merton(1.0, 1.0, 0.2, params, s0=1.0)
        assert math.isfinite(result.price)

    def test_higher_vol_wider_distribution(self):
        params = MertonParams()
        r_low = binary_price_merton(1.0, 0.5, 0.1, params, s0=1.0)
        r_high = binary_price_merton(1.0, 0.5, 0.5, params, s0=1.0)
        # Both should be valid prices
        assert 0.0 <= r_low.price <= 1.0
        assert 0.0 <= r_high.price <= 1.0


# ===========================================================================
# binary_price_vg
# ===========================================================================


class TestBinaryPriceVG:
    def test_basic(self):
        params = VGParams(sigma=0.2, theta=-0.1, nu=0.5)
        result = binary_price_vg(0.5, 0.25, params)
        assert 0.0 <= result.price <= 1.0

    def test_atm(self):
        params = VGParams(sigma=0.2, theta=0.0, nu=0.5)
        result = binary_price_vg(1.0, 0.25, params, s0=1.0)
        assert 0.2 <= result.price <= 0.8

    def test_price_finite(self):
        params = VGParams()
        result = binary_price_vg(1.0, 1.0, params, s0=1.0)
        assert math.isfinite(result.price)

    def test_with_custom_r(self):
        params = VGParams()
        result = binary_price_vg(1.0, 0.5, params, r=0.05, s0=1.0)
        assert 0.0 <= result.price <= 1.0

    def test_different_nu_different_price(self):
        p1 = VGParams(nu=0.1)
        p2 = VGParams(nu=2.0)
        r1 = binary_price_vg(1.0, 0.5, p1, s0=1.0)
        r2 = binary_price_vg(1.0, 0.5, p2, s0=1.0)
        assert math.isfinite(r1.price)
        assert math.isfinite(r2.price)

    def test_repr(self):
        params = VGParams()
        result = binary_price_vg(1.0, 0.25, params)
        r = repr(result)
        assert "BinaryPrice" in r


# ===========================================================================
# implied_vol_from_binary
# ===========================================================================


class TestImpliedVolFromBinary:
    def test_basic(self):
        iv = implied_vol_from_binary(0.5, 1.0, 0.25)
        assert math.isfinite(iv)
        assert iv > 0.0

    def test_deep_itm(self):
        iv = implied_vol_from_binary(0.9, 0.5, 0.25, s0=1.0)
        assert iv > 0.0

    def test_deep_otm(self):
        iv = implied_vol_from_binary(0.1, 1.5, 0.25, s0=1.0)
        assert iv > 0.0

    def test_roundtrip_with_heston(self):
        params = HestonParams(v0=0.04, kappa=2.0, theta=0.04, sigma_v=0.3, rho=-0.5)
        strike = 1.0
        t = 0.5
        bp = binary_price_heston(strike, t, params, s0=1.0)
        iv = implied_vol_from_binary(bp.price, strike, t, s0=1.0)
        assert iv > 0.0
        assert math.isfinite(iv)

    def test_atm_price(self):
        iv = implied_vol_from_binary(0.5, 1.0, 0.25, s0=1.0)
        assert iv > 0.0
        assert math.isfinite(iv)

    def test_with_custom_r(self):
        iv = implied_vol_from_binary(0.5, 1.0, 0.25, r=0.05, s0=1.0)
        assert iv > 0.0
        assert math.isfinite(iv)

    def test_longer_maturity(self):
        iv = implied_vol_from_binary(0.5, 1.0, 2.0, s0=1.0)
        assert iv > 0.0
        assert math.isfinite(iv)
