"""Tests for ReportGenerator helper methods.

Covers _truncate_at_sentence_boundary and _build_previous_context — two
pure-logic helpers with zero prior test coverage that are critical for
report quality and repetition avoidance.
"""

from unittest.mock import MagicMock

import pytest

from local_deep_research.report_generator import IntegratedReportGenerator


@pytest.fixture
def generator():
    """Create an IntegratedReportGenerator with mocked dependencies."""
    mock_llm = MagicMock()
    mock_search = MagicMock()
    gen = IntegratedReportGenerator.__new__(IntegratedReportGenerator)
    gen.llm = mock_llm
    gen.search_system = mock_search
    gen.max_context_sections = 3
    gen.max_context_chars = 4000
    return gen


# ── _truncate_at_sentence_boundary ──


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

    def test_text_shorter_than_limit_returned_unchanged(self, generator):
        text = "Short text."
        assert generator._truncate_at_sentence_boundary(text, 100) == text

    def test_text_exactly_at_limit_returned_unchanged(self, generator):
        text = "Exact." + "x" * 94  # 100 chars
        assert generator._truncate_at_sentence_boundary(text, 100) == text

    def test_truncates_at_period_followed_by_space(self, generator):
        text = "First sentence. Second sentence. Third sentence that goes on."
        result = generator._truncate_at_sentence_boundary(text, 35)
        assert result.startswith("First sentence. Second sentence.")
        assert result.endswith("\n[...truncated]")

    def test_truncates_at_exclamation_mark(self, generator):
        text = "Wow! This is amazing! More content follows here."
        result = generator._truncate_at_sentence_boundary(text, 25)
        assert "Wow! This is amazing!" in result
        assert "[...truncated]" in result

    def test_truncates_at_question_mark(self, generator):
        text = "Is this working? Yes it is working perfectly fine here."
        result = generator._truncate_at_sentence_boundary(text, 20)
        assert "Is this working?" in result
        assert "[...truncated]" in result

    def test_boundary_at_end_of_truncated_text(self, generator):
        # Period at exactly the last position of truncated text
        text = "Hello." + "x" * 100
        result = generator._truncate_at_sentence_boundary(text, 6)
        # "Hello." is 6 chars, truncated[:6] = "Hello."
        # last_sentence_end = 6, min_acceptable = int(6*0.8)=4, 6 > 4 → use boundary
        assert "Hello." in result
        assert "[...truncated]" in result

    def test_no_sentence_boundary_falls_back_to_hard_truncation(
        self, generator
    ):
        text = "a" * 200
        result = generator._truncate_at_sentence_boundary(text, 100)
        assert result == "a" * 100 + "\n[...truncated]"

    def test_sentence_boundary_too_early_falls_back(self, generator):
        # Period at position 5 out of 100 → below 80% threshold
        text = "Hi. " + "x" * 200
        result = generator._truncate_at_sentence_boundary(text, 100)
        # min_acceptable = 80, last_sentence_end = 4, 4 < 80 → hard truncation
        assert result == text[:100] + "\n[...truncated]"

    def test_sentence_boundary_at_80_percent_threshold(self, generator):
        # Exactly at 80% boundary
        # max_chars=100, min_acceptable=80
        text = "x" * 80 + ". " + "y" * 50
        result = generator._truncate_at_sentence_boundary(text, 100)
        # last_sentence_end=81, min_acceptable=80, 81 > 80 → use boundary
        assert result.endswith("\n[...truncated]")
        assert result.startswith("x" * 80 + ".")

    def test_period_followed_by_newline(self, generator):
        text = "First sentence.\nSecond sentence continues for a while here."
        result = generator._truncate_at_sentence_boundary(text, 20)
        assert "First sentence." in result
        assert "[...truncated]" in result

    def test_period_not_followed_by_space_or_newline_ignored(self, generator):
        # "3.14" has a period but it's followed by a digit, not space
        text = "The value is 3.14 and more text follows after that point."
        result = generator._truncate_at_sentence_boundary(text, 20)
        # Only sentence boundaries followed by space/newline are valid
        # In "The value is 3.14 a", the period at index 14 is followed by '1', not space
        # So falls back to hard truncation
        assert result == text[:20] + "\n[...truncated]"

    def test_empty_text(self, generator):
        assert generator._truncate_at_sentence_boundary("", 100) == ""

    def test_single_character(self, generator):
        assert generator._truncate_at_sentence_boundary("a", 100) == "a"

    def test_multiple_sentence_endings_uses_last_valid(self, generator):
        text = "One. Two. Three. Four. Five. Six. Seven. Eight."
        result = generator._truncate_at_sentence_boundary(text, 30)
        # Should find the last boundary within the first 30 chars
        # "One. Two. Three. Four. Five. " is 29 chars
        assert "[...truncated]" in result


# ── _build_previous_context ──


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

    def test_empty_list_returns_empty_string(self, generator):
        assert generator._build_previous_context([]) == ""

    def test_single_finding_included(self, generator):
        result = generator._build_previous_context(["Finding 1"])
        assert "Finding 1" in result
        assert "DO NOT REPEAT" in result
        assert "CONTENT ALREADY WRITTEN" in result

    def test_respects_max_context_sections_limit(self, generator):
        generator.max_context_sections = 2
        findings = ["Finding 1", "Finding 2", "Finding 3", "Finding 4"]
        result = generator._build_previous_context(findings)
        # Should only include last 2 findings
        assert "Finding 3" in result
        assert "Finding 4" in result
        assert "Finding 1" not in result
        assert "Finding 2" not in result

    def test_joins_with_separator(self, generator):
        result = generator._build_previous_context(["A", "B"])
        assert "\n\n---\n\n" in result

    def test_truncates_long_context(self, generator):
        generator.max_context_chars = 50
        long_finding = "x" * 100
        result = generator._build_previous_context([long_finding])
        # The content should be truncated
        assert "[...truncated]" in result

    def test_formatting_markers_present(self, generator):
        result = generator._build_previous_context(["Test content"])
        assert "=== CONTENT ALREADY WRITTEN (DO NOT REPEAT) ===" in result
        assert "=== END OF PREVIOUS CONTENT ===" in result
        assert "CRITICAL:" in result

    def test_context_within_char_limit_not_truncated(self, generator):
        generator.max_context_chars = 10000
        result = generator._build_previous_context(["Short finding."])
        assert "[...truncated]" not in result

    def test_uses_last_n_sections(self, generator):
        generator.max_context_sections = 3
        findings = [f"Finding {i}" for i in range(10)]
        result = generator._build_previous_context(findings)
        assert "Finding 7" in result
        assert "Finding 8" in result
        assert "Finding 9" in result
        assert "Finding 0" not in result
