"""
Pruebas de autorizacion.

Antes de esto solo habia autenticacion: `IsAuthenticated` responde a "¿tienes
una credencial valida?", asi que cualquier cliente con una clave podia enviar
por los tres canales y leer los envios de los demas.

Los dos casos que motivaron el cambio, comprobados sobre dos clientes
distintos de verdad:

* un cliente podia consultar el envio de otro con solo tener su UUID, que
  viaja en la respuesta del envio, en los webhooks y en los registros;
* `NotificationMessage.api_key` quedaba siempre a NULL, porque el token no
  guardaba relacion con la clave y `request.api_key_record` no llegaba nunca
  a las vistas de envio. Sin propietario, ni siquiera habia con que filtrar.
"""

from __future__ import annotations

import pytest
from django.contrib.auth import get_user_model
from django.urls import reverse
from rest_framework.test import APIClient

from authentication.models.account import Account
from authentication.models.roles import Roles
from keys.models.access_token import AccessToken
from keys.models.api_keys import (
    SCOPE_MAIL_SEND,
    SCOPE_MESSAGES_READ,
    SCOPE_SMS_SEND,
    SCOPE_WHATSAPP_SEND,
    ApiKeys,
)
from notifications.models.message import NotificationMessage

User = get_user_model()
pytestmark = pytest.mark.django_db

CORREO = {
    "from_email": "avisos@ejemplo.com",
    "to": ["destino@ejemplo.com"],
    "subject": "Asunto",
    "html": "<p>x</p>",
    "send_async": False,
}


def crear_cliente(nombre: str, **kwargs) -> tuple[ApiKeys, APIClient]:
    """Cuenta, usuario y clave propios: un cliente distinto de verdad."""
    usuario = User.objects.create_user(username=nombre, password="x" * 20)
    rol, _ = Roles.objects.get_or_create(description="Sistema")
    cuenta = Account.objects.create(name=nombre, active=True, rol=rol, user=usuario)
    clave, _ = ApiKeys.issue(cuenta, active=True, **kwargs)
    return clave, APIClient()


def autenticado(cliente: APIClient, clave: ApiKeys) -> APIClient:
    _, token = AccessToken.issue(clave)
    cliente.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
    return cliente


# ---------------------------------------------------------------------------
# Aislamiento entre clientes
# ---------------------------------------------------------------------------


def test_el_envio_registra_de_quien_es():
    """Quedaba a NULL: el token no sabia con que clave se habia pedido."""
    clave, api = crear_cliente("dependencia-a")
    respuesta = autenticado(api, clave).post(
        reverse("v1:mail-send"), CORREO, format="json"
    )
    assert respuesta.status_code == 200

    mensaje = NotificationMessage.objects.get(pk=respuesta.json()["message_id"])
    assert mensaje.api_key_id == clave.pk


def test_un_cliente_no_ve_los_envios_de_otro():
    """Esto devolvia 200 con asunto, destinatarios y remitente a la vista."""
    clave_a, api_a = crear_cliente("dependencia-a")
    clave_b, api_b = crear_cliente("dependencia-b")

    envio = autenticado(api_a, clave_a).post(
        reverse("v1:mail-send"), CORREO, format="json"
    )
    uuid = envio.json()["message_id"]

    respuesta = autenticado(api_b, clave_b).get(
        reverse("v1:message-status", kwargs={"pk": uuid})
    )
    # 404 y no 403: distinguir "existe pero no es tuyo" de "no existe" permite
    # sondear identificadores ajenos.
    assert respuesta.status_code == 404


def test_un_cliente_si_ve_sus_propios_envios():
    clave, api = crear_cliente("dependencia-a")
    envio = autenticado(api, clave).post(
        reverse("v1:mail-send"), CORREO, format="json"
    )
    uuid = envio.json()["message_id"]

    respuesta = autenticado(api, clave).get(
        reverse("v1:message-status", kwargs={"pk": uuid})
    )
    assert respuesta.status_code == 200
    assert respuesta.json()["subject"] == "Asunto"


def test_el_endpoint_heredado_de_correo_filtra_igual():
    """`/mail/<uuid>` sigue existiendo por compatibilidad y tenia el mismo fallo."""
    clave_a, api_a = crear_cliente("dependencia-a")
    clave_b, api_b = crear_cliente("dependencia-b")

    envio = autenticado(api_a, clave_a).post(
        reverse("v1:mail-send"), CORREO, format="json"
    )
    uuid = envio.json()["message_id"]

    respuesta = autenticado(api_b, clave_b).get(
        reverse("v1:mail-status", kwargs={"pk": uuid})
    )
    assert respuesta.status_code == 404


def test_rotar_la_clave_no_esconde_el_historial():
    """El filtro es por cuenta, no por clave: rotar no debe borrar el pasado."""
    clave, api = crear_cliente("dependencia-a")
    envio = autenticado(api, clave).post(
        reverse("v1:mail-send"), CORREO, format="json"
    )
    uuid = envio.json()["message_id"]

    clave.rotate()
    respuesta = autenticado(api, clave).get(
        reverse("v1:message-status", kwargs={"pk": uuid})
    )
    assert respuesta.status_code == 200


# ---------------------------------------------------------------------------
# Alcances
# ---------------------------------------------------------------------------

ENVIOS = [
    ("v1:mail-send", CORREO, SCOPE_MAIL_SEND),
    ("v1:sms-send", {"to": ["+525512345678"], "body": "hola"}, SCOPE_SMS_SEND),
    (
        "v1:whatsapp-send",
        {
            "to": ["+525512345678"],
            "vars": {"1": "Ana"},
            "template": "HX" + "a" * 32,
        },
        SCOPE_WHATSAPP_SEND,
    ),
]


@pytest.mark.parametrize(("ruta", "cuerpo", "scope"), ENVIOS)
def test_con_el_alcance_correcto_se_envia(ruta, cuerpo, scope):
    clave, api = crear_cliente(f"cliente-{scope}", scopes=[scope])
    respuesta = autenticado(api, clave).post(reverse(ruta), cuerpo, format="json")
    assert respuesta.status_code in (200, 202)


@pytest.mark.parametrize(("ruta", "cuerpo", "scope"), ENVIOS)
def test_sin_el_alcance_se_rechaza(ruta, cuerpo, scope):
    """Una clave para SMS ya no sirve para enviar correo del dominio."""
    otros = [s for _, _, s in ENVIOS if s != scope]
    clave, api = crear_cliente(f"cliente-sin-{scope}", scopes=otros)
    respuesta = autenticado(api, clave).post(reverse(ruta), cuerpo, format="json")
    assert respuesta.status_code == 403


def test_sin_alcances_no_se_puede_hacer_nada():
    """Una lista vacia no autoriza: falla cerrado."""
    clave, api = crear_cliente("cliente-mudo", scopes=[])
    respuesta = autenticado(api, clave).post(
        reverse("v1:mail-send"), CORREO, format="json"
    )
    assert respuesta.status_code == 403


def test_el_comodin_autoriza_todo():
    clave, api = crear_cliente("cliente-admin", scopes=["*"])
    respuesta = autenticado(api, clave).post(
        reverse("v1:mail-send"), CORREO, format="json"
    )
    assert respuesta.status_code == 200


def test_consultar_exige_su_propio_alcance():
    """Poder enviar no implica poder leer la auditoria."""
    clave, api = crear_cliente("solo-envia", scopes=[SCOPE_MAIL_SEND])
    envio = autenticado(api, clave).post(
        reverse("v1:mail-send"), CORREO, format="json"
    )
    uuid = envio.json()["message_id"]

    respuesta = autenticado(api, clave).get(
        reverse("v1:message-status", kwargs={"pk": uuid})
    )
    assert respuesta.status_code == 403


def test_el_token_declara_los_alcances():
    """El cliente sabe que puede hacer sin tener que probarlo endpoint a endpoint."""
    usuario = User.objects.create_user(username="cliente-sms", password="x" * 20)
    rol, _ = Roles.objects.get_or_create(description="Sistema")
    cuenta = Account.objects.create(
        name="cliente-sms", active=True, rol=rol, user=usuario
    )
    _, raw = ApiKeys.issue(
        cuenta, active=True, scopes=[SCOPE_SMS_SEND, SCOPE_MESSAGES_READ]
    )

    respuesta = APIClient().get("/auth/v1/token/", HTTP_X_API_KEY=raw)
    assert respuesta.status_code == 200
    assert set(respuesta.json()["scopes"]) == {SCOPE_SMS_SEND, SCOPE_MESSAGES_READ}
    assert respuesta.json()["expires_at"]


# ---------------------------------------------------------------------------
# Remitentes por cliente
# ---------------------------------------------------------------------------


def test_la_politica_de_remitentes_del_cliente_se_aplica():
    """
    `allowed_senders` no se aplicaba nunca: `validate_sender` recibia siempre
    `None` porque la vista no sabia que clave la habia llamado.
    """
    clave, api = crear_cliente(
        "cliente-restringido", allowed_senders=["avisos@ejemplo.com"]
    )

    permitido = autenticado(api, clave).post(
        reverse("v1:mail-send"), CORREO, format="json"
    )
    assert permitido.status_code == 200

    prohibido = autenticado(api, clave).post(
        reverse("v1:mail-send"),
        {**CORREO, "from_email": "nomina@ejemplo.com"},
        format="json",
    )
    assert prohibido.status_code == 400
    assert "no tiene autorizado" in str(prohibido.json()).lower()


# ---------------------------------------------------------------------------
# Ciclo de vida del token
# ---------------------------------------------------------------------------


def test_revocar_la_clave_invalida_sus_tokens():
    """Antes el token sobrevivia a la revocacion hasta que alguien lo gastaba."""
    clave, api = crear_cliente("dependencia-a")
    _, token = AccessToken.issue(clave)
    clave.revoke()

    api.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
    respuesta = api.post(reverse("v1:mail-send"), CORREO, format="json")
    assert respuesta.status_code == 401


def test_un_token_caducado_no_autentica():
    """Antes no caducaban: uno emitido y nunca usado valia para siempre."""
    from django.utils import timezone

    clave, api = crear_cliente("dependencia-a")
    registro, token = AccessToken.issue(clave)
    registro.expires_at = timezone.now() - timezone.timedelta(seconds=1)
    registro.save(update_fields=["expires_at"])

    api.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
    respuesta = api.post(reverse("v1:mail-send"), CORREO, format="json")
    assert respuesta.status_code == 401


def test_el_token_de_un_cliente_no_sirve_para_suplantar_a_otro():
    """
    Con `get_or_create(user=...)`, dos claves de la misma cuenta compartian
    token. Ahora cada token pertenece a una clave concreta.
    """
    clave_a, _ = crear_cliente("dependencia-a")
    clave_b, api_b = crear_cliente("dependencia-b")

    envio = autenticado(APIClient(), clave_a).post(
        reverse("v1:mail-send"), CORREO, format="json"
    )
    mensaje = NotificationMessage.objects.get(pk=envio.json()["message_id"])
    assert mensaje.api_key.account_id == clave_a.account_id
    assert mensaje.api_key.account_id != clave_b.account_id
