"""Tests for market making (Rust functions + Python pipeline)."""

import os
import pytest

os.environ.setdefault("HORIZON_API_KEY", "test-key-123")

from horizon._horizon import (
    Market,
    Quote,
    reservation_price,
    optimal_spread,
    competitive_spread,
    mm_size,
    estimate_volatility,
)
from horizon.context import Context, FeedData, InventorySnapshot
from horizon.mm import market_maker


# ---------------------------------------------------------------------------
# Rust function tests: reservation_price
# ---------------------------------------------------------------------------


class TestReservationPrice:
    def test_no_inventory(self):
        r = reservation_price(0.50, 0.0, 0.5, 0.02, 1.0)
        assert abs(r - 0.50) < 1e-10

    def test_long_inventory_skews_down(self):
        r = reservation_price(0.50, 10.0, 0.5, 0.02, 1.0)
        assert r < 0.50

    def test_short_inventory_skews_up(self):
        r = reservation_price(0.50, -10.0, 0.5, 0.02, 1.0)
        assert r > 0.50

    def test_formula(self):
        # r = 0.50 - 10*0.5*0.02^2*1.0 = 0.50 - 0.002 = 0.498
        r = reservation_price(0.50, 10.0, 0.5, 0.02, 1.0)
        assert abs(r - 0.498) < 1e-10

    def test_nan_returns_mid(self):
        r = reservation_price(0.50, float("nan"), 0.5, 0.02, 1.0)
        assert abs(r - 0.50) < 1e-10

    def test_inf_returns_mid(self):
        r = reservation_price(0.50, float("inf"), 0.5, 0.02, 1.0)
        assert abs(r - 0.50) < 1e-10


# ---------------------------------------------------------------------------
# Rust function tests: optimal_spread
# ---------------------------------------------------------------------------


class TestOptimalSpread:
    def test_positive(self):
        s = optimal_spread(0.02, 10.0, 0.5, 1.5, 1.0)
        assert s > 0.0

    def test_zero_gamma(self):
        assert optimal_spread(0.02, 10.0, 0.0, 1.5, 1.0) == 0.0

    def test_zero_kappa(self):
        assert optimal_spread(0.02, 10.0, 0.5, 0.0, 1.0) == 0.0

    def test_higher_vol_wider_spread(self):
        s_low = optimal_spread(0.01, 10.0, 0.5, 1.5, 1.0)
        s_high = optimal_spread(0.05, 10.0, 0.5, 1.5, 1.0)
        assert s_high > s_low


# ---------------------------------------------------------------------------
# Rust function tests: competitive_spread
# ---------------------------------------------------------------------------


class TestCompetitiveSpread:
    def test_pure_model(self):
        s = competitive_spread(0.04, 0.02, 0.0, 0.0)
        assert abs(s - 0.04) < 1e-10

    def test_pure_book(self):
        s = competitive_spread(0.04, 0.02, 0.0, 1.0)
        assert abs(s - 0.02) < 1e-10

    def test_blend(self):
        s = competitive_spread(0.04, 0.02, 0.0, 0.5)
        assert abs(s - 0.03) < 1e-10

    def test_clamped_min(self):
        s = competitive_spread(0.0001, 0.0001, 0.0, 0.5)
        assert s >= 0.001

    def test_clamped_max(self):
        s = competitive_spread(2.0, 2.0, 0.0, 0.5)
        assert s <= 1.0


# ---------------------------------------------------------------------------
# Rust function tests: mm_size
# ---------------------------------------------------------------------------


class TestMmSize:
    def test_no_inventory(self):
        bid, ask = mm_size(5.0, 0.0, 100.0, 0.3, 0.5)
        assert abs(bid - ask) < 1e-10
        assert bid > 0

    def test_long_skews(self):
        bid, ask = mm_size(5.0, 50.0, 100.0, 0.3, 0.5)
        assert bid < ask  # Reduce buying, increase selling

    def test_short_skews(self):
        bid, ask = mm_size(5.0, -50.0, 100.0, 0.3, 0.5)
        assert bid > ask  # Increase buying, reduce selling

    def test_zero_base(self):
        bid, ask = mm_size(0.0, 10.0, 100.0, 0.3, 0.5)
        assert bid == 0.0 and ask == 0.0

    def test_nan(self):
        bid, ask = mm_size(float("nan"), 10.0, 100.0, 0.3, 0.5)
        assert bid == 0.0 and ask == 0.0


# ---------------------------------------------------------------------------
# Rust function tests: estimate_volatility
# ---------------------------------------------------------------------------


class TestEstimateVolatility:
    def test_constant_prices(self):
        assert estimate_volatility([1.0, 1.0, 1.0, 1.0]) == 0.0

    def test_positive(self):
        vol = estimate_volatility([1.0, 1.01, 0.99, 1.02, 0.98])
        assert vol > 0.0

    def test_too_few(self):
        assert estimate_volatility([1.0]) == 0.0
        assert estimate_volatility([]) == 0.0


# ---------------------------------------------------------------------------
# Python pipeline tests: market_maker
# ---------------------------------------------------------------------------


def _make_ctx(feed_name="feed", bid=0.45, ask=0.55, market_id="mkt1", inventory=0.0):
    from horizon._horizon import Side, Position

    ctx = Context(
        feeds={feed_name: FeedData(bid=bid, ask=ask)},
        inventory=InventorySnapshot(),
        market=Market(market_id),
    )
    return ctx


class TestMarketMaker:
    def test_generates_quotes(self):
        mm = market_maker(feed_name="feed", size=5.0)
        ctx = _make_ctx()
        quotes = mm(ctx)
        assert isinstance(quotes, list)
        assert len(quotes) >= 1
        for q in quotes:
            assert isinstance(q, Quote)
            assert q.bid < q.ask
            assert 0.01 <= q.bid <= 0.99
            assert 0.01 <= q.ask <= 0.99

    def test_multi_level(self):
        mm = market_maker(feed_name="feed", size=5.0, num_levels=3, level_spacing=0.02)
        ctx = _make_ctx()
        quotes = mm(ctx)
        assert len(quotes) >= 2  # May skip levels if crossed

    def test_no_feed_returns_empty(self):
        mm = market_maker(feed_name="missing", size=5.0)
        ctx = Context(feeds={}, inventory=InventorySnapshot())
        quotes = mm(ctx)
        assert quotes == []

    def test_custom_params(self):
        mm = market_maker(
            base_spread=0.02,
            gamma=0.3,
            kappa=2.0,
            aggression=0.7,
            size=10.0,
        )
        ctx = _make_ctx()
        quotes = mm(ctx)
        assert len(quotes) >= 1
        assert quotes[0].size > 0

    def test_first_available_feed(self):
        mm = market_maker(size=5.0)  # No feed_name → use first
        ctx = Context(
            feeds={"auto": FeedData(bid=0.45, ask=0.55)},
            inventory=InventorySnapshot(),
            market=Market("mkt1"),
        )
        quotes = mm(ctx)
        assert len(quotes) >= 1

    def test_price_only_feed(self):
        mm = market_maker(feed_name="feed", size=5.0)
        ctx = Context(
            feeds={"feed": FeedData(price=0.50)},
            inventory=InventorySnapshot(),
            market=Market("mkt1"),
        )
        quotes = mm(ctx)
        assert len(quotes) >= 1
