"""
La pantalla de un lote de indexación, y las dos afirmaciones que no puede mezclar.

Lo que ninguna prueba de servicio puede ver es el viaje hasta el navegador. Acá
se defiende que llegue lo que la pantalla necesita para no mentir:

- que **«le pedimos que la mire» y «Google dice que está indexada» viajen en
  claves distintas**. Es la separación entera de la función: compartir campo las
  encimaría en la pantalla igual que las encimaría en la base;
- que la evidencia viaje cruda, sin resumir;
- que un borrador diga que es un borrador, porque de eso depende que la pantalla
  ofrezca «Enviar» y no dé por hecho que ya salió.
"""

import json

import pytest

from apps.indexing.models import IndexingRequest, IndexingState
from apps.indexing.services import create_batch, send_batch
from apps.jobs.models import Batch, BatchKind, BatchState
from apps.sitemaps.models import CoverageState, Url
from tests.doubles import FakeIndexingClient
from tests.factories import create_account, create_credential, create_domain

pytestmark = pytest.mark.django_db


@pytest.fixture
def account(client):
    account = create_account()
    create_credential(account)
    client.force_login(account)
    return account


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


def screen(client, batch) -> dict:
    """Las props que efectivamente recibe la vista."""
    response = client.get(f'/search-console/indexing/{batch.id}', HTTP_X_INERTIA='true')
    assert response.status_code == 200
    return response.json()['props']


def test_a_draft_says_it_is_a_draft_and_carries_nothing_sent(client, domain):
    url = Url.objects.create(domain=domain, loc='https://ejemplo.test/a')
    batch = create_batch(domain, [url])

    props = screen(client, batch)

    assert props['batch']['state'] == BatchState.DRAFT
    assert props['batch']['is_draft'] is True
    assert props['batch']['is_running'] is False
    assert props['progress'] == {'pending': 1, 'sent': 0, 'errors': 0, 'removed': 0}
    assert props['requests'][0]['has_evidence'] is False


def test_the_two_axes_travel_in_separate_keys(client, domain):
    """
    **La separación que sostiene la función entera.**

    Lo que Google dice de la dirección y lo que nosotros le pedimos son dos
    afirmaciones distintas. Si compartieran clave, la pantalla no tendría manera
    de dibujarlas separadas aunque quisiera.
    """
    url = Url.objects.create(
        domain=domain,
        loc='https://ejemplo.test/a',
        coverage_state=CoverageState.CRAWLED_NOT_INDEXED,
    )
    batch = create_batch(domain, [url])
    batch.state = BatchState.QUEUED
    batch.save(update_fields=['state'])
    send_batch(batch, client=FakeIndexingClient())

    row = screen(client, batch)['requests'][0]

    assert row['state'] == IndexingState.SENT
    assert row['coverage_state'] == CoverageState.CRAWLED_NOT_INDEXED
    # Y la fila **no** tiene un único campo que mezcle los dos.
    assert row['state'] != row['coverage_state']


def test_an_address_google_already_ruled_out_arrives_flagged(client, domain):
    """
    La advertencia viaja como código y no como frase.

    Pedir la indexación de una página con `noindex` es pelear contra la
    directiva del propio sitio. No se prohíbe —quien opera el sitio puede saber
    algo que nosotros no— pero la pantalla tiene que poder decirlo.
    """
    url = Url.objects.create(
        domain=domain,
        loc='https://ejemplo.test/a',
        coverage_state=CoverageState.EXCLUDED_NOINDEX,
    )
    batch = create_batch(domain, [url])

    row = screen(client, batch)['requests'][0]

    assert row['discouraged'] == 'EXCLUDED_NOINDEX'


def test_the_evidence_travels_raw_to_the_screen(client, domain):
    url = Url.objects.create(domain=domain, loc='https://ejemplo.test/a')
    batch = create_batch(domain, [url])
    batch.state = BatchState.QUEUED
    batch.save(update_fields=['state'])
    send_batch(batch, client=FakeIndexingClient())

    row = screen(client, batch)['requests'][0]

    assert row['has_evidence'] is True
    assert row['request_body']['url'] == 'https://ejemplo.test/a'
    assert row['response_body']['urlNotificationMetadata']['url'] == 'https://ejemplo.test/a'
    assert row['response_status'] == 200


def test_a_stopped_batch_says_why_as_a_code(client, domain):
    """
    El motivo va como código porque la pantalla ramifica por él (RT-08).

    Es lo que habilita «limpiar y reanudar» sin que la pantalla tenga que leer
    una frase para saber qué pasó.
    """
    from apps.gsc.errors import GoogleErrorCode

    rows = [
        Url.objects.create(domain=domain, loc=f'https://ejemplo.test/{n}') for n in range(1, 4)
    ]
    batch = create_batch(domain, rows)
    batch.state = BatchState.QUEUED
    batch.save(update_fields=['state'])
    send_batch(batch, client=FakeIndexingClient(default=(GoogleErrorCode.QUOTA_EXCEEDED, 429)))

    props = screen(client, batch)

    assert props['batch']['stopped_by'] == GoogleErrorCode.QUOTA_EXCEEDED
    assert props['progress']['pending'] == 2


def test_the_evidence_downloads_as_a_file_with_both_bodies(client, domain):
    """
    El entregable de la función, en un archivo que se adjunta a un informe.

    Es JSON y no CSV a propósito: un cuerpo anidado adentro de una celda de
    planilla deja de ser verificable.
    """
    url = Url.objects.create(domain=domain, loc='https://ejemplo.test/a')
    batch = create_batch(domain, [url])
    batch.state = BatchState.QUEUED
    batch.save(update_fields=['state'])
    send_batch(batch, client=FakeIndexingClient())

    response = client.get(f'/search-console/indexing/{batch.id}/evidence')

    assert response.status_code == 200
    assert response['Content-Disposition'].endswith(f'"indexing-{batch.id}.json"')

    payload = json.loads(response.content)
    assert payload['domain'] == domain.hostname
    assert payload['requests'][0]['url'] == 'https://ejemplo.test/a'
    assert payload['requests'][0]['request_body']['url'] == 'https://ejemplo.test/a'
    assert payload['requests'][0]['response_body']['urlNotificationMetadata']


def test_the_screen_sends_what_was_picked_without_asking_twice(client, domain):
    """
    El camino a mano crea **y encola**: el diálogo ya fue la confirmación.

    Es la única de las tres vías que no pasa por borrador, y no es una excepción
    a «nada sale sin que una persona lo confirme»: la cumple antes, en el
    diálogo que enumera las direcciones. Un borrador después sería preguntar dos
    veces lo mismo, que es como se enseña a apretar sin leer.

    **El cuerpo va como JSON, que es como lo manda el navegador.** Inertia envía
    JSON salvo que haya un archivo, y con JSON `request.POST` queda vacío. Esta
    prueba mandaba un formulario y pasaba mientras la pantalla real elegía
    cuatro direcciones, el servidor recibía cero y la persona volvía a la tabla
    sin que nada dijera qué había pasado.
    """
    rows = [
        Url.objects.create(domain=domain, loc=f'https://ejemplo.test/{n}') for n in range(1, 3)
    ]

    response = client.post(
        f'/search-console/domains/{domain.id}/indexing',
        {'urls': [str(row.id) for row in rows]},
        content_type='application/json',
    )

    assert response.status_code == 302
    # Se vuelve a la pestaña que muestra el lote saliendo, no a la ficha: lo que
    # se quiere ver después de mandar es que está saliendo.
    assert response.url.endswith('?tab=indexing')

    batch = Batch.objects.get(kind=BatchKind.URL_INDEXING)
    assert batch.state == BatchState.QUEUED
    assert IndexingRequest.objects.count() == 2


def test_the_same_selection_works_as_a_plain_form_too(client, domain):
    """
    La ruta existe fuera de Inertia y tiene que seguir contestando.

    Es la otra mitad de lo que `field_list` promete: leer las dos formas. Una
    vista que lea sólo la que usa la pantalla anda hasta el día que alguien
    adjunta un archivo y el envío pasa a multipart.
    """
    row = Url.objects.create(domain=domain, loc='https://ejemplo.test/a')

    response = client.post(
        f'/search-console/domains/{domain.id}/indexing', {'urls': [str(row.id)]}
    )

    assert response.status_code == 302
    assert IndexingRequest.objects.count() == 1


def test_the_other_two_paths_still_wait_for_a_person(domain):
    """
    Sólo el camino a mano se saltea el borrador, y sólo porque hubo un diálogo.

    Un despliegue por la API y un recorrido de fondo no tienen a nadie delante,
    así que siguen proponiendo. Si esto se afloja, el producto pasa a gastar
    cupo real mientras nadie mira, que es lo que la decisión D5 vino a impedir.
    """
    from apps.indexing.services import create_batch
    from apps.jobs.models import BatchOrigin

    url = Url.objects.create(domain=domain, loc='https://ejemplo.test/a')

    for origin in (BatchOrigin.API, BatchOrigin.SCHEDULED):
        Url.objects.filter(loc='https://ejemplo.test/a')
        batch = create_batch(domain, [url], origin=origin)
        assert batch.state == BatchState.DRAFT
        batch.delete()


def test_choosing_nothing_comes_back_saying_so(client, domain):
    """
    Un rechazo tiene que dejar rastro en la pantalla.

    Sin el error guardado, volver de un pedido rechazado se ve idéntico a que no
    haya pasado nada: la pantalla se recarga y listo. Es exactamente lo que se
    sentía cuando el servidor recibía la lista vacía.
    """
    response = client.post(
        f'/search-console/domains/{domain.id}/indexing',
        {'urls': []},
        content_type='application/json',
    )

    assert response.status_code == 302
    assert IndexingRequest.objects.count() == 0

    stashed = client.session.get('_validation_errors') or {}
    assert 'indexing' in stashed


def test_the_coverage_table_carries_the_two_axes_in_separate_columns(client, domain):
    """
    La tabla de cobertura sabe qué le pedimos, y lo dice en otra columna.

    Es la misma separación que en la ficha del lote, sostenida donde más fácil
    se rompe: en una fila estrecha, con los dos estados a dos centímetros uno
    del otro. La columna llega escondida, pero el dato viaja.
    """
    url = Url.objects.create(
        domain=domain,
        loc='https://ejemplo.test/a',
        coverage_state=CoverageState.CRAWLED_NOT_INDEXED,
    )
    batch = create_batch(domain, [url])
    batch.state = BatchState.QUEUED
    batch.save(update_fields=['state'])
    send_batch(batch, client=FakeIndexingClient())

    response = client.get(f'/search-console/domains/{domain.id}/coverage', HTTP_X_INERTIA='true')
    row = response.json()['props']['urls']['rows'][0]

    assert row['state'] == CoverageState.CRAWLED_NOT_INDEXED
    assert row['indexing_state'] == IndexingState.SENT


def test_an_address_nobody_asked_for_says_so_instead_of_guessing(client, domain):
    """
    Vacío y no «no indexada»: no pedimos nada por esa dirección.

    Rellenar la columna con cualquier cosa convertiría «no hicimos nada» en una
    afirmación sobre lo que hicimos.
    """
    Url.objects.create(domain=domain, loc='https://ejemplo.test/a')

    response = client.get(f'/search-console/domains/{domain.id}/coverage', HTTP_X_INERTIA='true')

    assert response.json()['props']['urls']['rows'][0]['indexing_state'] == ''


def test_an_address_taken_out_of_a_draft_never_shows_as_asked(client, domain):
    """Sacarla del lote es decidir no pedirla; la columna no puede decir lo contrario."""
    from apps.indexing.services import remove_url

    url = Url.objects.create(domain=domain, loc='https://ejemplo.test/a')
    batch = create_batch(domain, [url])
    remove_url(batch, IndexingRequest.objects.get(batch=batch).id)

    response = client.get(f'/search-console/domains/{domain.id}/coverage', HTTP_X_INERTIA='true')

    assert response.json()['props']['urls']['rows'][0]['indexing_state'] == ''


# --- La pestaña de cobertura, que muestra lo de ahora ------------------------


def tab(client, domain) -> dict:
    """Lo que la pestaña de indexación recibe."""
    response = client.get(f'/search-console/domains/{domain.id}/coverage', HTTP_X_INERTIA='true')
    return response.json()['props']['indexing']


def test_with_nothing_ever_sent_the_tab_has_no_batch(client, domain):
    assert tab(client, domain)['batch'] is None


def test_the_tab_shows_the_one_running_over_the_one_that_finished(client, domain):
    """
    Lo que está pasando ahora gana sobre lo que terminó, aunque sea más viejo.

    Es el único momento en que los dos conviven, y elegir por fecha dejaría a
    quien acaba de mandar un lote mirando el resumen del anterior.
    """
    first = Url.objects.create(domain=domain, loc='https://ejemplo.test/a')
    done = create_batch(domain, [first])
    done.state = BatchState.QUEUED
    done.save(update_fields=['state'])
    send_batch(done, client=FakeIndexingClient())

    second = Url.objects.create(domain=domain, loc='https://ejemplo.test/b')
    running = create_batch(domain, [second])
    running.state = BatchState.RUNNING
    running.save(update_fields=['state'])

    props = tab(client, domain)

    assert props['batch']['id'] == str(running.id)
    assert props['batch']['is_running'] is True


def test_the_tab_says_why_it_stopped_so_the_screen_can_explain_it(client, domain):
    """
    El motivo va como código, y la pestaña lo traduce.

    Es lo que permite ofrecer «retomar» sabiendo si tiene sentido —el cupo se
    repone mañana, un permiso no— sin que la pantalla lea una frase.
    """
    from apps.gsc.errors import GoogleErrorCode

    rows = [
        Url.objects.create(domain=domain, loc=f'https://ejemplo.test/{n}') for n in range(1, 4)
    ]
    batch = create_batch(domain, rows)
    batch.state = BatchState.QUEUED
    batch.save(update_fields=['state'])
    send_batch(batch, client=FakeIndexingClient(default=(GoogleErrorCode.QUOTA_EXCEEDED, 429)))

    props = tab(client, domain)

    assert props['batch']['stopped_by'] == GoogleErrorCode.QUOTA_EXCEEDED
    assert props['progress']['pending'] == 2
    # Y las direcciones, para poder ver cuál quedó contaminada y cuáles esperan.
    assert len(props['requests']) == 3


def test_a_batch_of_another_kind_is_not_an_indexing_batch(client, domain):
    """
    Una ficha de indexación sobre un lote de inspección contestaría con datos que
    no existen. Se contesta 404, que es lo que ese identificador es acá.
    """
    from apps.jobs.models import Batch, BatchKind, BatchOrigin

    other = Batch.objects.create(
        domain=domain, kind=BatchKind.URL_INSPECTION, origin=BatchOrigin.MANUAL
    )

    assert client.get(f'/search-console/indexing/{other.id}').status_code == 404
