"""Tests for lead-lag detection functions."""

import math
import pytest

from horizon._horizon import (
    GrangerResult,
    LeadLagNetwork,
    LeadLagResult,
    cross_correlation_lags,
    granger_causality,
    hayashi_yoshida,
    lead_lag_network,
)


# ===========================================================================
# cross_correlation_lags
# ===========================================================================


class TestCrossCorrelationLags:
    def test_basic(self):
        x = [1.0, 2.0, 3.0, 4.0, 5.0]
        y = [1.0, 2.0, 3.0, 4.0, 5.0]
        result = cross_correlation_lags(x, y, 2)
        assert len(result) > 0

    def test_returns_tuples(self):
        x = [1.0, 2.0, 3.0, 4.0, 5.0]
        y = [5.0, 4.0, 3.0, 2.0, 1.0]
        result = cross_correlation_lags(x, y, 1)
        for lag, corr in result:
            assert isinstance(lag, int)
            assert isinstance(corr, float)
            assert -1.0 <= corr <= 1.0

    def test_self_correlation_at_zero_lag(self):
        x = [1.0, 2.0, 3.0, 4.0, 5.0]
        result = cross_correlation_lags(x, x, 2)
        lag_zero = [c for (l, c) in result if l == 0]
        assert len(lag_zero) == 1
        assert lag_zero[0] == pytest.approx(1.0, abs=0.01)

    def test_negative_correlation(self):
        x = [1.0, 2.0, 3.0, 4.0, 5.0]
        y = [5.0, 4.0, 3.0, 2.0, 1.0]
        result = cross_correlation_lags(x, y, 0)
        lag_zero = [c for (l, c) in result if l == 0]
        assert lag_zero[0] < 0

    def test_different_lengths_raises(self):
        with pytest.raises(ValueError):
            cross_correlation_lags([1.0, 2.0], [1.0, 2.0, 3.0], 1)

    def test_too_short_raises(self):
        with pytest.raises(ValueError):
            cross_correlation_lags([1.0, 2.0], [1.0, 2.0], 1)

    def test_negative_max_lag_raises(self):
        with pytest.raises(ValueError):
            cross_correlation_lags([1.0, 2.0, 3.0], [1.0, 2.0, 3.0], -1)

    def test_zero_max_lag(self):
        x = [1.0, 2.0, 3.0, 4.0, 5.0]
        result = cross_correlation_lags(x, x, 0)
        assert len(result) == 1
        assert result[0][0] == 0

    def test_lag_count(self):
        x = [float(i) for i in range(10)]
        result = cross_correlation_lags(x, x, 3)
        # Should have lags from -3 to +3 = 7 entries
        assert len(result) == 7

    def test_constant_series_zero_corr(self):
        x = [5.0] * 10
        y = [float(i) for i in range(10)]
        result = cross_correlation_lags(x, y, 2)
        for lag, corr in result:
            assert corr == pytest.approx(0.0, abs=1e-10)

    def test_symmetric_property(self):
        x = [1.0, 3.0, 2.0, 5.0, 4.0, 6.0, 7.0]
        y = [2.0, 4.0, 1.0, 3.0, 5.0, 7.0, 6.0]
        result_xy = cross_correlation_lags(x, y, 2)
        result_yx = cross_correlation_lags(y, x, 2)
        # corr_xy(lag) == corr_yx(-lag)
        xy_dict = {lag: corr for lag, corr in result_xy}
        yx_dict = {lag: corr for lag, corr in result_yx}
        for lag in xy_dict:
            if -lag in yx_dict:
                assert xy_dict[lag] == pytest.approx(yx_dict[-lag], abs=1e-10)


# ===========================================================================
# granger_causality
# ===========================================================================


class TestGrangerCausality:
    def test_basic(self):
        x = [float(i) for i in range(30)]
        y = [0.0] + [float(i) for i in range(29)]  # y lags x
        result = granger_causality(x, y, 3)
        assert isinstance(result, GrangerResult)

    def test_result_fields(self):
        x = [float(i) for i in range(30)]
        y = [float(i) + 1.0 for i in range(30)]
        result = granger_causality(x, y, 2)
        assert hasattr(result, "f_statistic")
        assert hasattr(result, "p_value")
        assert hasattr(result, "lag")
        assert hasattr(result, "is_significant")

    def test_f_statistic_non_negative(self):
        x = [float(i) for i in range(30)]
        y = [float(i) for i in range(30)]
        result = granger_causality(x, y, 2)
        assert result.f_statistic >= 0.0

    def test_p_value_in_range(self):
        x = [float(i) for i in range(30)]
        y = [float(i) for i in range(30)]
        result = granger_causality(x, y, 2)
        assert 0.0 <= result.p_value <= 1.0

    def test_different_lengths_raises(self):
        with pytest.raises(ValueError):
            granger_causality([1.0, 2.0], [1.0, 2.0, 3.0], 1)

    def test_repr(self):
        x = [float(i) for i in range(30)]
        y = [float(i) for i in range(30)]
        result = granger_causality(x, y, 2)
        r = repr(result)
        assert "GrangerResult" in r

    def test_is_significant_is_bool(self):
        x = [float(i) for i in range(30)]
        y = [float(i) for i in range(30)]
        result = granger_causality(x, y, 2)
        assert isinstance(result.is_significant, bool)

    def test_lag_matches_input(self):
        x = [float(i) for i in range(30)]
        y = [float(i) for i in range(30)]
        result = granger_causality(x, y, 5)
        assert result.lag <= 5

    def test_too_short_series_raises(self):
        with pytest.raises(ValueError):
            granger_causality([1.0, 2.0, 3.0], [1.0, 2.0, 3.0], 2)


# ===========================================================================
# hayashi_yoshida
# ===========================================================================


class TestHayashiYoshida:
    def test_basic(self):
        ts_a = [0.0, 1.0, 2.0, 3.0, 4.0]
        prices_a = [100.0, 101.0, 102.0, 101.0, 103.0]
        ts_b = [0.0, 1.5, 3.0, 4.0]
        prices_b = [50.0, 51.0, 50.5, 52.0]
        result = hayashi_yoshida(ts_a, prices_a, ts_b, prices_b)
        assert math.isfinite(result)

    def test_synchronous_data(self):
        ts = [0.0, 1.0, 2.0, 3.0, 4.0]
        prices_a = [100.0, 101.0, 102.0, 103.0, 104.0]
        prices_b = [50.0, 51.0, 52.0, 53.0, 54.0]
        result = hayashi_yoshida(ts, prices_a, ts, prices_b)
        assert result > 0.0  # positively co-moving

    def test_mismatched_ts_prices_raises(self):
        with pytest.raises(ValueError):
            hayashi_yoshida([0.0, 1.0], [100.0], [0.0, 1.0], [50.0, 51.0])

    def test_too_short_raises(self):
        with pytest.raises(ValueError):
            hayashi_yoshida([0.0], [100.0], [0.0, 1.0], [50.0, 51.0])

    def test_non_finite_raises(self):
        with pytest.raises(ValueError):
            hayashi_yoshida(
                [0.0, float("nan")], [100.0, 101.0],
                [0.0, 1.0], [50.0, 51.0],
            )

    def test_opposite_movements(self):
        ts = [0.0, 1.0, 2.0, 3.0]
        prices_a = [100.0, 101.0, 102.0, 103.0]
        prices_b = [50.0, 49.0, 48.0, 47.0]
        result = hayashi_yoshida(ts, prices_a, ts, prices_b)
        assert result < 0.0

    def test_flat_prices_zero_covariance(self):
        ts = [0.0, 1.0, 2.0, 3.0]
        prices_a = [100.0, 100.0, 100.0, 100.0]
        prices_b = [50.0, 51.0, 52.0, 53.0]
        result = hayashi_yoshida(ts, prices_a, ts, prices_b)
        assert result == pytest.approx(0.0, abs=1e-10)


# ===========================================================================
# lead_lag_network
# ===========================================================================


class TestLeadLagNetwork:
    def test_basic(self):
        series = [
            [float(i) for i in range(20)],
            [0.0] + [float(i) for i in range(19)],
            [0.0, 0.0] + [float(i) for i in range(18)],
        ]
        names = ["A", "B", "C"]
        result = lead_lag_network(series, names, 3)
        assert isinstance(result, LeadLagNetwork)

    def test_result_fields(self):
        series = [
            [float(i) for i in range(20)],
            [float(i) + 1.0 for i in range(20)],
        ]
        names = ["X", "Y"]
        result = lead_lag_network(series, names, 2)
        assert hasattr(result, "edges")
        assert hasattr(result, "leaders")
        assert hasattr(result, "laggers")

    def test_too_few_series_raises(self):
        with pytest.raises(ValueError):
            lead_lag_network([[1.0, 2.0]], ["A"], 1)

    def test_mismatched_names_raises(self):
        with pytest.raises(ValueError):
            lead_lag_network(
                [[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]],
                ["A"],
                1,
            )

    def test_zero_max_lag_raises(self):
        with pytest.raises(ValueError):
            lead_lag_network(
                [[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]],
                ["A", "B"],
                0,
            )

    def test_different_length_series_raises(self):
        with pytest.raises(ValueError):
            lead_lag_network(
                [[1.0, 2.0, 3.0], [1.0, 2.0]],
                ["A", "B"],
                1,
            )

    def test_edges_format(self):
        series = [
            [float(i) for i in range(20)],
            [0.0] + [float(i) for i in range(19)],
        ]
        names = ["A", "B"]
        result = lead_lag_network(series, names, 3)
        for edge in result.edges:
            assert isinstance(edge, tuple)
            assert len(edge) == 3

    def test_repr(self):
        series = [
            [float(i) for i in range(20)],
            [float(i) + 1.0 for i in range(20)],
        ]
        names = ["X", "Y"]
        result = lead_lag_network(series, names, 2)
        r = repr(result)
        assert "LeadLagNetwork" in r

    def test_leaders_and_laggers_are_lists(self):
        series = [
            [float(i) for i in range(20)],
            [0.0] + [float(i) for i in range(19)],
        ]
        names = ["A", "B"]
        result = lead_lag_network(series, names, 3)
        assert isinstance(result.leaders, list)
        assert isinstance(result.laggers, list)
