"""Tests for execution algorithms: TWAP, VWAP, Iceberg."""

import time

import pytest
from horizon import (
    Engine,
    OrderRequest,
    OrderSide,
    RiskConfig,
    Side,
)
from horizon.algos import TWAP, VWAP, ExecAlgo, Iceberg


@pytest.fixture
def engine():
    config = RiskConfig(
        max_position_per_market=1000.0,
        max_portfolio_notional=10000.0,
        max_order_size=500.0,
        dedup_window_ms=0,
    )
    return Engine(risk_config=config, paper_fee_rate=0.0)


def buy_request(market="mkt_1", price=0.50, size=100.0):
    return OrderRequest(
        market_id=market,
        side=Side.Yes,
        order_side=OrderSide.Buy,
        size=size,
        price=price,
    )


# ============================================================
# TWAP
# ============================================================


class TestTWAP:
    def test_twap_creates_slices(self, engine):
        """TWAP should submit slices over time."""
        algo = TWAP(engine, duration_secs=1.0, num_slices=4)
        algo.start(buy_request(size=100.0))

        # First tick - should submit first slice immediately
        algo.on_tick(0.50, time.time())
        assert len(algo.child_order_ids) >= 1

    def test_twap_slice_size(self, engine):
        """Each TWAP slice should be total_size / num_slices."""
        algo = TWAP(engine, duration_secs=0.01, num_slices=4)
        algo.start(buy_request(size=100.0))

        # All slices should fire with small duration
        time.sleep(0.02)
        algo.on_tick(0.50, time.time())

        # Should have submitted all 4 slices
        assert len(algo.child_order_ids) == 4

        # Check order sizes
        orders = engine.open_orders()
        for order in orders:
            assert abs(order.size - 25.0) < 1e-10

    def test_twap_completes_after_all_slices_filled(self, engine):
        """TWAP is complete once all slices are filled."""
        algo = TWAP(engine, duration_secs=0.01, num_slices=2)
        algo.start(buy_request(size=20.0))

        time.sleep(0.02)
        algo.on_tick(0.50, time.time())
        assert len(algo.child_order_ids) == 2

        # Fill all orders
        engine.tick("mkt_1", 0.50)

        algo.on_tick(0.50, time.time())
        assert algo.is_complete

    def test_twap_respects_timing(self, engine):
        """TWAP should not submit all slices at once with long duration."""
        algo = TWAP(engine, duration_secs=10.0, num_slices=4)
        algo.start(buy_request(size=100.0))

        algo.on_tick(0.50, time.time())
        # Only the first slice should be submitted (interval = 2.5s)
        assert len(algo.child_order_ids) == 1


# ============================================================
# Iceberg
# ============================================================


class TestIceberg:
    def test_iceberg_shows_partial_size(self, engine):
        """Iceberg shows only show_size at a time."""
        algo = Iceberg(engine, show_size=10.0)
        algo.start(buy_request(size=50.0))

        algo.on_tick(0.50, time.time())
        assert len(algo.child_order_ids) == 1

        orders = engine.open_orders()
        assert len(orders) == 1
        assert abs(orders[0].size - 10.0) < 1e-10

    def test_iceberg_replenishes_after_fill(self, engine):
        """When visible portion fills, Iceberg submits next slice."""
        algo = Iceberg(engine, show_size=10.0)
        algo.start(buy_request(size=30.0))

        algo.on_tick(0.50, time.time())
        assert len(algo.child_order_ids) == 1

        # Fill the visible order
        engine.tick("mkt_1", 0.50)
        algo.on_tick(0.50, time.time())

        # Should have submitted another visible slice
        assert len(algo.child_order_ids) == 2

    def test_iceberg_completes_when_fully_filled(self, engine):
        """Iceberg is complete when total parent size is filled."""
        algo = Iceberg(engine, show_size=10.0)
        algo.start(buy_request(size=20.0))

        # Fill slice 1
        algo.on_tick(0.50, time.time())
        engine.tick("mkt_1", 0.50)

        # Fill slice 2
        algo.on_tick(0.50, time.time())
        engine.tick("mkt_1", 0.50)

        algo.on_tick(0.50, time.time())
        assert algo.is_complete

    def test_iceberg_last_slice_smaller(self, engine):
        """Last slice may be smaller than show_size."""
        algo = Iceberg(engine, show_size=30.0)
        algo.start(buy_request(size=50.0))

        # First slice: 30
        algo.on_tick(0.50, time.time())
        engine.tick("mkt_1", 0.50)

        # Second slice: 20 (remaining)
        algo.on_tick(0.50, time.time())
        orders = engine.open_orders()
        last_order = [o for o in orders if o.id == algo.child_order_ids[-1]]
        assert len(last_order) == 1
        assert abs(last_order[0].size - 20.0) < 1e-10

    def test_iceberg_no_new_order_while_visible_open(self, engine):
        """Don't submit another slice while visible portion is still open."""
        algo = Iceberg(engine, show_size=10.0)
        algo.start(buy_request(size=50.0))

        algo.on_tick(0.50, time.time())
        algo.on_tick(0.50, time.time())
        algo.on_tick(0.50, time.time())
        # Should still have only 1 child (not 3)
        assert len(algo.child_order_ids) == 1


# ============================================================
# VWAP
# ============================================================


class TestVWAP:
    def test_vwap_proportional_sizes(self, engine):
        """VWAP slices are proportional to volume profile weights."""
        algo = VWAP(engine, duration_secs=0.01, volume_profile=[1, 2, 1])
        algo.start(buy_request(size=100.0))

        time.sleep(0.02)
        algo.on_tick(0.50, time.time())

        assert len(algo.child_order_ids) == 3

        orders = engine.open_orders()
        sizes = sorted([o.size for o in orders])
        # Weights [1, 2, 1] → total=4 → sizes [25, 50, 25]
        assert abs(sizes[0] - 25.0) < 1e-10
        assert abs(sizes[1] - 25.0) < 1e-10
        assert abs(sizes[2] - 50.0) < 1e-10

    def test_vwap_equal_weights(self, engine):
        """Equal weights should produce equal slices (like TWAP)."""
        algo = VWAP(engine, duration_secs=0.01, volume_profile=[1, 1, 1, 1])
        algo.start(buy_request(size=100.0))

        time.sleep(0.02)
        algo.on_tick(0.50, time.time())

        assert len(algo.child_order_ids) == 4
        orders = engine.open_orders()
        for o in orders:
            assert abs(o.size - 25.0) < 1e-10

    def test_vwap_completes_after_fills(self, engine):
        """VWAP is complete once all slices are filled."""
        algo = VWAP(engine, duration_secs=0.01, volume_profile=[1, 1])
        algo.start(buy_request(size=20.0))

        time.sleep(0.02)
        algo.on_tick(0.50, time.time())
        engine.tick("mkt_1", 0.50)

        algo.on_tick(0.50, time.time())
        assert algo.is_complete


# ============================================================
# ExecAlgo base
# ============================================================


class TestExecAlgoBase:
    def test_not_complete_before_start(self, engine):
        algo = TWAP(engine, duration_secs=1.0, num_slices=2)
        assert not algo.is_complete

    def test_child_ids_empty_before_start(self, engine):
        algo = TWAP(engine, duration_secs=1.0, num_slices=2)
        assert algo.child_order_ids == []

    def test_start_resets_state(self, engine):
        algo = TWAP(engine, duration_secs=0.01, num_slices=2)
        algo.start(buy_request(size=20.0))
        time.sleep(0.02)
        algo.on_tick(0.50, time.time())
        engine.tick("mkt_1", 0.50)
        algo.on_tick(0.50, time.time())
        assert algo.is_complete

        # Restart
        algo.start(buy_request(size=20.0))
        assert not algo.is_complete
        assert algo.child_order_ids == []
