"""High-value edge case tests for followup_research module.

Covers gaps not addressed by existing test_models.py and test_service.py:
- perform_followup fallback context when parent missing
- prepare_research_context field mapping completeness
- FollowUpRequest/Response field types and boundary values
- Service interaction with various parent data shapes
"""

from unittest.mock import patch, MagicMock

from local_deep_research.followup_research.models import (
    FollowUpRequest,
    FollowUpResponse,
)
from local_deep_research.followup_research.service import (
    FollowUpResearchService,
)


class TestFollowUpRequestEdgeCases:
    """Edge cases for FollowUpRequest dataclass."""

    def test_negative_max_iterations_allowed(self):
        """Dataclass does not validate; negative values pass through."""
        req = FollowUpRequest(
            parent_research_id="123", question="test", max_iterations=-1
        )
        assert req.max_iterations == -1

    def test_zero_questions_per_iteration(self):
        """Zero questions_per_iteration is accepted (no validation)."""
        req = FollowUpRequest(
            parent_research_id="123", question="test", questions_per_iteration=0
        )
        d = req.to_dict()
        assert d["questions_per_iteration"] == 0

    def test_to_dict_returns_all_five_keys(self):
        """Verify to_dict returns exactly the expected keys."""
        req = FollowUpRequest(parent_research_id="p1", question="q1")
        d = req.to_dict()
        expected_keys = {
            "parent_research_id",
            "question",
            "strategy",
            "max_iterations",
            "questions_per_iteration",
        }
        assert set(d.keys()) == expected_keys

    def test_custom_strategy_round_trips(self):
        """Non-default strategy value round-trips through to_dict."""
        req = FollowUpRequest(
            parent_research_id="p1", question="q1", strategy="custom-strat"
        )
        assert req.to_dict()["strategy"] == "custom-strat"


class TestFollowUpResponseEdgeCases:
    """Edge cases for FollowUpResponse dataclass."""

    def test_to_dict_returns_all_seven_keys(self):
        """Verify to_dict returns exactly the expected keys."""
        resp = FollowUpResponse(
            research_id="r1",
            question="q1",
            answer="a1",
            sources_used=[],
            parent_context_used=False,
            reused_links_count=0,
            new_links_count=0,
        )
        expected_keys = {
            "research_id",
            "question",
            "answer",
            "sources_used",
            "parent_context_used",
            "reused_links_count",
            "new_links_count",
        }
        assert set(resp.to_dict().keys()) == expected_keys

    def test_sources_used_preserves_dict_structure(self):
        """Source dicts are passed through to_dict without modification."""
        sources = [{"url": "http://example.com", "title": "Ex"}]
        resp = FollowUpResponse(
            research_id="r1",
            question="q1",
            answer="a1",
            sources_used=sources,
            parent_context_used=True,
            reused_links_count=1,
            new_links_count=0,
        )
        assert resp.to_dict()["sources_used"] is sources

    def test_large_link_counts(self):
        """Large link counts are stored correctly."""
        resp = FollowUpResponse(
            research_id="r1",
            question="q1",
            answer="a1",
            sources_used=[],
            parent_context_used=True,
            reused_links_count=999999,
            new_links_count=888888,
        )
        d = resp.to_dict()
        assert d["reused_links_count"] == 999999
        assert d["new_links_count"] == 888888


class TestPerformFollowupEdgeCases:
    """Tests for FollowUpResearchService.perform_followup edge cases."""

    @patch.object(FollowUpResearchService, "prepare_research_context")
    def test_perform_followup_empty_context_uses_fallback(self, mock_prepare):
        """When prepare_research_context returns {}, perform_followup builds fallback context."""
        mock_prepare.return_value = {}
        service = FollowUpResearchService(username="testuser")
        request = FollowUpRequest(
            parent_research_id="missing-id", question="What next?"
        )

        result = service.perform_followup(request)

        # Should still produce valid research params
        assert result["query"] == "What next?"
        assert result["strategy"] == "contextual-followup"
        assert result["research_context"]["parent_research_id"] == "missing-id"
        assert result["research_context"]["past_links"] == []
        assert result["research_context"]["past_findings"] == ""

    @patch.object(FollowUpResearchService, "prepare_research_context")
    def test_perform_followup_preserves_delegate_strategy(self, mock_prepare):
        """The delegate_strategy from the request is passed through."""
        mock_prepare.return_value = {
            "parent_research_id": "p1",
            "past_links": [],
            "past_findings": "",
            "report_content": "",
            "resources": [],
            "all_links_of_system": [],
            "original_query": "orig",
        }
        service = FollowUpResearchService(username="testuser")
        request = FollowUpRequest(
            parent_research_id="p1",
            question="Follow up",
            strategy="evidence-based",
        )

        result = service.perform_followup(request)
        assert result["delegate_strategy"] == "evidence-based"

    @patch.object(FollowUpResearchService, "prepare_research_context")
    def test_perform_followup_passes_iterations_config(self, mock_prepare):
        """max_iterations and questions_per_iteration flow through to params."""
        mock_prepare.return_value = {
            "parent_research_id": "p1",
            "past_links": [],
            "past_findings": "",
            "report_content": "",
            "resources": [],
            "all_links_of_system": [],
            "original_query": "",
        }
        service = FollowUpResearchService(username="testuser")
        request = FollowUpRequest(
            parent_research_id="p1",
            question="q",
            max_iterations=5,
            questions_per_iteration=10,
        )

        result = service.perform_followup(request)
        assert result["max_iterations"] == 5
        assert result["questions_per_iteration"] == 10


class TestPrepareResearchContextFieldMapping:
    """Verify prepare_research_context maps parent_data fields correctly."""

    @patch.object(FollowUpResearchService, "load_parent_research")
    def test_all_context_keys_present(self, mock_load):
        """Context dict contains all expected keys."""
        mock_load.return_value = {
            "research_id": "r1",
            "query": "original query",
            "report_content": "report text",
            "formatted_findings": "findings text",
            "strategy": "some-strategy",
            "resources": [{"url": "http://example.com"}],
            "all_links_of_system": [{"url": "http://example.com"}],
        }
        service = FollowUpResearchService(username="testuser")
        ctx = service.prepare_research_context("r1")

        expected_keys = {
            "parent_research_id",
            "past_links",
            "past_findings",
            "report_content",
            "resources",
            "all_links_of_system",
            "original_query",
        }
        assert set(ctx.keys()) == expected_keys

    @patch.object(FollowUpResearchService, "load_parent_research")
    def test_missing_optional_fields_default_to_empty(self, mock_load):
        """When parent_data lacks optional keys, defaults are empty strings/lists."""
        mock_load.return_value = {
            "research_id": "r1",
            # Missing: formatted_findings, report_content, resources, all_links_of_system, query
        }
        service = FollowUpResearchService(username="testuser")
        ctx = service.prepare_research_context("r1")

        assert ctx["past_findings"] == ""
        assert ctx["report_content"] == ""
        assert ctx["resources"] == []
        assert ctx["original_query"] == ""


class TestLoadParentResearchMetaFallback:
    """Tests for load_parent_research meta sources fallback paths."""

    @patch("local_deep_research.followup_research.service.get_user_db_session")
    def test_load_with_meta_sources_key(self, mock_session_ctx):
        """When DB has no sources but research_meta has 'sources' key, it's used."""
        mock_session = MagicMock()
        mock_session_ctx.return_value.__enter__ = MagicMock(
            return_value=mock_session
        )
        mock_session_ctx.return_value.__exit__ = MagicMock(return_value=False)

        mock_research = MagicMock()
        mock_research.id = "r1"
        mock_research.query = "test query"
        mock_research.report_content = "report"
        mock_research.research_meta = {
            "sources": [{"url": "http://example.com", "title": "Test"}],
            "formatted_findings": "findings",
            "strategy_name": "test-strategy",
        }
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_research

        # Mock ResearchSourcesService
        with patch(
            "local_deep_research.followup_research.service.ResearchSourcesService"
        ) as mock_svc_cls:
            mock_svc = MagicMock()
            mock_svc_cls.return_value = mock_svc
            # First call: no sources in DB
            # After save: return the sources
            mock_svc.get_research_sources.side_effect = [
                [],  # First call: empty
                [{"url": "http://example.com", "title": "Test"}],  # After save
            ]
            mock_svc.save_research_sources.return_value = 1

            service = FollowUpResearchService(username="testuser")
            result = service.load_parent_research("r1")

            assert result["query"] == "test query"
            assert result["strategy"] == "test-strategy"
            assert len(result["resources"]) == 1
            mock_svc.save_research_sources.assert_called_once()
