"""Comprehensive tests for the ReportStorage abstract base class in storage/base.py.

Tests cover:
- Abstract class cannot be instantiated directly
- Subclass must implement all abstract methods
- Partial implementations raise TypeError
- A complete concrete implementation works correctly
- Method signatures and return types
- Edge cases: empty strings, None metadata, special characters in IDs
"""

import pytest
from abc import ABC
from typing import Dict, Any, Optional

from local_deep_research.storage.base import ReportStorage


# ---------------------------------------------------------------------------
# 1. ABC contract tests
# ---------------------------------------------------------------------------


class TestReportStorageIsAbstract:
    """Verify that ReportStorage behaves as a proper abstract base class."""

    def test_cannot_instantiate_directly(self):
        """ReportStorage cannot be instantiated because it has abstract methods."""
        with pytest.raises(TypeError):
            ReportStorage()

    def test_is_subclass_of_abc(self):
        """ReportStorage inherits from ABC."""
        assert issubclass(ReportStorage, ABC)

    def test_has_six_abstract_methods(self):
        """All six public methods are declared abstract."""
        expected = {
            "save_report",
            "get_report",
            "get_report_with_metadata",
            "delete_report",
            "list_reports",
            "report_exists",
        }
        assert ReportStorage.__abstractmethods__ == expected


# ---------------------------------------------------------------------------
# 2. Partial implementation tests
# ---------------------------------------------------------------------------


class TestPartialImplementationRaisesTypeError:
    """A subclass that skips any single abstract method cannot be instantiated."""

    def test_missing_save_report(self):
        class Incomplete(ReportStorage):
            def get_report(self, research_id, username=None):
                return None

            def get_report_with_metadata(self, research_id, username=None):
                return None

            def delete_report(self, research_id, username=None):
                return False

            def list_reports(self, username=None):
                return []

            def report_exists(self, research_id, username=None):
                return False

        with pytest.raises(TypeError):
            Incomplete()

    def test_missing_get_report(self):
        class Incomplete(ReportStorage):
            def save_report(
                self, research_id, content, metadata=None, username=None
            ):
                return True

            def get_report_with_metadata(self, research_id, username=None):
                return None

            def delete_report(self, research_id, username=None):
                return False

            def list_reports(self, username=None):
                return []

            def report_exists(self, research_id, username=None):
                return False

        with pytest.raises(TypeError):
            Incomplete()

    def test_missing_get_report_with_metadata(self):
        class Incomplete(ReportStorage):
            def save_report(
                self, research_id, content, metadata=None, username=None
            ):
                return True

            def get_report(self, research_id, username=None):
                return None

            def delete_report(self, research_id, username=None):
                return False

            def list_reports(self, username=None):
                return []

            def report_exists(self, research_id, username=None):
                return False

        with pytest.raises(TypeError):
            Incomplete()

    def test_missing_delete_report(self):
        class Incomplete(ReportStorage):
            def save_report(
                self, research_id, content, metadata=None, username=None
            ):
                return True

            def get_report(self, research_id, username=None):
                return None

            def get_report_with_metadata(self, research_id, username=None):
                return None

            def list_reports(self, username=None):
                return []

            def report_exists(self, research_id, username=None):
                return False

        with pytest.raises(TypeError):
            Incomplete()

    def test_missing_report_exists(self):
        class Incomplete(ReportStorage):
            def save_report(
                self, research_id, content, metadata=None, username=None
            ):
                return True

            def get_report(self, research_id, username=None):
                return None

            def get_report_with_metadata(self, research_id, username=None):
                return None

            def delete_report(self, research_id, username=None):
                return False

            def list_reports(self, username=None):
                return []

        with pytest.raises(TypeError):
            Incomplete()

    def test_missing_list_reports(self):
        class Incomplete(ReportStorage):
            def save_report(
                self, research_id, content, metadata=None, username=None
            ):
                return True

            def get_report(self, research_id, username=None):
                return None

            def get_report_with_metadata(self, research_id, username=None):
                return None

            def delete_report(self, research_id, username=None):
                return False

            def report_exists(self, research_id, username=None):
                return False

        with pytest.raises(TypeError):
            Incomplete()


# ---------------------------------------------------------------------------
# 3. In-memory concrete implementation used for functional tests
# ---------------------------------------------------------------------------


class InMemoryReportStorage(ReportStorage):
    """Minimal concrete implementation for testing the abstract interface."""

    def __init__(self):
        self._reports: Dict[str, Dict[str, Any]] = {}

    def _key(self, research_id: str, username: Optional[str] = None) -> str:
        return f"{username or ''}::{research_id}"

    def save_report(
        self,
        research_id: str,
        content: str,
        metadata: Optional[Dict[str, Any]] = None,
        username: Optional[str] = None,
    ) -> bool:
        self._reports[self._key(research_id, username)] = {
            "content": content,
            "metadata": metadata or {},
        }
        return True

    def get_report(
        self, research_id: str, username: Optional[str] = None
    ) -> Optional[str]:
        entry = self._reports.get(self._key(research_id, username))
        return entry["content"] if entry else None

    def get_report_with_metadata(
        self, research_id: str, username: Optional[str] = None
    ) -> Optional[Dict[str, Any]]:
        entry = self._reports.get(self._key(research_id, username))
        if entry is None:
            return None
        return {"content": entry["content"], "metadata": entry["metadata"]}

    def delete_report(
        self, research_id: str, username: Optional[str] = None
    ) -> bool:
        key = self._key(research_id, username)
        if key in self._reports:
            del self._reports[key]
            return True
        return False

    def list_reports(self, username=None):
        prefix = f"{username or ''}::"
        return [{"id": k} for k in self._reports if k.startswith(prefix)]

    def report_exists(
        self, research_id: str, username: Optional[str] = None
    ) -> bool:
        return self._key(research_id, username) in self._reports


@pytest.fixture
def storage():
    return InMemoryReportStorage()


# ---------------------------------------------------------------------------
# 4. Concrete implementation isinstance / subclass checks
# ---------------------------------------------------------------------------


class TestConcreteIsInstance:
    def test_isinstance_of_report_storage(self, storage):
        assert isinstance(storage, ReportStorage)

    def test_issubclass_of_report_storage(self):
        assert issubclass(InMemoryReportStorage, ReportStorage)


# ---------------------------------------------------------------------------
# 5. Functional tests via the in-memory implementation
# ---------------------------------------------------------------------------


class TestSaveReport:
    def test_save_returns_true(self, storage):
        assert storage.save_report("r1", "content") is True

    def test_save_with_metadata(self, storage):
        meta = {"key": "val"}
        assert storage.save_report("r1", "content", metadata=meta) is True

    def test_save_with_username(self, storage):
        assert storage.save_report("r1", "c", username="alice") is True

    def test_save_with_all_params(self, storage):
        assert storage.save_report("r1", "c", {"k": "v"}, "alice") is True

    def test_save_empty_content(self, storage):
        """Empty string is a valid report content."""
        assert storage.save_report("r1", "") is True
        assert storage.get_report("r1") == ""

    def test_save_overwrites_existing(self, storage):
        storage.save_report("r1", "v1")
        storage.save_report("r1", "v2")
        assert storage.get_report("r1") == "v2"


class TestGetReport:
    def test_get_existing(self, storage):
        storage.save_report("r1", "hello")
        assert storage.get_report("r1") == "hello"

    def test_get_nonexistent_returns_none(self, storage):
        assert storage.get_report("no-such-id") is None

    def test_get_with_username(self, storage):
        storage.save_report("r1", "alice-report", username="alice")
        assert storage.get_report("r1", username="alice") == "alice-report"
        # Without username should not find it
        assert storage.get_report("r1") is None

    def test_get_preserves_content_exactly(self, storage):
        content = (
            "# Title\n\n- bullet\n- bullet 2\n\n```python\nprint('hi')\n```"
        )
        storage.save_report("r1", content)
        assert storage.get_report("r1") == content


class TestGetReportWithMetadata:
    def test_returns_dict_with_content_and_metadata(self, storage):
        meta = {"source": "web"}
        storage.save_report("r1", "text", metadata=meta)
        result = storage.get_report_with_metadata("r1")
        assert result is not None
        assert result["content"] == "text"
        assert result["metadata"] == {"source": "web"}

    def test_returns_none_for_nonexistent(self, storage):
        assert storage.get_report_with_metadata("nope") is None

    def test_empty_metadata_when_none_provided(self, storage):
        storage.save_report("r1", "text")
        result = storage.get_report_with_metadata("r1")
        assert result["metadata"] == {}

    def test_with_username(self, storage):
        storage.save_report("r1", "u-text", {"k": 1}, username="bob")
        result = storage.get_report_with_metadata("r1", username="bob")
        assert result is not None
        assert result["content"] == "u-text"


class TestDeleteReport:
    def test_delete_existing_returns_true(self, storage):
        storage.save_report("r1", "data")
        assert storage.delete_report("r1") is True

    def test_delete_removes_report(self, storage):
        storage.save_report("r1", "data")
        storage.delete_report("r1")
        assert storage.get_report("r1") is None

    def test_delete_nonexistent_returns_false(self, storage):
        assert storage.delete_report("no-such") is False

    def test_delete_with_username(self, storage):
        storage.save_report("r1", "data", username="alice")
        assert storage.delete_report("r1", username="alice") is True
        assert storage.report_exists("r1", username="alice") is False

    def test_delete_one_user_does_not_affect_another(self, storage):
        storage.save_report("r1", "alice-data", username="alice")
        storage.save_report("r1", "bob-data", username="bob")
        storage.delete_report("r1", username="alice")
        assert storage.report_exists("r1", username="bob") is True
        assert storage.get_report("r1", username="bob") == "bob-data"


class TestReportExists:
    def test_exists_after_save(self, storage):
        storage.save_report("r1", "data")
        assert storage.report_exists("r1") is True

    def test_not_exists_before_save(self, storage):
        assert storage.report_exists("r1") is False

    def test_not_exists_after_delete(self, storage):
        storage.save_report("r1", "data")
        storage.delete_report("r1")
        assert storage.report_exists("r1") is False

    def test_exists_with_username(self, storage):
        storage.save_report("r1", "data", username="alice")
        assert storage.report_exists("r1", username="alice") is True
        assert storage.report_exists("r1") is False


# ---------------------------------------------------------------------------
# 6. Edge cases
# ---------------------------------------------------------------------------


class TestEdgeCases:
    def test_special_characters_in_research_id(self, storage):
        rid = "report/2024-01-01/some:id#1"
        storage.save_report(rid, "content")
        assert storage.get_report(rid) == "content"
        assert storage.report_exists(rid) is True

    def test_unicode_content(self, storage):
        content = "Zusammenfassung der Forschung: Ergebnis"
        storage.save_report("r1", content)
        assert storage.get_report("r1") == content

    def test_very_long_content(self, storage):
        content = "x" * 100_000
        storage.save_report("r1", content)
        assert storage.get_report("r1") == content
        assert len(storage.get_report("r1")) == 100_000

    def test_metadata_with_nested_dicts(self, storage):
        meta = {"a": {"b": {"c": [1, 2, 3]}}}
        storage.save_report("r1", "text", metadata=meta)
        result = storage.get_report_with_metadata("r1")
        assert result["metadata"]["a"]["b"]["c"] == [1, 2, 3]

    def test_multiple_reports_independent(self, storage):
        storage.save_report("r1", "content1")
        storage.save_report("r2", "content2")
        storage.save_report("r3", "content3")
        assert storage.get_report("r1") == "content1"
        assert storage.get_report("r2") == "content2"
        assert storage.get_report("r3") == "content3"
        storage.delete_report("r2")
        assert storage.report_exists("r1") is True
        assert storage.report_exists("r2") is False
        assert storage.report_exists("r3") is True

    def test_same_id_different_users_are_separate(self, storage):
        storage.save_report("r1", "alice", username="alice")
        storage.save_report("r1", "bob", username="bob")
        assert storage.get_report("r1", username="alice") == "alice"
        assert storage.get_report("r1", username="bob") == "bob"

    def test_metadata_none_explicit(self, storage):
        """Passing metadata=None explicitly should not cause errors."""
        assert storage.save_report("r1", "text", metadata=None) is True
        result = storage.get_report_with_metadata("r1")
        assert result["metadata"] == {}

    def test_username_none_explicit(self, storage):
        """Passing username=None explicitly should behave like omitting it."""
        storage.save_report("r1", "text", username=None)
        assert storage.get_report("r1", username=None) == "text"
        assert storage.report_exists("r1", username=None) is True


# ---------------------------------------------------------------------------
# 7. Method signature tests
# ---------------------------------------------------------------------------


class TestMethodSignatures:
    """Verify that the abstract methods accept the documented parameters."""

    def test_save_report_accepts_keyword_args(self, storage):
        result = storage.save_report(
            research_id="r1",
            content="text",
            metadata={"k": "v"},
            username="alice",
        )
        assert result is True

    def test_get_report_accepts_keyword_args(self, storage):
        storage.save_report("r1", "text")
        result = storage.get_report(research_id="r1", username=None)
        assert result == "text"

    def test_get_report_with_metadata_accepts_keyword_args(self, storage):
        storage.save_report("r1", "text")
        result = storage.get_report_with_metadata(
            research_id="r1", username=None
        )
        assert result is not None

    def test_delete_report_accepts_keyword_args(self, storage):
        storage.save_report("r1", "text")
        result = storage.delete_report(research_id="r1", username=None)
        assert result is True

    def test_report_exists_accepts_keyword_args(self, storage):
        storage.save_report("r1", "text")
        result = storage.report_exists(research_id="r1", username=None)
        assert result is True


# ---------------------------------------------------------------------------
# 8. Return type tests
# ---------------------------------------------------------------------------


class TestReturnTypes:
    def test_save_report_returns_bool(self, storage):
        result = storage.save_report("r1", "text")
        assert isinstance(result, bool)

    def test_get_report_returns_str_or_none(self, storage):
        assert storage.get_report("nope") is None
        storage.save_report("r1", "text")
        result = storage.get_report("r1")
        assert isinstance(result, str)

    def test_get_report_with_metadata_returns_dict_or_none(self, storage):
        assert storage.get_report_with_metadata("nope") is None
        storage.save_report("r1", "text", metadata={"a": 1})
        result = storage.get_report_with_metadata("r1")
        assert isinstance(result, dict)
        assert "content" in result
        assert "metadata" in result

    def test_delete_report_returns_bool(self, storage):
        assert isinstance(storage.delete_report("nope"), bool)

    def test_report_exists_returns_bool(self, storage):
        assert isinstance(storage.report_exists("nope"), bool)
