"""Extended tests for report_generator.py - covering factory function,
structure parsing edge cases, and source section removal."""

from unittest.mock import Mock, patch


class TestGetReportGenerator:
    """Tests for get_report_generator factory function."""

    def test_returns_integrated_report_generator(self):
        """get_report_generator should return IntegratedReportGenerator."""
        from local_deep_research.report_generator import (
            get_report_generator,
            IntegratedReportGenerator,
        )

        mock_search_system = Mock()
        mock_search_system.model = Mock()

        result = get_report_generator(search_system=mock_search_system)

        assert isinstance(result, IntegratedReportGenerator)

    def test_passes_search_system_through(self):
        """get_report_generator should pass search_system to constructor."""
        from local_deep_research.report_generator import get_report_generator

        mock_search_system = Mock()
        mock_search_system.model = Mock()

        result = get_report_generator(search_system=mock_search_system)

        assert result.search_system is mock_search_system

    def test_none_search_system_uses_fallback(self):
        """get_report_generator with None should use fallback initialization."""
        from local_deep_research.report_generator import get_report_generator

        with patch(
            "local_deep_research.report_generator.get_llm"
        ) as mock_get_llm:
            mock_llm = Mock()
            mock_get_llm.return_value = mock_llm

            with patch(
                "local_deep_research.report_generator.AdvancedSearchSystem"
            ) as mock_system_cls:
                mock_system_cls.return_value = Mock()
                result = get_report_generator(search_system=None)

                assert result is not None


class TestDetermineReportStructureParsing:
    """Tests for structure parsing in _determine_report_structure."""

    def _create_generator(self, llm_response):
        """Helper to create a generator with mocked LLM."""
        from local_deep_research.report_generator import (
            IntegratedReportGenerator,
        )

        mock_llm = Mock()
        mock_llm.invoke.return_value = Mock(content=llm_response)
        mock_search_system = Mock()
        mock_search_system.model = mock_llm

        gen = IntegratedReportGenerator(search_system=mock_search_system)
        gen.model = mock_llm
        return gen

    def test_parses_well_formed_structure(self):
        """Well-formed STRUCTURE response should be parsed correctly."""
        llm_response = """
STRUCTURE
1. Introduction
   - Overview | Provide project overview
   - Background | Historical context
2. Technical Details
   - Architecture | System architecture
END_STRUCTURE
"""
        gen = self._create_generator(llm_response)
        structure = gen._determine_report_structure(
            {"current_knowledge": "test content"}, "test query"
        )

        assert len(structure) == 2
        assert structure[0]["name"] == "Introduction"
        assert len(structure[0]["subsections"]) == 2
        assert structure[0]["subsections"][0]["name"] == "Overview"
        assert (
            structure[0]["subsections"][0]["purpose"]
            == "Provide project overview"
        )

    def test_subsection_without_purpose_gets_default(self):
        """Subsection without pipe separator should get default purpose."""
        llm_response = """
STRUCTURE
1. Main Section
   - Subsection Without Purpose
END_STRUCTURE
"""
        gen = self._create_generator(llm_response)
        structure = gen._determine_report_structure(
            {"current_knowledge": "test"}, "test query"
        )

        assert len(structure[0]["subsections"]) == 1
        assert (
            "Provide detailed information"
            in structure[0]["subsections"][0]["purpose"]
        )

    def test_removes_source_section_at_end(self):
        """Source-related last section should be removed."""
        llm_response = """
STRUCTURE
1. Content Section
   - Details | Main details
2. Sources and References
   - Bibliography | Citation list
END_STRUCTURE
"""
        gen = self._create_generator(llm_response)
        structure = gen._determine_report_structure(
            {"current_knowledge": "test"}, "test query"
        )

        assert len(structure) == 1
        assert structure[0]["name"] == "Content Section"

    def test_keeps_source_keyword_in_middle_section(self):
        """Source keyword in non-last section should be kept."""
        llm_response = """
STRUCTURE
1. Open Source Software
   - Overview | Open source overview
2. Conclusion
   - Summary | Final summary
END_STRUCTURE
"""
        gen = self._create_generator(llm_response)
        structure = gen._determine_report_structure(
            {"current_knowledge": "test"}, "test query"
        )

        # "Open Source Software" is not the last section, so it should be kept
        assert len(structure) == 2
        assert structure[0]["name"] == "Open Source Software"

    def test_removes_bibliography_section(self):
        """Bibliography section at end should be removed."""
        llm_response = """
STRUCTURE
1. Analysis
   - Details | Analysis details
2. Bibliography
   - References | All references
END_STRUCTURE
"""
        gen = self._create_generator(llm_response)
        structure = gen._determine_report_structure(
            {"current_knowledge": "test"}, "test query"
        )

        assert len(structure) == 1
        assert structure[0]["name"] == "Analysis"

    def test_removes_citation_section(self):
        """Citation section at end should be removed."""
        llm_response = """
STRUCTURE
1. Content
   - Main | Main content
2. Citations
   - List | Citation list
END_STRUCTURE
"""
        gen = self._create_generator(llm_response)
        structure = gen._determine_report_structure(
            {"current_knowledge": "test"}, "test query"
        )

        assert len(structure) == 1

    def test_empty_response_returns_empty_structure(self):
        """Empty LLM response should return empty structure."""
        gen = self._create_generator("")
        structure = gen._determine_report_structure(
            {"current_knowledge": "test"}, "test query"
        )

        assert structure == []

    def test_response_without_markers(self):
        """Response without STRUCTURE/END_STRUCTURE markers should still parse."""
        llm_response = """
1. First Section
   - Sub A | Purpose A
2. Second Section
   - Sub B | Purpose B
"""
        gen = self._create_generator(llm_response)
        structure = gen._determine_report_structure(
            {"current_knowledge": "test"}, "test query"
        )

        assert len(structure) == 2

    def test_subsection_with_multiple_pipes(self):
        """Subsection with multiple | characters should only split on first."""
        llm_response = """
STRUCTURE
1. Section
   - Sub | Purpose with | pipes | in it
END_STRUCTURE
"""
        gen = self._create_generator(llm_response)
        structure = gen._determine_report_structure(
            {"current_knowledge": "test"}, "test query"
        )

        assert structure[0]["subsections"][0]["name"] == "Sub"
        assert "pipes" in structure[0]["subsections"][0]["purpose"]

    def test_strips_think_tags_from_response(self):
        """Think tags in LLM response should be stripped."""
        llm_response = """<think>Let me think about this...</think>
STRUCTURE
1. Real Content
   - Overview | Content overview
END_STRUCTURE
"""
        gen = self._create_generator(llm_response)
        structure = gen._determine_report_structure(
            {"current_knowledge": "test"}, "test query"
        )

        assert len(structure) == 1
        assert structure[0]["name"] == "Real Content"


class TestTruncateAtSentenceBoundary:
    """Tests for _truncate_at_sentence_boundary."""

    def _create_generator(self):
        from local_deep_research.report_generator import (
            IntegratedReportGenerator,
        )

        mock_search_system = Mock()
        mock_search_system.model = Mock()
        return IntegratedReportGenerator(search_system=mock_search_system)

    def test_no_truncation_when_under_limit(self):
        """Text under the limit should not be truncated."""
        gen = self._create_generator()
        text = "Short text."
        result = gen._truncate_at_sentence_boundary(text, 100)
        assert result == text

    def test_truncates_at_period(self):
        """Should truncate at last period before limit."""
        gen = self._create_generator()
        text = (
            "First sentence. Second sentence. Third sentence that is very long."
        )
        result = gen._truncate_at_sentence_boundary(text, 35)
        assert result.startswith("First sentence. Second sentence.")
        assert "[...truncated]" in result

    def test_truncates_at_exclamation(self):
        """Should truncate at exclamation mark."""
        gen = self._create_generator()
        text = (
            "Amazing! This is great! But this part is too long for the limit."
        )
        result = gen._truncate_at_sentence_boundary(text, 25)
        assert "!" in result.split("[...truncated]")[0]

    def test_truncates_at_question_mark(self):
        """Should truncate at question mark."""
        gen = self._create_generator()
        text = "What happened? Then more text that goes past the boundary by a lot."
        result = gen._truncate_at_sentence_boundary(text, 20)
        assert "?" in result.split("[...truncated]")[0]

    def test_hard_truncation_fallback(self):
        """Should hard-truncate when no sentence boundary preserves 80%."""
        gen = self._create_generator()
        text = "A" * 100  # No sentence boundaries
        result = gen._truncate_at_sentence_boundary(text, 50)
        assert len(result.split("\n")[0]) == 50
        assert "[...truncated]" in result


class TestBuildPreviousContext:
    """Tests for _build_previous_context."""

    def _create_generator(self):
        from local_deep_research.report_generator import (
            IntegratedReportGenerator,
        )

        mock_search_system = Mock()
        mock_search_system.model = Mock()
        return IntegratedReportGenerator(search_system=mock_search_system)

    def test_empty_findings_returns_empty(self):
        """Empty accumulated findings should return empty string."""
        gen = self._create_generator()
        result = gen._build_previous_context([])
        assert result == ""

    def test_limits_to_max_context_sections(self):
        """Should only include last max_context_sections findings."""
        gen = self._create_generator()
        gen.max_context_sections = 2

        findings = [f"Finding {i}" for i in range(5)]
        result = gen._build_previous_context(findings)

        assert "Finding 3" in result
        assert "Finding 4" in result
        assert "Finding 0" not in result

    def test_includes_do_not_repeat_instruction(self):
        """Context should include DO NOT REPEAT instruction."""
        gen = self._create_generator()
        result = gen._build_previous_context(["Some finding"])

        assert "DO NOT REPEAT" in result
        assert "CRITICAL" in result

    def test_truncation_for_long_context(self):
        """Very long context should be truncated."""
        gen = self._create_generator()
        gen.max_context_chars = 100

        long_finding = "A" * 200
        result = gen._build_previous_context([long_finding])

        assert "[...truncated]" in result


class TestGenerateErrorReport:
    """Tests for _generate_error_report."""

    def test_error_report_format(self):
        """Error report should contain query and error message."""
        from local_deep_research.report_generator import (
            IntegratedReportGenerator,
        )

        mock_search_system = Mock()
        mock_search_system.model = Mock()
        gen = IntegratedReportGenerator(search_system=mock_search_system)

        result = gen._generate_error_report("test query", "Something failed")

        assert "test query" in result
        assert "Something failed" in result
        assert "ERROR REPORT" in result

    def test_error_report_with_special_characters(self):
        """Error report should handle special characters."""
        from local_deep_research.report_generator import (
            IntegratedReportGenerator,
        )

        mock_search_system = Mock()
        mock_search_system.model = Mock()
        gen = IntegratedReportGenerator(search_system=mock_search_system)

        result = gen._generate_error_report(
            "query with <html> & special chars", "Error: 'unexpected' \"token\""
        )

        assert "<html>" in result
        assert "unexpected" in result


class TestIntegratedReportGeneratorInit:
    """Tests for constructor edge cases."""

    def test_init_with_llm_only(self):
        """Constructor with only LLM should create search system."""
        from local_deep_research.report_generator import (
            IntegratedReportGenerator,
        )

        mock_llm = Mock()

        with patch(
            "local_deep_research.report_generator.AdvancedSearchSystem"
        ) as mock_system_cls:
            mock_system_cls.return_value = Mock()
            gen = IntegratedReportGenerator(llm=mock_llm)

            assert gen.model is mock_llm
            mock_system_cls.assert_called_once()

    def test_init_with_search_system_uses_system_llm(self):
        """Constructor with search_system should use system's LLM."""
        from local_deep_research.report_generator import (
            IntegratedReportGenerator,
        )

        mock_llm = Mock()
        mock_search_system = Mock()
        mock_search_system.model = mock_llm

        gen = IntegratedReportGenerator(search_system=mock_search_system)

        assert gen.model is mock_llm

    def test_init_with_custom_searches_per_section(self):
        """Custom searches_per_section should be stored."""
        from local_deep_research.report_generator import (
            IntegratedReportGenerator,
        )

        mock_search_system = Mock()
        mock_search_system.model = Mock()

        gen = IntegratedReportGenerator(
            search_system=mock_search_system,
            searches_per_section=5,
        )

        assert gen.searches_per_section == 5

    def test_context_settings_from_snapshot(self):
        """Settings snapshot should override default context settings."""
        from local_deep_research.report_generator import (
            IntegratedReportGenerator,
        )

        mock_search_system = Mock()
        mock_search_system.model = Mock()

        settings = {
            "report.max_context_sections": 5,
            "report.max_context_chars": 8000,
        }

        gen = IntegratedReportGenerator(
            search_system=mock_search_system,
            settings_snapshot=settings,
        )

        assert gen.max_context_sections == 5
        assert gen.max_context_chars == 8000

    def test_default_context_settings(self):
        """Without settings snapshot, defaults should be used."""
        from local_deep_research.report_generator import (
            IntegratedReportGenerator,
        )

        mock_search_system = Mock()
        mock_search_system.model = Mock()

        gen = IntegratedReportGenerator(search_system=mock_search_system)

        assert gen.max_context_sections == 3
        assert gen.max_context_chars == 4000
