"""T1 a T5: los oráculos numéricos del prompt. Si uno falla no se ajusta el oráculo."""

import math
import random
from datetime import date, timedelta

import numpy as np
import pytest
from estudio01 import engine, metrics, report

PRICE = 1.1211  # oraculo T2


def test_t1_ema_five_bars():
    """T1: serie 1.0, 1.1, 1.2, 1.3, 1.4 con n=3, EMA(5) = 1.3062500000."""
    values = engine.ema([1.0, 1.1, 1.2, 1.3, 1.4], 3)  # oraculo T1
    assert report.fmt(values[4]) == "1.3062500000"  # oraculo T1
    # EMA(1) = P_1 y la recursión con alfa = 2 / (n + 1) = 0.5
    assert values[0] == 1.0
    assert values[1] == pytest.approx(0.5 * 1.1 + 0.5 * 1.0)


def test_t2_spread_one_long_entry():
    """T2: entrada larga con P = 1.1211 y K- = 1 -> c = 0.0001783962."""
    assert report.fmt(engine.spread_cost(1, 0.0002, PRICE, 1.0)) == "0.0001783962"  # oraculo T2


def test_t2_spread_in_the_engine():
    """T2 dentro del motor: la entrada en la primera barra de la ventana tiene K- = 1."""
    prices = [1.0, 1.0, PRICE, PRICE, PRICE, PRICE]
    days = [date(2000, 1, 3) + timedelta(days=i) for i in range(len(prices))]
    targets: list[int | None] = [None] * (len(prices) + 1)
    targets[3] = engine.LONG
    zeros = [0.0] * len(prices)
    run = engine.simulate(prices, days, targets, zeros, zeros, 0.0002, 0.0, warmup=2)
    assert run.k_minus[0] == 1.0
    assert run.sides[0] == 1
    assert report.fmt(run.cost[0]) == "0.0001783962"  # oraculo T2
    assert run.ret[0] == pytest.approx(-0.0002 / PRICE, abs=1e-15)


@pytest.mark.parametrize(
    ("d", "r_eur", "r_usd", "expected"),
    [
        (engine.LONG, 0.04, 0.01, "0.0001666667"),  # oraculo T3
        (engine.LONG, 0.02, 0.01, "0.0000000000"),  # oraculo T3
        (engine.SHORT, 0.01, 0.04, "0.0001666667"),  # oraculo T3
    ],
)
def test_t3_swap(d, r_eur, r_usd, expected):
    """T3: K+ = 1, m = 0.01 y D = 3; largo con r_EUR 0.04 y 0.02, y corto con r_USD = 0.04."""
    assert report.fmt(engine.swap_amount(1.0, d, r_eur, r_usd, 0.01, 3.0)) == expected  # oraculo T3


def test_t4_dsr_normal_returns():
    """T4: SR = 0.05, T = 1000, g3 = 0, g4 = 3, V = 0.0001, N = 4."""
    result = metrics.deflated_sharpe(0.05, 1000, 0.0, 3.0, 0.0001, 4)  # oraculo T4
    assert report.fmt(result.sr0) == "0.0105212282"  # oraculo T4
    assert report.fmt(result.dsr) == "0.8938058093"  # oraculo T4
    assert report.fmt(result.psr0) == "0.9428733922"  # oraculo T4
    assert result.passes is False  # oraculo T4: 0.8938 < 0.95


def test_t4_dsr_skewed_and_fat_tailed():
    """T4: con g3 = -0.5 y g4 = 5 el DSR es 0.8908371942."""
    result = metrics.deflated_sharpe(0.05, 1000, -0.5, 5.0, 0.0001, 4)  # oraculo T4
    assert report.fmt(result.dsr) == "0.8908371942"  # oraculo T4


def test_t4_threshold_is_evaluated_on_the_unrounded_value():
    """El criterio DSR >= 0.95 usa el valor de 64 bits, no el redondeado a 10 decimales."""

    def dsr_at(sr: float) -> metrics.Dsr:
        return metrics.deflated_sharpe(sr, 1000, 0.0, 3.0, 0.0001, 4)

    low, high = 0.05, 0.2  # el DSR crece con el Sharpe: dsr_at(low) < 0.95 <= dsr_at(high)
    assert not dsr_at(low).passes
    assert dsr_at(high).passes
    for _ in range(200):
        middle = (low + high) / 2.0
        if dsr_at(middle).passes:
            high = middle
        else:
            low = middle
    just_below, just_above = dsr_at(low), dsr_at(high)
    assert just_below.dsr < 0.95 <= just_above.dsr
    assert report.fmt(just_below.dsr) == "0.9500000000"  # se muestra como 0.95...
    assert just_below.passes is False  # ...pero no pasa
    assert just_above.passes is True


def _base_series(length: int, seed: int = 5) -> list[float]:
    rng = random.Random(seed)
    return [rng.gauss(0.0, 0.01) for _ in range(length)]


def test_t5_pbo_identical_configurations():
    """T5: cuatro configuraciones idénticas -> PBO 1.0, rango 2.5, omega 0.5, lambda 0."""
    base = _base_series(325)  # 325 = 16 * 20 + 5: se descartan los 5 retornos más antiguos
    result = metrics.pbo_cscv(np.array([base] * 4).T)
    assert result.n_combinations == math.comb(16, 8) == 12870  # oraculo T5
    assert result.pbo == 1.0
    assert result.omega_counts == ((0.5, 12870),)  # oraculo T5
    assert math.log(0.5 / (1.0 - 0.5)) == 0.0  # oraculo T5
    assert (result.n_returns, result.n_dropped, result.n_used, result.block_length) == (
        325,
        5,
        320,
        20,
    )
    assert metrics.average_rank([7.0, 7.0, 7.0, 7.0], 0) == 2.5  # oraculo T5


def test_t5_pbo_one_configuration_always_better():
    """T5: la configuración 1 = serie base + constante positiva; las demás, la serie base."""
    base = _base_series(325)
    better = [value + 0.002 for value in base]
    result = metrics.pbo_cscv(np.array([better, base, base, base]).T)
    assert result.n_combinations == 12870  # oraculo T5
    assert result.pbo == 0.0
    assert result.omega_counts == ((0.8, 12870),)  # oraculo T5: rango 4 de N + 1 = 5


def test_t5_oldest_returns_are_dropped():
    """Los T mod 16 retornos más antiguos no influyen en el PBO."""
    rng = random.Random(11)
    matrix = np.array([[rng.gauss(0.0, 0.01) for _ in range(4)] for _ in range(325)])
    altered = matrix.copy()
    altered[:5] = 9.0  # los cinco más antiguos (325 mod 16 = 5)
    assert metrics.pbo_cscv(matrix) == metrics.pbo_cscv(altered)
