"""
La tabla de cobertura, desde que ordena y filtra en el navegador.

Lo que se fija acá es lo que ese cambio puso en juego: que el recorte del
servidor **se diga** en vez de pasar por completo, y que las tres familias que
el filtro ofrece partan el total sin superponerse. Las dos cosas fallan en
silencio —una tabla cortada se ve igual que una tabla entera, y dos familias que
se pisan devuelven filas de más sin avisar—, así que ningún otro test las
protege.
"""

import pytest
from django.utils import timezone

from apps.coverage import views as coverage_views
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 account():
    account = create_account()
    create_credential(account)
    return account


@pytest.fixture
def domain(account):
    """Un dominio con una URL de cada familia: indexada, no indexada y sin dato."""
    domain = create_domain(account)

    indexed = Url.objects.create(domain=domain, loc='https://ejemplo.test/uno', in_sitemap=True)
    indexed.coverage_state = CoverageState.INDEXED
    indexed.last_checked_at = timezone.now()
    indexed.save()

    crawled = Url.objects.create(domain=domain, loc='https://ejemplo.test/dos', in_sitemap=True)
    crawled.coverage_state = CoverageState.CRAWLED_NOT_INDEXED
    crawled.last_checked_at = timezone.now()
    crawled.save()

    # Sin tocar: queda en UNKNOWN, que es «todavía no preguntamos».
    Url.objects.create(domain=domain, loc='https://ejemplo.test/tres', in_sitemap=True)

    return domain


def props_of(client, domain) -> dict:
    return client.get(
        f'/search-console/domains/{domain.id}/coverage', HTTP_X_INERTIA='true'
    ).json()['props']


def test_the_rows_travel_whole_with_their_real_total(client, account, domain):
    """Sin tope alcanzado, lo que llega es todo y la pantalla no anuncia recorte."""
    client.force_login(account)

    urls = props_of(client, domain)['urls']

    assert len(urls['rows']) == 3
    assert urls['total'] == 3
    assert urls['truncated'] is False


def test_a_trimmed_list_says_so_and_keeps_the_real_total(client, account, domain, monkeypatch):
    """
    El recorte nunca es silencioso.

    Es la única protección contra leer «3 URLs» y concluir que el sitio tiene
    tres. `total` sigue diciendo cuántas hay de verdad, que es lo que la
    pantalla necesita para escribir «las primeras 2 de 3».
    """
    monkeypatch.setattr(coverage_views, 'TABLE_LIMIT', 2)
    client.force_login(account)

    urls = props_of(client, domain)['urls']

    assert len(urls['rows']) == 2
    assert urls['total'] == 3
    assert urls['truncated'] is True
    assert urls['limit'] == 2


def test_the_trimmed_rows_are_always_the_same_ones(client, account, domain, monkeypatch):
    """
    El recorte sale de un orden declarado y no del que devuelva la base.

    Sin `order_by`, dos visitas seguidas pueden traer conjuntos distintos y la
    URL que alguien vio recién desaparece al recargar.
    """
    monkeypatch.setattr(coverage_views, 'TABLE_LIMIT', 2)
    client.force_login(account)

    first = [row['loc'] for row in props_of(client, domain)['urls']['rows']]
    second = [row['loc'] for row in props_of(client, domain)['urls']['rows']]

    assert first == second
    assert first == ['https://ejemplo.test/dos', 'https://ejemplo.test/tres']


def test_every_row_carries_exactly_one_family(client, account, domain):
    """Cada URL cae en una familia y en una sola: es lo que el filtro necesita."""
    client.force_login(account)

    rows = props_of(client, domain)['urls']['rows']
    families = {row['loc']: row['family'] for row in rows}

    assert families == {
        'https://ejemplo.test/uno': 'indexed',
        'https://ejemplo.test/dos': 'not_indexed',
        'https://ejemplo.test/tres': 'without_data',
    }


def test_the_three_families_add_up_to_the_total(client, account, domain):
    """
    Las tres familias parten el total, sin solaparse y sin dejar nada afuera.

    Los `groups` del servidor **sí** se solapan —«con dato» contiene a «no
    indexadas»— y por eso no son lo que filtra la tabla. Si algún día alguien
    los cambia por aquéllos, este test es el que lo frena: la suma daría de más.
    """
    client.force_login(account)

    options = props_of(client, domain)['options']
    counts = {family['value']: family['count'] for family in options['families']}

    assert counts == {'indexed': 1, 'not_indexed': 1, 'without_data': 1}
    assert sum(counts.values()) == options['total']


def test_the_unchecked_ones_never_count_as_not_indexed(client, account, domain):
    """
    R-B, dicho sobre las familias.

    «No indexadas» son las que Google contestó que no. Las que todavía no
    preguntamos son otra cosa, y meterlas adentro convierte «no sabemos» en
    «Google dijo que no».
    """
    client.force_login(account)

    rows = props_of(client, domain)['urls']['rows']
    unchecked = next(row for row in rows if row['state'] == CoverageState.UNKNOWN)

    assert unchecked['family'] == 'without_data'
    assert unchecked['family'] != 'not_indexed'


class TestInspectingOnlyWhatWasPicked:
    """
    Volver a preguntar por cuatro direcciones no puede costar doscientas
    cuarenta y dos llamadas.

    La pantalla sólo sabía consultar el sitio entero, así que mirar si las que
    quedaron sin indexar cambiaron gastaba el cupo del día en las que ya estaban
    bien. El resultado práctico era no mirarlas nunca.
    """

    def endpoint(self, domain) -> str:
        return f'/search-console/domains/{domain.id}/coverage/inspect'

    def test_only_the_picked_ones_are_queried(self, client, account, domain, monkeypatch):
        client.force_login(account)
        picked = list(Url.objects.filter(domain=domain).order_by('loc')[:2])

        passed = {}
        monkeypatch.setattr(
            'apps.coverage.tasks.inspect_one_domain.delay',
            lambda *args, **kwargs: passed.update(kwargs),
        )

        # JSON y no formulario: es como manda Inertia cuando no hay archivos, y
        # un test que postee un formulario ejercita un camino que el navegador
        # nunca toma.
        response = client.post(
            self.endpoint(domain),
            data={'urls': [str(url.id) for url in picked]},
            content_type='application/json',
        )

        assert response.status_code == 302
        assert passed['url_ids'] == [str(url.id) for url in picked]

    def test_without_a_selection_the_whole_site_is_still_queried(
        self, client, account, domain, monkeypatch
    ):
        """
        `None` y lista vacía no significan lo mismo: sin selección el lote
        recorre lo pendiente, como siempre. Mandar `[]` acá convertiría «Query
        now» en un lote que no consulta nada.
        """
        client.force_login(account)

        passed = {}
        monkeypatch.setattr(
            'apps.coverage.tasks.inspect_one_domain.delay',
            lambda *args, **kwargs: passed.update(kwargs),
        )

        client.post(self.endpoint(domain))

        assert passed['url_ids'] is None

    def test_a_url_from_another_domain_is_dropped(self, account, domain):
        """
        Los identificadores llegan del navegador. Sin filtrar por dominio, un
        lote de este sitio podría consultar —y mostrar— direcciones de otro.
        """
        from apps.coverage.tasks import _selected_urls

        mine = Url.objects.filter(domain=domain).order_by('loc').first()
        other = create_domain(account, hostname='ajeno.test')
        theirs = Url.objects.create(domain=other, loc='https://ajeno.test/x', in_sitemap=True)

        resolved = _selected_urls(domain, [str(mine.id), str(theirs.id)])

        assert [url.id for url in resolved] == [mine.id]
