"""
Validación del archivo de clave, con los archivos equivocados más frecuentes.

Todos estos casos vienen de la misma zona de la consola de Google y se parecen
entre sí. Lo que se prueba no es que fallen —eso es fácil— sino que **cada uno
falle diciendo qué bajó de más**. Un «archivo inválido» para los cuatro deja a
la persona probando de nuevo al azar.

Lo que se afirma es el **código** y los datos que la oración necesita, nunca la
oración. Comparar prosa ataba la suite al idioma que tenía el servidor: hoy la
frase la arma el catálogo del cliente, y el mismo rechazo se lee distinto en
inglés y en español sin que nada de esto cambie. Que cada código tenga su texto
en los dos catálogos lo comprueba `tests/unit/test_i18n.py`.
"""

import json

import pytest

from apps.credentials.validation import (
    InvalidKeyFile,
    KeyFileError,
    validate_key_file,
    validate_project_id,
)
from tests.factories import TEST_KEY, key_json


def test_a_correct_key_returns_its_publishable_data():
    validated = validate_key_file(key_json())

    assert validated.project_id == TEST_KEY['project_id']
    assert validated.client_email == TEST_KEY['client_email']
    assert validated.private_key_id == TEST_KEY['private_key_id']
    assert validated.fingerprint


def test_the_fingerprint_does_not_contain_the_key():
    validated = validate_key_file(key_json())
    assert 'PRIVATE KEY' not in validated.fingerprint
    assert TEST_KEY['private_key'] not in validated.fingerprint


def test_an_api_key_is_recognized_and_it_warns_that_it_was_exposed():
    with pytest.raises(InvalidKeyFile) as exc:
        validate_key_file('AIzaSyA' + 'b' * 32)

    # El código es propio y no el genérico: es el que hace que el texto pueda
    # decir que esa clave quedó expuesta y conviene rotarla.
    assert exc.value.code == KeyFileError.FILE_IS_API_KEY


def test_a_desktop_application_credential_is_distinguished():
    file = json.dumps({'installed': {'client_id': 'x', 'client_secret': 'y'}})

    with pytest.raises(InvalidKeyFile) as exc:
        validate_key_file(file)

    assert exc.value.code == KeyFileError.FILE_IS_OAUTH_CLIENT_DESKTOP


def test_a_web_application_credential_is_distinguished_from_the_desktop_one():
    """
    Son dos códigos y no uno con el tipo como dato: «de escritorio» y «web» se
    declinan distinto en cada idioma, y un parámetro obligaría a pegarlos donde
    el inglés los pone.
    """
    file = json.dumps({'web': {'client_id': 'x', 'client_secret': 'y'}})

    with pytest.raises(InvalidKeyFile) as exc:
        validate_key_file(file)

    assert exc.value.code == KeyFileError.FILE_IS_OAUTH_CLIENT_WEB


def test_a_gcloud_user_credential_is_distinguished():
    file = json.dumps({'type': 'authorized_user', 'client_id': 'x', 'refresh_token': 'y'})

    with pytest.raises(InvalidKeyFile) as exc:
        validate_key_file(file)

    assert exc.value.code == KeyFileError.FILE_IS_USER_CREDENTIAL


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

    with pytest.raises(InvalidKeyFile) as exc:
        validate_key_file(json.dumps(data).encode())

    assert exc.value.code == KeyFileError.FIELD_MISSING
    assert exc.value.params['field'] == 'client_email'
    assert exc.value.field == 'client_email'


def test_a_json_missing_several_fields_lists_them():
    with pytest.raises(InvalidKeyFile) as exc:
        validate_key_file(json.dumps({'type': 'service_account'}).encode())

    assert exc.value.code == KeyFileError.FIELDS_MISSING
    assert 'project_id' in exc.value.params['fields']
    assert 'private_key' in exc.value.params['fields']


def test_a_truncated_file_says_that_it_is_not_json_and_where():
    with pytest.raises(InvalidKeyFile) as exc:
        validate_key_file('{"type": "service_account", "project_id":')

    assert exc.value.code == KeyFileError.FILE_NOT_JSON
    # La línea viaja como dato: sin ella el mensaje dice que el archivo está mal
    # pero no dónde mirarlo.
    assert exc.value.params['line'] == 1


def test_an_unreadable_file_does_not_break_with_an_encoding_error():
    with pytest.raises(InvalidKeyFile) as exc:
        validate_key_file(b'\xff\xfe\x00\x01binario')

    assert exc.value.code == KeyFileError.FILE_NOT_TEXT


def test_a_json_that_is_not_an_object_says_what_type_it_found():
    with pytest.raises(InvalidKeyFile) as exc:
        validate_key_file('[1, 2, 3]')

    assert exc.value.code == KeyFileError.FILE_NOT_OBJECT
    assert exc.value.params['type'] == 'list'


def test_a_different_type_says_which_one_it_found_and_which_one_it_expected():
    with pytest.raises(InvalidKeyFile) as exc:
        validate_key_file(key_json(type='external_account'))

    assert exc.value.code == KeyFileError.TYPE_MISMATCH
    assert exc.value.params['type'] == 'external_account'
    assert exc.value.params['expected'] == 'service_account'


def test_a_private_key_that_is_not_a_key_is_rejected():
    with pytest.raises(InvalidKeyFile) as exc:
        validate_key_file(key_json(private_key='pegué cualquier cosa'))

    assert exc.value.code == KeyFileError.PRIVATE_KEY_INVALID
    assert exc.value.field == 'private_key'


# --- Identificador de proyecto ---------------------------------------------


def test_a_valid_project_identifier_passes():
    assert validate_project_id(' mi-proyecto-123456 ') == 'mi-proyecto-123456'


def test_the_project_name_instead_of_the_identifier_is_recognized():
    """
    El error más frecuente del paso uno.

    El nombre lleva mayúsculas y espacios; el identificador no. Tiene código
    propio para que el texto pueda decirlo con esas palabras y evitar el ida y
    vuelta de «pero si lo copié de Google».
    """
    with pytest.raises(InvalidKeyFile) as exc:
        validate_project_id('Mi Proyecto')

    assert exc.value.code == KeyFileError.PROJECT_ID_UPPERCASE


def test_an_empty_identifier_is_rejected_indicating_where_to_find_it():
    with pytest.raises(InvalidKeyFile) as exc:
        validate_project_id('')

    assert exc.value.code == KeyFileError.PROJECT_ID_MISSING
    assert exc.value.field == 'project_id'


@pytest.mark.parametrize('value', ['ab', '1proyecto', 'proyecto_con_guion_bajo', 'x' * 40])
def test_identifiers_with_an_invalid_shape_are_rejected(value):
    with pytest.raises(InvalidKeyFile) as exc:
        validate_project_id(value)

    assert exc.value.code == KeyFileError.PROJECT_ID_MALFORMED
