"""Tests for treasury yield curve + Kelly integration.

Tests cover:
- Rust yield-adjusted Kelly functions (yield_adjusted_kelly, opportunity_cost, etc.)
- TreasuryFeed dataclass creation and env var fallback
- Python pipeline functions (yield_adjusted_sizer, opportunity_cost_filter, collateral_optimizer)
"""

import math
import pytest


# ---------------------------------------------------------------------------
# 1. yield_adjusted_kelly
# ---------------------------------------------------------------------------


class TestYieldAdjustedKelly:
    def test_basic(self):
        """With no risk-free rate, should equal fractional kelly."""
        from horizon._horizon import yield_adjusted_kelly, fractional_kelly

        # rfr=0 means no yield adjustment, should equal fractional kelly
        ya = yield_adjusted_kelly(0.60, 0.50, 0.0, 30.0, 0.5)
        fk = fractional_kelly(0.60, 0.50, 0.5)
        assert ya == pytest.approx(fk, abs=1e-10)

    def test_with_rfr(self):
        """Positive risk-free rate should reduce Kelly fraction."""
        from horizon._horizon import yield_adjusted_kelly

        no_rfr = yield_adjusted_kelly(0.60, 0.50, 0.0, 30.0, 0.5)
        with_rfr = yield_adjusted_kelly(0.60, 0.50, 0.05, 30.0, 0.5)
        assert with_rfr <= no_rfr

    def test_no_edge_after_rfr(self):
        """When edge barely exceeds rfr cost, result should be very small or zero."""
        from horizon._horizon import yield_adjusted_kelly

        # Tiny edge (0.51 vs 0.50), high rfr, long duration
        result = yield_adjusted_kelly(0.501, 0.50, 0.10, 365.0, 1.0)
        assert result >= 0.0
        assert result < 0.1  # should be very small

    def test_invalid_prob_above_one(self):
        from horizon._horizon import yield_adjusted_kelly

        result = yield_adjusted_kelly(1.5, 0.50, 0.05, 30.0, 0.5)
        assert result == 0.0

    def test_invalid_prob_negative(self):
        from horizon._horizon import yield_adjusted_kelly

        result = yield_adjusted_kelly(-0.1, 0.50, 0.05, 30.0, 0.5)
        assert result == 0.0

    def test_invalid_price_zero(self):
        from horizon._horizon import yield_adjusted_kelly

        result = yield_adjusted_kelly(0.60, 0.0, 0.05, 30.0, 0.5)
        assert result == 0.0

    def test_invalid_price_one(self):
        from horizon._horizon import yield_adjusted_kelly

        result = yield_adjusted_kelly(0.60, 1.0, 0.05, 30.0, 0.5)
        assert result == 0.0

    def test_nan_input(self):
        from horizon._horizon import yield_adjusted_kelly

        result = yield_adjusted_kelly(float("nan"), 0.50, 0.05, 30.0, 0.5)
        assert result == 0.0

    def test_inf_input(self):
        from horizon._horizon import yield_adjusted_kelly

        result = yield_adjusted_kelly(0.60, 0.50, float("inf"), 30.0, 0.5)
        assert result == 0.0


# ---------------------------------------------------------------------------
# 2. opportunity_cost
# ---------------------------------------------------------------------------


class TestOpportunityCost:
    def test_basic(self):
        from horizon._horizon import opportunity_cost

        # $1000 at 5% for 30 days = 1000 * 0.05 * 30/365
        oc = opportunity_cost(1000.0, 0.05, 30.0)
        expected = 1000.0 * 0.05 * 30.0 / 365.0
        assert oc == pytest.approx(expected, rel=1e-6)

    def test_zero_notional(self):
        from horizon._horizon import opportunity_cost

        assert opportunity_cost(0.0, 0.05, 30.0) == pytest.approx(0.0, abs=1e-10)

    def test_zero_rfr(self):
        from horizon._horizon import opportunity_cost

        assert opportunity_cost(1000.0, 0.0, 30.0) == pytest.approx(0.0, abs=1e-10)

    def test_zero_duration(self):
        from horizon._horizon import opportunity_cost

        assert opportunity_cost(1000.0, 0.05, 0.0) == pytest.approx(0.0, abs=1e-10)

    def test_invalid_negative_notional(self):
        from horizon._horizon import opportunity_cost

        assert opportunity_cost(-100.0, 0.05, 30.0) == 0.0

    def test_invalid_nan(self):
        from horizon._horizon import opportunity_cost

        assert opportunity_cost(float("nan"), 0.05, 30.0) == 0.0

    def test_invalid_inf(self):
        from horizon._horizon import opportunity_cost

        assert opportunity_cost(float("inf"), 0.05, 30.0) == 0.0


# ---------------------------------------------------------------------------
# 3. breakeven_edge
# ---------------------------------------------------------------------------


class TestBreakevenEdge:
    def test_basic(self):
        from horizon._horizon import breakeven_edge

        # 5% rate, 30 days = 0.05 * 30/365
        be = breakeven_edge(0.05, 30.0)
        expected = 0.05 * 30.0 / 365.0
        assert be == pytest.approx(expected, rel=1e-6)

    def test_zero_rfr(self):
        from horizon._horizon import breakeven_edge

        assert breakeven_edge(0.0, 30.0) == pytest.approx(0.0, abs=1e-10)

    def test_zero_duration(self):
        from horizon._horizon import breakeven_edge

        assert breakeven_edge(0.05, 0.0) == pytest.approx(0.0, abs=1e-10)

    def test_invalid_negative_duration(self):
        from horizon._horizon import breakeven_edge

        assert breakeven_edge(0.05, -10.0) == 0.0

    def test_invalid_nan(self):
        from horizon._horizon import breakeven_edge

        assert breakeven_edge(float("nan"), 30.0) == 0.0

    def test_invalid_inf(self):
        from horizon._horizon import breakeven_edge

        assert breakeven_edge(float("inf"), 30.0) == 0.0


# ---------------------------------------------------------------------------
# 4. yield_adjusted_kelly_size
# ---------------------------------------------------------------------------


class TestYieldAdjustedKellySize:
    def test_basic(self):
        from horizon._horizon import yield_adjusted_kelly_size

        size = yield_adjusted_kelly_size(0.60, 0.50, 0.05, 30.0, 10000.0, 0.5, 500.0)
        assert size > 0.0
        assert size <= 500.0  # capped at max_size

    def test_zero_bankroll(self):
        from horizon._horizon import yield_adjusted_kelly_size

        size = yield_adjusted_kelly_size(0.60, 0.50, 0.05, 30.0, 0.0, 0.5, 500.0)
        assert size == 0.0

    def test_zero_price(self):
        from horizon._horizon import yield_adjusted_kelly_size

        size = yield_adjusted_kelly_size(0.60, 0.0, 0.05, 30.0, 10000.0, 0.5, 500.0)
        assert size == 0.0

    def test_no_edge(self):
        from horizon._horizon import yield_adjusted_kelly_size

        size = yield_adjusted_kelly_size(0.50, 0.50, 0.05, 30.0, 10000.0, 0.5, 500.0)
        assert size == 0.0

    def test_capped_at_max(self):
        from horizon._horizon import yield_adjusted_kelly_size

        # Very large edge, small max
        size = yield_adjusted_kelly_size(0.95, 0.10, 0.0, 30.0, 100000.0, 1.0, 50.0)
        assert size <= 50.0


# ---------------------------------------------------------------------------
# 5. TreasuryFeed dataclass
# ---------------------------------------------------------------------------


class TestTreasuryFeed:
    def test_dataclass_creation(self):
        from horizon.feeds import TreasuryFeed

        feed = TreasuryFeed(api_key="test-key")
        assert feed.api_key == "test-key"
        assert feed._type == "treasury"
        assert "DGS10" in feed.series_ids
        assert feed.primary_series == "DGS10"
        assert feed.interval == 300.0

    def test_env_var_fallback(self):
        import os
        from horizon.feeds import TreasuryFeed

        old = os.environ.get("FRED_API_KEY")
        try:
            os.environ["FRED_API_KEY"] = "env-test-key"
            feed = TreasuryFeed()
            assert feed.api_key == "env-test-key"
        finally:
            if old is not None:
                os.environ["FRED_API_KEY"] = old
            elif "FRED_API_KEY" in os.environ:
                del os.environ["FRED_API_KEY"]

    def test_custom_series(self):
        from horizon.feeds import TreasuryFeed

        feed = TreasuryFeed(series_ids=["DGS1", "DGS5"], primary_series="DGS1")
        assert feed.series_ids == ["DGS1", "DGS5"]
        assert feed.primary_series == "DGS1"


# ---------------------------------------------------------------------------
# 6. Pipeline functions (Ultra tier)
# ---------------------------------------------------------------------------


class TestTreasuryPipeline:
    def test_yield_adjusted_sizer(self):
        try:
            from horizon.treasury import yield_adjusted_sizer
        except PermissionError:
            pytest.skip("Ultra tier required")

        fn = yield_adjusted_sizer(bankroll=10000.0, fraction=0.5, max_size=500.0)
        assert callable(fn)
        assert fn.__name__ == "yield_adjusted_sizer"

    def test_opportunity_cost_filter(self):
        try:
            from horizon.treasury import opportunity_cost_filter
        except PermissionError:
            pytest.skip("Ultra tier required")

        fn = opportunity_cost_filter(min_edge_multiple=2.0)
        assert callable(fn)
        assert fn.__name__ == "opportunity_cost_filter"

    def test_collateral_optimizer(self):
        try:
            from horizon.treasury import collateral_optimizer
        except PermissionError:
            pytest.skip("Ultra tier required")

        fn = collateral_optimizer(max_utilization=0.8)
        assert callable(fn)
        assert fn.__name__ == "collateral_optimizer"
