"""
Tests for lockstock_core.canonicalize — Layer 3 bencode canonical hashing.

These tests verify:
1. Bencode encoding of all JSON types
2. Canonical hash determinism
3. Round-trip (bencode → bdecode)
4. Type mapping edge cases (floats, bools, null)
5. Cross-language vectors matching Rust canonical.rs
"""

import hashlib
import json
import pytest

from lockstock_core.canonicalize import (
    bencode,
    bdecode,
    canonical_hash_from_bytes,
    canonical_hash_from_parsed,
    verify_hash,
    _bencode_str,
    _bencode_int,
    _bencode_bytes,
)


# ---------------------------------------------------------------------------
# Bencode primitives
# ---------------------------------------------------------------------------

class TestBencodeString:
    def test_empty_string(self):
        assert _bencode_str("") == b"0:"

    def test_simple_string(self):
        assert _bencode_str("hello") == b"5:hello"

    def test_agent_id(self):
        assert _bencode_str("agent_test_001") == b"14:agent_test_001"

    def test_unicode(self):
        # UTF-8 encoding: length is byte count, not char count
        result = _bencode_str("café")
        # "café" is 5 bytes in UTF-8 (c, a, f, 0xc3, 0xa9)
        assert result == b"5:caf\xc3\xa9"

    def test_matches_rust_bencode_string(self):
        """Must match Rust canonical.rs bencode_string() output."""
        # From canonical.rs test_canonical_bencode_known_vector:
        # "a" key → "1:a"
        assert _bencode_str("a") == b"1:a"
        # "at" key → "2:at"
        assert _bencode_str("at") == b"2:at"
        # "CHATCOMPLETION" value → "14:CHATCOMPLETION"
        assert _bencode_str("CHATCOMPLETION") == b"14:CHATCOMPLETION"


class TestBencodeInt:
    def test_zero(self):
        assert _bencode_int(0) == b"i0e"

    def test_positive(self):
        assert _bencode_int(42) == b"i42e"

    def test_negative(self):
        assert _bencode_int(-1) == b"i-1e"

    def test_large(self):
        assert _bencode_int(1700000000) == b"i1700000000e"

    def test_matches_rust_bencode_int(self):
        """Must match Rust canonical.rs bencode_int() output."""
        assert _bencode_int(2) == b"i2e"
        assert _bencode_int(1) == b"i1e"
        assert _bencode_int(42) == b"i42e"
        assert _bencode_int(1700000000) == b"i1700000000e"


class TestBencodeBytes:
    def test_empty(self):
        assert _bencode_bytes(b"") == b"0:"

    def test_binary(self):
        assert _bencode_bytes(b"\xff\x00\xfe") == b"3:\xff\x00\xfe"


# ---------------------------------------------------------------------------
# Bencode compound types
# ---------------------------------------------------------------------------

class TestBencodeCompound:
    def test_list(self):
        result = bencode([1, 2, 3])
        assert result == b"li1ei2ei3ee"

    def test_empty_list(self):
        assert bencode([]) == b"le"

    def test_nested_list(self):
        result = bencode([1, [2, 3]])
        assert result == b"li1eli2ei3eee"

    def test_dict_sorted_keys(self):
        """Dict keys must be sorted by raw byte value."""
        result = bencode({"b": 2, "a": 1})
        assert result == b"d1:ai1e1:bi2ee"

    def test_empty_dict(self):
        assert bencode({}) == b"de"

    def test_nested_dict(self):
        result = bencode({"outer": {"inner": 1}})
        assert result == b"d5:outerd5:inneri1eee"

    def test_mixed_types(self):
        result = bencode({"key": [1, "two", 3]})
        assert result == b"d3:keyli1e3:twoi3eee"


# ---------------------------------------------------------------------------
# JSON type mapping
# ---------------------------------------------------------------------------

class TestTypeMapping:
    def test_bool_true(self):
        """bool True → bencode integer 1."""
        assert bencode(True) == b"i1e"

    def test_bool_false(self):
        """bool False → bencode integer 0."""
        assert bencode(False) == b"i0e"

    def test_bool_before_int(self):
        """bool must be checked before int (bool is subclass of int in Python)."""
        # True == 1 in Python, but we want i1e not i1e (same result, but
        # the important thing is that True and 1 produce the same bencode)
        assert bencode(True) == bencode(1)
        assert bencode(False) == bencode(0)

    def test_none(self):
        """None → bencode string 'null'."""
        assert bencode(None) == b"4:null"

    def test_float(self):
        """float → bencode string of repr()."""
        result = bencode(3.14)
        assert result == _bencode_str(repr(3.14))

    def test_float_zero(self):
        result = bencode(0.0)
        assert result == _bencode_str(repr(0.0))

    def test_unsupported_type(self):
        with pytest.raises(TypeError, match="Cannot bencode type"):
            bencode(set())


# ---------------------------------------------------------------------------
# Canonical hash
# ---------------------------------------------------------------------------

class TestCanonicalHash:
    def test_determinism(self):
        """Same JSON bytes → same hash, every time."""
        body = b'{"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]}'
        h1 = canonical_hash_from_bytes(body)
        h2 = canonical_hash_from_bytes(body)
        assert h1 == h2
        assert len(h1) == 64
        assert all(c in "0123456789abcdef" for c in h1)

    def test_key_order_irrelevant(self):
        """Different JSON key ordering → same canonical hash."""
        body_a = b'{"b": 2, "a": 1}'
        body_b = b'{"a": 1, "b": 2}'
        assert canonical_hash_from_bytes(body_a) == canonical_hash_from_bytes(body_b)

    def test_whitespace_irrelevant(self):
        """Different JSON whitespace → same canonical hash."""
        body_compact = b'{"a":1,"b":2}'
        body_pretty = b'{\n  "a": 1,\n  "b": 2\n}'
        assert canonical_hash_from_bytes(body_compact) == canonical_hash_from_bytes(body_pretty)

    def test_from_parsed(self):
        """canonical_hash_from_parsed agrees with canonical_hash_from_bytes."""
        body = b'{"model": "gpt-4", "temperature": 0.7}'
        parsed = json.loads(body)
        assert canonical_hash_from_bytes(body) == canonical_hash_from_parsed(parsed)

    def test_verify_hash_pass(self):
        body = b'{"hello": "world"}'
        h = canonical_hash_from_bytes(body)
        assert verify_hash(body, h) is True

    def test_verify_hash_fail(self):
        body = b'{"hello": "world"}'
        assert verify_hash(body, "0" * 64) is False

    def test_different_content_different_hash(self):
        """Different payloads → different hashes."""
        body_a = b'{"prompt": "hello"}'
        body_b = b'{"prompt": "goodbye"}'
        assert canonical_hash_from_bytes(body_a) != canonical_hash_from_bytes(body_b)

    def test_invalid_json_raises(self):
        with pytest.raises(json.JSONDecodeError):
            canonical_hash_from_bytes(b"not json")

    def test_known_vector_simple(self):
        """Known test vector: {"a": 1} → bencode "d1:ai1ee" → SHA256."""
        body = b'{"a": 1}'
        expected_bencode = b"d1:ai1ee"
        expected_hash = hashlib.sha256(expected_bencode).hexdigest()
        assert canonical_hash_from_bytes(body) == expected_hash

    def test_known_vector_chatcompletion(self):
        """Known test vector simulating a chat completion request."""
        body = json.dumps({
            "model": "trinity",
            "messages": [
                {"role": "user", "content": "analyze this transaction"}
            ],
            "max_tokens": 2048,
            "temperature": 0.7,
        }).encode("utf-8")

        # Compute expected: parse → bencode → SHA256
        parsed = json.loads(body)
        bencoded = bencode(parsed)
        expected = hashlib.sha256(bencoded).hexdigest()

        assert canonical_hash_from_bytes(body) == expected

        # Verify the bencode is deterministic by checking byte content
        # Dict keys sorted: "max_tokens", "messages", "model", "temperature"
        assert bencoded.startswith(b"d")
        assert bencoded.endswith(b"e")

        # Record the reference vector for cross-language testing
        print(f"ChatCompletion request canonical hash: {expected}")
        print(f"Bencoded bytes ({len(bencoded)}): {bencoded!r}")


# ---------------------------------------------------------------------------
# Round-trip: bencode → bdecode
# ---------------------------------------------------------------------------

class TestRoundTrip:
    def test_string(self):
        assert bdecode(bencode("hello")) == "hello"

    def test_int(self):
        assert bdecode(bencode(42)) == 42

    def test_list(self):
        assert bdecode(bencode([1, "two", 3])) == [1, "two", 3]

    def test_dict(self):
        original = {"b": 2, "a": 1}
        decoded = bdecode(bencode(original))
        assert decoded == original

    def test_nested(self):
        original = {
            "messages": [
                {"role": "user", "content": "hello"},
                {"role": "assistant", "content": "hi"},
            ],
            "model": "gpt-4",
        }
        decoded = bdecode(bencode(original))
        assert decoded == original

    def test_trailing_data_raises(self):
        with pytest.raises(ValueError, match="Trailing data"):
            bdecode(b"i42eXXX")

    def test_bool_roundtrip(self):
        """bool encodes as int, so round-trip yields int (0 or 1)."""
        assert bdecode(bencode(True)) == 1
        assert bdecode(bencode(False)) == 0

    def test_none_roundtrip(self):
        """None encodes as string 'null', so round-trip yields string."""
        assert bdecode(bencode(None)) == "null"


# ---------------------------------------------------------------------------
# Cross-language vectors (must match Rust canonical.rs)
# ---------------------------------------------------------------------------

class TestCrossLanguageVectors:
    """
    These vectors must produce identical SHA-256 hashes in both
    Python (this file) and Rust (guard_ingest/src/canonical.rs).
    """

    def test_empty_dict(self):
        """Bencode of {} is "de". SHA256 of "de" is a fixed known value."""
        bencoded = bencode({})
        assert bencoded == b"de"
        h = hashlib.sha256(b"de").hexdigest()
        assert canonical_hash_from_parsed({}) == h
        print(f"Cross-language vector: empty dict hash = {h}")

    def test_simple_kv(self):
        """Bencode of {"a": 1} is "d1:ai1ee"."""
        bencoded = bencode({"a": 1})
        assert bencoded == b"d1:ai1ee"
        h = hashlib.sha256(b"d1:ai1ee").hexdigest()
        assert canonical_hash_from_parsed({"a": 1}) == h
        print(f"Cross-language vector: {{a:1}} hash = {h}")

    def test_key_sort_order(self):
        """Bencode of {"b": 2, "a": 1} must sort keys → "d1:ai1e1:bi2ee"."""
        bencoded = bencode({"b": 2, "a": 1})
        assert bencoded == b"d1:ai1e1:bi2ee"

    # =========================================================================
    # Hardcoded cross-language vectors (must match Rust guard_ingest tests)
    # =========================================================================

    def test_cross_vector_simple_dict(self):
        """{"a": 1, "b": "hello"} → known SHA-256."""
        h = canonical_hash_from_bytes(b'{"a": 1, "b": "hello"}')
        assert h == "80507c83acbfa16013c45da21de9b88f16be149943b192c2d8c2240691952b0f"

    def test_cross_vector_chat_completion(self):
        """Chat completion request body → known SHA-256."""
        body = b'{"messages": [{"content": "Hello", "role": "user"}], "model": "gpt-4"}'
        h = canonical_hash_from_bytes(body)
        assert h == "ff9dd5713358fb1df077282d0f1407d65a3c530ea88ea2cc34ae25e5a0a96251"

    def test_cross_vector_empty_dict(self):
        """{} → known SHA-256."""
        h = canonical_hash_from_bytes(b'{}')
        assert h == "959a45d44e6fcf58361ed004681556fe50129f2109e817dec098c00c9e5d2578"

    def test_cross_vector_mixed_types(self):
        """Dict with bool, float, null → known SHA-256."""
        h = canonical_hash_from_bytes(b'{"active": true, "rate": 0.7, "value": null}')
        assert h == "8a3edc0097ed185170223aa5f1a27c6215f0369a678caee0754af2679c21781e"
