"""SP17 — low-level backup crypto tests.

Pure-Python, no DB. Covers key derivation determinism, encrypt /
decrypt round-trip, tampered-ciphertext failure, wrong-passphrase
failure, and the sidecar JSON format.
"""
from __future__ import annotations

import json
import os

import pytest

from cyclone import backup as backup_mod


# ---------------------------------------------------------------------------
# Key derivation
# ---------------------------------------------------------------------------


def test_derive_key_is_deterministic():
    salt = os.urandom(16)
    k1 = backup_mod.derive_key("correct horse battery staple", salt)
    k2 = backup_mod.derive_key("correct horse battery staple", salt)
    assert k1 == k2
    assert len(k1) == backup_mod.KEY_LEN == 32


def test_derive_key_different_salts_produce_different_keys():
    """Salt is what makes the same passphrase produce different keys."""
    k1 = backup_mod.derive_key("hunter2", os.urandom(16))
    k2 = backup_mod.derive_key("hunter2", os.urandom(16))
    assert k1 != k2


def test_derive_key_different_passphrases_produce_different_keys():
    salt = os.urandom(16)
    k1 = backup_mod.derive_key("a", salt)
    k2 = backup_mod.derive_key("b", salt)
    assert k1 != k2


# ---------------------------------------------------------------------------
# Encrypt / decrypt round-trip
# ---------------------------------------------------------------------------


def test_encrypt_decrypt_roundtrip():
    key = os.urandom(32)
    plaintext = b"hello cyclone backup " * 1000
    blob = backup_mod.encrypt(plaintext, key)
    assert len(blob) == backup_mod.NONCE_LEN + len(plaintext) + 16  # tag
    out = backup_mod.decrypt(blob, key)
    assert out == plaintext


def test_encrypt_decrypt_empty_plaintext():
    """Edge case: zero-byte payload still produces nonce + tag."""
    key = os.urandom(32)
    blob = backup_mod.encrypt(b"", key)
    out = backup_mod.decrypt(blob, key)
    assert out == b""


def test_decrypt_with_wrong_key_raises():
    plaintext = b"some bytes"
    key1 = os.urandom(32)
    key2 = os.urandom(32)
    blob = backup_mod.encrypt(plaintext, key1)
    with pytest.raises(backup_mod.BackupDecryptError):
        backup_mod.decrypt(blob, key2)


def test_decrypt_tampered_ciphertext_raises():
    """Flipping a single ciphertext byte must fail GCM auth."""
    key = os.urandom(32)
    blob = backup_mod.encrypt(b"a" * 200, key)
    tampered = bytearray(blob)
    # Flip a bit somewhere in the ciphertext region (past the nonce).
    tampered[backup_mod.NONCE_LEN + 5] ^= 0x01
    with pytest.raises(backup_mod.BackupDecryptError):
        backup_mod.decrypt(bytes(tampered), key)


def test_decrypt_truncated_blob_raises():
    key = os.urandom(32)
    blob = backup_mod.encrypt(b"x" * 100, key)
    with pytest.raises(backup_mod.BackupDecryptError):
        # Strip the GCM tag.
        backup_mod.decrypt(blob[: -16], key)


def test_encrypt_with_wrong_key_length_raises():
    with pytest.raises(backup_mod.BackupError):
        backup_mod.encrypt(b"data", b"short")  # not 32 bytes


# ---------------------------------------------------------------------------
# Fingerprint
# ---------------------------------------------------------------------------


def test_fingerprint_format_and_stability():
    fp = backup_mod.fingerprint(b"hello")
    assert fp.startswith("sha256:")
    assert len(fp) == len("sha256:") + 64
    assert fp == backup_mod.fingerprint(b"hello")
    assert fp != backup_mod.fingerprint(b"hellp")


def test_fingerprint_file_matches_fingerprint_bytes(tmp_path):
    p = tmp_path / "data.bin"
    p.write_bytes(b"\x00\x01\x02" * 100)
    assert backup_mod.fingerprint_file(p) == backup_mod.fingerprint(p.read_bytes())


# ---------------------------------------------------------------------------
# Sidecar
# ---------------------------------------------------------------------------


def test_sidecar_round_trip_json():
    sc = backup_mod.Sidecar(
        format_version="v1",
        created_at="2026-06-21T15:30:00+00:00",
        db_fingerprint="sha256:" + "a" * 64,
        table_count=11,
        size_bytes=1024,
        kdf="PBKDF2-HMAC-SHA256",
        kdf_iterations=200_000,
        cipher="AES-256-GCM",
        key_fingerprint="sha256:" + "b" * 64,
    )
    text = sc.to_json()
    parsed = json.loads(text)
    assert parsed["format_version"] == "v1"
    assert parsed["encryption"]["kdf_iterations"] == 200_000
    sc2 = backup_mod.Sidecar.from_json(text)
    assert sc2 == sc


# ---------------------------------------------------------------------------
# Filenames
# ---------------------------------------------------------------------------


def test_backup_filename_format():
    """The timestamp prefix is fixed; the suffix is random per call."""
    import re
    from datetime import datetime, timezone
    ts = datetime(2026, 6, 21, 15, 30, 0, tzinfo=timezone.utc)
    name = backup_mod.backup_filename(ts)
    assert re.match(r"^cyclone-backup-20260621T153000Z-[0-9a-f]{8}\.bin$", name), name


def test_backup_filename_random_suffix_avoids_collisions():
    """Two calls in the same second get different filenames."""
    a = backup_mod.backup_filename()
    b = backup_mod.backup_filename()
    assert a != b


def test_sidecar_filename_appends_meta_json():
    assert backup_mod.sidecar_filename("foo.bin") == "foo.bin.meta.json"
