"""Unit tests for github.py — context-recovery list helpers.

Covers list_thread_comments, list_open_issues, list_recent_discussions,
list_milestones_with_progress. These are the reads that back the
recover_context tool (#58).
"""
from __future__ import annotations

import json
from unittest.mock import MagicMock, patch

import pytest

from active_claude_github_mcp.github.core import GitHubError
from active_claude_github_mcp.github.recovery import (
    get_milestone_by_title,
    list_milestones_with_progress,
    list_open_issues,
    list_recent_discussions,
    list_thread_comments,
)


TOKEN = "ghp_test_token"


def _mock_response(data: dict, status_code: int = 200):
    mock = MagicMock()
    mock.status_code = status_code
    mock.json.return_value = data
    mock.text = json.dumps(data)
    return mock


class TestListThreadComments:
    def _make_response(self, nodes):
        return {"data": {"node": {"comments": {"nodes": nodes}}}}

    def test_discussion(self):
        nodes = [
            {
                "id": "DC_1",
                "body": "hi",
                "createdAt": "2026-01-01T00:00:00Z",
                "author": {"login": "alice"},
            },
            {
                "id": "DC_2",
                "body": "hey",
                "createdAt": "2026-01-02T00:00:00Z",
                "author": None,  # author can be null
            },
        ]
        with patch("httpx.post", return_value=_mock_response(self._make_response(nodes))):
            out = list_thread_comments(TOKEN, "D_abc", "discussion")
        assert len(out) == 2
        assert out[0]["comment_id"] == "DC_1"
        assert out[0]["author"] == "alice"
        assert out[1]["author"] == ""  # null mapped to empty string

    def test_issue(self):
        nodes = [
            {
                "id": "IC_1",
                "body": "report",
                "createdAt": "2026-02-01T00:00:00Z",
                "author": {"login": "bob"},
            },
        ]
        with patch("httpx.post", return_value=_mock_response(self._make_response(nodes))):
            out = list_thread_comments(TOKEN, "I_abc", "issue")
        assert out[0]["author"] == "bob"

    def test_missing_thread_raises(self):
        with patch("httpx.post", return_value=_mock_response({"data": {"node": None}})):
            with pytest.raises(GitHubError, match="not found"):
                list_thread_comments(TOKEN, "D_missing", "discussion")

    def test_invalid_type_raises(self):
        with pytest.raises(ValueError, match="thread_type"):
            list_thread_comments(TOKEN, "D_abc", "pull_request")

    def test_empty_thread_id_raises(self):
        with pytest.raises(ValueError, match="thread_id"):
            list_thread_comments(TOKEN, "", "discussion")

    def test_limit_out_of_range_raises(self):
        with pytest.raises(ValueError, match="limit"):
            list_thread_comments(TOKEN, "D_abc", "discussion", limit=0)
        with pytest.raises(ValueError, match="limit"):
            list_thread_comments(TOKEN, "D_abc", "discussion", limit=101)


class TestListOpenIssues:
    def test_maps_fields_and_labels(self):
        response_data = {
            "data": {
                "repository": {
                    "issues": {
                        "nodes": [
                            {
                                "number": 7,
                                "title": "Alpha",
                                "url": "https://github.com/o/r/issues/7",
                                "updatedAt": "2026-04-20T00:00:00Z",
                                "labels": {"nodes": [{"name": "bug"}, {"name": "area:mcp"}]},
                            },
                            {
                                "number": 8,
                                "title": "Beta",
                                "url": "https://github.com/o/r/issues/8",
                                "updatedAt": "2026-04-19T00:00:00Z",
                                "labels": {"nodes": []},
                            },
                        ]
                    }
                }
            }
        }
        with patch("httpx.post", return_value=_mock_response(response_data)):
            issues = list_open_issues(TOKEN, "o", "r", limit=50)
        assert len(issues) == 2
        assert issues[0]["number"] == 7
        assert issues[0]["labels"] == ["bug", "area:mcp"]
        assert issues[1]["labels"] == []

    def test_empty_repo_returns_empty_list(self):
        response_data = {"data": {"repository": {"issues": {"nodes": []}}}}
        with patch("httpx.post", return_value=_mock_response(response_data)):
            assert list_open_issues(TOKEN, "o", "r") == []

    def test_empty_owner_raises(self):
        with pytest.raises(ValueError, match="owner"):
            list_open_issues(TOKEN, "", "r")

    def test_limit_out_of_range_raises(self):
        with pytest.raises(ValueError, match="limit"):
            list_open_issues(TOKEN, "o", "r", limit=0)
        with pytest.raises(ValueError, match="limit"):
            list_open_issues(TOKEN, "o", "r", limit=101)


class TestListRecentDiscussions:
    def _make_response(self, nodes):
        return {"data": {"repository": {"discussions": {"nodes": nodes}}}}

    def test_filters_older_than_cutoff(self):
        from datetime import datetime, timedelta, timezone
        now = datetime.now(timezone.utc)
        fresh = (now - timedelta(days=3)).isoformat().replace("+00:00", "Z")
        stale = (now - timedelta(days=30)).isoformat().replace("+00:00", "Z")
        nodes = [
            {
                "number": 1,
                "title": "Fresh",
                "url": "u1",
                "updatedAt": fresh,
                "category": {"name": "Architecture"},
            },
            {
                "number": 2,
                "title": "Stale",
                "url": "u2",
                "updatedAt": stale,
                "category": {"name": "Ideas"},
            },
        ]
        with patch("httpx.post", return_value=_mock_response(self._make_response(nodes))):
            out = list_recent_discussions(TOKEN, "o", "r", since_days=7)
        assert len(out) == 1
        assert out[0]["number"] == 1
        assert out[0]["category"] == "Architecture"

    def test_empty_repo_returns_empty(self):
        with patch("httpx.post", return_value=_mock_response(self._make_response([]))):
            assert list_recent_discussions(TOKEN, "o", "r") == []

    def test_zero_since_days_raises(self):
        with pytest.raises(ValueError, match="since_days"):
            list_recent_discussions(TOKEN, "o", "r", since_days=0)

    def test_empty_owner_raises(self):
        with pytest.raises(ValueError, match="owner"):
            list_recent_discussions(TOKEN, "", "r")


class TestListMilestonesWithProgress:
    def test_maps_fields(self):
        response_data = {
            "data": {
                "repository": {
                    "milestones": {
                        "nodes": [
                            {
                                "title": "1.2.0",
                                "dueOn": "2026-05-01T00:00:00Z",
                                "progressPercentage": 44.4,
                            },
                            {
                                "title": "1.3.0",
                                "dueOn": None,
                                "progressPercentage": 0,
                            },
                        ]
                    }
                }
            }
        }
        with patch("httpx.post", return_value=_mock_response(response_data)):
            out = list_milestones_with_progress(TOKEN, "o", "r")
        assert len(out) == 2
        assert out[0]["title"] == "1.2.0"
        assert out[0]["due_on"] == "2026-05-01T00:00:00Z"
        assert out[0]["percent_complete"] == pytest.approx(44.4)
        assert out[1]["due_on"] is None
        assert out[1]["percent_complete"] == 0.0

    def test_empty_owner_raises(self):
        with pytest.raises(ValueError, match="owner"):
            list_milestones_with_progress(TOKEN, "", "r")

    def test_empty_repo_returns_empty_list(self):
        response_data = {"data": {"repository": {"milestones": {"nodes": []}}}}
        with patch("httpx.post", return_value=_mock_response(response_data)):
            assert list_milestones_with_progress(TOKEN, "o", "r") == []


class TestGetMilestoneByTitle:
    def _response(self, milestones: list[dict]):
        return {"data": {"repository": {"milestones": {"nodes": milestones}}}}

    def test_exact_match_returns_details(self):
        resp = self._response([
            {
                "number": 2,
                "title": "1.3.0",
                "state": "OPEN",
                "issues": {
                    "nodes": [
                        {"number": 103, "title": "skill: prose", "url": "u/103"},
                        {"number": 107, "title": "MCP: pre-flight", "url": "u/107"},
                    ]
                },
            },
        ])
        with patch("httpx.post", return_value=_mock_response(resp)):
            out = get_milestone_by_title(TOKEN, "o", "r", "1.3.0")
        assert out is not None
        assert out["number"] == 2
        assert out["state"] == "OPEN"
        assert out["open_issues"] == [
            {"number": 103, "title": "skill: prose", "url": "u/103"},
            {"number": 107, "title": "MCP: pre-flight", "url": "u/107"},
        ]

    def test_no_match_returns_none(self):
        resp = self._response([
            {"number": 1, "title": "1.2.0", "state": "CLOSED",
             "issues": {"nodes": []}},
        ])
        with patch("httpx.post", return_value=_mock_response(resp)):
            assert get_milestone_by_title(TOKEN, "o", "r", "1.3.0") is None

    def test_closed_milestone_no_open_issues(self):
        resp = self._response([
            {"number": 2, "title": "1.3.0", "state": "CLOSED",
             "issues": {"nodes": []}},
        ])
        with patch("httpx.post", return_value=_mock_response(resp)):
            out = get_milestone_by_title(TOKEN, "o", "r", "1.3.0")
        assert out["state"] == "CLOSED"
        assert out["open_issues"] == []

    def test_empty_title_raises(self):
        with pytest.raises(ValueError, match="title"):
            get_milestone_by_title(TOKEN, "o", "r", "   ")

    def test_empty_owner_raises(self):
        with pytest.raises(ValueError, match="owner"):
            get_milestone_by_title(TOKEN, "", "r", "1.3.0")


if __name__ == "__main__":
    import sys
    import pytest
    sys.exit(pytest.main([__file__, "-v"]))
