"""
Omitir el recorrido no restringe nada (FR-018).

El recorrido es un atajo, no una llave. Quien lo omite tiene que poder hacer
exactamente lo mismo por los formularios completos: cargar la credencial,
comprobarla, dar de alta el dominio, confirmar su acceso, registrar el sitemap y
sincronizar. Por eso este archivo recorre la API pública entera **después** de
omitir, y no comprueba una bandera sino que cada operación siga contestando.

La otra mitad es que omitir no borre nada: lo hecho por los formularios completos
tiene que aparecer cumplido si la persona vuelve al recorrido, y el recorrido
tiene que seguir disponible para retomarlo.
"""

import json
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.credentials.models import Credential
from apps.domains.models import AccessState, Domain
from apps.jobs.models import Batch
from apps.onboarding import content, services
from apps.onboarding.models import OnboardingProgress
from tests.doubles import FakeClient, response
from tests.factories import TEST_KEY, create_account

pytestmark = pytest.mark.django_db

FIXTURES = Path(settings.BASE_DIR) / 'tests' / 'fixtures' / 'sitemaps'
SITEMAP = 'https://ejemplo.test/sitemap-1.xml'


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


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


@pytest.fixture
def google(monkeypatch):
    """
    Deja los tres endpoints que hablan con Google apuntando al doble.

    El corte va en cada módulo de API y no en el servicio para que lo que se
    ejercita siga siendo el camino completo: la vista, el servicio y la
    clasificación de la respuesta.
    """
    fake = FakeClient(list_sites=response('sites_list'), get_site={}, submit_sitemap={})

    def site(request: httpx.Request) -> httpx.Response:
        if str(request.url) != SITEMAP:
            return httpx.Response(404, text='no encontrado')
        return httpx.Response(200, content=(FIXTURES / 'urlset.xml').read_bytes())

    http = httpx.Client(transport=httpx.MockTransport(site))

    from apps.credentials.services import verify_credential
    from apps.domains.services import check_access
    from apps.sitemaps.services import sync_domain

    monkeypatch.setattr(
        'apps.credentials.api.verify_credential',
        lambda credential, client=None: verify_credential(credential, client=fake),
    )
    monkeypatch.setattr(
        'apps.domains.api.check_access',
        lambda domain, client=None: check_access(domain, client=fake),
    )
    # Se sustituye el encolado y no la tarea: el endpoint ahora crea el lote y
    # delega en Celery, así que interceptarlo acá es lo que deja inyectar el
    # cliente falso sin que la tarea salga a la red. Lo que el test verifica
    # —que la API ofrece las mismas operaciones— no depende de por dónde corre
    # el trabajo.
    monkeypatch.setattr(
        'apps.sitemaps.api.queue_sync',
        lambda domain, origin=None, **_: sync_domain(domain, origin=origin, client=fake, http=http),
    )
    # El alta de un sitemap lee el archivo antes de guardarlo (T094), y eso es
    # tráfico hacia el sitio del usuario, no hacia Google: se le pasa el mismo
    # sitio simulado que usa la sincronización.
    from apps.sitemaps.reader import read

    monkeypatch.setattr(
        'apps.sitemaps.services.read',
        lambda location, *, client=None, timeout=None: read(location, client=http),
    )
    return fake


def everything_through_the_full_forms(api_client) -> dict:
    """Recorre la API pública de punta a punta y devuelve lo que fue respondiendo."""
    credential = api_client.post(
        '/api/v1/credentials',
        {'project_id': TEST_KEY['project_id'], 'key_file': json.dumps(TEST_KEY)},
        format='json',
    )
    verification = api_client.post(f'/api/v1/credentials/{credential.json()["id"]}/verify')
    domain = api_client.post(
        '/api/v1/domains', {'hostname': 'ejemplo.test', 'property_type': 'DOMAIN'}, format='json'
    )
    domain_id = domain.json()['id']
    access = api_client.post(f'/api/v1/domains/{domain_id}/check-access')
    sitemap = api_client.post(
        f'/api/v1/domains/{domain_id}/sitemaps', {'location': SITEMAP}, format='json'
    )
    sync = api_client.post(f'/api/v1/domains/{domain_id}/sync')

    return {
        'credential': credential,
        'verification': verification,
        'domain': domain,
        'access': access,
        'sitemap': sitemap,
        'sync': sync,
    }


# --- Omitir no restringe ----------------------------------------------------


def test_after_dismissing_the_full_forms_still_offer_everything(api_client, account, google):
    api_client.post('/api/v1/onboarding/dismiss')

    responses = everything_through_the_full_forms(api_client)

    assert responses['credential'].status_code == 201
    assert responses['verification'].json()['ok'] is True
    assert responses['domain'].status_code == 201
    assert responses['access'].json()['access_state'] == AccessState.OPERATIONAL
    assert responses['sitemap'].status_code == 201
    assert responses['sync'].status_code == 202
    assert Batch.objects.filter(domain__account=account).count() == 1


def test_dismissing_does_not_touch_any_object_of_the_account(api_client, account, google):
    everything_through_the_full_forms(api_client)
    before = list(Domain.objects.filter(account=account).values_list('id', 'access_state'))

    api_client.post('/api/v1/onboarding/dismiss')

    assert list(Domain.objects.filter(account=account).values_list('id', 'access_state')) == before
    assert Credential.objects.filter(account=account).count() == 1


def test_dismissing_only_stops_offering_it(api_client, account):
    api_client.post('/api/v1/onboarding/dismiss')

    assert services.should_offer(account) is False
    assert OnboardingProgress.objects.get(account=account).completed_steps == []


# --- Omitir no borra --------------------------------------------------------


def test_the_tour_is_still_reachable_by_direct_url(api_client, google):
    api_client.post('/api/v1/onboarding/dismiss')

    api_response = api_client.get('/api/v1/onboarding')

    assert api_response.status_code == 200
    assert api_response.json()['is_dismissed'] is True
    assert len(api_response.json()['steps']) == len(content.STEP_CODES)


def test_what_was_done_through_the_full_forms_appears_completed_on_returning(api_client, google):
    """
    Retomar no puede pedir de nuevo lo que la persona ya hizo por su cuenta.

    Los pasos se marcan igual que siempre —comprobando el estado real— así que
    esto no es una excepción a FR-017 sino su consecuencia.
    """
    api_client.post('/api/v1/onboarding/dismiss')
    everything_through_the_full_forms(api_client)

    data = api_client.get('/api/v1/onboarding').json()

    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['created']['hostname'] == 'ejemplo.test'
    # Las propiedades salen de la credencial, no del contexto del recorrido: por
    # eso están aunque el asistente no haya intervenido en ningún momento.
    assert data['created']['properties'][0]['property_uri'] == 'sc-domain:ejemplo.test'


def test_resuming_it_after_dismissing_it_puts_it_back_in_progress(api_client, account):
    api_client.post('/api/v1/onboarding/dismiss')

    data = api_client.post(
        '/api/v1/onboarding/steps/GOOGLE_PROJECT/verify',
        {'project_id': TEST_KEY['project_id']},
        format='json',
    ).json()

    assert data['is_dismissed'] is False
    assert data['dismissed_at'] is None
    assert data['completed_steps'] == ['GOOGLE_PROJECT']
    assert services.should_offer(account) is True


def test_a_finished_tour_stops_offering_itself_on_its_own(api_client, account, google):
    everything_through_the_full_forms(api_client)

    data = api_client.get('/api/v1/onboarding').json()

    assert data['is_finished'] is True
    assert data['is_dismissed'] is False
    assert data['should_offer'] is False
    assert services.should_offer(account) is False
