"""Unit tests for github.py — ID resolvers and validators.

Covers get_label_id, resolve_thread_id, validate_node_id, and the
InvalidNodeIdError exception.
"""
from __future__ import annotations

import json
from unittest.mock import MagicMock, patch

import pytest

from active_claude_github_mcp.github.core import GitHubError, InvalidNodeIdError
from active_claude_github_mcp.github.issues import get_label_id
from active_claude_github_mcp.github.resolvers import (
    get_file_content,
    get_repo_visibility,
    resolve_thread_id,
    validate_node_id,
)
from active_claude_github_mcp.github.scaffold import (
    create_label,
    create_repository,
    get_organization_id,
)


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 TestGetLabelId:
    def test_success(self):
        response_data = {
            "data": {"repository": {"label": {"id": "LA_bug123"}}}
        }
        with patch("httpx.post", return_value=_mock_response(response_data)):
            label_id = get_label_id(TOKEN, "owner", "repo", "bug")
        assert label_id == "LA_bug123"

    def test_label_not_found_raises(self):
        response_data = {"data": {"repository": {"label": None}}}
        with patch("httpx.post", return_value=_mock_response(response_data)):
            with pytest.raises(GitHubError, match="not found"):
                get_label_id(TOKEN, "owner", "repo", "nonexistent")

    def test_empty_label_name_raises(self):
        with pytest.raises(ValueError, match="label_name"):
            get_label_id(TOKEN, "owner", "repo", "")


class TestValidateNodeId:
    def test_success_returns_none(self):
        response_data = {"data": {"node": {"id": "I_kwDO_abc"}}}
        with patch("httpx.post", return_value=_mock_response(response_data)):
            assert validate_node_id(TOKEN, "issue_id", "I_kwDO_abc") is None

    def test_null_node_raises_invalid(self):
        response_data = {"data": {"node": None}}
        with patch("httpx.post", return_value=_mock_response(response_data)):
            with pytest.raises(InvalidNodeIdError) as exc_info:
                validate_node_id(TOKEN, "thread_id", "I_kwDO_missing")
        assert exc_info.value.param_name == "thread_id"
        assert exc_info.value.value == "I_kwDO_missing"
        assert "thread_id" in str(exc_info.value)

    def test_could_not_resolve_error_raises_invalid(self):
        response_data = {
            "errors": [{"message": "Could not resolve to a node with the id of 'garbage'"}]
        }
        with patch("httpx.post", return_value=_mock_response(response_data)):
            with pytest.raises(InvalidNodeIdError):
                validate_node_id(TOKEN, "thread_id", "garbage")

    def test_transient_github_error_propagates(self):
        # A generic GitHub error (e.g. 500) should NOT be rewritten as
        # InvalidNodeIdError — it may succeed on retry.
        with patch("httpx.post", return_value=_mock_response({}, status_code=500)):
            with pytest.raises(GitHubError):
                validate_node_id(TOKEN, "thread_id", "I_kwDO_any")

    def test_empty_id_raises_value_error(self):
        with pytest.raises(ValueError, match="thread_id"):
            validate_node_id(TOKEN, "thread_id", "")

    def test_whitespace_id_raises_value_error(self):
        with pytest.raises(ValueError, match="issue_id"):
            validate_node_id(TOKEN, "issue_id", "   ")


class TestResolveThreadId:
    def test_resolves_issue(self):
        response_data = {
            "data": {"repository": {"issue": {"id": "I_kwDO_abc"}}}
        }
        with patch("httpx.post", return_value=_mock_response(response_data)):
            node_id = resolve_thread_id(TOKEN, "owner", "repo", 42, "issue")
        assert node_id == "I_kwDO_abc"

    def test_resolves_discussion(self):
        response_data = {
            "data": {"repository": {"discussion": {"id": "D_kwDO_abc"}}}
        }
        with patch("httpx.post", return_value=_mock_response(response_data)):
            node_id = resolve_thread_id(TOKEN, "owner", "repo", 7, "discussion")
        assert node_id == "D_kwDO_abc"

    def test_missing_issue_raises(self):
        response_data = {"data": {"repository": {"issue": None}}}
        with patch("httpx.post", return_value=_mock_response(response_data)):
            with pytest.raises(GitHubError, match="not found"):
                resolve_thread_id(TOKEN, "owner", "repo", 999, "issue")

    def test_missing_discussion_raises(self):
        response_data = {"data": {"repository": {"discussion": None}}}
        with patch("httpx.post", return_value=_mock_response(response_data)):
            with pytest.raises(GitHubError, match="not found"):
                resolve_thread_id(TOKEN, "owner", "repo", 999, "discussion")

    def test_empty_owner_raises(self):
        with pytest.raises(ValueError, match="owner"):
            resolve_thread_id(TOKEN, "", "repo", 1, "issue")

    def test_empty_repo_raises(self):
        with pytest.raises(ValueError, match="repo"):
            resolve_thread_id(TOKEN, "owner", "", 1, "issue")

    def test_invalid_number_raises(self):
        with pytest.raises(ValueError, match="number"):
            resolve_thread_id(TOKEN, "owner", "repo", 0, "issue")
        with pytest.raises(ValueError, match="number"):
            resolve_thread_id(TOKEN, "owner", "repo", -5, "issue")

    def test_invalid_kind_raises(self):
        with pytest.raises(ValueError, match="kind"):
            resolve_thread_id(TOKEN, "owner", "repo", 1, "pull_request")


class TestGetFileContent:
    def test_returns_text(self):
        response_data = {
            "data": {
                "repository": {
                    "object": {"text": "file body contents"}
                }
            }
        }
        with patch("httpx.post", return_value=_mock_response(response_data)):
            out = get_file_content(TOKEN, "o", "r", "main", "README.md")
        assert out == "file body contents"

    def test_returns_none_when_object_missing(self):
        """Missing file (non-existent path/ref) → None, not an error."""
        response_data = {"data": {"repository": {"object": None}}}
        with patch("httpx.post", return_value=_mock_response(response_data)):
            assert get_file_content(TOKEN, "o", "r", "main", "nope.md") is None

    def test_empty_owner_raises(self):
        with pytest.raises(ValueError, match="owner"):
            get_file_content(TOKEN, "", "r", "main", "x.md")

    def test_empty_ref_raises(self):
        with pytest.raises(ValueError, match="ref"):
            get_file_content(TOKEN, "o", "r", "", "x.md")

    def test_empty_path_raises(self):
        with pytest.raises(ValueError, match="path"):
            get_file_content(TOKEN, "o", "r", "main", "")


class TestGetRepoVisibility:
    """#116 — get_repo_visibility for the open_session repo-shape audit."""

    def test_returns_visibility_string(self):
        response_data = {
            "data": {"repository": {"visibility": "PRIVATE"}}
        }
        with patch("httpx.post", return_value=_mock_response(response_data)):
            assert get_repo_visibility(TOKEN, "o", "r") == "PRIVATE"

    def test_returns_public_for_public_repo(self):
        response_data = {
            "data": {"repository": {"visibility": "PUBLIC"}}
        }
        with patch("httpx.post", return_value=_mock_response(response_data)):
            assert get_repo_visibility(TOKEN, "o", "r") == "PUBLIC"

    def test_returns_none_when_repo_missing(self):
        """Repo not found → None (not an error). Distinguishes ABSENT from
        transient API failures."""
        response_data = {"data": {"repository": None}}
        with patch("httpx.post", return_value=_mock_response(response_data)):
            assert get_repo_visibility(TOKEN, "o", "ghost-repo") is None

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

    def test_empty_repo_raises(self):
        with pytest.raises(ValueError, match="repo"):
            get_repo_visibility(TOKEN, "o", "")


class TestGetOrganizationId:
    def test_returns_id_when_org_exists(self):
        response_data = {"data": {"organization": {"id": "O_kwDO_org"}}}
        with patch("httpx.post", return_value=_mock_response(response_data)):
            assert get_organization_id(TOKEN, "myorg") == "O_kwDO_org"

    def test_returns_none_when_org_missing(self):
        response_data = {"data": {"organization": None}}
        with patch("httpx.post", return_value=_mock_response(response_data)):
            assert get_organization_id(TOKEN, "ghost-org") is None

    def test_empty_login_raises(self):
        with pytest.raises(ValueError, match="login"):
            get_organization_id(TOKEN, "")


class TestCreateRepository:
    def test_returns_fields(self):
        response_data = {
            "data": {
                "createRepository": {
                    "repository": {
                        "id": "R_kwDO_new",
                        "url": "https://github.com/org/name",
                        "nameWithOwner": "org/name",
                    }
                }
            }
        }
        with patch("httpx.post", return_value=_mock_response(response_data)):
            out = create_repository(TOKEN, "O_kwDO_org", "name", "PUBLIC", "desc")
        assert out["id"] == "R_kwDO_new"
        assert out["name_with_owner"] == "org/name"

    def test_invalid_visibility_raises(self):
        with pytest.raises(ValueError, match="visibility"):
            create_repository(TOKEN, "O_abc", "name", "SECRET")

    def test_empty_name_raises(self):
        with pytest.raises(ValueError, match="name"):
            create_repository(TOKEN, "O_abc", "", "PUBLIC")

    def test_empty_owner_id_raises(self):
        with pytest.raises(ValueError, match="owner_id"):
            create_repository(TOKEN, "", "name", "PUBLIC")


class TestCreateLabel:
    def test_returns_label_id(self):
        response_data = {
            "data": {"createLabel": {"label": {"id": "LA_new"}}}
        }
        with patch("httpx.post", return_value=_mock_response(response_data)):
            assert create_label(TOKEN, "R_repo", "bug", "d73a4a", "Issue") == "LA_new"

    def test_empty_name_raises(self):
        with pytest.raises(ValueError, match="name"):
            create_label(TOKEN, "R_repo", "")

    def test_github_error_propagates(self):
        """Already-exists errors surface as GitHubError — caller is
        responsible for idempotent catching."""
        response_data = {
            "errors": [{"message": "Name has already been taken"}]
        }
        with patch("httpx.post", return_value=_mock_response(response_data)):
            with pytest.raises(GitHubError):
                create_label(TOKEN, "R_repo", "bug")


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