"""Tests for vine copula dependence modeling."""

import math
import pytest

from horizon._horizon import (
    VineType,
    fit_vine,
    vine_sample,
    vine_tail_risk,
)


# ===========================================================================
# Helper data
# ===========================================================================

def _multivariate_data(n_vars=4, n_obs=50, seed=42):
    """Generate pseudo-random multivariate data in (0, 1)."""
    import random
    rng = random.Random(seed)
    data = []
    for _ in range(n_vars):
        series = [rng.random() * 0.8 + 0.1 for _ in range(n_obs)]
        data.append(series)
    return data


def _correlated_multivariate(n_vars=3, n_obs=80, seed=42):
    """Generate correlated multivariate data in (0, 1)."""
    import random
    rng = random.Random(seed)
    # Generate a correlated base
    base = [rng.gauss(0, 1) for _ in range(n_obs)]
    data = []
    for j in range(n_vars):
        series = []
        for i in range(n_obs):
            noise = rng.gauss(0, 0.3)
            val = 0.7 * base[i] + noise
            # Map to (0, 1) via rough CDF
            mapped = max(0.01, min(0.99, 0.5 + 0.5 * math.erf(val / math.sqrt(2))))
            series.append(mapped)
        data.append(series)
    return data


# ===========================================================================
# fit_vine
# ===========================================================================


class TestFitVine:
    def test_cvine(self):
        data = _multivariate_data(n_vars=3, n_obs=50)
        result = fit_vine(data, VineType.CVine)
        assert result is not None

    def test_dvine(self):
        data = _multivariate_data(n_vars=3, n_obs=50)
        result = fit_vine(data, VineType.DVine)
        assert result is not None

    def test_result_fields(self):
        data = _multivariate_data(n_vars=3, n_obs=50)
        result = fit_vine(data, VineType.CVine)
        assert hasattr(result, "vine_type")
        assert hasattr(result, "pair_copulas")
        assert hasattr(result, "n_variables")
        assert hasattr(result, "log_likelihood")

    def test_n_variables(self):
        data = _multivariate_data(n_vars=4, n_obs=50)
        result = fit_vine(data, VineType.CVine)
        assert result.n_variables == 4

    def test_log_likelihood_finite(self):
        data = _multivariate_data(n_vars=3, n_obs=50)
        result = fit_vine(data, VineType.CVine)
        assert math.isfinite(result.log_likelihood)

    def test_too_few_variables_raises(self):
        data = _multivariate_data(n_vars=2, n_obs=50)
        with pytest.raises(ValueError):
            fit_vine(data, VineType.CVine)

    def test_too_few_observations_raises(self):
        data = _multivariate_data(n_vars=3, n_obs=5)
        with pytest.raises(ValueError):
            fit_vine(data, VineType.CVine)

    def test_four_variables(self):
        data = _multivariate_data(n_vars=4, n_obs=50)
        result = fit_vine(data, VineType.CVine)
        assert result is not None

    def test_five_variables(self):
        data = _multivariate_data(n_vars=5, n_obs=60)
        result = fit_vine(data, VineType.DVine)
        assert result.n_variables == 5

    def test_pair_copulas_not_empty(self):
        data = _multivariate_data(n_vars=3, n_obs=50)
        result = fit_vine(data, VineType.CVine)
        assert len(result.pair_copulas) > 0

    def test_correlated_data(self):
        data = _correlated_multivariate(n_vars=3, n_obs=80)
        result = fit_vine(data, VineType.CVine)
        assert math.isfinite(result.log_likelihood)


# ===========================================================================
# vine_sample
# ===========================================================================


class TestVineSample:
    def test_basic(self):
        # vine_sample returns n_vars rows, each with n_samples values
        data = _multivariate_data(n_vars=3, n_obs=50)
        vine = fit_vine(data, VineType.CVine)
        samples = vine_sample(vine, 20, 42)
        assert len(samples) == 3  # n_vars rows
        for row in samples:
            assert len(row) == 20  # n_samples per variable

    def test_sample_dimensions(self):
        data = _multivariate_data(n_vars=4, n_obs=50)
        vine = fit_vine(data, VineType.DVine)
        samples = vine_sample(vine, 10, 42)
        assert len(samples) == 4  # n_vars rows
        for row in samples:
            assert len(row) == 10  # n_samples per variable

    def test_deterministic(self):
        data = _multivariate_data(n_vars=3, n_obs=50)
        vine = fit_vine(data, VineType.CVine)
        s1 = vine_sample(vine, 5, 42)
        s2 = vine_sample(vine, 5, 42)
        for a, b in zip(s1, s2):
            for x, y in zip(a, b):
                assert x == pytest.approx(y)

    def test_samples_in_unit_interval(self):
        data = _multivariate_data(n_vars=3, n_obs=50)
        vine = fit_vine(data, VineType.CVine)
        samples = vine_sample(vine, 50, 42)
        for row in samples:
            for val in row:
                assert 0.0 <= val <= 1.0

    def test_different_seeds(self):
        data = _multivariate_data(n_vars=3, n_obs=50)
        vine = fit_vine(data, VineType.CVine)
        s1 = vine_sample(vine, 10, 42)
        s2 = vine_sample(vine, 10, 99)
        differs = any(
            abs(x - y) > 1e-10
            for a, b in zip(s1, s2) for x, y in zip(a, b)
        )
        assert differs

    def test_large_sample(self):
        data = _multivariate_data(n_vars=3, n_obs=50)
        vine = fit_vine(data, VineType.CVine)
        samples = vine_sample(vine, 500, 42)
        assert len(samples) == 3  # n_vars rows
        for row in samples:
            assert len(row) == 500  # n_samples per variable


# ===========================================================================
# vine_tail_risk
# ===========================================================================


class TestVineTailRisk:
    def test_basic(self):
        data = _multivariate_data(n_vars=3, n_obs=50)
        vine = fit_vine(data, VineType.CVine)
        risk = vine_tail_risk(vine, 0.05, 1000, 42)
        assert math.isfinite(risk)

    def test_lower_threshold_higher_risk(self):
        data = _multivariate_data(n_vars=3, n_obs=50)
        vine = fit_vine(data, VineType.CVine)
        r1 = vine_tail_risk(vine, 0.10, 1000, 42)
        r2 = vine_tail_risk(vine, 0.01, 1000, 42)
        # More extreme threshold generally gives higher or equal risk metric
        assert math.isfinite(r1)
        assert math.isfinite(r2)

    def test_deterministic(self):
        data = _multivariate_data(n_vars=3, n_obs=50)
        vine = fit_vine(data, VineType.CVine)
        r1 = vine_tail_risk(vine, 0.05, 500, 42)
        r2 = vine_tail_risk(vine, 0.05, 500, 42)
        assert r1 == pytest.approx(r2)

    def test_risk_non_negative(self):
        data = _multivariate_data(n_vars=3, n_obs=50)
        vine = fit_vine(data, VineType.CVine)
        risk = vine_tail_risk(vine, 0.05, 1000, 42)
        assert risk >= 0.0

    def test_more_simulations_stable(self):
        data = _multivariate_data(n_vars=3, n_obs=50)
        vine = fit_vine(data, VineType.CVine)
        r1 = vine_tail_risk(vine, 0.05, 100, 42)
        r2 = vine_tail_risk(vine, 0.05, 5000, 42)
        # Both should be finite
        assert math.isfinite(r1)
        assert math.isfinite(r2)
