"""Integration tests for patatt sign/verify within b4.

Uses ephemeral ed25519 keys so no external key material is needed.
"""
import base64
import email.message
import os
import tempfile
from collections.abc import Generator
from typing import Tuple

import pytest

import b4
import patatt

from nacl.signing import SigningKey


@pytest.fixture()
def ed25519_keypair() -> Generator[Tuple[str, str, str, str], None, None]:
    """Generate an ephemeral ed25519 keypair written to temp files.

    Returns (privkey_path, verify_key_b64, identity, selector).
    The private key file is written so patatt can find it via
    signingkey = ed25519:/path/to/key.
    """
    sk = SigningKey.generate()
    sk_b64 = base64.b64encode(sk.encode()).decode()
    vk_b64 = base64.b64encode(sk.verify_key.encode()).decode()
    with tempfile.NamedTemporaryFile(mode='w', suffix='.key',
                                     delete=False) as fh:
        fh.write(sk_b64)
        privkey_path = fh.name
    yield privkey_path, vk_b64, 'test@example.com', 'default'
    os.unlink(privkey_path)


@pytest.fixture()
def keyring_dir(ed25519_keypair: Tuple[str, str, str, str]) -> Generator[str, None, None]:
    """Create a temporary keyring directory with the ephemeral public key.

    The directory layout follows patatt's expected structure:
    <keyring>/<identity>/ed25519/<selector>
    """
    _privkey, vk_b64, identity, selector = ed25519_keypair
    # patatt looks up keys as: ed25519/<domain>/<localpart>/<selector>
    local, domain = identity.split('@', 1)
    with tempfile.TemporaryDirectory() as tmpdir:
        key_dir = os.path.join(tmpdir, 'ed25519', domain, local)
        os.makedirs(key_dir)
        key_file = os.path.join(key_dir, selector)
        with open(key_file, 'w') as fh:
            fh.write(vk_b64)
        yield tmpdir


def _make_test_message(from_addr: str = 'test@example.com',
                       subject: str = 'Test patch',
                       body: str = 'This is a test.\n') -> bytes:
    """Build a minimal RFC2822 message as bytes."""
    msg = email.message.EmailMessage()
    msg['From'] = from_addr
    msg['To'] = 'list@example.com'
    msg['Subject'] = subject
    msg['Message-ID'] = '<test-001@example.com>'
    msg.set_payload(body, charset='utf-8')
    return msg.as_bytes(policy=b4.emlpolicy)


class TestPatattSignVerify:
    """Round-trip sign and verify using ephemeral ed25519 keys."""

    def test_sign_and_verify(self, ed25519_keypair: Tuple[str, str, str, str],
                             keyring_dir: str) -> None:
        """A signed message should validate with the matching public key."""
        privkey_path, _vk_b64, identity, selector = ed25519_keypair
        msg_bytes = _make_test_message(from_addr=identity)

        config = {
            'identity': identity,
            'selector': selector,
            'signingkey': f'ed25519:{privkey_path}',
        }
        signed = patatt.rfc2822_sign(msg_bytes, config=config)  # type: ignore[attr-defined]
        assert b'X-Developer-Signature' in signed

        results = patatt.validate_message(signed, [keyring_dir])  # type: ignore[attr-defined]
        assert len(results) > 0
        assert results[0][0] == patatt.RES_VALID  # type: ignore[attr-defined]

    def test_tampered_body_fails(self, ed25519_keypair: Tuple[str, str, str, str],
                                 keyring_dir: str) -> None:
        """Modifying the body after signing should fail validation."""
        privkey_path, _vk_b64, identity, selector = ed25519_keypair
        msg_bytes = _make_test_message(from_addr=identity)

        config = {
            'identity': identity,
            'selector': selector,
            'signingkey': f'ed25519:{privkey_path}',
        }
        signed = patatt.rfc2822_sign(msg_bytes, config=config)  # type: ignore[attr-defined]

        # Tamper with the body
        tampered = signed.replace(b'This is a test.', b'This is TAMPERED.')
        results = patatt.validate_message(tampered, [keyring_dir])  # type: ignore[attr-defined]
        assert len(results) > 0
        assert results[0][0] == patatt.RES_BADSIG  # type: ignore[attr-defined]

    def test_wrong_key_fails(self, ed25519_keypair: Tuple[str, str, str, str]) -> None:
        """Validating against a different public key should fail."""
        privkey_path, _vk_b64, identity, selector = ed25519_keypair
        msg_bytes = _make_test_message(from_addr=identity)

        config = {
            'identity': identity,
            'selector': selector,
            'signingkey': f'ed25519:{privkey_path}',
        }
        signed = patatt.rfc2822_sign(msg_bytes, config=config)  # type: ignore[attr-defined]

        # Create a keyring with a different key
        other_sk = SigningKey.generate()
        other_vk_b64 = base64.b64encode(other_sk.verify_key.encode()).decode()
        local, domain = identity.split('@', 1)
        with tempfile.TemporaryDirectory() as tmpdir:
            key_dir = os.path.join(tmpdir, 'ed25519', domain, local)
            os.makedirs(key_dir)
            with open(os.path.join(key_dir, selector), 'w') as fh:
                fh.write(other_vk_b64)
            results = patatt.validate_message(signed, [tmpdir])  # type: ignore[attr-defined]
            assert len(results) > 0
            assert results[0][0] == patatt.RES_BADSIG  # type: ignore[attr-defined]

    def test_no_key_available(self, ed25519_keypair: Tuple[str, str, str, str]) -> None:
        """Validating with an empty keyring should return RES_NOKEY."""
        privkey_path, _vk_b64, identity, selector = ed25519_keypair
        msg_bytes = _make_test_message(from_addr=identity)

        config = {
            'identity': identity,
            'selector': selector,
            'signingkey': f'ed25519:{privkey_path}',
        }
        signed = patatt.rfc2822_sign(msg_bytes, config=config)  # type: ignore[attr-defined]

        with tempfile.TemporaryDirectory() as empty_keyring:
            results = patatt.validate_message(signed, [empty_keyring])  # type: ignore[attr-defined]
            assert len(results) > 0
            assert results[0][0] == patatt.RES_NOKEY  # type: ignore[attr-defined]

    def test_unsigned_message(self, keyring_dir: str) -> None:
        """An unsigned message should return RES_NOSIG."""
        msg_bytes = _make_test_message()
        results = patatt.validate_message(msg_bytes, [keyring_dir])  # type: ignore[attr-defined]
        assert len(results) == 1
        assert results[0][0] == patatt.RES_NOSIG  # type: ignore[attr-defined]

    def test_sign_adds_developer_key_header(self, ed25519_keypair: Tuple[str, str, str, str]) -> None:
        """Signing adds both X-Developer-Signature and X-Developer-Key."""
        privkey_path, _vk_b64, identity, selector = ed25519_keypair
        msg_bytes = _make_test_message(from_addr=identity)

        config = {
            'identity': identity,
            'selector': selector,
            'signingkey': f'ed25519:{privkey_path}',
        }
        signed = patatt.rfc2822_sign(msg_bytes, config=config)  # type: ignore[attr-defined]
        assert b'X-Developer-Signature' in signed
        assert b'X-Developer-Key' in signed
        assert b'a=ed25519' in signed
        assert identity.encode() in signed
