"""
Los avisos de nivel cuenta: adónde lleva cada uno y dónde se muestran.

`account.notices` es **una sola lista** con todo lo que la cuenta tiene
pendiente, de lo más grave a lo menos, y la barra superior la rinde entera bajo
un indicador que sólo existe cuando hay algo (D7). Que la lista esté completa
importa más que antes: ya no hay un bloque por familia repartido entre pantallas
que la complete por su cuenta.

Cada aviso viaja con su acción, y la acción tiene que ser una dirección relativa:
es lo que `<Link>` de Inertia sabe seguir sin recargar la página.

La otra mitad —que el indicador esté en el armazón y no en cada vista— se
comprueba leyendo los archivos. No demuestra que esté bien dibujado, pero sí que
no se movió de lugar, y moverse de lugar es exactamente como el aviso anterior
terminó repetido en tres pantallas.
"""

import re
from pathlib import Path

import pytest
from django.conf import settings
from django.utils import timezone

from apps.accounts.services import Notice, account_state
from apps.credentials.models import CredentialStatus
from apps.domains.models import AccessState
from apps.jobs.models import Batch, BatchKind, BatchOrigin, BatchState
from tests.factories import create_account, create_credential, create_domain

FRONTEND = Path(settings.BASE_DIR) / 'frontend'
ACCOUNT_STATUS_TSX = FRONTEND / 'components' / 'AccountStatus.tsx'
SITE_HEADER_TSX = FRONTEND / 'components' / 'site-header.tsx'
APP_LAYOUT_TSX = FRONTEND / 'layouts' / 'AppLayout.tsx'


def _notice(account, code: str) -> Notice:
    found = [notice for notice in account_state(account).notices if notice.code == code]
    assert found, f'La cuenta no emitió el aviso {code}.'
    return found[0]


@pytest.mark.django_db
@pytest.mark.parametrize(
    ('status', 'code'),
    [
        (None, 'CREDENTIAL_MISSING'),
        (CredentialStatus.UNVERIFIED, 'CREDENTIAL_UNVERIFIED'),
        (CredentialStatus.REVOKED, 'CREDENTIAL_REVOKED'),
        (CredentialStatus.INVALID, 'CREDENTIAL_INVALID'),
    ],
)
def test_every_credential_notice_points_at_the_connection_screen(status, code):
    """Las cuatro variantes del mismo problema mandan a la misma pantalla."""
    account = create_account(email=f'{code.lower()}@ejemplo.test')
    if status is not None:
        create_credential(account, status=status)

    assert _notice(account, code).action_path == '/search-console/settings'


@pytest.mark.django_db
def test_the_lost_access_notice_points_where_it_is_fixed():
    """
    A la conexión, que es donde el sitio muestra su acceso y ofrece comprobarlo.

    Antes llevaba al listado con el filtro puesto, que era el mejor destino
    cuando la cuenta podía tener varios dominios: recortaba la lista a los que
    estaban en ese estado. Con un solo sitio ese recorte no existe, y un aviso
    tiene que llevar a la pantalla donde el problema **se resuelve**, no a una
    donde se lo vuelve a leer (RT-18).
    """
    account = create_account()
    create_credential(account, status=CredentialStatus.VERIFIED)
    create_domain(account, access_state=AccessState.ACCESS_LOST)

    assert _notice(account, 'DOMAINS_ACCESS_LOST').action_path == '/search-console/settings'


@pytest.mark.django_db
def test_a_domain_still_awaiting_access_is_a_warning_and_not_an_error():
    """
    Esperar autorización no es lo mismo que haberla perdido.

    Los dos dejan al dominio sin datos nuevos, pero uno es el estado normal de un
    alta recién hecha y el otro es algo que se rompió. Con el mismo nivel, la
    lista abriría en rojo el día que alguien agrega su primer dominio.
    """
    account = create_account()
    create_credential(account, status=CredentialStatus.VERIFIED)
    create_domain(account, access_state=AccessState.AWAITING_ACCESS)

    notice = _notice(account, 'DOMAINS_AWAITING_ACCESS')

    assert notice.level == 'warning'
    # Mismo destino que el de acceso perdido: en la conexión está la dirección de
    # la cuenta de servicio que hay que autorizar, que es el paso que falta.
    assert notice.action_path == '/search-console/settings'


@pytest.mark.django_db
def test_a_batch_that_failed_long_ago_is_history_and_not_a_pending_item():
    """
    La ventana es lo que impide que la lista se vuelva un archivo histórico.

    Un lote que falló hace tres semanas por una credencial que ya se arregló no
    es trabajo pendiente, y dejarlo ahí entrena a ignorar la lista entera.
    """
    account = create_account()
    create_credential(account, status=CredentialStatus.VERIFIED)
    domain = create_domain(account)
    Batch.objects.create(
        domain=domain,
        kind=BatchKind.URL_INSPECTION,
        origin=BatchOrigin.SCHEDULED,
        state=BatchState.FAILED,
        finished_at=timezone.now() - timezone.timedelta(days=21),
    )

    codes = [notice.code for notice in account_state(account).notices]

    assert 'BATCHES_FAILED' not in codes


@pytest.mark.django_db
def test_a_recent_failed_batch_leads_to_the_batches_of_its_own_domain():
    """
    La lista de lotes es **de un dominio**, así que con uno solo golpeado se
    puede llevar ahí. Con varios no hay una pantalla que los muestre juntos y el
    aviso lleva a la lista de dominios en vez de inventar un destino.
    """
    account = create_account()
    create_credential(account, status=CredentialStatus.VERIFIED)
    domain = create_domain(account)
    Batch.objects.create(
        domain=domain,
        kind=BatchKind.URL_INSPECTION,
        origin=BatchOrigin.SCHEDULED,
        state=BatchState.FAILED,
        finished_at=timezone.now(),
    )

    notice = _notice(account, 'BATCHES_FAILED')

    assert notice.level == 'warning'
    assert notice.action_path == f'/search-console/domains/{domain.id}/batches?state=failed'


@pytest.mark.django_db
def test_the_gravest_notice_is_first_in_the_list():
    """
    El orden es el de la lectura: la lista se abre entera y de una sola vez.

    Lo que impide operar va antes que lo que puede esperar. Sin eso, una cuenta
    con la conexión caída y tres dominios sin autorizar abriría el panel con lo
    menos urgente arriba.
    """
    account = create_account(email='mezcla@ejemplo.test')
    create_credential(account, status=CredentialStatus.INVALID)
    create_domain(account, hostname='esperando.test', access_state=AccessState.AWAITING_ACCESS)

    levels = [notice.level for notice in account_state(account).notices]

    assert levels == ['error', 'warning']


@pytest.mark.django_db
def test_every_notice_travels_as_a_relative_path():
    """
    Una dirección absoluta la seguiría el navegador con recarga completa, y una
    sin barra inicial se resolvería contra la pantalla en la que estás.
    """
    account = create_account(email='rota@ejemplo.test')
    create_credential(account, status=CredentialStatus.INVALID)
    create_domain(account, access_state=AccessState.ACCESS_LOST)

    for notice in account_state(account).notices:
        assert notice.action_path.startswith('/'), (
            f'El aviso {notice.code} apunta a «{notice.action_path}», que no es una '
            f'dirección relativa: `<Link>` de Inertia no la va a poder seguir.'
        )


def test_the_indicator_disappears_when_there_is_nothing_pending():
    """
    **La ausencia del indicador es la señal de salud** (D7).

    Es la mitad que hace que el indicador sirva: si dibujara algo con la cuenta
    sana —un tilde verde, un contador en cero— volvería a ser un adorno fijo que
    se aprende a ignorar, que es de lo que veníamos.
    """
    source = ACCOUNT_STATUS_TSX.read_text(encoding='utf-8')

    assert re.search(r'if\s*\(\s*notices\.length\s*===\s*0\s*\)\s*return\s+null', source), (
        'Con la lista vacía el indicador tiene que devolver `null`. Cualquier cosa '
        'dibujada en el estado sano rompe la regla que lo hace legible.'
    )


def test_the_indicator_lives_in_the_shell_and_not_in_the_views():
    """
    Que esté en el armazón es lo que impide que una vista se olvide de mostrarlo,
    y que **no** esté además en el contenido es lo que impide que vuelva a ser el
    bloque de 120 px que este cambio sacó.
    """
    assert 'AccountStatus' in SITE_HEADER_TSX.read_text(encoding='utf-8'), (
        'El estado de la cuenta lo rinde la barra superior. Fuera de ahí, cada vista '
        'tendría que acordarse de pedirlo.'
    )
    assert 'AccountNotices' not in APP_LAYOUT_TSX.read_text(encoding='utf-8'), (
        'El aviso volvió a rendirse como bloque dentro del contenido. Es el bloque '
        'que D7 reemplazó por el indicador de la barra.'
    )
