"""
El recorrido completo, de una cuenta vacía al primer lote encolado.

Se ejercita con Google y el sitio del usuario simulados, y lo que verifica no es
que cada paso ande por separado sino que ninguno se marque cumplido sin que su
requisito esté de verdad. Es el invariante del que depende todo lo demás: alguien
que termina el recorrido con un paso marcado a mano se va con una configuración
que no funciona y se entera recién cuando no llega ningún dato (FR-017).

El otro invariante que se cruza acá es el de la cuota: el doble de Google protesta
si lo llaman sin reserva concedida, así que cada llamada del recorrido queda
comprobada de verdad.
"""

import json
from pathlib import Path

import httpx
import pytest
from django.conf import settings

from apps.credentials.models import Credential, CredentialStatus
from apps.domains.models import AccessState, Domain
from apps.jobs.models import Batch, BatchState
from apps.onboarding import services
from apps.sitemaps.models import Sitemap, Url
from tests.doubles import FakeClient, http_error, response
from tests.factories import TEST_KEY, create_account

pytestmark = pytest.mark.django_db

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


def fake_site(**paths) -> httpx.Client:
    """Simula el sitio del usuario. Leer un sitemap no gasta cuota de Google."""

    def respond(request: httpx.Request) -> httpx.Response:
        body = paths.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))


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


@pytest.fixture
def site():
    return fake_site(
        **{
            'https://ejemplo.test/sitemap.xml': (FIXTURES / 'indice.xml').read_bytes(),
            'https://ejemplo.test/sitemap-1.xml': (FIXTURES / 'urlset.xml').read_bytes(),
            'https://ejemplo.test/sitemap-2.xml': (
                FIXTURES / 'urlset_sin_espacio_de_nombres.xml'
            ).read_bytes(),
        }
    )


def step(data: dict, code: str) -> dict:
    return next(p for p in data['steps'] if p['code'] == code)


def _declare_project(account):
    return services.verify(account, 'GOOGLE_PROJECT', {'project_id': TEST_KEY['project_id']})


def _upload_key(account, google):
    return services.verify(
        account,
        'SERVICE_ACCOUNT_KEY',
        {'project_id': TEST_KEY['project_id'], 'key_file': json.dumps(TEST_KEY)},
        client=google,
    )


def _add_the_domain(account, google=None):
    return services.verify(
        account,
        'ADD_DOMAIN',
        {'hostname': 'ejemplo.test', 'property_type': 'DOMAIN'},
        client=google or FakeClient(get_site={}),
    )


def _add_the_sitemap(account, site):
    return services.verify(
        account, 'ADD_SITEMAP', {'location': 'https://ejemplo.test/sitemap.xml'}, http=site
    )


def _sync(account, site):
    return services.verify(
        account, 'FIRST_BATCH', {}, client=FakeClient(submit_sitemap={}), http=site
    )


# --- El camino entero -------------------------------------------------------


def test_from_an_empty_account_to_the_first_queued_batch(account, site):
    google = FakeClient(list_sites=response('sites_list'))

    _declare_project(account)
    _upload_key(account, google)
    _add_the_domain(account)
    _add_the_sitemap(account, site)
    data = _sync(account, site)

    assert data['completed_steps'] == [
        'GOOGLE_PROJECT',
        'ENABLE_API',
        'SERVICE_ACCOUNT_KEY',
        'AUTHORIZE_PROPERTY',
        'ADD_DOMAIN',
        'ADD_SITEMAP',
        'FIRST_BATCH',
    ]
    assert data['is_finished'] is True
    assert data['completed_at']
    assert data['should_offer'] is False
    assert services.should_offer(account) is False


def test_the_closing_links_to_the_queued_batch_and_to_the_coverage(account, site):
    google = FakeClient(list_sites=response('sites_list'))
    _declare_project(account)
    _upload_key(account, google)
    _add_the_domain(account)
    _add_the_sitemap(account, site)

    data = _sync(account, site)
    created = data['created']
    batch = Batch.objects.get(domain__account=account)

    assert created['batch_id'] == str(batch.id)
    assert created['batch_state'] == BatchState.COMPLETED
    assert created['coverage_path'] == f'/search-console/domains/{created["domain_id"]}/coverage'
    assert Url.objects.filter(domain__account=account).count() == 4


def test_no_call_of_the_tour_goes_out_without_a_granted_reservation(account, site):
    """
    El doble protesta si lo llaman sin cupo, así que esto se prueba de verdad.

    La cuota es del usuario y el recorrido no la gasta sin haberla contado antes,
    por más que sea un asistente y no un trabajo de fondo.
    """
    google = FakeClient(list_sites=response('sites_list'), get_site={}, submit_sitemap={})

    _declare_project(account)
    _upload_key(account, google)
    _add_the_domain(account, google)
    _add_the_sitemap(account, site)
    services.verify(account, 'FIRST_BATCH', {}, client=google, http=site)

    assert google.calls
    for operation, reservation in google.calls:
        assert reservation.granted > 0, f'«{operation}» corrió sin cupo'


# --- Un paso no se cumple por haber sido visitado ---------------------------


def test_looking_at_the_tour_completes_no_step(account):
    services.state(account)
    services.state(account)

    data = services.state(account)

    assert data['completed_steps'] == []
    assert Credential.objects.filter(account=account).count() == 0


def test_verifying_without_having_done_anything_does_not_mark_the_step(account):
    data = services.verify(account, 'ADD_DOMAIN', {})

    assert step(data, 'ADD_DOMAIN')['state'] == 'BLOCKED'
    assert data['completed_steps'] == []
    assert Domain.objects.filter(account=account).count() == 0


def test_without_a_verified_credential_the_domain_creation_says_where_the_problem_is(account):
    """
    No se puede dar de alta un sitio sin credencial comprobada, y el paso lo dice.

    Contestar «todavía no diste de alta tu sitio» mandaría a escribir el dominio
    para que no pase nada y el mensaje siga igual: el callejón sin salida que
    este recorrido viene a sacar (FR-017).
    """
    _upload_key(account, FakeClient(list_sites=response('sites_list_vacio')))

    data = services.verify(
        account, 'ADD_DOMAIN', {'hostname': 'ejemplo.test', 'property_type': 'DOMAIN'}
    )

    domain = step(data, 'ADD_DOMAIN')
    assert domain['state'] == 'BLOCKED'
    assert domain['blocked_by'] == 'AUTHORIZE_PROPERTY'
    assert Domain.objects.filter(account=account).count() == 0
    assert data['current_step'] == 'AUTHORIZE_PROPERTY'


def test_the_domain_is_completed_only_when_google_confirms_the_access(account):
    """
    Dar de alta el sitio no es lo mismo que poder leerlo.

    Con la propiedad sin autorizar, el dominio existe y el paso sigue pendiente:
    darlo por bueno dejaría a la sincronización del paso siguiente fallando sin
    que nadie entienda por qué.
    """
    _declare_project(account)
    _upload_key(account, FakeClient(list_sites=response('sites_list')))

    data = _add_the_domain(account, FakeClient(get_site=http_error('error_permiso_denegado')))

    domain = Domain.objects.get(account=account)
    assert domain.access_state == AccessState.AWAITING_ACCESS
    assert step(data, 'ADD_DOMAIN')['state'] == 'MISSING'
    assert 'ADD_DOMAIN' not in data['completed_steps']

    add_domain = step(data, 'ADD_DOMAIN')
    # Con motivo de Google, el código es el que lo incluye en la frase: son dos
    # redacciones distintas y no una con un hueco que a veces queda vacío.
    assert add_domain['reason'] == 'DOMAIN_NOT_OPERATIONAL_DETAIL'
    # El dominio viaja como dato de la frase: sin él, «el acceso no está
    # confirmado» no dice de cuál de los sitios de la cuenta habla.
    assert add_domain['reason_params']['hostname'] == domain.hostname


def test_a_sitemap_that_cannot_be_downloaded_does_not_complete_the_step(account):
    """
    Registrar la dirección no prueba nada: el error frecuente es un 404.

    El mensaje trae el motivo tal como lo devolvió el sitio, porque «revisá el
    sitemap» no le dice a nadie qué revisar.
    """
    _declare_project(account)
    _upload_key(account, FakeClient(list_sites=response('sites_list')))
    _add_the_domain(account)

    data = services.verify(
        account,
        'ADD_SITEMAP',
        {'location': 'https://ejemplo.test/no-existe.xml'},
        http=fake_site(),
    )

    assert Sitemap.objects.filter(domain__account=account).count() == 1
    assert step(data, 'ADD_SITEMAP')['state'] == 'MISSING'

    sitemap_step = step(data, 'ADD_SITEMAP')
    assert sitemap_step['reason'] == 'SITEMAP_UNREADABLE'
    # Lo que dijo el servidor del sitio viaja como dato: es la diferencia entre
    # «no lo pudimos leer» y «devolvió 404».
    assert '404' in sitemap_step['reason_params']['detail']
    # El código viaja además del texto: la pantalla separa «revisá la dirección»
    # de «revisá el archivo» sin tener que leer la oración.
    assert step(data, 'ADD_SITEMAP')['evidence']['error_code'] == 'UNREACHABLE'


def test_a_url_without_a_scheme_is_rejected_as_invalid_data(account):
    from apps.core.errors import ValidationFailed

    _declare_project(account)
    _upload_key(account, FakeClient(list_sites=response('sites_list')))
    _add_the_domain(account)

    with pytest.raises(ValidationFailed) as failure:
        services.verify(account, 'ADD_SITEMAP', {'location': 'ejemplo.test/sitemap.xml'})

    assert failure.value.details['field'] == 'location'


# --- El reparto de los fallos de Google entre los pasos ---------------------


def test_a_valid_key_without_authorized_properties_completes_the_key_step(account):
    """
    La distinción decisiva del recorrido.

    La clave funciona: lo que falta es autorizarla en Search Console. Marcar el
    paso de la clave como incumplido mandaría a esa persona a crear otra clave,
    que tampoco va a servir.
    """
    data = _upload_key(account, FakeClient(list_sites=response('sites_list_vacio')))

    assert step(data, 'SERVICE_ACCOUNT_KEY')['state'] == 'PASSED'
    assert step(data, 'ENABLE_API')['state'] == 'PASSED'
    assert step(data, 'AUTHORIZE_PROPERTY')['state'] == 'MISSING'

    authorize = step(data, 'AUTHORIZE_PROPERTY')
    assert authorize['reason'] == 'AUTH_NOT_GRANTED'
    # La dirección de la cuenta de servicio va como dato: es la que hay que
    # pegar en Search Console, y sin ella la frase no dice qué autorizar.
    assert authorize['reason_params']['client_email'] == TEST_KEY['client_email']
    assert data['current_step'] == 'AUTHORIZE_PROPERTY'


def test_the_api_being_off_belongs_to_step_two_and_not_to_the_key_step(account):
    """
    Cada motivo de Google lo reclama el paso que lo resuelve.

    Con la API apagada la clave es impecable: mandar a bajar otra sería hacerle
    perder la tarde a alguien para volver al mismo lugar.
    """
    data = _upload_key(account, FakeClient(list_sites=http_error('error_api_no_habilitada')))

    assert step(data, 'ENABLE_API')['state'] == 'MISSING'
    assert step(data, 'ENABLE_API')['reason'] == 'API_NOT_ENABLED_NAMED'
    assert step(data, 'SERVICE_ACCOUNT_KEY')['state'] == 'PASSED'
    assert step(data, 'AUTHORIZE_PROPERTY')['state'] == 'BLOCKED'
    assert step(data, 'AUTHORIZE_PROPERTY')['blocked_by'] == 'ENABLE_API'
    assert data['current_step'] == 'ENABLE_API'


def test_a_key_rejected_by_google_belongs_to_the_key_step(account):
    data = _upload_key(account, FakeClient(list_sites=http_error('error_permiso_denegado')))

    credential = Credential.objects.get(account=account)
    assert credential.status == CredentialStatus.INVALID
    assert step(data, 'SERVICE_ACCOUNT_KEY')['state'] == 'PASSED'
    assert step(data, 'AUTHORIZE_PROPERTY')['state'] == 'MISSING'


def test_google_being_down_leaves_the_step_unconfirmed_and_not_unmet(account):
    """
    Un corte de red no es un error de quien está configurando.

    Marcar el paso como incumplido lo mandaría a rehacer algo que estaba bien.
    """
    data = _upload_key(account, FakeClient(list_sites=http_error('error_google_caido')))

    key = step(data, 'SERVICE_ACCOUNT_KEY')
    assert key['state'] == 'UNCONFIRMED'
    # Un código propio, distinto del de un paso incumplido: es lo que deja a la
    # pantalla decir «no es algo que hayas hecho mal» en vez de mandar a rehacer.
    assert key['reason'] == 'NO_RESPONSE'
    assert data['completed_steps'] == ['GOOGLE_PROJECT']
