"""
Tests for report_generator.py - Structure Parsing Edge Cases

Tests cover the parsing of LLM-generated report structures, including:
- Subsection parsing with pipes
- Malformed structure handling
- Source section filtering

These tests address real bugs like the one fixed in commit 5128c1d6 for pipes in purpose.
"""

from unittest.mock import MagicMock, patch

import pytest


class TestSubsectionParsingEdgeCases:
    """Tests for edge cases in subsection parsing."""

    @pytest.fixture
    def report_generator(self):
        """Create a report generator with mocked dependencies."""
        with patch("local_deep_research.report_generator.AdvancedSearchSystem"):
            with patch(
                "local_deep_research.report_generator.get_llm"
            ) as mock_get_llm:
                mock_llm = MagicMock()
                mock_get_llm.return_value = mock_llm

                from local_deep_research.report_generator import (
                    IntegratedReportGenerator,
                )

                generator = IntegratedReportGenerator(llm=mock_llm)
                yield generator

    def test_multiple_pipes_in_purpose(self, report_generator):
        """'Overview | What is x | How it works' preserves all after first pipe."""
        # Simulate LLM response with multiple pipes
        response = """
        STRUCTURE
        1. Introduction
           - Overview | What is x | How it works
        END_STRUCTURE
        """
        report_generator.model.invoke.return_value = MagicMock(content=response)

        findings = {"current_knowledge": "Test content " * 100}
        structure = report_generator._determine_report_structure(
            findings, "test query"
        )

        assert len(structure) == 1
        assert len(structure[0]["subsections"]) == 1
        # Should preserve everything after the first pipe
        assert structure[0]["subsections"][0]["name"] == "Overview"
        assert (
            "What is x | How it works"
            in structure[0]["subsections"][0]["purpose"]
        )

    def test_pipe_at_start_of_purpose(self, report_generator):
        """'|| double pipe' handles empty before first pipe."""
        response = """
        STRUCTURE
        1. Section
           - || double pipe content
        END_STRUCTURE
        """
        report_generator.model.invoke.return_value = MagicMock(content=response)

        findings = {"current_knowledge": "Test content " * 100}
        structure = report_generator._determine_report_structure(
            findings, "test query"
        )

        assert len(structure) == 1
        # Empty name before first pipe - should handle gracefully
        if structure[0]["subsections"]:
            subsection = structure[0]["subsections"][0]
            # The name might be empty or trimmed
            assert "name" in subsection
        # The name might be empty or the whole thing might be skipped

    def test_empty_purpose_after_pipe(self, report_generator):
        """'Overview |' uses default purpose."""
        response = """
        STRUCTURE
        1. Section
           - Overview |
        END_STRUCTURE
        """
        report_generator.model.invoke.return_value = MagicMock(content=response)

        findings = {"current_knowledge": "Test content " * 100}
        structure = report_generator._determine_report_structure(
            findings, "test query"
        )

        assert len(structure) == 1
        subsection = structure[0]["subsections"][0]
        assert subsection["name"] == "Overview"
        # Empty purpose after pipe should be empty string
        assert subsection["purpose"] == ""

    def test_whitespace_only_after_pipe(self, report_generator):
        """'Overview |   ' strips to empty, uses default."""
        response = """
        STRUCTURE
        1. Section
           - Overview |
        END_STRUCTURE
        """
        report_generator.model.invoke.return_value = MagicMock(content=response)

        findings = {"current_knowledge": "Test content " * 100}
        structure = report_generator._determine_report_structure(
            findings, "test query"
        )

        subsection = structure[0]["subsections"][0]
        assert subsection["name"] == "Overview"
        # Whitespace should be stripped
        assert subsection["purpose"].strip() == ""

    def test_special_chars_in_section_name(self, report_generator):
        """[Section 1] (Important) parsed correctly."""
        response = """
        STRUCTURE
        1. [Section 1] (Important)
           - Details | Explanation
        END_STRUCTURE
        """
        report_generator.model.invoke.return_value = MagicMock(content=response)

        findings = {"current_knowledge": "Test content " * 100}
        structure = report_generator._determine_report_structure(
            findings, "test query"
        )

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

    def test_unicode_in_section_names(self, report_generator):
        """Non-ASCII characters handled."""
        response = """
        STRUCTURE
        1. Introducción
           - Resumen | Descripción general
        2. 日本語セクション
           - 詳細 | 説明
        END_STRUCTURE
        """
        report_generator.model.invoke.return_value = MagicMock(content=response)

        findings = {"current_knowledge": "Test content " * 100}
        structure = report_generator._determine_report_structure(
            findings, "test query"
        )

        assert len(structure) == 2
        assert "Introducción" in structure[0]["name"]
        assert "日本語セクション" in structure[1]["name"]

    def test_very_long_section_names(self, report_generator):
        """Names over 200 chars."""
        long_name = "A" * 250
        response = f"""
        STRUCTURE
        1. {long_name}
           - Subsection | Purpose
        END_STRUCTURE
        """
        report_generator.model.invoke.return_value = MagicMock(content=response)

        findings = {"current_knowledge": "Test content " * 100}
        structure = report_generator._determine_report_structure(
            findings, "test query"
        )

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

    def test_numbered_section_with_leading_whitespace(self, report_generator):
        """'  1. Intro' parsed correctly."""
        response = """
        STRUCTURE
          1. Introduction
           - Overview | Purpose
        END_STRUCTURE
        """
        report_generator.model.invoke.return_value = MagicMock(content=response)

        findings = {"current_knowledge": "Test content " * 100}
        structure = report_generator._determine_report_structure(
            findings, "test query"
        )

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

    def test_subsection_without_dash(self, report_generator):
        """Missing dash marker handled."""
        response = """
        STRUCTURE
        1. Section
           Subsection without dash | Purpose
        END_STRUCTURE
        """
        report_generator.model.invoke.return_value = MagicMock(content=response)

        findings = {"current_knowledge": "Test content " * 100}
        structure = report_generator._determine_report_structure(
            findings, "test query"
        )

        # Subsection without dash should not be parsed
        assert len(structure) == 1
        # May have empty subsections list or no subsections
        assert len(structure[0]["subsections"]) == 0

    def test_consecutive_pipe_characters(self, report_generator):
        """'Name|||Purpose' handles multiple pipes."""
        response = """
        STRUCTURE
        1. Section
           - Name|||Purpose with pipes
        END_STRUCTURE
        """
        report_generator.model.invoke.return_value = MagicMock(content=response)

        findings = {"current_knowledge": "Test content " * 100}
        structure = report_generator._determine_report_structure(
            findings, "test query"
        )

        subsection = structure[0]["subsections"][0]
        assert subsection["name"] == "Name"
        # Everything after first pipe preserved
        assert "||Purpose with pipes" in subsection["purpose"]


class TestMalformedStructureHandling:
    """Tests for handling malformed LLM responses."""

    @pytest.fixture
    def report_generator(self):
        """Create a report generator with mocked dependencies."""
        with patch("local_deep_research.report_generator.AdvancedSearchSystem"):
            with patch(
                "local_deep_research.report_generator.get_llm"
            ) as mock_get_llm:
                mock_llm = MagicMock()
                mock_get_llm.return_value = mock_llm

                from local_deep_research.report_generator import (
                    IntegratedReportGenerator,
                )

                generator = IntegratedReportGenerator(llm=mock_llm)
                yield generator

    def test_missing_structure_keyword(self, report_generator):
        """No STRUCTURE marker in response."""
        response = """
        1. Introduction
           - Overview | Purpose
        2. Main Content
           - Details | Information
        """
        report_generator.model.invoke.return_value = MagicMock(content=response)

        findings = {"current_knowledge": "Test content " * 100}
        structure = report_generator._determine_report_structure(
            findings, "test query"
        )

        # Should still parse numbered sections
        assert len(structure) >= 0  # Might be empty or partially parsed

    def test_missing_end_structure(self, report_generator):
        """No END_STRUCTURE marker."""
        response = """
        STRUCTURE
        1. Introduction
           - Overview | Purpose
        2. Main Content
           - Details | Information
        """
        report_generator.model.invoke.return_value = MagicMock(content=response)

        findings = {"current_knowledge": "Test content " * 100}
        structure = report_generator._determine_report_structure(
            findings, "test query"
        )

        # Should still parse content after STRUCTURE
        assert len(structure) == 2

    def test_empty_structure_block(self, report_generator):
        """STRUCTURE...END_STRUCTURE with nothing between."""
        response = """
        STRUCTURE
        END_STRUCTURE
        """
        report_generator.model.invoke.return_value = MagicMock(content=response)

        findings = {"current_knowledge": "Test content " * 100}
        structure = report_generator._determine_report_structure(
            findings, "test query"
        )

        # Should return empty structure
        assert structure == []

    def test_invalid_json_like_structure(self, report_generator):
        """LLM returns JSON instead of expected format."""
        response = """
        STRUCTURE
        {
            "sections": [
                {"name": "Introduction", "subsections": []}
            ]
        }
        END_STRUCTURE
        """
        report_generator.model.invoke.return_value = MagicMock(content=response)

        findings = {"current_knowledge": "Test content " * 100}
        structure = report_generator._determine_report_structure(
            findings, "test query"
        )

        # Should not crash, might return empty
        assert isinstance(structure, list)

    def test_partial_section_definition(self, report_generator):
        """Incomplete section definition."""
        response = """
        STRUCTURE
        1. Complete Section
           - Subsection | Purpose
        2.
        3. Another Complete
           - Sub | Purpose
        END_STRUCTURE
        """
        report_generator.model.invoke.return_value = MagicMock(content=response)

        findings = {"current_knowledge": "Test content " * 100}
        structure = report_generator._determine_report_structure(
            findings, "test query"
        )

        # Should parse complete sections, skip incomplete
        # Section 2 has no name after the dot
        assert len(structure) >= 2

    def test_determine_structure_handles_string_response(
        self, report_generator
    ):
        """A raw string LLM return (no .content) is handled without AttributeError."""
        report_generator.model.invoke.return_value = (
            "STRUCTURE\n1. Introduction\n   - Overview | Purpose\nEND_STRUCTURE"
        )

        findings = {"current_knowledge": "Test content " * 100}
        structure = report_generator._determine_report_structure(
            findings, "test query"
        )

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

    def test_determine_structure_strips_think_tags(self, report_generator):
        """<think> reasoning is stripped before parsing so it can't create bogus sections."""
        response = (
            "<think>\n"
            "1. First I should consider the topic\n"
            "2. Then organize by theme\n"
            "</think>\n"
            "STRUCTURE\n"
            "1. Introduction\n"
            "   - Overview | Purpose\n"
            "END_STRUCTURE"
        )
        report_generator.model.invoke.return_value = MagicMock(content=response)

        findings = {"current_knowledge": "Test content " * 100}
        structure = report_generator._determine_report_structure(
            findings, "test query"
        )

        # Without stripping, the numbered lines inside <think> would parse as
        # bogus sections; with stripping only the real section remains.
        assert len(structure) == 1
        assert structure[0]["name"] == "Introduction"


class TestSourceSectionFiltering:
    """Tests for filtering source-related sections."""

    @pytest.fixture
    def report_generator(self):
        """Create a report generator with mocked dependencies."""
        with patch("local_deep_research.report_generator.AdvancedSearchSystem"):
            with patch(
                "local_deep_research.report_generator.get_llm"
            ) as mock_get_llm:
                mock_llm = MagicMock()
                mock_get_llm.return_value = mock_llm

                from local_deep_research.report_generator import (
                    IntegratedReportGenerator,
                )

                generator = IntegratedReportGenerator(llm=mock_llm)
                yield generator

    def test_references_section_removed(self, report_generator):
        """'References' as last section removed."""
        response = """
        STRUCTURE
        1. Introduction
           - Overview | Purpose
        2. Main Content
           - Details | Information
        3. References
           - List | Sources used
        END_STRUCTURE
        """
        report_generator.model.invoke.return_value = MagicMock(content=response)

        findings = {"current_knowledge": "Test content " * 100}
        structure = report_generator._determine_report_structure(
            findings, "test query"
        )

        # References should be removed
        assert len(structure) == 2
        assert all("References" not in s["name"] for s in structure)

    def test_bibliography_section_removed(self, report_generator):
        """'Bibliography' as last section removed."""
        response = """
        STRUCTURE
        1. Introduction
           - Overview | Purpose
        2. Bibliography
           - Sources | References
        END_STRUCTURE
        """
        report_generator.model.invoke.return_value = MagicMock(content=response)

        findings = {"current_knowledge": "Test content " * 100}
        structure = report_generator._determine_report_structure(
            findings, "test query"
        )

        assert len(structure) == 1
        assert "Bibliography" not in structure[0]["name"]

    def test_sources_section_not_last_preserved(self, report_generator):
        """'Sources' not at end preserved."""
        response = """
        STRUCTURE
        1. Data Sources
           - Overview | Where data comes from
        2. Analysis
           - Details | Information
        3. Conclusion
           - Summary | Final thoughts
        END_STRUCTURE
        """
        report_generator.model.invoke.return_value = MagicMock(content=response)

        findings = {"current_knowledge": "Test content " * 100}
        structure = report_generator._determine_report_structure(
            findings, "test query"
        )

        # Data Sources is not last, should be preserved
        # Only the LAST section is checked for source keywords
        assert len(structure) == 3
        assert any("Sources" in s["name"] for s in structure)

    def test_multiple_source_sections(self, report_generator):
        """Only last source-related section removed."""
        response = """
        STRUCTURE
        1. Data Sources Overview
           - Types | Different sources
        2. Main Content
           - Details | Information
        3. Citation Sources
           - List | All citations
        END_STRUCTURE
        """
        report_generator.model.invoke.return_value = MagicMock(content=response)

        findings = {"current_knowledge": "Test content " * 100}
        structure = report_generator._determine_report_structure(
            findings, "test query"
        )

        # Only last section (Citation Sources) should be removed
        assert len(structure) == 2
        # First sources section should still be there
        assert "Data Sources Overview" in structure[0]["name"]

    def test_case_insensitive_source_detection(self, report_generator):
        """'REFERENCES', 'references' both detected."""
        # Test uppercase
        response = """
        STRUCTURE
        1. Introduction
           - Overview | Purpose
        2. REFERENCES
           - List | Sources
        END_STRUCTURE
        """
        report_generator.model.invoke.return_value = MagicMock(content=response)

        findings = {"current_knowledge": "Test content " * 100}
        structure = report_generator._determine_report_structure(
            findings, "test query"
        )

        assert len(structure) == 1
        assert "REFERENCES" not in structure[0]["name"]


class TestReportGenerationIntegration:
    """Integration tests for report generation flow."""

    @pytest.fixture
    def report_generator(self):
        """Create a report generator with mocked dependencies."""
        with patch(
            "local_deep_research.report_generator.AdvancedSearchSystem"
        ) as mock_search:
            with patch(
                "local_deep_research.report_generator.get_llm"
            ) as mock_get_llm:
                mock_llm = MagicMock()
                mock_get_llm.return_value = mock_llm

                # Mock search system
                mock_search_instance = MagicMock()
                mock_search_instance.all_links_of_system = []
                mock_search_instance.analyze_topic.return_value = {
                    "current_knowledge": "Section content"
                }
                mock_search.return_value = mock_search_instance

                from local_deep_research.report_generator import (
                    IntegratedReportGenerator,
                )

                generator = IntegratedReportGenerator(llm=mock_llm)
                generator.search_system = mock_search_instance
                yield generator

    def test_section_without_subsections_creates_default(
        self, report_generator
    ):
        """Section with no subsections gets default subsection."""
        response = """
        STRUCTURE
        1. Standalone Section
        END_STRUCTURE
        """
        report_generator.model.invoke.return_value = MagicMock(content=response)

        findings = {"current_knowledge": "Test content " * 100}
        structure = report_generator._determine_report_structure(
            findings, "test query"
        )

        assert len(structure) == 1
        assert structure[0]["subsections"] == []  # No subsections in structure

    def test_research_and_generate_handles_empty_subsections(
        self, report_generator
    ):
        """_research_and_generate_sections handles sections with no subsections."""
        structure = [{"name": "Standalone", "subsections": []}]

        # This should create default subsections during generation
        sections = report_generator._research_and_generate_sections(
            {"current_knowledge": "test", "questions_by_iteration": {}},
            structure,
            "test query",
        )

        assert "Standalone" in sections


class TestSplitDotBug:
    """Section-name parsing splits only on the first period (split('.', 1)).

    The parser does: section_name = line.split(".", 1)[1].strip(), so section
    names containing periods (e.g. "1. U.S. Policy") are preserved intact.
    """

    @pytest.fixture
    def report_generator(self):
        with patch("local_deep_research.report_generator.AdvancedSearchSystem"):
            with patch(
                "local_deep_research.report_generator.get_llm"
            ) as mock_get_llm:
                mock_llm = MagicMock()
                mock_get_llm.return_value = mock_llm
                from local_deep_research.report_generator import (
                    IntegratedReportGenerator,
                )

                generator = IntegratedReportGenerator(llm=mock_llm)
                yield generator

    def test_period_in_section_name_preserved(self, report_generator):
        """'1. U.S. Policy' keeps periods after the section number (split('.', 1))."""
        response = "STRUCTURE\n1. U.S. Policy\nEND_STRUCTURE"
        report_generator.model.invoke.return_value = MagicMock(content=response)
        findings = {"current_knowledge": "Test " * 100}
        structure = report_generator._determine_report_structure(findings, "q")
        assert len(structure) == 1
        assert structure[0]["name"] == "U.S. Policy"

    def test_abbreviation_in_section_name_preserved(self, report_generator):
        """'1. IPv4 vs. IPv6' keeps the trailing abbreviation (split('.', 1))."""
        response = "STRUCTURE\n1. IPv4 vs. IPv6\nEND_STRUCTURE"
        report_generator.model.invoke.return_value = MagicMock(content=response)
        findings = {"current_knowledge": "Test " * 100}
        structure = report_generator._determine_report_structure(findings, "q")
        assert structure[0]["name"] == "IPv4 vs. IPv6"

    def test_double_digit_section_number(self, report_generator):
        """'10. Conclusion' parses correctly since split('.')[1] = ' Conclusion'."""
        response = "STRUCTURE\n10. Conclusion\nEND_STRUCTURE"
        report_generator.model.invoke.return_value = MagicMock(content=response)
        findings = {"current_knowledge": "Test " * 100}
        structure = report_generator._determine_report_structure(findings, "q")
        assert structure[0]["name"] == "Conclusion"

    def test_orphan_subsection_before_section(self, report_generator):
        """Subsection before any section header is skipped."""
        response = (
            "STRUCTURE\n   - Orphan | purpose\n1. Real Section\nEND_STRUCTURE"
        )
        report_generator.model.invoke.return_value = MagicMock(content=response)
        findings = {"current_knowledge": "Test " * 100}
        structure = report_generator._determine_report_structure(findings, "q")
        assert len(structure) == 1
        assert structure[0]["name"] == "Real Section"
        assert structure[0]["subsections"] == []

    def test_only_source_section_returns_empty(self, report_generator):
        """If only section is source-related, result is empty."""
        response = "STRUCTURE\n1. References\nEND_STRUCTURE"
        report_generator.model.invoke.return_value = MagicMock(content=response)
        findings = {"current_knowledge": "Test " * 100}
        structure = report_generator._determine_report_structure(findings, "q")
        assert len(structure) == 0

    def test_empty_lines_between_sections(self, report_generator):
        """Empty lines between sections are ignored."""
        response = "STRUCTURE\n1. Intro\n\n\n2. Methods\nEND_STRUCTURE"
        report_generator.model.invoke.return_value = MagicMock(content=response)
        findings = {"current_knowledge": "Test " * 100}
        structure = report_generator._determine_report_structure(findings, "q")
        assert len(structure) == 2


class TestFormatFinalReport:
    """Tests for final report formatting."""

    @pytest.fixture
    def report_generator(self):
        """Create a report generator with mocked dependencies."""
        with patch("local_deep_research.report_generator.AdvancedSearchSystem"):
            with patch(
                "local_deep_research.report_generator.get_llm"
            ) as mock_get_llm:
                mock_llm = MagicMock()
                mock_get_llm.return_value = mock_llm

                mock_search_instance = MagicMock()
                mock_search_instance.all_links_of_system = []

                from local_deep_research.report_generator import (
                    IntegratedReportGenerator,
                )

                generator = IntegratedReportGenerator(llm=mock_llm)
                generator.search_system = mock_search_instance
                yield generator

    def test_format_includes_toc(self, report_generator):
        """Final report includes table of contents."""
        structure = [
            {
                "name": "Introduction",
                "subsections": [
                    {"name": "Overview", "purpose": "General intro"}
                ],
            }
        ]
        sections = {"Introduction": "# Introduction\n\nContent here"}

        with patch(
            "local_deep_research.report_generator.importlib"
        ) as mock_import:
            mock_utils = MagicMock()
            mock_utils.search_utilities.format_links_to_markdown.return_value = ""
            mock_import.import_module.return_value = mock_utils

            report = report_generator._format_final_report(
                sections, structure, "test query"
            )

        assert "Table of Contents" in report["content"]
        assert "Introduction" in report["content"]

    def test_format_includes_metadata(self, report_generator):
        """Final report includes metadata."""
        structure = []
        sections = {}

        with patch(
            "local_deep_research.report_generator.importlib"
        ) as mock_import:
            mock_utils = MagicMock()
            mock_utils.search_utilities.format_links_to_markdown.return_value = ""
            mock_import.import_module.return_value = mock_utils

            report = report_generator._format_final_report(
                sections, structure, "test query"
            )

        assert "metadata" in report
        assert "generated_at" in report["metadata"]
        assert "query" in report["metadata"]
        assert report["metadata"]["query"] == "test query"
