"""
La ficha de un dominio (V6): si está operativo, qué falta si no, y cuánto cupo queda.

Lo que gobierna esta pantalla es el estado de acceso. Sin acceso confirmado, el
cupo, los sitemaps y la cobertura son ruido: la ficha tiene que reordenarse
alrededor de la instrucción de autorización, y para eso la dirección de la cuenta
de servicio tiene que llegar a las props aunque el dominio todavía no sirva para
nada.

La otra regla que se verifica acá es RT-18: el estado de la cuenta manda sobre el
del dominio. Un dominio en `OPERATIONAL` con la credencial de la cuenta rota no
está operativo, y la pantalla recibe las dos mitades del dato para poder decirlo.
"""

import pytest

from apps.credentials.models import CredentialStatus
from apps.domains.models import AccessState, Domain
from apps.jobs.models import Batch, BatchKind, BatchOrigin, BatchState
from apps.sitemaps.models import Url
from tests.factories import create_account, create_credential, create_domain

pytestmark = pytest.mark.django_db


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


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


def props(client, domain) -> dict:
    response = client.get(f'/search-console/domains/{domain.id}', HTTP_X_INERTIA='true')
    assert response.status_code == 200
    return response.json()['props']


class TestTheScreen:
    def test_the_detail_exists_and_is_its_own_page(self, client, domain):
        """
        El hostname del listado tiene adónde llevar.

        Antes de esto el listado enlazaba directo a la cobertura, que es una de
        las tres vistas hijas: entrar por una de ellas obligaba a volver atrás
        para ver el estado de acceso o el cupo.
        """
        response = client.get(f'/search-console/domains/{domain.id}', HTTP_X_INERTIA='true')

        assert response.json()['component'] == 'Domains/Show'

    def test_without_a_session_it_sends_to_the_login(self, client, domain):
        client.logout()

        response = client.get(f'/search-console/domains/{domain.id}')

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

    def test_the_detail_created_by_another_profile_is_shared(self, client, account):
        other_account = create_account(email='ajena@ejemplo.test')
        other_domain = create_domain(other_account, hostname='ajeno.test')

        assert client.get(f'/search-console/domains/{other_domain.id}').status_code == 200

    def test_the_access_state_travels_with_its_date(self, client, domain):
        """
        Un estado sin fecha no dice si es de hace un minuto o de hace una semana.

        La ficha muestra las dos cosas juntas, y cuando nunca se comprobó la
        fecha viaja nula: no se inventa la de creación de la fila.
        """
        data = props(client, domain)['domain']

        # El código y no la etiqueta: la palabra de cada estado vive una sola vez,
        # en el catálogo del cliente, que es el único que sabe en qué idioma se
        # está leyendo.
        assert data['access_state'] == AccessState.OPERATIONAL
        assert 'access_state_label' not in data
        assert data['access_checked_at'] is None

    def test_the_daily_quota_arrives_with_its_two_pockets(self, client, domain):
        """
        Nunca un medidor solo: el ciclo automático no puede tocar la reserva manual.

        Esa garantía la sostiene `apps/jobs/budget.py`, y sólo sirve si se ve.
        """
        quota = props(client, domain)['quota']

        assert quota['limit_total'] == 1000
        assert quota['manual_reserve'] == 100
        assert quota['automatic_remaining'] == 900
        # La asimetría viaja tal cual y la ficha la explica: la reserva le
        # garantiza un piso al trabajo manual, no le pone un techo, así que lo
        # que el ciclo automático todavía no gastó también queda a su alcance.
        assert quota['manual_remaining'] == 1000
        # La fecha del presupuesto viaja siempre: «el cupo de hoy» tiene que
        # poder verificarse contra un día concreto.
        assert quota['date']

    def test_the_coverage_arrives_with_its_denominator(self, client, domain):
        """
        «12 URLs indexadas» sin decir sobre cuántas se lee como si fueran todas.

        La ficha manda las dos cifras para que el acceso a la cobertura diga
        cuánto del sitio tiene dato y cuánto todavía no se consultó.
        """
        Url.objects.create(domain=domain, loc='https://ejemplo.test/uno', in_sitemap=True)
        Url.objects.create(domain=domain, loc='https://ejemplo.test/dos', in_sitemap=True)

        coverage = props(client, domain)['coverage']

        assert coverage['total'] == 2
        assert coverage['with_data'] == 0
        assert coverage['last_checked_at'] is None

    def test_the_detail_says_how_many_urls_a_manual_inspection_would_consume(self, client, domain):
        """
        La confirmación tiene que decir el número, no «las que haya».

        Aceptar sin saber cuánto cupo se gasta es firmar en blanco, y la reserva
        manual es lo que queda para el resto del día.
        """
        Url.objects.create(domain=domain, loc='https://ejemplo.test/uno', in_sitemap=True)

        data = props(client, domain)

        assert data['pending_urls'] == 1
        assert data['cycle']['monitored_urls'] == 1
        assert data['cycle']['queries_per_day'] == 900

    def test_the_running_batch_and_the_last_finished_one_travel_separately(self, client, domain):
        """
        Son dos preguntas distintas y la pantalla hace algo distinto con cada una.

        El que está en curso es lo que la mantiene actualizándose; el último
        terminado es el número que resume qué pasó. Colapsarlos obligaría a la
        pantalla a adivinar cuál de los dos le tocó.
        """
        Batch.objects.create(
            domain=domain,
            kind=BatchKind.URL_INSPECTION,
            origin=BatchOrigin.SCHEDULED,
            state=BatchState.PARTIAL,
        )
        Batch.objects.create(
            domain=domain,
            kind=BatchKind.URL_INSPECTION,
            origin=BatchOrigin.SCHEDULED,
            state=BatchState.RUNNING,
        )

        data = props(client, domain)

        assert data['running_batch']['state'] == BatchState.RUNNING
        assert data['last_batch']['state'] == BatchState.PARTIAL


class TestWithoutAccess:
    def test_the_address_to_authorize_arrives_even_if_the_domain_is_not_useful_yet(
        self, client, account
    ):
        """
        Esperando acceso, la ficha **es** la instrucción de autorización.

        Sin la dirección en pantalla no hay nada que hacer: es el único dato que
        hay que pegar en Search Console para que el dominio empiece a servir.
        """
        domain = create_domain(
            account, hostname='nuevo.test', access_state=AccessState.AWAITING_ACCESS
        )

        data = props(client, domain)

        assert data['client_email'].endswith('.iam.gserviceaccount.com')
        assert data['domain']['is_operational'] is False

    def test_the_reason_of_the_last_attempt_arrives_with_its_code(self, client, account):
        """
        La ficha ramifica por código, no por el texto del error (RT-08).

        «No tenemos permiso» y «la propiedad no existe con esa forma» se
        resuelven en dos lugares distintos de Search Console: deducir cuál es
        leyendo la prosa rompería la pantalla al primer cambio de redacción.
        """
        domain = create_domain(
            account,
            hostname='sinpermiso.test',
            access_state=AccessState.AWAITING_ACCESS,
            access_error='La cuenta de servicio no tiene permiso sobre esta propiedad.',
            access_error_code='PERMISSION_DENIED',
        )

        data = props(client, domain)['domain']

        assert data['access_error_code'] == 'PERMISSION_DENIED'
        assert data['access_error']

    def test_with_a_broken_credential_the_domain_is_not_presented_as_operational(self, client):
        """
        RT-18: el estado de la cuenta manda sobre el del dominio.

        La columna del dominio describe su propiedad en Search Console; la de la
        cuenta, si tenemos con qué consultarla. Las dos viajan a la pantalla y se
        miran juntas antes de decir que algo funciona.
        """
        account = create_account(email='rota@ejemplo.test')
        create_credential(account, status=CredentialStatus.INVALID)
        client.force_login(account)
        domain = create_domain(account, hostname='operativo.test')

        data = props(client, domain)

        assert data['domain']['access_state'] == AccessState.OPERATIONAL
        assert data['domain']['is_operational'] is True
        assert data['account']['can_operate'] is False
        assert data['account']['notices']

    def test_checking_the_access_from_the_detail_returns_to_the_detail(
        self, client, domain, monkeypatch
    ):
        """
        El resultado de una comprobación aparece en el bloque del objeto (RT-11).

        Desde la ficha, ese bloque está en la ficha. Mandar al listado esconde
        justo el recuadro donde queda escrito qué pasó.
        """
        import apps.domains.views as views

        monkeypatch.setattr(views, 'check_access', lambda dom: dom)

        response = client.post(
            f'/search-console/domains/{domain.id}/check-access', {'from': 'show'}
        )

        assert response.headers['Location'] == f'/search-console/domains/{domain.id}'

    def test_without_declaring_an_origin_it_returns_to_the_connection(
        self, client, domain, monkeypatch
    ):
        """
        La conexión es el destino por omisión desde que el listado no tiene ruta.

        Sigue siendo la misma regla —el resultado de una comprobación aterriza en
        la pantalla que muestra lo comprobado (RT-11)— y lo único que cambió es
        cuál es esa pantalla cuando quien dispara no declara de dónde viene.
        """
        import apps.domains.views as views

        monkeypatch.setattr(views, 'check_access', lambda dom: dom)

        response = client.post(f'/search-console/domains/{domain.id}/check-access')

        assert response.headers['Location'] == '/search-console/settings'


class TestTheSettings:
    def patch(self, client, domain, body):
        return client.patch(
            f'/search-console/domains/{domain.id}', body, content_type='application/json'
        )

    def test_the_notifications_switch_saves_and_returns_to_the_detail(self, client, domain):
        """
        Guarda solo, sin botón: apagar un aviso es reversible y se nota en el acto.

        Vuelve a la misma ficha para que la confirmación aparezca donde estaba la
        mirada, y no en otra pantalla.
        """
        response = self.patch(client, domain, {'notifications_enabled': False})

        assert response.status_code == 302
        assert response.headers['Location'] == f'/search-console/domains/{domain.id}'
        domain.refresh_from_db()
        assert domain.notifications_enabled is False

    def test_both_quotas_save_with_the_same_patch(self, client, domain):
        self.patch(client, domain, {'daily_inspection_budget': 1500, 'manual_reserve': 300})

        domain.refresh_from_db()
        assert domain.daily_inspection_budget == 1500
        assert domain.manual_reserve == 300

    def test_a_reserve_larger_than_the_budget_returns_with_the_error_in_its_field(
        self, client, domain
    ):
        """
        El error aparece junto al campo, con el porqué en números (RT-08, RT-17).

        Y nada se guarda: dejar pasar la mitad del cambio sería peor que
        rechazarlo entero, porque el dominio quedaría en un estado que nadie pidió.
        """
        response = self.patch(
            client, domain, {'daily_inspection_budget': 200, 'manual_reserve': 300}
        )

        assert response.headers['Location'] == f'/search-console/domains/{domain.id}'
        domain.refresh_from_db()
        assert domain.daily_inspection_budget == 1000
        assert domain.manual_reserve == 100

        # Se comprueba sobre las props que recibe la pantalla y no sobre la
        # sesión: lo que importa es que el mensaje llegue al campo donde se
        # corrige, no dónde viajó en el camino.
        errors = props(client, domain)['errors']
        fault = errors['daily_inspection_budget']

        # Las dos cifras viajan como datos: la frase las nombra para que se vea
        # que la reserva no entra en el presupuesto, y no sólo que «no es válido».
        assert fault['params']['manual_reserve'] == 300
        assert fault['params']['daily_inspection_budget'] == 200

    def test_a_budget_of_zero_is_not_saved(self, client, domain):
        self.patch(client, domain, {'daily_inspection_budget': 0})

        domain.refresh_from_db()
        assert domain.daily_inspection_budget == 1000
        assert props(client, domain)['errors']['daily_inspection_budget']

    def test_the_numbers_arrive_as_text_from_the_form(self, client, domain):
        """
        Los campos numéricos viajan como cadena y el servidor los acepta igual.

        La pantalla los guarda como texto para que se puedan dejar vacíos
        mientras se escribe; el contrato no puede depender de ese detalle de la
        interfaz.
        """
        self.patch(client, domain, {'daily_inspection_budget': '1200', 'manual_reserve': '150'})

        domain.refresh_from_db()
        assert domain.daily_inspection_budget == 1200
        assert domain.manual_reserve == 150

    def test_the_detail_created_by_another_profile_can_be_edited(self, client, account):
        other_account = create_account(email='ajena@ejemplo.test')
        other_domain = create_domain(other_account, hostname='ajeno.test')

        response = self.patch(client, other_domain, {'manual_reserve': 1})

        assert response.status_code == 302
        assert Domain.objects.get(pk=other_domain.pk).manual_reserve == 1
