"""
Comprehensive tests for encrypted_db.py DatabaseManager.

Tests cover:
- Database creation and opening
- Password validation and changes
- User existence checks
- Database integrity verification
- Memory usage tracking
- Thread engine management
- Session creation
- Multi-user isolation
- Error scenarios
"""

import threading
import time
from unittest.mock import MagicMock, patch

import pytest


class TestDatabaseCreation:
    """Tests for create_user_database functionality."""

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_create_database_with_valid_password(self, mock_data_dir, tmp_path):
        """Test database creation with a valid password."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

        with patch.object(
            DatabaseManager, "_check_encryption_available", return_value=False
        ):
            with patch.dict("os.environ", {"LDR_ALLOW_UNENCRYPTED": "true"}):
                manager = DatabaseManager()

                # Mock the internal operations
                with patch.object(manager, "_get_user_db_path") as mock_path:
                    mock_db_path = tmp_path / "test_user.db"
                    mock_path.return_value = mock_db_path

                    with patch(
                        "local_deep_research.database.encrypted_db.create_engine"
                    ) as mock_engine:
                        mock_engine_instance = MagicMock()
                        mock_engine.return_value = mock_engine_instance

                        with patch(
                            "local_deep_research.database.encrypted_db.event"
                        ):
                            with patch(
                                "local_deep_research.database.initialize.initialize_database"
                            ):
                                engine = manager.create_user_database(
                                    "testuser", "validpassword"
                                )

                                assert engine is mock_engine_instance
                                assert "testuser" in manager.connections

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_create_database_with_empty_password_raises(
        self, mock_data_dir, tmp_path
    ):
        """Test database creation fails with empty password."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            with pytest.raises(ValueError, match="Invalid encryption key"):
                manager.create_user_database("testuser", "")

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_create_database_with_none_password_raises(
        self, mock_data_dir, tmp_path
    ):
        """Test database creation fails with None password."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            with pytest.raises(ValueError, match="Invalid encryption key"):
                manager.create_user_database("testuser", None)

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_create_database_existing_user_raises(
        self, mock_data_dir, tmp_path
    ):
        """Test database creation fails if database already exists."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

        # Create the database file
        db_file = tmp_path / "encrypted_databases" / "test_db.db"
        db_file.parent.mkdir(parents=True, exist_ok=True)
        db_file.touch()

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

            with patch.object(
                manager, "_get_user_db_path", return_value=db_file
            ):
                with pytest.raises(ValueError, match="already exists"):
                    manager.create_user_database("existinguser", "password")


class TestDatabaseOpening:
    """Tests for open_user_database functionality."""

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_open_database_with_valid_password(self, mock_data_dir, tmp_path):
        """Test opening database with valid password."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

        # Create db file
        db_file = tmp_path / "encrypted_databases" / "test.db"
        db_file.parent.mkdir(parents=True, exist_ok=True)
        db_file.touch()

        with patch.object(
            DatabaseManager, "_check_encryption_available", return_value=False
        ):
            with patch.dict("os.environ", {"LDR_ALLOW_UNENCRYPTED": "true"}):
                manager = DatabaseManager()

                with patch.object(
                    manager, "_get_user_db_path", return_value=db_file
                ):
                    with patch(
                        "local_deep_research.database.encrypted_db.get_key_from_password",
                        return_value=b"\x00" * 32,
                    ):
                        with patch(
                            "local_deep_research.database.encrypted_db.create_engine"
                        ) as mock_engine:
                            mock_engine_instance = MagicMock()
                            mock_conn = MagicMock()
                            mock_engine_instance.connect.return_value.__enter__ = MagicMock(
                                return_value=mock_conn
                            )
                            mock_engine_instance.connect.return_value.__exit__ = MagicMock(
                                return_value=False
                            )
                            mock_engine.return_value = mock_engine_instance

                            with patch(
                                "local_deep_research.database.encrypted_db.event"
                            ):
                                with patch(
                                    "local_deep_research.database.alembic_runner.needs_migration",
                                    return_value=False,
                                ):
                                    with patch(
                                        "local_deep_research.database.initialize.initialize_database"
                                    ):
                                        engine = manager.open_user_database(
                                            "testuser", "validpassword"
                                        )

                                        assert engine is mock_engine_instance

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_open_database_with_empty_password_raises(
        self, mock_data_dir, tmp_path
    ):
        """Test opening database fails with empty password."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            with pytest.raises(ValueError, match="Invalid encryption key"):
                manager.open_user_database("testuser", "")

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_open_database_nonexistent_returns_none(
        self, mock_data_dir, tmp_path
    ):
        """Test opening nonexistent database returns None."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            nonexistent = tmp_path / "nonexistent.db"
            with patch.object(
                manager, "_get_user_db_path", return_value=nonexistent
            ):
                result = manager.open_user_database("nonexistent", "password")

                assert result is None

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_open_database_already_open_returns_cached(
        self, mock_data_dir, tmp_path
    ):
        """Test opening already-open database returns cached engine."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            mock_engine = MagicMock()
            manager.connections["testuser"] = mock_engine

            result = manager.open_user_database("testuser", "password")

            assert result is mock_engine

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_open_database_connection_error_returns_none(
        self, mock_data_dir, tmp_path
    ):
        """Test that connection errors return None."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

        db_file = tmp_path / "encrypted_databases" / "test.db"
        db_file.parent.mkdir(parents=True, exist_ok=True)
        db_file.touch()

        with patch.object(
            DatabaseManager, "_check_encryption_available", return_value=False
        ):
            with patch.dict("os.environ", {"LDR_ALLOW_UNENCRYPTED": "true"}):
                manager = DatabaseManager()

                with patch.object(
                    manager, "_get_user_db_path", return_value=db_file
                ):
                    with patch(
                        "local_deep_research.database.encrypted_db.create_engine"
                    ) as mock_engine:
                        mock_engine_instance = MagicMock()
                        # Use context manager that raises on enter
                        mock_context = MagicMock()
                        mock_context.__enter__ = MagicMock(
                            side_effect=Exception("Connection failed")
                        )
                        mock_context.__exit__ = MagicMock(return_value=False)
                        mock_engine_instance.connect.return_value = mock_context
                        mock_engine.return_value = mock_engine_instance

                        with patch(
                            "local_deep_research.database.encrypted_db.event"
                        ):
                            result = manager.open_user_database(
                                "testuser", "password"
                            )

                            assert result is None


class TestDatabaseClosure:
    """Tests for close_user_database functionality."""

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_close_database_disposes_engine(self, mock_data_dir, tmp_path):
        """Test closing database disposes engine and removes from connections."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            mock_engine = MagicMock()
            manager.connections["testuser"] = mock_engine

            manager.close_user_database("testuser")

            mock_engine.dispose.assert_called_once()
            assert "testuser" not in manager.connections

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_close_database_cleans_thread_engines(
        self, mock_data_dir, tmp_path
    ):
        """Test closing database also cleans thread engines."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            mock_engine = MagicMock()
            manager.connections["testuser"] = mock_engine

            # Add thread engine
            thread_engine = MagicMock()
            manager._thread_engines[("testuser", 12345)] = thread_engine

            manager.close_user_database("testuser")

            assert ("testuser", 12345) not in manager._thread_engines
            thread_engine.dispose.assert_called_once()

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_close_nonexistent_database_no_error(self, mock_data_dir, tmp_path):
        """Test closing nonexistent database doesn't raise error."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            # Should not raise
            manager.close_user_database("nonexistent")


class TestCloseAllDatabases:
    """Tests for close_all_databases functionality."""

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_close_all_databases_continues_after_dispose_failure(
        self, mock_data_dir, tmp_path
    ):
        """Verify one engine's dispose failure doesn't prevent others from closing."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            engine_ok = MagicMock()
            engine_fail = MagicMock()
            engine_fail.dispose.side_effect = RuntimeError("dispose exploded")

            manager.connections["alice"] = engine_fail
            manager.connections["bob"] = engine_ok

            manager.close_all_databases()

            # Both engines attempted dispose
            engine_fail.dispose.assert_called_once()
            engine_ok.dispose.assert_called_once()
            # Connections dict is cleared regardless
            assert len(manager.connections) == 0


class TestPasswordChange:
    """Tests for change_password functionality."""

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_change_password_no_encryption_returns_false(
        self, mock_data_dir, tmp_path
    ):
        """Test password change returns False when encryption not available."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

        with patch.object(
            DatabaseManager, "_check_encryption_available", return_value=False
        ):
            with patch.dict("os.environ", {"LDR_ALLOW_UNENCRYPTED": "true"}):
                manager = DatabaseManager()

                result = manager.change_password(
                    "testuser", "oldpass", "newpass"
                )

                assert result is False

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_change_password_nonexistent_db_returns_false(
        self, mock_data_dir, tmp_path
    ):
        """Test password change returns False for nonexistent database."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            nonexistent = tmp_path / "nonexistent.db"
            with patch.object(
                manager, "_get_user_db_path", return_value=nonexistent
            ):
                result = manager.change_password(
                    "testuser", "oldpass", "newpass"
                )

                assert result is False

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_change_password_wrong_old_password_returns_false(
        self, mock_data_dir, tmp_path
    ):
        """Test password change returns False with wrong old password."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

        db_file = tmp_path / "encrypted_databases" / "test.db"
        db_file.parent.mkdir(parents=True, exist_ok=True)
        db_file.touch()

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

            with patch.object(
                manager, "_get_user_db_path", return_value=db_file
            ):
                # open_user_database fails with wrong password
                with patch.object(
                    manager, "open_user_database", return_value=None
                ):
                    result = manager.change_password(
                        "testuser", "wrongpass", "newpass"
                    )

                    assert result is False


class TestUserExists:
    """Tests for user_exists functionality."""

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_user_exists_true(self, mock_data_dir, tmp_path):
        """Test user_exists returns True for existing user."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            with patch(
                "local_deep_research.database.auth_db.get_auth_db_session"
            ) as mock_auth:
                mock_session = MagicMock()
                mock_auth.return_value = mock_session

                mock_user = MagicMock()
                mock_session.query.return_value.filter_by.return_value.first.return_value = mock_user

                result = manager.user_exists("existinguser")

                assert result is True
                mock_session.close.assert_called_once()

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_user_exists_false(self, mock_data_dir, tmp_path):
        """Test user_exists returns False for nonexistent user."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            with patch(
                "local_deep_research.database.auth_db.get_auth_db_session"
            ) as mock_auth:
                mock_session = MagicMock()
                mock_auth.return_value = mock_session

                mock_session.query.return_value.filter_by.return_value.first.return_value = None

                result = manager.user_exists("nonexistent")

                assert result is False


class TestDatabaseIntegrity:
    """Tests for check_database_integrity functionality."""

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_integrity_check_no_connection_returns_false(
        self, mock_data_dir, tmp_path
    ):
        """Test integrity check returns False when no connection exists."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            result = manager.check_database_integrity("nonexistent")

            assert result is False

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_integrity_check_success(self, mock_data_dir, tmp_path):
        """Test successful integrity check returns True."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            mock_engine = MagicMock()
            mock_conn = MagicMock()
            mock_engine.connect.return_value.__enter__ = MagicMock(
                return_value=mock_conn
            )
            mock_engine.connect.return_value.__exit__ = MagicMock(
                return_value=False
            )

            # Mock successful integrity checks
            mock_conn.execute.side_effect = [
                MagicMock(
                    fetchone=MagicMock(return_value=("ok",))
                ),  # quick_check
                iter([]),  # cipher_integrity_check - no failures
            ]

            manager.connections["testuser"] = mock_engine

            result = manager.check_database_integrity("testuser")

            assert result is True

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_integrity_check_quick_check_failure(self, mock_data_dir, tmp_path):
        """Test integrity check fails on quick_check failure."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            mock_engine = MagicMock()
            mock_conn = MagicMock()
            mock_engine.connect.return_value.__enter__ = MagicMock(
                return_value=mock_conn
            )
            mock_engine.connect.return_value.__exit__ = MagicMock(
                return_value=False
            )

            # Mock failed quick_check
            mock_conn.execute.return_value.fetchone.return_value = ("corrupt",)

            manager.connections["testuser"] = mock_engine

            result = manager.check_database_integrity("testuser")

            assert result is False

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_integrity_check_cipher_failure(self, mock_data_dir, tmp_path):
        """Test integrity check fails on cipher_integrity_check failure."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            mock_engine = MagicMock()
            mock_conn = MagicMock()
            mock_engine.connect.return_value.__enter__ = MagicMock(
                return_value=mock_conn
            )
            mock_engine.connect.return_value.__exit__ = MagicMock(
                return_value=False
            )

            # Mock successful quick_check but failed cipher check
            mock_conn.execute.side_effect = [
                MagicMock(
                    fetchone=MagicMock(return_value=("ok",))
                ),  # quick_check
                iter(
                    [("HMAC failure",)]
                ),  # cipher_integrity_check - has failures
            ]

            manager.connections["testuser"] = mock_engine

            result = manager.check_database_integrity("testuser")

            assert result is False

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_integrity_check_exception_returns_false(
        self, mock_data_dir, tmp_path
    ):
        """Test integrity check returns False on exception."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            mock_engine = MagicMock()
            mock_engine.connect.side_effect = Exception("Connection failed")

            manager.connections["testuser"] = mock_engine

            result = manager.check_database_integrity("testuser")

            assert result is False


class TestMemoryUsage:
    """Tests for get_memory_usage functionality."""

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_memory_usage_empty(self, mock_data_dir, tmp_path):
        """Test memory usage with no connections."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            usage = manager.get_memory_usage()

            assert usage["active_connections"] == 0
            assert usage["thread_engines"] == 0
            assert usage["active_sessions"] == 0
            assert usage["estimated_memory_mb"] == 0

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_memory_usage_with_connections(self, mock_data_dir, tmp_path):
        """Test memory usage with active connections."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            # Add connections
            manager.connections["user1"] = MagicMock()
            manager.connections["user2"] = MagicMock()
            manager._thread_engines[("user1", 100)] = MagicMock()

            usage = manager.get_memory_usage()

            assert usage["active_connections"] == 2
            assert usage["thread_engines"] == 1
            assert usage["estimated_memory_mb"] == 10.5  # (2 + 1) * 3.5

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_memory_usage_calculation(self, mock_data_dir, tmp_path):
        """Test memory usage calculation formula."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            # Add 5 connections and 3 thread engines
            for i in range(5):
                manager.connections[f"user{i}"] = MagicMock()
            for i in range(3):
                manager._thread_engines[(f"user{i}", i * 100)] = MagicMock()

            usage = manager.get_memory_usage()

            # (5 + 3) * 3.5 = 28 MB
            assert usage["estimated_memory_mb"] == 28


class TestThreadEngineCleanup:
    """Tests for thread engine cleanup functionality."""

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_cleanup_by_username(self, mock_data_dir, tmp_path):
        """Test cleanup thread engines by username."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            mock_engine1 = MagicMock()
            mock_engine2 = MagicMock()
            mock_engine3 = MagicMock()

            manager._thread_engines[("user1", 100)] = mock_engine1
            manager._thread_engines[("user1", 200)] = mock_engine2
            manager._thread_engines[("user2", 100)] = mock_engine3

            manager.cleanup_thread_engines(username="user1")

            assert ("user1", 100) not in manager._thread_engines
            assert ("user1", 200) not in manager._thread_engines
            assert ("user2", 100) in manager._thread_engines
            mock_engine1.dispose.assert_called_once()
            mock_engine2.dispose.assert_called_once()
            mock_engine3.dispose.assert_not_called()

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_cleanup_by_thread_id(self, mock_data_dir, tmp_path):
        """Test cleanup thread engines by thread ID."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            mock_engine1 = MagicMock()
            mock_engine2 = MagicMock()

            manager._thread_engines[("user1", 100)] = mock_engine1
            manager._thread_engines[("user1", 200)] = mock_engine2

            manager.cleanup_thread_engines(thread_id=100)

            assert ("user1", 100) not in manager._thread_engines
            assert ("user1", 200) in manager._thread_engines

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_cleanup_by_both_username_and_thread_id(
        self, mock_data_dir, tmp_path
    ):
        """Test cleanup with both username and thread_id specified."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            mock_engine1 = MagicMock()
            mock_engine2 = MagicMock()
            mock_engine3 = MagicMock()

            manager._thread_engines[("user1", 100)] = mock_engine1
            manager._thread_engines[("user1", 200)] = mock_engine2
            manager._thread_engines[("user2", 100)] = mock_engine3

            manager.cleanup_thread_engines(username="user1", thread_id=100)

            assert ("user1", 100) not in manager._thread_engines
            assert ("user1", 200) in manager._thread_engines
            assert ("user2", 100) in manager._thread_engines

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_cleanup_default_uses_current_thread(self, mock_data_dir, tmp_path):
        """Test cleanup defaults to current thread when no args provided."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            current_thread_id = threading.get_ident()
            mock_engine = MagicMock()

            manager._thread_engines[("user1", current_thread_id)] = mock_engine

            manager.cleanup_thread_engines()

            assert ("user1", current_thread_id) not in manager._thread_engines
            mock_engine.dispose.assert_called_once()

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_cleanup_all_thread_engines(self, mock_data_dir, tmp_path):
        """Test cleanup_all_thread_engines clears all engines."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            for i in range(5):
                manager._thread_engines[(f"user{i}", i * 100)] = MagicMock()

            assert len(manager._thread_engines) == 5

            manager.cleanup_all_thread_engines()

            assert len(manager._thread_engines) == 0

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_cleanup_handles_dispose_errors(self, mock_data_dir, tmp_path):
        """Test cleanup handles dispose errors gracefully."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            mock_engine = MagicMock()
            mock_engine.dispose.side_effect = Exception("Dispose failed")

            manager._thread_engines[("user1", 100)] = mock_engine

            # Should not raise
            manager.cleanup_thread_engines(username="user1")

            assert ("user1", 100) not in manager._thread_engines


class TestThreadSafeSessionForMetrics:
    """Tests for create_thread_safe_session_for_metrics functionality."""

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_thread_safe_session_nonexistent_db_raises(
        self, mock_data_dir, tmp_path
    ):
        """Test thread-safe session raises for nonexistent database."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            nonexistent = tmp_path / "nonexistent.db"
            with patch.object(
                manager, "_get_user_db_path", return_value=nonexistent
            ):
                with pytest.raises(ValueError, match="No database found"):
                    manager.create_thread_safe_session_for_metrics(
                        "nonexistent", "password"
                    )

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_thread_safe_session_reuses_engine(self, mock_data_dir, tmp_path):
        """Test thread-safe session reuses existing engine for same thread/user."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

        db_file = tmp_path / "encrypted_databases" / "test.db"
        db_file.parent.mkdir(parents=True, exist_ok=True)
        db_file.touch()

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

            thread_id = threading.get_ident()
            mock_engine = MagicMock()
            mock_conn = MagicMock()
            mock_engine.connect.return_value.__enter__ = MagicMock(
                return_value=mock_conn
            )
            mock_engine.connect.return_value.__exit__ = MagicMock(
                return_value=False
            )

            manager._thread_engines[("testuser", thread_id)] = mock_engine

            with patch.object(
                manager, "_get_user_db_path", return_value=db_file
            ):
                with patch(
                    "local_deep_research.database.encrypted_db.sessionmaker"
                ) as mock_sm:
                    mock_session = MagicMock()
                    mock_sm.return_value = MagicMock(return_value=mock_session)

                    result = manager.create_thread_safe_session_for_metrics(
                        "testuser", "password"
                    )

                    assert result is mock_session


class TestSessionManagement:
    """Tests for get_session functionality."""

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_get_session_no_connection_returns_none(
        self, mock_data_dir, tmp_path
    ):
        """Test get_session returns None when no connection exists."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            result = manager.get_session("nonexistent")

            assert result is None

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_get_session_creates_new_session(self, mock_data_dir, tmp_path):
        """Test get_session creates a new session from existing connection."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            mock_engine = MagicMock()
            manager.connections["testuser"] = mock_engine

            with patch(
                "local_deep_research.database.encrypted_db.sessionmaker"
            ) as mock_sm:
                mock_session = MagicMock()
                mock_sm.return_value = MagicMock(return_value=mock_session)

                result = manager.get_session("testuser")

                assert result is mock_session
                mock_sm.assert_called_once_with(bind=mock_engine)


class TestMultiUserIsolation:
    """Tests for multi-user database isolation."""

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_multiple_users_separate_connections(self, mock_data_dir, tmp_path):
        """Test multiple users have separate connections."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            mock_engine1 = MagicMock()
            mock_engine2 = MagicMock()

            manager.connections["user1"] = mock_engine1
            manager.connections["user2"] = mock_engine2

            assert (
                manager.connections["user1"] is not manager.connections["user2"]
            )
            assert len(manager.connections) == 2

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_closing_one_user_doesnt_affect_others(
        self, mock_data_dir, tmp_path
    ):
        """Test closing one user's database doesn't affect others."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            mock_engine1 = MagicMock()
            mock_engine2 = MagicMock()

            manager.connections["user1"] = mock_engine1
            manager.connections["user2"] = mock_engine2

            manager.close_user_database("user1")

            assert "user1" not in manager.connections
            assert "user2" in manager.connections
            assert manager.connections["user2"] is mock_engine2


class TestConcurrentAccess:
    """Tests for concurrent access handling."""

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_concurrent_thread_cleanup(self, mock_data_dir, tmp_path):
        """Test concurrent thread cleanup operations are safe."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            # Add many thread engines
            for i in range(50):
                manager._thread_engines[(f"user{i}", i)] = MagicMock()

            errors = []

            def cleanup_thread(username):
                try:
                    manager.cleanup_thread_engines(username=username)
                except Exception as e:
                    errors.append(e)

            threads = [
                threading.Thread(target=cleanup_thread, args=(f"user{i}",))
                for i in range(50)
            ]

            for t in threads:
                t.start()
            for t in threads:
                t.join()

            assert len(errors) == 0

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_concurrent_connection_storage(self, mock_data_dir, tmp_path):
        """Test concurrent connection storage operations."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            results = {}

            def add_connection(user_id):
                mock_engine = MagicMock()
                manager.connections[f"user{user_id}"] = mock_engine
                time.sleep(0.001)
                results[user_id] = manager.connections.get(f"user{user_id}")

            threads = [
                threading.Thread(target=add_connection, args=(i,))
                for i in range(20)
            ]

            for t in threads:
                t.start()
            for t in threads:
                t.join()

            # All connections should be stored
            assert len(manager.connections) == 20


class TestEncryptionAvailability:
    """Tests for encryption availability checking."""

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_encryption_available_true(self, mock_data_dir, tmp_path):
        """Test has_encryption is True when SQLCipher available."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            assert manager.has_encryption is True

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_encryption_available_false(self, mock_data_dir, tmp_path):
        """Test has_encryption is False when SQLCipher not available."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

        with patch.dict("os.environ", {"LDR_ALLOW_UNENCRYPTED": "true"}):
            with patch.object(
                DatabaseManager,
                "_check_encryption_available",
                return_value=False,
            ):
                manager = DatabaseManager()

                assert manager.has_encryption is False


class TestPoolConfiguration:
    """Tests for connection pool configuration."""

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_static_pool_in_testing_mode(self, mock_data_dir, tmp_path):
        """Test StaticPool is used in testing mode."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

        with patch.dict("os.environ", {"TESTING": "true"}):
            with patch.object(
                DatabaseManager,
                "_check_encryption_available",
                return_value=True,
            ):
                manager = DatabaseManager()

                assert manager._use_static_pool is True
                assert manager._get_pool_kwargs() == {}

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_queue_pool_in_production_mode(self, mock_data_dir, tmp_path):
        """Test QueuePool is used in production mode."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

        with patch.dict("os.environ", {}, clear=True):
            with patch.object(
                DatabaseManager,
                "_check_encryption_available",
                return_value=True,
            ):
                manager = DatabaseManager()
                manager._use_static_pool = False

                kwargs = manager._get_pool_kwargs()

                assert "pool_size" in kwargs
                assert kwargs["pool_size"] == 10
                assert kwargs["max_overflow"] == 30


class TestValidEncryptionKey:
    """Tests for _is_valid_encryption_key functionality."""

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_valid_keys(self, mock_data_dir, tmp_path):
        """Test valid encryption keys are accepted."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            assert manager._is_valid_encryption_key("password") is True
            assert manager._is_valid_encryption_key("a") is True
            assert manager._is_valid_encryption_key("complex!@#$%^&*()") is True
            assert manager._is_valid_encryption_key("123456") is True
            assert manager._is_valid_encryption_key("   spaces   ") is True

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_invalid_keys(self, mock_data_dir, tmp_path):
        """Test invalid encryption keys are rejected."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

            assert manager._is_valid_encryption_key(None) is False
            assert manager._is_valid_encryption_key("") is False


class TestMakeSqlcipherConnectionCursorCloseLogging:
    """Tests for cursor.close() failure logging in _make_sqlcipher_connection (PR #2145)."""

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_cursor_close_failure_logs_warning(self, mock_data_dir, tmp_path):
        """When cursor.close() fails during cleanup, logger.warning is called."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

        # Mock sqlcipher3 module
        mock_conn = MagicMock()
        mock_cursor = MagicMock()
        mock_conn.cursor.return_value = mock_cursor

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

        # Make set_sqlcipher_key raise to trigger cleanup path
        with patch(
            "local_deep_research.database.encrypted_db.get_sqlcipher_module",
            return_value=mock_sqlcipher3,
        ):
            with patch(
                "local_deep_research.database.encrypted_db.set_sqlcipher_key",
                side_effect=ValueError("Bad key"),
            ):
                # Make cursor.close() also fail
                mock_cursor.close.side_effect = Exception(
                    "Cursor already closed"
                )

                with patch(
                    "local_deep_research.database.encrypted_db.logger"
                ) as mock_logger:
                    with pytest.raises(ValueError, match="Bad key"):
                        DatabaseManager._make_sqlcipher_connection(
                            tmp_path / "test.db", "password"
                        )

                    # Verify logger.warning was called about cursor close failure
                    mock_logger.warning.assert_called_once()
                    call_args = mock_logger.warning.call_args
                    assert "Failed to close cursor" in call_args[0][0]

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_original_exception_propagates_despite_cursor_close_failure(
        self, mock_data_dir, tmp_path
    ):
        """Original exception still propagates even when cursor.close() fails."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

        mock_conn = MagicMock()
        mock_cursor = MagicMock()
        mock_conn.cursor.return_value = mock_cursor

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

        with patch(
            "local_deep_research.database.encrypted_db.get_sqlcipher_module",
            return_value=mock_sqlcipher3,
        ):
            with patch(
                "local_deep_research.database.encrypted_db.set_sqlcipher_key",
                side_effect=ValueError("Verification failed"),
            ):
                mock_cursor.close.side_effect = RuntimeError("Cannot close")

                # The original ValueError should propagate, not the RuntimeError
                with pytest.raises(ValueError, match="Verification failed"):
                    DatabaseManager._make_sqlcipher_connection(
                        tmp_path / "test.db", "password"
                    )

                # conn.close() should still be called
                mock_conn.close.assert_called_once()

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_cursor_close_success_no_warning(self, mock_data_dir, tmp_path):
        """No warning logged when cursor.close() succeeds during cleanup."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

        mock_conn = MagicMock()
        mock_cursor = MagicMock()
        mock_conn.cursor.return_value = mock_cursor

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

        with patch(
            "local_deep_research.database.encrypted_db.get_sqlcipher_module",
            return_value=mock_sqlcipher3,
        ):
            with patch(
                "local_deep_research.database.encrypted_db.set_sqlcipher_key",
                side_effect=ValueError("Bad key"),
            ):
                # cursor.close() succeeds (no side_effect)
                with patch(
                    "local_deep_research.database.encrypted_db.logger"
                ) as mock_logger:
                    with pytest.raises(ValueError, match="Bad key"):
                        DatabaseManager._make_sqlcipher_connection(
                            tmp_path / "test.db", "password"
                        )

                    # No warning should be logged when cursor.close() succeeds
                    mock_logger.warning.assert_not_called()


class TestTimingAttackPrevention:
    """Tests for timing attack prevention in open_user_database and
    create_thread_safe_session_for_metrics (PR #2168).

    Key derivation (get_key_from_password) must happen BEFORE the
    db_path.exists() check so that both existing and non-existing users
    take the same amount of time, preventing username enumeration.
    """

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_open_user_database_derives_key_before_file_check(
        self, mock_data_dir, tmp_path
    ):
        """open_user_database derives key before checking if user db exists."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

        with patch(
            "local_deep_research.database.encrypted_db.get_key_from_password"
        ) as mock_derive:
            mock_derive.return_value = b"\x00" * 32

            # Non-existent user — no database file exists
            result = manager.open_user_database(
                "nonexistent_user", "password123"
            )

            # Should return None
            assert result is None
            # Key derivation must have been called
            mock_derive.assert_called_once()

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_open_user_database_derives_key_for_existing_user(
        self, mock_data_dir, tmp_path
    ):
        """Key derivation called for existing users too (timing consistency)."""
        from local_deep_research.config.paths import get_user_database_filename
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

        # Create database at the correct path
        username = "test_user"
        db_path = tmp_path / get_user_database_filename(username)
        db_path.parent.mkdir(parents=True, exist_ok=True)
        db_path.touch()

        with patch.object(manager, "_get_user_db_path", return_value=db_path):
            with patch(
                "local_deep_research.database.encrypted_db.get_key_from_password"
            ) as mock_derive:
                mock_derive.return_value = b"\x00" * 32

                try:
                    manager.open_user_database(username, "password123")
                except Exception:
                    pass  # May fail later; we verify key derivation happened

                # Verify key derivation was called with the expected arguments.
                # Use assert_any_call instead of assert_called_once because
                # concurrent xdist workers sharing the same process may trigger
                # additional calls from other tests (e.g. auth route tests).
                mock_derive.assert_any_call("password123", db_path=db_path)

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_metrics_session_derives_key_before_file_check(
        self, mock_data_dir, tmp_path
    ):
        """create_thread_safe_session_for_metrics derives key before db check."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

        with patch(
            "local_deep_research.database.encrypted_db.get_key_from_password"
        ) as mock_derive:
            mock_derive.return_value = b"\x00" * 32

            with pytest.raises(ValueError, match="No database found"):
                manager.create_thread_safe_session_for_metrics(
                    "nonexistent_user", "password123"
                )

            mock_derive.assert_called_once()

    @patch("local_deep_research.database.encrypted_db.get_data_directory")
    def test_salt_warning_not_called_for_nonexistent_db(
        self, mock_data_dir, tmp_path
    ):
        """has_per_database_salt not called when db doesn't exist."""
        from local_deep_research.database.encrypted_db import DatabaseManager

        mock_data_dir.return_value = tmp_path

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

        with (
            patch(
                "local_deep_research.database.encrypted_db.get_key_from_password"
            ) as mock_derive,
            patch(
                "local_deep_research.database.encrypted_db.has_per_database_salt"
            ) as mock_salt,
        ):
            mock_derive.return_value = b"\x00" * 32

            with pytest.raises(ValueError, match="No database found"):
                manager.create_thread_safe_session_for_metrics(
                    "nonexistent_user", "password123"
                )

            # has_per_database_salt must NOT be called for non-existent db
            mock_salt.assert_not_called()
