"""Unit tests for github.py — ProjectV2 operations.

Covers get_project_item_id (resolves Issue node ID → ProjectV2Item ID).
Also exercises the `get_project_status_field` and `move_project_item`
helpers indirectly via the dispatch tests (`tests/tools/
test_move_project_item.py`).
"""
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.projects import (
    STATUS_OPTIONS,
    create_projectv2,
    get_project_item_id,
    link_projectv2_to_repo,
    list_owner_projects,
    update_projectv2_status_options,
)


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 TestGetProjectItemId:
    def test_success(self):
        response_data = {
            "data": {
                "node": {
                    "projectItems": {
                        "nodes": [
                            {"id": "PVTI_other", "project": {"id": "PVT_other"}},
                            {"id": "PVTI_match", "project": {"id": "PVT_target"}},
                        ]
                    }
                }
            }
        }
        with patch("httpx.post", return_value=_mock_response(response_data)):
            item_id = get_project_item_id(TOKEN, "I_issue1", "PVT_target")
        assert item_id == "PVTI_match"

    def test_issue_not_on_project_raises(self):
        response_data = {
            "data": {
                "node": {
                    "projectItems": {
                        "nodes": [
                            {"id": "PVTI_other", "project": {"id": "PVT_other"}},
                        ]
                    }
                }
            }
        }
        with patch("httpx.post", return_value=_mock_response(response_data)):
            with pytest.raises(GitHubError, match="not on project"):
                get_project_item_id(TOKEN, "I_issue1", "PVT_target")

    def test_issue_has_no_projects_raises(self):
        response_data = {
            "data": {"node": {"projectItems": {"nodes": []}}}
        }
        with patch("httpx.post", return_value=_mock_response(response_data)):
            with pytest.raises(GitHubError, match="not on project"):
                get_project_item_id(TOKEN, "I_issue1", "PVT_target")

    def test_issue_node_missing_raises(self):
        response_data = {"data": {"node": None}}
        with patch("httpx.post", return_value=_mock_response(response_data)):
            with pytest.raises(GitHubError, match="not on project"):
                get_project_item_id(TOKEN, "I_issue1", "PVT_target")

    def test_empty_issue_id_raises(self):
        with pytest.raises(ValueError, match="issue_id"):
            get_project_item_id(TOKEN, "", "PVT_target")

    def test_empty_project_id_raises(self):
        with pytest.raises(ValueError, match="project_id"):
            get_project_item_id(TOKEN, "I_issue1", "")


class TestListOwnerProjects:
    def test_empty_owner_id_raises(self):
        with pytest.raises(ValueError, match="owner_id"):
            list_owner_projects(TOKEN, "")

    def test_owner_with_projects(self):
        response_data = {
            "data": {
                "node": {
                    "projectsV2": {
                        "nodes": [
                            {"id": "PVT_1", "number": 1, "title": "widgets", "url": "u1"},
                            {"id": "PVT_2", "number": 2, "title": "gadgets", "url": "u2"},
                        ]
                    }
                }
            }
        }
        with patch("httpx.post", return_value=_mock_response(response_data)):
            out = list_owner_projects(TOKEN, "O_org")
        assert [p["title"] for p in out] == ["widgets", "gadgets"]
        assert out[0]["id"] == "PVT_1"

    def test_owner_without_projects(self):
        response_data = {"data": {"node": {"projectsV2": {"nodes": []}}}}
        with patch("httpx.post", return_value=_mock_response(response_data)):
            assert list_owner_projects(TOKEN, "O_org") == []

    def test_owner_not_found_returns_empty(self):
        response_data = {"data": {"node": None}}
        with patch("httpx.post", return_value=_mock_response(response_data)):
            assert list_owner_projects(TOKEN, "O_missing") == []


class TestCreateProjectV2:
    def test_empty_owner_id_raises(self):
        with pytest.raises(ValueError, match="owner_id"):
            create_projectv2(TOKEN, "", "title")

    def test_empty_title_raises(self):
        with pytest.raises(ValueError, match="title"):
            create_projectv2(TOKEN, "O_org", "")

    def test_success(self):
        response_data = {
            "data": {
                "createProjectV2": {
                    "projectV2": {
                        "id": "PVT_new",
                        "number": 7,
                        "url": "https://github.com/orgs/o/projects/7",
                    }
                }
            }
        }
        with patch("httpx.post", return_value=_mock_response(response_data)):
            out = create_projectv2(TOKEN, "O_org", "widgets")
        assert out == {
            "id": "PVT_new",
            "number": 7,
            "url": "https://github.com/orgs/o/projects/7",
        }

    def test_empty_response_raises(self):
        response_data = {"data": {"createProjectV2": {"projectV2": None}}}
        with patch("httpx.post", return_value=_mock_response(response_data)):
            with pytest.raises(GitHubError, match="no project"):
                create_projectv2(TOKEN, "O_org", "widgets")


class TestLinkProjectV2ToRepo:
    def test_empty_project_id_raises(self):
        with pytest.raises(ValueError, match="project_id"):
            link_projectv2_to_repo(TOKEN, "", "R_dev")

    def test_empty_repo_id_raises(self):
        with pytest.raises(ValueError, match="repo_id"):
            link_projectv2_to_repo(TOKEN, "PVT_1", "")

    def test_success(self):
        response_data = {
            "data": {"linkProjectV2ToRepository": {"repository": {"id": "R_dev"}}}
        }
        with patch("httpx.post", return_value=_mock_response(response_data)):
            assert link_projectv2_to_repo(TOKEN, "PVT_1", "R_dev") is True


class TestUpdateProjectV2StatusOptions:
    def _status_read_response(self, option_names: list[str]) -> dict:
        """Build a `get_project_status_field` response with the given options."""
        return {
            "data": {
                "node": {
                    "fields": {
                        "nodes": [
                            {
                                "id": "PVTSSF_status",
                                "name": "Status",
                                "options": [
                                    {"id": f"OPT_{i}", "name": n}
                                    for i, n in enumerate(option_names)
                                ],
                            }
                        ]
                    }
                }
            }
        }

    def _status_write_response(self, option_names: list[str]) -> dict:
        return {
            "data": {
                "updateProjectV2Field": {
                    "projectV2Field": {
                        "id": "PVTSSF_status",
                        "options": [
                            {"id": f"OPT_new_{i}", "name": n}
                            for i, n in enumerate(option_names)
                        ],
                    }
                }
            }
        }

    def test_empty_project_id_raises(self):
        with pytest.raises(ValueError, match="project_id"):
            update_projectv2_status_options(TOKEN, "", "F_x")

    def test_empty_field_id_raises(self):
        with pytest.raises(ValueError, match="field_id"):
            update_projectv2_status_options(TOKEN, "PVT_1", "")

    def test_empty_desired_raises(self):
        with pytest.raises(ValueError, match="desired_names"):
            update_projectv2_status_options(TOKEN, "PVT_1", "F_x", [])

    def test_duplicate_desired_raises(self):
        with pytest.raises(ValueError, match="unique"):
            update_projectv2_status_options(TOKEN, "PVT_1", "F_x", ["A", "A"])

    def test_no_op_when_matches(self):
        """Read-diff-write: if current == desired, no mutation is issued."""
        read = self._status_read_response(list(STATUS_OPTIONS))
        mock_post = MagicMock(return_value=_mock_response(read))
        with patch("httpx.post", mock_post):
            out = update_projectv2_status_options(TOKEN, "PVT_1", "F_x", STATUS_OPTIONS)
        assert out["changed"] is False
        assert list(out["options"].keys()) == list(STATUS_OPTIONS)
        # Exactly one GraphQL call — the read. No write.
        assert mock_post.call_count == 1

    def test_mutates_when_differs(self):
        """When current options differ from desired, mutation is issued."""
        read = self._status_read_response(["Todo", "In Progress", "Done"])
        write = self._status_write_response(list(STATUS_OPTIONS))
        responses = [_mock_response(read), _mock_response(write)]
        mock_post = MagicMock(side_effect=responses)
        with patch("httpx.post", mock_post):
            out = update_projectv2_status_options(TOKEN, "PVT_1", "F_x", STATUS_OPTIONS)
        assert out["changed"] is True
        assert list(out["options"].keys()) == list(STATUS_OPTIONS)
        assert mock_post.call_count == 2
        # Verify the second call was the mutation with the correct options.
        write_call = mock_post.call_args_list[1]
        sent_vars = write_call.kwargs["json"]["variables"]
        assert [o["name"] for o in sent_vars["options"]] == list(STATUS_OPTIONS)

    def test_default_uses_canonical_status_set(self):
        """Calling without `desired_names` uses the module's STATUS_OPTIONS."""
        read = self._status_read_response(list(STATUS_OPTIONS))
        with patch("httpx.post", return_value=_mock_response(read)):
            out = update_projectv2_status_options(TOKEN, "PVT_1", "F_x")
        assert out["changed"] is False
        assert list(out["options"].keys()) == list(STATUS_OPTIONS)


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