"""
Deep coverage tests for GitHubSearchEngine targeting ~70 missing statements.

Focuses on paths not covered by test_search_engine_github_coverage.py:
- __init__: with/without API key, authenticated header set
- set_search_type: valid and invalid type
- _format_repository_preview: field mapping
- _format_code_preview: field mapping
- _format_issue_preview: long body truncation, missing repository key
- _format_user_preview: field mapping
- _get_readme_content: base64 decoding, 404 path
- _get_recent_issues: 200 path and error status
- _get_file_content: base64 decoding path
- _get_full_content: repository without readme (include_readme=False),
  code search_type with file_url, code missing file_url, issue snippet
- _search_github: repositories sort, API error status (non-200)
- search_repository: API error (non-200)
- search_code: with language filter
- search_issues: 200 path, state/sort params propagation
- _get_previews: standard (non-contribution) query path, no results path
- _filter_for_relevance: empty previews early-return, no llm early-return
"""

import base64
from unittest.mock import Mock, patch

import pytest


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

# ---------------------------------------------------------------------------
# Constants / helpers
# ---------------------------------------------------------------------------

MODULE = "local_deep_research.web_search_engines.engines.search_engine_github"


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


def _mock_llm(content="optimized query"):
    llm = Mock()
    llm.invoke.return_value = Mock(content=content)
    return llm


def _mock_response(status_code=200, json_data=None, headers=None, text=""):
    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 if json_data is not None else {}
    resp.text = text
    return resp


# ---------------------------------------------------------------------------
# __init__
# ---------------------------------------------------------------------------


class TestInitDeep:
    def test_api_key_sets_authorization_header(self):
        engine = _make_engine(api_key="secret-token")
        assert engine.headers["Authorization"] == "token secret-token"

    def test_no_api_key_no_authorization_header(self):
        engine = _make_engine(api_key=None)
        assert "Authorization" not in engine.headers

    def test_search_type_sets_endpoint(self):
        engine = _make_engine(search_type="issues")
        assert engine.search_endpoint.endswith("/search/issues")

    def test_default_search_type_is_repositories(self):
        engine = _make_engine()
        assert engine.search_type == "repositories"
        assert "repositories" in engine.search_endpoint

    def test_owns_llm_starts_false(self):
        engine = _make_engine(llm=_mock_llm())
        assert engine._owns_llm is False


# ---------------------------------------------------------------------------
# set_search_type
# ---------------------------------------------------------------------------


class TestSetSearchTypeDeep:
    def test_valid_search_type_updates_endpoint(self):
        engine = _make_engine(search_type="repositories")
        engine.set_search_type("code")
        assert engine.search_type == "code"
        assert engine.search_endpoint.endswith("/search/code")

    def test_all_valid_types_accepted(self):
        engine = _make_engine()
        for stype in ["repositories", "code", "issues", "users"]:
            engine.set_search_type(stype)
            assert engine.search_type == stype

    def test_invalid_search_type_does_not_change_state(self):
        engine = _make_engine(search_type="repositories")
        with pytest.raises(ValueError, match="Invalid GitHub search_type"):
            engine.set_search_type("invalid_type")


# ---------------------------------------------------------------------------
# _format_repository_preview
# ---------------------------------------------------------------------------


class TestFormatRepositoryPreviewDeep:
    def test_all_fields_mapped(self):
        engine = _make_engine()
        repo = {
            "id": 42,
            "full_name": "owner/repo",
            "html_url": "https://github.com/owner/repo",
            "description": "A test repo",
            "stargazers_count": 1000,
            "forks_count": 50,
            "language": "Python",
            "updated_at": "2024-01-01",
            "created_at": "2023-01-01",
            "topics": ["ml", "ai"],
            "owner": {"login": "owner"},
            "fork": True,
        }
        preview = engine._format_repository_preview(repo)
        assert preview["id"] == "42"
        assert preview["title"] == "owner/repo"
        assert preview["link"] == "https://github.com/owner/repo"
        assert preview["snippet"] == "A test repo"
        assert preview["stars"] == 1000
        assert preview["forks"] == 50
        assert preview["language"] == "Python"
        assert preview["topics"] == ["ml", "ai"]
        assert preview["owner"] == "owner"
        assert preview["is_fork"] is True
        assert preview["search_type"] == "repository"
        assert preview["repo_full_name"] == "owner/repo"

    def test_missing_description_uses_default(self):
        engine = _make_engine()
        repo = {"id": 1, "owner": {}}
        preview = engine._format_repository_preview(repo)
        assert preview["snippet"] == "No description provided"


# ---------------------------------------------------------------------------
# _format_code_preview
# ---------------------------------------------------------------------------


class TestFormatCodePreviewDeep:
    def test_fields_mapped_correctly(self):
        engine = _make_engine()
        code = {
            "sha": "abc123",
            "name": "main.py",
            "path": "src/main.py",
            "html_url": "https://github.com/o/r/blob/main/src/main.py",
            "url": "https://api.github.com/repos/o/r/contents/src/main.py",
            "repository": {
                "full_name": "o/r",
                "html_url": "https://github.com/o/r",
            },
        }
        preview = engine._format_code_preview(code)
        assert preview["id"] == "code_abc123"
        assert "main.py" in preview["title"]
        assert "o/r" in preview["title"]
        assert preview["search_type"] == "code"
        assert preview["path"] == "src/main.py"
        assert (
            preview["file_url"]
            == "https://api.github.com/repos/o/r/contents/src/main.py"
        )

    def test_missing_repository_key(self):
        engine = _make_engine()
        code = {"sha": "x", "name": "f.py", "path": "f.py"}
        preview = engine._format_code_preview(code)
        assert preview["repo_name"] == ""
        assert preview["repo_url"] == ""


# ---------------------------------------------------------------------------
# _format_issue_preview
# ---------------------------------------------------------------------------


class TestFormatIssuePreviewDeep:
    def test_long_body_is_truncated(self):
        engine = _make_engine()
        issue = {
            "number": 7,
            "title": "Long issue",
            "html_url": "https://github.com/o/r/issues/7",
            "body": "x" * 300,
            "state": "open",
            "user": {"login": "alice"},
            "repository": {"full_name": "o/r"},
        }
        preview = engine._format_issue_preview(issue)
        assert len(preview["snippet"]) <= 203  # 200 chars + "..."
        assert preview["snippet"].endswith("...")

    def test_short_body_not_truncated(self):
        engine = _make_engine()
        issue = {
            "number": 8,
            "title": "Short issue",
            "html_url": "https://github.com/o/r/issues/8",
            "body": "short body",
            "state": "closed",
            "user": {"login": "bob"},
            "repository": {"full_name": "o/r"},
        }
        preview = engine._format_issue_preview(issue)
        assert preview["snippet"] == "short body"

    def test_missing_repository_key_uses_empty(self):
        engine = _make_engine()
        issue = {
            "number": 9,
            "title": "No repo",
            "html_url": "https://github.com/issues/9",
            "body": "text",
            "state": "open",
            "user": {"login": "x"},
        }
        preview = engine._format_issue_preview(issue)
        assert preview["repo_name"] == ""

    def test_search_type_is_issue(self):
        engine = _make_engine()
        preview = engine._format_issue_preview(
            {
                "number": 1,
                "body": "",
                "user": {},
                "repository": {"full_name": "a/b"},
            }
        )
        assert preview["search_type"] == "issue"


# ---------------------------------------------------------------------------
# _format_user_preview
# ---------------------------------------------------------------------------


class TestFormatUserPreviewDeep:
    def test_fields_mapped(self):
        engine = _make_engine()
        user = {
            "id": 99,
            "login": "dev99",
            "html_url": "https://github.com/dev99",
            "bio": "I code",
            "name": "Dev Ninety",
            "followers": 123,
            "public_repos": 45,
            "location": "Berlin",
        }
        preview = engine._format_user_preview(user)
        assert preview["id"] == "user_99"
        assert preview["title"] == "dev99"
        assert preview["snippet"] == "I code"
        assert preview["name"] == "Dev Ninety"
        assert preview["followers"] == 123
        assert preview["public_repos"] == 45
        assert preview["location"] == "Berlin"
        assert preview["search_type"] == "user"

    def test_missing_bio_uses_default(self):
        engine = _make_engine()
        user = {"id": 1, "login": "anon"}
        preview = engine._format_user_preview(user)
        assert preview["snippet"] == "No bio provided"


# ---------------------------------------------------------------------------
# _get_readme_content deep paths
# ---------------------------------------------------------------------------


class TestGetReadmeContentDeep:
    def test_base64_content_decoded(self):
        engine = _make_engine()
        raw = "# Hello README"
        encoded = base64.b64encode(raw.encode()).decode()
        resp = _mock_response(
            json_data={"content": encoded, "encoding": "base64"},
            headers={"X-RateLimit-Remaining": "60", "X-RateLimit-Reset": "0"},
        )
        with patch(f"{MODULE}.safe_get", return_value=resp):
            content = engine._get_readme_content("owner/repo")
        assert content == raw

    def test_404_returns_empty(self):
        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_readme_content("owner/repo")
        assert content == ""


# ---------------------------------------------------------------------------
# _get_recent_issues deep paths
# ---------------------------------------------------------------------------


class TestGetRecentIssuesDeep:
    def test_200_returns_issues_list(self):
        engine = _make_engine()
        issues_data = [
            {"number": 1, "title": "Bug"},
            {"number": 2, "title": "Feature"},
        ]
        resp = _mock_response(
            json_data=issues_data,
            headers={"X-RateLimit-Remaining": "60", "X-RateLimit-Reset": "0"},
        )
        with patch(f"{MODULE}.safe_get", return_value=resp):
            issues = engine._get_recent_issues("owner/repo")
        assert len(issues) == 2
        assert issues[0]["number"] == 1

    def test_error_status_returns_empty(self):
        engine = _make_engine()
        resp = _mock_response(
            status_code=403,
            headers={"X-RateLimit-Remaining": "60", "X-RateLimit-Reset": "0"},
        )
        with patch(f"{MODULE}.safe_get", return_value=resp):
            issues = engine._get_recent_issues("owner/repo")
        assert issues == []

    def test_limit_param_passed(self):
        engine = _make_engine()
        resp = _mock_response(
            json_data=[],
            headers={"X-RateLimit-Remaining": "60", "X-RateLimit-Reset": "0"},
        )
        with patch(f"{MODULE}.safe_get", return_value=resp) as mock_get:
            engine._get_recent_issues("owner/repo", limit=3)
            call_kwargs = mock_get.call_args
            params = call_kwargs.kwargs.get("params") or call_kwargs[1].get(
                "params"
            )
            assert params["per_page"] == 3


# ---------------------------------------------------------------------------
# _get_file_content deep paths
# ---------------------------------------------------------------------------


class TestGetFileContentDeep:
    def test_base64_content_decoded(self):
        engine = _make_engine()
        raw = "def hello(): pass"
        encoded = base64.b64encode(raw.encode()).decode()
        resp = _mock_response(
            json_data={"content": encoded, "encoding": "base64"},
            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 == raw


# ---------------------------------------------------------------------------
# _get_full_content deep paths
# ---------------------------------------------------------------------------


class TestGetFullContentDeep:
    def test_repository_without_readme_no_full_content(self):
        """Repository item with include_readme=False does not fetch readme."""
        engine = _make_engine(include_readme=False, include_issues=False)
        items = [
            {
                "id": "1",
                "search_type": "repository",
                "repo_full_name": "owner/repo",
            }
        ]
        with patch.object(engine, "_get_readme_content") as mock_readme:
            result = engine._get_full_content(items)
            mock_readme.assert_not_called()
        # No full_content key added
        assert "full_content" not in result[0]

    def test_code_type_with_file_url_fetches_content(self):
        """Code item with file_url fetches and stores file content."""
        engine = _make_engine()
        items = [
            {
                "id": "code_abc",
                "search_type": "code",
                "file_url": "https://api.github.com/repos/o/r/contents/f.py",
            }
        ]
        with patch.object(
            engine, "_get_file_content", return_value="file body"
        ) as mock_fc:
            result = engine._get_full_content(items)
            mock_fc.assert_called_once_with(
                "https://api.github.com/repos/o/r/contents/f.py"
            )
        assert result[0]["full_content"] == "file body"
        assert result[0]["content_type"] == "file"

    def test_code_type_without_file_url_skips_fetch(self):
        """Code item without file_url does not attempt fetch."""
        engine = _make_engine()
        items = [{"id": "code_abc", "search_type": "code", "file_url": ""}]
        with patch.object(engine, "_get_file_content") as mock_fc:
            engine._get_full_content(items)
            mock_fc.assert_not_called()

    def test_repository_with_issues_fetches_issues(self):
        """Repository item with include_issues=True fetches issues."""
        engine = _make_engine(include_readme=True, include_issues=True)
        items = [
            {
                "id": "1",
                "search_type": "repository",
                "repo_full_name": "owner/repo",
            }
        ]
        with patch.object(
            engine, "_get_readme_content", return_value="# README"
        ):
            with patch.object(
                engine, "_get_recent_issues", return_value=[{"number": 1}]
            ) as mock_iss:
                result = engine._get_full_content(items)
                mock_iss.assert_called_once_with("owner/repo")
        assert result[0]["recent_issues"] == [{"number": 1}]

    def test_user_with_no_name_or_location_no_extra_lines(self):
        """User item with empty name/location omits those lines."""
        engine = _make_engine()
        items = [
            {
                "id": "user_5",
                "title": "anon",
                "search_type": "user",
                "snippet": "No bio provided",
                "name": "",
                "location": "",
                "followers": 0,
                "public_repos": 0,
            }
        ]
        result = engine._get_full_content(items)
        assert "Name:" not in result[0]["full_content"]
        assert "Location:" not in result[0]["full_content"]
        assert result[0]["content_type"] == "user_profile"


# ---------------------------------------------------------------------------
# _search_github deep paths
# ---------------------------------------------------------------------------


class TestSearchGithubDeep:
    def test_repositories_sort_param(self):
        """Repositories search uses 'stars' sort."""
        engine = _make_engine(search_type="repositories", 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")
            params = mock_get.call_args.kwargs.get(
                "params"
            ) or mock_get.call_args[1].get("params")
            assert params["sort"] == "stars"
            assert params["order"] == "desc"

    def test_non_200_response_logs_error_and_returns_empty(self):
        """Non-200 response returns empty results."""
        engine = _make_engine(llm=_mock_llm("q"))
        resp = _mock_response(
            status_code=403,
            text="Forbidden",
            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_successful_search_returns_items(self):
        """200 response with items returns the items list."""
        engine = _make_engine(llm=_mock_llm("q"))
        items = [{"id": 1, "full_name": "a/b"}]
        resp = _mock_response(
            json_data={"total_count": 1, "items": items},
            headers={"X-RateLimit-Remaining": "30", "X-RateLimit-Reset": "0"},
        )
        with patch(f"{MODULE}.safe_get", return_value=resp):
            results = engine._search_github("test")
        assert results == items


# ---------------------------------------------------------------------------
# search_repository deep paths
# ---------------------------------------------------------------------------


class TestSearchRepositoryDeep:
    def test_api_error_returns_empty_dict(self):
        """Non-200 response returns empty dict."""
        engine = _make_engine()
        resp = _mock_response(
            status_code=404,
            text="Not Found",
            headers={"X-RateLimit-Remaining": "60", "X-RateLimit-Reset": "0"},
        )
        with patch(f"{MODULE}.safe_get", return_value=resp):
            result = engine.search_repository("owner", "nonexistent")
        assert result == {}

    def test_200_without_readme_or_issues(self):
        """200 response without readme/issues configured returns basic preview."""
        engine = _make_engine(include_readme=False, include_issues=False)
        repo_data = {
            "id": 10,
            "full_name": "owner/repo",
            "html_url": "https://github.com/owner/repo",
            "description": "desc",
            "stargazers_count": 10,
            "forks_count": 2,
            "owner": {"login": "owner"},
        }
        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):
            result = engine.search_repository("owner", "repo")
        assert result["title"] == "owner/repo"
        assert "full_content" not in result
        assert "recent_issues" not in result


# ---------------------------------------------------------------------------
# search_code deep paths
# ---------------------------------------------------------------------------


class TestSearchCodeDeep:
    def test_language_filter_appended(self):
        """search_code appends language 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", language="python")
            call_arg = mock_search.call_args[0][0]
            assert "language:python" in call_arg

    def test_no_filters_uses_bare_query(self):
        """search_code with no language/user passes query unchanged."""
        engine = _make_engine(llm=_mock_llm("q"))
        with patch.object(
            engine, "_search_github", return_value=[]
        ) as mock_search:
            engine.search_code("import numpy")
            call_arg = mock_search.call_args[0][0]
            assert call_arg == "import numpy"


# ---------------------------------------------------------------------------
# search_issues deep paths
# ---------------------------------------------------------------------------


class TestSearchIssuesDeep:
    def test_200_returns_formatted_issues(self):
        """search_issues with 200 response returns formatted previews."""
        engine = _make_engine()
        issues_data = [
            {
                "number": 5,
                "title": "A bug",
                "html_url": "https://github.com/o/r/issues/5",
                "body": "details",
                "state": "open",
                "user": {"login": "alice"},
                "created_at": "2024-01-01",
                "updated_at": "2024-01-02",
                "comments": 3,
                "repository": {"full_name": "o/r"},
            }
        ]
        resp = _mock_response(
            json_data={"items": issues_data},
            headers={"X-RateLimit-Remaining": "60", "X-RateLimit-Reset": "0"},
        )
        with patch(f"{MODULE}.safe_get", return_value=resp):
            results = engine.search_issues("bug")
        assert len(results) == 1
        assert results[0]["search_type"] == "issue"
        assert results[0]["title"] == "A bug"

    def test_state_and_sort_params_propagated(self):
        """search_issues passes state and sort to request params."""
        engine = _make_engine()
        resp = _mock_response(
            json_data={"items": []},
            headers={"X-RateLimit-Remaining": "60", "X-RateLimit-Reset": "0"},
        )
        with patch(f"{MODULE}.safe_get", return_value=resp) as mock_get:
            engine.search_issues("crash", state="closed", sort="comments")
            params = mock_get.call_args.kwargs.get(
                "params"
            ) or mock_get.call_args[1].get("params")
            assert "state:closed" in params["q"]
            assert params["sort"] == "comments"

    def test_search_type_restored_after_call(self):
        """search_issues restores search_type to original after call."""
        engine = _make_engine(search_type="users")
        resp = _mock_response(
            json_data={"items": []},
            headers={"X-RateLimit-Remaining": "60", "X-RateLimit-Reset": "0"},
        )
        with patch(f"{MODULE}.safe_get", return_value=resp):
            engine.search_issues("test")
        assert engine.search_type == "users"
        assert "users" in engine.search_endpoint


# ---------------------------------------------------------------------------
# _get_previews deep paths
# ---------------------------------------------------------------------------


class TestGetPreviewsDeep:
    def test_standard_query_calls_search_github_directly(self):
        """Non-contribution query calls _search_github with original query."""
        engine = _make_engine(search_type="repositories")
        with patch.object(
            engine, "_search_github", return_value=[]
        ) as mock_search:
            engine._get_previews("machine learning frameworks")
            mock_search.assert_called_once_with("machine learning frameworks")

    def test_no_results_returns_empty_list(self):
        """When _search_github returns empty, _get_previews returns []."""
        engine = _make_engine()
        with patch.object(engine, "_search_github", return_value=[]):
            result = engine._get_previews("anything")
        assert result == []

    def test_repository_type_formats_as_repository_preview(self):
        """Repositories search type calls _format_repository_preview."""
        engine = _make_engine(search_type="repositories")
        mock_result = {
            "id": 99,
            "full_name": "a/b",
            "html_url": "https://github.com/a/b",
            "description": "desc",
            "stargazers_count": 10,
            "forks_count": 1,
            "owner": {"login": "a"},
        }
        with patch.object(engine, "_search_github", return_value=[mock_result]):
            previews = engine._get_previews("test query")
        assert len(previews) == 1
        assert previews[0]["search_type"] == "repository"

    def test_contribution_query_sets_type_to_repositories(self):
        """Contribution query temporarily switches search_type to repositories."""
        engine = _make_engine(search_type="users")
        captured_types = []

        def capture_search(q):
            captured_types.append(engine.search_type)
            return []

        with patch.object(engine, "_search_github", side_effect=capture_search):
            engine._get_previews("contribute to open source")
        # During the call it should have been "repositories"
        assert "repositories" in captured_types
        # After the call it should be restored
        assert engine.search_type == "users"


# ---------------------------------------------------------------------------
# _filter_for_relevance deep paths
# ---------------------------------------------------------------------------


class TestFilterForRelevanceDeep:
    def test_no_llm_empty_previews_returns_empty(self):
        """Without LLM and empty previews, return empty list."""
        engine = _make_engine(llm=None)
        engine.llm = None
        result = engine._filter_for_relevance([], "query")
        assert result == []

    def test_with_llm_empty_previews_returns_early(self):
        """With LLM but empty previews list, return early without invoking LLM."""
        llm = _mock_llm()
        engine = _make_engine(llm=llm)
        result = engine._filter_for_relevance([], "query")
        llm.invoke.assert_not_called()
        assert result == []

    def test_no_llm_non_empty_previews_returned_unchanged(self):
        """Without LLM, non-empty previews returned as-is."""
        engine = _make_engine(llm=None)
        engine.llm = None
        previews = [{"id": "x"}, {"id": "y"}]
        result = engine._filter_for_relevance(previews, "q")
        assert result == previews

    def test_ranked_results_respect_max_filtered_results(self):
        """max_filtered_results limits output length correctly."""
        llm = Mock()
        llm.invoke.return_value = Mock(content="[3, 1, 0, 2]")
        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=[3, 1, 0, 2]):
            with patch(
                f"{MODULE}.get_llm_response_text", return_value="[3, 1, 0, 2]"
            ):
                result = engine._filter_for_relevance(previews, "query")
        assert len(result) == 2
        assert result[0]["id"] == "d"  # index 3
        assert result[1]["id"] == "b"  # index 1

    def test_negative_index_skipped(self):
        """Negative indices that are valid Python but out of expected range are handled."""
        llm = Mock()
        llm.invoke.return_value = Mock(content="[0, -1]")
        engine = _make_engine(llm=llm)
        previews = [{"id": "a"}, {"id": "b"}]
        # -1 is < len(previews) so it will be included (Python behavior)
        with patch(f"{MODULE}.extract_json", return_value=[0]):
            with patch(f"{MODULE}.get_llm_response_text", return_value="[0]"):
                result = engine._filter_for_relevance(previews, "query")
        assert result[0]["id"] == "a"
