"""
Tests for database/library_init.py.

Tests the library initialization module which handles:
- Seeding source_types table with predefined types
- Creating the default "Library" collection
- Full library initialization orchestration
"""

from unittest.mock import MagicMock, patch

import pytest
from sqlalchemy.exc import IntegrityError

from tests.test_utils import add_src_to_path

add_src_to_path()

from local_deep_research.database.library_init import (  # noqa: E402
    seed_source_types,
    ensure_default_library_collection,
    ensure_research_history_collection,
    initialize_library_for_user,
    get_default_library_id,
    get_source_type_id,
)


class TestSeedSourceTypes:
    """Tests for seed_source_types function."""

    @patch("local_deep_research.database.library_init.get_user_db_session")
    @patch("local_deep_research.database.library_init.uuid.uuid4")
    def test_creates_all_predefined_types_when_none_exist(
        self, mock_uuid, mock_get_session
    ):
        """Should create all predefined source types."""
        mock_session = MagicMock()
        mock_get_session.return_value.__enter__.return_value = mock_session
        # Simulate no existing types
        mock_session.query.return_value.filter_by.return_value.first.return_value = None
        mock_uuid.return_value = "test-uuid-123"

        seed_source_types("testuser", "testpass")

        # Should have queried for each type and added 5 new ones
        # (research_download, user_upload, manual_entry, research_report, research_source)
        assert mock_session.add.call_count == 5
        mock_session.commit.assert_called_once()

    @patch("local_deep_research.database.library_init.get_user_db_session")
    def test_skips_existing_types(self, mock_get_session):
        """Should not duplicate existing source types."""
        mock_session = MagicMock()
        mock_get_session.return_value.__enter__.return_value = mock_session
        # Simulate all types already exist
        mock_existing = MagicMock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_existing

        seed_source_types("testuser", "testpass")

        # Should not add any new types
        mock_session.add.assert_not_called()
        mock_session.commit.assert_called_once()

    @patch("local_deep_research.database.library_init.get_user_db_session")
    def test_handles_integrity_error_gracefully(self, mock_get_session):
        """Should handle IntegrityError gracefully without raising."""
        mock_session = MagicMock()
        mock_get_session.return_value.__enter__.return_value = mock_session
        mock_session.query.return_value.filter_by.return_value.first.return_value = None
        mock_session.commit.side_effect = IntegrityError(
            "statement", "params", "orig"
        )

        # Should not raise
        seed_source_types("testuser", "testpass")

    @patch("local_deep_research.database.library_init.get_user_db_session")
    def test_raises_on_unexpected_error(self, mock_get_session):
        """Should re-raise unexpected exceptions."""
        mock_session = MagicMock()
        mock_get_session.return_value.__enter__.return_value = mock_session
        mock_session.query.side_effect = RuntimeError(
            "Database connection failed"
        )

        with pytest.raises(RuntimeError, match="Database connection failed"):
            seed_source_types("testuser", "testpass")

    @patch("local_deep_research.database.library_init.logger")
    @patch("local_deep_research.database.library_init.get_user_db_session")
    def test_logs_creation_messages(self, mock_get_session, mock_logger):
        """Should log when creating source types."""
        mock_session = MagicMock()
        mock_get_session.return_value.__enter__.return_value = mock_session
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        seed_source_types("testuser", "testpass")

        # Should log info for each created type and final success
        assert mock_logger.info.call_count >= 3

    @patch("local_deep_research.database.library_init.get_user_db_session")
    def test_creates_types_with_correct_attributes(self, mock_get_session):
        """Should create source types with correct name, display_name, description, icon."""
        mock_session = MagicMock()
        mock_get_session.return_value.__enter__.return_value = mock_session
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        seed_source_types("testuser")

        # Check the types created
        added_types = [call.args[0] for call in mock_session.add.call_args_list]
        type_names = [t.name for t in added_types]

        assert "research_download" in type_names
        assert "user_upload" in type_names
        assert "manual_entry" in type_names

    @patch("local_deep_research.database.library_init.get_user_db_session")
    def test_password_is_optional(self, mock_get_session):
        """Should work when password is not provided."""
        mock_session = MagicMock()
        mock_get_session.return_value.__enter__.return_value = mock_session
        mock_session.query.return_value.filter_by.return_value.first.return_value = MagicMock()

        # Should not raise when password is None
        seed_source_types("testuser")

        mock_get_session.assert_called_once_with("testuser", None)


class TestEnsureDefaultLibraryCollection:
    """Tests for ensure_default_library_collection function."""

    @patch("local_deep_research.database.library_init.get_user_db_session")
    @patch("local_deep_research.database.library_init.uuid.uuid4")
    def test_creates_collection_when_none_exists(
        self, mock_uuid, mock_get_session
    ):
        """Should create default Library collection when none exists."""
        mock_session = MagicMock()
        mock_get_session.return_value.__enter__.return_value = mock_session
        mock_session.query.return_value.filter_by.return_value.first.return_value = None
        mock_uuid.return_value = "new-library-uuid"

        result = ensure_default_library_collection("testuser", "testpass")

        assert result == "new-library-uuid"
        mock_session.add.assert_called_once()
        mock_session.commit.assert_called_once()

    @patch("local_deep_research.database.library_init.get_user_db_session")
    def test_returns_existing_collection_id(self, mock_get_session):
        """Should return ID of existing default collection."""
        mock_session = MagicMock()
        mock_get_session.return_value.__enter__.return_value = mock_session
        mock_existing = MagicMock()
        mock_existing.id = "existing-library-uuid"
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_existing

        result = ensure_default_library_collection("testuser", "testpass")

        assert result == "existing-library-uuid"
        mock_session.add.assert_not_called()

    @patch("local_deep_research.database.library_init.get_user_db_session")
    def test_creates_with_is_default_true(self, mock_get_session):
        """Should set is_default=True on new collection."""
        mock_session = MagicMock()
        mock_get_session.return_value.__enter__.return_value = mock_session
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        ensure_default_library_collection("testuser")

        # Check that the collection was created with is_default=True
        added_collection = mock_session.add.call_args.args[0]
        assert added_collection.is_default is True

    @patch("local_deep_research.database.library_init.get_user_db_session")
    def test_creates_with_correct_name_and_type(self, mock_get_session):
        """Should create collection with name='Library' and type='default_library'."""
        mock_session = MagicMock()
        mock_get_session.return_value.__enter__.return_value = mock_session
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        ensure_default_library_collection("testuser")

        added_collection = mock_session.add.call_args.args[0]
        assert added_collection.name == "Library"
        assert added_collection.collection_type == "default_library"

    @patch("local_deep_research.database.library_init.get_user_db_session")
    def test_raises_on_database_error(self, mock_get_session):
        """Should re-raise exceptions from database operations."""
        mock_session = MagicMock()
        mock_get_session.return_value.__enter__.return_value = mock_session
        mock_session.query.side_effect = RuntimeError("Database error")

        with pytest.raises(RuntimeError, match="Database error"):
            ensure_default_library_collection("testuser")


class TestEnsureResearchHistoryCollection:
    """Tests for ensure_research_history_collection function."""

    @patch("local_deep_research.database.library_init.get_user_db_session")
    @patch("local_deep_research.database.library_init.uuid.uuid4")
    def test_creates_collection_when_none_exists(
        self, mock_uuid, mock_get_session
    ):
        """Should create Research History collection when none exists."""
        mock_session = MagicMock()
        mock_get_session.return_value.__enter__.return_value = mock_session
        mock_session.query.return_value.filter_by.return_value.first.return_value = None
        mock_uuid.return_value = "new-history-uuid"

        result = ensure_research_history_collection("testuser", "testpass")

        assert result == "new-history-uuid"
        mock_session.add.assert_called_once()
        mock_session.commit.assert_called_once()

    @patch("local_deep_research.database.library_init.get_user_db_session")
    def test_returns_existing_collection_id(self, mock_get_session):
        """Should return ID of existing Research History collection."""
        mock_session = MagicMock()
        mock_get_session.return_value.__enter__.return_value = mock_session
        mock_existing = MagicMock()
        mock_existing.id = "existing-history-uuid"
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_existing

        result = ensure_research_history_collection("testuser", "testpass")

        assert result == "existing-history-uuid"
        mock_session.add.assert_not_called()

    @patch("local_deep_research.database.library_init.get_user_db_session")
    def test_reraises_exception(self, mock_get_session):
        """Should re-raise exceptions from database operations."""
        mock_session = MagicMock()
        mock_get_session.return_value.__enter__.return_value = mock_session
        mock_session.query.side_effect = RuntimeError("Database error")

        with pytest.raises(RuntimeError, match="Database error"):
            ensure_research_history_collection("testuser")


class TestInitializeLibraryForUser:
    """Tests for initialize_library_for_user function."""

    @patch(
        "local_deep_research.database.library_init.ensure_research_history_collection"
    )
    @patch(
        "local_deep_research.database.library_init.ensure_default_library_collection"
    )
    @patch("local_deep_research.database.library_init.seed_source_types")
    def test_returns_success_result(self, mock_seed, mock_ensure, mock_history):
        """Should return dict with success=True on success."""
        mock_ensure.return_value = "library-uuid-123"
        mock_history.return_value = "history-uuid-456"

        result = initialize_library_for_user("testuser", "testpass")

        assert result["success"] is True
        assert result["source_types_seeded"] is True
        assert result["library_collection_id"] == "library-uuid-123"
        assert result["research_history_collection_id"] == "history-uuid-456"
        assert "error" not in result

    @patch(
        "local_deep_research.database.library_init.ensure_research_history_collection"
    )
    @patch(
        "local_deep_research.database.library_init.ensure_default_library_collection"
    )
    @patch("local_deep_research.database.library_init.seed_source_types")
    def test_returns_error_on_seed_failure(
        self, mock_seed, mock_ensure, mock_history
    ):
        """Should include error message when seed_source_types fails."""
        mock_seed.side_effect = RuntimeError("Seeding failed")

        result = initialize_library_for_user("testuser")

        assert result["success"] is False
        assert result["source_types_seeded"] is False
        assert "error" in result
        assert "Seeding failed" in result["error"]

    @patch(
        "local_deep_research.database.library_init.ensure_research_history_collection"
    )
    @patch(
        "local_deep_research.database.library_init.ensure_default_library_collection"
    )
    @patch("local_deep_research.database.library_init.seed_source_types")
    def test_returns_error_on_ensure_failure(
        self, mock_seed, mock_ensure, mock_history
    ):
        """Should include error message when ensure_default_library_collection fails."""
        mock_ensure.side_effect = RuntimeError("Collection creation failed")

        result = initialize_library_for_user("testuser")

        assert result["success"] is False
        assert result["source_types_seeded"] is True  # Seeding succeeded
        assert result["library_collection_id"] is None
        assert "error" in result
        assert "Collection creation failed" in result["error"]

    @patch(
        "local_deep_research.database.library_init.ensure_research_history_collection"
    )
    @patch(
        "local_deep_research.database.library_init.ensure_default_library_collection"
    )
    @patch("local_deep_research.database.library_init.seed_source_types")
    def test_calls_seed_and_ensure_in_order(
        self, mock_seed, mock_ensure, mock_history
    ):
        """Should call both seed_source_types and ensure_default_library_collection."""
        mock_ensure.return_value = "library-uuid"
        call_order = []
        mock_seed.side_effect = lambda *args, **kwargs: call_order.append(
            "seed"
        )
        mock_ensure.side_effect = lambda *args, **kwargs: (
            call_order.append("ensure"),
            "library-uuid",
        )[1]

        initialize_library_for_user("testuser", "testpass")

        assert call_order == ["seed", "ensure"]
        mock_seed.assert_called_once_with("testuser", "testpass")
        mock_ensure.assert_called_once_with("testuser", "testpass")

    @patch(
        "local_deep_research.database.library_init.ensure_default_library_collection"
    )
    @patch("local_deep_research.database.library_init.seed_source_types")
    def test_returns_all_expected_keys(self, mock_seed, mock_ensure):
        """Should return dict with all expected keys."""
        mock_ensure.return_value = "lib-id"

        result = initialize_library_for_user("testuser")

        assert "source_types_seeded" in result
        assert "library_collection_id" in result
        assert "success" in result


class TestGetDefaultLibraryId:
    """Tests for get_default_library_id function."""

    @patch(
        "local_deep_research.database.library_init.ensure_default_library_collection"
    )
    def test_returns_library_id(self, mock_ensure):
        """Should return the library collection ID."""
        mock_ensure.return_value = "default-lib-uuid"

        result = get_default_library_id("testuser", "testpass")

        assert result == "default-lib-uuid"
        mock_ensure.assert_called_once_with("testuser", "testpass")

    @patch(
        "local_deep_research.database.library_init.ensure_default_library_collection"
    )
    def test_creates_library_if_missing(self, mock_ensure):
        """Should create library if it doesn't exist (via ensure_default_library_collection)."""
        mock_ensure.return_value = "new-lib-uuid"

        result = get_default_library_id("testuser")

        assert result == "new-lib-uuid"


class TestGetSourceTypeId:
    """Tests for get_source_type_id function."""

    @patch("local_deep_research.database.library_init.get_user_db_session")
    def test_returns_id_for_valid_type(self, mock_get_session):
        """Should return ID for existing source type."""
        mock_session = MagicMock()
        mock_get_session.return_value.__enter__.return_value = mock_session
        mock_source_type = MagicMock()
        mock_source_type.id = "research-download-uuid"
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_source_type

        result = get_source_type_id("testuser", "research_download", "testpass")

        assert result == "research-download-uuid"

    @patch("local_deep_research.database.library_init.get_user_db_session")
    def test_raises_value_error_for_unknown_type(self, mock_get_session):
        """Should raise ValueError for non-existent type."""
        mock_session = MagicMock()
        mock_get_session.return_value.__enter__.return_value = mock_session
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        with pytest.raises(ValueError, match="Source type not found"):
            get_source_type_id("testuser", "nonexistent_type")

    @patch("local_deep_research.database.library_init.get_user_db_session")
    def test_raises_on_database_error(self, mock_get_session):
        """Should re-raise database errors after logging."""
        mock_session = MagicMock()
        mock_get_session.return_value.__enter__.return_value = mock_session
        mock_session.query.side_effect = RuntimeError("Connection lost")

        with pytest.raises(RuntimeError, match="Connection lost"):
            get_source_type_id("testuser", "user_upload")

    @patch("local_deep_research.database.library_init.get_user_db_session")
    def test_password_is_optional(self, mock_get_session):
        """Should work when password is not provided."""
        mock_session = MagicMock()
        mock_get_session.return_value.__enter__.return_value = mock_session
        mock_source_type = MagicMock()
        mock_source_type.id = "type-uuid"
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_source_type

        result = get_source_type_id("testuser", "manual_entry")

        assert result == "type-uuid"
        mock_get_session.assert_called_once_with("testuser", None)
