import json
from pathlib import Path

import pytest

from aeg import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4

from .util import random_split_bytes

# All AEGIS algorithm modules
ALL_ALGORITHMS = [aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4]


def load_mac_test_vectors():
    """Load MAC test vectors from JSON file."""
    test_vectors_path = (
        Path(__file__).parent / "test-vectors" / "aegismac-test-vectors.json"
    )
    with open(test_vectors_path, "r") as f:
        return json.load(f)


def get_algorithm_module(name):
    """Map test vector name to algorithm module."""
    if "128L" in name:
        return aegis128l
    elif "128X2" in name:
        return aegis128x2
    elif "128X4" in name:
        return aegis128x4
    elif "256" in name and "256X2" not in name and "256X4" not in name:
        return aegis256
    elif "256X2" in name:
        return aegis256x2
    elif "256X4" in name:
        return aegis256x4
    else:
        raise ValueError(f"Unknown algorithm in test vector name: {name}")


def get_test_id(vector):
    """Generate a test ID from the vector name."""
    name = vector["name"]
    # Extract algorithm name, e.g., "AEGISMAC-128L Test Vector" -> "128L"
    if "AEGISMAC-" in name:
        return name.split("AEGISMAC-")[1].split(" ")[0]
    return name


@pytest.mark.parametrize("vector", load_mac_test_vectors(), ids=get_test_id)
def test_mac(vector):
    """Test MAC computation against test vectors."""
    alg = get_algorithm_module(vector["name"])

    key = bytes.fromhex(vector["key"])
    nonce = bytes.fromhex(vector["nonce"])
    data = bytes.fromhex(vector["data"])

    # Test 128-bit MAC if present
    if "tag128" in vector:
        expected_tag128 = bytes.fromhex(vector["tag128"])
        computed_tag128 = alg.mac(key, nonce, data, maclen=16)
        assert computed_tag128 == expected_tag128, (
            f"128-bit MAC mismatch for {vector['name']}"
        )

    # Test 256-bit MAC if present
    if "tag256" in vector:
        expected_tag256 = bytes.fromhex(vector["tag256"])
        computed_tag256 = alg.mac(key, nonce, data, maclen=32)
        assert computed_tag256 == expected_tag256, (
            f"256-bit MAC mismatch for {vector['name']}"
        )


@pytest.mark.parametrize("vector", load_mac_test_vectors(), ids=get_test_id)
def test_mac_class(vector):
    """Test MAC computation using the Mac class against test vectors."""
    alg = get_algorithm_module(vector["name"])

    key = bytes.fromhex(vector["key"])
    nonce = bytes.fromhex(vector["nonce"])
    data = bytes.fromhex(vector["data"])

    # Test 128-bit MAC if present
    if "tag128" in vector:
        expected_tag128 = bytes.fromhex(vector["tag128"])
        mac_state = alg.Mac(key, nonce, maclen=16)
        for chunk in random_split_bytes(data):
            mac_state.update(chunk)
        computed_tag128 = mac_state.final()
        assert computed_tag128 == expected_tag128, (
            f"128-bit MAC mismatch for {vector['name']}"
        )

    # Test 256-bit MAC if present
    if "tag256" in vector:
        expected_tag256 = bytes.fromhex(vector["tag256"])
        mac_state = alg.Mac(key, nonce, maclen=32)
        for chunk in random_split_bytes(data):
            mac_state.update(chunk)
        computed_tag256 = mac_state.final()
        assert computed_tag256 == expected_tag256, (
            f"256-bit MAC mismatch for {vector['name']}"
        )


@pytest.mark.parametrize("vector", load_mac_test_vectors(), ids=get_test_id)
def test_mac_class_with_digest(vector):
    """Test MAC computation using digest() and hexdigest() instead of final()."""
    alg = get_algorithm_module(vector["name"])

    key = bytes.fromhex(vector["key"])
    nonce = bytes.fromhex(vector["nonce"])
    data = bytes.fromhex(vector["data"])

    # Test 128-bit MAC if present
    if "tag128" in vector:
        expected_tag128 = bytes.fromhex(vector["tag128"])

        # Test with digest()
        mac_state = alg.Mac(key, nonce, maclen=16)
        for chunk in random_split_bytes(data):
            mac_state.update(chunk)
        computed_tag128 = mac_state.digest()
        assert computed_tag128 == expected_tag128, (
            f"128-bit MAC mismatch for {vector['name']} using digest()"
        )

        # Test that digest() can be called multiple times
        computed_tag128_again = mac_state.digest()
        assert computed_tag128 == computed_tag128_again, (
            "digest() should return the same value on repeated calls"
        )

        # Test hexdigest()
        mac_state2 = alg.Mac(key, nonce, maclen=16)
        for chunk in random_split_bytes(data):
            mac_state2.update(chunk)
        hex_tag = mac_state2.hexdigest()
        assert hex_tag == expected_tag128.hex(), (
            f"128-bit MAC hexdigest mismatch for {vector['name']}"
        )

        # Test that hexdigest() can be called multiple times
        hex_tag_again = mac_state2.hexdigest()
        assert hex_tag == hex_tag_again, (
            "hexdigest() should return the same value on repeated calls"
        )

    # Test 256-bit MAC if present
    if "tag256" in vector:
        expected_tag256 = bytes.fromhex(vector["tag256"])

        # Test with digest()
        mac_state = alg.Mac(key, nonce, maclen=32)
        for chunk in random_split_bytes(data):
            mac_state.update(chunk)
        computed_tag256 = mac_state.digest()
        assert computed_tag256 == expected_tag256, (
            f"256-bit MAC mismatch for {vector['name']} using digest()"
        )


@pytest.mark.parametrize("vector", load_mac_test_vectors(), ids=get_test_id)
def test_mac_clone(vector):
    """Test that cloning a Mac state works correctly."""
    alg = get_algorithm_module(vector["name"])

    key = bytes.fromhex(vector["key"])
    nonce = bytes.fromhex(vector["nonce"])
    data = bytes.fromhex(vector["data"])

    # Test 128-bit MAC if present
    if "tag128" in vector:
        expected_tag128 = bytes.fromhex(vector["tag128"])

        mac_state = alg.Mac(key, nonce, maclen=16)
        for chunk in random_split_bytes(data):
            mac_state.update(chunk)

        # Clone the state
        cloned_state = mac_state.clone()

        # Both should produce the same tag
        tag1 = mac_state.final()
        tag2 = cloned_state.final()

        assert tag1 == expected_tag128
        assert tag2 == expected_tag128
        assert tag1 == tag2


@pytest.mark.parametrize("vector", load_mac_test_vectors(), ids=get_test_id)
def test_mac_reset(vector):
    """Test that resetting a Mac state works correctly."""
    alg = get_algorithm_module(vector["name"])

    key = bytes.fromhex(vector["key"])
    nonce = bytes.fromhex(vector["nonce"])
    data = bytes.fromhex(vector["data"])

    # Test 128-bit MAC if present
    if "tag128" in vector:
        expected_tag128 = bytes.fromhex(vector["tag128"])

        mac_state = alg.Mac(key, nonce, maclen=16)
        for chunk in random_split_bytes(data):
            mac_state.update(chunk)
        tag1 = mac_state.final()
        assert tag1 == expected_tag128

        # Reset and compute again
        mac_state.reset()
        for chunk in random_split_bytes(data):
            mac_state.update(chunk)
        tag2 = mac_state.final()

        assert tag2 == expected_tag128
        assert tag1 == tag2


@pytest.mark.parametrize("alg", ALL_ALGORITHMS, ids=lambda x: x.__name__.split(".")[-1])
def test_mac_reset_after_digest(alg):
    """Test that reset() clears the cached digest and allows reuse."""
    key = alg.random_key()
    nonce = alg.random_nonce()

    mac_state = alg.Mac(key, nonce)
    mac_state.update(b"Hello, world!")
    tag1 = mac_state.digest()

    # After digest(), update should fail
    with pytest.raises(RuntimeError):
        mac_state.update(b"More data")

    # Reset should clear the cached digest
    mac_state.reset()

    # Now we should be able to update again
    mac_state.update(b"Different data")
    tag2 = mac_state.digest()

    # Tags should be different since we used different data
    assert tag1 != tag2


@pytest.mark.parametrize("alg", ALL_ALGORITHMS, ids=lambda x: x.__name__.split(".")[-1])
def test_mac_clone_preserves_cached_digest(alg):
    """Test that cloning preserves the cached digest state."""
    key = alg.random_key()
    nonce = alg.random_nonce()

    mac_state = alg.Mac(key, nonce)
    mac_state.update(b"Hello, world!")
    tag1 = mac_state.digest()

    # Clone after digest
    cloned_state = mac_state.clone()

    # Both should return the same cached tag
    tag2 = cloned_state.digest()
    assert tag1 == tag2

    # Both should be unable to update
    with pytest.raises(RuntimeError):
        mac_state.update(b"More data")
    with pytest.raises(RuntimeError):
        cloned_state.update(b"More data")
