"""
Los lotes de un dominio (V9) y la ficha de uno (V10).

Hasta ahora estas dos pantallas no tenían dirección publicada, así que nadie las
había abierto nunca: el trabajo estaba escrito y era inalcanzable. Lo que estos
tests fijan es lo que hace que sirvan para la pregunta que las trae, que no es
«¿qué corrió?» sino **«¿qué quedó sin hacer?»**.

De ahí sale casi todo lo que se verifica acá: que un lote cortado por cuota
llegue como `PARTIAL` con su motivo y no redondeado a terminado (R-C), que las
cifras viajen crudas para que la barra no pueda contradecir a la frase, y que el
sondeo se decida con los lotes del dominio entero y no con los de la página que
se está mirando.
"""

import pytest
from django.utils import timezone

from apps.core.dates import display_timezone
from apps.gsc.errors import MESSAGES, GoogleErrorCode
from apps.jobs.models import Batch, BatchKind, BatchOrigin, BatchState, QuotaBudget
from apps.jobs.views import BatchReason
from tests.factories import create_account, create_domain

pytestmark = pytest.mark.django_db


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


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


def create_batch(domain, **extra) -> Batch:
    data = {
        'domain': domain,
        'kind': BatchKind.URL_INSPECTION,
        'origin': BatchOrigin.SCHEDULED,
        'state': BatchState.COMPLETED,
        'total_items': 2000,
        'processed_items': 2000,
        'started_at': timezone.now(),
        'finished_at': timezone.now(),
    }
    return Batch.objects.create(**(data | extra))


def batch_list(client, domain) -> dict:
    response = client.get(f'/search-console/domains/{domain.id}/batches', HTTP_X_INERTIA='true')
    assert response.status_code == 200
    return response.json()['props']


def batch_detail(client, batch) -> dict:
    response = client.get(f'/search-console/batches/{batch.id}', HTTP_X_INERTIA='true')
    assert response.status_code == 200
    return response.json()['props']


class TestBothScreensExist:
    def test_the_list_is_its_own_page(self, client, domain):
        response = client.get(f'/search-console/domains/{domain.id}/batches', HTTP_X_INERTIA='true')

        assert response.json()['component'] == 'Batches/Index'

    def test_the_detail_is_its_own_page(self, client, domain):
        batch = create_batch(domain)

        response = client.get(f'/search-console/batches/{batch.id}', HTTP_X_INERTIA='true')

        assert response.json()['component'] == 'Batches/Show'

    def test_without_a_session_both_send_to_the_login(self, client, domain):
        batch = create_batch(domain)
        client.logout()

        assert (
            client.get(f'/search-console/domains/{domain.id}/batches')
            .headers['Location']
            .startswith('/login')
        )
        assert (
            client.get(f'/search-console/batches/{batch.id}')
            .headers['Location']
            .startswith('/login')
        )

    def test_the_batch_created_by_another_profile_is_shared(self, client, account):
        foreign = create_account(email='ajena@ejemplo.test')
        batch = create_batch(create_domain(foreign, hostname='ajeno.test'))

        assert client.get(f'/search-console/batches/{batch.id}').status_code == 200


class TestWhatWasLeftUndone:
    def test_a_batch_cut_by_quota_ends_partial_with_its_reason(self, client, domain):
        """
        El caso que la restricción R-C protege.

        Se pidió cupo para dos mil y se concedió para cuatrocientas: el corte lo
        pusimos nosotros y las mil quinientas ochenta y ocho restantes siguen en
        la cola. Redondearlo a terminado convertiría un resultado parcial en una
        afirmación falsa sobre la cobertura del sitio.
        """
        batch = create_batch(
            domain,
            state=BatchState.PARTIAL,
            total_items=2000,
            processed_items=412,
            summary={'quota_requested': 2000, 'quota_granted': 412},
        )

        data = batch_detail(client, batch)['batch']

        assert data['state'] == BatchState.PARTIAL
        assert data['reason'] == BatchReason.QUOTA_EXHAUSTED
        assert data['pending_items'] == 1588

    def test_the_figures_travel_raw_and_not_as_a_percentage(self, client, domain):
        """
        La barra se dibuja de las cifras y la frase se escribe de las mismas.

        Con un porcentaje ya resuelto en el servidor, el hueco del final de la
        barra y el número de la frase podrían contradecirse, y un lote parcial
        aparecería lleno.
        """
        batch = create_batch(
            domain, state=BatchState.PARTIAL, total_items=2000, processed_items=412
        )

        data = batch_detail(client, batch)['batch']

        assert (data['total_items'], data['processed_items']) == (2000, 412)
        assert 'progress' not in data and 'percent' not in data

    def test_the_google_limit_is_distinguished_from_the_own_quota(self, client, domain):
        """
        Son dos motivos con soluciones distintas y no pueden llegar iguales.

        El cupo propio se resuelve subiendo el presupuesto del dominio; el límite
        de Google, esperando. Presentarlos con la misma frase mandaría a tocar
        una perilla que no cambia nada.
        """
        batch = create_batch(
            domain,
            state=BatchState.PARTIAL,
            processed_items=800,
            summary={'errors': [MESSAGES[GoogleErrorCode.QUOTA_EXCEEDED]]},
        )

        assert batch_detail(client, batch)['batch']['reason'] == BatchReason.PROVIDER_LIMIT

    def test_a_fully_finished_batch_does_not_invent_a_reason(self, client, domain):
        assert batch_detail(client, create_batch(domain))['batch']['reason'] is None

    def test_a_note_is_not_read_as_a_failed_item(self, client, domain):
        """
        El motivo que hacía escribir «Fallaron 0 de 7 sitemaps».

        `summary['errors']` guarda en la misma lista dos cosas distintas: los
        intentos que fallaron, que incrementan `failed_items`, y las notas que
        explican hasta dónde llegó el lote —«no quedaba cupo para enviar este
        sitemap»—, que no lo incrementan porque no falló nada. Deducir «fallaron
        ítems» de que la lista no esté vacía convierte una nota en un fracaso, y
        la pantalla termina contando cero fallas con la frase de las fallas.
        """
        batch = create_batch(
            domain,
            kind=BatchKind.SITEMAP_SYNC,
            state=BatchState.PARTIAL,
            total_items=7,
            processed_items=7,
            failed_items=0,
            summary={
                'errors': [
                    'No quedaba cupo para enviar «https://ejemplo.test/sitemap.xml». '
                    'Se retoma en el próximo ciclo.'
                ]
            },
        )

        assert batch_detail(client, batch)['batch']['reason'] == BatchReason.INTERRUPTED

    def test_with_items_that_did_fail_the_reason_is_still_the_item_errors_one(self, client, domain):
        """La contracara: el arreglo no puede tapar los fracasos que sí hubo."""
        batch = create_batch(
            domain,
            kind=BatchKind.SITEMAP_SYNC,
            state=BatchState.PARTIAL,
            total_items=7,
            processed_items=5,
            failed_items=2,
            summary={'errors': ['https://ejemplo.test/sitemap-1.xml: Devolvió 404']},
        )

        assert batch_detail(client, batch)['batch']['reason'] == BatchReason.ITEM_ERRORS


class TestTheList:
    def test_the_most_recent_one_goes_on_top(self, client, domain):
        """Lo que trae a esta pantalla es si lo último que corrió terminó bien."""
        create_batch(domain, kind=BatchKind.SITEMAP_SYNC)
        recent = create_batch(domain, started_at=timezone.now() + timezone.timedelta(hours=1))

        rows = batch_list(client, domain)['batches']

        assert rows[0]['id'] == str(recent.id)

    def test_the_batches_of_another_domain_are_not_mixed_in(self, client, account, domain):
        other = create_domain(account, hostname='otro.test')
        create_batch(other)
        own = create_batch(domain)

        rows = batch_list(client, domain)['batches']

        assert [row['id'] for row in rows] == [str(own.id)]

    def test_the_polling_looks_at_the_whole_domain_and_not_the_page(self, client, domain):
        """
        Con el lote en curso caído en la página dos, la pantalla se quedaría
        quieta justo cuando alguien la mira esperando que se mueva (RT-13).
        """
        for _ in range(60):
            create_batch(domain)
        create_batch(domain, state=BatchState.RUNNING, processed_items=0, finished_at=None)

        data = batch_list(client, domain)

        assert len(data['batches']) == 61
        assert BatchState.RUNNING in data['active_states']

    def test_without_anything_running_there_is_no_reason_to_keep_polling(self, client, domain):
        create_batch(domain)

        assert batch_list(client, domain)['active_states'] == []

    def test_rows_carry_the_raw_values_used_by_client_filters(self, client, domain):
        batch = create_batch(
            domain,
            kind=BatchKind.SITEMAP_SYNC,
            origin=BatchOrigin.MANUAL,
            state=BatchState.PARTIAL,
            processed_items=10,
        )

        row = next(item for item in batch_list(client, domain)['batches'] if item['id'] == str(batch.id))

        assert row['state'] == BatchState.PARTIAL
        assert row['kind'] == BatchKind.SITEMAP_SYNC
        assert row['origin'] == BatchOrigin.MANUAL

    def test_filter_options_use_the_same_raw_values_as_rows(self, client, domain):
        options = batch_list(client, domain)['options']

        assert set(options['state']) == set(BatchState.values)
        assert set(options['kind']) == set(BatchKind.values)
        assert set(options['origin']) == set(BatchOrigin.values)


class TestTheDetail:
    def test_the_failures_arrive_split_into_item_and_reason(self, client, domain):
        """
        Una tabla con encabezados y no un párrafo con comas.

        El motivo de una URL concreta es lo que permite arreglarla, y mezclado en
        prosa no se lee ni se copia.
        """
        batch = create_batch(
            domain,
            state=BatchState.PARTIAL,
            failed_items=1,
            summary={
                'errors': [
                    'https://ejemplo.test/producto/42: Google no pudo obtener la página',
                    'no quedaba cupo diario',
                ]
            },
        )

        failures = batch_detail(client, batch)['failures']

        assert failures[0] == {
            'item': 'https://ejemplo.test/producto/42',
            'reason': 'Google no pudo obtener la página',
        }
        assert failures[1] == {'item': None, 'reason': 'no quedaba cupo diario'}

    def test_the_quota_that_is_shown_is_the_one_of_the_day_of_the_batch(self, client, domain):
        """
        No se crea la fila que falte.

        Copiar los límites de hoy explicaría un lote de hace un mes con un cupo
        que ese día no existió, que es justo el historial falso que la tabla de
        presupuestos evita guardando los límites vigentes al crearla.
        """
        # El día se corta en la zona de presentación y no en la del servidor: son
        # zonas distintas y, en la franja horaria en que no coinciden, «el cupo de
        # ese día» sería el de otro día. Es la misma que usa el presupuesto para
        # cobrar, así que es la única con la que la fila se encuentra.
        today = timezone.localtime(timezone=display_timezone()).date()
        QuotaBudget.objects.create(
            domain=domain, date=today, limit_total=2000, manual_reserve=200, used_automatic=412
        )
        batch = create_batch(domain, quota_consumed=412)

        quota = batch_detail(client, batch)['quota']

        assert quota['date'] == today.isoformat()
        assert quota['limit_total'] == 2000

    def test_without_a_budget_for_that_day_none_is_invented(self, client, domain):
        assert batch_detail(client, create_batch(domain))['quota'] is None

    def test_each_state_brings_the_addresses_this_batch_left_in_it(self, client, domain):
        """
        La cifra y la lista tienen que ser del mismo hecho.

        Antes la cifra salía del historial del lote y el enlace de al lado
        llevaba a la tabla de cobertura filtrada por ese estado, que muestra el
        **último** estado de cada dirección. Con un recorrido posterior de por
        medio son dos conjuntos distintos, y quien hacía clic aterrizaba en una
        lista que no coincidía con el número que acababa de leer.
        """
        from apps.coverage.models import CoverageRecord
        from apps.sitemaps.models import CoverageState, Url

        batch = create_batch(domain)
        url = Url.objects.create(domain=domain, loc='https://ejemplo.test/a', in_sitemap=True)
        CoverageRecord.objects.create(
            url=url,
            batch=batch,
            state=CoverageState.CRAWLED_NOT_INDEXED,
            fetched_at=timezone.now(),
        )

        # Un recorrido posterior la devuelve a indexada: la tabla de cobertura ya
        # no la muestra en el estado que este lote registró.
        url.coverage_state = CoverageState.INDEXED
        url.save(update_fields=['coverage_state'])

        by_state = batch_detail(client, batch)['inspection']['by_state']

        assert by_state == [
            {
                'state': CoverageState.CRAWLED_NOT_INDEXED,
                'total': 1,
                'urls': ['https://ejemplo.test/a'],
            }
        ]

    def test_a_running_batch_brings_the_counts_without_the_addresses(self, client, domain):
        """
        Las listas viajan sólo cuando el lote cerró.

        La pantalla se repide cada cinco segundos mientras corre, y mandar cada
        dirección en cada tic es tráfico por una lista que todavía va a cambiar.
        Es el mismo criterio con el que ya se calculan las transiciones.
        """
        from apps.coverage.models import CoverageRecord
        from apps.sitemaps.models import CoverageState, Url

        batch = create_batch(domain, state=BatchState.RUNNING, finished_at=None)
        url = Url.objects.create(domain=domain, loc='https://ejemplo.test/b', in_sitemap=True)
        CoverageRecord.objects.create(
            url=url, batch=batch, state=CoverageState.INDEXED, fetched_at=timezone.now()
        )

        by_state = batch_detail(client, batch)['inspection']['by_state']

        assert by_state == [{'state': CoverageState.INDEXED, 'total': 1, 'urls': []}]

    def test_a_synchronization_does_not_bring_an_inspection_block(self, client, domain):
        """Cada tipo de lote muestra su resumen, y el que no corresponde no ocupa lugar (RT-07)."""
        batch = create_batch(domain, kind=BatchKind.SITEMAP_SYNC)

        assert batch_detail(client, batch)['inspection'] is None

    def test_the_neighbours_allow_walking_through_the_history(self, client, domain):
        previous = create_batch(domain)
        middle = create_batch(domain)
        following = create_batch(domain)

        neighbours = batch_detail(client, middle)['neighbours']

        assert neighbours['previous']['id'] == str(previous.id)
        assert neighbours['next']['id'] == str(following.id)

    def test_the_first_batch_has_no_previous_one(self, client, domain):
        assert batch_detail(client, create_batch(domain))['neighbours']['previous'] is None
