"""Tests for canonical hashing (JCS + SHA-256) — stability and correctness."""

from __future__ import annotations

import hashlib

import pytest

from abi_control_core.sdk.canonical_hash import (
    canonical_hash,
    canonical_hash_jcs_sha256,
    canonical_serialize,
    jcs_serialize,
)


class TestJcsSerialize:
    """RFC 8785 serialization correctness."""

    def test_empty_object(self) -> None:
        assert jcs_serialize({}) == b"{}"

    def test_empty_array(self) -> None:
        assert jcs_serialize([]) == b"[]"

    def test_null(self) -> None:
        assert jcs_serialize(None) == b"null"

    def test_true(self) -> None:
        assert jcs_serialize(True) == b"true"

    def test_false(self) -> None:
        assert jcs_serialize(False) == b"false"

    def test_integer(self) -> None:
        assert jcs_serialize(42) == b"42"

    def test_negative_integer(self) -> None:
        assert jcs_serialize(-1) == b"-1"

    def test_zero(self) -> None:
        assert jcs_serialize(0) == b"0"

    def test_float_zero(self) -> None:
        assert jcs_serialize(0.0) == b"0"

    def test_string(self) -> None:
        assert jcs_serialize("hello") == b'"hello"'

    def test_string_with_quotes(self) -> None:
        assert jcs_serialize('say "hi"') == b'"say \\"hi\\""'

    def test_string_with_backslash(self) -> None:
        assert jcs_serialize("a\\b") == b'"a\\\\b"'

    def test_object_key_sorting(self) -> None:
        """Keys must be sorted lexicographically."""
        obj = {"b": 2, "a": 1, "c": 3}
        assert jcs_serialize(obj) == b'{"a":1,"b":2,"c":3}'

    def test_nested_object_sorting(self) -> None:
        obj = {"z": {"b": 2, "a": 1}, "a": [3, 2, 1]}
        expected = b'{"a":[3,2,1],"z":{"a":1,"b":2}}'
        assert jcs_serialize(obj) == expected

    def test_array_order_preserved(self) -> None:
        assert jcs_serialize([3, 1, 2]) == b"[3,1,2]"

    def test_unicode_string(self) -> None:
        result = jcs_serialize("café")
        assert result == "\"café\"".encode()

    def test_control_char_escaped(self) -> None:
        result = jcs_serialize("a\nb")
        assert result == b'"a\\nb"'


class TestCanonicalHash:
    """SHA-256 hash stability — these digests must never change."""

    def test_empty_object_hash(self) -> None:
        digest = canonical_hash_jcs_sha256({})
        expected = hashlib.sha256(b"{}").hexdigest()
        assert digest == expected

    def test_simple_manifest_hash_stable(self) -> None:
        """A known object must produce a stable, reproducible hash."""
        obj = {
            "schema_version": "run-manifest/1.0",
            "run_id": "abcd1234",
            "verdict": "pass",
        }
        digest1 = canonical_hash_jcs_sha256(obj)
        digest2 = canonical_hash_jcs_sha256(obj)
        assert digest1 == digest2
        assert len(digest1) == 64

    def test_key_order_does_not_affect_hash(self) -> None:
        """Two dicts with same content but different insertion order must hash the same."""
        obj_a = {"z": 1, "a": 2, "m": 3}
        obj_b = {"a": 2, "m": 3, "z": 1}
        assert canonical_hash_jcs_sha256(obj_a) == canonical_hash_jcs_sha256(obj_b)

    def test_different_values_produce_different_hash(self) -> None:
        obj_a = {"x": 1}
        obj_b = {"x": 2}
        assert canonical_hash_jcs_sha256(obj_a) != canonical_hash_jcs_sha256(obj_b)

    def test_known_vector(self) -> None:
        """Verify against a hand-computed known hash vector."""
        # JCS of {"a":1} is exactly {"a":1} (7 bytes)
        canonical = b'{"a":1}'
        expected_digest = hashlib.sha256(canonical).hexdigest()
        assert canonical_hash_jcs_sha256({"a": 1}) == expected_digest

    def test_complex_object_stability(self) -> None:
        """A complex nested object must hash deterministically."""
        obj = {
            "run_id": "abcd1234",
            "summary": {"total": 5, "passed": 5, "failed": 0},
            "tags": ["a", "b", "c"],
            "nested": {"x": {"y": True, "z": None}},
        }
        # Run multiple times to confirm stability
        digests = {canonical_hash_jcs_sha256(obj) for _ in range(10)}
        assert len(digests) == 1

    def test_rejects_nan(self) -> None:
        with pytest.raises(ValueError, match="JCS does not support"):
            canonical_hash_jcs_sha256({"x": float("nan")})

    def test_rejects_infinity(self) -> None:
        with pytest.raises(ValueError, match="JCS does not support"):
            canonical_hash_jcs_sha256({"x": float("inf")})


class TestCrossRepoCompatibility:
    """Cross-repo hash consistency — ensure aliases work correctly."""

    def test_canonical_serialize_alias(self) -> None:
        """canonical_serialize must be an alias for jcs_serialize."""
        obj = {"z": 1, "a": [1, 2, 3], "m": {"nested": True}}
        assert canonical_serialize(obj) == jcs_serialize(obj)

    def test_canonical_hash_alias(self) -> None:
        """canonical_hash must be an alias for canonical_hash_jcs_sha256."""
        obj = {"z": 1, "a": [1, 2, 3], "m": {"nested": True}}
        assert canonical_hash(obj) == canonical_hash_jcs_sha256(obj)

    def test_fallback_implementation_consistency(self) -> None:
        """Fallback implementation (used by other repos) must produce identical hash.

        This tests the fallback logic that other repos use when abi-core is not installed.
        The fallback MUST delegate to the RFC 8785 implementation via import, or use
        an identical algorithm (json.dumps with sort_keys, separators, ensure_ascii).

        Cross-repo consistency is CRITICAL for determinism gates.
        """
        import hashlib
        import json

        # Test objects spanning simple and complex cases
        test_cases = [
            {"a": 1, "b": 2, "c": 3},  # Simple sorted dict
            {"z": 1, "a": 2, "m": 3},  # Unsorted dict
            {"nested": {"x": [1, 2, 3], "y": True}},  # Nested structures
            {"float": 1.5, "int": 42, "null": None},  # Mixed types
        ]

        for obj in test_cases:
            # Recommended fallback implementation (RFC 8785 compliant)
            fallback_bytes = json.dumps(
                obj, sort_keys=True, separators=(",", ":"), ensure_ascii=True
            ).encode()
            fallback_hash = hashlib.sha256(fallback_bytes).hexdigest()

            # RFC 8785 implementation
            jcs_hash = canonical_hash(obj)

            # They MUST match for cross-repo determinism
            assert fallback_hash == jcs_hash, (
                f"Fallback hash differs from JCS hash.\n"
                f"Object: {obj}\n"
                f"Fallback: {fallback_hash}\n"
                f"JCS:      {jcs_hash}\n"
                f"This breaks cross-repo determinism."
            )

    def test_complex_object_fallback_divergence(self) -> None:
        """Document that fallback MAY diverge from RFC 8785 for complex objects.

        This test exists to document that the fallback is NOT RFC 8785 compliant
        and may produce different hashes for edge cases (floats, unicode, etc.).
        """
        import hashlib
        import json

        # Object with float that might serialize differently
        obj_with_float = {"value": 1.5e-7}

        fallback_bytes = json.dumps(
            obj_with_float, sort_keys=True, separators=(",", ":"), ensure_ascii=True
        ).encode()
        fallback_hash = hashlib.sha256(fallback_bytes).hexdigest()

        jcs_hash = canonical_hash(obj_with_float)

        # For this simple float, they might actually match, but we document
        # that they are NOT guaranteed to match in general
        # We don't assert here, just log for documentation purposes
        if fallback_hash != jcs_hash:
            pytest.skip(
                f"Fallback diverged from JCS (expected for complex types):\n"
                f"Fallback: {fallback_hash}\n"
                f"JCS:      {jcs_hash}"
            )
