"""
Contrato de credenciales y módulos.

El caso que más importa es el `422`: su detalle tiene que nombrar el campo que
falta o el tipo que se encontró. Un `422` con «datos inválidos» obliga a quien
integra a probar de a un campo por vez hasta dar con el que molesta.
"""

import json

import pytest
from rest_framework.test import APIClient

from apps.accounts.models import ApiKey
from apps.credentials.models import Credential, CredentialStatus, Module, ModuleCredential
from apps.credentials.validation import KeyFileError
from tests.doubles import FakeClient, http_error, response
from tests.factories import TEST_KEY, create_account

pytestmark = pytest.mark.django_db


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


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


# --- Módulos ----------------------------------------------------------------


def test_the_catalog_of_modules_brings_the_apis_and_the_permissions(client):
    """
    Las APIs necesarias viajan en el módulo y no en un texto de la pantalla.

    Es lo que permite que la guía diga qué habilitar sin que nadie tenga que
    acordarse de actualizar dos lugares cuando se sume un módulo.
    """
    data = client.get('/api/v1/modules').json()

    assert len(data) == 1
    assert data[0]['code'] == Module.SEARCH_CONSOLE
    assert data[0]['required_apis'][0]['service'] == 'searchconsole.googleapis.com'
    assert 'webmasters' in data[0]['required_scopes'][0]


# --- Alta -------------------------------------------------------------------


def test_uploading_a_valid_credential_returns_201_without_the_material(client):
    api_response = client.post(
        '/api/v1/credentials',
        {'project_id': TEST_KEY['project_id'], 'key_file': json.dumps(TEST_KEY)},
        format='json',
    )

    assert api_response.status_code == 201
    body = api_response.json()
    assert body['client_email'] == TEST_KEY['client_email']
    assert 'encrypted_key' not in body
    assert 'PRIVATE KEY' not in json.dumps(body)


def test_the_upload_leaves_the_credential_assigned_to_the_module(client, account):
    client.post(
        '/api/v1/credentials',
        {'project_id': TEST_KEY['project_id'], 'key_file': json.dumps(TEST_KEY)},
        format='json',
    )

    assignment = ModuleCredential.objects.get(account=account, is_active=True)
    assert assignment.module.code == Module.SEARCH_CONSOLE


def test_a_422_names_the_missing_field(client):
    data = dict(TEST_KEY)
    del data['client_email']

    api_response = client.post(
        '/api/v1/credentials',
        {'project_id': TEST_KEY['project_id'], 'key_file': json.dumps(data)},
        format='json',
    )

    assert api_response.status_code == 422
    error = api_response.json()['error']
    assert error['code'] == 'VALIDATION_FAILED'
    assert 'client_email' in error['message']
    assert error['details']['field'] == 'client_email'


def test_a_422_says_what_type_of_file_was_found(client):
    api_response = client.post(
        '/api/v1/credentials',
        {
            'project_id': TEST_KEY['project_id'],
            'key_file': json.dumps({'installed': {'client_id': 'x'}}),
        },
        format='json',
    )

    assert api_response.status_code == 422
    error = api_response.json()['error']

    # El tipo de archivo viaja como código: es lo que deja a un cliente
    # distinguir «bajaste la credencial de una app de escritorio» de «bajaste tu
    # propia credencial de usuario» sin leer prosa. El `message` acompaña, en
    # inglés, que es el idioma del contrato de la API.
    assert error['details']['reason'] == KeyFileError.FILE_IS_OAUTH_CLIENT_DESKTOP
    assert 'desktop' in error['message']


def test_a_key_from_another_project_is_rejected_naming_both(client):
    api_response = client.post(
        '/api/v1/credentials',
        {'project_id': 'otro-proyecto-999', 'key_file': json.dumps(TEST_KEY)},
        format='json',
    )

    assert api_response.status_code == 422
    message = api_response.json()['error']['message']
    assert 'otro-proyecto-999' in message
    assert TEST_KEY['project_id'] in message


def test_without_a_file_the_message_says_how_to_send_it(client):
    api_response = client.post(
        '/api/v1/credentials', {'project_id': TEST_KEY['project_id']}, format='json'
    )

    assert api_response.status_code == 422
    assert 'key_file' in api_response.json()['error']['message']


# --- Comprobación -----------------------------------------------------------


def _create(client):
    return client.post(
        '/api/v1/credentials',
        {'project_id': TEST_KEY['project_id'], 'key_file': json.dumps(TEST_KEY)},
        format='json',
    ).json()['id']


def _verify(credential_id, double, monkeypatch):
    import apps.credentials.services as services

    original = services.verify_credential

    def wrapped(credential, *, client=None):
        return original(credential, client=double)

    monkeypatch.setattr(services, 'verify_credential', wrapped)
    monkeypatch.setattr('apps.credentials.api.verify_credential', wrapped)
    return credential_id


@pytest.mark.parametrize(
    ('fixture', 'expected_code'),
    [
        ('error_api_no_habilitada', 'API_NOT_ENABLED'),
        ('error_permiso_denegado', 'NO_PROPERTIES'),
        ('error_propiedad_inexistente', 'NO_PROPERTIES'),
        ('error_google_caido', 'PROVIDER_UNAVAILABLE'),
    ],
)
def test_the_verification_failures_are_distinguished_from_each_other(
    client, monkeypatch, fixture, expected_code
):
    """
    Cada motivo se arregla en una pantalla distinta de Google.

    Mezclarlos en un «no se pudo verificar» deja a la persona probando las cinco
    cosas, que es exactamente el problema que este producto viene a sacar.
    """
    credential_id = _create(client)
    double = FakeClient(list_sites=http_error(fixture))
    _verify(credential_id, double, monkeypatch)

    data = client.post(f'/api/v1/credentials/{credential_id}/verify').json()

    assert data['ok'] is False
    assert data['error_code'] == expected_code


def test_a_valid_key_without_authorized_properties_is_not_the_same_as_a_broken_key(
    client, monkeypatch
):
    """
    La distinción decisiva del recorrido de configuración.

    La clave funciona: lo que falta es autorizarla en Search Console. Decir «la
    clave no sirve» mandaría a la persona a crear otra, que tampoco va a servir.
    """
    credential_id = _create(client)
    double = FakeClient(list_sites=response('sites_list_vacio'))
    _verify(credential_id, double, monkeypatch)

    data = client.post(f'/api/v1/credentials/{credential_id}/verify').json()

    assert data['error_code'] == 'NO_PROPERTIES'
    assert 'propietario' in data['hint']
    assert TEST_KEY['client_email'] in data['hint']


def test_a_working_credential_ends_up_verified_and_lists_the_properties(client, monkeypatch):
    credential_id = _create(client)
    double = FakeClient(list_sites=response('sites_list'))
    _verify(credential_id, double, monkeypatch)

    data = client.post(f'/api/v1/credentials/{credential_id}/verify').json()

    assert data['ok'] is True
    assert data['status'] == CredentialStatus.VERIFIED
    assert len(data['properties']) == 2
    assert data['properties'][0]['property_uri'] == 'sc-domain:ejemplo.test'


def test_google_being_down_does_not_mark_the_credential_as_invalid(client, monkeypatch):
    """
    Un corte de red no dice nada malo de la credencial.

    Marcarla inválida obligaría a volver a cargarla sin que hubiera nada roto.
    """
    credential_id = _create(client)
    double = FakeClient(list_sites=http_error('error_google_caido'))
    _verify(credential_id, double, monkeypatch)

    client.post(f'/api/v1/credentials/{credential_id}/verify')

    credential = Credential.objects.get(id=credential_id)
    assert credential.status == CredentialStatus.UNVERIFIED


def test_replacing_a_credential_deactivates_the_previous_one_and_does_not_touch_the_domains(
    client, account
):
    from tests.factories import create_domain

    _create(client)
    domain = create_domain(account)
    previous_state = (domain.access_state, domain.updated_at)

    _create(client)

    assert ModuleCredential.objects.filter(account=account, is_active=True).count() == 1
    assert Credential.objects.filter(account=account).count() == 2

    domain.refresh_from_db()
    assert (domain.access_state, domain.updated_at) == previous_state


def test_a_credential_created_by_another_profile_is_shared(client):
    foreign = create_account(email='ajena@ejemplo.test')
    from tests.factories import create_credential

    credential = create_credential(foreign)

    api_response = client.post(f'/api/v1/credentials/{credential.id}/verify')

    assert api_response.status_code == 200
