"""Tests for source adapters — GitHub and Hacker News."""

from __future__ import annotations

from unittest.mock import MagicMock, patch

from priorart.sources.github import search_repos
from priorart.sources.hn import search_discussions


def make_gh_response(items: list[dict]) -> MagicMock:
    mock = MagicMock()
    mock.json.return_value = {"items": items}
    mock.raise_for_status.return_value = None
    return mock


def make_hn_response(hits: list[dict]) -> MagicMock:
    mock = MagicMock()
    mock.json.return_value = {"hits": hits}
    mock.raise_for_status.return_value = None
    return mock


SAMPLE_GH_ITEM = {
    "full_name": "org/repo",
    "stargazers_count": 1234,
    "description": "A great tool",
    "html_url": "https://github.com/org/repo",
    "updated_at": "2024-06-01T10:00:00Z",
    "language": "Python",
}

SAMPLE_HN_HIT = {
    "title": "Ask HN: Best tools?",
    "points": 55,
    "num_comments": 20,
    "objectID": "12345678",
    "created_at": "2024-03-01T00:00:00.000Z",
}


class TestSearchRepos:
    def test_returns_list_of_dicts(self):
        with patch("httpx.get", return_value=make_gh_response([SAMPLE_GH_ITEM])):
            result = search_repos("test query", "fake-token")
        assert isinstance(result, list)
        assert len(result) == 1

    def test_result_has_required_keys(self):
        with patch("httpx.get", return_value=make_gh_response([SAMPLE_GH_ITEM])):
            result = search_repos("test query", "fake-token")
        r = result[0]
        assert "name" in r
        assert "stars" in r
        assert "description" in r
        assert "url" in r
        assert "updated" in r
        assert "language" in r

    def test_result_maps_fields_correctly(self):
        with patch("httpx.get", return_value=make_gh_response([SAMPLE_GH_ITEM])):
            result = search_repos("test query", "fake-token")
        r = result[0]
        assert r["name"] == "org/repo"
        assert r["stars"] == 1234
        assert r["description"] == "A great tool"
        assert r["url"] == "https://github.com/org/repo"
        assert r["updated"] == "2024-06-01"
        assert r["language"] == "Python"

    def test_empty_response_returns_empty_list(self):
        with patch("httpx.get", return_value=make_gh_response([])):
            result = search_repos("test query", "fake-token")
        assert result == []

    def test_description_truncated_to_120_chars(self):
        long_desc = "x" * 200
        item = {**SAMPLE_GH_ITEM, "description": long_desc}
        with patch("httpx.get", return_value=make_gh_response([item])):
            result = search_repos("test query", "fake-token")
        assert len(result[0]["description"]) <= 120

    def test_none_description_becomes_empty_string(self):
        item = {**SAMPLE_GH_ITEM, "description": None}
        with patch("httpx.get", return_value=make_gh_response([item])):
            result = search_repos("test query", "fake-token")
        assert result[0]["description"] == ""

    def test_none_language_becomes_dash(self):
        item = {**SAMPLE_GH_ITEM, "language": None}
        with patch("httpx.get", return_value=make_gh_response([item])):
            result = search_repos("test query", "fake-token")
        assert result[0]["language"] == "—"

    def test_network_error_returns_empty_list(self):
        with patch("httpx.get", side_effect=Exception("network error")):
            result = search_repos("test query", "fake-token")
        assert result == []

    def test_respects_max_results(self):
        items = [SAMPLE_GH_ITEM] * 20
        with patch("httpx.get", return_value=make_gh_response(items)):
            result = search_repos("test query", "fake-token", max_results=3)
        assert len(result) <= 3

    def test_sends_authorization_header(self):
        with patch("httpx.get", return_value=make_gh_response([])) as mock_get:
            search_repos("test query", "my-token")
        call_kwargs = mock_get.call_args
        # headers may be positional or keyword arg
        if call_kwargs.kwargs.get("headers"):
            headers = call_kwargs.kwargs["headers"]
        else:
            headers = call_kwargs.args[1] if len(call_kwargs.args) > 1 else {}
        assert "Authorization" in headers
        assert "my-token" in headers["Authorization"]


class TestSearchDiscussions:
    def test_returns_list_of_dicts(self):
        with patch("httpx.get", return_value=make_hn_response([SAMPLE_HN_HIT])):
            result = search_discussions("test query")
        assert isinstance(result, list)
        assert len(result) == 1

    def test_result_has_required_keys(self):
        with patch("httpx.get", return_value=make_hn_response([SAMPLE_HN_HIT])):
            result = search_discussions("test query")
        h = result[0]
        assert "title" in h
        assert "points" in h
        assert "comments" in h
        assert "url" in h
        assert "date" in h

    def test_result_maps_fields_correctly(self):
        with patch("httpx.get", return_value=make_hn_response([SAMPLE_HN_HIT])):
            result = search_discussions("test query")
        h = result[0]
        assert h["title"] == "Ask HN: Best tools?"
        assert h["points"] == 55
        assert h["comments"] == 20
        assert h["url"] == "https://news.ycombinator.com/item?id=12345678"
        assert h["date"] == "2024-03-01"

    def test_empty_response_returns_empty_list(self):
        with patch("httpx.get", return_value=make_hn_response([])):
            result = search_discussions("test query")
        assert result == []

    def test_network_error_returns_empty_list(self):
        with patch("httpx.get", side_effect=Exception("network error")):
            result = search_discussions("test query")
        assert result == []

    def test_missing_points_defaults_to_zero(self):
        hit = {**SAMPLE_HN_HIT}
        del hit["points"]
        with patch("httpx.get", return_value=make_hn_response([hit])):
            result = search_discussions("test query")
        assert result[0]["points"] == 0

    def test_missing_num_comments_defaults_to_zero(self):
        hit = {**SAMPLE_HN_HIT}
        del hit["num_comments"]
        with patch("httpx.get", return_value=make_hn_response([hit])):
            result = search_discussions("test query")
        assert result[0]["comments"] == 0

    def test_respects_max_results(self):
        hits = [SAMPLE_HN_HIT] * 10
        with patch("httpx.get", return_value=make_hn_response(hits)):
            result = search_discussions("test query", max_results=3)
        assert len(result) <= 3


# ---------------------------------------------------------------------------
# Request parameter verification
# ---------------------------------------------------------------------------


class TestSearchReposParams:
    def test_sends_query_as_q_param(self):
        with patch("httpx.get", return_value=make_gh_response([])) as mock_get:
            search_repos("my search query", "token")
        params = mock_get.call_args.kwargs["params"]
        assert params["q"] == "my search query"

    def test_sorts_by_stars(self):
        with patch("httpx.get", return_value=make_gh_response([])) as mock_get:
            search_repos("test", "token")
        params = mock_get.call_args.kwargs["params"]
        assert params["sort"] == "stars"
        assert params["order"] == "desc"

    def test_per_page_matches_max_results(self):
        with patch("httpx.get", return_value=make_gh_response([])) as mock_get:
            search_repos("test", "token", max_results=5)
        params = mock_get.call_args.kwargs["params"]
        assert params["per_page"] == 5

    def test_http_status_error_returns_empty(self):
        mock = make_gh_response([])
        mock.raise_for_status.side_effect = Exception("403 Forbidden")
        with patch("httpx.get", return_value=mock):
            result = search_repos("test", "token")
        assert result == []

    def test_uses_bearer_auth_format(self):
        with patch("httpx.get", return_value=make_gh_response([])) as mock_get:
            search_repos("test", "my-secret-token")
        headers = mock_get.call_args.kwargs["headers"]
        assert headers["Authorization"] == "Bearer my-secret-token"


class TestSearchDiscussionsParams:
    def test_sends_query_param(self):
        with patch("httpx.get", return_value=make_hn_response([])) as mock_get:
            search_discussions("my hn query")
        params = mock_get.call_args.kwargs["params"]
        assert params["query"] == "my hn query"

    def test_filters_to_story_tag(self):
        with patch("httpx.get", return_value=make_hn_response([])) as mock_get:
            search_discussions("test")
        params = mock_get.call_args.kwargs["params"]
        assert params["tags"] == "story"

    def test_sends_points_filter(self):
        with patch("httpx.get", return_value=make_hn_response([])) as mock_get:
            search_discussions("test")
        params = mock_get.call_args.kwargs["params"]
        assert "points>5" in params["numericFilters"]

    def test_hits_per_page_matches_max_results(self):
        with patch("httpx.get", return_value=make_hn_response([])) as mock_get:
            search_discussions("test", max_results=7)
        params = mock_get.call_args.kwargs["params"]
        assert params["hitsPerPage"] == 7

    def test_http_status_error_returns_empty(self):
        mock = make_hn_response([])
        mock.raise_for_status.side_effect = Exception("500 Internal Server Error")
        with patch("httpx.get", return_value=mock):
            result = search_discussions("test")
        assert result == []
