"""
Coverage tests for FindingsRepository.synthesize_findings.

Tests cover:
- Windows timeout path: success, timeout error, string response
- Unix timeout path: success, timeout error, string response
- Error classification: timeout, token_limit, rate_limit, connection,
  authentication, unknown
- Knowledge truncation: exceeds 24000 chars, under limit
- Old formatting: string-to-dict conversion, dict passthrough,
  format_findings call
- Accumulated knowledge: None joins findings, provided value used directly
"""

from unittest.mock import MagicMock, patch

from local_deep_research.advanced_search_system.findings.repository import (
    FindingsRepository,
)


def _make_repo(model=None):
    """Create a FindingsRepository with a mocked model."""
    if model is None:
        model = MagicMock()
        model.invoke.return_value = MagicMock(content="synthesized")
    return FindingsRepository(model=model)


# ---------------------------------------------------------------------------
# Windows timeout path
# ---------------------------------------------------------------------------


class TestWindowsTimeoutSuccess:
    """synthesize_findings on Windows: successful LLM invocation."""

    @patch("platform.system", return_value="Windows")
    def test_windows_success_returns_content_attribute(self, _mock_sys):
        """On Windows, response with .content attribute returns its value."""
        model = MagicMock()
        response = MagicMock()
        response.content = "windows answer"
        model.invoke.return_value = response

        repo = _make_repo(model)
        result = repo.synthesize_findings(
            query="q", sub_queries=["sq"], findings=[{"content": "data"}]
        )

        assert result == "windows answer"

    @patch("platform.system", return_value="Windows")
    def test_windows_success_string_response(self, _mock_sys):
        """On Windows, a plain string response is converted via str()."""
        model = MagicMock()
        plain = "plain string win"
        model.invoke.return_value = plain

        repo = _make_repo(model)
        result = repo.synthesize_findings(
            query="q", sub_queries=[], findings=[{"content": "data"}]
        )

        assert "plain string win" in result


class TestWindowsTimeoutError:
    """synthesize_findings on Windows: LLM times out."""

    @patch("platform.system", return_value="Windows")
    def test_windows_timeout_returns_error_message(self, _mock_sys):
        """On Windows, when thread never completes, timeout error returned."""

        class FakeThread:
            """Thread that simulates never completing."""

            def __init__(self, *args, **kwargs):
                self.daemon = False

            def start(self):
                pass

            def join(self, timeout=None):
                pass

            def is_alive(self):
                return True

        model = MagicMock()

        with patch("threading.Thread", FakeThread):
            repo = _make_repo(model)
            result = repo.synthesize_findings(
                query="q",
                sub_queries=[],
                findings=[{"content": "data"}],
            )

        assert "timeout" in result.lower() or "Error" in result


# ---------------------------------------------------------------------------
# Unix timeout path
# ---------------------------------------------------------------------------


class TestUnixTimeoutSuccess:
    """synthesize_findings on Unix: successful LLM invocation."""

    def test_unix_success_returns_content_attribute(self):
        """On Unix (default platform in CI), .content is returned."""
        model = MagicMock()
        response = MagicMock()
        response.content = "unix answer"
        model.invoke.return_value = response

        repo = _make_repo(model)
        result = repo.synthesize_findings(
            query="q", sub_queries=["sq"], findings=[{"content": "data"}]
        )

        assert result == "unix answer"

    def test_unix_success_string_response(self):
        """On Unix, a plain string response is converted via str()."""
        model = MagicMock()
        model.invoke.return_value = "unix plain string"

        repo = _make_repo(model)
        result = repo.synthesize_findings(
            query="q", sub_queries=[], findings=[{"content": "data"}]
        )

        assert "unix plain string" in result


class TestUnixTimeoutError:
    """synthesize_findings on Unix: LLM times out via SIGALRM."""

    def test_unix_timeout_returns_error_message(self):
        """On Unix, TimeoutError from model returns error message."""
        model = MagicMock()
        model.invoke.side_effect = TimeoutError(
            "LLM invocation timed out after 120 seconds"
        )

        repo = _make_repo(model)
        result = repo.synthesize_findings(
            query="q", sub_queries=[], findings=[{"content": "data"}]
        )

        assert "timeout" in result.lower()
        assert "Error" in result


# ---------------------------------------------------------------------------
# Error classification
# ---------------------------------------------------------------------------


class TestErrorClassificationTimeout:
    """Error classification: timeout errors."""

    def test_timeout_keyword(self):
        model = MagicMock()
        model.invoke.side_effect = Exception("Request timeout occurred")
        repo = _make_repo(model)
        result = repo.synthesize_findings(
            query="q", sub_queries=[], findings=[{"content": "f"}]
        )
        assert "timeout" in result.lower()

    def test_timed_out_keyword(self):
        model = MagicMock()
        model.invoke.side_effect = Exception("Operation timed out")
        repo = _make_repo(model)
        result = repo.synthesize_findings(
            query="q", sub_queries=[], findings=[{"content": "f"}]
        )
        assert "timeout" in result.lower()


class TestErrorClassificationTokenLimit:
    """Error classification: token limit errors."""

    def test_too_many_tokens(self):
        model = MagicMock()
        model.invoke.side_effect = Exception("too many tokens in request")
        repo = _make_repo(model)
        result = repo.synthesize_findings(
            query="q", sub_queries=[], findings=[{"content": "f"}]
        )
        assert "token limit" in result.lower()

    def test_context_length(self):
        model = MagicMock()
        model.invoke.side_effect = Exception("maximum context length exceeded")
        repo = _make_repo(model)
        result = repo.synthesize_findings(
            query="q", sub_queries=[], findings=[{"content": "f"}]
        )
        assert "token limit" in result.lower()

    def test_token_limit_phrase(self):
        model = MagicMock()
        model.invoke.side_effect = Exception("token limit has been reached")
        repo = _make_repo(model)
        result = repo.synthesize_findings(
            query="q", sub_queries=[], findings=[{"content": "f"}]
        )
        assert "token limit" in result.lower()


class TestErrorClassificationRateLimit:
    """Error classification: rate limit errors."""

    def test_rate_limit_space(self):
        model = MagicMock()
        model.invoke.side_effect = Exception("rate limit exceeded")
        repo = _make_repo(model)
        result = repo.synthesize_findings(
            query="q", sub_queries=[], findings=[{"content": "f"}]
        )
        assert "rate limit" in result.lower()

    def test_rate_limit_underscore(self):
        model = MagicMock()
        model.invoke.side_effect = Exception("rate_limit error from API")
        repo = _make_repo(model)
        result = repo.synthesize_findings(
            query="q", sub_queries=[], findings=[{"content": "f"}]
        )
        assert "rate limit" in result.lower()


class TestErrorClassificationConnection:
    """Error classification: connection errors."""

    def test_connection_keyword(self):
        model = MagicMock()
        model.invoke.side_effect = Exception("connection refused")
        repo = _make_repo(model)
        result = repo.synthesize_findings(
            query="q", sub_queries=[], findings=[{"content": "f"}]
        )
        assert "connection" in result.lower()

    def test_network_keyword(self):
        model = MagicMock()
        model.invoke.side_effect = Exception("network is unreachable")
        repo = _make_repo(model)
        result = repo.synthesize_findings(
            query="q", sub_queries=[], findings=[{"content": "f"}]
        )
        assert "connection" in result.lower()


class TestErrorClassificationAuthentication:
    """Error classification: authentication errors."""

    def test_api_key_keyword(self):
        model = MagicMock()
        model.invoke.side_effect = Exception("Invalid api key provided")
        repo = _make_repo(model)
        result = repo.synthesize_findings(
            query="q", sub_queries=[], findings=[{"content": "f"}]
        )
        assert "authentication" in result.lower() or "API keys" in result

    def test_authentication_keyword(self):
        model = MagicMock()
        model.invoke.side_effect = Exception("authentication failed")
        repo = _make_repo(model)
        result = repo.synthesize_findings(
            query="q", sub_queries=[], findings=[{"content": "f"}]
        )
        assert "authentication" in result.lower()


class TestErrorClassificationUnknown:
    """Error classification: unknown/generic errors."""

    def test_unknown_error_includes_details(self):
        model = MagicMock()
        model.invoke.side_effect = Exception("something completely unexpected")
        repo = _make_repo(model)
        result = repo.synthesize_findings(
            query="q", sub_queries=[], findings=[{"content": "f"}]
        )
        assert "Error" in result
        assert "something completely unexpected" in result


# ---------------------------------------------------------------------------
# Knowledge truncation
# ---------------------------------------------------------------------------


class TestKnowledgeTruncation:
    """Verify truncation when content exceeds 24000 characters."""

    def test_content_over_24000_is_truncated_in_prompt(self):
        """Content > 24000 chars gets truncated with a marker."""
        model = MagicMock()
        model.invoke.return_value = MagicMock(content="result")
        repo = _make_repo(model)

        long_content = "A" * 50000
        repo.synthesize_findings(
            query="q",
            sub_queries=[],
            findings=[{"content": long_content}],
        )

        prompt = model.invoke.call_args[0][0]
        assert "[...content truncated due to length...]" in prompt

    def test_content_under_24000_not_truncated(self):
        """Content <= 24000 chars is not truncated."""
        model = MagicMock()
        model.invoke.return_value = MagicMock(content="result")
        repo = _make_repo(model)

        short_content = "B" * 20000
        repo.synthesize_findings(
            query="q",
            sub_queries=[],
            findings=[{"content": short_content}],
        )

        prompt = model.invoke.call_args[0][0]
        assert "[...content truncated due to length...]" not in prompt


# ---------------------------------------------------------------------------
# Old formatting path
# ---------------------------------------------------------------------------


class TestOldFormattingStringConversion:
    """old_formatting=True converts string findings to dicts."""

    @patch(
        "local_deep_research.advanced_search_system"
        ".findings.repository.format_findings"
    )
    def test_strings_converted_to_dicts_with_phase(self, mock_format):
        """String findings get phase labels."""
        mock_format.return_value = "formatted"
        repo = _make_repo()

        repo.synthesize_findings(
            query="q",
            sub_queries=[],
            findings=["alpha", "beta"],
            old_formatting=True,
        )

        call_kwargs = mock_format.call_args
        findings_list = (
            call_kwargs.kwargs.get("findings_list")
            if "findings_list" in call_kwargs.kwargs
            else call_kwargs[0][0]
        )

        assert findings_list[0] == {
            "phase": "Finding 1",
            "content": "alpha",
        }
        assert findings_list[1] == {
            "phase": "Finding 2",
            "content": "beta",
        }


class TestOldFormattingDictPassthrough:
    """old_formatting=True passes dict findings through unchanged."""

    @patch(
        "local_deep_research.advanced_search_system"
        ".findings.repository.format_findings"
    )
    def test_dicts_passed_through(self, mock_format):
        """Dict findings are passed directly to format_findings."""
        mock_format.return_value = "formatted"
        repo = _make_repo()
        finding = {"phase": "Custom", "content": "custom content"}

        repo.synthesize_findings(
            query="q",
            sub_queries=[],
            findings=[finding],
            old_formatting=True,
        )

        call_kwargs = mock_format.call_args
        findings_list = (
            call_kwargs.kwargs.get("findings_list")
            if "findings_list" in call_kwargs.kwargs
            else call_kwargs[0][0]
        )

        assert findings_list[0] is finding


class TestOldFormattingCallsFormatFindings:
    """old_formatting=True calls format_findings with correct arguments."""

    @patch(
        "local_deep_research.advanced_search_system"
        ".findings.repository.format_findings"
    )
    def test_format_findings_called_with_correct_args(self, mock_format):
        """format_findings receives correct arguments."""
        mock_format.return_value = "the report"
        repo = _make_repo()
        repo.questions_by_iteration = {1: ["Q1"]}

        result = repo.synthesize_findings(
            query="q",
            sub_queries=[],
            findings=[{"content": "data"}],
            accumulated_knowledge="prebuilt knowledge",
            old_formatting=True,
        )

        assert result == "the report"
        mock_format.assert_called_once()

        call_kwargs = mock_format.call_args
        synth = (
            call_kwargs.kwargs.get("synthesized_content")
            if "synthesized_content" in call_kwargs.kwargs
            else call_kwargs[0][1]
        )
        qbi = (
            call_kwargs.kwargs.get("questions_by_iteration")
            if "questions_by_iteration" in call_kwargs.kwargs
            else call_kwargs[0][2]
        )

        assert synth == "prebuilt knowledge"
        assert qbi == {1: ["Q1"]}


# ---------------------------------------------------------------------------
# Accumulated knowledge assembly
# ---------------------------------------------------------------------------


class TestAccumulatedKnowledgeNone:
    """When accumulated_knowledge is None, findings are joined."""

    @patch(
        "local_deep_research.advanced_search_system"
        ".findings.repository.format_findings"
    )
    def test_none_joins_dict_findings(self, mock_format):
        """None accumulated_knowledge builds text from finding contents."""
        mock_format.return_value = "out"
        repo = _make_repo()

        repo.synthesize_findings(
            query="q",
            sub_queries=[],
            findings=[{"content": "Fact A"}, {"content": "Fact B"}],
            accumulated_knowledge=None,
            old_formatting=True,
        )

        call_kwargs = mock_format.call_args
        synth = (
            call_kwargs.kwargs.get("synthesized_content")
            if "synthesized_content" in call_kwargs.kwargs
            else call_kwargs[0][1]
        )
        assert "Fact A" in synth
        assert "Fact B" in synth

    @patch(
        "local_deep_research.advanced_search_system"
        ".findings.repository.format_findings"
    )
    def test_none_joins_string_findings(self, mock_format):
        """None accumulated_knowledge builds text from string findings."""
        mock_format.return_value = "out"
        repo = _make_repo()

        repo.synthesize_findings(
            query="q",
            sub_queries=[],
            findings=["Text A", "Text B"],
            accumulated_knowledge=None,
            old_formatting=True,
        )

        call_kwargs = mock_format.call_args
        synth = (
            call_kwargs.kwargs.get("synthesized_content")
            if "synthesized_content" in call_kwargs.kwargs
            else call_kwargs[0][1]
        )
        assert "Text A" in synth
        assert "Text B" in synth


class TestAccumulatedKnowledgeProvided:
    """When accumulated_knowledge is provided, it is used directly."""

    @patch(
        "local_deep_research.advanced_search_system"
        ".findings.repository.format_findings"
    )
    def test_provided_knowledge_used_directly(self, mock_format):
        """Provided accumulated_knowledge forwarded as synthesized_content."""
        mock_format.return_value = "out"
        repo = _make_repo()

        repo.synthesize_findings(
            query="q",
            sub_queries=[],
            findings=[{"content": "should be ignored"}],
            accumulated_knowledge="Provided knowledge",
            old_formatting=True,
        )

        call_kwargs = mock_format.call_args
        synth = (
            call_kwargs.kwargs.get("synthesized_content")
            if "synthesized_content" in call_kwargs.kwargs
            else call_kwargs[0][1]
        )
        assert synth == "Provided knowledge"
