"""Comprehensive coverage tests for error_reporter and resource_utils modules."""

import pytest

from local_deep_research.error_handling.error_reporter import (
    ErrorCategory,
    ErrorReporter,
)
from local_deep_research.utilities.resource_utils import safe_close


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def reporter():
    return ErrorReporter()


# ===========================================================================
# ErrorReporter.categorize_error
# ===========================================================================


class TestCategorizeErrorConnection:
    """Connection-error pattern coverage."""

    @pytest.mark.parametrize(
        "msg",
        [
            "POST predict encountered EOF",
            "Connection refused on port 8080",
            "Request timeout after 30s",
            "Connection to server failed",
            "HTTP error 502 bad gateway",
            "network read error during request",
            "[Errno 111] Connection refused",
            "Cannot reach host.docker.internal",
            "host localhost Docker unreachable",
            "127.0.0.1 Docker container refused",
            "localhost 1234 Docker service down",
            "LM Studio Docker Mac connection issue",
        ],
        ids=lambda m: m[:40],
    )
    def test_connection_patterns(self, reporter, msg):
        assert reporter.categorize_error(msg) == ErrorCategory.CONNECTION_ERROR


class TestCategorizeErrorModel:
    """Model-error pattern coverage."""

    @pytest.mark.parametrize(
        "msg",
        [
            "Model 'gpt-5' not found",
            "Invalid model identifier provided",
            "Ollama service not available",
            "API key is invalid or expired",
            "Authentication error: forbidden",
            "max_workers must be greater than 0",
            "TypeError Context Size mismatch",
            "'<' not supported between instances of 'int' and 'NoneType'",
            "No auth credentials found for provider",
            "401 API key missing or revoked",
        ],
        ids=lambda m: m[:40],
    )
    def test_model_patterns(self, reporter, msg):
        assert reporter.categorize_error(msg) == ErrorCategory.MODEL_ERROR


class TestCategorizeErrorRateLimit:
    """Rate-limit-error pattern coverage."""

    @pytest.mark.parametrize(
        "msg",
        [
            "429 resource exhausted please retry",
            "429 too many requests slow down",
            "rate limit exceeded for this key",
            "rate_limit reached on endpoint",
            "ratelimit triggered by server",
            "quota exceeded for project",
            "resource exhausted quota depleted",
            "threshold requests per minute hit",
            "LLM rate limit in effect",
            "API rate limit for anthropic",
            "maximum 60 requests per minute allowed",
            "maximum 1000 requests per hour reached",
        ],
        ids=lambda m: m[:40],
    )
    def test_rate_limit_patterns(self, reporter, msg):
        assert reporter.categorize_error(msg) == ErrorCategory.RATE_LIMIT_ERROR


class TestCategorizeErrorSearch:
    """Search-error pattern coverage."""

    @pytest.mark.parametrize(
        "msg",
        [
            "Search operation failed unexpectedly",
            "No search results returned",
            "Search engine encountered an error",
            "The search is longer than 256 characters",
            "Failed to create search engine instance",
            "search engine 'xyz' could not be found",
            "GitHub API error 403 forbidden",
        ],
        ids=lambda m: m[:40],
    )
    def test_search_patterns(self, reporter, msg):
        assert reporter.categorize_error(msg) == ErrorCategory.SEARCH_ERROR


class TestCategorizeErrorSynthesis:
    """Synthesis-error pattern coverage."""

    @pytest.mark.parametrize(
        "msg",
        [
            "Error during synthesis of report",
            "Failed to generate summary",
            "Error in synthesis of the final report",
            "detailed report stuck at 95 percent",
            "report taking too long to finish",
            "progress 100 stuck and not completing",
        ],
        ids=lambda m: m[:40],
    )
    def test_synthesis_patterns(self, reporter, msg):
        assert reporter.categorize_error(msg) == ErrorCategory.SYNTHESIS_ERROR


class TestCategorizeErrorFile:
    """File-error pattern coverage."""

    @pytest.mark.parametrize(
        "msg",
        [
            "Permission denied: /var/data/output.txt",
            "File report.pdf not found on disk",
            "Cannot write to file /tmp/out.csv",
            "Disk space full, unable to save",
            "No module named local_deep_research.core",
            "File config.yaml not found in expected location",
            "Attempt to write readonly database",
            "database is locked, try again",
            "database table is locked",
        ],
        ids=lambda m: m[:40],
    )
    def test_file_patterns(self, reporter, msg):
        assert reporter.categorize_error(msg) == ErrorCategory.FILE_ERROR


class TestCategorizeErrorUnknown:
    """Unknown / fallback behaviour."""

    def test_random_message_returns_unknown(self, reporter):
        assert (
            reporter.categorize_error("xyzzy gibberish 42")
            == ErrorCategory.UNKNOWN_ERROR
        )

    def test_empty_string_returns_unknown(self, reporter):
        assert reporter.categorize_error("") == ErrorCategory.UNKNOWN_ERROR

    def test_non_string_input_coerced(self, reporter):
        """categorize_error calls str() on its input, so non-strings work."""
        result = reporter.categorize_error(12345)
        assert result == ErrorCategory.UNKNOWN_ERROR

    def test_case_insensitive(self, reporter):
        """All upper-case should still match."""
        assert (
            reporter.categorize_error("CONNECTION REFUSED")
            == ErrorCategory.CONNECTION_ERROR
        )


# ===========================================================================
# ErrorReporter.get_user_friendly_title
# ===========================================================================


class TestGetUserFriendlyTitle:
    """Every ErrorCategory should map to a known non-empty title."""

    EXPECTED_TITLES = {
        ErrorCategory.CONNECTION_ERROR: "Connection Issue",
        ErrorCategory.MODEL_ERROR: "LLM Service Error",
        ErrorCategory.SEARCH_ERROR: "Search Service Error",
        ErrorCategory.SYNTHESIS_ERROR: "Report Generation Error",
        ErrorCategory.FILE_ERROR: "File System Error",
        ErrorCategory.RATE_LIMIT_ERROR: "API Rate Limit Exceeded",
        ErrorCategory.UNKNOWN_ERROR: "Unexpected Error",
    }

    @pytest.mark.parametrize(
        "category,expected",
        list(EXPECTED_TITLES.items()),
        ids=lambda c: c.name if isinstance(c, ErrorCategory) else c,
    )
    def test_title_per_category(self, reporter, category, expected):
        assert reporter.get_user_friendly_title(category) == expected


# ===========================================================================
# ErrorReporter.get_suggested_actions
# ===========================================================================


class TestGetSuggestedActions:
    """Every ErrorCategory should yield a non-empty list of strings."""

    @pytest.mark.parametrize(
        "category", list(ErrorCategory), ids=lambda c: c.name
    )
    def test_suggestions_non_empty(self, reporter, category):
        actions = reporter.get_suggested_actions(category)
        assert isinstance(actions, list)
        assert len(actions) > 0
        assert all(isinstance(a, str) for a in actions)


# ===========================================================================
# ErrorReporter.analyze_error
# ===========================================================================


class TestAnalyzeError:
    """analyze_error should return a complete analysis dict."""

    def test_basic_keys_present(self, reporter):
        result = reporter.analyze_error("Connection refused")
        for key in (
            "category",
            "title",
            "original_error",
            "suggestions",
            "severity",
            "recoverable",
        ):
            assert key in result

    def test_category_matches_categorize_error(self, reporter):
        msg = "429 too many requests"
        result = reporter.analyze_error(msg)
        assert result["category"] == ErrorCategory.RATE_LIMIT_ERROR

    def test_original_error_preserved(self, reporter):
        msg = "unique error 9823749"
        result = reporter.analyze_error(msg)
        assert result["original_error"] == msg

    def test_context_included_when_provided(self, reporter):
        ctx = {"query": "test", "findings": ["f1"]}
        result = reporter.analyze_error("Connection refused", context=ctx)
        assert result["context"] is ctx

    def test_has_partial_results_true(self, reporter):
        ctx = {"findings": ["f1"]}
        result = reporter.analyze_error("some error", context=ctx)
        assert result["has_partial_results"] is True

    def test_has_partial_results_false_empty_context(self, reporter):
        result = reporter.analyze_error("some error", context={})
        assert result.get("has_partial_results", False) is False

    def test_no_context_key_when_none(self, reporter):
        result = reporter.analyze_error("some error")
        assert "context" not in result


# ===========================================================================
# ErrorReporter._determine_severity
# ===========================================================================


class TestDetermineSeverity:
    """Each category maps to a fixed severity string."""

    EXPECTED = {
        ErrorCategory.CONNECTION_ERROR: "high",
        ErrorCategory.MODEL_ERROR: "high",
        ErrorCategory.UNKNOWN_ERROR: "high",
        ErrorCategory.SEARCH_ERROR: "medium",
        ErrorCategory.FILE_ERROR: "medium",
        ErrorCategory.RATE_LIMIT_ERROR: "medium",
        ErrorCategory.SYNTHESIS_ERROR: "low",
    }

    @pytest.mark.parametrize(
        "category,expected",
        list(EXPECTED.items()),
        ids=lambda c: c.name if isinstance(c, ErrorCategory) else c,
    )
    def test_severity_per_category(self, reporter, category, expected):
        assert reporter._determine_severity(category) == expected


# ===========================================================================
# ErrorReporter._is_recoverable
# ===========================================================================


class TestIsRecoverable:
    """All categories are recoverable except UNKNOWN_ERROR."""

    @pytest.mark.parametrize(
        "category",
        [c for c in ErrorCategory if c != ErrorCategory.UNKNOWN_ERROR],
        ids=lambda c: c.name,
    )
    def test_recoverable_categories(self, reporter, category):
        assert reporter._is_recoverable(category) is True

    def test_unknown_not_recoverable(self, reporter):
        assert reporter._is_recoverable(ErrorCategory.UNKNOWN_ERROR) is False


# ===========================================================================
# ErrorReporter._extract_service_name
# ===========================================================================


class TestExtractServiceName:
    """Service-name extraction from error text."""

    @pytest.mark.parametrize(
        "msg,expected",
        [
            ("OpenAI API returned 500", "Openai"),
            ("anthropic rate limit hit", "Anthropic"),
            ("google gemini auth failure", "Google"),
            ("Ollama connection refused on port 11434", "Ollama"),
            ("SearXNG instance unreachable", "Searxng"),
            ("Tavily search API error", "Tavily"),
            ("Brave search returned empty results", "Brave"),
            ("Something went wrong with the API", "API Service"),
        ],
        ids=[
            "openai",
            "anthropic",
            "google",
            "ollama",
            "searxng",
            "tavily",
            "brave",
            "unknown",
        ],
    )
    def test_service_extraction(self, reporter, msg, expected):
        assert reporter._extract_service_name(msg) == expected


# ===========================================================================
# safe_close (resource_utils)
# ===========================================================================


class _FakeResource:
    """Helper that records whether close() was called."""

    def __init__(self, *, raise_on_close=False):
        self.closed = False
        self._raise = raise_on_close

    def close(self):
        if self._raise:
            raise RuntimeError("boom")
        self.closed = True


class TestSafeClose:
    """Tests for local_deep_research.utilities.resource_utils.safe_close."""

    def test_calls_close_on_resource(self):
        r = _FakeResource()
        safe_close(r, "test")
        assert r.closed is True

    def test_none_resource_default_does_not_raise(self):
        """allow_none=False (default) logs but does not raise."""
        safe_close(None, "test")  # should not raise

    def test_none_resource_allow_none_silent(self):
        """allow_none=True should silently skip None."""
        safe_close(None, "test", allow_none=True)  # should not raise

    def test_no_close_method_default_does_not_raise(self):
        """Object without close() and closing_optional=False logs warning."""
        safe_close("a plain string", "test")  # should not raise

    def test_no_close_method_closing_optional_silent(self):
        """closing_optional=True should silently skip objects without close()."""
        safe_close(42, "test", closing_optional=True)  # should not raise

    def test_exception_during_close_suppressed(self):
        """If close() raises, safe_close must swallow it."""
        r = _FakeResource(raise_on_close=True)
        safe_close(r, "test")  # should not raise

    def test_both_flags_true_with_none(self):
        safe_close(None, "test", allow_none=True, closing_optional=True)

    def test_both_flags_true_without_close(self):
        safe_close(
            {"no": "close"}, "test", allow_none=True, closing_optional=True
        )
