"""
Coverage tests for GitHubSearchEngine focusing on untested logic paths.

Covers edge cases in:
- _optimize_github_query: LLM returns None, raises exception, string response, empty query
- _search_github: sort params per search type, empty results tips, exception
- _get_previews: contribution query with language, keyword extraction, search type restore,
  code/issues/users/unknown type dispatch
- _get_full_content: snippets-only mode, issue content, user bio inclusion/exclusion,
  missing repo full_name
- _handle_rate_limits: zero remaining sleeps, remaining >= 5 no warning
- _get_readme_content: non-base64 encoding, exception
- _get_recent_issues: exception returns empty
- _get_file_content: non-base64, failure status, exception
- search_repository: with readme+issues, exception
- search_code: user filter, search type restore, full content retrieval
- search_issues: API error, search type restore
- _filter_for_relevance: no LLM, empty previews, ranking, max results, invalid JSON, exception
- close: owned vs non-owned LLM
"""

import time
from unittest.mock import Mock, patch

import pytest

from local_deep_research.web_search_engines.engines.search_engine_github import (
    GitHubSearchEngine,
)


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

MODULE = "local_deep_research.web_search_engines.engines.search_engine_github"


def _make_engine(**kwargs):
    """Create a GitHubSearchEngine with sensible test defaults."""
    defaults = {"api_key": "test-token", "max_results": 10}
    defaults.update(kwargs)
    return GitHubSearchEngine(**defaults)


def _mock_llm(content="optimized query"):
    """Return a mock LLM whose invoke() returns a response with .content."""
    llm = Mock()
    llm.invoke.return_value = Mock(content=content)
    return llm


def _mock_response(status_code=200, json_data=None, headers=None, text=""):
    """Build a mock HTTP response."""
    resp = Mock()
    resp.status_code = status_code
    resp.headers = headers or {
        "X-RateLimit-Remaining": "60",
        "X-RateLimit-Reset": "0",
    }
    resp.json.return_value = json_data or {}
    resp.text = text
    return resp


# ---------------------------------------------------------------------------
# _optimize_github_query
# ---------------------------------------------------------------------------


class TestOptimizeGithubQueryCoverage:
    """Edge cases for _optimize_github_query."""

    def test_llm_returns_none_from_config(self):
        """When no LLM is set and config returns None, return original query."""
        engine = _make_engine(llm=None)
        engine.llm = None
        with patch(f"{MODULE}.llm_config.get_llm", return_value=None):
            result = engine._optimize_github_query("my query")
        assert result == "my query"

    def test_llm_config_raises_exception(self):
        """When llm_config.get_llm raises, return original query."""
        engine = _make_engine(llm=None)
        engine.llm = None
        with patch(
            f"{MODULE}.llm_config.get_llm", side_effect=RuntimeError("fail")
        ):
            result = engine._optimize_github_query("my query")
        assert result == "my query"

    def test_llm_invoke_returns_string_without_content_attr(self):
        """LLM returns a plain string (no .content attribute)."""
        llm = Mock()
        llm.invoke.return_value = "plain string response"
        engine = _make_engine(llm=llm)
        result = engine._optimize_github_query("my query")
        assert result == "plain string response"

    def test_llm_invoke_returns_empty_content(self):
        """LLM returns empty content, should fall back to original."""
        llm = _mock_llm(content="")
        engine = _make_engine(llm=llm)
        result = engine._optimize_github_query("my query")
        assert result == "my query"

    def test_llm_invoke_raises_exception(self):
        """LLM invoke raises exception, should return original query."""
        llm = Mock()
        llm.invoke.side_effect = RuntimeError("LLM exploded")
        engine = _make_engine(llm=llm)
        result = engine._optimize_github_query("my query")
        assert result == "my query"

    def test_llm_config_sets_owns_llm_flag(self):
        """When LLM is loaded from config, _owns_llm should be True."""
        mock_llm = _mock_llm("optimized")
        engine = _make_engine(llm=None)
        engine.llm = None
        with patch(f"{MODULE}.llm_config.get_llm", return_value=mock_llm):
            engine._optimize_github_query("test")
        assert engine._owns_llm is True


# ---------------------------------------------------------------------------
# _search_github
# ---------------------------------------------------------------------------


class TestSearchGithubCoverage:
    """Edge cases for _search_github."""

    def test_sort_params_code(self):
        """Code search type uses 'indexed' sort."""
        engine = _make_engine(search_type="code", llm=_mock_llm("q"))
        resp = _mock_response(
            json_data={"total_count": 0, "items": []},
            headers={"X-RateLimit-Remaining": "60", "X-RateLimit-Reset": "0"},
        )
        with patch(f"{MODULE}.safe_get", return_value=resp) as mock_get:
            engine._search_github("test")
            call_kwargs = mock_get.call_args
            params = call_kwargs.kwargs.get("params") or call_kwargs[1].get(
                "params"
            )
            assert params["sort"] == "indexed"

    def test_sort_params_issues(self):
        """Issues search type uses 'updated' sort."""
        engine = _make_engine(search_type="issues", llm=_mock_llm("q"))
        resp = _mock_response(
            json_data={"total_count": 0, "items": []},
            headers={"X-RateLimit-Remaining": "60", "X-RateLimit-Reset": "0"},
        )
        with patch(f"{MODULE}.safe_get", return_value=resp) as mock_get:
            engine._search_github("test")
            call_kwargs = mock_get.call_args
            params = call_kwargs.kwargs.get("params") or call_kwargs[1].get(
                "params"
            )
            assert params["sort"] == "updated"

    def test_sort_params_users(self):
        """Users search type uses 'followers' sort."""
        engine = _make_engine(search_type="users", llm=_mock_llm("q"))
        resp = _mock_response(
            json_data={"total_count": 0, "items": []},
            headers={"X-RateLimit-Remaining": "60", "X-RateLimit-Reset": "0"},
        )
        with patch(f"{MODULE}.safe_get", return_value=resp) as mock_get:
            engine._search_github("test")
            call_kwargs = mock_get.call_args
            params = call_kwargs.kwargs.get("params") or call_kwargs[1].get(
                "params"
            )
            assert params["sort"] == "followers"

    def test_empty_results_logs_tips(self):
        """When API returns 200 with empty items, tips are logged."""
        engine = _make_engine(llm=_mock_llm("q"))
        resp = _mock_response(
            json_data={"total_count": 0, "items": []},
            headers={"X-RateLimit-Remaining": "60", "X-RateLimit-Reset": "0"},
        )
        with patch(f"{MODULE}.safe_get", return_value=resp):
            results = engine._search_github("test")
        assert results == []

    def test_exception_returns_empty(self):
        """When safe_get raises, return empty list."""
        engine = _make_engine(llm=_mock_llm("q"))
        with patch(
            f"{MODULE}.safe_get", side_effect=ConnectionError("net down")
        ):
            results = engine._search_github("test")
        assert results == []


# ---------------------------------------------------------------------------
# _get_previews
# ---------------------------------------------------------------------------


class TestGetPreviewsCoverage:
    """Edge cases for _get_previews."""

    def test_contribution_query_with_language(self):
        """Contribution query extracts language from query text."""
        engine = _make_engine(search_type="repositories")
        mock_results = [
            {
                "id": 1,
                "full_name": "org/proj",
                "html_url": "https://github.com/org/proj",
                "description": "Python project",
                "stargazers_count": 500,
                "owner": {"login": "org"},
            }
        ]
        with patch.object(
            engine, "_search_github", return_value=mock_results
        ) as mock_search:
            engine._get_previews("contribute to python projects")
            # Should have been called with a specialized query containing language:python
            call_arg = mock_search.call_args[0][0]
            assert "language:python" in call_arg

    def test_contribution_query_extracts_keywords(self):
        """Contribution query extracts domain keywords (>3 chars, not stopwords)."""
        engine = _make_engine(search_type="code")
        with patch.object(
            engine, "_search_github", return_value=[]
        ) as mock_search:
            engine._get_previews("contribute to machine learning tools")
            call_arg = mock_search.call_args[0][0]
            assert "machine" in call_arg
            assert "learning" in call_arg
            assert "tools" in call_arg

    def test_contribution_query_restores_search_type(self):
        """After contribution query, search type is restored to original."""
        engine = _make_engine(search_type="code")
        with patch.object(engine, "_search_github", return_value=[]):
            engine._get_previews("contribute to open source")
        assert engine.search_type == "code"
        assert "code" in engine.search_endpoint

    def test_code_type_dispatch(self):
        """Code search type dispatches to _format_code_preview."""
        engine = _make_engine(search_type="code")
        mock_results = [
            {
                "sha": "abc",
                "name": "test.py",
                "path": "src/test.py",
                "html_url": "https://github.com/o/r/blob/main/src/test.py",
                "url": "https://api.github.com/repos/o/r/contents/src/test.py",
                "repository": {
                    "full_name": "o/r",
                    "html_url": "https://github.com/o/r",
                },
            }
        ]
        with patch.object(engine, "_search_github", return_value=mock_results):
            previews = engine._get_previews("test query")
        assert len(previews) == 1
        assert previews[0]["search_type"] == "code"

    def test_issues_type_dispatch(self):
        """Issues search type dispatches to _format_issue_preview."""
        engine = _make_engine(search_type="issues")
        mock_results = [
            {
                "number": 10,
                "title": "Bug",
                "html_url": "https://github.com/o/r/issues/10",
                "body": "A bug",
                "state": "open",
                "user": {"login": "u"},
                "repository": {"full_name": "o/r"},
            }
        ]
        with patch.object(engine, "_search_github", return_value=mock_results):
            previews = engine._get_previews("bug fix")
        assert len(previews) == 1
        assert previews[0]["search_type"] == "issue"

    def test_users_type_dispatch(self):
        """Users search type dispatches to _format_user_preview."""
        engine = _make_engine(search_type="users")
        mock_results = [
            {
                "id": 99,
                "login": "dev",
                "html_url": "https://github.com/dev",
                "bio": "Coder",
            }
        ]
        with patch.object(engine, "_search_github", return_value=mock_results):
            previews = engine._get_previews("developer")
        assert len(previews) == 1
        assert previews[0]["search_type"] == "user"

    def test_unknown_type_skips_result(self):
        """Unknown search type raises ValueError in __init__."""
        with pytest.raises(ValueError, match="Invalid GitHub search_type"):
            _make_engine(search_type="unknown_type")


# ---------------------------------------------------------------------------
# _get_full_content
# ---------------------------------------------------------------------------


class TestGetFullContentCoverage:
    """Edge cases for _get_full_content."""

    def test_issue_content_uses_snippet(self):
        """Issue search type sets full_content to snippet."""
        engine = _make_engine()
        items = [
            {
                "id": "issue_1",
                "search_type": "issue",
                "snippet": "Bug description here",
            }
        ]
        result = engine._get_full_content(items)
        assert result[0]["full_content"] == "Bug description here"
        assert result[0]["content_type"] == "issue"

    def test_user_bio_included(self):
        """User with bio gets bio in full_content."""
        engine = _make_engine()
        items = [
            {
                "id": "user_1",
                "title": "dev",
                "search_type": "user",
                "snippet": "Full-stack developer",
                "name": "Dev Person",
                "location": "NYC",
                "followers": 50,
                "public_repos": 20,
            }
        ]
        result = engine._get_full_content(items)
        assert "Bio: Full-stack developer" in result[0]["full_content"]
        assert "Name: Dev Person" in result[0]["full_content"]
        assert "Location: NYC" in result[0]["full_content"]

    def test_user_no_bio_excluded(self):
        """User with 'No bio provided' does not get Bio line."""
        engine = _make_engine()
        items = [
            {
                "id": "user_2",
                "title": "anon",
                "search_type": "user",
                "snippet": "No bio provided",
                "name": "",
                "location": "",
                "followers": 0,
                "public_repos": 0,
            }
        ]
        result = engine._get_full_content(items)
        assert "Bio:" not in result[0]["full_content"]

    def test_repository_missing_full_name(self):
        """Repository with empty repo_full_name skips readme fetch."""
        engine = _make_engine(include_readme=True)
        items = [{"id": "1", "search_type": "repository", "repo_full_name": ""}]
        with patch.object(engine, "_get_readme_content") as mock_readme:
            result = engine._get_full_content(items)
            mock_readme.assert_not_called()
        assert "full_content" not in result[0]


# ---------------------------------------------------------------------------
# _handle_rate_limits
# ---------------------------------------------------------------------------


class TestHandleRateLimitsCoverage:
    """Edge cases for _handle_rate_limits."""

    def test_zero_remaining_sleeps(self):
        """When remaining is 0 and reset is in the future, sleep is called."""
        engine = _make_engine()
        future_reset = int(time.time()) + 30
        resp = _mock_response(
            headers={
                "X-RateLimit-Remaining": "0",
                "X-RateLimit-Reset": str(future_reset),
            }
        )
        with patch(f"{MODULE}.time.sleep") as mock_sleep:
            engine._handle_rate_limits(resp)
            mock_sleep.assert_called_once()
            # Sleep should be capped at 60
            sleep_arg = mock_sleep.call_args[0][0]
            assert 0 < sleep_arg <= 60

    def test_remaining_gte_5_no_warning(self):
        """When remaining >= 5, no warning or sleep."""
        engine = _make_engine()
        resp = _mock_response(
            headers={
                "X-RateLimit-Remaining": "10",
                "X-RateLimit-Reset": "0",
            }
        )
        with patch(f"{MODULE}.time.sleep") as mock_sleep:
            engine._handle_rate_limits(resp)
            mock_sleep.assert_not_called()

    def test_low_remaining_but_not_zero_no_sleep(self):
        """When remaining is 1-4, warning is logged but no sleep."""
        engine = _make_engine()
        resp = _mock_response(
            headers={
                "X-RateLimit-Remaining": "3",
                "X-RateLimit-Reset": str(int(time.time()) + 30),
            }
        )
        with patch(f"{MODULE}.time.sleep") as mock_sleep:
            engine._handle_rate_limits(resp)
            mock_sleep.assert_not_called()


# ---------------------------------------------------------------------------
# _get_readme_content
# ---------------------------------------------------------------------------


class TestGetReadmeContentCoverage:
    """Edge cases for _get_readme_content."""

    def test_non_base64_encoding(self):
        """When encoding is not base64, return raw content."""
        engine = _make_engine()
        resp = _mock_response(
            json_data={"content": "raw text here", "encoding": "utf-8"},
            headers={"X-RateLimit-Remaining": "60", "X-RateLimit-Reset": "0"},
        )
        with patch(f"{MODULE}.safe_get", return_value=resp):
            content = engine._get_readme_content("o/r")
        assert content == "raw text here"

    def test_exception_returns_empty(self):
        """Exception during readme fetch returns empty string."""
        engine = _make_engine()
        with patch(f"{MODULE}.safe_get", side_effect=ConnectionError("fail")):
            content = engine._get_readme_content("o/r")
        assert content == ""


# ---------------------------------------------------------------------------
# _get_recent_issues
# ---------------------------------------------------------------------------


class TestGetRecentIssuesCoverage:
    """Edge cases for _get_recent_issues."""

    def test_exception_returns_empty(self):
        """Exception during issues fetch returns empty list."""
        engine = _make_engine()
        with patch(f"{MODULE}.safe_get", side_effect=ConnectionError("fail")):
            issues = engine._get_recent_issues("o/r")
        assert issues == []


# ---------------------------------------------------------------------------
# _get_file_content
# ---------------------------------------------------------------------------


class TestGetFileContentCoverage:
    """Edge cases for _get_file_content."""

    def test_non_base64_encoding(self):
        """When encoding is not base64, return raw content string."""
        engine = _make_engine()
        resp = _mock_response(
            json_data={"content": "print('hello')", "encoding": "utf-8"},
            headers={"X-RateLimit-Remaining": "60", "X-RateLimit-Reset": "0"},
        )
        with patch(f"{MODULE}.safe_get", return_value=resp):
            content = engine._get_file_content(
                "https://api.github.com/repos/o/r/contents/f.py"
            )
        assert content == "print('hello')"

    def test_failure_status_returns_empty(self):
        """Non-200 status returns empty string."""
        engine = _make_engine()
        resp = _mock_response(
            status_code=404,
            headers={"X-RateLimit-Remaining": "60", "X-RateLimit-Reset": "0"},
        )
        with patch(f"{MODULE}.safe_get", return_value=resp):
            content = engine._get_file_content(
                "https://api.github.com/repos/o/r/contents/f.py"
            )
        assert content == ""

    def test_exception_returns_empty(self):
        """Exception during file fetch returns empty string."""
        engine = _make_engine()
        with patch(f"{MODULE}.safe_get", side_effect=RuntimeError("boom")):
            content = engine._get_file_content(
                "https://api.github.com/repos/o/r/contents/f.py"
            )
        assert content == ""


# ---------------------------------------------------------------------------
# search_repository
# ---------------------------------------------------------------------------


class TestSearchRepositoryCoverage:
    """Edge cases for search_repository."""

    def test_with_readme_and_issues(self):
        """search_repository fetches both readme and issues when configured."""
        engine = _make_engine(include_readme=True, include_issues=True)
        repo_data = {
            "id": 1,
            "full_name": "org/proj",
            "html_url": "https://github.com/org/proj",
            "description": "desc",
            "stargazers_count": 200,
            "forks_count": 10,
            "language": "Python",
            "owner": {"login": "org"},
        }
        resp = _mock_response(
            json_data=repo_data,
            headers={"X-RateLimit-Remaining": "60", "X-RateLimit-Reset": "0"},
        )
        with patch(f"{MODULE}.safe_get", return_value=resp):
            with patch.object(
                engine, "_get_readme_content", return_value="# README"
            ) as mock_rm:
                with patch.object(
                    engine, "_get_recent_issues", return_value=[{"number": 1}]
                ) as mock_iss:
                    result = engine.search_repository("org", "proj")
                    mock_rm.assert_called_once_with("org/proj")
                    mock_iss.assert_called_once_with("org/proj")
        assert result["full_content"] == "# README"
        assert result["recent_issues"] == [{"number": 1}]

    def test_exception_returns_empty_dict(self):
        """Exception during repository fetch returns empty dict."""
        engine = _make_engine()
        with patch(f"{MODULE}.safe_get", side_effect=RuntimeError("fail")):
            result = engine.search_repository("org", "proj")
        assert result == {}


# ---------------------------------------------------------------------------
# search_code
# ---------------------------------------------------------------------------


class TestSearchCodeCoverage:
    """Edge cases for search_code."""

    def test_user_filter(self):
        """search_code appends user filter to query."""
        engine = _make_engine(llm=_mock_llm("q"))
        with patch.object(
            engine, "_search_github", return_value=[]
        ) as mock_search:
            engine.search_code("def main", user="octocat")
            call_arg = mock_search.call_args[0][0]
            assert "user:octocat" in call_arg

    def test_restores_search_type(self):
        """search_code restores original search type after completion."""
        engine = _make_engine(search_type="repositories")
        with patch.object(engine, "_search_github", return_value=[]):
            engine.search_code("test")
        assert engine.search_type == "repositories"
        assert "repositories" in engine.search_endpoint

    def test_restores_search_type_on_exception(self):
        """search_code restores search type even when _search_github raises."""
        engine = _make_engine(search_type="repositories")
        with patch.object(
            engine, "_search_github", side_effect=RuntimeError("fail")
        ):
            with pytest.raises(RuntimeError):
                engine.search_code("test")
        assert engine.search_type == "repositories"

    def test_full_content_retrieval(self):
        """search_code calls _get_full_content."""
        engine = _make_engine()
        mock_results = [
            {
                "sha": "x",
                "name": "f.py",
                "path": "f.py",
                "html_url": "https://github.com/o/r/blob/main/f.py",
                "url": "https://api.github.com/repos/o/r/contents/f.py",
                "repository": {
                    "full_name": "o/r",
                    "html_url": "https://github.com/o/r",
                },
            }
        ]
        with patch.object(engine, "_search_github", return_value=mock_results):
            with patch.object(
                engine,
                "_get_full_content",
                return_value=[{"full_content": "code"}],
            ) as mock_fc:
                engine.search_code("test")
                mock_fc.assert_called_once()


# ---------------------------------------------------------------------------
# search_issues
# ---------------------------------------------------------------------------


class TestSearchIssuesCoverage:
    """Edge cases for search_issues."""

    def test_api_error_returns_empty(self):
        """search_issues returns empty list on API error."""
        engine = _make_engine()
        resp = _mock_response(
            status_code=422,
            text="Validation Failed",
            headers={"X-RateLimit-Remaining": "60", "X-RateLimit-Reset": "0"},
        )
        with patch(f"{MODULE}.safe_get", return_value=resp):
            results = engine.search_issues("bug")
        assert results == []

    def test_restores_search_type(self):
        """search_issues restores original search type after completion."""
        engine = _make_engine(search_type="repositories")
        resp = _mock_response(
            json_data={"total_count": 0, "items": []},
            headers={"X-RateLimit-Remaining": "60", "X-RateLimit-Reset": "0"},
        )
        with patch(f"{MODULE}.safe_get", return_value=resp):
            engine.search_issues("bug")
        assert engine.search_type == "repositories"
        assert "repositories" in engine.search_endpoint


# ---------------------------------------------------------------------------
# _filter_for_relevance
# ---------------------------------------------------------------------------


class TestFilterForRelevanceCoverage:
    """Edge cases for _filter_for_relevance."""

    def test_no_llm_returns_previews_as_is(self):
        """Without LLM, return previews unchanged."""
        engine = _make_engine(llm=None)
        engine.llm = None
        previews = [{"id": "1"}, {"id": "2"}]
        result = engine._filter_for_relevance(previews, "query")
        assert result == previews

    def test_empty_previews_returns_empty(self):
        """Empty previews list returns empty."""
        engine = _make_engine(llm=_mock_llm())
        result = engine._filter_for_relevance([], "query")
        assert result == []

    def test_ranking_reorders_results(self):
        """LLM ranking reorders results by relevance indices."""
        llm = Mock()
        llm.invoke.return_value = Mock(content="[2, 0, 1]")
        engine = _make_engine(llm=llm)
        previews = [{"id": "a"}, {"id": "b"}, {"id": "c"}]
        with patch(f"{MODULE}.extract_json", return_value=[2, 0, 1]):
            with patch(
                f"{MODULE}.get_llm_response_text", return_value="[2, 0, 1]"
            ):
                result = engine._filter_for_relevance(previews, "query")
        assert [r["id"] for r in result] == ["c", "a", "b"]

    def test_max_results_limits_output(self):
        """max_filtered_results truncates ranked results."""
        llm = Mock()
        llm.invoke.return_value = Mock(content="[0, 1, 2, 3]")
        engine = _make_engine(llm=llm, max_filtered_results=2)
        previews = [{"id": "a"}, {"id": "b"}, {"id": "c"}, {"id": "d"}]
        with patch(f"{MODULE}.extract_json", return_value=[0, 1, 2, 3]):
            with patch(
                f"{MODULE}.get_llm_response_text", return_value="[0, 1, 2, 3]"
            ):
                result = engine._filter_for_relevance(previews, "query")
        assert len(result) == 2

    def test_invalid_json_returns_empty(self):
        """When extract_json returns None, return empty list."""
        llm = Mock()
        llm.invoke.return_value = Mock(content="not json")
        engine = _make_engine(llm=llm)
        previews = [{"id": "a"}]
        with patch(f"{MODULE}.extract_json", return_value=None):
            with patch(
                f"{MODULE}.get_llm_response_text", return_value="not json"
            ):
                result = engine._filter_for_relevance(previews, "query")
        assert result == []

    def test_exception_returns_empty(self):
        """Exception during filtering returns empty list."""
        llm = Mock()
        llm.invoke.side_effect = RuntimeError("LLM error")
        engine = _make_engine(llm=llm)
        previews = [{"id": "a"}]
        result = engine._filter_for_relevance(previews, "query")
        assert result == []

    def test_out_of_range_index_skipped(self):
        """Indices beyond preview length are silently skipped."""
        llm = Mock()
        llm.invoke.return_value = Mock(content="[0, 99, 1]")
        engine = _make_engine(llm=llm)
        previews = [{"id": "a"}, {"id": "b"}]
        with patch(f"{MODULE}.extract_json", return_value=[0, 99, 1]):
            with patch(
                f"{MODULE}.get_llm_response_text", return_value="[0, 99, 1]"
            ):
                result = engine._filter_for_relevance(previews, "query")
        assert [r["id"] for r in result] == ["a", "b"]

    def test_negative_index_rejected(self):
        """Negative indices must be rejected, not select the last preview."""
        llm = Mock()
        engine = _make_engine(llm=llm)
        previews = [{"id": "a"}, {"id": "b"}, {"id": "c"}]
        with patch(f"{MODULE}.extract_json", return_value=[-1, 0, 2]):
            with patch(
                f"{MODULE}.get_llm_response_text", return_value="[-1, 0, 2]"
            ):
                result = engine._filter_for_relevance(previews, "query")
        # -1 previously selected previews[-1] (the last preview); it must
        # now be dropped entirely.
        assert [r["id"] for r in result] == ["a", "c"]

    def test_duplicate_indices_deduplicated(self):
        """Repeated indices contribute one preview, preserving rank order."""
        llm = Mock()
        engine = _make_engine(llm=llm)
        previews = [{"id": "a"}, {"id": "b"}, {"id": "c"}]
        with patch(f"{MODULE}.extract_json", return_value=[2, 2, 0, 2, 1]):
            with patch(
                f"{MODULE}.get_llm_response_text",
                return_value="[2, 2, 0, 2, 1]",
            ):
                result = engine._filter_for_relevance(previews, "query")
        assert [r["id"] for r in result] == ["c", "a", "b"]

    def test_non_integer_indices_skipped(self):
        """Booleans, strings, and floats are skipped without raising."""
        llm = Mock()
        engine = _make_engine(llm=llm)
        previews = [{"id": "a"}, {"id": "b"}, {"id": "c"}]
        with patch(
            f"{MODULE}.extract_json", return_value=[0, "1", True, 1.0, 2]
        ):
            with patch(
                f"{MODULE}.get_llm_response_text",
                return_value='[0, "1", true, 1.0, 2]',
            ):
                result = engine._filter_for_relevance(previews, "query")
        # Only the genuine ints 0 and 2 survive; "1"/True/1.0 are dropped
        # rather than raising and nuking the whole result set.
        assert [r["id"] for r in result] == ["a", "c"]


# ---------------------------------------------------------------------------
# close
# ---------------------------------------------------------------------------


class TestCloseCoverage:
    """Edge cases for close method."""

    def test_close_owned_llm(self):
        """When engine owns the LLM, safe_close is called."""
        engine = _make_engine(llm=_mock_llm())
        engine._owns_llm = True
        with patch(
            f"{MODULE.rsplit('.', 1)[0]}.search_engine_github.GitHubSearchEngine.close"
        ):
            # Call the actual method, not the patched one
            pass
        # Use a direct approach
        with patch(
            "local_deep_research.utilities.resource_utils.safe_close"
        ) as mock_close:
            engine.close()
            mock_close.assert_called_once()

    def test_close_non_owned_llm(self):
        """When engine does not own the LLM, safe_close is not called."""
        engine = _make_engine(llm=_mock_llm())
        engine._owns_llm = False
        with patch(
            "local_deep_research.utilities.resource_utils.safe_close"
        ) as mock_close:
            engine.close()
            mock_close.assert_not_called()
