"""Tests for _make_sqlcipher_connection() factory method.

Verifies:
- Factory method creates connections with correct parameters
- Correct call order: key → pragmas → verify → performance
- Parameters are passed through correctly
- Failed verification raises ValueError
- Metrics path preserves error logging
"""

from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from local_deep_research.database.encrypted_db import DatabaseManager

# Module path prefix for patching
_EDB = "local_deep_research.database.encrypted_db"


@pytest.fixture
def manager():
    """Create a DatabaseManager with encryption disabled for testing."""
    with patch(f"{_EDB}.get_data_directory") as mock_dir:
        mock_dir.return_value = Path("/tmp/test")
        with patch.object(
            DatabaseManager, "_check_encryption_available", return_value=False
        ):
            with patch.dict("os.environ", {"LDR_ALLOW_UNENCRYPTED": "true"}):
                return DatabaseManager()


@pytest.fixture
def mock_sqlcipher():
    """Mock all SQLCipher dependencies for _make_sqlcipher_connection."""
    mock_conn = MagicMock()
    mock_cursor = MagicMock()
    mock_conn.cursor.return_value = mock_cursor

    mock_module = MagicMock()
    mock_module.connect.return_value = mock_conn

    with (
        patch(
            f"{_EDB}.get_sqlcipher_module", return_value=mock_module
        ) as mock_get,
        patch(f"{_EDB}.set_sqlcipher_key") as mock_set_key,
        patch(
            f"{_EDB}.verify_sqlcipher_connection", return_value=True
        ) as mock_verify,
        patch(f"{_EDB}.apply_sqlcipher_pragmas") as mock_pragmas,
        patch(f"{_EDB}.apply_performance_pragmas") as mock_perf,
    ):
        yield {
            "module": mock_module,
            "conn": mock_conn,
            "cursor": mock_cursor,
            "get_module": mock_get,
            "set_key": mock_set_key,
            "verify": mock_verify,
            "pragmas": mock_pragmas,
            "performance": mock_perf,
        }


class TestMakeSqlcipherConnection:
    """Verify _make_sqlcipher_connection creates connections correctly."""

    def test_returns_connection(self, manager, mock_sqlcipher):
        """Factory should return the connection object from sqlcipher3.connect."""
        result = manager._make_sqlcipher_connection(
            Path("/tmp/test.db"), "secret"
        )
        assert result is mock_sqlcipher["conn"]

    def test_connect_with_default_params(self, manager, mock_sqlcipher):
        """Default params: isolation_level=IMMEDIATE, check_same_thread=False."""
        manager._make_sqlcipher_connection(Path("/tmp/test.db"), "secret")

        mock_sqlcipher["module"].connect.assert_called_once_with(
            "/tmp/test.db",
            isolation_level="IMMEDIATE",
            check_same_thread=False,
        )

    def test_connect_with_custom_isolation_level(self, manager, mock_sqlcipher):
        """Metrics path passes isolation_level='' for deferred transactions."""
        manager._make_sqlcipher_connection(
            Path("/tmp/test.db"), "secret", isolation_level=""
        )

        mock_sqlcipher["module"].connect.assert_called_once_with(
            "/tmp/test.db",
            isolation_level="",
            check_same_thread=False,
        )

    def test_connect_with_check_same_thread(self, manager, mock_sqlcipher):
        """check_same_thread should be forwarded to sqlcipher3.connect."""
        manager._make_sqlcipher_connection(
            Path("/tmp/test.db"), "secret", check_same_thread=True
        )

        mock_sqlcipher["module"].connect.assert_called_once_with(
            "/tmp/test.db",
            isolation_level="IMMEDIATE",
            check_same_thread=True,
        )

    def test_sets_sqlcipher_key(self, manager, mock_sqlcipher):
        """Must call set_sqlcipher_key with cursor, password, and db_path."""
        manager._make_sqlcipher_connection(Path("/tmp/test.db"), "my_pass")

        mock_sqlcipher["set_key"].assert_called_once_with(
            mock_sqlcipher["cursor"], "my_pass", db_path=Path("/tmp/test.db")
        )

    def test_verifies_connection(self, manager, mock_sqlcipher):
        """Must verify connection works after setting key."""
        manager._make_sqlcipher_connection(Path("/tmp/test.db"), "secret")

        mock_sqlcipher["verify"].assert_called_once_with(
            mock_sqlcipher["cursor"]
        )

    def test_applies_sqlcipher_pragmas(self, manager, mock_sqlcipher):
        """Must apply SQLCipher pragmas with creation_mode=False."""
        manager._make_sqlcipher_connection(Path("/tmp/test.db"), "secret")

        mock_sqlcipher["pragmas"].assert_called_once_with(
            mock_sqlcipher["cursor"], creation_mode=False
        )

    def test_applies_performance_pragmas(self, manager, mock_sqlcipher):
        """Must call apply_performance_pragmas with cursor."""
        manager._make_sqlcipher_connection(Path("/tmp/test.db"), "secret")

        mock_sqlcipher["performance"].assert_called_once_with(
            mock_sqlcipher["cursor"]
        )

    def test_closes_cursor(self, manager, mock_sqlcipher):
        """Cursor must be closed before returning."""
        manager._make_sqlcipher_connection(Path("/tmp/test.db"), "secret")

        mock_sqlcipher["cursor"].close.assert_called_once()

    def test_call_order(self, manager, mock_sqlcipher):
        """Operations must happen in order: key → pragmas → verify → performance."""
        call_order = []
        mock_sqlcipher["set_key"].side_effect = lambda *a, **kw: (
            call_order.append("key")
        )
        mock_sqlcipher["verify"].side_effect = lambda *a: (
            call_order.append("verify") or True
        )
        mock_sqlcipher["pragmas"].side_effect = lambda *a, **kw: (
            call_order.append("pragmas")
        )
        mock_sqlcipher["performance"].side_effect = lambda *a: (
            call_order.append("performance")
        )

        manager._make_sqlcipher_connection(Path("/tmp/test.db"), "secret")

        assert call_order == ["key", "pragmas", "verify", "performance"]

    def test_raises_on_failed_verification(self, manager, mock_sqlcipher):
        """Must raise ValueError when verification fails."""
        mock_sqlcipher["verify"].return_value = False

        with pytest.raises(ValueError, match="Failed to verify database key"):
            manager._make_sqlcipher_connection(Path("/tmp/test.db"), "secret")

    def test_no_performance_pragmas_after_failed_verification(
        self, manager, mock_sqlcipher
    ):
        """Cipher pragmas run before verify, but performance pragmas must not."""
        mock_sqlcipher["verify"].return_value = False

        with pytest.raises(ValueError):
            manager._make_sqlcipher_connection(Path("/tmp/test.db"), "secret")

        mock_sqlcipher["pragmas"].assert_called_once_with(
            mock_sqlcipher["cursor"], creation_mode=False
        )
        mock_sqlcipher["performance"].assert_not_called()

    def test_closes_cursor_and_conn_on_verification_failure(
        self, manager, mock_sqlcipher
    ):
        """Cursor and connection must be closed when verification fails."""
        mock_sqlcipher["verify"].return_value = False

        with pytest.raises(ValueError):
            manager._make_sqlcipher_connection(Path("/tmp/test.db"), "secret")

        mock_sqlcipher["cursor"].close.assert_called_once()
        mock_sqlcipher["conn"].close.assert_called_once()

    def test_closes_cursor_and_conn_on_key_error(self, manager, mock_sqlcipher):
        """Cursor and connection must be closed when set_sqlcipher_key raises."""
        mock_sqlcipher["set_key"].side_effect = RuntimeError("key failed")

        with pytest.raises(RuntimeError):
            manager._make_sqlcipher_connection(Path("/tmp/test.db"), "secret")

        mock_sqlcipher["cursor"].close.assert_called_once()
        mock_sqlcipher["conn"].close.assert_called_once()

    def test_closes_cursor_and_conn_on_pragma_error(
        self, manager, mock_sqlcipher
    ):
        """Cursor and connection must be closed when pragma application raises."""
        mock_sqlcipher["pragmas"].side_effect = RuntimeError("pragma failed")

        with pytest.raises(RuntimeError):
            manager._make_sqlcipher_connection(Path("/tmp/test.db"), "secret")

        mock_sqlcipher["cursor"].close.assert_called_once()
        mock_sqlcipher["conn"].close.assert_called_once()
        mock_sqlcipher["verify"].assert_not_called()
        mock_sqlcipher["performance"].assert_not_called()

    def test_connect_converts_path_to_string(self, manager, mock_sqlcipher):
        """Path objects must be converted to str for sqlcipher3.connect."""
        manager._make_sqlcipher_connection(Path("/some/dir/test.db"), "secret")

        call_args = mock_sqlcipher["module"].connect.call_args
        assert call_args[0][0] == "/some/dir/test.db"
        assert isinstance(call_args[0][0], str)

    def test_connect_with_none_isolation_level(self, manager, mock_sqlcipher):
        """isolation_level=None (autocommit) must be forwarded to connect."""
        manager._make_sqlcipher_connection(
            Path("/tmp/test.db"), "secret", isolation_level=None
        )

        mock_sqlcipher["module"].connect.assert_called_once_with(
            "/tmp/test.db",
            isolation_level=None,
            check_same_thread=False,
        )

    def test_conn_not_closed_on_success(self, manager, mock_sqlcipher):
        """On success, cursor is closed but connection must remain open."""
        result = manager._make_sqlcipher_connection(
            Path("/tmp/test.db"), "secret"
        )

        mock_sqlcipher["cursor"].close.assert_called_once()
        mock_sqlcipher["conn"].close.assert_not_called()
        assert result is mock_sqlcipher["conn"]

    def test_key_error_prevents_further_calls(self, manager, mock_sqlcipher):
        """If set_sqlcipher_key fails, no pragmas or verify should run."""
        mock_sqlcipher["set_key"].side_effect = RuntimeError("bad key")

        with pytest.raises(RuntimeError):
            manager._make_sqlcipher_connection(Path("/tmp/test.db"), "secret")

        mock_sqlcipher["pragmas"].assert_not_called()
        mock_sqlcipher["verify"].assert_not_called()
        mock_sqlcipher["performance"].assert_not_called()

    def test_original_exception_propagated(self, manager, mock_sqlcipher):
        """The original exception must propagate without wrapping."""
        original = RuntimeError("specific error")
        mock_sqlcipher["pragmas"].side_effect = original

        with pytest.raises(RuntimeError) as exc_info:
            manager._make_sqlcipher_connection(Path("/tmp/test.db"), "secret")

        assert exc_info.value is original

    def test_conn_closed_even_if_cursor_close_raises(
        self, manager, mock_sqlcipher
    ):
        """Connection must close even when cursor.close() raises during cleanup."""
        mock_sqlcipher["verify"].return_value = False
        mock_sqlcipher["cursor"].close.side_effect = RuntimeError(
            "cursor close failed"
        )

        with pytest.raises(ValueError, match="Failed to verify database key"):
            manager._make_sqlcipher_connection(Path("/tmp/test.db"), "secret")

        mock_sqlcipher["conn"].close.assert_called_once()

    def test_import_error_propagates(self, manager):
        """ImportError from get_sqlcipher_module() must propagate cleanly."""
        with patch(
            f"{_EDB}.get_sqlcipher_module",
            side_effect=ImportError("no sqlcipher3"),
        ):
            with pytest.raises(ImportError, match="no sqlcipher3"):
                manager._make_sqlcipher_connection(
                    Path("/tmp/test.db"), "secret"
                )

    def test_connect_failure_propagates(self, manager, mock_sqlcipher):
        """Exceptions from sqlcipher3.connect() must propagate without cleanup attempts."""
        mock_sqlcipher["module"].connect.side_effect = OSError("disk full")

        with pytest.raises(OSError, match="disk full"):
            manager._make_sqlcipher_connection(Path("/tmp/test.db"), "secret")

        mock_sqlcipher["set_key"].assert_not_called()

    def test_closes_cursor_and_conn_on_performance_pragma_error(
        self, manager, mock_sqlcipher
    ):
        """Cursor and connection must be closed when apply_performance_pragmas raises."""
        mock_sqlcipher["performance"].side_effect = RuntimeError(
            "perf pragma failed"
        )

        with pytest.raises(RuntimeError, match="perf pragma failed"):
            manager._make_sqlcipher_connection(Path("/tmp/test.db"), "secret")

        mock_sqlcipher["cursor"].close.assert_called_once()
        mock_sqlcipher["conn"].close.assert_called_once()
        mock_sqlcipher["verify"].assert_called_once()

    def test_closes_cursor_and_conn_on_verify_exception(
        self, manager, mock_sqlcipher
    ):
        """Cleanup must happen when verify_sqlcipher_connection raises (not just returns False)."""
        mock_sqlcipher["verify"].side_effect = RuntimeError("verify crashed")

        with pytest.raises(RuntimeError, match="verify crashed"):
            manager._make_sqlcipher_connection(Path("/tmp/test.db"), "secret")

        mock_sqlcipher["cursor"].close.assert_called_once()
        mock_sqlcipher["conn"].close.assert_called_once()
        mock_sqlcipher["performance"].assert_not_called()


class TestMetricsThreadConnectionLogging:
    """Verify that create_thread_safe_session_for_metrics preserves error logging."""

    @patch(f"{_EDB}.get_data_directory")
    def test_logs_exception_on_connection_failure(self, mock_dir, tmp_path):
        """Metrics thread connection failures must be logged with logger.exception."""
        mock_dir.return_value = tmp_path

        with patch.object(
            DatabaseManager, "_check_encryption_available", return_value=True
        ):
            manager = DatabaseManager()
            manager.has_encryption = True

            db_path = tmp_path / "test.db"
            db_path.touch()

            with patch.object(
                manager, "_get_user_db_path", return_value=db_path
            ):
                # Make create_sqlcipher_connection raise an error
                with patch(
                    f"{_EDB}.create_sqlcipher_connection",
                    side_effect=RuntimeError("connection failed"),
                ):
                    with patch(f"{_EDB}.logger") as mock_logger:
                        with patch(f"{_EDB}.create_engine") as mock_engine:
                            # create_engine should use our creator function
                            # Capture the creator argument and call it
                            def capture_creator(*args, **kwargs):
                                creator = kwargs.get("creator")
                                if creator:
                                    with pytest.raises(RuntimeError):
                                        creator()
                                return MagicMock()

                            mock_engine.side_effect = capture_creator

                            manager.create_thread_safe_session_for_metrics(
                                "testuser", "secret"
                            )

                            # Verify logger.exception was called
                            mock_logger.exception.assert_called_once()
                            log_msg = mock_logger.exception.call_args[0][0]
                            assert "testuser" in log_msg
                            assert (
                                "Failed to create thread connection" in log_msg
                            )
