"""Tests for synthetic index construction.

Tests cover:
- Rust functions: compute_index_level, compute_weights_*, rebalance_weights,
  index_tracking_error, index_arb_signal
- Rust types: WeightingMethod, IndexLevel, IndexRebalanceAction, IndexArbSignal
- Python pipeline functions: index(), track_index(), index_arb()
"""

import math
import pytest


# ---------------------------------------------------------------------------
# 1. compute_index_level
# ---------------------------------------------------------------------------


class TestComputeIndexLevel:
    def test_basic(self):
        from horizon._horizon import compute_index_level

        level = compute_index_level([0.5, 0.3, 0.8], [1.0, 1.0, 1.0], 100.0)
        # weighted avg = (0.5+0.3+0.8)/3 = 0.5333
        # level = 100 * 0.5333 = 53.33
        assert level.level == pytest.approx(53.333, abs=0.1)
        assert level.num_components == 3
        assert level.total_weight == pytest.approx(3.0, abs=1e-10)

    def test_empty(self):
        from horizon._horizon import compute_index_level

        level = compute_index_level([], [], 100.0)
        assert level.level == pytest.approx(100.0, abs=1e-6)
        assert level.num_components == 0
        assert level.total_weight == pytest.approx(0.0, abs=1e-10)

    def test_weighted(self):
        from horizon._horizon import compute_index_level

        level = compute_index_level([0.5, 0.8], [2.0, 1.0], 100.0)
        # weighted avg = (0.5*2 + 0.8*1)/3 = 0.6
        # level = 100 * 0.6 = 60.0
        assert level.level == pytest.approx(60.0, abs=0.1)
        assert level.num_components == 2

    def test_single_component(self):
        from horizon._horizon import compute_index_level

        level = compute_index_level([0.7], [1.0], 100.0)
        assert level.level == pytest.approx(70.0, abs=1e-6)

    def test_mismatched_lengths(self):
        from horizon._horizon import compute_index_level

        # Should use min of lengths
        level = compute_index_level([0.5, 0.3], [1.0], 100.0)
        assert level.num_components == 1

    def test_default_base_level(self):
        from horizon._horizon import compute_index_level

        level = compute_index_level([0.5], [1.0])
        assert level.level == pytest.approx(50.0, abs=1e-6)


# ---------------------------------------------------------------------------
# 2. compute_weights
# ---------------------------------------------------------------------------


class TestComputeWeights:
    def test_equal(self):
        from horizon._horizon import compute_weights_equal

        w = compute_weights_equal(4)
        assert len(w) == 4
        for wi in w:
            assert wi == pytest.approx(0.25, abs=1e-10)

    def test_equal_zero(self):
        from horizon._horizon import compute_weights_equal

        assert compute_weights_equal(0) == []

    def test_equal_one(self):
        from horizon._horizon import compute_weights_equal

        w = compute_weights_equal(1)
        assert len(w) == 1
        assert w[0] == pytest.approx(1.0, abs=1e-10)

    def test_inverse_vol(self):
        from horizon._horizon import compute_weights_inverse_vol

        w = compute_weights_inverse_vol([0.1, 0.2, 0.4])
        # inv: 10, 5, 2.5 -> total=17.5
        assert w[0] == pytest.approx(10.0 / 17.5, abs=1e-6)
        assert w[1] == pytest.approx(5.0 / 17.5, abs=1e-6)
        assert w[2] == pytest.approx(2.5 / 17.5, abs=1e-6)
        assert sum(w) == pytest.approx(1.0, abs=1e-6)

    def test_inverse_vol_zero_vols(self):
        from horizon._horizon import compute_weights_inverse_vol

        # Falls back to equal weights
        w = compute_weights_inverse_vol([0.0, 0.0])
        assert w[0] == pytest.approx(0.5, abs=1e-6)
        assert w[1] == pytest.approx(0.5, abs=1e-6)

    def test_inverse_vol_empty(self):
        from horizon._horizon import compute_weights_inverse_vol

        assert compute_weights_inverse_vol([]) == []

    def test_liquidity(self):
        from horizon._horizon import compute_weights_liquidity

        w = compute_weights_liquidity([1000.0, 3000.0])
        assert w[0] == pytest.approx(0.25, abs=1e-6)
        assert w[1] == pytest.approx(0.75, abs=1e-6)

    def test_liquidity_zero_volumes(self):
        from horizon._horizon import compute_weights_liquidity

        # Falls back to equal
        w = compute_weights_liquidity([0.0, 0.0])
        assert w[0] == pytest.approx(0.5, abs=1e-6)

    def test_liquidity_empty(self):
        from horizon._horizon import compute_weights_liquidity

        assert compute_weights_liquidity([]) == []


# ---------------------------------------------------------------------------
# 3. rebalance_weights
# ---------------------------------------------------------------------------


class TestRebalanceWeights:
    def test_basic(self):
        from horizon._horizon import rebalance_weights

        actions = rebalance_weights(
            ["A", "B"], [0.6, 0.4], [0.5, 0.5]
        )
        assert len(actions) == 2
        assert actions[0].delta == pytest.approx(-0.1, abs=1e-10)
        assert actions[1].delta == pytest.approx(0.1, abs=1e-10)
        assert actions[0].market_id == "A"
        assert actions[1].market_id == "B"

    def test_no_change_needed(self):
        from horizon._horizon import rebalance_weights

        actions = rebalance_weights(
            ["A", "B"], [0.5, 0.5], [0.5, 0.5]
        )
        for a in actions:
            assert a.delta == pytest.approx(0.0, abs=1e-10)

    def test_empty(self):
        from horizon._horizon import rebalance_weights

        actions = rebalance_weights([], [], [])
        assert len(actions) == 0

    def test_single_component(self):
        from horizon._horizon import rebalance_weights

        actions = rebalance_weights(["A"], [0.3], [1.0])
        assert len(actions) == 1
        assert actions[0].delta == pytest.approx(0.7, abs=1e-10)


# ---------------------------------------------------------------------------
# 4. index_tracking_error
# ---------------------------------------------------------------------------


class TestIndexTrackingError:
    def test_identical(self):
        from horizon._horizon import index_tracking_error

        te = index_tracking_error(
            [0.01, 0.02, -0.01, 0.03],
            [0.01, 0.02, -0.01, 0.03],
        )
        assert te < 1e-10

    def test_different(self):
        from horizon._horizon import index_tracking_error

        te = index_tracking_error(
            [0.01, 0.05, -0.02, 0.04],
            [0.02, 0.01, -0.01, 0.01],
        )
        assert te > 0.0

    def test_short(self):
        from horizon._horizon import index_tracking_error

        # Less than 2 observations returns 0
        assert index_tracking_error([0.01], [0.02]) == pytest.approx(0.0, abs=1e-10)

    def test_empty(self):
        from horizon._horizon import index_tracking_error

        assert index_tracking_error([], []) == pytest.approx(0.0, abs=1e-10)


# ---------------------------------------------------------------------------
# 5. index_arb_signal
# ---------------------------------------------------------------------------


class TestIndexArbSignal:
    def test_no_deviation(self):
        from horizon._horizon import index_arb_signal

        sig = index_arb_signal(0.5, [0.5, 0.5], [1.0, 1.0], 0.01)
        assert abs(sig.deviation) < 1e-10
        assert abs(sig.signal_strength) < 1e-10

    def test_with_deviation(self):
        from horizon._horizon import index_arb_signal

        sig = index_arb_signal(0.55, [0.5, 0.5], [1.0, 1.0], 0.01)
        assert sig.fair_level == pytest.approx(0.5, abs=1e-10)
        assert sig.deviation == pytest.approx(0.05, abs=1e-10)
        assert sig.signal_strength > 0.0

    def test_empty(self):
        from horizon._horizon import index_arb_signal

        sig = index_arb_signal(0.5, [], [], 0.01)
        assert abs(sig.deviation) < 1e-10
        assert sig.fair_level == pytest.approx(0.5, abs=1e-10)

    def test_below_threshold(self):
        from horizon._horizon import index_arb_signal

        # Deviation of 0.001 on 0.5 = 0.002, below 0.01 threshold
        sig = index_arb_signal(0.501, [0.5, 0.5], [1.0, 1.0], 0.01)
        assert sig.signal_strength == pytest.approx(0.0, abs=1e-10)


# ---------------------------------------------------------------------------
# 6. Pipeline functions (Ultra tier)
# ---------------------------------------------------------------------------


class TestIndexPipeline:
    def test_index(self):
        try:
            from horizon.synthetic_index import index
        except PermissionError:
            pytest.skip("Ultra tier required")

        fn = index(market_feeds={"m1": "feed1", "m2": "feed2"}, weighting="equal")
        assert callable(fn)
        assert fn.__name__ == "index"

    def test_track_index(self):
        try:
            from horizon.synthetic_index import track_index
        except PermissionError:
            pytest.skip("Ultra tier required")

        fn = track_index(market_feeds={"m1": "f1", "m2": "f2"})
        assert callable(fn)
        assert fn.__name__ == "track_index"

    def test_index_arb(self):
        try:
            from horizon.synthetic_index import index_arb
        except PermissionError:
            pytest.skip("Ultra tier required")

        fn = index_arb(market_feeds={"m1": "f1"}, threshold=0.02)
        assert callable(fn)
        assert fn.__name__ == "index_arb"
