"""Pruebas del cifrado de secretos y de la validación del CSD.

Nada sale a la red ni toca MinIO: la validación del par ``.cer``/``.key`` es criptografía
pura, y es justo la parte que importa comprobar.
"""

import datetime

import pytest
from cryptography import x509
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
from fastapi import HTTPException

from api.v1.modules.fin.issuer import csd_service
from core import crypto

CERT_NUMBER = "00001000000700000001"
PASSWORD = "12345678a"


def _par(cert_number: str = CERT_NUMBER, password: str = PASSWORD):
    """Genera un CSD de juguete con el formato del SAT: (cer_der, key_der_cifrada)."""
    key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
    nombre = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "EMISOR DE PRUEBA")])
    cert = (
        x509.CertificateBuilder()
        .subject_name(nombre)
        .issuer_name(nombre)
        .public_key(key.public_key())
        .serial_number(int.from_bytes(cert_number.encode("ascii"), "big"))
        .not_valid_before(datetime.datetime(2026, 1, 1))
        .not_valid_after(datetime.datetime(2035, 1, 1))
        .sign(key, hashes.SHA256())
    )
    return (
        cert.public_bytes(serialization.Encoding.DER),
        key.private_bytes(
            serialization.Encoding.DER,
            serialization.PrivateFormat.PKCS8,
            serialization.BestAvailableEncryption(password.encode()),
        ),
    )


# ---------------------------------------------------------------------------------------
# Cifrado de secretos
# ---------------------------------------------------------------------------------------
@pytest.fixture
def clave_maestra(monkeypatch):
    clave = Fernet.generate_key().decode()
    monkeypatch.setattr(crypto.settings, "CSD_ENCRYPTION_KEY", clave, raising=False)
    return clave


def test_cifrar_y_recuperar(clave_maestra):
    token = crypto.encrypt_secret(PASSWORD)
    assert token != PASSWORD, "el secreto no puede quedar en claro"
    assert crypto.decrypt_secret(token) == PASSWORD


def test_dos_cifrados_del_mismo_valor_no_son_iguales(clave_maestra):
    """Fernet incluye IV y timestamp: dos tokens distintos para el mismo dato.

    Importa porque si fueran iguales, comparar columnas revelaría qué empresas comparten
    contraseña.
    """
    assert crypto.encrypt_secret(PASSWORD) != crypto.encrypt_secret(PASSWORD)


def test_sin_clave_maestra_no_se_cifra(monkeypatch):
    monkeypatch.setattr(crypto.settings, "CSD_ENCRYPTION_KEY", "", raising=False)
    assert crypto.secrets_available() is False
    with pytest.raises(crypto.SecretsNotConfigured):
        crypto.encrypt_secret(PASSWORD)


def test_clave_maestra_invalida_se_detecta(monkeypatch):
    monkeypatch.setattr(crypto.settings, "CSD_ENCRYPTION_KEY", "no-es-una-clave", raising=False)
    with pytest.raises(crypto.SecretsNotConfigured):
        crypto.encrypt_secret(PASSWORD)


def test_otra_clave_maestra_no_descifra(clave_maestra, monkeypatch):
    """Rotar la clave maestra deja ilegibles los secretos: tiene que decirlo, no romperse raro."""
    token = crypto.encrypt_secret(PASSWORD)
    monkeypatch.setattr(
        crypto.settings, "CSD_ENCRYPTION_KEY", Fernet.generate_key().decode(), raising=False
    )
    with pytest.raises(crypto.SecretDecryptionError):
        crypto.decrypt_secret(token)


def test_no_se_cifra_un_valor_vacio(clave_maestra):
    with pytest.raises(ValueError):
        crypto.encrypt_secret("")


# ---------------------------------------------------------------------------------------
# Validación del par .cer / .key
# ---------------------------------------------------------------------------------------
def test_par_correcto_devuelve_numero_de_certificado():
    cer, key = _par()
    assert csd_service._verify_pair(cer, key, PASSWORD) == CERT_NUMBER


def test_key_de_otro_certificado_se_rechaza():
    """El caso que motiva la validación: dos CSD mezclados.

    Sin esto, el error aparecería hasta que el PAC rechace el comprobante, con un mensaje que
    no menciona el certificado.
    """
    cer, _ = _par()
    _, key_ajena = _par(cert_number="00001000000700000002")
    with pytest.raises(HTTPException) as exc:
        csd_service._verify_pair(cer, key_ajena, PASSWORD)
    assert exc.value.status_code == 422
    assert "no corresponde" in str(exc.value.detail)


def test_contrasena_incorrecta_se_rechaza():
    cer, key = _par()
    with pytest.raises(HTTPException) as exc:
        csd_service._verify_pair(cer, key, "incorrecta")
    assert exc.value.status_code == 422


def test_certificado_que_no_es_x509_se_rechaza():
    _, key = _par()
    with pytest.raises(HTTPException) as exc:
        csd_service._verify_pair(b"esto no es un certificado", key, PASSWORD)
    assert exc.value.status_code == 422


def test_llave_que_no_es_una_llave_se_rechaza():
    cer, _ = _par()
    with pytest.raises(HTTPException) as exc:
        csd_service._verify_pair(cer, b"esto no es una llave", PASSWORD)
    assert exc.value.status_code == 422
