"""
Unit tests for the server package — build-server fast-fail, tool
registration, and the `resolve_thread_identifier` dispatch helper.

The pure parsers (parse_skill_version, parse_changelog, semver_gt,
parse_owner_repo) live in test_parsers.py.

The IO-free utilities (SessionActivity, compose_session_summary,
meets_confidence_threshold, check_overwrite_size) live in
test_utils.py.

Full tool execution is covered in tests/tools/.
MCP protocol compliance is covered in tests/integration/.
"""

from __future__ import annotations

from unittest.mock import patch

import pytest

from active_claude_github_mcp.config import ConfigError
from active_claude_github_mcp.server import build_server
from active_claude_github_mcp.server.helpers import resolve_thread_identifier


class TestResolveThreadIdentifier:
    """#56 — tools accept either a node ID or (number, owner, repo).

    `resolve_thread_identifier` calls `resolve_thread_id` through the
    `active_claude_github_mcp.server` package namespace (late binding),
    so tests can still monkey-patch `...server.resolve_thread_id`.
    """

    def test_uses_node_id_when_provided(self):
        # Resolver should not call any GitHub API when node ID is present
        with patch(
            "active_claude_github_mcp.server.resolve_thread_id"
        ) as mock_resolve:
            result = resolve_thread_identifier(
                "ghp_test",
                {"issue_id": "I_abc"},
                primary_id_param="issue_id",
                kind="issue",
                default_owner="owner",
                default_repo="repo",
            )
        assert result == "I_abc"
        mock_resolve.assert_not_called()

    def test_resolves_from_number_with_default_owner_repo(self):
        with patch(
            "active_claude_github_mcp.server.resolve_thread_id",
            return_value="I_resolved",
        ) as mock_resolve:
            result = resolve_thread_identifier(
                "ghp_test",
                {"number": 42},
                primary_id_param="issue_id",
                kind="issue",
                default_owner="defowner",
                default_repo="defrepo",
            )
        assert result == "I_resolved"
        mock_resolve.assert_called_once_with(
            "ghp_test", "defowner", "defrepo", 42, "issue"
        )

    def test_explicit_owner_repo_override_defaults(self):
        with patch(
            "active_claude_github_mcp.server.resolve_thread_id",
            return_value="D_resolved",
        ) as mock_resolve:
            resolve_thread_identifier(
                "ghp_test",
                {"number": 7, "owner": "alt", "repo": "altrepo"},
                primary_id_param="discussion_id",
                kind="discussion",
                default_owner="defowner",
                default_repo="defrepo",
            )
        mock_resolve.assert_called_once_with(
            "ghp_test", "alt", "altrepo", 7, "discussion"
        )

    def test_both_forms_raises(self):
        with pytest.raises(ValueError, match="either"):
            resolve_thread_identifier(
                "ghp_test",
                {"issue_id": "I_abc", "number": 42},
                primary_id_param="issue_id",
                kind="issue",
                default_owner="owner",
                default_repo="repo",
            )

    def test_neither_form_raises(self):
        with pytest.raises(ValueError, match="must be provided"):
            resolve_thread_identifier(
                "ghp_test",
                {},
                primary_id_param="issue_id",
                kind="issue",
                default_owner="owner",
                default_repo="repo",
            )

    def test_non_integer_number_raises(self):
        with pytest.raises(ValueError, match="number must be"):
            resolve_thread_identifier(
                "ghp_test",
                {"number": "not_an_int"},
                primary_id_param="issue_id",
                kind="issue",
                default_owner="owner",
                default_repo="repo",
            )


TOOL_NAMES = {
    "open_session",
    "recover_context",
    "check_skill_version",
    "replay_pending",
    "get_thread_comments",
    "close_issue_with_comment",
    "close_session",
    "post_turn",
    "create_thread",
    "classify_turn",
    "classify_and_create",
    "scaffold_project",
    "detect_theme_change",
    "fork_discussion",
    "create_issue",
    "update_issue_toc",
    "move_project_item",
    "update_project_toc",
    "mark_discussion_decided",
    "validate_release",
    "release_project",
    "generate_changelog",
}


class TestBuildServerFastFail:
    def test_raises_config_error_if_project_dir_missing(self, tmp_path):
        missing = tmp_path / "no-such-dir"
        with pytest.raises(ConfigError, match="does not exist"):
            build_server(missing)

    def test_raises_config_error_if_no_config_file(
        self, empty_project_dir, monkeypatch
    ):
        monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-test")
        monkeypatch.setenv("GITHUB_TOKEN", "ghp_test")
        with pytest.raises(ConfigError, match="not found"):
            build_server(empty_project_dir)

    def test_raises_config_error_if_api_key_missing(
        self, project_dir, monkeypatch
    ):
        monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
        monkeypatch.setenv("GITHUB_TOKEN", "ghp_test")
        with pytest.raises(ConfigError, match="ANTHROPIC_API_KEY"):
            build_server(project_dir)


class TestToolRegistration:
    """
    Verify that all expected tools are registered and have
    valid inputSchema definitions.
    """

    @pytest.fixture
    def server_and_config(self, project_dir, monkeypatch):
        monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-test")
        monkeypatch.setenv("GITHUB_TOKEN", "ghp_test")
        return build_server(project_dir)

    @pytest.mark.asyncio
    async def test_all_tools_registered(self, server_and_config):
        server, _, _, _ = server_and_config
        tools = await server.list_tools_handler()
        tool_names = {t.name for t in tools}
        assert TOOL_NAMES == tool_names, (
            f"Missing tools: {TOOL_NAMES - tool_names}\n"
            f"Extra tools: {tool_names - TOOL_NAMES}"
        )

    @pytest.mark.asyncio
    async def test_all_tools_have_schemas(self, server_and_config):
        server, _, _, _ = server_and_config
        tools = await server.list_tools_handler()
        for tool in tools:
            assert tool.inputSchema is not None, f"{tool.name} missing inputSchema"
            assert tool.inputSchema.get("type") == "object", (
                f"{tool.name} inputSchema type should be 'object'"
            )

    @pytest.mark.asyncio
    async def test_required_fields_declared(self, server_and_config):
        server, _, _, _ = server_and_config
        tools = await server.list_tools_handler()
        tool_map = {t.name: t for t in tools}

        # post_turn requires thread_type / speaker / body. thread_id is
        # optional post-#56 — callers can provide number + owner/repo
        # instead.
        post_turn = tool_map["post_turn"]
        assert set(post_turn.inputSchema.get("required", [])) >= {
            "thread_type", "speaker", "body"
        }
        props = post_turn.inputSchema.get("properties", {})
        assert "thread_id" in props
        assert "number" in props

        # create_issue requires title and body. Either `labels` (list)
        # or `label` (deprecated single) must be supplied at call time
        # but neither is declared required in the schema (#55).
        create_issue = tool_map["create_issue"]
        assert set(create_issue.inputSchema.get("required", [])) >= {
            "title", "body"
        }
        props = create_issue.inputSchema.get("properties", {})
        assert "labels" in props
        assert "label" in props  # deprecated alias still present


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