"""Contabilidad de capital del motor: un ejemplo calculado a mano con Decimal, y los casos
de borde (swap de largo y corto, sin spread, sin swap, sin posición, entradas inválidas)."""

from datetime import date
from decimal import Decimal, getcontext

import pytest
from estudio01 import engine

getcontext().prec = 50
DAYS = [
    date(2000, 1, 3),  # lunes
    date(2000, 1, 4),
    date(2000, 1, 5),
    date(2000, 1, 6),
    date(2000, 1, 7),  # viernes
    date(2000, 1, 10),  # lunes: tres días naturales después del viernes
]
PRICES = [1.00, 1.00, 1.10, 1.21, 1.21, 1.10]
R_EUR = [0.04] * 6
R_USD = [0.01] * 6
SPREAD, M = 0.0002, 0.01


def targets_with(**by_bar: int) -> list[int | None]:
    targets: list[int | None] = [None] * (len(PRICES) + 1)
    for bar, direction in by_bar.items():
        targets[int(bar.removeprefix("b"))] = direction
    return targets


def test_long_position_matches_a_hand_calculation_in_decimal():
    run = engine.simulate(
        PRICES, DAYS, targets_with(b3=engine.LONG), R_EUR, R_USD, SPREAD, M, warmup=2
    )
    spread, carry, p = Decimal("0.0002"), Decimal("0.02"), [Decimal(str(x)) for x in PRICES]
    # barra 3: entrada desde sin posición (un lado); el swap cubre 1 día (de 01-05 a 01-06)
    c3 = spread / p[2]
    k3 = Decimal(1) - c3
    s3 = k3 * carry * 1 / 360
    # barra 4: capital con el cambio de precio y el swap previo; 1 día (01-06 a 01-07)
    k4 = k3 * (1 + (p[3] / p[2] - 1)) + s3
    s4 = k4 * carry * 1 / 360
    # barra 5: precio sin cambio; 3 días (de viernes 01-07 a lunes 01-10)
    k5 = k4 * (1 + (p[4] / p[3] - 1)) + s4
    s5 = k5 * carry * 3 / 360
    # barra 6 = L: solo se cierra (un lado) y el swap de L es 0
    k6_minus = k5 * (1 + (p[5] / p[4] - 1)) + s5
    k6 = k6_minus - spread / p[5] * k6_minus

    assert run.dates == tuple(DAYS[2:])
    assert run.d == (1, 1, 1, 0)
    assert run.sides == (1, 0, 0, 1)
    assert run.k_minus[0] == 1.0
    checks = [
        (run.k_plus[0], k3),
        (run.k_plus[1], k4),
        (run.k_plus[2], k5),
        (run.k_plus[3], k6),
        (run.swap[0], s3),
        (run.swap[1], s4),
        (run.swap[2], s5),
        (run.cost[0], c3),
        (run.cost[3], spread / p[5] * k6_minus),
        (run.k_minus[3], k6_minus),
    ]
    for got, want in checks:
        assert got == pytest.approx(float(want), rel=1e-13)
    assert run.swap[3] == 0.0
    assert run.ret[3] == pytest.approx(float(k6 / k5 - 1), rel=1e-12)


def test_the_swap_of_each_interval_uses_the_rates_in_force_on_that_bar():
    """S_t usa las tasas vigentes en la barra t, no las de la anterior ni las de la siguiente."""
    r_eur = [0.00, 0.00, 0.04, 0.10, 0.01, 0.07]  # una tasa distinta del euro en cada barra
    r_usd = [0.00, 0.00, 0.01, 0.02, 0.05, 0.03]
    run = engine.simulate(
        PRICES, DAYS, targets_with(b3=engine.LONG), r_eur, r_usd, SPREAD, M, warmup=2
    )
    interval_days = [(DAYS[t] - DAYS[t - 1]).days for t in (3, 4, 5)]  # D de las barras 3, 4 y 5
    assert interval_days == [1, 1, 3]
    for index, bar in enumerate((3, 4, 5)):
        carry = r_eur[bar - 1] - r_usd[bar - 1] - M  # compra euros y vende dólares
        want = run.k_plus[index] * carry * interval_days[index] / 360.0
        assert run.swap[index] == pytest.approx(want, rel=1e-13), bar
    assert run.swap[0] > 0.0 and run.swap[1] > 0.0 and run.swap[2] < 0.0  # cambia el signo


def test_short_position_earns_the_carry_in_the_other_direction():
    """En corto la moneda comprada es el dólar: con r_USD > r_EUR el swap suma."""
    flat = [1.0] * 6
    r_eur, r_usd = [0.01] * 6, [0.04] * 6
    run = engine.simulate(flat, DAYS, targets_with(b3=engine.SHORT), r_eur, r_usd, 0.0, M, warmup=2)
    assert run.swap[0] > 0.0
    assert run.swap[0] == pytest.approx((0.04 - 0.01 - 0.01) * 1 / 360)
    long_run = engine.simulate(
        flat, DAYS, targets_with(b3=engine.LONG), r_eur, r_usd, 0.0, M, warmup=2
    )
    assert long_run.swap[0] == pytest.approx((0.01 - 0.04 - 0.01) * 1 / 360)  # negativo


def test_swap_off_and_zero_spread_remove_each_cost():
    long = targets_with(b3=engine.LONG)
    base = engine.simulate(PRICES, DAYS, long, R_EUR, R_USD, SPREAD, M, warmup=2)
    no_swap = engine.simulate(PRICES, DAYS, long, R_EUR, R_USD, SPREAD, M, swap_on=False, warmup=2)
    no_spread = engine.simulate(PRICES, DAYS, long, R_EUR, R_USD, 0.0, M, warmup=2)
    assert all(value == 0.0 for value in no_swap.swap)
    assert all(value == 0.0 for value in no_spread.cost)
    assert base.cost[0] == no_swap.cost[0]  # la entrada ocurre con K- = 1 en las dos corridas
    # sin swap el capital solo sigue al precio y paga el spread de entrada y de cierre
    k3 = 1.0 - SPREAD / 1.10
    k4 = k3 * (1.21 / 1.10)
    k5 = k4
    k6 = k5 * (1.10 / 1.21) * (1.0 - SPREAD / 1.10)
    assert no_swap.k_plus == pytest.approx((k3, k4, k5, k6), rel=1e-14)
    assert no_spread.k_plus[-1] > base.k_plus[-1]  # quitar el spread siempre suma capital


def test_without_any_execution_the_capital_never_moves():
    run = engine.simulate(PRICES, DAYS, targets_with(), R_EUR, R_USD, SPREAD, M, warmup=2)
    assert run.k_plus == (1.0, 1.0, 1.0, 1.0)
    assert run.ret == (0.0, 0.0, 0.0, 0.0)
    assert run.sides == (0, 0, 0, 0)
    assert run.swap == (0.0, 0.0, 0.0, 0.0)


def test_inputs_must_be_coherent():
    ok = targets_with()
    with pytest.raises(ValueError, match="coherentes"):
        engine.simulate(PRICES, DAYS[:-1], ok, R_EUR, R_USD, SPREAD, M, warmup=2)
    with pytest.raises(ValueError, match="coherentes"):
        engine.simulate(PRICES, DAYS, ok[:-1], R_EUR, R_USD, SPREAD, M, warmup=2)
    with pytest.raises(ValueError, match="barras"):
        engine.simulate(PRICES[:3], DAYS[:3], [None] * 4, R_EUR[:3], R_USD[:3], SPREAD, M, warmup=2)
    with pytest.raises(ValueError, match="misma longitud"):
        engine.pending_targets([1.0] * 5, [1.0] * 4)


def test_ema_of_a_constant_series_is_that_constant_exactly():
    """Base de la prueba T6 (i): con precio constante 1.0 las dos EMAs son exactamente 1.0."""
    for fast, slow in engine.EMA_PAIRS:
        assert set(engine.ema([1.0] * 1200, fast)) == {1.0}
        assert set(engine.ema([1.0] * 1200, slow)) == {1.0}


def test_ema_follows_the_recursion_in_decimal():
    prices = [Decimal(str(p)) for p in (1.2, 1.18, 1.25, 1.19, 1.3, 1.28)]
    n = 9
    alpha = Decimal(2) / (n + 1)
    expected = [prices[0]]
    for price in prices[1:]:
        expected.append(alpha * price + (1 - alpha) * expected[-1])
    got = engine.ema([float(p) for p in prices], n)
    assert got == pytest.approx([float(v) for v in expected], rel=1e-14)


def test_scenarios_are_the_three_declared_ones():
    assert [(s.name, s.spread, s.m) for s in engine.SCENARIOS] == [
        ("low", 0.0001, 0.005),
        ("base", 0.0002, 0.01),
        ("high", 0.0004, 0.02),
    ]
    assert engine.EMA_PAIRS == ((9, 20), (9, 21), (12, 26), (50, 200))
    assert engine.WARMUP == 1_000
