"""
La evidencia de un pedido de indexación, que es el entregable de la función.

Estos tests no comprueban que la aplicación sepa hablar con Google: eso lo
comprueba el cliente. Comprueban lo único que sostiene el producto delante de
alguien que no cree lo que le dicen: que **el pedido y la respuesta quedan
guardados tal como viajaron, y que quedan guardados también cuando Google dice
que no**.

Es la parte que falla en silencio. Un lote que guarda la evidencia sólo en el
camino feliz se ve idéntico a uno que la guarda siempre, hasta el día en que hay
que mostrar por qué se detuvo y no hay nada que mostrar.
"""

import pytest

from apps.gsc.errors import GoogleErrorCode
from apps.indexing.errors import IndexingErrorCode
from apps.indexing.models import IndexingRequest, IndexingState
from apps.indexing.services import (
    clear_error,
    create_batch,
    resume_batch,
    send_batch,
)
from apps.jobs.models import BatchOrigin, BatchState
from apps.sitemaps.models import Url
from tests.doubles import FakeIndexingClient
from tests.factories import create_account, create_credential, create_domain

pytestmark = pytest.mark.django_db


QUOTA = (GoogleErrorCode.QUOTA_EXCEEDED, 429)
DENIED = (GoogleErrorCode.PERMISSION_DENIED, 403)
REJECTED = (IndexingErrorCode.URL_REJECTED, 400)


@pytest.fixture
def domain():
    # Con credencial: armar un lote pasa por `ensure_operational`, que frena
    # cualquier trabajo sobre una cuenta que todavía no conectó Google.
    account = create_account()
    create_credential(account)
    return create_domain(account)


def urls(domain, count):
    return [
        Url.objects.create(domain=domain, loc=f'https://{domain.hostname}/p{index}')
        for index in range(1, count + 1)
    ]


def test_a_request_that_google_accepts_keeps_both_bodies(domain):
    """
    Lo enviado y lo recibido, enteros.

    No un booleano ni un resumen: seis meses después, un `ok=True` no prueba
    nada. Lo que se puede mostrar es lo que viajó.
    """
    batch = create_batch(domain, urls(domain, 1))
    batch.state = BatchState.QUEUED
    batch.save(update_fields=['state'])

    send_batch(batch, client=FakeIndexingClient())

    request = IndexingRequest.objects.get(batch=batch)
    assert request.state == IndexingState.SENT
    assert request.sent_at is not None
    assert request.request_body['url'] == f'https://{domain.hostname}/p1'
    assert request.request_body['type'] == 'URL_UPDATED'
    assert request.response_status == 200
    assert request.response_body['urlNotificationMetadata']['url'] == request.loc


def test_a_request_that_google_turns_down_keeps_both_bodies_too(domain):
    """
    **El caso que importa de verdad.**

    La respuesta que informa que se agotó la cuota es tan evidencia como la que
    confirma un envío: es la que contesta «¿por qué se detuvo?» y la que
    documenta que el límite existe y no lo inventó la aplicación. Guardarla sólo
    en el camino feliz dejaría sin prueba justo la discusión que esto viene a
    terminar.
    """
    batch = create_batch(domain, urls(domain, 1))
    batch.state = BatchState.QUEUED
    batch.save(update_fields=['state'])

    send_batch(batch, client=FakeIndexingClient(default=QUOTA))

    request = IndexingRequest.objects.get(batch=batch)
    assert request.state == IndexingState.ERROR
    assert request.error_code == GoogleErrorCode.QUOTA_EXCEEDED
    assert request.request_body['url'] == request.loc
    assert request.response_status == 429
    assert request.response_body['error']['status'] == GoogleErrorCode.QUOTA_EXCEEDED


def test_running_out_of_quota_stops_the_batch_and_leaves_the_rest_pending(domain):
    """
    Se corta donde Google dijo basta, y lo que sigue queda esperando.

    No se marca fallido lo que nunca se intentó: cuatro direcciones sin enviar
    no son cuatro rechazos, y contarlas como tales inventaría cuatro respuestas
    de Google que no existen.
    """
    rows = urls(domain, 5)
    batch = create_batch(domain, rows)
    batch.state = BatchState.QUEUED
    batch.save(update_fields=['state'])

    client = FakeIndexingClient(answers={rows[2].loc: QUOTA})
    send_batch(batch, client=client)

    assert client.published == [rows[0].loc, rows[1].loc, rows[2].loc]

    states = list(
        IndexingRequest.objects.filter(batch=batch).order_by('position').values_list(
            'state', flat=True
        )
    )
    assert states == [
        IndexingState.SENT,
        IndexingState.SENT,
        IndexingState.ERROR,
        IndexingState.PENDING,
        IndexingState.PENDING,
    ]

    batch.refresh_from_db()
    assert batch.state == BatchState.PARTIAL
    assert batch.summary['stopped_by'] == GoogleErrorCode.QUOTA_EXCEEDED


def test_a_rejected_address_is_marked_and_the_batch_goes_on(domain):
    """
    Una dirección mal formada no condena a las otras cuatrocientas noventa y nueve.

    Es la única excepción a «apenas responda mal, se corta», y existe porque el
    alcance del problema es otro: la cuota y el permiso condenan todo lo que
    venga detrás, una dirección rechazada no dice nada sobre las demás.
    """
    rows = urls(domain, 4)
    batch = create_batch(domain, rows)
    batch.state = BatchState.QUEUED
    batch.save(update_fields=['state'])

    client = FakeIndexingClient(answers={rows[1].loc: REJECTED})
    send_batch(batch, client=client)

    assert client.published == [row.loc for row in rows]

    batch.refresh_from_db()
    assert batch.state == BatchState.PARTIAL
    assert batch.processed_items == 3
    assert batch.failed_items == 1
    assert 'stopped_by' not in batch.summary


def test_permission_denied_stops_the_batch(domain):
    """Un permiso que Google niega condena todo lo que venga detrás."""
    rows = urls(domain, 3)
    batch = create_batch(domain, rows)
    batch.state = BatchState.QUEUED
    batch.save(update_fields=['state'])

    client = FakeIndexingClient(default=DENIED)
    send_batch(batch, client=client)

    assert client.published == [rows[0].loc]
    batch.refresh_from_db()
    assert batch.summary['stopped_by'] == GoogleErrorCode.PERMISSION_DENIED


def test_resuming_never_resends_what_already_went_out(domain):
    """
    Cada reenvío gasta del techo real de Google.

    Un reanudar que recorriera el lote entero le diría dos veces lo mismo sobre
    las direcciones que ya salieron, gastaría el cupo dos veces y ensuciaría la
    evidencia con envíos que nadie pidió.
    """
    rows = urls(domain, 4)
    batch = create_batch(domain, rows)
    batch.state = BatchState.QUEUED
    batch.save(update_fields=['state'])

    send_batch(batch, client=FakeIndexingClient(answers={rows[1].loc: QUOTA}))

    stuck = IndexingRequest.objects.get(batch=batch, position=2)
    clear_error(stuck)

    batch.refresh_from_db()
    batch.state = BatchState.QUEUED
    batch.save(update_fields=['state'])

    second = FakeIndexingClient()
    send_batch(batch, client=second)

    # La primera ya había salido: no vuelve a entrar.
    assert second.published == [rows[1].loc, rows[2].loc, rows[3].loc]

    batch.refresh_from_db()
    assert batch.state == BatchState.COMPLETED


def test_clearing_an_error_does_not_erase_the_evidence(domain):
    """
    Se limpia lo que frena el lote, no el rastro de lo que pasó.

    Un pedido fallido y su respuesta son prueba tanto como un éxito. Si limpiar
    el error borrara los cuerpos, reanudar un lote sería la forma más fácil de
    destruir justamente la evidencia que hay que conservar.
    """
    rows = urls(domain, 1)
    batch = create_batch(domain, rows)
    batch.state = BatchState.QUEUED
    batch.save(update_fields=['state'])

    send_batch(batch, client=FakeIndexingClient(default=REJECTED))

    request = IndexingRequest.objects.get(batch=batch)
    before = (request.request_body, request.response_body, request.response_status)

    clear_error(request)
    request.refresh_from_db()

    assert request.state == IndexingState.PENDING
    assert request.error_code == ''
    assert (request.request_body, request.response_body, request.response_status) == before


def test_resume_refuses_a_batch_with_nothing_pending(domain):
    """Retomar algo que no dejó nada pendiente sería reenviarlo entero."""
    from apps.indexing.services import NothingToSend

    batch = create_batch(domain, urls(domain, 2))
    batch.state = BatchState.QUEUED
    batch.save(update_fields=['state'])
    send_batch(batch, client=FakeIndexingClient())

    batch.refresh_from_db()
    with pytest.raises(NothingToSend):
        resume_batch(batch)


def test_the_evidence_survives_the_url_leaving_the_inventory(domain):
    """
    La prueba no cae con la fila de la dirección.

    Es la razón por la que `loc` se guarda congelado al lado de la clave foránea
    en vez de leerse de ella: una dirección borrada no puede llevarse consigo la
    constancia de que un día se la mandamos a Google.
    """
    rows = urls(domain, 1)
    batch = create_batch(domain, rows)
    batch.state = BatchState.QUEUED
    batch.save(update_fields=['state'])
    send_batch(batch, client=FakeIndexingClient())

    location = rows[0].loc
    rows[0].delete()

    request = IndexingRequest.objects.get(batch=batch)
    assert request.url_id is None
    assert request.loc == location
    assert request.request_body['url'] == location


def test_a_batch_created_by_hand_does_not_notify(domain, django_capture_on_commit_callbacks):
    """
    Quien lo armó lo tiene en pantalla.

    Avisarle de algo que está mirando gasta el único canal que el producto tiene
    para decir que algo pasó, y ése es el canal que sostiene los otros dos
    caminos.
    """
    from apps.notifications.models import Notification

    with django_capture_on_commit_callbacks(execute=True):
        create_batch(domain, urls(domain, 1), origin=BatchOrigin.MANUAL)

    assert not Notification.objects.filter(kind='INDEXING_BATCH_READY').exists()


def test_a_batch_created_by_the_api_notifies_and_asks_for_action(
    domain, django_capture_on_commit_callbacks
):
    """
    Un borrador que nadie mira es trabajo que se acumula invisible.

    Los caminos que no tienen a una persona delante —un despliegue, un recorrido
    de fondo— tienen que avisar, o el producto vuelve a tener el problema con el
    que empezó.
    """
    from apps.notifications.models import Notification, NotificationLevel

    # El aviso se deja al confirmar la transacción, porque apunta a un lote que
    # hasta entonces no existe para nadie más. En un test hay que provocar ese
    # confirmado a mano: sin esto, la aserción probaría el envoltorio de pytest
    # y no el aviso.
    with django_capture_on_commit_callbacks(execute=True):
        batch = create_batch(domain, urls(domain, 3), origin=BatchOrigin.API)

    notification = Notification.objects.get(kind='INDEXING_BATCH_READY')
    assert notification.level == NotificationLevel.ACTION
    assert notification.dedupe_key == f'indexing-batch:{batch.id}'
    assert notification.params['total'] == 3
