"""Tests for adaptive spread modifier."""

import os

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

from horizon._horizon import Market, Quote
from horizon.context import Context, FeedData, InventorySnapshot
from horizon.adaptive_spread import adaptive_spread, SpreadMetrics


def _make_ctx(market_id="mkt1", bid=0.45, ask=0.55, fills_this_cycle=None, params=None):
    market = Market(id=market_id, name=market_id, slug=market_id)
    p = params if params is not None else {}
    if fills_this_cycle:
        p["fills_this_cycle"] = fills_this_cycle
    return Context(
        feeds={"feed": FeedData(price=0.5, bid=bid, ask=ask, timestamp=1.0)},
        inventory=InventorySnapshot(),
        market=market,
        params=p,
    )


class TestSpreadMetrics:
    def test_defaults(self):
        m = SpreadMetrics()
        assert m.fill_rate == 0.0
        assert m.realized_vol == 0.0
        assert m.spread_multiplier == 1.0


class TestAdaptiveSpread:
    def test_no_quotes_returns_empty(self):
        spread = adaptive_spread()
        ctx = _make_ctx()
        assert spread(ctx, []) == []
        assert spread(ctx, None) == []

    def test_base_case_multiplier_near_one(self):
        spread = adaptive_spread()
        ctx = _make_ctx()
        quotes = [Quote(bid=0.45, ask=0.55, size=5.0)]
        result = spread(ctx, quotes)
        assert len(result) == 1
        # Multiplier should be ~1.0 with no fills and no vol
        metrics = ctx.params["spread_metrics"]
        assert isinstance(metrics, SpreadMetrics)
        assert metrics.fill_rate == 0.0

    def test_high_fill_rate_widens_spread(self):
        spread = adaptive_spread(fill_rate_window=10, fill_rate_target=0.3)
        # Simulate many fills
        for _ in range(10):
            ctx = _make_ctx(fills_this_cycle={"mkt1": True})
            quotes = [Quote(bid=0.45, ask=0.55, size=5.0)]
            result = spread(ctx, quotes)
        metrics = ctx.params["spread_metrics"]
        assert metrics.fill_rate > 0.5
        assert metrics.spread_multiplier > 1.0
        # Spread should be wider than original 0.10
        assert (result[0].ask - result[0].bid) > 0.10

    def test_no_fills_keeps_tight(self):
        spread = adaptive_spread(fill_rate_window=10)
        for _ in range(10):
            ctx = _make_ctx()
            quotes = [Quote(bid=0.45, ask=0.55, size=5.0)]
            result = spread(ctx, quotes)
        metrics = ctx.params["spread_metrics"]
        assert metrics.fill_rate == 0.0
        # Spread should not be wider than original (multiplier ~1.0)
        original_spread = 0.10
        actual_spread = result[0].ask - result[0].bid
        assert actual_spread <= original_spread * 1.1  # Small tolerance for vol

    def test_multiplier_clamped_min(self):
        spread = adaptive_spread(min_multiplier=0.8, max_multiplier=3.0)
        ctx = _make_ctx()
        quotes = [Quote(bid=0.45, ask=0.55, size=5.0)]
        spread(ctx, quotes)
        assert ctx.params["spread_metrics"].spread_multiplier >= 0.8

    def test_multiplier_clamped_max(self):
        spread = adaptive_spread(
            fill_rate_window=5, fill_rate_target=0.01,
            vol_sensitivity=100.0, max_multiplier=2.0,
        )
        # Feed lots of fills to push multiplier high
        for _ in range(5):
            ctx = _make_ctx(fills_this_cycle={"mkt1": True})
            quotes = [Quote(bid=0.45, ask=0.55, size=5.0)]
            spread(ctx, quotes)
        assert ctx.params["spread_metrics"].spread_multiplier <= 2.0

    def test_volatility_widens_spread(self):
        spread = adaptive_spread(vol_window=10, vol_sensitivity=5.0)
        # Feed volatile prices to build up vol estimate
        prices = [0.3, 0.7, 0.3, 0.7, 0.3, 0.7, 0.3, 0.7, 0.3, 0.7]
        for p in prices:
            ctx = _make_ctx(bid=p - 0.05, ask=p + 0.05)
            quotes = [Quote(bid=0.45, ask=0.55, size=5.0)]
            result = spread(ctx, quotes)
        metrics = ctx.params["spread_metrics"]
        assert metrics.realized_vol > 0
        assert metrics.spread_multiplier > 1.0

    def test_clamped_to_prediction_range(self):
        spread = adaptive_spread(max_multiplier=10.0)
        ctx = _make_ctx()
        # Wide original spread near limits
        quotes = [Quote(bid=0.02, ask=0.98, size=5.0)]
        result = spread(ctx, quotes)
        for q in result:
            assert q.bid >= 0.01
            assert q.ask <= 0.99

    def test_bid_less_than_ask(self):
        spread = adaptive_spread(max_multiplier=5.0)
        ctx = _make_ctx()
        quotes = [Quote(bid=0.499, ask=0.501, size=5.0)]
        result = spread(ctx, quotes)
        for q in result:
            assert q.bid < q.ask

    def test_multiple_quotes(self):
        spread = adaptive_spread()
        ctx = _make_ctx()
        quotes = [
            Quote(bid=0.40, ask=0.50, size=5.0),
            Quote(bid=0.45, ask=0.55, size=3.0),
        ]
        result = spread(ctx, quotes)
        assert len(result) == 2

    def test_per_market_isolation(self):
        spread = adaptive_spread(fill_rate_window=5)
        # Market A gets fills
        for _ in range(5):
            ctx_a = _make_ctx(market_id="mkt_a", fills_this_cycle={"mkt_a": True})
            spread(ctx_a, [Quote(bid=0.45, ask=0.55, size=5.0)])
        # Market B: no fills
        for _ in range(5):
            ctx_b = _make_ctx(market_id="mkt_b")
            spread(ctx_b, [Quote(bid=0.45, ask=0.55, size=5.0)])
        metrics_a = ctx_a.params["spread_metrics"]
        metrics_b = ctx_b.params["spread_metrics"]
        assert metrics_a.fill_rate > metrics_b.fill_rate

    def test_function_name(self):
        spread = adaptive_spread()
        assert spread.__name__ == "adaptive_spread"
