"""Unit tests for mbedtls.md."""

# Disable checks for violations that are acceptable in tests.
# pylint: disable=missing-docstring
# pylint: disable=attribute-defined-outside-init
# pylint: disable=invalid-name

import pickle
from collections.abc import Collection

import pytest

import mbedtls
from mbedtls import hashlib
from mbedtls import hmac as hmaclib
from mbedtls._md import Hash


def make_chunks(buffer, size):
    for i in range(0, len(buffer), size):
        yield buffer[i : i + size]


def test_make_chunks(randbytes):
    buffer = randbytes(1024)
    assert b"".join(buf for buf in make_chunks(buffer, 100)) == buffer


def test_algorithms():
    assert isinstance(hashlib.algorithms_guaranteed, Collection)
    assert hashlib.algorithms_guaranteed

    assert isinstance(hashlib.algorithms_available, Collection)
    assert hashlib.algorithms_available

    assert frozenset(hashlib.algorithms_guaranteed).issubset(
        hashlib.algorithms_available
    )


def test_invalid_name_raises_TypeError():
    with pytest.raises(TypeError):
        Hash(42)


def test_unavailable_cipher_raises_ValueError():
    with pytest.raises(ValueError):
        Hash("unavailable")


class _TestMDBase:
    @pytest.fixture
    def algorithm(self):
        raise NotImplementedError

    @pytest.fixture
    def digest_size(self):
        raise NotImplementedError

    @pytest.fixture
    def block_size(self):
        raise NotImplementedError

    @pytest.mark.parametrize("repr_", (repr, str), ids=lambda f: f.__name__)
    def test_repr(self, repr_, algorithm):
        assert isinstance(repr_(algorithm), str)

    def test_pickle(self, algorithm):
        with pytest.raises(TypeError) as excinfo:
            pickle.dumps(algorithm)

        assert str(excinfo.value).startswith("cannot pickle")

    def test_digest_size_accessor(self, algorithm, digest_size):
        assert algorithm.digest_size == digest_size

    def test_digest_size(self, algorithm, digest_size):
        assert len(algorithm.digest()) == digest_size

    def test_block_size_accessor(self, algorithm, block_size):
        assert algorithm.block_size == block_size


class _TestHash(_TestMDBase):
    @pytest.fixture
    def buffer(self, randbytes):
        return randbytes(512)

    def test_new(self, algorithm, buffer):
        copy = hashlib.new(algorithm.name, buffer)
        algorithm.update(buffer)
        assert algorithm.digest() == copy.digest()
        assert algorithm.hexdigest() == copy.hexdigest()

    def test_copy_and_update(self, algorithm, buffer):
        copy = algorithm.copy()
        algorithm.update(buffer)
        copy.update(buffer)
        assert algorithm.digest() == copy.digest()
        assert algorithm.hexdigest() == copy.hexdigest()

    def test_copy_and_update_nothing(self, algorithm, buffer):
        copy = algorithm.copy()
        algorithm.update(b"")
        assert algorithm.digest() == copy.digest()
        assert algorithm.hexdigest() == copy.hexdigest()


@pytest.mark.skipif(
    not mbedtls.has_feature("md2"), reason="requires MD2 support in libmbedtls"
)
class TestHashMD2(_TestHash):
    @pytest.fixture
    def algorithm(self):
        return hashlib.md2()

    @pytest.fixture
    def digest_size(self):
        return 16

    @pytest.fixture
    def block_size(self):
        return 16


@pytest.mark.skipif(
    not mbedtls.has_feature("md4"), reason="requires MD4 support in libmbedtls"
)
class TestHashMD4(_TestHash):
    @pytest.fixture
    def algorithm(self):
        return hashlib.md4()

    @pytest.fixture
    def digest_size(self):
        return 16

    @pytest.fixture
    def block_size(self):
        return 64


@pytest.mark.skipif(
    not mbedtls.has_feature("md5"), reason="requires MD5 support in libmbedtls"
)
class TestHashMD5(_TestHash):
    @pytest.fixture
    def algorithm(self):
        return hashlib.md5()

    @pytest.fixture
    def type_(self):
        return 3

    @pytest.fixture
    def digest_size(self):
        return 16

    @pytest.fixture
    def block_size(self):
        return 64


@pytest.mark.skipif(
    not mbedtls.has_feature("sha1"),
    reason="requires SHA1 support in libmbedtls",
)
class TestHashSHA1(_TestHash):
    @pytest.fixture
    def algorithm(self):
        return hashlib.sha1()

    @pytest.fixture
    def digest_size(self):
        return 20

    @pytest.fixture
    def block_size(self):
        return 64


@pytest.mark.skipif(
    not mbedtls.has_feature("sha256"),
    reason="requires SHA256 support in libmbedtls",
)
class TestHashSHA224(_TestHash):
    @pytest.fixture
    def algorithm(self):
        return hashlib.sha224()

    @pytest.fixture
    def digest_size(self):
        return 28

    @pytest.fixture
    def block_size(self):
        return 64


@pytest.mark.skipif(
    not mbedtls.has_feature("sha256"),
    reason="requires SHA256 support in libmbedtls",
)
class TestHashSHA256(_TestHash):
    @pytest.fixture
    def algorithm(self):
        return hashlib.sha256()

    @pytest.fixture
    def digest_size(self):
        return 32

    @pytest.fixture
    def block_size(self):
        return 64


@pytest.mark.skipif(
    not mbedtls.has_feature("sha512"),
    reason="requires SHA384 support in libmbedtls",
)
class TestHashSHA384(_TestHash):
    @pytest.fixture
    def algorithm(self):
        return hashlib.sha384()

    @pytest.fixture
    def digest_size(self):
        return 48

    @pytest.fixture
    def block_size(self):
        return 128


@pytest.mark.skipif(
    not mbedtls.has_feature("sha512"),
    reason="requires SHA512 support in libmbedtls",
)
class TestHashSHA512(_TestHash):
    @pytest.fixture
    def algorithm(self):
        return hashlib.sha512()

    @pytest.fixture
    def digest_size(self):
        return 64

    @pytest.fixture
    def block_size(self):
        return 128


@pytest.mark.skipif(
    not mbedtls.has_feature("ripemd160"),
    reason="requires RIPEMD160 support in libmbedtls",
)
class TestHashRIPEMD160(_TestHash):
    @pytest.fixture
    def algorithm(self):
        return hashlib.ripemd160()

    @pytest.fixture
    def digest_size(self):
        return 20

    @pytest.fixture
    def block_size(self):
        return 64


class _TestHmac(_TestMDBase):
    @pytest.fixture(params=[0, 16])
    def key(self, request, randbytes):
        return randbytes(request.param)

    @pytest.fixture
    def buffer(self, randbytes):
        return randbytes(512)

    def test_new(self, algorithm, key, buffer):
        copy = hmaclib.new(key, buffer, algorithm.name)
        algorithm.update(buffer)
        assert algorithm.digest() == copy.digest()
        assert algorithm.hexdigest() == copy.hexdigest()


@pytest.mark.skipif(
    not mbedtls.has_feature("md2"), reason="requires MD2 support in libmbedtls"
)
class TestHmacMD2(_TestHmac):
    @pytest.fixture
    def algorithm(self, key):
        return hmaclib.md2(key)

    @pytest.fixture
    def digest_size(self):
        return 16

    @pytest.fixture
    def block_size(self):
        return 16


@pytest.mark.skipif(
    not mbedtls.has_feature("md4"), reason="requires MD4 support in libmbedtls"
)
class TestHmacMD4(_TestHmac):
    @pytest.fixture
    def algorithm(self, key):
        return hmaclib.md4(key)

    @pytest.fixture
    def digest_size(self):
        return 16

    @pytest.fixture
    def block_size(self):
        return 64


@pytest.mark.skipif(
    not mbedtls.has_feature("md5"), reason="requires MD5 support in libmbedtls"
)
class TestHmacMD5(_TestHmac):
    @pytest.fixture
    def algorithm(self, key):
        return hmaclib.md5(key)

    @pytest.fixture
    def type_(self):
        return 3

    @pytest.fixture
    def digest_size(self):
        return 16

    @pytest.fixture
    def block_size(self):
        return 64


@pytest.mark.skipif(
    not mbedtls.has_feature("sha1"),
    reason="requires SHA1 support in libmbedtls",
)
class TestHmacSHA1(_TestHmac):
    @pytest.fixture
    def algorithm(self, key):
        return hmaclib.sha1(key)

    @pytest.fixture
    def digest_size(self):
        return 20

    @pytest.fixture
    def block_size(self):
        return 64


@pytest.mark.skipif(
    not mbedtls.has_feature("sha256"),
    reason="requires SHA256 support in libmbedtls",
)
class TestHmacSHA224(_TestHmac):
    @pytest.fixture
    def algorithm(self, key):
        return hmaclib.sha224(key)

    @pytest.fixture
    def digest_size(self):
        return 28

    @pytest.fixture
    def block_size(self):
        return 64


@pytest.mark.skipif(
    not mbedtls.has_feature("sha256"),
    reason="requires SHA256 support in libmbedtls",
)
class TestHmacSHA256(_TestHmac):
    @pytest.fixture
    def algorithm(self, key):
        return hmaclib.sha256(key)

    @pytest.fixture
    def digest_size(self):
        return 32

    @pytest.fixture
    def block_size(self):
        return 64


@pytest.mark.skipif(
    not mbedtls.has_feature("sha512"),
    reason="requires SHA512 support in libmbedtls",
)
class TestHmacSHA384(_TestHmac):
    @pytest.fixture
    def algorithm(self, key):
        return hmaclib.sha384(key)

    @pytest.fixture
    def digest_size(self):
        return 48

    @pytest.fixture
    def block_size(self):
        return 128


@pytest.mark.skipif(
    not mbedtls.has_feature("sha512"),
    reason="requires SHA512 support in libmbedtls",
)
class TestHmacSHA512(_TestHmac):
    @pytest.fixture
    def algorithm(self, key):
        return hmaclib.sha512(key)

    @pytest.fixture
    def digest_size(self):
        return 64

    @pytest.fixture
    def block_size(self):
        return 128


@pytest.mark.skipif(
    not mbedtls.has_feature("ripemd160"),
    reason="requires RIPEMD160 support in libmbedtls",
)
class TestHmacRIPEMD160(_TestHmac):
    @pytest.fixture
    def algorithm(self, key):
        return hmaclib.ripemd160(key)

    @pytest.fixture
    def digest_size(self):
        return 20

    @pytest.fixture
    def block_size(self):
        return 64
