"""Tests for canonical JSON serialization.

These tests verify the FROZEN serialization spec. If any of these
fail after a code change, you've broken the canonical form and
all existing hash chains are invalid.
"""

from __future__ import annotations

from datetime import UTC, datetime

from attestmcp.canonical import (
    GENESIS_HASH,
    canonical_json,
    compute_chain_hash,
    compute_hash,
    compute_params_hash,
    compute_result_hash,
    normalize_string,
)


class TestCanonicalJson:
    """Canonical JSON must be deterministic and sorted."""

    def test_keys_sorted_alphabetically(self) -> None:
        obj = {"zebra": 1, "apple": 2, "mango": 3}
        result = canonical_json(obj)
        assert result == '{"apple":2,"mango":3,"zebra":1}'

    def test_no_whitespace(self) -> None:
        obj = {"key": "value", "nested": {"a": 1}}
        result = canonical_json(obj)
        assert " " not in result
        assert "\n" not in result

    def test_nested_objects_sorted(self) -> None:
        obj = {"outer": {"z": 1, "a": 2}, "inner": 3}
        result = canonical_json(obj)
        assert result == '{"inner":3,"outer":{"a":2,"z":1}}'

    def test_none_values_serialized_as_null(self) -> None:
        obj = {"key": None}
        result = canonical_json(obj)
        assert result == '{"key":null}'

    def test_datetime_serialized_as_iso8601(self) -> None:
        dt = datetime(2026, 3, 21, 14, 32, 1, 847392, tzinfo=UTC)
        obj = {"timestamp": dt}
        result = canonical_json(obj)
        assert "2026-03-21T14:32:01.847392+00:00" in result

    def test_exclude_fields(self) -> None:
        obj = {"keep": 1, "drop": 2, "also_keep": 3}
        result = canonical_json(obj, exclude_fields={"drop"})
        assert '"drop"' not in result
        assert '"keep":1' in result
        assert '"also_keep":3' in result

    def test_deterministic_repeated_calls(self) -> None:
        obj = {"b": 2, "a": 1, "c": {"z": 9, "y": 8}}
        results = [canonical_json(obj) for _ in range(100)]
        assert len(set(results)) == 1

    def test_unicode_preserved(self) -> None:
        obj = {"name": "café"}
        result = canonical_json(obj)
        assert "café" in result


class TestNormalizeString:
    """Unicode NFC normalization."""

    def test_nfc_normalization(self) -> None:
        # é as e + combining accent (NFD) should normalize to single codepoint (NFC)
        nfd = "e\u0301"  # e + combining acute accent
        nfc = "\u00e9"  # é as single codepoint
        assert normalize_string(nfd) == nfc

    def test_already_nfc(self) -> None:
        text = "hello world"
        assert normalize_string(text) == text


class TestComputeHash:
    """SHA-256 hashing."""

    def test_empty_string(self) -> None:
        result = compute_hash("")
        assert len(result) == 64
        # Known SHA-256 of empty string
        assert result == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"

    def test_deterministic(self) -> None:
        assert compute_hash("test") == compute_hash("test")

    def test_different_inputs_different_hashes(self) -> None:
        assert compute_hash("a") != compute_hash("b")


class TestComputeParamsHash:
    """Request parameter hashing."""

    def test_none_params(self) -> None:
        result = compute_params_hash(None)
        assert len(result) == 64

    def test_dict_params(self) -> None:
        result = compute_params_hash({"query": "test", "limit": 10})
        assert len(result) == 64

    def test_key_order_irrelevant(self) -> None:
        h1 = compute_params_hash({"b": 2, "a": 1})
        h2 = compute_params_hash({"a": 1, "b": 2})
        assert h1 == h2


class TestComputeResultHash:
    """Response body hashing."""

    def test_none_result(self) -> None:
        assert compute_result_hash(None) is None

    def test_string_result(self) -> None:
        result = compute_result_hash('{"ok": true}')
        assert result is not None
        assert len(result) == 64

    def test_bytes_result(self) -> None:
        result = compute_result_hash(b'{"ok": true}')
        assert result is not None
        assert len(result) == 64


class TestComputeChainHash:
    """Hash chain computation."""

    def test_genesis(self) -> None:
        result = compute_chain_hash(GENESIS_HASH, '{"test":1}')
        assert len(result) == 64

    def test_chain_depends_on_previous(self) -> None:
        h1 = compute_chain_hash("aaa", '{"test":1}')
        h2 = compute_chain_hash("bbb", '{"test":1}')
        assert h1 != h2

    def test_chain_depends_on_record(self) -> None:
        h1 = compute_chain_hash(GENESIS_HASH, '{"test":1}')
        h2 = compute_chain_hash(GENESIS_HASH, '{"test":2}')
        assert h1 != h2

    def test_deterministic(self) -> None:
        h1 = compute_chain_hash(GENESIS_HASH, '{"data":"hello"}')
        h2 = compute_chain_hash(GENESIS_HASH, '{"data":"hello"}')
        assert h1 == h2
