"""
La guardia del cliente de Google.

Un solo hecho, y es el que sostiene el principio II: **ninguna llamada a Google
puede ejecutarse sin una reserva concedida**. Se prueba sobre todas las
operaciones y no sobre una, porque el riesgo real no es la que ya existe sino la
que alguien agregue mañana olvidándose del control.
"""

import pytest

from apps.gsc.client import MissingReservation, SearchConsoleClient
from apps.jobs.budget import Origin, Reservation


class ServiceThatMustNotBeUsed:
    """
    Doble que falla si alguien lo toca.

    Es la forma de distinguir «la llamada fue rechazada» de «la llamada salió y
    devolvió vacío»: si la guardia fallara, este objeto lo diría en voz alta.
    """

    def __getattr__(self, name):
        raise AssertionError(
            f'Se intentó usar el servicio de Google («{name}») sin reserva concedida.'
        )


DATE = '2026-08-18'
WITHOUT_QUOTA = Reservation(
    domain_id='d', origin=Origin.AUTOMATIC, granted=0, requested=10, date=DATE
)
WITH_QUOTA = Reservation(domain_id='d', origin=Origin.AUTOMATIC, granted=1, requested=1, date=DATE)


@pytest.fixture
def client():
    return SearchConsoleClient(service=ServiceThatMustNotBeUsed())


CALLS = [
    ('inspect_url', {'property_uri': 'sc-domain:ejemplo.test', 'url': 'https://ejemplo.test/'}),
    ('list_sitemaps', {'property_uri': 'sc-domain:ejemplo.test'}),
    ('submit_sitemap', {'property_uri': 'sc-domain:ejemplo.test', 'location': 'https://e/s.xml'}),
    ('delete_sitemap', {'property_uri': 'sc-domain:ejemplo.test', 'location': 'https://e/s.xml'}),
    ('list_sites', {}),
    ('get_site', {'property_uri': 'sc-domain:ejemplo.test'}),
]


@pytest.mark.parametrize(('method', 'kwargs'), CALLS)
def test_no_call_runs_without_a_reservation(client, method, kwargs):
    with pytest.raises(MissingReservation):
        getattr(client, method)(reservation=None, **kwargs)


@pytest.mark.parametrize(('method', 'kwargs'), CALLS)
def test_no_call_runs_with_the_reservation_denied(client, method, kwargs):
    """Una reserva de cero no es un permiso: es el registro de que no había cupo."""
    with pytest.raises(MissingReservation):
        getattr(client, method)(reservation=WITHOUT_QUOTA, **kwargs)


#: Las operaciones que **no** pasan por el control de cuota, uña por uña.
#:
#: `QuotaBudget` modela un cupo **agotable** —2.000 inspecciones por día y por
#: sitio, y cuando se acaban hay que esperar al día siguiente—. Search Analytics
#: tiene otra clase de límite: 1.200 por minuto y por sitio, sin tope diario, que
#: se renuevan solos. Descontarlo de ese presupuesto le comería inspecciones al
#: recorrido de cobertura sin usar ninguna de las suyas.
#:
#: La lista es explícita para que agregar una segunda excepción **obligue a
#: tocar este archivo**, que es donde se explica el criterio. Antes de sumar una,
#: hay que comprobar en la documentación de Google que el límite de esa API
#: también se renueva solo: confundir el alcance de una cuota ya causó errores acá.
UNMETERED_OPERATIONS = frozenset({'search_analytics'})


def test_every_public_operation_requires_a_reservation():
    """
    Ninguna operación del cliente puede omitir la reserva sin estar declarada.

    Es la parte del test que mira hacia adelante: si mañana se agrega un método
    público sin `reservation` y sin anotarlo arriba, esto falla antes de que
    llegue a producción.
    """
    import inspect

    public_names = [
        name
        for name in dir(SearchConsoleClient)
        if not name.startswith('_') and callable(getattr(SearchConsoleClient, name, None))
    ]

    for name in public_names:
        signature = inspect.signature(getattr(SearchConsoleClient, name))
        if name in UNMETERED_OPERATIONS:
            assert 'reservation' not in signature.parameters, (
                f'«{name}» está declarada sin medición y sin embargo pide reserva. '
                f'O se saca de UNMETERED_OPERATIONS o se le saca el parámetro.'
            )
            continue

        assert 'reservation' in signature.parameters, (
            f'«{name}» habla con Google sin pedir reserva de cupo. '
            f'Toda operación tiene que recibir una reserva concedida, salvo las '
            f'declaradas en UNMETERED_OPERATIONS con su motivo.'
        )


def test_the_unmetered_list_has_no_dead_names():
    """
    Lo que se declara sin medición tiene que existir.

    Sin esto, un método renombrado dejaría su nombre viejo en la lista y el
    nombre nuevo entraría sin control ni aviso: la excepción sobreviviría a la
    operación que la justificaba.
    """
    for name in UNMETERED_OPERATIONS:
        assert callable(getattr(SearchConsoleClient, name, None)), (
            f'«{name}» está declarada sin medición y no existe en el cliente.'
        )


def test_with_quota_the_guard_lets_it_through_and_the_double_fails(client):
    """
    Confirma que la guardia es lo que rechaza, y no otra cosa.

    Con reserva concedida la llamada avanza hasta el servicio, y el doble
    protesta. Sin esta comprobación, un cliente roto que rechazara todo pasaría
    los tests anteriores igual.
    """
    with pytest.raises(AssertionError, match='sin reserva concedida'):
        client.list_sites(reservation=WITH_QUOTA)
