"""
El alta de un sitemap comprueba antes de guardar (T094).

Es la diferencia entre enterarse al pegar la dirección y enterarse al día
siguiente. Hasta ahora el alta guardaba cualquier cosa —el servicio ya sabía
distinguir un archivo ilegible de uno mal formado, pero recién al sincronizar— y
el error aparecía en el resumen del lote de la madrugada, lejos del formulario
donde se había escrito.

Con las URLs ajenas el trato es distinto a propósito. Que un sitemap declare
algunas direcciones de otro sitio es común y no lo invalida: la sincronización
las descarta con su aviso. Sólo se rechaza cuando **ninguna** pertenece al
dominio, que es el caso en que se pegó el sitemap de otro sitio.
"""

from pathlib import Path

import httpx
import pytest
from django.conf import settings
from rest_framework.test import APIClient

from apps.accounts.models import ApiKey
from apps.sitemaps.models import Sitemap, SitemapError
from apps.sitemaps.services import check_sitemap
from tests.factories import create_account, create_credential, create_domain

pytestmark = pytest.mark.django_db

FIXTURES = Path(settings.BASE_DIR) / 'tests' / 'fixtures' / 'sitemaps'

LOCATION = 'https://ejemplo.test/sitemap.xml'


def fake_site(**routes) -> httpx.Client:
    """El sitio del usuario. Una dirección que no está devuelve 404."""

    def respond(request: httpx.Request) -> httpx.Response:
        body = routes.get(str(request.url))
        if body is None:
            return httpx.Response(404, text='no encontrado')
        return httpx.Response(200, content=body)

    return httpx.Client(transport=httpx.MockTransport(respond))


def read_file(name: str) -> bytes:
    return (FIXTURES / name).read_bytes()


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


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


@pytest.fixture
def api(account):
    client = APIClient()
    _, plain_text = ApiKey.issue(account=account, name='pipeline')
    client.credentials(HTTP_AUTHORIZATION=f'Api-Key {plain_text}')
    return client


def error_of(check) -> tuple[str, str]:
    """El motivo y la clase de problema de un alta rechazada."""
    from apps.core.errors import ApiError

    with pytest.raises(ApiError) as raised:
        check()
    return raised.value.message, raised.value.details.get('reason', '')


def test_a_sitemap_that_can_be_read_is_accepted(domain):
    check_sitemap(domain, LOCATION, http=fake_site(**{LOCATION: read_file('urlset.xml')}))


def test_an_unreachable_address_is_rejected_with_its_reason(domain):
    message, reason = error_of(lambda: check_sitemap(domain, LOCATION, http=fake_site()))

    assert reason == SitemapError.UNREACHABLE
    assert '404' in message


def test_a_file_that_is_not_a_sitemap_is_rejected_as_malformed(domain):
    site = fake_site(**{LOCATION: read_file('pagina_de_error.html')})

    _message, reason = error_of(lambda: check_sitemap(domain, LOCATION, http=site))

    assert reason == SitemapError.MALFORMED


def test_a_sitemap_of_another_site_is_rejected(domain):
    """Ninguna de sus URLs pertenece al dominio: es el sitemap de otro sitio."""
    site = fake_site(
        **{
            LOCATION: b'<?xml version="1.0"?><urlset><url><loc>https://otrositio.test/uno'
            b'</loc></url></urlset>'
        }
    )

    message, reason = error_of(lambda: check_sitemap(domain, LOCATION, http=site))

    assert reason == SitemapError.FOREIGN_URLS
    assert domain.property_uri in message


def test_some_foreign_urls_do_not_reject_the_file(domain):
    """
    Es común y no lo invalida: la sincronización las descarta con su aviso.

    Rechazar acá dejaría afuera sitemaps legítimos por un puñado de enlaces a
    otro sitio, y el usuario no tendría forma de registrar el suyo.
    """
    site = fake_site(
        **{
            LOCATION: b'<?xml version="1.0"?><urlset>'
            b'<url><loc>https://ejemplo.test/uno</loc></url>'
            b'<url><loc>https://otrositio.test/dos</loc></url>'
            b'</urlset>'
        }
    )

    check_sitemap(domain, LOCATION, http=site)


def test_an_index_is_not_judged_by_the_urls_it_declares(domain):
    """Lo que declara son otros sitemaps, y de cada uno se ocupa la sincronización."""
    site = fake_site(**{LOCATION: read_file('indice.xml')})

    check_sitemap(domain, LOCATION, http=site)


# --- La API -----------------------------------------------------------------


def test_the_api_answers_422_and_does_not_save_anything(api, domain, monkeypatch):
    monkeypatch.setattr('apps.sitemaps.services.read', _reader())

    response = api.post(
        f'/api/v1/domains/{domain.id}/sitemaps', {'location': LOCATION}, format='json'
    )

    assert response.status_code == 422
    assert response.json()['error']['details']['reason'] == SitemapError.UNREACHABLE
    # Nada guardado: una fila que quedara pese al rechazo aparecería en la tabla
    # como un sitemap más, y el próximo ciclo volvería a intentar leerla.
    assert not Sitemap.objects.filter(domain=domain).exists()


def test_the_api_saves_what_it_could_read(api, domain, monkeypatch):
    monkeypatch.setattr('apps.sitemaps.services.read', _reader(read_file('urlset.xml')))

    response = api.post(
        f'/api/v1/domains/{domain.id}/sitemaps', {'location': LOCATION}, format='json'
    )

    assert response.status_code == 201
    assert Sitemap.objects.get(domain=domain).location == LOCATION


# --- La pantalla ------------------------------------------------------------


def test_the_screen_shows_the_reason_next_to_the_field(client, account, domain, monkeypatch):
    monkeypatch.setattr('apps.sitemaps.services.read', _reader())
    client.force_login(account)

    client.post(f'/search-console/domains/{domain.id}/sitemaps/new', {'location': LOCATION})
    props = client.get(
        f'/search-console/domains/{domain.id}/sitemaps', HTTP_X_INERTIA='true'
    ).json()['props']

    assert props['errors']['location']['code'] == 'UNREACHABLE'
    assert not Sitemap.objects.filter(domain=domain).exists()


def _reader(body: bytes | None = None):
    """
    Sustituye la descarga del alta por una respuesta grabada.

    El sitio simulado se inyecta en el servicio y no en la petición: quien da de
    alta desde la API o desde la pantalla no tiene por dónde pasar un cliente
    HTTP, que es justamente lo que hace falta para probar el camino entero.
    """
    from apps.sitemaps.reader import read

    site = fake_site(**({LOCATION: body} if body is not None else {}))

    def fake_read(location, *, client=None, timeout=None):
        return read(location, client=site)

    return fake_read
