"""Property-based tests using Hypothesis.

Tests invariant properties of critical Census functions:
- Hash determinism and format
- Manifest round-trip persistence
- Size parsing inverse relationships
- Seal/verify cycle correctness
- Encryption round-trip integrity
"""

from __future__ import annotations

import re
import string
import tempfile
from pathlib import Path

from hypothesis import given, settings, assume, HealthCheck
from hypothesis import strategies as st

from certisigma_census.manifest import Manifest, ManifestEntry


_hex64 = st.text(alphabet="0123456789abcdef", min_size=64, max_size=64)
_relpath = st.text(
    alphabet=string.ascii_letters + string.digits + "/_.-",
    min_size=1,
    max_size=200,
).filter(lambda s: not s.startswith("/") and "\x00" not in s)
_size = st.integers(min_value=0, max_value=2**40)
_mtime = st.floats(min_value=0.0, max_value=2**40, allow_nan=False, allow_infinity=False)


# ── Hash determinism ──────────────────────────────────────────────────


class TestHashProperties:
    @given(data=st.binary(min_size=1, max_size=4096))
    @settings(max_examples=50)
    def test_hash_bytes_deterministic(self, data: bytes) -> None:
        from certisigma import hash_bytes

        h1 = hash_bytes(data)
        h2 = hash_bytes(data)
        assert h1 == h2

    @given(data=st.binary(min_size=1, max_size=4096))
    @settings(max_examples=50)
    def test_hash_bytes_format(self, data: bytes) -> None:
        from certisigma import hash_bytes

        h = hash_bytes(data)
        assert re.fullmatch(r"[0-9a-f]{64}", h), f"Invalid hash format: {h}"

    @given(data=st.binary(min_size=1, max_size=4096))
    @settings(max_examples=30, suppress_health_check=[HealthCheck.function_scoped_fixture])
    def test_hash_file_matches_hash_bytes(self, data: bytes, tmp_path: Path) -> None:
        from certisigma import hash_bytes, hash_file

        p = tmp_path / "test.bin"
        p.write_bytes(data)
        assert hash_file(str(p)) == hash_bytes(data)


# ── Manifest round-trip ───────────────────────────────────────────────


class TestManifestProperties:
    @given(hash_hex=_hex64, path=_relpath, size=_size, mtime=_mtime)
    @settings(max_examples=50)
    def test_entry_round_trip(self, hash_hex: str, path: str, size: int, mtime: float) -> None:
        m = Manifest(root_dir="/data")
        try:
            entry = ManifestEntry(hash_hex=hash_hex, path=path, size=size, mtime=mtime)
            m.add(entry)
            loaded = m.get(hash_hex)
            assert loaded is not None
            assert loaded.hash_hex == hash_hex
            assert loaded.path == path
            assert loaded.size == size
        finally:
            m.close()

    @given(hash_hex=_hex64, path=_relpath, size=_size, mtime=_mtime)
    @settings(max_examples=30)
    def test_save_load_round_trip(self, hash_hex: str, path: str, size: int, mtime: float) -> None:
        with tempfile.TemporaryDirectory() as td:
            db = Path(td) / "manifest.db"
            m = Manifest(root_dir="/data")
            try:
                m.add(ManifestEntry(hash_hex=hash_hex, path=path, size=size, mtime=mtime))
                m.save(db)
            finally:
                m.close()

            m2 = Manifest.load(db)
            try:
                assert m2.total == 1
                loaded = m2.get(hash_hex)
                assert loaded is not None
                assert loaded.path == path
            finally:
                m2.close()


# ── Size parsing ──────────────────────────────────────────────────────


class TestSizeParsingProperties:
    @given(n=st.integers(min_value=0, max_value=10_000))
    @settings(max_examples=50)
    def test_plain_number_round_trips(self, n: int) -> None:
        from certisigma_census.cli import _parse_size

        assert _parse_size(str(n)) == n

    @given(n=st.integers(min_value=1, max_value=1000))
    @settings(max_examples=30)
    def test_kb_is_1024x(self, n: int) -> None:
        from certisigma_census.cli import _parse_size

        assert _parse_size(f"{n}K") == n * 1024

    @given(n=st.integers(min_value=1, max_value=100))
    @settings(max_examples=30)
    def test_mb_is_1048576x(self, n: int) -> None:
        from certisigma_census.cli import _parse_size

        assert _parse_size(f"{n}M") == n * 1024 * 1024


# ── Seal/verify cycle ────────────────────────────────────────────────


class TestSealProperties:
    _hex_key = st.text(alphabet="0123456789abcdef", min_size=64, max_size=64)

    @given(key=_hex_key)
    @settings(max_examples=30)
    def test_seal_verify_round_trip(self, key: str) -> None:
        from certisigma_census.seal import create_seal, verify_seal

        with tempfile.TemporaryDirectory() as td:
            db = Path(td) / "manifest.db"
            m = Manifest(root_dir="/data")
            try:
                m.add(ManifestEntry(hash_hex="aa" * 32, path="a.txt", size=10, mtime=1.0))
                m.save(db)
            finally:
                m.close()

            create_seal(db, key)
            result = verify_seal(db, key)
            assert result.valid is True

    @given(
        key=_hex_key,
        wrong_key=_hex_key,
    )
    @settings(max_examples=30)
    def test_wrong_key_fails_verification(self, key: str, wrong_key: str) -> None:
        assume(key != wrong_key)
        from certisigma_census.seal import create_seal, verify_seal

        with tempfile.TemporaryDirectory() as td:
            db = Path(td) / "manifest.db"
            m = Manifest(root_dir="/data")
            try:
                m.add(ManifestEntry(hash_hex="bb" * 32, path="b.txt", size=20, mtime=2.0))
                m.save(db)
            finally:
                m.close()

            create_seal(db, key)
            result = verify_seal(db, wrong_key)
            assert result.valid is False


# ── Encryption round-trip ─────────────────────────────────────────────


class TestEncryptionProperties:
    @given(data=st.binary(min_size=1, max_size=8192))
    @settings(max_examples=30)
    def test_encrypt_decrypt_round_trip(self, data: bytes) -> None:
        from certisigma_census.encryption import decrypt_file, encrypt_file

        key = "aa" * 32

        with tempfile.TemporaryDirectory() as td:
            src = Path(td) / "plain.bin"
            enc = Path(td) / "encrypted.bin"
            src.write_bytes(data)
            encrypt_file(src, enc, key)
            pt = decrypt_file(enc, key)
            assert pt == data

    @given(data=st.binary(min_size=1, max_size=4096))
    @settings(max_examples=20)
    def test_encrypted_format_has_magic(self, data: bytes) -> None:
        from certisigma_census.encryption import HEADER, encrypt_file

        key = "bb" * 32

        with tempfile.TemporaryDirectory() as td:
            src = Path(td) / "plain.bin"
            enc = Path(td) / "encrypted.bin"
            src.write_bytes(data)
            encrypt_file(src, enc, key)
            assert enc.read_bytes().startswith(HEADER)
