"""
Los tres caminos que crean un lote de indexación, y la regla que los une.

**Nada sale a Google sin que una persona lo confirme.** Los tres —a mano, por la
API tras un despliegue, y solo después de un recorrido— terminan en un lote en
borrador, y `run_batch()` es el único paso de ahí a la cola.

Es la clase de regla que se rompe sin síntoma: un camino que enviara solo se ve
igual que uno que espera, hasta el día en que gastó el cupo de alguien mientras
nadie miraba.
"""

import pytest

from apps.indexing.models import IndexingRequest, IndexingState
from apps.indexing.rules import triggers_automatic_request
from apps.indexing.services import (
    BatchNotEditable,
    create_batch,
    delete_batch,
    eligible_urls,
    remove_url,
    run_batch,
)
from apps.jobs.models import BatchKind, BatchOrigin, BatchState
from apps.sitemaps.models import CoverageState, Url
from tests.factories import create_account, create_credential, create_domain

pytestmark = pytest.mark.django_db


@pytest.fixture
def domain():
    account = create_account()
    create_credential(account)
    return create_domain(account)


def url(domain, path, **extra):
    return Url.objects.create(domain=domain, loc=f'https://{domain.hostname}/{path}', **extra)


# --- La regla que une los tres ----------------------------------------------


@pytest.mark.parametrize(
    'origin', [BatchOrigin.MANUAL, BatchOrigin.API, BatchOrigin.SCHEDULED]
)
def test_every_path_creates_the_batch_in_draft(domain, origin):
    """
    Venga de donde venga, nace esperando.

    Es lo que deja un solo disparador de ejecución en lugar de tres mecánicas
    que habría que mantener parejas.
    """
    batch = create_batch(domain, [url(domain, 'a')], origin=origin)

    assert batch.state == BatchState.DRAFT
    assert batch.kind == BatchKind.URL_INDEXING
    assert not batch.is_terminal


def test_a_draft_has_not_sent_anything(domain):
    """Un borrador tiene sus filas y ninguna salió."""
    batch = create_batch(domain, [url(domain, 'a'), url(domain, 'b')])

    rows = IndexingRequest.objects.filter(batch=batch)
    assert rows.count() == 2
    assert all(row.state == IndexingState.PENDING for row in rows)
    assert all(row.request_body == {} for row in rows)
    assert all(row.sent_at is None for row in rows)


def test_running_a_draft_is_the_only_way_into_the_queue(domain, django_capture_on_commit_callbacks):
    batch = create_batch(domain, [url(domain, 'a')])

    with django_capture_on_commit_callbacks(execute=False):
        run_batch(batch)

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


def test_a_batch_that_left_draft_can_no_longer_be_edited(domain):
    """
    Lo que salió hacia Google no se puede cambiar.

    No es una restricción técnica: un lote que ya corrió tiene evidencia
    adentro, y editarlo después sería reescribir la prueba.
    """
    batch = create_batch(domain, [url(domain, 'a')])
    batch.state = BatchState.QUEUED
    batch.save(update_fields=['state'])

    request = IndexingRequest.objects.get(batch=batch)

    with pytest.raises(BatchNotEditable):
        remove_url(batch, request.id)

    with pytest.raises(BatchNotEditable):
        delete_batch(batch)


def test_taking_an_address_out_marks_it_and_keeps_it_in_the_list(domain):
    """
    Se marca en vez de borrar: el lote tiene que poder explicar qué se decidió
    no enviar, y una fila que desaparece no explica nada.
    """
    batch = create_batch(domain, [url(domain, 'a'), url(domain, 'b')])
    request = IndexingRequest.objects.get(batch=batch, position=1)

    remove_url(batch, request.id)

    request.refresh_from_db()
    batch.refresh_from_db()
    assert request.state == IndexingState.REMOVED
    assert IndexingRequest.objects.filter(batch=batch).count() == 2
    assert batch.total_items == 1


# --- Camino 2: lo que la API descarta y por qué ------------------------------


def test_the_api_path_discards_foreign_addresses_and_says_so(domain):
    """
    Quien integra esto lee la respuesta en su registro de despliegue.

    Tiene que poder contestar «mandé cuarenta, entraron treinta y siete, ¿qué
    pasó con las otras tres?» sin abrir la pantalla.
    """
    url(domain, 'a')

    accepted, discarded = eligible_urls(
        domain,
        [f'https://{domain.hostname}/a', 'https://otro-sitio.test/b'],
    )

    assert [item.loc for item in accepted] == [f'https://{domain.hostname}/a']
    assert discarded['foreign'] == ['https://otro-sitio.test/b']
    assert discarded['retired'] == []


def test_the_api_path_discards_what_left_the_sitemap(domain):
    """
    Son dos motivos distintos y se informan por separado.

    Una dirección ajena a la propiedad nunca va a entrar; una que salió del
    sitemap puede volver a entrar mañana, y confundirlas mandaría a arreglar el
    problema equivocado.
    """
    url(domain, 'a')
    url(domain, 'vieja', in_sitemap=False)

    accepted, discarded = eligible_urls(
        domain, [f'https://{domain.hostname}/a', f'https://{domain.hostname}/vieja']
    )

    assert len(accepted) == 1
    assert discarded['retired'] == [f'https://{domain.hostname}/vieja']
    assert discarded['foreign'] == []


def test_a_repeated_address_enters_once(domain):
    """El mismo pedido dos veces gastaría dos del techo real para decir lo mismo."""
    url(domain, 'a')

    accepted, _ = eligible_urls(
        domain, [f'https://{domain.hostname}/a', f'https://{domain.hostname}/a']
    )

    assert len(accepted) == 1


# --- Camino 3: sobre qué estados dispara solo -------------------------------


@pytest.mark.parametrize(
    'state',
    [
        CoverageState.CRAWLED_NOT_INDEXED,
        CoverageState.DISCOVERED_NOT_INDEXED,
        CoverageState.OTHER_NOT_INDEXED,
        CoverageState.FETCH_ERROR,
    ],
)
def test_the_four_states_where_asking_can_resolve_something_do_trigger(state):
    assert triggers_automatic_request(state) is True


@pytest.mark.parametrize(
    'state',
    [
        CoverageState.INDEXED,
        CoverageState.EXCLUDED_NOINDEX,
        CoverageState.BLOCKED_ROBOTS,
        CoverageState.DUPLICATE_CANONICAL,
        CoverageState.REDIRECT,
        CoverageState.UNKNOWN,
    ],
)
def test_the_states_where_asking_is_condemned_never_trigger(state):
    """
    No es «todo lo distinto de indexada».

    Pedir la indexación de una página que declara `noindex` es pelear contra la
    directiva del propio sitio, y el cupo que se gasta ahí le falta a una que sí
    podía prosperar. `UNKNOWN` queda afuera por otro motivo: nunca se consultó,
    así que pedirlo sería adivinar.
    """
    assert triggers_automatic_request(state) is False


def test_an_address_already_waiting_is_not_proposed_again(domain):
    """
    Dos borradores con la misma dirección la mandarían dos veces.

    Cada envío gasta del techo real de Google, así que proponerla de nuevo
    mientras la anterior espera es gastar el cupo dos veces para decir lo mismo.
    """
    from apps.indexing.services import pending_request_exists

    target = url(domain, 'a')
    create_batch(domain, [target])

    assert pending_request_exists(target) is True


def test_an_address_never_requested_is_open_for_the_once_only_rule(domain):
    """
    `FETCH_ERROR` entra al automático una sola vez por dirección.

    Si el fallo de descarga era pasajero, el pedido sirvió. Si al siguiente
    recorrido sigue igual, el problema está en el sitio y no en el índice de
    Google, y reintentar sólo quema cupo.
    """
    from apps.indexing.services import has_been_requested

    target = url(domain, 'a', coverage_state=CoverageState.FETCH_ERROR)
    assert has_been_requested(target) is False

    batch = create_batch(domain, [target])
    IndexingRequest.objects.filter(batch=batch).update(state=IndexingState.SENT)

    assert has_been_requested(target) is True
