"""
El comando que fuerza el ciclo de un dominio (T085).

Es una herramienta de operación, así que lo que se verifica no es sólo que haga
el trabajo sino que **diga qué pasó** y que se detenga donde el ciclo automático
se detiene: sin acceso confirmado, los pasos siguientes sólo producirían lotes
fallidos y gastarían cupo en llamadas que Google va a rechazar.
"""

import pytest
from django.core.management import call_command
from django.core.management.base import CommandError

from apps.domains.models import AccessState
from apps.jobs.models import Batch, BatchKind, BatchState
from apps.sitemaps.models import CoverageState, Url
from tests.doubles import FakeClient, http_error, response
from tests.factories import create_account, create_credential, create_domain

pytestmark = pytest.mark.django_db


@pytest.fixture
def domain():
    account = create_account()
    create_credential(account)
    return create_domain(account, daily_inspection_budget=100, manual_reserve=10)


@pytest.fixture
def google(monkeypatch):
    """
    Deja los tres pasos del ciclo hablando con el doble de Google.

    Se corta en cada servicio y no en el cliente porque el comando los llama a
    los tres y ninguno recibe el cliente desde afuera: es la costura que hay.
    """
    from apps.coverage import services as coverage
    from apps.domains import services as domains
    from apps.sitemaps import services as sitemaps

    fake = FakeClient(
        get_site={},
        list_sites=response('sites_list'),
        submit_sitemap={},
        inspect_url=response('url_inspection_indexada'),
    )

    command = 'apps.coverage.management.commands.run_daily_cycle'
    monkeypatch.setattr(
        f'{command}.check_access', lambda dom, client=None: domains.check_access(dom, client=fake)
    )
    monkeypatch.setattr(
        f'{command}.sync_domain',
        lambda dom, **kwargs: sitemaps.sync_domain(dom, client=fake, **kwargs),
    )
    monkeypatch.setattr(
        f'{command}.inspect_domain',
        lambda dom, **kwargs: coverage.inspect_domain(dom, client=fake, **kwargs),
    )
    return fake


def run(*args, **options) -> str:
    from io import StringIO

    out = StringIO()
    call_command('run_daily_cycle', *args, stdout=out, stderr=out, **options)
    return out.getvalue()


def test_it_runs_the_three_steps_and_reports_each_one(domain, google, capsys):
    Url.objects.create(domain=domain, loc='https://ejemplo.test/uno', in_sitemap=True)

    output = run(domain.hostname)

    assert f'Access: {AccessState(AccessState.OPERATIONAL).label}' in output
    assert 'Sitemaps:' in output
    assert 'Inspection:' in output
    assert Batch.objects.filter(domain=domain, kind=BatchKind.SITEMAP_SYNC).exists()

    inspection = Batch.objects.get(domain=domain, kind=BatchKind.URL_INSPECTION)
    assert inspection.state == BatchState.COMPLETED
    assert inspection.processed_items == 1


def test_it_takes_the_domain_by_its_identifier_too(domain, google):
    """Es lo que trae el enlace de un aviso o de una pantalla."""
    output = run(str(domain.id))

    assert domain.hostname in output


def test_a_domain_that_does_not_exist_says_so_without_a_traceback(db):
    with pytest.raises(CommandError, match='No domain is named'):
        run('no-existe.test')


def test_without_confirmed_access_it_stops_before_spending_quota(domain, monkeypatch):
    """
    El mismo orden que el ciclo automático, por el mismo motivo.

    Si el acceso se perdió, sincronizar e inspeccionar sólo dejarían dos lotes
    fallidos por corrida y gastarían cupo en llamadas que van a fallar.
    """
    from apps.domains import services as domains

    fake = FakeClient(get_site=http_error('error_permiso_denegado'))
    monkeypatch.setattr(
        'apps.coverage.management.commands.run_daily_cycle.check_access',
        lambda dom, client=None: domains.check_access(dom, client=fake),
    )

    output = run(domain.hostname)

    domain.refresh_from_db()
    assert domain.access_state == AccessState.ACCESS_LOST
    assert 'Stopping here' in output
    assert not Batch.objects.filter(domain=domain).exists()


def test_the_steps_can_be_skipped_one_by_one(domain, google):
    output = run(domain.hostname, skip_sync=True, skip_inspection=True)

    assert 'Access:' in output
    assert 'Sitemaps:' not in output
    assert not Batch.objects.filter(domain=domain).exists()


def test_the_inspection_spends_the_automatic_budget_and_not_the_manual_reserve(domain, google):
    """
    Esto reemplaza al trabajo del ciclo, no lo agrega.

    Gastar la reserva manual dejaría a la persona sin el cupo que tiene guardado
    para lo que dispara desde la pantalla, por una corrida que ella no pidió.
    """
    from apps.jobs.budget import budget_for

    Url.objects.create(domain=domain, loc='https://ejemplo.test/uno', in_sitemap=True)

    run(domain.hostname, skip_sync=True)

    budget = budget_for(domain)
    assert budget.used_automatic == 1
    assert budget.used_manual == 0


def test_queueing_does_not_do_the_work_inside_the_command(domain, google, monkeypatch):
    """Con `--queue` los lotes nacen en cola y los toma un trabajador."""
    enqueued = []
    monkeypatch.setattr(
        'apps.sitemaps.tasks.sync_one_domain.delay', lambda *a, **kw: enqueued.append(kw)
    )
    monkeypatch.setattr(
        'apps.coverage.tasks.inspect_one_domain.delay', lambda *a, **kw: enqueued.append(kw)
    )

    run(domain.hostname, queue=True)

    assert len(enqueued) == 2
    assert list(Batch.objects.filter(domain=domain).values_list('state', flat=True)) == [
        BatchState.QUEUED,
        BatchState.QUEUED,
    ]


def test_it_reports_what_the_inspection_found(domain, google):
    Url.objects.create(
        domain=domain,
        loc='https://ejemplo.test/uno',
        in_sitemap=True,
        coverage_state=CoverageState.UNKNOWN,
    )

    output = run(domain.hostname, skip_sync=True)

    assert '1 of 1 processed' in output
    assert 'quota spent 1' in output
