import importlib.metadata
import subprocess
import sys
from pathlib import Path
from typing import Any

import pytest

from mcp.cli.cli import _build_uv_command, _get_npx_command, _parse_file_path  # type: ignore[reportPrivateUsage]


def _set_mcp_version(monkeypatch: pytest.MonkeyPatch, version: str) -> None:
    real_version = importlib.metadata.version

    def fake_version(distribution_name: str) -> str:
        return version if distribution_name == "mcp" else real_version(distribution_name)

    monkeypatch.setattr(importlib.metadata, "version", fake_version)


@pytest.mark.parametrize(
    "spec, expected_obj",
    [
        ("server.py", None),
        ("foo.py:srv_obj", "srv_obj"),
    ],
)
def test_parse_file_path_accepts_valid_specs(tmp_path: Path, spec: str, expected_obj: str | None):
    """Should accept valid file specs."""
    file = tmp_path / spec.split(":")[0]
    file.write_text("x = 1")
    path, obj = _parse_file_path(f"{file}:{expected_obj}" if ":" in spec else str(file))
    assert path == file.resolve()
    assert obj == expected_obj


def test_parse_file_path_missing(tmp_path: Path):
    """Should system exit if a file is missing."""
    with pytest.raises(SystemExit):
        _parse_file_path(str(tmp_path / "missing.py"))


def test_parse_file_exit_on_dir(tmp_path: Path):
    """Should system exit if a directory is passed"""
    dir_path = tmp_path / "dir"
    dir_path.mkdir()
    with pytest.raises(SystemExit):
        _parse_file_path(str(dir_path))


def test_build_uv_command_pins_the_running_mcp_version(monkeypatch: pytest.MonkeyPatch):
    """The spawned environment installs the same SDK version that is running, not the latest stable."""
    _set_mcp_version(monkeypatch, "1.2.3")
    cmd = _build_uv_command("foo.py")
    assert cmd == ["uv", "run", "--with", "mcp==1.2.3", "mcp", "run", "foo.py"]


def test_build_uv_command_leaves_source_builds_unpinned(monkeypatch: pytest.MonkeyPatch):
    """Source-build versions are not on PyPI, so the requirement stays unpinned."""
    _set_mcp_version(monkeypatch, "2.0.0a2.dev3+g0123abc")
    cmd = _build_uv_command("foo.py")
    assert cmd == ["uv", "run", "--with", "mcp", "mcp", "run", "foo.py"]


def test_build_uv_command_adds_editable_and_packages(monkeypatch: pytest.MonkeyPatch):
    """Should include --with-editable and every --with pkg in correct order."""
    _set_mcp_version(monkeypatch, "1.2.3")
    test_path = Path("/pkg")
    cmd = _build_uv_command(
        "foo.py",
        with_editable=test_path,
        with_packages=["package1", "package2"],
    )
    assert cmd == [
        "uv",
        "run",
        "--with",
        "mcp==1.2.3",
        "--with-editable",
        str(test_path),  # Use str() to match what the function does
        "--with",
        "package1",
        "--with",
        "package2",
        "mcp",
        "run",
        "foo.py",
    ]


def test_get_npx_unix_like(monkeypatch: pytest.MonkeyPatch):
    """Should return "npx" on unix-like systems."""
    monkeypatch.setattr(sys, "platform", "linux")
    assert _get_npx_command() == "npx"


def test_get_npx_windows(monkeypatch: pytest.MonkeyPatch):
    """Should return one of the npx candidates on Windows."""
    candidates = ["npx.cmd", "npx.exe", "npx"]

    def fake_run(cmd: list[str], **kw: Any) -> subprocess.CompletedProcess[bytes]:
        if cmd[0] in candidates:
            return subprocess.CompletedProcess(cmd, 0)
        else:  # pragma: no cover
            raise subprocess.CalledProcessError(1, cmd[0])

    monkeypatch.setattr(sys, "platform", "win32")
    monkeypatch.setattr(subprocess, "run", fake_run)
    assert _get_npx_command() in candidates


def test_get_npx_returns_none_when_npx_missing(monkeypatch: pytest.MonkeyPatch):
    """Should give None if every candidate fails."""
    monkeypatch.setattr(sys, "platform", "win32", raising=False)

    def always_fail(*args: Any, **kwargs: Any) -> subprocess.CompletedProcess[bytes]:
        raise subprocess.CalledProcessError(1, args[0])

    monkeypatch.setattr(subprocess, "run", always_fail)
    assert _get_npx_command() is None
