"""T6: reglas de posición de §2 sobre series sintéticas de 1,010 barras (L = 1,010).

Cada caso comprueba primero su precondición (por ejemplo, que el cruce exista en la barra
elegida) para que la prueba no pueda pasar por una razón equivocada.
"""

from datetime import date, timedelta

import pytest
from estudio01 import engine

W = engine.WARMUP  # 1,000
LENGTH = 1010
SPREAD = 0.0002
LONG, SHORT = engine.LONG, engine.SHORT
ZEROS = [0.0] * LENGTH


def days(n: int = LENGTH) -> list[date]:
    return [date(2000, 1, 3) + timedelta(days=i) for i in range(n)]


def at(run: engine.Run, bar: int) -> int:
    """Índice dentro de la corrida de una barra (la corrida arranca en la barra 1,001)."""
    return bar - W - 1


def simulate(prices, targets) -> engine.Run:
    return engine.simulate(prices, days(len(prices)), targets, ZEROS, ZEROS, SPREAD, 0.0)


def simulate_from_emas(fast, slow) -> tuple[list[int | None], engine.Run]:
    targets = engine.pending_targets(fast, slow)
    return targets, simulate([1.0] * LENGTH, targets)


def prices_and_emas(prices):
    return engine.ema(prices, 9), engine.ema(prices, 20)


def test_t6_i_exact_tie_at_bar_1000_takes_no_position():
    """(i) Empate exacto en la barra 1,000: sin posición y R_1001 = 0."""
    prices = [1.0] * 1003 + [1.1] * 7
    fast, slow = prices_and_emas(prices)
    assert fast[W - 1] == slow[W - 1]  # precondición: empate exacto en la barra 1,000
    targets = engine.pending_targets(fast, slow)
    run = simulate(prices, targets)
    assert targets[W + 1] is None
    assert run.d[at(run, 1001)] == 0
    assert run.ret[at(run, 1001)] == 0.0
    assert run.sides[at(run, 1001)] == 0
    # el primer cruce (la subida de la barra 1,004) se detecta en 1,004 y se ejecuta en 1,005
    assert fast[1003] > slow[1003] and fast[1002] <= slow[1002]
    assert targets[1005] == LONG
    assert [run.d[at(run, bar)] for bar in (1001, 1002, 1003, 1004)] == [0, 0, 0, 0]
    assert run.d[at(run, 1005)] == LONG
    assert run.sides[at(run, 1005)] == 1  # entrada desde sin posición: un lado


def test_t6_ii_signal_detected_at_bar_l_is_not_executed():
    """(ii) Una señal detectada en la barra L no se ejecuta: solo se cierra la posición."""
    prices = [1.0] * 999 + [0.9] * 10 + [1.2]
    fast, slow = prices_and_emas(prices)
    assert len(prices) == LENGTH
    assert fast[W - 1] < slow[W - 1]  # posición inicial corta
    assert fast[LENGTH - 1] > slow[LENGTH - 1] and fast[LENGTH - 2] <= slow[LENGTH - 2]
    targets = engine.pending_targets(fast, slow)
    run = simulate(prices, targets)
    assert targets[W + 1] == SHORT
    assert len(targets) == LENGTH + 1  # ninguna ejecución más allá de L
    assert run.d[at(run, LENGTH - 1)] == SHORT
    assert run.d[-1] == 0  # en L solo se cierra: no se abre la larga de la señal
    assert run.sides[-1] == 1
    assert sum(1 for sides in run.sides if sides > 0) == 2  # entrada y cierre


def test_t6_iii_pending_execution_at_bar_l_only_closes_with_one_side():
    """(iii) Una ejecución pendiente en L solo cierra: paga un lado, no dos."""
    prices = [1.0] * 999 + [1.1] * 9 + [0.8, 0.8]
    fast, slow = prices_and_emas(prices)
    assert len(prices) == LENGTH
    assert fast[W - 1] > slow[W - 1]  # posición inicial larga
    assert fast[LENGTH - 2] < slow[LENGTH - 2] and fast[LENGTH - 3] >= slow[LENGTH - 3]
    targets = engine.pending_targets(fast, slow)
    run = simulate(prices, targets)
    assert targets[LENGTH] == SHORT  # señal corta detectada en L - 1, pendiente para L
    assert run.d[at(run, LENGTH - 1)] == LONG
    assert run.sides[-1] == 1
    assert run.d[-1] == 0
    expected = 1 * (SPREAD / prices[-1]) * run.k_minus[-1]
    assert run.cost[-1] == pytest.approx(expected, rel=1e-15)


def test_t6_iv_signal_in_the_direction_in_force_pays_nothing():
    """(iv) Una señal en la dirección vigente se ignora y no tiene costo."""
    slow = [1.0] * LENGTH
    fast = [1.01] * LENGTH
    fast[1001] = 1.0  # barra 1,002: empate exacto, sin señal corta (necesita f < s)
    assert fast[1001] == slow[1001]
    targets, run = simulate_from_emas(fast, slow)
    assert targets[W + 1] == LONG
    # barra 1,003: f > s con f <= s en la anterior -> señal larga, ejecutable en 1,004
    assert targets[1004] == LONG
    assert run.d[at(run, 1003)] == LONG
    assert run.sides[at(run, 1004)] == 0
    assert run.cost[at(run, 1004)] == 0.0
    assert run.d[at(run, 1004)] == LONG
    assert [i + W + 1 for i, sides in enumerate(run.sides) if sides > 0] == [1001, LENGTH]


def test_t6_v_change_of_direction_pays_two_sides():
    """(v) Un cambio de dirección paga dos lados; entrar y cerrar pagan uno cada uno."""
    slow = [1.0] * LENGTH
    fast = [1.01] * 1002 + [0.99] * (LENGTH - 1002)  # f > s hasta la barra 1,002; luego f < s
    targets, run = simulate_from_emas(fast, slow)
    assert targets[W + 1] == LONG
    assert targets[1004] == SHORT  # señal corta detectada en 1,003, ejecutada en 1,004
    assert run.d[at(run, 1003)] == LONG
    assert run.d[at(run, 1004)] == SHORT
    assert run.sides[at(run, 1004)] == 2
    expected = 2 * (SPREAD / 1.0) * run.k_minus[at(run, 1004)]
    assert run.cost[at(run, 1004)] == pytest.approx(expected, rel=1e-15)
    assert [run.sides[at(run, bar)] for bar in (1001, 1004, LENGTH)] == [1, 2, 1]
    assert sum(run.sides) == 4


@pytest.mark.parametrize(
    ("before", "now", "initial", "signal"),
    [
        (-1, 1, SHORT, LONG),  # f < s y luego f > s
        (0, 1, None, LONG),  # el empate previo cuenta como f <= s
        (1, 1, LONG, None),
        (0, 0, None, None),
        (-1, 0, SHORT, None),  # f = s no es f > s
        (1, 0, LONG, None),  # f = s no es f < s
        (1, -1, LONG, SHORT),
        (0, -1, None, SHORT),  # el empate previo cuenta como f >= s
        (-1, -1, SHORT, None),
    ],
)
def test_t6_the_four_inequalities_and_the_initial_position(before, now, initial, signal):
    """Posición inicial por el signo en la barra de calentamiento y las cuatro desigualdades.

    Con calentamiento 2, la barra 2 fija la posición inicial (se ejecuta en la barra 3) y la
    barra 3 detecta la señal (se ejecuta en la barra 4). `before` y `now` son el signo de
    (rápida - lenta) en esas dos barras; después el signo no cambia, así que no hay más señales.
    """
    slow = [1.0] * 5
    fast = [1.0, 1.0 + 0.5 * before, 1.0 + 0.5 * now, 1.0 + 0.5 * now, 1.0 + 0.5 * now]
    targets = engine.pending_targets(fast, slow, warmup=2)
    assert targets[1:3] == [None, None]
    assert targets[3] == initial
    assert targets[4] == signal
    assert targets[5] is None


def test_t6_crossings_before_the_warmup_do_not_open_positions():
    """Las barras 1 a 1,000 solo calculan las medias: ninguna ejecución antes de la 1,001."""
    prices = [1.0 + 0.05 * ((i // 40) % 2) for i in range(LENGTH)]  # cruza muchas veces
    fast, slow = prices_and_emas(prices)
    targets = engine.pending_targets(fast, slow)
    assert all(target is None for target in targets[: W + 1])


def test_t6_reference_enters_at_bar_1001_and_closes_at_l():
    """Referencia larga 1x: abre en 1,001, paga el spread de entrada y de cierre."""
    prices = [1.0 + 0.0001 * (i % 7) for i in range(LENGTH)]
    run = simulate(prices, engine.reference_targets(LENGTH))
    assert run.d[0] == LONG and run.d[-1] == 0
    assert run.sides[0] == 1 and run.sides[-1] == 1
    assert sum(run.sides) == 2
    assert all(d == LONG for d in run.d[:-1])
