"""
Las variaciones de posición: lo que la pantalla promete y lo que se rompe callado.

Es la regla más frágil del módulo. Un error acá no tumba nada: dibuja una caída
donde no la hubo, o esconde una que sí pasó, y nadie se entera hasta que alguien
compara con Search Console.

Todo esto se prueba **sin base**: son funciones sobre series, y ejercitarlas
contra el ORM cambiaría lo que se comprueba por cómo se guarda.
"""

from datetime import date, timedelta

from apps.seo.services import MIN_IMPRESSIONS, deltas, window_average

LAST = date(2026, 8, 24)


def series(days: int, impressions: int, position: float) -> dict:
    """Una serie plana: el mismo día repetido hacia atrás."""
    return {LAST - timedelta(days=offset): (impressions, position) for offset in range(days)}


class TestTheMinimum:
    def test_a_window_below_the_minimum_has_no_average(self):
        """
        Con dos impresiones el promedio describe a quien buscó, no al sitio.

        Es el caso que origina la regla: un día con puestos 30·22·20 da 24 y el
        siguiente con 9·8 da 8, y la tabla anunciaría «+16 puestos» sin que nada
        se haya movido.
        """
        assert window_average({LAST: (2, 8.0)}, LAST, 1) is None

    def test_at_the_minimum_it_computes(self):
        point = window_average({LAST: (MIN_IMPRESSIONS, 7.0)}, LAST, 1)

        assert point is not None
        assert point.position == 7.0

    def test_the_minimum_applies_to_the_whole_range_and_not_to_each_day(self):
        """
        Los rangos acumulan muestra, y por eso la regla es una y no una excepción.

        Una consulta con dos impresiones por día no llega al mínimo en la ventana
        de un día y sí lo pasa en la de siete. Escribir el piso sólo para Δ1
        dejaría afuera a la consulta casi muerta, donde ni quince días juntan
        diez impresiones y dibujar una tendencia sería igual de falso.
        """
        thin = series(days=7, impressions=2, position=7.0)

        assert window_average(thin, LAST, 1) is None
        assert window_average(thin, LAST, 7) is not None

    def test_a_query_that_never_reaches_the_minimum_gets_nothing(self):
        barely = {LAST - timedelta(days=offset): (0, 7.0) for offset in range(40)}
        barely[LAST] = (1, 7.0)

        assert all(value is None for value in deltas(barely, LAST).values())


class TestTheAverage:
    def test_it_weighs_by_impressions(self):
        """
        Un día de quinientas impresiones y uno de cinco no describen lo mismo.

        Promediados en partes iguales, el día flojo tendría el mismo poder de
        mover la tendencia que el día que la sostiene.
        """
        point = window_average(
            {LAST: (500, 6.0), LAST - timedelta(days=1): (5, 30.0)}, LAST, 2
        )

        assert point is not None
        # Sin ponderar daría 18. El promedio real está pegado al día que pesa.
        assert 6.0 < point.position < 6.3

    def test_a_missing_day_is_skipped_and_not_counted_as_zero(self):
        """
        Google omite las consultas poco frecuentes para proteger la privacidad.

        Un día ausente casi nunca significa «no apareciste»: significa «no te lo
        puedo decir». Rellenarlo con un cero inventaría una posición perfecta y
        con ella una mejora que nadie observó.
        """
        with_hole = {LAST: (100, 8.0), LAST - timedelta(days=2): (100, 8.0)}

        point = window_average(with_hole, LAST, 3)

        assert point is not None
        assert point.position == 8.0


class TestTheFourWindows:
    def test_positive_means_it_improved(self):
        """
        La convención va al revés de la intuición y por eso se prueba.

        En Google el 1 es mejor que el 12, así que subir de la 12 a la 7 tiene
        que dar `+5` y no `-5`.
        """
        history = series(days=40, impressions=200, position=12.0)
        history[LAST] = (200, 7.0)

        assert deltas(history, LAST)[1]['value'] == 5.0

    def test_one_odd_day_does_not_bend_the_long_windows(self):
        """
        El caso que decidió que los rangos se comparen promediados.

        Las cuatro columnas parten del mismo día cerrado, así que comparando días
        sueltos un solo día raro las tuerce todas a la vez: una consulta clavada
        en 7 que un día marca 11 mostraría «se derrumbó» cuatro veces.

        Con promedios, las cortas avisan —que es su trabajo— y las largas dicen
        que la tendencia sigue intacta.
        """
        history = series(days=40, impressions=200, position=7.0)
        history[LAST] = (180, 11.0)

        result = deltas(history, LAST)

        assert result[1]['value'] == -4.0
        assert abs(result[7]['value']) < 1.0
        assert abs(result[15]['value']) < 0.5

    def test_a_real_drop_is_recorded_once_it_is_confirmed(self):
        """
        El promedio no esconde caídas: espera a confirmarlas.

        Con el sitio ya asentado en la posición nueva, la ventana larga registra
        la caída entera. Si no lo hiciera, promediar sería tapar.
        """
        history = series(days=40, impressions=200, position=7.0)
        for offset in range(7):
            history[LAST - timedelta(days=offset)] = (200, 11.0)

        assert deltas(history, LAST)[7]['value'] == -4.0

    def test_it_carries_both_ends_so_the_tooltip_can_show_them(self):
        history = series(days=40, impressions=200, position=7.0)
        history[LAST] = (200, 5.0)

        first = deltas(history, LAST)[1]

        assert first['current'] == 5.0
        assert first['previous'] == 7.0

    def test_without_history_the_long_windows_have_nothing_to_compare(self):
        """
        Δ15 necesita treinta días. Durante el primer mes dice «—», y está bien:
        prometer una tendencia sin período anterior sería inventarla.
        """
        short = series(days=5, impressions=200, position=7.0)

        result = deltas(short, LAST)

        assert result[1] is not None
        assert result[15] is None
