"""
De dónde sale el rango de la próxima corrida.

La regla que se defiende acá es una sola y es la que sostiene todo el módulo:
**el rango sale del estado guardado y no del calendario.** Escrito contra la
fecha de hoy, un día en que el trabajador no corra queda como un agujero
permanente en la serie, porque la corrida siguiente arrancaría más adelante.
"""

from datetime import date, timedelta

import pytest

from apps.seo.models import ModuleStatus, SyncState
from apps.seo.services import LATENCY_DAYS, last_available_date, months_before, sync_range
from tests.factories import create_account, create_domain


@pytest.fixture
def domain(db):
    return create_domain(create_account())


class TestTheLatency:
    def test_the_most_recent_day_is_never_today(self):
        """
        Google publica con dos o tres días de atraso: «ayer» no existe en la API.

        Pedirlo devolvería un día a medio completar que después habría que volver
        a consultar, y ese margen es justamente lo que permite no reconsultar
        nada.
        """
        assert last_available_date() < date.today()
        assert (date.today() - last_available_date()).days == LATENCY_DAYS


class TestTheWindow:
    def test_without_history_there_is_nothing_to_continue(self, domain):
        """
        Sin backfill no hay desde dónde seguir, y lo que corresponde es importar.

        Devolver un rango igual arrancaría la serie en un día suelto y dejaría
        todo lo anterior sin traer, sin que nada lo diga.
        """
        state = SyncState.objects.create(domain=domain)

        assert sync_range(state) is None

    def test_in_the_normal_case_it_asks_for_one_day(self, domain):
        last = last_available_date()
        state = SyncState.objects.create(domain=domain, last_closed_date=last - timedelta(days=1))

        assert sync_range(state) == (last, last)

    def test_up_to_date_it_asks_for_nothing(self, domain):
        state = SyncState.objects.create(domain=domain, last_closed_date=last_available_date())

        assert sync_range(state) is None

    def test_a_missed_day_is_recovered_on_the_next_run(self, domain):
        """
        **La razón de que el rango salga del estado.**

        Si el trabajador no corre en cinco días —un despliegue, Redis caído, un
        error de red—, la corrida siguiente trae sola los cinco. Con un «pedí el
        día de hace tres» escrito contra el calendario, esos cinco días no se
        pedirían nunca más.
        """
        last = last_available_date()
        state = SyncState.objects.create(domain=domain, last_closed_date=last - timedelta(days=5))

        start, end = sync_range(state)

        assert start == last - timedelta(days=4)
        assert end == last
        assert (end - start).days + 1 == 5

    def test_a_state_ahead_of_the_available_day_asks_for_nothing(self, domain):
        """
        Nunca debería pasar, pero si pasa no se pide un rango invertido.

        Un rango con el final antes del principio le pediría a Google algo que no
        significa nada, y la respuesta sería un error en vez de un vacío.
        """
        state = SyncState.objects.create(
            domain=domain, last_closed_date=last_available_date() + timedelta(days=10)
        )

        assert sync_range(state) is None


class TestTheCalendar:
    def test_months_are_counted_and_not_approximated(self):
        """
        Restar treinta días por mes acumularía cinco días de error en un año, y
        el día en que arranca la historia es justamente lo que la pantalla dice.
        """
        assert months_before(date(2026, 8, 24), 12) == date(2025, 8, 24)
        assert months_before(date(2026, 8, 24), 1) == date(2026, 7, 24)

    def test_a_day_that_does_not_exist_in_the_target_month_is_clamped(self):
        """El 31 de marzo, un mes atrás, no puede ser el 31 de febrero."""
        assert months_before(date(2026, 3, 31), 1) == date(2026, 2, 28)

    def test_it_crosses_the_year(self):
        assert months_before(date(2026, 2, 15), 3) == date(2025, 11, 15)


class TestTheInitialState:
    def test_a_new_site_starts_without_data_and_that_is_not_an_error(self, domain):
        """
        `NO_DATA` no es un fallo: es la integración sana que todavía no trajo nada.

        Confundirlo con un error mandaría a revisar permisos que están perfectos.
        """
        state = SyncState.objects.create(domain=domain)

        assert state.status == ModuleStatus.NO_DATA
        assert state.last_closed_date is None
