"""
Tests for SQLCipher key derivation functions.

These tests verify that password-to-key derivation is consistent,
deterministic, and secure.
"""

import shutil
import tempfile
from pathlib import Path

import pytest

from src.local_deep_research.database.sqlcipher_utils import (
    LEGACY_PBKDF2_SALT,
    _get_key_from_password,
    get_sqlcipher_settings,
    set_sqlcipher_key,
    set_sqlcipher_rekey,
)
from src.local_deep_research.database.sqlcipher_compat import (
    get_sqlcipher_module,
)


@pytest.fixture
def temp_db_path():
    """Create a temporary database path."""
    temp_dir = tempfile.mkdtemp()
    db_path = Path(temp_dir) / "test_key.db"
    yield db_path
    shutil.rmtree(temp_dir)


@pytest.fixture
def sqlcipher_module():
    """Get the SQLCipher module."""
    return get_sqlcipher_module()


class TestKeyDerivation:
    """Tests for password-to-key derivation."""

    def test_key_derivation_deterministic(self):
        """Verify same password always produces same key."""
        password = "test_password_123"

        # Clear cache to ensure fresh derivation
        _get_key_from_password.cache_clear()

        key1 = _get_key_from_password(
            password,
            LEGACY_PBKDF2_SALT,
            get_sqlcipher_settings()["kdf_iterations"],
        )
        key2 = _get_key_from_password(
            password,
            LEGACY_PBKDF2_SALT,
            get_sqlcipher_settings()["kdf_iterations"],
        )

        assert key1 == key2, "Same password should always produce same key"

    def test_key_derivation_different_passwords(self):
        """Verify different passwords produce different keys."""
        _get_key_from_password.cache_clear()

        key1 = _get_key_from_password(
            "password1",
            LEGACY_PBKDF2_SALT,
            get_sqlcipher_settings()["kdf_iterations"],
        )
        key2 = _get_key_from_password(
            "password2",
            LEGACY_PBKDF2_SALT,
            get_sqlcipher_settings()["kdf_iterations"],
        )

        assert key1 != key2, "Different passwords should produce different keys"

    def test_key_derivation_caching(self):
        """Verify key derivation results are cached correctly."""
        _get_key_from_password.cache_clear()

        password = "cached_test_password"
        key1 = _get_key_from_password(
            password,
            LEGACY_PBKDF2_SALT,
            get_sqlcipher_settings()["kdf_iterations"],
        )

        # Check cache info
        cache_info = _get_key_from_password.cache_info()
        assert cache_info.misses >= 1, "First call should be a cache miss"

        # Second call should hit cache
        key2 = _get_key_from_password(
            password,
            LEGACY_PBKDF2_SALT,
            get_sqlcipher_settings()["kdf_iterations"],
        )
        cache_info_after = _get_key_from_password.cache_info()
        assert cache_info_after.hits >= 1, "Second call should be a cache hit"

        assert key1 == key2, "Cached key should match original"

    def test_key_hex_encoding_only_hex_chars(self):
        """Verify derived key only contains hex characters (SQL injection safe)."""
        _get_key_from_password.cache_clear()

        key = _get_key_from_password(
            "any_password",
            LEGACY_PBKDF2_SALT,
            get_sqlcipher_settings()["kdf_iterations"],
        )
        hex_string = key.hex()

        # Verify only hex characters
        valid_hex_chars = set("0123456789abcdef")
        assert all(c in valid_hex_chars for c in hex_string), (
            "Key hex encoding should only contain [0-9a-f]"
        )

    def test_special_characters_in_password(self):
        """Verify passwords with special characters are handled correctly."""
        _get_key_from_password.cache_clear()

        special_passwords = [
            "password with spaces",
            "password'with'quotes",
            'password"with"double"quotes',
            "password\\with\\backslashes",
            "password\nwith\nnewlines",
            "password\twith\ttabs",
            "unicode: 日本語 emoji: 🔐🔑",
            "sql_injection: '; DROP TABLE users; --",
            "null_byte: \x00test",
        ]

        keys = []
        for password in special_passwords:
            try:
                key = _get_key_from_password(
                    password,
                    LEGACY_PBKDF2_SALT,
                    get_sqlcipher_settings()["kdf_iterations"],
                )
                keys.append(key)
                # Verify key is valid bytes
                assert isinstance(key, bytes), (
                    f"Key should be bytes for: {password}"
                )
                assert len(key) > 0, f"Key should not be empty for: {password}"
            except Exception as e:
                pytest.fail(f"Password '{password}' caused error: {e}")

        # All keys should be unique
        assert len(set(keys)) == len(keys), (
            "All special passwords should produce unique keys"
        )

    def test_key_length_correct(self):
        """Verify derived key has correct length (64 bytes = 512 bits for SHA512)."""
        _get_key_from_password.cache_clear()

        key = _get_key_from_password(
            "test_password",
            LEGACY_PBKDF2_SALT,
            get_sqlcipher_settings()["kdf_iterations"],
        )

        # SHA512-based PBKDF2 produces 64-byte key (512 bits)
        assert len(key) == 64, f"Key should be 64 bytes, got {len(key)}"

    def test_empty_password_produces_key(self):
        """Verify empty password still produces a valid key (validation is elsewhere)."""
        _get_key_from_password.cache_clear()

        # Note: Empty password validation happens in DatabaseManager, not in key derivation
        # The key derivation function itself should work with any input
        key = _get_key_from_password(
            "", LEGACY_PBKDF2_SALT, get_sqlcipher_settings()["kdf_iterations"]
        )
        assert isinstance(key, bytes), (
            "Empty password should still produce bytes"
        )
        assert len(key) == 64, (
            "Key length should be consistent (64 bytes for SHA512)"
        )


class TestKeyDerivationWithDatabase:
    """Tests for key derivation in actual database operations."""

    def test_derived_key_opens_database(self, sqlcipher_module, temp_db_path):
        """Verify derived key can create and open a database."""
        from src.local_deep_research.database.sqlcipher_utils import (
            apply_cipher_defaults_before_key,
        )

        password = "test_database_password"

        # Create database
        conn = sqlcipher_module.connect(str(temp_db_path))
        cursor = conn.cursor()
        # New database, so use creation_mode=True
        apply_cipher_defaults_before_key(cursor)
        set_sqlcipher_key(cursor, password)
        cursor.close()

        conn.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)")
        conn.execute("INSERT INTO test VALUES (1)")
        conn.commit()  # Important: commit before closing
        conn.close()

        # Reopen with same password
        from src.local_deep_research.database.sqlcipher_utils import (
            apply_sqlcipher_pragmas,
        )

        conn2 = sqlcipher_module.connect(str(temp_db_path))
        cursor2 = conn2.cursor()
        # Existing database: key first, then cipher_* pragmas
        set_sqlcipher_key(cursor2, password)
        apply_sqlcipher_pragmas(cursor2, creation_mode=False)
        cursor2.close()

        result = conn2.execute("SELECT * FROM test").fetchone()
        assert result == (1,), "Should be able to read data with same password"
        conn2.close()

    def test_wrong_password_cannot_open(self, sqlcipher_module, temp_db_path):
        """Verify wrong password cannot open database."""
        from src.local_deep_research.database.sqlcipher_utils import (
            apply_cipher_defaults_before_key,
        )

        # Create database with password1
        conn = sqlcipher_module.connect(str(temp_db_path))
        cursor = conn.cursor()
        # New database, so use creation_mode=True
        apply_cipher_defaults_before_key(cursor)
        set_sqlcipher_key(cursor, "password1")
        cursor.close()

        conn.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)")
        conn.commit()
        conn.close()

        # Try to open with password2
        from src.local_deep_research.database.sqlcipher_utils import (
            apply_sqlcipher_pragmas,
        )

        conn2 = sqlcipher_module.connect(str(temp_db_path))
        cursor2 = conn2.cursor()
        # Existing database: key first, then cipher_* pragmas
        set_sqlcipher_key(cursor2, "password2")
        apply_sqlcipher_pragmas(cursor2, creation_mode=False)
        cursor2.close()

        # Should fail when trying to access data
        with pytest.raises(Exception):
            conn2.execute("SELECT * FROM test").fetchone()

        conn2.close()

    def test_rekey_uses_same_derivation(self, sqlcipher_module, temp_db_path):
        """Verify rekey produces key compatible with set_sqlcipher_key."""
        from src.local_deep_research.database.sqlcipher_utils import (
            apply_cipher_defaults_before_key,
        )

        # Create database
        conn = sqlcipher_module.connect(str(temp_db_path))
        cursor = conn.cursor()
        # New database, so use creation_mode=True
        apply_cipher_defaults_before_key(cursor)
        set_sqlcipher_key(cursor, "original_password")
        cursor.close()

        conn.execute("CREATE TABLE test (id INTEGER, value TEXT)")
        conn.execute("INSERT INTO test VALUES (1, 'secret_data')")
        conn.commit()

        # Rekey to new password
        set_sqlcipher_rekey(conn, "new_password")
        conn.close()

        # Open with new password using set_sqlcipher_key
        from src.local_deep_research.database.sqlcipher_utils import (
            apply_sqlcipher_pragmas,
        )

        conn2 = sqlcipher_module.connect(str(temp_db_path))
        cursor2 = conn2.cursor()
        # Existing database: key first, then cipher_* pragmas
        set_sqlcipher_key(cursor2, "new_password")
        apply_sqlcipher_pragmas(cursor2, creation_mode=False)
        cursor2.close()

        # Should be able to read data
        result = conn2.execute("SELECT value FROM test WHERE id = 1").fetchone()
        assert result[0] == "secret_data", "Data should be readable after rekey"
        conn2.close()

        # Original password should no longer work
        conn3 = sqlcipher_module.connect(str(temp_db_path))
        cursor3 = conn3.cursor()
        # Existing database: key first, then cipher_* pragmas
        set_sqlcipher_key(cursor3, "original_password")
        apply_sqlcipher_pragmas(cursor3, creation_mode=False)
        cursor3.close()

        with pytest.raises(Exception):
            conn3.execute("SELECT * FROM test").fetchone()
        conn3.close()


class TestKeyDerivationSecurity:
    """Security-focused tests for key derivation."""

    def test_pbkdf2_iterations_sufficient(self):
        """Verify KDF iterations are set to a secure value."""
        settings = get_sqlcipher_settings()
        iterations = settings["kdf_iterations"]

        # OWASP recommends at least 600,000 for PBKDF2-HMAC-SHA512
        # We use 256,000 which is a reasonable balance for performance
        assert iterations >= 100000, (
            f"KDF iterations should be at least 100000, got {iterations}"
        )

    def test_key_not_stored_in_plaintext(self):
        """Verify derived key is bytes, not plaintext password."""
        _get_key_from_password.cache_clear()

        password = "my_secret_password"
        key = _get_key_from_password(
            password,
            LEGACY_PBKDF2_SALT,
            get_sqlcipher_settings()["kdf_iterations"],
        )

        # Key should be bytes, not string
        assert isinstance(key, bytes), "Key should be bytes"

        # Key should not contain the password
        assert password.encode() not in key, (
            "Key should not contain plaintext password"
        )

    def test_different_salts_would_produce_different_keys(self):
        """
        Document that our fixed salt means same password = same key.

        This is intentional for database compatibility - if salt changed,
        existing databases would become inaccessible.
        """
        _get_key_from_password.cache_clear()

        # Same password should always produce same key (fixed salt)
        key1 = _get_key_from_password(
            "test",
            LEGACY_PBKDF2_SALT,
            get_sqlcipher_settings()["kdf_iterations"],
        )
        _get_key_from_password.cache_clear()
        key2 = _get_key_from_password(
            "test",
            LEGACY_PBKDF2_SALT,
            get_sqlcipher_settings()["kdf_iterations"],
        )

        assert key1 == key2, (
            "Fixed salt ensures consistent key derivation for database compatibility"
        )
