"""
Test suite for kHeavyHash implementation.

Includes hardcoded test vectors and optional Go binary comparison.

IMPORTANT: This test suite requires actual test vectors from the Go reference
implementation. See tests/test_vectors.py for instructions on how to obtain them.
"""

import subprocess
from pathlib import Path

import pytest

from kheavyhash import kheavyhash
from kheavyhash.kheavyhash_python import (
    Xoshiro256PlusPlus,
    _gf16_inverse,
    bytes_to_nibbles,
    matrix_rank_gf16,
    matrix_vector_multiply_gf16,
    nibbles_to_bytes,
)

# Import test vectors (will fail if not properly configured)
# Mypy sees redefinitions here due to try/except fallback pattern - this is intentional
try:
    from tests.test_vectors import (  # type: ignore[no-redef]
        TEST_VECTOR_1_NONCE,
        TEST_VECTOR_1_PRE_POW_HASH,
        TEST_VECTOR_1_TIMESTAMP,
        TEST_VECTORS,
        make_pre_pow_hash_from_bytes,
    )
except ImportError:
    try:
        # Fallback for when tests directory is in path
        from test_vectors import (  # type: ignore[no-redef]
            TEST_VECTOR_1_NONCE,
            TEST_VECTOR_1_PRE_POW_HASH,
            TEST_VECTOR_1_TIMESTAMP,
            TEST_VECTORS,
            make_pre_pow_hash_from_bytes,
        )
    except ImportError:
        # If test_vectors.py doesn't exist or can't be imported, create empty list
        # Tests that require vectors will be skipped
        # Fallback values if test_vectors not available
        # These are intentionally redefined as fallbacks - ignore mypy warnings
        TEST_VECTORS = []  # type: ignore[no-redef]
        TEST_VECTOR_1_PRE_POW_HASH = bytes.fromhex(  # type: ignore[no-redef]
            "0ad86e9bef09726cdc75913e44ec96521391c7ceb2aae3c633f46a94bf4d2546"
        )
        TEST_VECTOR_1_TIMESTAMP = bytes.fromhex("3e6bd36895010000")  # type: ignore[no-redef]
        TEST_VECTOR_1_NONCE = bytes.fromhex("39fd069384065157")  # type: ignore[no-redef]
        import hashlib

        def make_pre_pow_hash_from_bytes(data: bytes) -> bytes:  # type: ignore[no-redef]
            first_hash = hashlib.sha256(data).digest()
            return hashlib.sha256(first_hash).digest()


class TestKHeavyHash:
    """Test cases for kHeavyHash implementation."""

    def test_input_length_validation(self):
        """Test that invalid input lengths raise ValueError."""
        # Too short
        with pytest.raises(ValueError, match="exactly 80 bytes"):
            kheavyhash(bytes(79))

        # Too long
        with pytest.raises(ValueError, match="exactly 80 bytes"):
            kheavyhash(bytes(81))

    def test_output_length(self):
        """Test that output is always 32 bytes."""
        # Test with known good values from test vectors
        padding = bytes(32)
        nonce = bytes(8)
        test_input = TEST_VECTOR_1_PRE_POW_HASH + TEST_VECTOR_1_TIMESTAMP + padding + nonce

        result = kheavyhash(test_input)
        assert len(result) == 32
        assert isinstance(result, bytes)

        # Test with pattern
        pre_pow_hash2 = bytes(range(32))
        test_input2 = pre_pow_hash2 + TEST_VECTOR_1_TIMESTAMP + padding + nonce
        result2 = kheavyhash(test_input2)
        assert len(result2) == 32

    def test_deterministic(self):
        """Test that same input produces same output."""
        padding = bytes(32)
        nonce = bytes(8)
        test_input = TEST_VECTOR_1_PRE_POW_HASH + TEST_VECTOR_1_TIMESTAMP + padding + nonce
        result1 = kheavyhash(test_input)
        result2 = kheavyhash(test_input)
        assert result1 == result2

    def test_different_inputs_produce_different_outputs(self):
        """Test that different inputs produce different outputs."""
        padding = bytes(32)
        nonce = bytes(8)
        input1 = TEST_VECTOR_1_PRE_POW_HASH + TEST_VECTOR_1_TIMESTAMP + padding + nonce

        # Different pre_pow_hash
        pre_pow_hash2 = bytes([1] + [0] * 31)
        input2 = pre_pow_hash2 + TEST_VECTOR_1_TIMESTAMP + padding + nonce

        result1 = kheavyhash(input1)
        result2 = kheavyhash(input2)
        assert result1 != result2

    def test_all_zeros_input(self):
        """Test with minimal input using known good values.

        Note: In real usage, timestamp cannot be 0 and pre_pow_hash cannot be all zeros.
        This test uses known good values to verify the algorithm works correctly.
        """
        # Use known good values from test vectors
        padding = bytes(32)
        nonce = bytes(8)
        work_order = TEST_VECTOR_1_PRE_POW_HASH + TEST_VECTOR_1_TIMESTAMP + padding + nonce

        result = kheavyhash(work_order)
        assert len(result) == 32
        # Result should be deterministic
        assert result == kheavyhash(work_order)

    def test_all_ones_input(self):
        """Test with all-ones input (edge case)."""
        pre_pow_hash = bytes([0xFF] * 32)
        timestamp = TEST_VECTOR_1_TIMESTAMP
        padding = bytes(32)
        nonce = bytes(8)
        test_input = pre_pow_hash + timestamp + padding + nonce
        result = kheavyhash(test_input)
        assert len(result) == 32

    @pytest.mark.parametrize("input_hex,expected_hex", [
        (tv[0], tv[1]) for tv in TEST_VECTORS if tv[1] is not None
    ])
    def test_hardcoded_vector(self, input_hex, expected_hex):
        """
        Test against hardcoded test vectors from Go reference implementation.

        These vectors are obtained from the Go kheavyhash package and ensure
        our implementation produces identical results.

        This test works with both C extension and native Python implementation,
        automatically using whichever is available.
        """
        if expected_hex is None:
            pytest.skip(
                "Test vector not yet populated. "
                "Run scripts/generate_test_vectors.go and update test_vectors.py"
            )

        work_order = bytes.fromhex(input_hex)
        assert len(work_order) == 80, f"Input must be 80 bytes, got {len(work_order)}"

        result = kheavyhash(work_order)
        expected = bytes.fromhex(expected_hex)

        assert len(result) == 32, "Output must be 32 bytes"
        assert len(expected) == 32, "Expected output must be 32 bytes"
        assert result == expected, (
            f"Output mismatch!\n"
            f"Input:    {input_hex}\n"
            f"Expected: {expected_hex}\n"
            f"Got:      {result.hex()}\n"
            f"This indicates the implementation does not match the Go reference."
        )

    def test_known_test_vector_1(self):
        """
        Test vector 1: Simple pattern.

        NOTE: This test only verifies the function runs without error.
        For correctness validation, use test_hardcoded_vector with vectors from Go.
        """
        # PrePowHash: 32 bytes of pattern 0x00, 0x01, 0x02, ...
        pre_pow_hash = bytes(range(32))
        timestamp = TEST_VECTOR_1_TIMESTAMP
        # Padding: 32 bytes of zeros
        padding = bytes(32)
        # Nonce: 0x0000000000000000 (little-endian)
        nonce = bytes(8)

        work_order = pre_pow_hash + timestamp + padding + nonce
        result = kheavyhash(work_order)

        assert len(result) == 32
        # Verify it's deterministic
        assert result == kheavyhash(work_order)

    def test_known_test_vector_2(self):
        """
        Test vector 2: Non-zero timestamp and nonce.

        NOTE: This test only verifies the function runs without error.
        For correctness validation, use test_hardcoded_vector with vectors from Go.
        """
        pre_pow_hash = TEST_VECTOR_1_PRE_POW_HASH
        # Timestamp: 0x0102030405060708 (little-endian) = 72623859790382856
        timestamp = bytes([0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01])
        # Padding: all zeros
        padding = bytes(32)
        # Nonce: 0x0807060504030201 (little-endian)
        nonce = bytes([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08])

        work_order = pre_pow_hash + timestamp + padding + nonce
        result = kheavyhash(work_order)

        assert len(result) == 32

    def test_known_test_vector_3(self):
        """
        Test vector 3: Pattern with alternating bytes.

        NOTE: This test only verifies the function runs without error.
        For correctness validation, use test_hardcoded_vector with vectors from Go.
        """
        # Create a pattern: alternating 0xAA and 0x55
        pattern = bytes([0xAA if i % 2 == 0 else 0x55 for i in range(32)])
        pre_pow_hash = pattern
        timestamp = TEST_VECTOR_1_TIMESTAMP
        padding = bytes(32)
        nonce = bytes([0x00] * 8)

        work_order = pre_pow_hash + timestamp + padding + nonce
        result = kheavyhash(work_order)

        assert len(result) == 32

    def test_work_order_structure(self):
        """Test that work order structure is correctly interpreted."""
        # Create a work order with distinct values in each section
        pre_pow_hash = bytes([0x11] * 32)
        timestamp = bytes([0x22] * 8)
        padding = bytes([0x33] * 32)
        nonce = bytes([0x44] * 8)

        work_order = pre_pow_hash + timestamp + padding + nonce
        result = kheavyhash(work_order)

        # Verify each section is used (result should differ if we change any section)
        work_order2 = (
            bytes([0x11] * 32)
            + bytes([0x22] * 8)
            + bytes([0x33] * 32)
            + bytes([0x55] * 8)
        )
        result2 = kheavyhash(work_order2)
        assert result != result2  # Different nonce should produce different result

    def test_matrix_full_rank_requirement(self):
        """
        Test that the algorithm successfully generates full-rank matrices.

        This test verifies that the matrix generation and rank checking works correctly.
        """
        # Use various inputs
        # Start from 1 to avoid all-zero pre_pow_hash
        padding = bytes(32)
        nonce = bytes(8)
        for i in range(1, 11):
            pre_pow_hash = bytes([i] * 32)
            test_input = pre_pow_hash + TEST_VECTOR_1_TIMESTAMP + padding + nonce
            # Should not raise RuntimeError about failing to generate full-rank matrix
            result = kheavyhash(test_input)
            assert len(result) == 32


class TestHelperFunctions:
    """Test helper functions and utilities."""

    def test_bytes_to_nibbles(self):
        """Test bytes_to_nibbles conversion."""
        # Test basic conversion
        result = bytes_to_nibbles(b"\x12\x34")
        assert result == [1, 2, 3, 4]

        # Test all zeros
        result = bytes_to_nibbles(b"\x00\x00")
        assert result == [0, 0, 0, 0]

        # Test all ones
        result = bytes_to_nibbles(b"\xFF\xFF")
        assert result == [15, 15, 15, 15]

        # Test empty input
        result = bytes_to_nibbles(b"")
        assert result == []

        # Test type validation
        with pytest.raises(TypeError, match="must be bytes"):
            bytes_to_nibbles("not bytes")

    def test_nibbles_to_bytes(self):
        """Test nibbles_to_bytes conversion."""
        # Test basic conversion
        result = nibbles_to_bytes([1, 2, 3, 4])
        assert result == b"\x12\x34"

        # Test all zeros
        result = nibbles_to_bytes([0, 0, 0, 0])
        assert result == b"\x00\x00"

        # Test all ones
        result = nibbles_to_bytes([15, 15, 15, 15])
        assert result == b"\xFF\xFF"

        # Test empty input
        result = nibbles_to_bytes([])
        assert result == b""

        # Test odd length
        with pytest.raises(ValueError, match="even length"):
            nibbles_to_bytes([1, 2, 3])

        # Test out of range (too high)
        with pytest.raises(ValueError, match="range"):
            nibbles_to_bytes([16, 0])

        # Test out of range (negative)
        with pytest.raises(ValueError, match="range"):
            nibbles_to_bytes([-1, 0])

    def test_gf16_inverse(self):
        """Test GF(16) multiplicative inverse."""
        # Verify the inverse table is a valid permutation (each element 1-15 appears exactly once)
        inverses = [_gf16_inverse(i) for i in range(1, 16)]
        assert len(inverses) == len(set(inverses)), "Inverse table must be a permutation of 1-15"
        assert set(inverses) == set(range(1, 16)), "Inverse table must contain all elements 1-15"

        # Verify that each inverse is in valid range
        for i in range(1, 16):
            inv = _gf16_inverse(i)
            assert 1 <= inv <= 15, f"Inverse of {i} is {inv}, must be in range [1, 15]"

        # Test zero (should raise ValueError)
        with pytest.raises(ValueError, match="Cannot compute inverse of 0"):
            _gf16_inverse(0)

    def test_matrix_rank_gf16(self):
        """Test matrix rank computation over GF(16)."""
        # Test identity matrix (should have full rank)
        identity = [[1 if i == j else 0 for j in range(64)] for i in range(64)]
        rank = matrix_rank_gf16(identity)
        assert rank == 64, "Identity matrix should have full rank"

        # Test zero matrix (should have rank 0)
        zero_matrix = [[0 for _ in range(64)] for _ in range(64)]
        rank = matrix_rank_gf16(zero_matrix)
        assert rank == 0, "Zero matrix should have rank 0"

        # Test matrix with first row only (should have rank 1)
        single_row = [[1 if j < 10 else 0 for j in range(64)]]
        single_row.extend([[0 for _ in range(64)] for _ in range(63)])
        rank = matrix_rank_gf16(single_row)
        assert rank == 1, "Matrix with one non-zero row should have rank 1"

    def test_matrix_vector_multiply_gf16(self):
        """Test matrix-vector multiplication over GF(16)."""
        # Test with identity matrix
        identity = [[1 if i == j else 0 for j in range(64)] for i in range(64)]
        vector = list(range(64))
        result = matrix_vector_multiply_gf16(identity, vector)
        # With identity, result should be similar to vector (after scaling)
        assert len(result) == 64

        # Test with zero matrix
        zero_matrix = [[0 for _ in range(64)] for _ in range(64)]
        vector = [1] * 64
        result = matrix_vector_multiply_gf16(zero_matrix, vector)
        assert all(x == 0 for x in result), "Zero matrix should produce zero vector"

        # Test size validation
        with pytest.raises(ValueError, match="size"):
            matrix_vector_multiply_gf16([[1] * 64], [1] * 64)

    def test_xoshiro256_prng(self):
        """Test xoshiro256++ PRNG."""
        # Test with known seed
        seed = (0x1234567890ABCDEF, 0xFEDCBA0987654321, 0x1111222233334444, 0x5555666677778888)
        prng = Xoshiro256PlusPlus(seed)

        # Generate some numbers
        nums = [prng.next() for _ in range(10)]

        # Should all be 64-bit integers
        for num in nums:
            assert 0 <= num <= 0xFFFFFFFFFFFFFFFF

        # Should be deterministic
        prng2 = Xoshiro256PlusPlus(seed)
        nums2 = [prng2.next() for _ in range(10)]
        assert nums == nums2, "PRNG should be deterministic with same seed"

        # Test invalid seed
        with pytest.raises(ValueError, match="exactly 4"):
            Xoshiro256PlusPlus((1, 2, 3))


class TestFallbackMechanism:
    """Test the fallback mechanism between Cython and pure Python."""

    def test_fallback_to_native(self):
        """Test that fallback to native works when Cython extension unavailable."""
        # This test verifies the import fallback works
        # We can't easily test the Cython import failing, but we can verify
        # that the Python implementation is accessible

        # Import should work
        from kheavyhash import kheavyhash

        padding = bytes(32)
        nonce = bytes(8)
        test_input = TEST_VECTOR_1_PRE_POW_HASH + TEST_VECTOR_1_TIMESTAMP + padding + nonce

        # Should be callable
        result = kheavyhash(test_input)
        assert len(result) == 32

        # Verify we can also import Python implementation directly
        from kheavyhash.kheavyhash_python import kheavyhash as python_kheavyhash
        result2 = python_kheavyhash(test_input)
        assert result == result2

    def test_import_behavior(self):
        """Test that imports work correctly."""
        # Main import should work
        import kheavyhash
        assert hasattr(kheavyhash, "kheavyhash")

        padding = bytes(32)
        nonce = bytes(8)
        test_input = TEST_VECTOR_1_PRE_POW_HASH + TEST_VECTOR_1_TIMESTAMP + padding + nonce

        # Should be able to call it
        result = kheavyhash.kheavyhash(test_input)
        assert len(result) == 32


class TestGoBinaryComparison:
    """Tests comparing with Go reference implementation using the Go wrapper."""

    @pytest.fixture
    def go_wrapper_path(self):
        """Try to find or build the Go kheavyhash wrapper."""
        wrapper_path = Path("scripts/go_kheavyhash_wrapper")

        # If wrapper exists, use it
        if wrapper_path.exists() and wrapper_path.is_file():
            return str(wrapper_path)

        # Try to build it if Go is available
        try:
            result = subprocess.run(
                ["command", "-v", "go"],
                capture_output=True,
                text=True,
                timeout=1,
            )
            if result.returncode == 0:
                # Go is available, try to build the wrapper
                scripts_dir = Path("scripts")
                if (scripts_dir / "go_kheavyhash_wrapper.go").exists():
                    # Set up Go module if needed
                    if not (scripts_dir / "go.mod").exists():
                        subprocess.run(
                            ["go", "mod", "init", "kheavyhash_compare"],
                            cwd=scripts_dir,
                            capture_output=True,
                            timeout=10,
                        )
                    # Get the package
                    subprocess.run(
                        ["go", "get", "github.com/bcutil/kheavyhash@latest"],
                        cwd=scripts_dir,
                        capture_output=True,
                        timeout=30,
                    )
                    # Build the wrapper
                    build_result = subprocess.run(
                        ["go", "build", "-o", "go_kheavyhash_wrapper", "go_kheavyhash_wrapper.go"],
                        cwd=scripts_dir,
                        capture_output=True,
                        timeout=30,
                    )
                    if build_result.returncode == 0 and wrapper_path.exists():
                        return str(wrapper_path)
        except (subprocess.TimeoutExpired, FileNotFoundError):
            pass

        return None

    def test_go_binary_comparison_all_zeros(self, go_wrapper_path):
        """Compare output with Go reference implementation."""
        if go_wrapper_path is None:
            pytest.skip(
                "Go wrapper not available. "
                "Run 'make compare' to build it, or install Go and the kheavyhash package."
            )

        padding = bytes(32)
        nonce = bytes(8)
        test_input = TEST_VECTOR_1_PRE_POW_HASH + TEST_VECTOR_1_TIMESTAMP + padding + nonce
        python_result = kheavyhash(test_input)

        # Call Go wrapper with hex input
        hex_input = test_input.hex()

        try:
            result = subprocess.run(
                [go_wrapper_path, hex_input],
                capture_output=True,
                text=True,
                timeout=5,
            )

            if result.returncode != 0:
                pytest.fail(f"Go wrapper failed: {result.stderr}")

            go_output_hex = result.stdout.strip()
            # Remove any whitespace/newlines
            go_output_hex = "".join(go_output_hex.split())
            # Convert to bytes
            go_result = bytes.fromhex(go_output_hex)

            assert len(go_result) == 32, f"Go wrapper returned {len(go_result)} bytes, expected 32"
            assert python_result == go_result, (
                f"Python and Go results differ:\n"
                f"Python: {python_result.hex()}\n"
                f"Go:     {go_result.hex()}"
            )
        except (subprocess.TimeoutExpired, ValueError, FileNotFoundError) as e:
            pytest.fail(f"Could not run Go wrapper comparison: {e}")

    def test_go_binary_comparison_pattern(self, go_wrapper_path):
        """Compare output with Go reference implementation for pattern input."""
        if go_wrapper_path is None:
            pytest.skip(
                "Go wrapper not available. "
                "Run 'make compare' to build it, or install Go and the kheavyhash package."
            )

        # Create a test pattern
        pre_pow_hash = bytes(range(32))
        timestamp = TEST_VECTOR_1_TIMESTAMP
        padding = bytes(32)
        nonce = bytes([0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])  # 2 in little-endian

        test_input = pre_pow_hash + timestamp + padding + nonce
        python_result = kheavyhash(test_input)

        hex_input = test_input.hex()

        try:
            result = subprocess.run(
                [go_wrapper_path, hex_input],
                capture_output=True,
                text=True,
                timeout=5,
            )

            if result.returncode != 0:
                pytest.fail(f"Go wrapper failed: {result.stderr}")

            go_output_hex = result.stdout.strip()
            go_output_hex = "".join(go_output_hex.split())
            go_result = bytes.fromhex(go_output_hex)

            assert len(go_result) == 32, f"Go wrapper returned {len(go_result)} bytes, expected 32"
            assert python_result == go_result, (
                f"Python and Go results differ:\n"
                f"Python: {python_result.hex()}\n"
                f"Go:     {go_result.hex()}"
            )
        except (subprocess.TimeoutExpired, ValueError, FileNotFoundError) as e:
            pytest.fail(f"Could not run Go wrapper comparison: {e}")


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
