"""
Behavioral tests for error_handling/report_generator module.

Tests ErrorReportGenerator's pure logic: _make_error_user_friendly,
_format_partial_results, _get_technical_context.
"""


class TestMakeErrorUserFriendly:
    """Tests for ErrorReportGenerator._make_error_user_friendly() method."""

    def test_max_workers_error_replaced(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        result = gen._make_error_user_friendly(
            "max_workers must be greater than 0"
        )
        assert "LLM failed to generate search questions" in result
        assert "Technical error:" in result

    def test_connection_refused_error_replaced(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        result = gen._make_error_user_friendly("Connection refused [Errno 111]")
        assert "Cannot connect to the LLM service" in result

    def test_search_too_long_replaced(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        result = gen._make_error_user_friendly(
            "The search is longer than 256 characters"
        )
        assert "too long for GitHub" in result

    def test_model_not_found_replaced(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        result = gen._make_error_user_friendly(
            "Model 'llama3' not found in Ollama"
        )
        assert "model isn't available in Ollama" in result

    def test_no_auth_credentials_replaced(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        result = gen._make_error_user_friendly("No auth credentials found")
        assert "API key is missing" in result

    def test_readonly_database_replaced(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        result = gen._make_error_user_friendly(
            "Attempt to write readonly database"
        )
        assert "Permission issue" in result

    def test_no_module_error_replaced(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        result = gen._make_error_user_friendly(
            "No module named 'local_deep_research'"
        )
        assert "Installation issue" in result

    def test_no_search_results_replaced(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        result = gen._make_error_user_friendly("No search results found")
        assert "No search results were found" in result

    def test_unknown_error_returns_original(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        msg = "Some completely unknown error xyz123"
        result = gen._make_error_user_friendly(msg)
        assert result == msg

    def test_case_insensitive_matching(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        result = gen._make_error_user_friendly("CONNECTION REFUSED [ERRNO 111]")
        assert "Cannot connect" in result

    def test_docker_networking_error_replaced(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        result = gen._make_error_user_friendly(
            "host localhost Docker container failed"
        )
        assert "Docker networking issue" in result

    def test_context_size_error_replaced(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        result = gen._make_error_user_friendly(
            "TypeError: Context Size invalid"
        )
        assert "Model configuration issue" in result

    def test_searxng_config_error_replaced(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        # Was "database is locked" -- that bare SQLite message is unrelated to
        # SearXNG and is now classified separately (see
        # TestBroadPatternTightening). Use a real SearXNG error here.
        result = gen._make_error_user_friendly(
            "Invalid value for SearXNG setting"
        )
        assert "SearXNG configuration" in result

    def test_replaced_message_includes_technical_error(self):
        """All replaced messages should include the original technical error."""
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        original = "max_workers must be greater than 0"
        result = gen._make_error_user_friendly(original)
        assert original in result

    def test_model_dump_pattern_replaced(self):
        # Issue #3897: surface a multi-cause "Try this" hint when the agent
        # crashes with `'str' object has no attribute 'model_dump'`. The hint
        # must name proxy/shim, server, and model as possible causes.
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        result = gen._make_error_user_friendly(
            "Agent error: AttributeError: 'str' object has no attribute 'model_dump'"
        )

        assert "LangChain could not parse" in result
        assert "proxy/shim" in result
        assert "Try this" in result
        assert "issue #3897" in result
        assert "Technical error:" in result

    def test_unrelated_attribute_error_passes_through(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        msg = "AttributeError: 'NoneType' object has no attribute 'foo'"
        result = gen._make_error_user_friendly(msg)

        # No model_dump match → original message returned verbatim, no hint.
        assert result == msg

    def test_model_dump_pattern_matches_dict_variant(self):
        # langchain-ai/langchain#31391 documents the same bug class with
        # `'dict' object has no attribute 'model_dump'`. The hint applies
        # equally — confirm the regex catches it.
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        result = gen._make_error_user_friendly(
            "Agent error: AttributeError: 'dict' object has no attribute 'model_dump'"
        )

        assert "LangChain could not parse" in result

    def test_unrelated_model_dump_substring_does_not_match(self):
        # Earlier draft of the regex contained a bare `model_dump` alternative
        # that fired on any text containing the substring — including unrelated
        # Pydantic serialization traces. The tightened pattern requires the
        # canonical `object has no attribute 'model_dump'` shape.
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        msg = (
            "ValidationError: 1 validation error for SomeModel\n"
            "  some_field: Invalid input — please call model_dump() before serializing."
        )
        result = gen._make_error_user_friendly(msg)

        assert result == msg


class TestErrorTypeTokenShortCircuit:
    """Tests for the ``(Error type: <code>)`` short-circuit in
    ``_make_error_user_friendly()`` (PR #4087).

    Upstream code (``openai_compat_errors``, the status-code/Ollama branches in
    ``research_service``) emits already-friendly messages carrying a
    ``(Error type: <code>)`` token. The replacement table must NOT clobber those
    -- but the ``unknown`` token is attached to a RAW exception string upstream
    could not classify, so it must still fall through to the table.
    """

    def test_specific_token_message_returned_verbatim(self):
        # The actual #4087 bug: friendly_openai_compatible_error() builds a
        # message that names the provider/URL/model AND happens to contain the
        # substrings "Connection refused" / the Docker hint, which the generic
        # replacement patterns matched and overwrote. With the short-circuit it
        # must be returned untouched.
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        msg = (
            "Cannot reach LM Studio at http://localhost:1234. Check that the "
            "server is running and the URL is correct. (from inside Docker, "
            "localhost is the container itself -- use host.docker.internal, the "
            "host IP, or run with --network=host to share the host network "
            "namespace) (Error type: openai_connection_refused) | Details: "
            "Connection refused [Errno 111]"
        )
        result = gen._make_error_user_friendly(msg)

        # Returned verbatim -- the tailored message survives.
        assert result == msg
        # And specifically NOT replaced by either generic hint.
        assert "Cannot connect to the LLM service" not in result
        assert "Docker networking issue" not in result

    def test_non_token_message_still_replaced(self):
        # Over-fire guard: a raw exception with NO token must still be
        # classified by the replacement table.
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        result = gen._make_error_user_friendly("Connection refused [Errno 111]")

        assert "Cannot connect to the LLM service" in result
        assert "Technical error:" in result

    def test_unknown_token_falls_through_to_replacement_table(self):
        # The #4087 follow-up fix: "(Error type: unknown)" is appended by
        # research_service to a RAW str(exc) it could not classify (e.g. the
        # LLM generated no questions -> ThreadPoolExecutor(max_workers=0)).
        # It must still get the friendly replacement, with the original message
        # (token included) preserved in the technical-error suffix.
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        msg = "max_workers must be greater than 0 (Error type: unknown)"
        result = gen._make_error_user_friendly(msg)

        assert "LLM failed to generate search questions" in result
        assert "Technical error:" in result
        assert "(Error type: unknown)" in result

    def test_unknown_token_with_no_match_returns_original(self):
        # Falling through is safe when nothing matches: an unclassifiable
        # "unknown" message is returned as-is, exactly as before.
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        msg = "Totally novel failure xyz123 (Error type: unknown)"
        result = gen._make_error_user_friendly(msg)

        assert result == msg


class TestBroadPatternTightening:
    """Tests that over-broad replacement patterns no longer misclassify
    unrelated exceptions into wrong advice.

    Three table arms used to match far more than intended:
    ``could not be found`` (any "X could not be found"), ``database.*locked``
    (any SQLite "database is locked" -> wrong SearXNG advice), and
    ``'<' not supported between`` (any heterogeneous-sort TypeError -> wrong
    model-context-size advice). Each was anchored to its real context.
    """

    def test_database_locked_gets_db_advice_not_searxng(self):
        # A plain SQLite "database is locked" (real, under per-user encrypted
        # DBs) must get database-contention advice, NOT the old SearXNG /
        # "use a VPN" advice it used to be misclassified into.
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        result = gen._make_error_user_friendly("Error: database is locked")

        assert "local database is temporarily locked" in result
        assert "SearXNG configuration" not in result
        assert "use a VPN" not in result

    def test_searxng_invalid_value_still_matched(self):
        # The legitimate SearXNG arm must keep working after database.*locked
        # was removed from it.
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        result = gen._make_error_user_friendly(
            "Invalid value for SearXNG setting"
        )
        assert "SearXNG configuration" in result

    def test_generic_could_not_be_found_not_misclassified(self):
        # An unrelated "could not be found" must NOT be classified as a search
        # engine configuration problem.
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        msg = "Model file could not be found at /models/x.gguf"
        result = gen._make_error_user_friendly(msg)

        assert result == msg
        assert "Search engine configuration problem" not in result

    def test_search_engine_errors_still_matched(self):
        # Both the factory failure and an engine-scoped "could not be found"
        # must still get search-engine advice.
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        for msg in (
            "Failed to create search engine 'brave'",
            "search engine 'brave' could not be found",
        ):
            result = gen._make_error_user_friendly(msg)
            assert "Search engine configuration problem" in result

    def test_generic_sort_typeerror_not_misclassified(self):
        # A heterogeneous-sort TypeError unrelated to model config must NOT be
        # classified as a context-size problem.
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        msg = (
            "TypeError: '<' not supported between instances of 'str' and 'int'"
        )
        result = gen._make_error_user_friendly(msg)

        assert result == msg
        assert "Model configuration issue" not in result

    def test_nonetype_context_comparison_still_matched(self):
        # The realistic missing-context-size case (a numeric setting left None
        # then compared) must still get the model-config advice.
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        msg = "'<' not supported between instances of 'int' and 'NoneType'"
        result = gen._make_error_user_friendly(msg)

        assert "Model configuration issue" in result


class TestFormatPartialResults:
    """Tests for ErrorReportGenerator._format_partial_results() method."""

    def test_none_returns_empty(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        assert gen._format_partial_results(None) == ""

    def test_empty_dict_returns_empty(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        assert gen._format_partial_results({}) == ""

    def test_current_knowledge_included(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        partial = {"current_knowledge": "A" * 100}
        result = gen._format_partial_results(partial)
        assert "Research Summary" in result

    def test_short_knowledge_excluded(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        partial = {"current_knowledge": "short"}
        result = gen._format_partial_results(partial)
        assert "Research Summary" not in result

    def test_search_results_included(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        partial = {
            "search_results": [
                {"title": "Result 1", "url": "http://example.com"},
                {"title": "Result 2"},
            ]
        }
        result = gen._format_partial_results(partial)
        assert "Search Results Found" in result
        assert "Result 1" in result

    def test_findings_included(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        partial = {
            "findings": [
                {"phase": "Phase 1", "content": "Found something important"}
            ]
        }
        result = gen._format_partial_results(partial)
        assert "Research Findings" in result
        assert "Phase 1" in result

    def test_error_findings_excluded(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        partial = {
            "findings": [
                {"phase": "Phase 1", "content": "Error: something failed"}
            ]
        }
        result = gen._format_partial_results(partial)
        assert "Error: something failed" not in result

    def test_knowledge_truncated_at_1000(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        partial = {"current_knowledge": "A" * 2000}
        result = gen._format_partial_results(partial)
        assert "..." in result

    def test_max_5_search_results(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        partial = {
            "search_results": [{"title": f"Result {i}"} for i in range(10)]
        }
        result = gen._format_partial_results(partial)
        assert "Result 4" in result
        assert "Result 5" not in result

    def test_includes_note_about_partial_results(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        partial = {"current_knowledge": "A" * 100}
        result = gen._format_partial_results(partial)
        assert "before the error occurred" in result


class TestGetTechnicalContext:
    """Tests for ErrorReportGenerator._get_technical_context() method."""

    def test_empty_context(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        result = gen._get_technical_context({}, None)
        assert result == ""

    def test_includes_start_time(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        partial = {"start_time": "2024-01-01T00:00:00"}
        result = gen._get_technical_context({}, partial)
        assert "Start Time" in result

    def test_includes_model_info(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        partial = {
            "model_config": {"model_name": "llama3", "provider": "ollama"}
        }
        result = gen._get_technical_context({}, partial)
        assert "llama3" in result
        assert "ollama" in result

    def test_includes_search_config(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        partial = {"search_config": {"engine": "searxng", "max_results": 10}}
        result = gen._get_technical_context({}, partial)
        assert "searxng" in result

    def test_includes_status_code(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        partial = {"status_code": 500}
        result = gen._get_technical_context({}, partial)
        assert "500" in result

    def test_connection_error_category_adds_network_note(self):
        from local_deep_research.error_handling.error_reporter import (
            ErrorCategory,
        )
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        analysis = {"category": ErrorCategory.CONNECTION_ERROR}
        result = gen._get_technical_context(analysis, None)
        assert "Network Error" in result

    def test_model_error_category_adds_model_note(self):
        from local_deep_research.error_handling.error_reporter import (
            ErrorCategory,
        )
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        analysis = {"category": ErrorCategory.MODEL_ERROR}
        result = gen._get_technical_context(analysis, None)
        assert "Model Error" in result


class TestGenerateErrorReport:
    """Tests for ErrorReportGenerator.generate_error_report() method."""

    def test_returns_markdown(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        result = gen.generate_error_report("some error", "test query")
        assert "# " in result

    def test_includes_error_type(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        result = gen.generate_error_report("Connection refused", "test query")
        assert "Error Type" in result

    def test_includes_what_happened(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        result = gen.generate_error_report("some error", "test query")
        assert "What happened" in result

    def test_includes_help_section(self):
        from local_deep_research.error_handling.report_generator import (
            ErrorReportGenerator,
        )

        gen = ErrorReportGenerator()
        result = gen.generate_error_report("some error", "test query")
        assert "Get Help" in result
        assert "Discord" in result
        assert "GitHub Issues" in result
