"""Security-specific tests: timing, replay, edge cases, finalization."""

import hmac
import inspect

import pytest

from ads_foundation.bst import BST, LEAF_HASH, verify_proof, generate_proof, insert_auth
from ads_foundation.core import (
    Auth,
    Mode,
    ProofStream,
    auth,
    prover_context,
    unauth,
    verifier_context,
)
from ads_foundation.errors import ADSError, VerificationError
from ads_foundation.hash import sha256, shallow_hash


def build_tree(values: list[int]) -> BST[int] | None:
    tree: BST[int] | None = None
    for v in values:
        tree = BST.insert(v, tree)
    return tree


class TestConstantTimeComparison:
    """Verify that hash comparisons use hmac.compare_digest."""

    def test_verify_proof_uses_constant_time(self):
        source = inspect.getsource(verify_proof)
        assert "hmac.compare_digest" in source

    def test_unauth_uses_constant_time(self):
        source = inspect.getsource(unauth)
        assert "hmac.compare_digest" in source


class TestTypeDiscriminationSecurity:
    """Ensure Python's 1 == True cannot cause hash collisions."""

    def test_int_1_vs_true(self):
        assert 1 == True  # Python says equal
        assert shallow_hash(1) != shallow_hash(True)  # but hashes must differ

    def test_int_0_vs_false(self):
        assert 0 == False
        assert shallow_hash(0) != shallow_hash(False)

    def test_int_1_vs_str_1(self):
        assert shallow_hash(1) != shallow_hash("1")

    def test_int_1_vs_float_1(self):
        assert shallow_hash(1) != shallow_hash(1.0)

    def test_int_0_vs_float_0(self):
        assert shallow_hash(0) != shallow_hash(0.0)


class TestProofStreamFinalization:
    """Ensure proof streams cannot be reused after context manager exit."""

    def test_prover_finalized_after_context(self):
        with prover_context() as ctx:
            auth(42, ctx)
        with pytest.raises(ADSError, match="finalized"):
            ctx.append(sha256(b"attack"))

    def test_verifier_finalized_after_context(self):
        h = sha256(b"x")
        with verifier_context([h]) as ctx:
            ctx.consume()
        with pytest.raises(ADSError, match="finalized"):
            ctx.consume()


class TestHashLengthValidation:
    """All hashes must be exactly 32 bytes."""

    def test_proof_stream_rejects_short_hash(self):
        ps = ProofStream(Mode.PROVER)
        with pytest.raises(AssertionError):
            ps.append(b"short")

    def test_proof_stream_rejects_long_hash(self):
        ps = ProofStream(Mode.PROVER)
        with pytest.raises(AssertionError):
            ps.append(b"\x00" * 64)

    def test_all_hashes_are_32_bytes(self):
        assert len(sha256(b"anything")) == 32
        assert len(shallow_hash(42)) == 32
        assert len(shallow_hash("hello")) == 32
        assert len(shallow_hash(True)) == 32
        assert len(shallow_hash(None)) == 32
        assert len(LEAF_HASH) == 32


class TestProofStreamCompleteness:
    """Verifier context must check all proof elements consumed."""

    def test_unused_elements_raise(self):
        with pytest.raises(VerificationError, match="unused proof elements"):
            with verifier_context([b"\x00" * 32, b"\x00" * 32]) as ctx:
                ctx.consume()  # consume only 1 of 2

    def test_empty_proof_no_error(self):
        with verifier_context([]) as ctx:
            pass  # no elements to consume, should be fine


class TestEmptyTreeEdgeCases:
    def test_proof_for_empty_tree(self):
        proof = generate_proof(42, None)
        assert not proof.found
        assert proof.path == []

    def test_verify_empty_proof_root_element(self):
        tree = BST(value=5)
        root_hash = shallow_hash(tree)
        proof = generate_proof(5, tree)
        assert proof.found
        assert proof.path == []  # root element has no proof path
        assert verify_proof(5, root_hash, proof)

    def test_singleton_tree_lookup(self):
        tree = BST(value=42)
        assert BST.lookup(42, tree) == 42
        assert BST.lookup(99, tree) is None


class TestAuthFrozen:
    """Auth values must be immutable."""

    def test_auth_is_frozen(self):
        a = Auth.prover(sha256(b"test"), 42)
        with pytest.raises(AttributeError):
            a._hash = b"\x00" * 32  # type: ignore[misc]

    def test_auth_value_not_directly_accessible_via_attribute(self):
        a = Auth.prover(sha256(b"test"), 42)
        # _value is private by convention; access via unwrap() only
        assert a.unwrap() == 42


class TestBSTFrozen:
    """BST nodes must be immutable."""

    def test_bst_is_frozen(self):
        tree = BST(value=5)
        with pytest.raises(AttributeError):
            tree.value = 10  # type: ignore[misc]


class TestReplayAttacks:
    """Proofs from one context should not work in another."""

    def test_membership_proof_not_replayable(self):
        """A membership proof for tree1 cannot verify against tree2's root."""
        tree1 = build_tree([5, 3, 7])
        tree2 = build_tree([10, 20, 30])
        root1 = shallow_hash(tree1)
        root2 = shallow_hash(tree2)

        proof = generate_proof(5, tree1)
        assert verify_proof(5, root1, proof)     # valid for tree1
        assert not verify_proof(5, root2, proof)  # invalid for tree2
