"""Tests for queue position modeling."""

import math
import pytest

from horizon._horizon import FillProbResult, QueueModel, queue_fill_prob


# ===========================================================================
# QueueModel construction
# ===========================================================================


class TestQueueModelConstruction:
    def test_basic(self):
        qm = QueueModel(500.0, 200.0)
        assert qm.queue_position() == pytest.approx(200.0)

    def test_orders_ahead_clamped_to_queue(self):
        # orders_ahead should be clamped to initial_queue
        qm = QueueModel(100.0, 500.0)
        assert qm.queue_position() <= 100.0

    def test_zero_queue(self):
        qm = QueueModel(0.0, 0.0)
        assert qm.queue_position() == pytest.approx(0.0)

    def test_negative_queue_raises(self):
        with pytest.raises(ValueError):
            QueueModel(-1.0, 0.0)

    def test_negative_orders_raises(self):
        with pytest.raises(ValueError):
            QueueModel(100.0, -1.0)

    def test_nan_queue_raises(self):
        with pytest.raises(ValueError):
            QueueModel(float("nan"), 0.0)

    def test_inf_queue_raises(self):
        with pytest.raises(ValueError):
            QueueModel(float("inf"), 0.0)

    def test_nan_orders_raises(self):
        with pytest.raises(ValueError):
            QueueModel(100.0, float("nan"))

    def test_repr(self):
        qm = QueueModel(500.0, 200.0)
        r = repr(qm)
        assert "QueueModel" in r

    def test_large_queue(self):
        qm = QueueModel(100000.0, 50000.0)
        assert qm.queue_position() == pytest.approx(50000.0)


# ===========================================================================
# QueueModel update_book
# ===========================================================================


class TestQueueModelUpdateBook:
    def test_basic_update(self):
        qm = QueueModel(500.0, 200.0)
        qm.update_book(480.0, 10.0, 25.0, 5.0)

    def test_multiple_updates(self):
        qm = QueueModel(500.0, 200.0)
        for _ in range(10):
            qm.update_book(480.0, 10.0, 25.0, 5.0)

    def test_negative_queue_size_raises(self):
        qm = QueueModel(500.0, 200.0)
        with pytest.raises(ValueError):
            qm.update_book(-1.0, 10.0, 5.0, 5.0)

    def test_negative_arrived_raises(self):
        qm = QueueModel(500.0, 200.0)
        with pytest.raises(ValueError):
            qm.update_book(480.0, -10.0, 5.0, 5.0)

    def test_negative_cancelled_raises(self):
        qm = QueueModel(500.0, 200.0)
        with pytest.raises(ValueError):
            qm.update_book(480.0, 10.0, -5.0, 5.0)

    def test_negative_filled_raises(self):
        qm = QueueModel(500.0, 200.0)
        with pytest.raises(ValueError):
            qm.update_book(480.0, 10.0, 5.0, -5.0)

    def test_nan_raises(self):
        qm = QueueModel(500.0, 200.0)
        with pytest.raises(ValueError):
            qm.update_book(float("nan"), 10.0, 5.0, 5.0)

    def test_update_with_zero_flow(self):
        qm = QueueModel(500.0, 200.0)
        qm.update_book(500.0, 0.0, 0.0, 0.0)
        # Position should not change
        assert qm.queue_position() == pytest.approx(200.0, abs=1.0)


# ===========================================================================
# QueueModel fill_probability
# ===========================================================================


class TestQueueModelFillProbability:
    def test_basic(self):
        qm = QueueModel(500.0, 200.0)
        qm.update_book(480.0, 10.0, 25.0, 5.0)
        prob = qm.fill_probability(60.0)
        assert 0.0 <= prob <= 1.0

    def test_zero_horizon_zero_prob(self):
        qm = QueueModel(500.0, 200.0)
        qm.update_book(480.0, 10.0, 25.0, 5.0)
        prob = qm.fill_probability(0.0)
        assert prob >= 0.0

    def test_longer_horizon_higher_prob(self):
        qm = QueueModel(500.0, 200.0)
        for _ in range(5):
            qm.update_book(480.0, 10.0, 25.0, 5.0)
        p1 = qm.fill_probability(10.0)
        p2 = qm.fill_probability(100.0)
        assert p2 >= p1 - 1e-10

    def test_front_of_queue_prob_one(self):
        qm = QueueModel(100.0, 0.0)
        prob = qm.fill_probability(60.0)
        assert prob == pytest.approx(1.0)

    def test_negative_horizon_raises(self):
        qm = QueueModel(500.0, 200.0)
        with pytest.raises(ValueError):
            qm.fill_probability(-1.0)

    def test_nan_horizon_raises(self):
        qm = QueueModel(500.0, 200.0)
        with pytest.raises(ValueError):
            qm.fill_probability(float("nan"))

    def test_inf_horizon_raises(self):
        qm = QueueModel(500.0, 200.0)
        with pytest.raises(ValueError):
            qm.fill_probability(float("inf"))

    def test_probability_bounded_by_one(self):
        qm = QueueModel(100.0, 10.0)
        for _ in range(20):
            qm.update_book(90.0, 5.0, 10.0, 5.0)
        prob = qm.fill_probability(10000.0)
        assert prob <= 1.0


# ===========================================================================
# QueueModel expected_fill_time
# ===========================================================================


class TestQueueModelExpectedFillTime:
    def test_basic(self):
        qm = QueueModel(500.0, 200.0)
        qm.update_book(480.0, 10.0, 25.0, 5.0)
        t = qm.expected_fill_time()
        assert t >= 0.0

    def test_front_of_queue_zero_time(self):
        qm = QueueModel(100.0, 0.0)
        t = qm.expected_fill_time()
        assert t == pytest.approx(0.0)

    def test_finite_result(self):
        qm = QueueModel(500.0, 200.0)
        for _ in range(5):
            qm.update_book(480.0, 10.0, 25.0, 5.0)
        t = qm.expected_fill_time()
        assert math.isfinite(t)


# ===========================================================================
# QueueModel rate accessors
# ===========================================================================


class TestQueueModelRates:
    def test_initial_rates_zero(self):
        qm = QueueModel(500.0, 200.0)
        assert qm.arrival_rate() == pytest.approx(0.0)
        assert qm.cancel_rate() == pytest.approx(0.0)

    def test_rates_after_update(self):
        qm = QueueModel(500.0, 200.0)
        qm.update_book(480.0, 10.0, 25.0, 5.0)
        assert qm.arrival_rate() >= 0.0
        assert qm.cancel_rate() >= 0.0

    def test_queue_position_decreases_with_fills(self):
        qm = QueueModel(500.0, 200.0)
        pos_before = qm.queue_position()
        qm.update_book(450.0, 0.0, 0.0, 50.0)
        pos_after = qm.queue_position()
        assert pos_after < pos_before

    def test_rates_non_negative_after_many_updates(self):
        qm = QueueModel(1000.0, 500.0)
        for _ in range(50):
            qm.update_book(950.0, 20.0, 30.0, 10.0)
        assert qm.arrival_rate() >= 0.0
        assert qm.cancel_rate() >= 0.0


# ===========================================================================
# QueueModel fill_prob_result
# ===========================================================================


class TestQueueModelFillProbResult:
    def test_basic(self):
        qm = QueueModel(500.0, 200.0)
        qm.update_book(480.0, 10.0, 25.0, 5.0)
        result = qm.fill_prob_result(60.0)
        assert isinstance(result, FillProbResult)

    def test_fields(self):
        qm = QueueModel(500.0, 200.0)
        qm.update_book(480.0, 10.0, 25.0, 5.0)
        result = qm.fill_prob_result(60.0)
        assert hasattr(result, "probability")
        assert hasattr(result, "expected_time")
        assert hasattr(result, "queue_position")

    def test_probability_range(self):
        qm = QueueModel(500.0, 200.0)
        qm.update_book(480.0, 10.0, 25.0, 5.0)
        result = qm.fill_prob_result(60.0)
        assert 0.0 <= result.probability <= 1.0

    def test_expected_time_non_negative(self):
        qm = QueueModel(500.0, 200.0)
        qm.update_book(480.0, 10.0, 25.0, 5.0)
        result = qm.fill_prob_result(60.0)
        assert result.expected_time >= 0.0

    def test_negative_horizon_raises(self):
        qm = QueueModel(500.0, 200.0)
        with pytest.raises(ValueError):
            qm.fill_prob_result(-1.0)

    def test_queue_position_non_negative(self):
        qm = QueueModel(500.0, 200.0)
        qm.update_book(480.0, 10.0, 25.0, 5.0)
        result = qm.fill_prob_result(60.0)
        assert result.queue_position >= 0.0


# ===========================================================================
# queue_fill_prob (standalone)
# ===========================================================================


class TestQueueFillProb:
    def test_basic(self):
        prob = queue_fill_prob(500.0, 200.0, 10.0, 25.0, 60.0)
        assert 0.0 <= prob <= 1.0

    def test_zero_position_prob_one(self):
        prob = queue_fill_prob(500.0, 0.0, 10.0, 25.0, 60.0)
        assert prob == pytest.approx(1.0)

    def test_zero_horizon_zero_prob(self):
        prob = queue_fill_prob(500.0, 200.0, 10.0, 25.0, 0.0)
        assert prob == pytest.approx(0.0)

    def test_longer_horizon_higher_prob(self):
        p1 = queue_fill_prob(500.0, 200.0, 10.0, 25.0, 10.0)
        p2 = queue_fill_prob(500.0, 200.0, 10.0, 25.0, 100.0)
        assert p2 >= p1 - 1e-10

    def test_negative_queue_raises(self):
        with pytest.raises(ValueError):
            queue_fill_prob(-1.0, 200.0, 10.0, 25.0, 60.0)

    def test_negative_position_raises(self):
        with pytest.raises(ValueError):
            queue_fill_prob(500.0, -1.0, 10.0, 25.0, 60.0)

    def test_negative_arrival_raises(self):
        with pytest.raises(ValueError):
            queue_fill_prob(500.0, 200.0, -1.0, 25.0, 60.0)

    def test_negative_cancel_raises(self):
        with pytest.raises(ValueError):
            queue_fill_prob(500.0, 200.0, 10.0, -1.0, 60.0)

    def test_negative_horizon_raises(self):
        with pytest.raises(ValueError):
            queue_fill_prob(500.0, 200.0, 10.0, 25.0, -1.0)

    def test_nan_raises(self):
        with pytest.raises(ValueError):
            queue_fill_prob(float("nan"), 200.0, 10.0, 25.0, 60.0)

    def test_inf_raises(self):
        with pytest.raises(ValueError):
            queue_fill_prob(float("inf"), 200.0, 10.0, 25.0, 60.0)

    def test_zero_rates_zero_prob(self):
        prob = queue_fill_prob(500.0, 200.0, 0.0, 0.0, 60.0)
        # With near-zero cancel rate, there's still a small floor fill rate
        assert 0.0 <= prob <= 1.0

    def test_higher_cancel_rate_higher_prob(self):
        p1 = queue_fill_prob(500.0, 200.0, 10.0, 5.0, 60.0)
        p2 = queue_fill_prob(500.0, 200.0, 10.0, 50.0, 60.0)
        # Higher cancel rate should generally improve fill probability
        assert p2 >= p1 - 1e-10

    def test_smaller_position_higher_prob(self):
        p1 = queue_fill_prob(500.0, 400.0, 10.0, 25.0, 60.0)
        p2 = queue_fill_prob(500.0, 50.0, 10.0, 25.0, 60.0)
        assert p2 >= p1 - 1e-10
