"""High-value tests for the storage module.

Covers gaps not addressed by existing tests:
- ABC enforcement (cannot instantiate ReportStorage directly)
- DatabaseReportStorage CRUD with mocked session
- FileReportStorage read/write/delete with tmp_path
- Module __all__ exports
"""

import pytest
from unittest.mock import MagicMock

from local_deep_research.storage.base import ReportStorage
from local_deep_research.storage.database import DatabaseReportStorage
from local_deep_research.storage.file import FileReportStorage
from local_deep_research.storage.database_with_file_backup import (
    DatabaseWithFileBackupStorage,
)


class TestReportStorageABC:
    """Test the abstract base class enforcement."""

    def test_cannot_instantiate_abstract_base_class(self):
        """ReportStorage cannot be instantiated directly."""
        with pytest.raises(TypeError):
            ReportStorage()

    def test_subclass_without_all_methods_raises(self):
        """Incomplete subclass raises TypeError on instantiation."""

        class PartialStorage(ReportStorage):
            def save_report(self, *args, **kwargs):
                pass

        with pytest.raises(TypeError):
            PartialStorage()

    def test_subclass_with_all_methods_is_instantiable(self):
        """A complete subclass can be instantiated."""

        class CompleteStorage(ReportStorage):
            def save_report(self, *a, **kw):
                return True

            def get_report(self, *a, **kw):
                return None

            def get_report_with_metadata(self, *a, **kw):
                return None

            def delete_report(self, *a, **kw):
                return True

            def list_reports(self, *a, **kw):
                return []

            def report_exists(self, *a, **kw):
                return False

        storage = CompleteStorage()
        assert isinstance(storage, ReportStorage)

    def test_all_concrete_classes_inherit_from_base(self):
        """All concrete storage classes are subclasses of ReportStorage."""
        assert issubclass(DatabaseReportStorage, ReportStorage)
        assert issubclass(FileReportStorage, ReportStorage)
        assert issubclass(DatabaseWithFileBackupStorage, ReportStorage)


class TestDatabaseReportStorageCRUD:
    """Test DatabaseReportStorage CRUD operations with mocked session."""

    def _make_storage(self):
        session = MagicMock()
        return DatabaseReportStorage(session), session

    def test_save_report_not_found_returns_false(self):
        """save_report returns False when research doesn't exist."""
        storage, session = self._make_storage()
        session.query.return_value.filter_by.return_value.first.return_value = (
            None
        )

        result = storage.save_report("nonexistent", "content")
        assert result is False

    def test_save_report_updates_content_and_commits(self):
        """save_report sets report_content and calls commit."""
        storage, session = self._make_storage()
        mock_research = MagicMock()
        mock_research.research_meta = None
        session.query.return_value.filter_by.return_value.first.return_value = (
            mock_research
        )

        result = storage.save_report("r1", "new content")
        assert result is True
        assert mock_research.report_content == "new content"
        session.commit.assert_called_once()

    def test_save_report_updates_existing_metadata(self):
        """save_report merges metadata into existing research_meta."""
        storage, session = self._make_storage()
        mock_research = MagicMock()
        # Use a real dict so .update() works as expected
        mock_research.research_meta = {"existing_key": "val"}
        session.query.return_value.filter_by.return_value.first.return_value = (
            mock_research
        )

        storage.save_report("r1", "content", metadata={"new_key": "new_val"})
        assert "new_key" in mock_research.research_meta
        assert mock_research.research_meta["new_key"] == "new_val"
        assert mock_research.research_meta["existing_key"] == "val"

    def test_save_report_sets_metadata_when_none(self):
        """save_report sets research_meta when it was None."""
        storage, session = self._make_storage()
        mock_research = MagicMock()
        mock_research.research_meta = None
        session.query.return_value.filter_by.return_value.first.return_value = (
            mock_research
        )

        storage.save_report("r1", "content", metadata={"key": "val"})
        assert mock_research.research_meta == {"key": "val"}

    def test_save_report_rollback_on_exception(self):
        """save_report calls rollback when commit raises."""
        storage, session = self._make_storage()
        mock_research = MagicMock()
        mock_research.research_meta = None
        session.query.return_value.filter_by.return_value.first.return_value = (
            mock_research
        )
        session.commit.side_effect = Exception("DB error")

        result = storage.save_report("r1", "content")
        assert result is False
        session.rollback.assert_called_once()

    def test_get_report_returns_none_for_missing(self):
        """get_report returns None when research not found."""
        storage, session = self._make_storage()
        session.query.return_value.filter_by.return_value.first.return_value = (
            None
        )

        assert storage.get_report("nonexistent") is None

    def test_get_report_returns_none_for_empty_content(self):
        """get_report returns None when report_content is None."""
        storage, session = self._make_storage()
        mock_research = MagicMock()
        mock_research.report_content = None
        session.query.return_value.filter_by.return_value.first.return_value = (
            mock_research
        )

        assert storage.get_report("r1") is None

    def test_get_report_returns_content(self):
        """get_report returns the report content string."""
        storage, session = self._make_storage()
        mock_research = MagicMock()
        mock_research.report_content = "# Report"
        session.query.return_value.filter_by.return_value.first.return_value = (
            mock_research
        )

        assert storage.get_report("r1") == "# Report"

    def test_get_report_with_metadata_returns_expected_keys(self):
        """get_report_with_metadata returns all expected keys."""
        storage, session = self._make_storage()
        mock_research = MagicMock()
        mock_research.report_content = "content"
        mock_research.research_meta = {"key": "val"}
        mock_research.query = "test query"
        mock_research.mode = "deep"
        mock_research.created_at = "2024-01-01"
        mock_research.completed_at = "2024-01-02"
        mock_research.duration_seconds = 120
        session.query.return_value.filter_by.return_value.first.return_value = (
            mock_research
        )

        result = storage.get_report_with_metadata("r1")
        expected_keys = {
            "content",
            "metadata",
            "query",
            "mode",
            "created_at",
            "completed_at",
            "duration_seconds",
        }
        assert set(result.keys()) == expected_keys

    def test_delete_report_sets_content_to_none(self):
        """delete_report nullifies report_content instead of removing the row."""
        storage, session = self._make_storage()
        mock_research = MagicMock()
        mock_research.report_content = "content"
        session.query.return_value.filter_by.return_value.first.return_value = (
            mock_research
        )

        result = storage.delete_report("r1")
        assert result is True
        assert mock_research.report_content is None
        session.commit.assert_called_once()

    def test_delete_report_returns_false_for_missing(self):
        """delete_report returns False when research not found."""
        storage, session = self._make_storage()
        session.query.return_value.filter_by.return_value.first.return_value = (
            None
        )

        assert storage.delete_report("nonexistent") is False

    def test_delete_report_rollback_on_exception(self):
        """delete_report calls rollback on commit failure."""
        storage, session = self._make_storage()
        mock_research = MagicMock()
        session.query.return_value.filter_by.return_value.first.return_value = (
            mock_research
        )
        session.commit.side_effect = Exception("DB error")

        result = storage.delete_report("r1")
        assert result is False
        session.rollback.assert_called_once()

    def test_report_exists_true(self):
        """report_exists returns True when report with content exists."""
        storage, session = self._make_storage()
        mock_research = MagicMock()
        mock_research.report_content = "content"
        session.query.return_value.filter_by.return_value.first.return_value = (
            mock_research
        )

        assert storage.report_exists("r1") is True

    def test_report_exists_false_when_no_content(self):
        """report_exists returns False when report_content is None."""
        storage, session = self._make_storage()
        mock_research = MagicMock()
        mock_research.report_content = None
        session.query.return_value.filter_by.return_value.first.return_value = (
            mock_research
        )

        assert storage.report_exists("r1") is False

    def test_report_exists_false_on_exception(self):
        """report_exists returns False on database exception."""
        storage, session = self._make_storage()
        session.query.side_effect = Exception("DB error")

        assert storage.report_exists("r1") is False


class TestFileReportStoragePaths:
    """Test FileReportStorage path construction and file operations."""

    def test_report_path_ends_with_md(self, tmp_path):
        """Report files use .md extension."""
        storage = FileReportStorage(base_dir=tmp_path)
        path = storage._get_report_path("test-id")
        assert str(path).endswith("test-id.md")

    def test_metadata_path_ends_with_metadata_json(self, tmp_path):
        """Metadata files use _metadata.json suffix."""
        storage = FileReportStorage(base_dir=tmp_path)
        path = storage._get_metadata_path("test-id")
        assert str(path).endswith("test-id_metadata.json")

    def test_report_exists_checks_md_file_only(self, tmp_path):
        """report_exists only checks for the .md file, not metadata."""
        storage = FileReportStorage(base_dir=tmp_path)

        # Create only metadata file
        meta_path = storage._get_metadata_path("test-id")
        meta_path.write_text("{}")

        assert storage.report_exists("test-id") is False

    def test_get_report_returns_none_for_nonexistent(self, tmp_path):
        """get_report returns None when file doesn't exist."""
        storage = FileReportStorage(base_dir=tmp_path)
        assert storage.get_report("nonexistent") is None

    def test_get_report_with_metadata_returns_none_when_no_report(
        self, tmp_path
    ):
        """get_report_with_metadata returns None when no report file exists."""
        storage = FileReportStorage(base_dir=tmp_path)
        assert storage.get_report_with_metadata("nonexistent") is None

    def test_delete_report_returns_false_for_nonexistent(self, tmp_path):
        """delete_report returns False when no report file exists."""
        storage = FileReportStorage(base_dir=tmp_path)
        assert storage.delete_report("nonexistent") is False

    def test_base_dir_created_if_not_exists(self, tmp_path):
        """FileReportStorage creates base_dir including nested dirs."""
        nested = tmp_path / "deep" / "nested" / "dir"
        FileReportStorage(base_dir=nested)
        assert nested.exists()


class TestModuleExports:
    """Test the storage module exports."""

    def test_storage_init_exports(self):
        """The storage __init__ module exports expected names."""
        import local_deep_research.storage as storage_module

        # Should be able to import key classes
        assert hasattr(storage_module, "ReportStorage") or hasattr(
            storage_module, "get_report_storage"
        )
