"""Tests for the cryptographic operations module."""

from __future__ import annotations

import time

import pytest
from cryptography import x509
from cryptography.hazmat.primitives.asymmetric import ec, rsa

from ecp_auth.crypto import (
    Challenge,
    decode_signature_b64,
    encode_signature_b64,
    generate_challenge,
    get_certificate_fingerprint,
    sign_data,
    verify_challenge_not_expired,
    verify_signature,
)
from ecp_auth.exceptions import (
    ChallengeError,
    ChallengeExpiredError,
    SignatureVerificationError,
    UnsupportedAlgorithmError,
)


class TestChallengeGeneration:
    """Tests for challenge nonce generation."""

    def test_generate_challenge(self) -> None:
        """Test generating a challenge produces valid nonce."""
        challenge = generate_challenge()

        assert isinstance(challenge, Challenge)
        assert len(challenge.nonce) == 64  # 32 bytes hex
        assert len(challenge.nonce_bytes) == 32
        assert challenge.timeout > 0
        assert challenge.created_at > 0

    def test_challenge_uniqueness(self) -> None:
        """Test that consecutive challenges are unique."""
        c1 = generate_challenge()
        c2 = generate_challenge()

        assert c1.nonce != c2.nonce

    def test_challenge_not_expired(self) -> None:
        """Test that a fresh challenge is not expired."""
        challenge = generate_challenge()
        assert challenge.is_expired is False

    def test_challenge_serialization(self) -> None:
        """Test challenge to/from dict serialization."""
        challenge = generate_challenge()
        data = challenge.to_dict()

        assert "nonce" in data
        assert "created_at" in data
        assert "timeout" in data

        restored = Challenge.from_dict(data)
        assert restored.nonce == challenge.nonce
        assert restored.timeout == challenge.timeout

    def test_challenge_from_invalid_dict(self) -> None:
        """Test that invalid dict raises ChallengeError."""
        with pytest.raises(ChallengeError):
            Challenge.from_dict({})


class TestChallengeExpiration:
    """Tests for challenge expiration checking."""

    def test_valid_challenge(self) -> None:
        """Test that a fresh challenge passes expiration check."""
        challenge = generate_challenge()
        verify_challenge_not_expired(challenge)  # Should not raise

    def test_expired_challenge(self) -> None:
        """Test that an expired challenge raises ChallengeExpiredError."""
        challenge = Challenge(
            nonce="aabb",
            nonce_bytes=bytes.fromhex("aabb"),
            created_at=time.time() - 1000,
            timeout=1,
        )

        with pytest.raises(ChallengeExpiredError):
            verify_challenge_not_expired(challenge)


class TestSignAndVerify:
    """Tests for digital signature creation and verification."""

    def test_sign_and_verify_rsa(
        self,
        rsa_private_key: rsa.RSAPrivateKey,
        self_signed_certificate: x509.Certificate,
    ) -> None:
        """Test RSA signing and verification round-trip."""
        data = b"test challenge data"
        signature = sign_data(rsa_private_key, data, "sha256")

        assert len(signature) > 0
        result = verify_signature(
            self_signed_certificate, signature, data, "sha256"
        )
        assert result is True

    def test_sign_and_verify_ec(
        self,
        ec_private_key: ec.EllipticCurvePrivateKey,
        ec_certificate: x509.Certificate,
    ) -> None:
        """Test ECDSA signing and verification round-trip."""
        data = b"test challenge data"
        signature = sign_data(ec_private_key, data, "sha256")

        assert len(signature) > 0
        result = verify_signature(ec_certificate, signature, data, "sha256")
        assert result is True

    def test_verify_wrong_data(
        self,
        rsa_private_key: rsa.RSAPrivateKey,
        self_signed_certificate: x509.Certificate,
    ) -> None:
        """Test that verification fails with wrong data."""
        data = b"original data"
        signature = sign_data(rsa_private_key, data, "sha256")

        with pytest.raises(SignatureVerificationError):
            verify_signature(
                self_signed_certificate, signature, b"tampered data", "sha256"
            )

    def test_sign_sha384(
        self,
        rsa_private_key: rsa.RSAPrivateKey,
        self_signed_certificate: x509.Certificate,
    ) -> None:
        """Test SHA-384 signing and verification."""
        data = b"sha384 test"
        signature = sign_data(rsa_private_key, data, "sha384")
        result = verify_signature(
            self_signed_certificate, signature, data, "sha384"
        )
        assert result is True

    def test_sign_sha512(
        self,
        rsa_private_key: rsa.RSAPrivateKey,
        self_signed_certificate: x509.Certificate,
    ) -> None:
        """Test SHA-512 signing and verification."""
        data = b"sha512 test"
        signature = sign_data(rsa_private_key, data, "sha512")
        result = verify_signature(
            self_signed_certificate, signature, data, "sha512"
        )
        assert result is True

    def test_unsupported_hash_algorithm(
        self, rsa_private_key: rsa.RSAPrivateKey
    ) -> None:
        """Test that unsupported hash algorithm raises error."""
        with pytest.raises(UnsupportedAlgorithmError):
            sign_data(rsa_private_key, b"data", "md5")


class TestBase64Encoding:
    """Tests for Base64 signature encoding/decoding."""

    def test_encode_decode_roundtrip(self) -> None:
        """Test Base64 encode/decode round-trip."""
        original = b"\x00\x01\x02\xff\xfe\xfd"
        encoded = encode_signature_b64(original)
        decoded = decode_signature_b64(encoded)

        assert decoded == original

    def test_decode_invalid_b64(self) -> None:
        """Test decoding invalid Base64 raises error."""
        with pytest.raises(SignatureVerificationError):
            decode_signature_b64("not!valid!base64!!!")


class TestCertificateFingerprint:
    """Tests for certificate fingerprint calculation."""

    def test_fingerprint_consistency(
        self, self_signed_certificate: x509.Certificate
    ) -> None:
        """Test that fingerprint is consistent for the same cert."""
        fp1 = get_certificate_fingerprint(self_signed_certificate)
        fp2 = get_certificate_fingerprint(self_signed_certificate)

        assert fp1 == fp2
        assert len(fp1) == 64  # SHA-256 hex
