import json
from pathlib import Path

import pytest
from mrp.auth import Keypair, _b64url_decode, _b64url_encode
from mrp.crypto import (
    decrypt_message,
    ed25519_to_x25519_private,
    ed25519_to_x25519_public,
    encrypt_message,
)


class TestKeyConversion:
    def test_ed25519_to_x25519_public(self):
        kp = Keypair.generate()
        x_pub = ed25519_to_x25519_public(kp.public_key)
        # X25519 public key should be 32 bytes
        from cryptography.hazmat.primitives import serialization
        raw = x_pub.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
        assert len(raw) == 32

    def test_ed25519_to_x25519_private(self):
        kp = Keypair.generate()
        x_priv = ed25519_to_x25519_private(kp.seed)
        # Should produce a valid X25519 private key
        from cryptography.hazmat.primitives import serialization
        raw = x_priv.private_bytes(
            serialization.Encoding.Raw,
            serialization.PrivateFormat.Raw,
            serialization.NoEncryption(),
        )
        assert len(raw) == 32

    def test_different_keys_different_x25519(self):
        kp1 = Keypair.generate()
        kp2 = Keypair.generate()
        from cryptography.hazmat.primitives import serialization
        x1 = ed25519_to_x25519_public(kp1.public_key).public_bytes(
            serialization.Encoding.Raw, serialization.PublicFormat.Raw
        )
        x2 = ed25519_to_x25519_public(kp2.public_key).public_bytes(
            serialization.Encoding.Raw, serialization.PublicFormat.Raw
        )
        assert x1 != x2


class TestEncryptDecrypt:
    def test_round_trip(self):
        sender = Keypair.generate()
        recipient = Keypair.generate()
        plaintext = b'{"message": "hello"}'

        encrypted = encrypt_message(
            sender.seed,
            recipient.public_key,
            plaintext,
            "application/json",
        )

        # Verify encrypted body has v2 fields
        assert encrypted["v"] == 2
        assert "enc" in encrypted
        assert "ct" in encrypted
        assert encrypted["ct_content_type"] == "application/json"

        # Decrypt
        decrypted, ct = decrypt_message(
            recipient.seed,
            sender.public_key,
            encrypted,
        )
        assert decrypted == plaintext
        assert ct == "application/json"

    def test_decrypt_with_wrong_key_fails(self):
        sender = Keypair.generate()
        recipient = Keypair.generate()
        wrong = Keypair.generate()

        encrypted = encrypt_message(
            sender.seed,
            recipient.public_key,
            b"secret",
            "text/plain",
        )

        with pytest.raises(Exception):
            decrypt_message(wrong.seed, sender.public_key, encrypted)

    def test_different_encryptions_differ(self):
        sender = Keypair.generate()
        recipient = Keypair.generate()
        plaintext = b"same message"

        e1 = encrypt_message(sender.seed, recipient.public_key, plaintext)
        e2 = encrypt_message(sender.seed, recipient.public_key, plaintext)

        # Different enc values -> different ciphertexts
        assert e1["enc"] != e2["enc"]
        assert e1["ct"] != e2["ct"]

    def test_large_message(self):
        sender = Keypair.generate()
        recipient = Keypair.generate()
        plaintext = b"x" * 100_000

        encrypted = encrypt_message(sender.seed, recipient.public_key, plaintext)
        decrypted, _ = decrypt_message(recipient.seed, sender.public_key, encrypted)
        assert decrypted == plaintext

    def test_empty_message(self):
        sender = Keypair.generate()
        recipient = Keypair.generate()

        encrypted = encrypt_message(sender.seed, recipient.public_key, b"")
        decrypted, _ = decrypt_message(recipient.seed, sender.public_key, encrypted)
        assert decrypted == b""

    def test_unsupported_version(self):
        sender = Keypair.generate()
        recipient = Keypair.generate()

        encrypted = encrypt_message(sender.seed, recipient.public_key, b"test")
        encrypted["v"] = 99

        with pytest.raises(ValueError, match="Unsupported encryption version"):
            decrypt_message(recipient.seed, sender.public_key, encrypted)


class TestCrossSDKVectors:
    @pytest.fixture
    def fixture(self):
        fixture_path = Path(__file__).parent / ".." / ".." / "testdata" / "crypto_test_vectors.json"
        return json.loads(fixture_path.read_text())

    def test_decrypt_basic_json(self, fixture):
        self._decrypt_vector(fixture, "basic_json")

    def test_decrypt_empty_message(self, fixture):
        self._decrypt_vector(fixture, "empty_message")

    def test_decrypt_binary_payload(self, fixture):
        self._decrypt_vector(fixture, "binary_payload")

    def test_wrong_sender_fails(self, fixture):
        """Decryption must fail with a wrong sender key."""
        recipient_seed = bytes.fromhex(fixture["recipient_seed_hex"])
        wrong = Keypair.generate()

        for vec in fixture["vectors"]:
            with pytest.raises(Exception):
                decrypt_message(
                    recipient_seed, wrong.public_key, vec["encrypted_body"],
                )

    def _decrypt_vector(self, fixture, name):
        recipient_seed = bytes.fromhex(fixture["recipient_seed_hex"])
        sender_pub = _b64url_decode(fixture["sender_public_key_b64url"])

        vec = next(v for v in fixture["vectors"] if v["name"] == name)
        expected_plaintext = _b64url_decode(vec["plaintext_b64url"]) if vec["plaintext_b64url"] else b""

        decrypted, ct = decrypt_message(
            recipient_seed, sender_pub, vec["encrypted_body"],
        )
        assert decrypted == expected_plaintext
        assert ct == vec["expected_content_type"]


class TestCiphertextTampering:
    def test_tampered_ciphertext(self):
        sender = Keypair.generate()
        recipient = Keypair.generate()
        encrypted = encrypt_message(sender.seed, recipient.public_key, b"secret")
        ct_bytes = bytearray(_b64url_decode(encrypted["ct"]))
        ct_bytes[0] ^= 0xFF
        encrypted["ct"] = _b64url_encode(bytes(ct_bytes))
        with pytest.raises(Exception):
            decrypt_message(recipient.seed, sender.public_key, encrypted)

    def test_tampered_enc(self):
        sender = Keypair.generate()
        recipient = Keypair.generate()
        encrypted = encrypt_message(sender.seed, recipient.public_key, b"secret")
        enc_bytes = bytearray(_b64url_decode(encrypted["enc"]))
        enc_bytes[0] ^= 0xFF
        encrypted["enc"] = _b64url_encode(bytes(enc_bytes))
        with pytest.raises(Exception):
            decrypt_message(recipient.seed, sender.public_key, encrypted)


class TestMalformedFields:
    def test_invalid_base64_enc(self):
        sender = Keypair.generate()
        recipient = Keypair.generate()
        encrypted = encrypt_message(sender.seed, recipient.public_key, b"test")
        encrypted["enc"] = "!!!invalid!!!"
        with pytest.raises(Exception):
            decrypt_message(recipient.seed, sender.public_key, encrypted)

    def test_invalid_base64_ct(self):
        sender = Keypair.generate()
        recipient = Keypair.generate()
        encrypted = encrypt_message(sender.seed, recipient.public_key, b"test")
        encrypted["ct"] = "!!!invalid!!!"
        with pytest.raises(Exception):
            decrypt_message(recipient.seed, sender.public_key, encrypted)

    def test_missing_ct_content_type(self):
        sender = Keypair.generate()
        recipient = Keypair.generate()
        encrypted = encrypt_message(sender.seed, recipient.public_key, b"test")
        del encrypted["ct_content_type"]
        with pytest.raises(KeyError):
            decrypt_message(recipient.seed, sender.public_key, encrypted)


class TestSelfEncrypt:
    def test_self_encrypt_decrypt(self):
        """Sender == recipient is valid (agent encrypting to itself for storage)."""
        kp = Keypair.generate()
        plaintext = b'{"self": "message"}'

        encrypted = encrypt_message(kp.seed, kp.public_key, plaintext, "application/json")
        decrypted, ct = decrypt_message(kp.seed, kp.public_key, encrypted)
        assert decrypted == plaintext
        assert ct == "application/json"


class TestSenderKeyRequired:
    def test_decrypt_fails_with_wrong_sender_key(self):
        """In v2 (HPKE Auth mode), the sender's key is cryptographically bound.
        Decryption with a wrong sender public key must fail."""
        sender = Keypair.generate()
        recipient = Keypair.generate()
        unrelated = Keypair.generate()

        encrypted = encrypt_message(sender.seed, recipient.public_key, b"hello")

        with pytest.raises(Exception):
            decrypt_message(recipient.seed, unrelated.public_key, encrypted)
