"""
Coverage tests for JournalReputationFilter.

Tests cover missing branches in:
- __init__: SearXNG engine None or is_available=False raises JournalFilterError
- close(): closes engine and LLM when owns_llm=True
- create_default(): disabled by settings returns None; JournalFilterError caught returns None
- __check_result: no journal_ref with exclude_non_published=True/False
- __check_result: db hit within reanalysis period returns cached decision
- __check_result: LLM ValueError in analyze -> returns True (accept by default)
- filter_results: exception in filter returns original results unchanged
"""

from datetime import timedelta
from unittest.mock import MagicMock, patch

import pytest

MODULE = "local_deep_research.advanced_search_system.filters.journal_reputation_filter"


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_filter(
    engine_available=True,
    threshold=5,
    max_context=3000,
    exclude_non_published=False,
    reanalysis_days=365,
):
    """
    Construct a JournalReputationFilter bypassing real SearXNG and LLM.
    """
    from local_deep_research.advanced_search_system.filters.journal_reputation_filter import (
        JournalReputationFilter,
    )

    mock_model = MagicMock()
    mock_engine = MagicMock()
    mock_engine.is_available = engine_available

    with (
        patch(f"{MODULE}.create_search_engine", return_value=mock_engine),
        patch(f"{MODULE}.get_llm", return_value=mock_model),
    ):
        filt = JournalReputationFilter(
            model=mock_model,
            reliability_threshold=threshold,
            max_context=max_context,
            exclude_non_published=exclude_non_published,
            quality_reanalysis_period=timedelta(days=reanalysis_days),
        )

    return filt, mock_model, mock_engine


# ---------------------------------------------------------------------------
# __init__ error paths
# ---------------------------------------------------------------------------


class TestInitErrors:
    """JournalFilterError raised when SearXNG is unavailable."""

    def test_none_engine_raises(self):
        from local_deep_research.advanced_search_system.filters.journal_reputation_filter import (
            JournalFilterError,
            JournalReputationFilter,
        )

        mock_model = MagicMock()
        with patch(f"{MODULE}.create_search_engine", return_value=None):
            with pytest.raises(JournalFilterError):
                JournalReputationFilter(
                    model=mock_model,
                    reliability_threshold=5,
                    max_context=3000,
                    exclude_non_published=False,
                    quality_reanalysis_period=timedelta(days=365),
                )

    def test_engine_not_available_raises(self):
        from local_deep_research.advanced_search_system.filters.journal_reputation_filter import (
            JournalFilterError,
            JournalReputationFilter,
        )

        mock_model = MagicMock()
        mock_engine = MagicMock()
        mock_engine.is_available = False
        with patch(f"{MODULE}.create_search_engine", return_value=mock_engine):
            with pytest.raises(JournalFilterError):
                JournalReputationFilter(
                    model=mock_model,
                    reliability_threshold=5,
                    max_context=3000,
                    exclude_non_published=False,
                    quality_reanalysis_period=timedelta(days=365),
                )


# ---------------------------------------------------------------------------
# close()
# ---------------------------------------------------------------------------


class TestClose:
    """close() cleans up engine and optionally LLM."""

    def test_close_calls_safe_close_on_engine(self):
        filt, mock_model, mock_engine = _make_filter()

        with patch(f"{MODULE}.safe_close") as mock_sc:
            filt.close()

        mock_sc.assert_any_call(mock_engine, "SearXNG engine")

    def test_close_does_not_close_externally_supplied_llm(self):
        """When model was passed in (not owned), LLM is not closed."""
        filt, mock_model, _ = _make_filter()
        # _owns_llm is False when model is passed in
        filt._owns_llm = False

        with patch(f"{MODULE}.safe_close") as mock_sc:
            filt.close()

        # Ensure LLM safe_close was not called
        for call in mock_sc.call_args_list:
            assert "LLM" not in str(call)


# ---------------------------------------------------------------------------
# create_default()
# ---------------------------------------------------------------------------


class TestCreateDefault:
    """create_default class method."""

    def test_returns_none_when_disabled_in_settings(self):
        from local_deep_research.advanced_search_system.filters.journal_reputation_filter import (
            JournalReputationFilter,
        )

        with patch(
            "local_deep_research.config.search_config.get_setting_from_snapshot",
            return_value=False,
        ):
            result = JournalReputationFilter.create_default(
                engine_name="arxiv", settings_snapshot={}
            )

        assert result is None

    def test_returns_none_on_journal_filter_error(self):
        from local_deep_research.advanced_search_system.filters.journal_reputation_filter import (
            JournalFilterError,
            JournalReputationFilter,
        )

        with (
            patch(
                "local_deep_research.config.search_config.get_setting_from_snapshot",
                return_value=True,
            ),
            patch.object(
                JournalReputationFilter,
                "__init__",
                side_effect=JournalFilterError("SearXNG not available"),
            ),
        ):
            result = JournalReputationFilter.create_default(
                engine_name="arxiv", settings_snapshot={}
            )

        assert result is None


# ---------------------------------------------------------------------------
# __check_result paths
# ---------------------------------------------------------------------------


class TestCheckResultNullJournal:
    """Behaviour when result has no journal_ref."""

    def test_no_journal_ref_exclude_true_returns_false(self):
        """exclude_non_published=True -> result with no journal is excluded."""
        filt, _, _ = _make_filter(exclude_non_published=True)

        # Access private method via name mangling
        result = filt._JournalReputationFilter__check_result({"title": "paper"})
        assert result is False

    def test_no_journal_ref_exclude_false_returns_true(self):
        """exclude_non_published=False -> result with no journal is kept."""
        filt, _, _ = _make_filter(exclude_non_published=False)

        result = filt._JournalReputationFilter__check_result({"title": "paper"})
        assert result is True


class TestCheckResultCachedDb:
    """Behaviour when journal is found in the database."""

    def test_cached_quality_above_threshold_returns_true(self):
        """DB hit with quality >= threshold within reanalysis period -> True."""
        filt, mock_model, _ = _make_filter(threshold=5)

        # Fake journal DB record
        import time

        fake_journal = MagicMock()
        fake_journal.quality = 8
        fake_journal.quality_analysis_time = int(time.time())  # fresh

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_session)
        mock_session.__exit__ = MagicMock(return_value=False)
        mock_session.query.return_value.filter_by.return_value.first.return_value = fake_journal

        # Mock clean_journal_name to return predictable name
        with (
            patch.object(
                filt,
                "_JournalReputationFilter__clean_journal_name",
                return_value="Nature",
            ),
            patch.object(
                filt,
                "_JournalReputationFilter__db_session",
                return_value=mock_session,
            ),
        ):
            result = filt._JournalReputationFilter__check_result(
                {"journal_ref": "Nature (2023)"}
            )

        assert result is True

    def test_cached_quality_below_threshold_returns_false(self):
        """DB hit with quality < threshold within reanalysis period -> False."""
        filt, _, _ = _make_filter(threshold=6)

        import time

        fake_journal = MagicMock()
        fake_journal.quality = 3  # below threshold
        fake_journal.quality_analysis_time = int(time.time())

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_session)
        mock_session.__exit__ = MagicMock(return_value=False)
        mock_session.query.return_value.filter_by.return_value.first.return_value = fake_journal

        with (
            patch.object(
                filt,
                "_JournalReputationFilter__clean_journal_name",
                return_value="Predatory Journal",
            ),
            patch.object(
                filt,
                "_JournalReputationFilter__db_session",
                return_value=mock_session,
            ),
        ):
            result = filt._JournalReputationFilter__check_result(
                {"journal_ref": "Predatory Journal Vol 1"}
            )

        assert result is False


class TestCheckResultValueError:
    """ValueError from analyze_journal_reputation -> accept by default."""

    def test_value_error_returns_true(self):
        """If LLM response cannot be parsed, result is accepted (True)."""
        filt, _, _ = _make_filter(threshold=5)

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_session)
        mock_session.__exit__ = MagicMock(return_value=False)
        # No DB record found
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        with (
            patch.object(
                filt,
                "_JournalReputationFilter__clean_journal_name",
                return_value="Unknown Journal",
            ),
            patch.object(
                filt,
                "_JournalReputationFilter__db_session",
                return_value=mock_session,
            ),
            patch.object(
                filt,
                "_JournalReputationFilter__analyze_journal_reputation",
                side_effect=ValueError("bad parse"),
            ),
        ):
            result = filt._JournalReputationFilter__check_result(
                {"journal_ref": "Unknown Journal 2022"}
            )

        assert result is True


# ---------------------------------------------------------------------------
# filter_results exception path
# ---------------------------------------------------------------------------


class TestFilterResultsException:
    """filter_results returns original results on unexpected exception."""

    def test_exception_returns_original_results(self):
        filt, _, _ = _make_filter()
        results = [{"title": "A"}, {"title": "B"}]

        with patch.object(
            filt,
            "_JournalReputationFilter__check_result",
            side_effect=RuntimeError("unexpected"),
        ):
            output = filt.filter_results(results, "query")

        assert output == results
