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

import math
import pytest

from horizon._horizon import (
    CopulaFamily,
    CopulaFit,
    best_copula,
    copula_cdf,
    copula_sample,
    fit_copula,
    rank_transform,
)


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

def _uniform_pairs(n=100, seed=42):
    """Generate pseudo-uniform pairs in (0, 1) using a simple LCG."""
    import random
    rng = random.Random(seed)
    u = [rng.random() * 0.8 + 0.1 for _ in range(n)]
    v = [rng.random() * 0.8 + 0.1 for _ in range(n)]
    return u, v


def _correlated_pairs(n=100, seed=42):
    """Generate correlated pseudo-uniform pairs."""
    import random
    rng = random.Random(seed)
    u = []
    v = []
    for _ in range(n):
        x = rng.gauss(0, 1)
        y = 0.7 * x + 0.3 * rng.gauss(0, 1)
        # Map to (0, 1) via rough CDF approximation
        u.append(max(0.01, min(0.99, 0.5 + 0.5 * math.erf(x / math.sqrt(2)))))
        v.append(max(0.01, min(0.99, 0.5 + 0.5 * math.erf(y / math.sqrt(2)))))
    return u, v


# ===========================================================================
# rank_transform
# ===========================================================================


class TestRankTransform:
    def test_basic(self):
        data = [3.0, 1.0, 2.0, 5.0, 4.0]
        ranks = rank_transform(data)
        assert len(ranks) == 5

    def test_values_in_0_1(self):
        data = [float(i) for i in range(20)]
        ranks = rank_transform(data)
        for r in ranks:
            assert 0.0 < r < 1.0

    def test_monotonic_input(self):
        data = [1.0, 2.0, 3.0, 4.0, 5.0]
        ranks = rank_transform(data)
        for i in range(len(ranks) - 1):
            assert ranks[i] < ranks[i + 1]

    def test_reverse_input(self):
        data = [5.0, 4.0, 3.0, 2.0, 1.0]
        ranks = rank_transform(data)
        for i in range(len(ranks) - 1):
            assert ranks[i] > ranks[i + 1]

    def test_constant_input(self):
        data = [5.0] * 10
        ranks = rank_transform(data)
        assert len(ranks) == 10
        # All ranks should be the same (tied)
        for r in ranks:
            assert 0.0 < r < 1.0

    def test_single_element(self):
        ranks = rank_transform([42.0])
        assert len(ranks) == 1
        assert 0.0 < ranks[0] < 1.0

    def test_two_elements(self):
        ranks = rank_transform([10.0, 20.0])
        assert len(ranks) == 2
        assert ranks[0] < ranks[1]


# ===========================================================================
# fit_copula
# ===========================================================================


class TestFitCopula:
    def test_gaussian(self):
        u, v = _correlated_pairs()
        result = fit_copula(u, v, CopulaFamily.Gaussian)
        assert isinstance(result, CopulaFit)
        assert hasattr(result, "parameter")
        assert math.isfinite(result.parameter)

    def test_clayton(self):
        u, v = _correlated_pairs()
        result = fit_copula(u, v, CopulaFamily.Clayton)
        assert math.isfinite(result.parameter)

    def test_gumbel(self):
        u, v = _correlated_pairs()
        result = fit_copula(u, v, CopulaFamily.Gumbel)
        assert math.isfinite(result.parameter)

    def test_frank(self):
        u, v = _correlated_pairs()
        result = fit_copula(u, v, CopulaFamily.Frank)
        assert math.isfinite(result.parameter)

    def test_result_fields(self):
        u, v = _correlated_pairs()
        result = fit_copula(u, v, CopulaFamily.Gaussian)
        assert hasattr(result, "family")
        assert hasattr(result, "parameter")
        assert hasattr(result, "log_likelihood")
        assert hasattr(result, "aic")
        assert hasattr(result, "kendall_tau")

    def test_log_likelihood_is_finite(self):
        u, v = _correlated_pairs()
        result = fit_copula(u, v, CopulaFamily.Gaussian)
        assert math.isfinite(result.log_likelihood)

    def test_aic_is_finite(self):
        u, v = _correlated_pairs()
        result = fit_copula(u, v, CopulaFamily.Gaussian)
        assert math.isfinite(result.aic)

    def test_kendall_tau_in_range(self):
        u, v = _correlated_pairs()
        result = fit_copula(u, v, CopulaFamily.Gaussian)
        assert -1.0 <= result.kendall_tau <= 1.0

    def test_too_short_raises(self):
        with pytest.raises(ValueError):
            fit_copula([0.5, 0.6], [0.3, 0.4], CopulaFamily.Gaussian)

    def test_different_length_raises(self):
        u, v = _uniform_pairs()
        with pytest.raises(ValueError):
            fit_copula(u, v[:50], CopulaFamily.Gaussian)

    def test_out_of_range_raises(self):
        u, v = _uniform_pairs()
        u[0] = 1.5  # out of (0,1)
        with pytest.raises(ValueError):
            fit_copula(u, v, CopulaFamily.Gaussian)

    def test_zero_value_raises(self):
        u, v = _uniform_pairs()
        u[0] = 0.0  # boundary
        with pytest.raises(ValueError):
            fit_copula(u, v, CopulaFamily.Gaussian)

    def test_one_value_raises(self):
        u, v = _uniform_pairs()
        u[0] = 1.0  # boundary
        with pytest.raises(ValueError):
            fit_copula(u, v, CopulaFamily.Gaussian)

    def test_gaussian_parameter_in_minus1_1(self):
        u, v = _correlated_pairs()
        result = fit_copula(u, v, CopulaFamily.Gaussian)
        assert -1.0 <= result.parameter <= 1.0

    def test_correlated_data_positive_parameter(self):
        u, v = _correlated_pairs()
        result = fit_copula(u, v, CopulaFamily.Gaussian)
        assert result.parameter > 0.0


# ===========================================================================
# best_copula
# ===========================================================================


class TestBestCopula:
    def test_basic(self):
        u, v = _correlated_pairs()
        result = best_copula(u, v)
        assert isinstance(result, CopulaFit)
        assert hasattr(result, "parameter")

    def test_too_short_raises(self):
        with pytest.raises(ValueError):
            best_copula([0.5, 0.6], [0.3, 0.4])

    def test_result_has_family(self):
        u, v = _correlated_pairs()
        result = best_copula(u, v)
        assert hasattr(result, "family")

    def test_best_has_lowest_aic(self):
        u, v = _correlated_pairs()
        best = best_copula(u, v)
        # Best AIC should be lower or equal to any specific fit
        gauss = fit_copula(u, v, CopulaFamily.Gaussian)
        assert best.aic <= gauss.aic + 1e-6


# ===========================================================================
# copula_sample
# ===========================================================================


class TestCopulaSample:
    def test_basic(self):
        samples = copula_sample(CopulaFamily.Gaussian, 0.5, 100, 42)
        assert len(samples) == 100

    def test_samples_in_range(self):
        samples = copula_sample(CopulaFamily.Gaussian, 0.5, 50, 42)
        for u, v in samples:
            assert 0.0 <= u <= 1.0
            assert 0.0 <= v <= 1.0

    def test_deterministic(self):
        s1 = copula_sample(CopulaFamily.Gaussian, 0.5, 10, 42)
        s2 = copula_sample(CopulaFamily.Gaussian, 0.5, 10, 42)
        for (u1, v1), (u2, v2) in zip(s1, s2):
            assert u1 == pytest.approx(u2)
            assert v1 == pytest.approx(v2)

    def test_different_seeds(self):
        s1 = copula_sample(CopulaFamily.Gaussian, 0.5, 10, 42)
        s2 = copula_sample(CopulaFamily.Gaussian, 0.5, 10, 99)
        differs = any(
            abs(u1 - u2) > 1e-10 or abs(v1 - v2) > 1e-10
            for (u1, v1), (u2, v2) in zip(s1, s2)
        )
        assert differs

    def test_clayton_sample(self):
        samples = copula_sample(CopulaFamily.Clayton, 2.0, 50, 42)
        assert len(samples) == 50
        for u, v in samples:
            assert 0.0 <= u <= 1.0
            assert 0.0 <= v <= 1.0

    def test_gumbel_sample(self):
        samples = copula_sample(CopulaFamily.Gumbel, 2.0, 50, 42)
        assert len(samples) == 50

    def test_frank_sample(self):
        samples = copula_sample(CopulaFamily.Frank, 5.0, 50, 42)
        assert len(samples) == 50


# ===========================================================================
# copula_cdf
# ===========================================================================


class TestCopulaCDF:
    def test_basic(self):
        cdf = copula_cdf(CopulaFamily.Gaussian, 0.5, 0.5, 0.5)
        assert 0.0 <= cdf <= 1.0

    def test_corners(self):
        # C(0, v) ~ 0 and C(u, 0) ~ 0
        assert copula_cdf(CopulaFamily.Gaussian, 0.5, 0.0, 0.5) == pytest.approx(0.0, abs=0.1)
        assert copula_cdf(CopulaFamily.Gaussian, 0.5, 0.5, 0.0) == pytest.approx(0.0, abs=0.1)

    def test_upper_corner(self):
        # C(1, 1) should be close to 1
        cdf = copula_cdf(CopulaFamily.Gaussian, 0.5, 1.0, 1.0)
        assert cdf == pytest.approx(1.0, abs=0.1)

    def test_monotonic_in_u(self):
        c1 = copula_cdf(CopulaFamily.Gaussian, 0.5, 0.3, 0.5)
        c2 = copula_cdf(CopulaFamily.Gaussian, 0.5, 0.7, 0.5)
        assert c2 >= c1 - 1e-10

    def test_monotonic_in_v(self):
        c1 = copula_cdf(CopulaFamily.Gaussian, 0.5, 0.5, 0.3)
        c2 = copula_cdf(CopulaFamily.Gaussian, 0.5, 0.5, 0.7)
        assert c2 >= c1 - 1e-10

    def test_independence_copula(self):
        # With parameter 0 (independent), C(u, v) ~ u*v
        cdf = copula_cdf(CopulaFamily.Gaussian, 0.0, 0.5, 0.5)
        assert cdf == pytest.approx(0.25, abs=0.05)
