"""
Autenticación de la API por clave.

Lo que se verifica es que los tres rechazos —falta, no es válida, fue
revocada— den 401 y se distingan por su mensaje. Un cliente que integra desde un
pipeline necesita saber si tiene que generar una clave nueva o si simplemente la
escribió mal, y un 401 sin más deja las dos posibilidades abiertas.
"""

import pytest
from rest_framework.test import APIClient

from apps.accounts.models import ApiKey
from tests.factories import create_account

pytestmark = pytest.mark.django_db

ENDPOINT = '/api/v1/account'


@pytest.fixture
def client():
    return APIClient()


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


def test_without_a_key_it_responds_401_with_the_code_from_the_contract(client):
    response = client.get(ENDPOINT)

    assert response.status_code == 401
    assert response.json()['error']['code'] == 'UNAUTHENTICATED'


def test_a_valid_key_responds_200(client, account):
    _, plain_text = ApiKey.issue(account=account, name='pipeline')
    client.credentials(HTTP_AUTHORIZATION=f'Api-Key {plain_text}')

    response = client.get(ENDPOINT)

    assert response.status_code == 200
    assert response.json()['can_operate'] is False


def test_a_key_whose_secret_carries_an_underscore_still_works(client, account):
    """
    Regresión: el secreto va en base64 url-safe y ese alfabeto incluye «_».

    Partir el texto por todos los guiones bajos rechazaba, al azar, buena parte
    de las claves emitidas: fallaban las que por sorteo tenían uno adentro.
    """
    key = ApiKey.objects.create(
        account=account,
        name='con guion bajo',
        prefix='aabbccdd',
        hashed_key=ApiKey.hash_key('irk_aabbccdd_un_secreto_con_guiones_bajos'),
    )
    client.credentials(HTTP_AUTHORIZATION='Api-Key irk_aabbccdd_un_secreto_con_guiones_bajos')

    assert client.get(ENDPOINT).status_code == 200
    assert ApiKey.split('irk_aabbccdd_un_secreto_con_guiones_bajos') == key.prefix


def test_a_made_up_key_responds_401(client, account):
    client.credentials(HTTP_AUTHORIZATION='Api-Key irk_deadbeef_estonoexiste')

    response = client.get(ENDPOINT)

    assert response.status_code == 401
    assert 'no es válida' in response.json()['error']['message']


def test_a_key_with_another_format_is_distinguished_from_an_invalid_one(client):
    client.credentials(HTTP_AUTHORIZATION='Api-Key esto-no-tiene-forma-de-clave')

    response = client.get(ENDPOINT)

    assert response.status_code == 401
    assert 'formato' in response.json()['error']['message']


def test_a_revoked_key_responds_401_and_says_so(client, account):
    """
    El mensaje importa: revocada y equivocada se arreglan distinto.

    Con «no es válida» para las dos, quien integra revisa si copió mal la clave
    en vez de emitir una nueva, que es lo que hace falta.
    """
    key, plain_text = ApiKey.issue(account=account, name='vieja')
    key.revoke()
    client.credentials(HTTP_AUTHORIZATION=f'Api-Key {plain_text}')

    response = client.get(ENDPOINT)

    assert response.status_code == 401
    assert 'revocada' in response.json()['error']['message']


def test_the_key_of_a_deactivated_account_does_not_work(client, account):
    _, plain_text = ApiKey.issue(account=account, name='pipeline')
    account.is_active = False
    account.save()
    client.credentials(HTTP_AUTHORIZATION=f'Api-Key {plain_text}')

    assert client.get(ENDPOINT).status_code == 401


def test_a_badly_built_header_says_so(client):
    client.credentials(HTTP_AUTHORIZATION='Api-Key')

    response = client.get(ENDPOINT)

    assert response.status_code == 401
    assert 'Api-Key <clave>' in response.json()['error']['message']


def test_the_plaintext_key_is_not_stored_anywhere(account):
    """
    El valor completo existe una sola vez, en la respuesta que lo devuelve.

    Lo que queda en la base es su hash. Guardar algo reversible sería exponerse
    sin ganar nada: acá nunca hace falta recuperar el valor, sólo compararlo.
    """
    key, plain_text = ApiKey.issue(account=account, name='pipeline')
    key.refresh_from_db()

    assert plain_text not in key.hashed_key
    assert key.hashed_key != plain_text
    assert key.matches(plain_text)
    assert not key.matches(plain_text + 'x')


def test_the_usage_is_recorded_in_a_deferred_way(client, account):
    """
    La marca de último uso no se escribe en cada petición.

    La API se llama muchas veces seguidas desde un pipeline: una escritura por
    llamada agregaría carga para ganar una precisión que a nadie le sirve.
    """
    key, plain_text = ApiKey.issue(account=account, name='pipeline')
    client.credentials(HTTP_AUTHORIZATION=f'Api-Key {plain_text}')

    client.get(ENDPOINT)
    key.refresh_from_db()
    first_mark = key.last_used_at
    assert first_mark is not None

    client.get(ENDPOINT)
    key.refresh_from_db()
    assert key.last_used_at == first_mark
