"""
Qué pasa cuando alguien entra sin sesión.

Este archivo existe por un fallo concreto: toda la verificación anterior se hizo
con la sesión ya iniciada, así que el camino del visitante anónimo —que es el
primero que recorre cualquiera— nunca se ejercitó. Django mandaba a
`/accounts/login/`, su valor de fábrica, y ahí no hay nada: 404 en la puerta de
entrada.

Por eso el test recorre **todas** las rutas protegidas y no una de muestra. Una
ruta nueva que se agregue mañana entra sola en la lista, y si redirige a otro
lado la suite lo dice.
"""

import uuid

import pytest
from django.conf import settings
from django.urls import reverse

from apps.core import routes
from apps.onboarding import services as onboarding
from tests.factories import create_account, create_domain

pytestmark = pytest.mark.django_db

#: Rutas publicadas que **no** exigen sesión: son la puerta misma.
PUBLIC_ROUTES = {'login', 'logout', 'invitation.accept'}


def _protected_routes(domain, credential_id):
    """Arma la dirección concreta de cada ruta publicada que exige sesión."""
    parameters = {
        'domain_id': domain.id,
        'credential_id': credential_id,
        'session_id': uuid.uuid4(),
        'key_id': uuid.uuid4(),
        'batch_id': uuid.uuid4(),
        'export_id': uuid.uuid4(),
        'request_id': uuid.uuid4(),
        'notification_id': uuid.uuid4(),
        'account_id': uuid.uuid4(),
        'token': 'invitation-token',
        # El único parámetro que no es un identificador. Va un código de paso
        # real: `onboarding.verify` acepta cualquier cadena en la dirección y es
        # la vista la que rechaza los que no existen, así que un valor inventado
        # acá probaría el 404 en vez del ingreso, que es lo que se mide.
        'step': 'GOOGLE_PROJECT',
    }

    urls = []
    for name in routes.PUBLISHED_ROUTES:
        if name in PUBLIC_ROUTES:
            continue
        _uri, params = routes._uri_and_params(name)
        urls.append((name, reverse(name, kwargs={p: parameters[p] for p in params})))
    return urls


@pytest.fixture
def urls():
    account = create_account()
    return _protected_routes(create_domain(account), uuid.uuid4())


def test_login_url_points_to_a_route_that_exists():
    """
    El valor de fábrica de Django es «/accounts/login/» y acá no existe.

    Es una línea de configuración cuya ausencia no rompe nada al arrancar: el
    proyecto pasa `manage.py check`, la suite pasa, y el fallo aparece recién
    cuando alguien sin sesión abre la aplicación.
    """
    assert settings.LOGIN_URL == 'login'
    assert reverse(settings.LOGIN_URL) == '/login'


def test_without_a_session_every_screen_sends_to_the_login(client, urls):
    for name, url in urls:
        response = client.get(url)

        assert response.status_code == 302, f'«{name}» no redirigió a nadie sin sesión'
        assert response.headers['Location'].startswith('/login'), (
            f'«{name}» mandó a {response.headers["Location"]} en vez de al ingreso. '
            f'Revisá LOGIN_URL en config/settings/base.py.'
        )


def test_the_redirect_keeps_where_the_visitor_wanted_to_go(client, urls):
    """
    Sin el destino, quien entra por un enlace cae en el listado y pierde el rumbo.

    Es la diferencia entre «iniciá sesión y seguimos» y «iniciá sesión y buscá
    de nuevo lo que estabas mirando».
    """
    _name, url = urls[0]

    response = client.get(url)

    assert f'next={url}' in response.headers['Location']


def test_the_site_root_sends_to_the_login(client):
    """El caso del reporte: abrir la aplicación sin sesión."""
    response = client.get('/')

    assert response.status_code == 302
    assert response.headers['Location'].startswith('/login')


def test_with_a_session_the_tool_offers_the_tour(client):
    """
    Quien todavía no dejó su cuenta conectada entra por el recorrido (FR-015).

    Lo ofrece la herramienta y no la raíz: el recorrido conecta **una**
    herramienta, y la raíz es justamente donde se elige cuál.
    """
    account = create_account()
    client.force_login(account)

    response = client.get('/search-console')

    assert response.status_code == 302
    assert response.headers['Location'] == '/search-console/onboarding'


def test_with_the_tour_dismissed_the_tool_shows_the_dashboard(client):
    """
    Omitir tiene que durar: si la herramienta volviera a ofrecerlo, «Omitir»
    sería un botón que no hace nada más que devolver a la misma pantalla al
    recargar.

    Y deja de redirigir: la raíz de la herramienta es el tablero (V0). Antes
    mandaba al listado de dominios, que es un inventario y no un estado.
    """
    account = create_account()
    client.force_login(account)
    onboarding.dismiss(account)

    response = client.get('/search-console', HTTP_X_INERTIA='true')

    assert response.status_code == 200
    assert response.json()['component'] == 'Dashboard'


def test_after_logging_in_the_visitor_goes_back_to_where_they_wanted_to_go(client, urls):
    """
    El destino pedido se respeta, y sólo si es interno.

    Un `next` que apunta afuera convertiría la pantalla de ingreso en un
    trampolín hacia un sitio ajeno con la apariencia de venir del nuestro.
    """
    account = create_account(email='vuelve@ejemplo.test', password='contraseña-de-prueba')
    _name, target = urls[0]

    response = client.post(
        f'/login?next={target}',
        {'email': 'vuelve@ejemplo.test', 'password': 'contraseña-de-prueba'},
    )

    assert response.status_code == 302
    assert response.headers['Location'] == target
    assert account.is_authenticated


def test_an_external_target_is_ignored(client):
    create_account(email='externo@ejemplo.test', password='contraseña-de-prueba')

    response = client.post(
        '/login?next=https://sitio-ajeno.test/robar',
        {'email': 'externo@ejemplo.test', 'password': 'contraseña-de-prueba'},
    )

    # A la raíz y no al listado: sin destino pedido, adónde va cada quien lo
    # decide `home` según el estado de su cuenta, en un solo lugar.
    assert response.headers['Location'] == '/'


def test_the_login_page_receives_the_target_as_a_prop(client, urls):
    """
    El eslabón que faltaba, y el que ningún test del servidor veía.

    La dirección trae el destino en la cadena de consulta, pero el formulario
    envía a «/login» a secas. Si el destino no llega a la página como prop para
    que ella lo reenvíe como campo, se pierde en el envío y quien entró por un
    enlace termina en el listado.
    """
    _name, target = urls[0]

    response = client.get(f'/login?next={target}', HTTP_X_INERTIA='true')
    props = response.json()['props']

    assert props['next'] == target


def test_an_external_target_does_not_reach_the_page(client):
    response = client.get('/login?next=https://sitio-ajeno.test', HTTP_X_INERTIA='true')

    assert response.json()['props']['next'] == ''


def test_a_wrong_password_does_not_lose_the_target(client, urls):
    """
    Volver al formulario tiene que conservar adónde se quería ir.

    Perderlo convertiría una contraseña mal escrita en la pérdida del enlace por
    el que la persona había llegado.
    """
    create_account(email='destino@ejemplo.test', password='contraseña-de-prueba')
    _name, target = urls[0]

    response = client.post(
        '/login',
        {'email': 'destino@ejemplo.test', 'password': 'equivocada', 'next': target},
    )

    assert response.headers['Location'] == f'/login?next={target}'


def test_the_credentials_error_belongs_to_the_form_and_not_to_a_field(client):
    """
    El mensaje habla de **dos** campos, así que no puede colgar de uno.

    «El correo o la contraseña no coinciden» es la única ambigüedad deliberada
    del producto —separarla convertiría el formulario en un enumerador de
    correos—, y colgada de `password` marcaba como inválido un campo que puede
    estar perfecto. Va como error del formulario, arriba de los dos (RT-08).
    """
    create_account(email='ambiguo@ejemplo.test', password='contraseña-de-prueba')

    client.post('/login', {'email': 'ambiguo@ejemplo.test', 'password': 'equivocada'})
    errors = client.get('/login', HTTP_X_INERTIA='true').json()['props']['errors']

    assert errors['form']['code'] == 'CREDENTIALS_INVALID'
    assert 'email' not in errors and 'password' not in errors
    assert 'password' not in errors
    assert 'email' not in errors


def test_an_empty_field_still_says_so_next_to_that_field(client):
    """
    La contracara: un campo vacío sí sabe cuál es, y su mensaje va pegado a él.

    Si todo error de ingreso subiera al formulario, «Escribí tu contraseña»
    dejaría de señalar dónde escribirla.
    """
    client.post('/login', {'email': 'alguien@ejemplo.test', 'password': ''})
    errors = client.get('/login', HTTP_X_INERTIA='true').json()['props']['errors']

    assert 'password' in errors
    assert 'form' not in errors


def test_the_target_sent_by_the_form_is_honored(client, urls):
    create_account(email='campo@ejemplo.test', password='contraseña-de-prueba')
    _name, target = urls[0]

    response = client.post(
        '/login',
        {'email': 'campo@ejemplo.test', 'password': 'contraseña-de-prueba', 'next': target},
    )

    assert response.headers['Location'] == target
