import hashlib
import io
import logging

import pytest

from mcard.config.config_constants import (
    MD5_COLLISION_PAIR_IN_BYTES_a,
    MD5_COLLISION_PAIR_IN_BYTES_b,
)
from mcard.model.hash.validator import HashAlgorithm, HashValidator

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

hash_functions = {
    HashAlgorithm.MD5: hashlib.md5,
    HashAlgorithm.SHA1: hashlib.sha1,
    HashAlgorithm.SHA224: hashlib.sha224,
    HashAlgorithm.SHA256: hashlib.sha256,
    HashAlgorithm.SHA384: hashlib.sha384,
    HashAlgorithm.SHA512: hashlib.sha512,
}


class TestHashString:
    # Mock binary data
    MOCK_PDF = (
        b"%PDF-1.4\n"
        b"%\xe2\xe3\xcf\xd3\n"
        b"1 0 obj\n"
        b"<< /Type /Catalog /Pages 2 0 R >>\n"
        b"endobj\n"
    )

    MOCK_JPEG = (
        b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00H\x00H\x00\x00\xff"
        b"\xdb\x00C\x00\x08\x06\x06\x07\x06\x05\x08\x07\x07\x07\t\t\x08\n\x0c"
        b"\x14\r\x0c\x0b\x0b\x0c\x19\x12\x13\x0f\x14\x1d\x1a\x1f\x1e\x1d\x1a"
        b"\x1c\x1c $.' \",#\x1c\x1c(7),01444\x1f'9=82<.342"
    )

    MOCK_PNG = (
        b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
        b"\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\x00\x00"
        b"\x00\x02\x00\x01\xe5\x27\xde\xfc\x00\x00\x00\x00IEND\xaeB`\x82"
    )

    MOCK_GIF = (
        b"GIF89a\x01\x00\x01\x00\x80\x00\x00\xff\xff\xff\xff\xff\xff!\xf9\x04"
        b"\x00\x00\x00\x00\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D"
        b"\x01\x00;"
    )

    MOCK_WAV = (
        b"RIFF\x24\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x01\x00\x44\xac"
        b"\x00\x00\x88X\x01\x00\x02\x00\x10\x00data\x00\x00\x00\x00"
    )

    MOCK_ZIP = (
        b"PK\x03\x04\x14\x00\x00\x00\x08\x00\xb3>_T\x00\x00\x00\x00\x00\x00\x00"
        b"\x00\x00\x00\x00\x00\x04\x00\x00\x00test/PK\x01\x02\x14\x00\x14\x00"
        b"\x00\x00\x08\x00\xb3>_T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
        b"\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00"
        b"\x00\x00test/PK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x002\x00\x00\x00"
        b"6\x00\x00\x00\x00\x00"
    )

    def test_hash_computation_with_string(self):
        """Test hash computation with string content"""
        content = "Test content"
        hash_validator = HashValidator(content, HashAlgorithm.DEFAULT)
        expected_hash = hash_functions[HashAlgorithm.DEFAULT](
            content.encode()
        ).hexdigest()
        assert hash_validator.hash_value == expected_hash

    def test_hash_computation_with_bytes(self):
        """Test hash computation with bytes content"""
        content = b"Binary content"
        hash_validator = HashValidator(content, HashAlgorithm.DEFAULT)
        expected_hash = hash_functions[HashAlgorithm.DEFAULT](content).hexdigest()
        assert hash_validator.hash_value == expected_hash

    def test_hash_validation_success(self):
        """Test successful hash validation"""
        content = "Test content"
        hash_validator = HashValidator(content, HashAlgorithm.DEFAULT)
        expected_hash = hash_functions[HashAlgorithm.DEFAULT](
            content.encode()
        ).hexdigest()
        # This should not raise an error
        hash_validator.validate(expected_hash)
        assert hash_validator.hash_value == expected_hash

    def test_hash_validation_failure(self):
        """Test hash validation failure"""
        content = "Test content"
        hash_validator = HashValidator(content)
        invalid_hash = "invalidhash"
        with pytest.raises(ValueError, match="Hash mismatch:"):
            hash_validator.validate(invalid_hash)

    def test_hash_computation_with_empty_string(self):
        """Test hash computation with empty string"""
        content = ""
        hash_validator = HashValidator(content, HashAlgorithm.DEFAULT)
        expected_hash = hash_functions[HashAlgorithm.DEFAULT](
            content.encode()
        ).hexdigest()
        assert hash_validator.hash_value == expected_hash

    def test_hash_computation_with_empty_bytes(self):
        """Test hash computation with empty bytes"""
        content = b""
        hash_validator = HashValidator(content, HashAlgorithm.SHA256)
        expected_hash = hashlib.sha256(content).hexdigest()
        assert hash_validator.hash_value == expected_hash

    def test_unsupported_hash_function(self):
        """Test that compute_hash raises error for unsupported hash functions"""
        content = "Test content"
        with pytest.raises(
            ValueError, match="'unsupported_hash' is not a valid HashAlgorithm"
        ):
            HashValidator.compute_hash(content, hash_algorithm="unsupported_hash")

    def test_string_representation(self):
        """Test string representation of HashValidator"""
        content = "Test content"
        hash_validator = HashValidator(content, HashAlgorithm.DEFAULT)
        expected_hash = hash_functions[HashAlgorithm.DEFAULT](
            content.encode()
        ).hexdigest()
        assert str(hash_validator) == expected_hash

    def test_hash_computation_with_special_characters(self):
        """Test hash computation with special characters"""
        content = "!@#$%^&*()_+-=[]{}|;:'\",.<>/?"
        hash_validator = HashValidator(content, HashAlgorithm.DEFAULT)
        expected_hash = hash_functions[HashAlgorithm.DEFAULT](
            content.encode()
        ).hexdigest()
        assert hash_validator.hash_value == expected_hash

    def test_hash_computation_with_unicode(self):
        """Test hash computation with unicode characters"""
        content = "Hello, 世界! 👋"
        hash_validator = HashValidator(content, HashAlgorithm.DEFAULT)
        expected_hash = hash_functions[HashAlgorithm.DEFAULT](
            content.encode()
        ).hexdigest()
        assert hash_validator.hash_value == expected_hash

    @pytest.mark.parametrize(
        "algorithm,hash_func",
        [
            (HashAlgorithm.MD5, hashlib.md5),
            (HashAlgorithm.SHA1, hashlib.sha1),
            (HashAlgorithm.SHA224, hashlib.sha224),
            (HashAlgorithm.SHA256, hashlib.sha256),
            (HashAlgorithm.SHA384, hashlib.sha384),
            (HashAlgorithm.SHA512, hashlib.sha512),
        ],
    )
    def test_different_hash_algorithms(self, algorithm, hash_func):
        """Test different hash algorithms"""
        content = "Test content"
        hash_validator = HashValidator(content, hash_algorithm=algorithm)
        expected_hash = hash_func(content.encode()).hexdigest()
        assert hash_validator.hash_value == expected_hash

    def test_custom_hash_algorithm_error(self):
        """Test that using CUSTOM hash algorithm without providing function raises error"""
        content = "Test content"
        with pytest.raises(ValueError, match="Custom hash function must be provided"):
            HashValidator(content, hash_algorithm=HashAlgorithm.CUSTOM)

    def test_hash_algorithm_string_input(self):
        """Test using string input for hash algorithm"""
        content = "Test content"
        hash_validator = HashValidator(content, HashAlgorithm.DEFAULT)
        expected_hash = hash_functions[HashAlgorithm.DEFAULT](
            content.encode()
        ).hexdigest()
        assert hash_validator.hash_value == expected_hash

    @pytest.mark.parametrize(
        "binary_data,description",
        [
            (MOCK_PDF, "PDF"),
            (MOCK_JPEG, "JPEG"),
            (MOCK_PNG, "PNG"),
            (MOCK_GIF, "GIF"),
            (MOCK_WAV, "WAV"),
            (MOCK_ZIP, "ZIP"),
        ],
    )
    def test_hash_computation_with_binary_files(self, binary_data, description):
        """Test hash computation with various binary file formats"""
        hash_validator = HashValidator(binary_data, HashAlgorithm.DEFAULT)
        expected_hash = hash_functions[HashAlgorithm.DEFAULT](binary_data).hexdigest()
        assert hash_validator.hash_value == expected_hash

    def test_hash_computation_with_large_binary(self):
        """Test hash computation with a large binary file"""
        # Create a 1MB binary file with random-like content
        large_binary = bytes([i % 256 for i in range(1024 * 1024)])
        hash_validator = HashValidator(large_binary, HashAlgorithm.DEFAULT)
        expected_hash = hash_functions[HashAlgorithm.DEFAULT](large_binary).hexdigest()
        assert hash_validator.hash_value == expected_hash

    def test_hash_computation_with_binary_stream(self):
        """Test hash computation with a binary stream"""
        # Create a binary stream with PDF content
        stream = io.BytesIO(self.MOCK_PDF)
        content = stream.getvalue()
        hash_validator = HashValidator(content, HashAlgorithm.DEFAULT)
        expected_hash = hash_functions[HashAlgorithm.DEFAULT](content).hexdigest()
        assert hash_validator.hash_value == expected_hash

    def test_md5_collision_detection(self):
        """Test MD5 collision detection with known colliding inputs"""
        # These are known MD5 collision pairs from the FastColl attack
        # See README.md for details, the two differences are 200 vs. 202 and d15 vs. d1d
        # MD5_COLLISION_PAIR_IN_BYTES_a = bytes.fromhex("4dc968ff0ee35c209572d4777b721587d36fa7b21bdc56b74a3dc0783e7b9518afbfa200a8284bf36e8e4b55b35f427593d849676da0d1555d8360fb5f07fea2")
        # MD5_COLLISION_PAIR_IN_BYTES_b = bytes.fromhex("4dc968ff0ee35c209572d4777b721587d36fa7b21bdc56b74a3dc0783e7b9518afbfa202a8284bf36e8e4b55b35f427593d849676da0d1d55d8360fb5f07fea2")

        # Calculate MD5 hashes using hashlib for verification
        hash1_from_hashlib = hashlib.md5(MD5_COLLISION_PAIR_IN_BYTES_a).hexdigest()
        hash2_from_hashlib = hashlib.md5(MD5_COLLISION_PAIR_IN_BYTES_b).hexdigest()

        logger.debug(f"MD5 Hash of string1: {hash1_from_hashlib}")
        logger.debug(f"MD5 Hash of string2: {hash2_from_hashlib}")

        # Verify collision with hashlib
        assert (
            hash1_from_hashlib == hash2_from_hashlib
        ), "These strings should produce identical MD5 hashes"

        # Demonstrate no collision in sha1
        hash1_sha1 = hashlib.sha1(MD5_COLLISION_PAIR_IN_BYTES_a).hexdigest()
        hash2_sha1 = hashlib.sha1(MD5_COLLISION_PAIR_IN_BYTES_b).hexdigest()

        assert hash1_sha1 != hash2_sha1, "MD5 hashes should be different"

        # Create HashValidators for both contents
        validator1 = HashValidator(MD5_COLLISION_PAIR_IN_BYTES_a, HashAlgorithm.MD5)
        validator2 = HashValidator(MD5_COLLISION_PAIR_IN_BYTES_b, HashAlgorithm.MD5)

        # Log the hashes
        logger.debug(f"HashValidator1: {validator1.hash_value}")
        logger.debug(f"HashValidator2: {validator2.hash_value}")

        # Assert that the hashes are equal, indicating a collision
        assert (
            validator1.hash_value == validator2.hash_value
        ), "HashValidators should produce equal hashes for colliding inputs"

        # Validate that the contents are different
        assert (
            MD5_COLLISION_PAIR_IN_BYTES_a != MD5_COLLISION_PAIR_IN_BYTES_b
        ), "Contents should be different for collision"
