"""
Tests for encryption_check module.

Covers functions for checking SQLCipher availability and providing
appropriate connection strings and warnings.
"""

import subprocess
import warnings
from unittest.mock import MagicMock, patch


from tests.test_utils import add_src_to_path

add_src_to_path()

from local_deep_research.database.encryption_check import (  # noqa: E402
    check_sqlcipher_available,
    get_connection_string,
    warn_if_no_encryption,
)


class TestCheckSqlcipherAvailable:
    """Tests for check_sqlcipher_available function."""

    def test_returns_true_when_sqlcipher3_available(self):
        """Should return (True, None) when sqlcipher3 module is found."""
        mock_spec = MagicMock()
        with patch("importlib.util.find_spec", return_value=mock_spec):
            available, message = check_sqlcipher_available()

            assert available is True
            assert message is None

    def test_returns_false_when_module_not_found(self):
        """Should return (False, message) when sqlcipher3 not found."""
        with patch("importlib.util.find_spec", return_value=None):
            with patch("subprocess.run") as mock_run:
                mock_run.side_effect = FileNotFoundError()

                available, message = check_sqlcipher_available()

                assert available is False
                assert message is not None
                assert "not installed" in message.lower()

    def test_detects_sqlcipher_without_python_bindings(self):
        """Should detect when SQLCipher CLI exists but Python bindings missing."""
        with patch("importlib.util.find_spec", return_value=None):
            mock_result = MagicMock()
            mock_result.returncode = 0
            with patch("subprocess.run", return_value=mock_result):
                available, message = check_sqlcipher_available()

                assert available is False
                assert message is not None
                assert "bindings" in message.lower()
                assert "pdm install" in message.lower()

    def test_handles_subprocess_timeout(self):
        """Should handle subprocess timeout gracefully."""
        with patch("importlib.util.find_spec", return_value=None):
            with patch("subprocess.run") as mock_run:
                mock_run.side_effect = subprocess.TimeoutExpired(
                    cmd="sqlcipher", timeout=5
                )

                available, message = check_sqlcipher_available()

                assert available is False
                assert message is not None
                assert "not installed" in message.lower()

    def test_handles_import_error(self):
        """Should handle ImportError during find_spec."""
        with patch("importlib.util.find_spec") as mock_find:
            mock_find.side_effect = ImportError("Test error")
            with patch("subprocess.run") as mock_run:
                mock_run.side_effect = FileNotFoundError()

                available, message = check_sqlcipher_available()

                assert available is False
                assert message is not None

    def test_subprocess_called_with_correct_args(self):
        """Should call subprocess.run with correct arguments."""
        with patch("importlib.util.find_spec", return_value=None):
            with patch("subprocess.run") as mock_run:
                mock_run.side_effect = FileNotFoundError()

                check_sqlcipher_available()

                mock_run.assert_called_once()
                call_args = mock_run.call_args
                assert call_args[0][0] == ["sqlcipher", "--version"]
                assert call_args[1]["capture_output"] is True
                assert call_args[1]["text"] is True
                assert call_args[1]["timeout"] == 5


class TestWarnIfNoEncryption:
    """Tests for warn_if_no_encryption function."""

    def test_returns_true_when_encryption_available(self):
        """Should return True when SQLCipher is available."""
        mock_spec = MagicMock()
        with patch("importlib.util.find_spec", return_value=mock_spec):
            result = warn_if_no_encryption()

            assert result is True

    def test_issues_user_warning_when_not_available(self):
        """Should issue UserWarning with security info when not available."""
        with patch("importlib.util.find_spec", return_value=None):
            with patch("subprocess.run") as mock_run:
                mock_run.side_effect = FileNotFoundError()

                with warnings.catch_warnings(record=True) as w:
                    warnings.simplefilter("always")
                    result = warn_if_no_encryption()

                    assert result is False
                    assert len(w) == 1
                    assert issubclass(w[0].category, UserWarning)
                    warning_msg = str(w[0].message)
                    assert "SECURITY WARNING" in warning_msg
                    assert "UNENCRYPTED" in warning_msg
                    assert "API keys" in warning_msg

    def test_logs_warning_when_not_available(self):
        """Should log warning when encryption not available."""
        with patch("importlib.util.find_spec", return_value=None):
            with patch("subprocess.run") as mock_run:
                mock_run.side_effect = FileNotFoundError()
                with patch(
                    "local_deep_research.database.encryption_check.logger"
                ) as mock_logger:
                    with warnings.catch_warnings():
                        warnings.simplefilter("ignore")
                        warn_if_no_encryption()

                    mock_logger.warning.assert_called_once()
                    call_args = mock_logger.warning.call_args[0][0]
                    assert "without database encryption" in call_args.lower()


class TestGetConnectionString:
    """Tests for get_connection_string function."""

    def test_returns_encrypted_string_when_available_and_password(self):
        """Should return encrypted connection string when SQLCipher available."""
        mock_spec = MagicMock()
        with patch("importlib.util.find_spec", return_value=mock_spec):
            result = get_connection_string("/path/to/db.sqlite", "mypassword")

            assert (
                result == "sqlite+pysqlcipher://:mypassword@//path/to/db.sqlite"
            )

    def test_returns_sqlite_when_not_available(self):
        """Should return regular SQLite string when SQLCipher not available."""
        with patch("importlib.util.find_spec", return_value=None):
            with patch("subprocess.run") as mock_run:
                mock_run.side_effect = FileNotFoundError()

                result = get_connection_string(
                    "/path/to/db.sqlite", "mypassword"
                )

                assert result == "sqlite:////path/to/db.sqlite"

    def test_returns_sqlite_when_no_password(self):
        """Should return SQLite string when no password provided."""
        mock_spec = MagicMock()
        with patch("importlib.util.find_spec", return_value=mock_spec):
            result = get_connection_string("/path/to/db.sqlite")

            assert result == "sqlite:////path/to/db.sqlite"

    def test_returns_sqlite_when_password_is_none(self):
        """Should return SQLite string when password is explicitly None."""
        mock_spec = MagicMock()
        with patch("importlib.util.find_spec", return_value=mock_spec):
            result = get_connection_string("/path/to/db.sqlite", None)

            assert result == "sqlite:////path/to/db.sqlite"

    def test_logs_warning_when_password_ignored(self):
        """Should log warning when password provided but SQLCipher unavailable."""
        with patch("importlib.util.find_spec", return_value=None):
            with patch("subprocess.run") as mock_run:
                mock_run.side_effect = FileNotFoundError()
                with patch(
                    "local_deep_research.database.encryption_check.logger"
                ) as mock_logger:
                    get_connection_string("/path/to/db.sqlite", "mypassword")

                    mock_logger.warning.assert_called_once()
                    call_args = mock_logger.warning.call_args[0][0]
                    assert "password provided" in call_args.lower()
                    assert "unencrypted" in call_args.lower()

    def test_no_warning_when_no_password_and_unavailable(self):
        """Should not log warning when no password and SQLCipher unavailable."""
        with patch("importlib.util.find_spec", return_value=None):
            with patch("subprocess.run") as mock_run:
                mock_run.side_effect = FileNotFoundError()
                with patch(
                    "local_deep_research.database.encryption_check.logger"
                ) as mock_logger:
                    get_connection_string("/path/to/db.sqlite")

                    mock_logger.warning.assert_not_called()

    def test_connection_string_format_verification(self):
        """Verify exact format of connection strings."""
        # Test encrypted format
        mock_spec = MagicMock()
        with patch("importlib.util.find_spec", return_value=mock_spec):
            result = get_connection_string("test.db", "secret")
            assert result.startswith("sqlite+pysqlcipher://")
            assert ":secret@/" in result
            assert result.endswith("test.db")

        # Test unencrypted format
        with patch("importlib.util.find_spec", return_value=None):
            with patch("subprocess.run") as mock_run:
                mock_run.side_effect = FileNotFoundError()
                result = get_connection_string("test.db")
                assert result.startswith("sqlite:///")
                assert result.endswith("test.db")
