"""Tests for Combinatorial Purged Cross-Validation (CPCV)."""

import math
import pytest
from horizon.cpcv import CPCVResult, cpcv, probability_of_overfitting


def _simple_sharpe(returns):
    """Compute Sharpe from a returns list."""
    if len(returns) < 2:
        return 0.0
    mean = sum(returns) / len(returns)
    var = sum((r - mean) ** 2 for r in returns) / len(returns)
    std = math.sqrt(max(var, 1e-15))
    return mean / std if std > 1e-10 else 0.0


def _make_pipeline_factory():
    """Factory that returns strategy functions parameterized by a multiplier."""

    def factory(params):
        multiplier = params.get("multiplier", 1.0)

        def strategy_fn(data):
            # Simple "strategy": multiply returns by multiplier, compute Sharpe
            adjusted = [r * multiplier for r in data]
            return _simple_sharpe(adjusted)

        return strategy_fn

    return factory


class TestCPCVResult:
    def test_default(self):
        r = CPCVResult()
        assert r.oos_sharpes == []
        assert r.pbo == 0.0
        assert r.is_overfit is False
        assert r.n_combinations == 0


class TestCPCV:
    def test_basic(self):
        data = [0.001 * (-1) ** i for i in range(200)]
        factory = _make_pipeline_factory()
        grid = [{"multiplier": 1.0}, {"multiplier": 2.0}, {"multiplier": 0.5}]
        result = cpcv(data, factory, grid, n_groups=4)
        assert isinstance(result, CPCVResult)
        assert result.n_combinations > 0
        assert len(result.oos_sharpes) == result.n_combinations

    def test_small_data(self):
        result = cpcv([0.01] * 5, _make_pipeline_factory(), [{"multiplier": 1.0}], n_groups=4)
        assert result.n_combinations == 0

    def test_empty_param_grid(self):
        data = [0.001 * i for i in range(100)]
        result = cpcv(data, _make_pipeline_factory(), [], n_groups=4)
        assert result.n_combinations == 0

    def test_pbo_range(self):
        data = [0.001 * math.sin(i * 0.1) for i in range(400)]
        factory = _make_pipeline_factory()
        grid = [{"multiplier": m} for m in [0.5, 1.0, 1.5, 2.0]]
        result = cpcv(data, factory, grid, n_groups=4)
        assert 0.0 <= result.pbo <= 1.0

    def test_is_overfit_flag(self):
        r = CPCVResult(oos_sharpes=[0.1], pbo=0.8, is_overfit=True, n_combinations=1)
        assert r.is_overfit is True
        r2 = CPCVResult(oos_sharpes=[0.1], pbo=0.2, is_overfit=False, n_combinations=1)
        assert r2.is_overfit is False

    def test_purge_gap(self):
        data = [0.001 * (-1) ** i for i in range(300)]
        factory = _make_pipeline_factory()
        grid = [{"multiplier": 1.0}]
        result = cpcv(data, factory, grid, n_groups=4, purge_gap=0.1)
        assert result.n_combinations > 0

    def test_max_combinations(self):
        data = [0.001 * i for i in range(500)]
        factory = _make_pipeline_factory()
        grid = [{"multiplier": 1.0}]
        result = cpcv(data, factory, grid, n_groups=8, max_combinations=5)
        assert result.n_combinations <= 5

    def test_odd_n_groups_bumped(self):
        data = [0.001 * i for i in range(200)]
        factory = _make_pipeline_factory()
        grid = [{"multiplier": 1.0}]
        # n_groups=5 should be bumped to 6
        result = cpcv(data, factory, grid, n_groups=5)
        assert result.n_combinations > 0


class TestProbabilityOfOverfitting:
    def test_basic(self):
        r = CPCVResult(oos_sharpes=[0.1, 0.2], pbo=0.65, is_overfit=True, n_combinations=2)
        assert probability_of_overfitting(r) == 0.65

    def test_zero(self):
        r = CPCVResult()
        assert probability_of_overfitting(r) == 0.0
