"""Tests for BookSim exchange — L2 matching, fill models, impact, latency."""

import pytest
from horizon._horizon import (
    Engine,
    Market,
    Quote,
    RiskConfig,
    Side,
    OrderSide,
    OrderRequest,
    OrderType,
    TimeInForce,
)
from horizon.backtest import backtest


@pytest.fixture
def relaxed_risk():
    return RiskConfig(
        max_position_per_market=10000.0,
        max_portfolio_notional=100000.0,
        max_daily_drawdown_pct=100.0,
        max_order_size=10000.0,
        rate_limit_sustained=1_000_000,
        rate_limit_burst=1_000_000,
        dedup_window_ms=0,
    )


class TestBookSimEngine:
    """Test BookSim exchange via Engine API."""

    def test_create_book_sim_engine(self, relaxed_risk):
        engine = Engine(
            risk_config=relaxed_risk,
            exchange_type="book_sim",
            fill_model="deterministic",
        )
        assert engine.exchange_name() == "book_sim"

    def test_update_book_and_tick(self, relaxed_risk):
        engine = Engine(
            risk_config=relaxed_risk,
            exchange_type="book_sim",
            fill_model="deterministic",
        )
        # Update book
        bids = [(0.55, 100.0), (0.54, 200.0)]
        asks = [(0.56, 80.0), (0.57, 150.0)]
        engine.update_book("mkt", bids, asks, 1.0)

        # Submit buy order that should fill through asks
        req = OrderRequest("mkt", Side.Yes, OrderSide.Buy, 50.0, 0.57)
        engine.submit_order(req)

        # Tick to match
        fills = engine.tick("mkt", 0.0)  # mid_price ignored for BookSim
        assert fills > 0

    def test_no_fill_without_book(self, relaxed_risk):
        engine = Engine(
            risk_config=relaxed_risk,
            exchange_type="book_sim",
            fill_model="deterministic",
        )
        req = OrderRequest("mkt", Side.Yes, OrderSide.Buy, 10.0, 0.60)
        engine.submit_order(req)
        fills = engine.tick("mkt", 0.0)
        assert fills == 0

    def test_sell_fills_through_bids(self, relaxed_risk):
        engine = Engine(
            risk_config=relaxed_risk,
            exchange_type="book_sim",
            fill_model="deterministic",
        )
        bids = [(0.55, 100.0), (0.54, 200.0)]
        asks = [(0.56, 80.0)]
        engine.update_book("mkt", bids, asks, 1.0)

        req = OrderRequest("mkt", Side.Yes, OrderSide.Sell, 50.0, 0.54)
        engine.submit_order(req)
        fills = engine.tick("mkt", 0.0)
        assert fills > 0

        recent = engine.recent_fills()
        assert len(recent) >= 1
        assert recent[0].size == pytest.approx(50.0, abs=1e-6)

    def test_probabilistic_fill_model(self, relaxed_risk):
        """Probabilistic model should produce stochastic fills."""
        fill_count = 0
        for seed in range(50):
            engine = Engine(
                risk_config=relaxed_risk,
                exchange_type="book_sim",
                fill_model="probabilistic",
                fill_lambda=5.0,
                fill_queue_position=0.5,
                rng_seed=seed,
            )
            bids = [(0.50, 100.0)]
            asks = [(0.52, 1000.0)]
            engine.update_book("mkt", bids, asks, 1.0)

            req = OrderRequest("mkt", Side.Yes, OrderSide.Buy, 10.0, 0.52)
            engine.submit_order(req)
            if engine.tick("mkt", 0.0) > 0:
                fill_count += 1

        assert fill_count > 0, "should fill at least once"
        assert fill_count < 50, "should not fill every time"

    def test_glft_fill_model(self, relaxed_risk):
        engine = Engine(
            risk_config=relaxed_risk,
            exchange_type="book_sim",
            fill_model="glft",
            fill_intensity=1.0,
            fill_kappa=1.5,
            rng_seed=42,
        )
        bids = [(0.50, 100.0)]
        asks = [(0.52, 1000.0)]
        engine.update_book("mkt", bids, asks, 1.0)

        req = OrderRequest("mkt", Side.Yes, OrderSide.Buy, 10.0, 0.52)
        engine.submit_order(req)
        # Should at least be able to tick without error
        engine.tick("mkt", 0.0)

    def test_latency_delays_orders(self, relaxed_risk):
        engine = Engine(
            risk_config=relaxed_risk,
            exchange_type="book_sim",
            fill_model="deterministic",
            latency_ticks=2,
        )
        bids = [(0.50, 100.0)]
        asks = [(0.52, 100.0)]
        engine.update_book("mkt", bids, asks, 1.0)

        req = OrderRequest("mkt", Side.Yes, OrderSide.Buy, 10.0, 0.52)
        engine.submit_order(req)

        # Tick 1: order in latency queue
        assert engine.tick("mkt", 0.0) == 0

        # Tick 2: order promoted and fills
        assert engine.tick("mkt", 0.0) > 0

    def test_impact_raises_price(self, relaxed_risk):
        engine = Engine(
            risk_config=relaxed_risk,
            exchange_type="book_sim",
            fill_model="deterministic",
            impact_temporary_bps=100.0,  # 100 bps
        )
        asks = [(0.52, 200.0)]
        bids = [(0.50, 200.0)]
        engine.update_book("mkt", bids, asks, 1.0)

        req = OrderRequest("mkt", Side.Yes, OrderSide.Buy, 100.0, 0.60)
        engine.submit_order(req)
        engine.tick("mkt", 0.0)

        recent = engine.recent_fills()
        assert len(recent) >= 1
        # With impact, effective price should be above the ask
        assert recent[0].price >= 0.52

    def test_update_book_rejects_non_booksim(self):
        engine = Engine(exchange_type="paper")
        with pytest.raises(Exception, match="BookSim"):
            engine.update_book("mkt", [(0.5, 10.0)], [(0.6, 10.0)])


class TestBookSimBacktest:
    """Test BookSim through the backtest() API."""

    def test_backtest_with_book_data(self):
        data = [
            {"timestamp": 1.0, "price": 0.50},
            {"timestamp": 2.0, "price": 0.50},
            {"timestamp": 3.0, "price": 0.50},
        ]
        book_data = {
            "market": [
                {
                    "timestamp": 1.0,
                    "bids": [(0.48, 100), (0.47, 200)],
                    "asks": [(0.52, 100), (0.53, 200)],
                },
                {
                    "timestamp": 2.0,
                    "bids": [(0.49, 150), (0.48, 250)],
                    "asks": [(0.51, 150), (0.52, 250)],
                },
                {
                    "timestamp": 3.0,
                    "bids": [(0.49, 150), (0.48, 250)],
                    "asks": [(0.51, 150), (0.52, 250)],
                },
            ]
        }

        result = backtest(
            data=data,
            book_data=book_data,
            pipeline=[lambda ctx: Quote(0.48, 0.52, 5)],
            fill_model="deterministic",
        )
        assert len(result.equity_curve) == 3

    def test_backtest_probabilistic_model(self):
        data = [
            {"timestamp": float(i), "price": 0.50}
            for i in range(1, 21)
        ]
        book_data = {
            "market": [
                {
                    "timestamp": float(i),
                    "bids": [(0.48, 100)],
                    "asks": [(0.52, 100)],
                }
                for i in range(1, 21)
            ]
        }

        result = backtest(
            data=data,
            book_data=book_data,
            pipeline=[lambda ctx: Quote(0.48, 0.52, 5)],
            fill_model="probabilistic",
            fill_model_params={"lambda": 2.0, "queue_frac": 0.3},
            rng_seed=42,
        )
        assert len(result.equity_curve) == 20

    def test_backtest_with_latency(self):
        data = [
            {"timestamp": float(i), "price": 0.50}
            for i in range(1, 11)
        ]
        book_data = {
            "market": [
                {
                    "timestamp": float(i),
                    "bids": [(0.48, 100)],
                    "asks": [(0.52, 100)],
                }
                for i in range(1, 11)
            ]
        }

        result = backtest(
            data=data,
            book_data=book_data,
            pipeline=[lambda ctx: Quote(0.48, 0.52, 5)],
            latency_ms=2000.0,  # 2 second latency
        )
        assert len(result.equity_curve) == 10

    def test_backtest_without_book_data_uses_paper(self):
        """When no book_data and fill_model=deterministic, use paper exchange."""
        data = [
            {"timestamp": 1.0, "price": 0.50},
            {"timestamp": 2.0, "price": 0.50},
        ]
        result = backtest(
            data=data,
            pipeline=[lambda ctx: Quote(0.48, 0.52, 5)],
        )
        assert len(result.equity_curve) == 2
