"""Tests for the fastmcp project prepare command."""

import subprocess
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from fastmcp.utilities.mcp_server_config import MCPServerConfig
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource


class TestMCPServerConfigPrepare:
    """Test the MCPServerConfig.prepare() method."""

    @patch(
        "fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_source",
        new_callable=AsyncMock,
    )
    @patch(
        "fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_environment",
        new_callable=AsyncMock,
    )
    async def test_prepare_calls_both_methods(self, mock_env, mock_src):
        """Test that prepare() calls both prepare_environment and prepare_source."""
        config = MCPServerConfig(
            source=FileSystemSource(path="server.py"),
            environment=UVEnvironment(python="3.10"),
        )

        await config.prepare()

        mock_env.assert_called_once()
        mock_src.assert_called_once()

    @patch(
        "fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_source",
        new_callable=AsyncMock,
    )
    @patch(
        "fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_environment",
        new_callable=AsyncMock,
    )
    async def test_prepare_with_output_dir(self, mock_env, mock_src):
        """Test that prepare() with output_dir calls prepare_environment with it."""
        config = MCPServerConfig(
            source=FileSystemSource(path="server.py"),
            environment=UVEnvironment(python="3.10"),
        )

        output_path = Path("/tmp/test-env")
        await config.prepare(skip_source=False, output_dir=output_path)

        mock_env.assert_called_once_with(output_dir=output_path)
        mock_src.assert_called_once()

    @patch(
        "fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_source",
        new_callable=AsyncMock,
    )
    @patch(
        "fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_environment",
        new_callable=AsyncMock,
    )
    async def test_prepare_skip_source(self, mock_env, mock_src):
        """Test that prepare() skips source when skip_source=True."""
        config = MCPServerConfig(
            source=FileSystemSource(path="server.py"),
            environment=UVEnvironment(python="3.10"),
        )

        await config.prepare(skip_source=True)

        mock_env.assert_called_once_with(output_dir=None)
        mock_src.assert_not_called()

    @patch(
        "fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_source",
        new_callable=AsyncMock,
    )
    @patch(
        "fastmcp.utilities.mcp_server_config.v1.environments.uv.UVEnvironment.prepare",
        new_callable=AsyncMock,
    )
    async def test_prepare_no_environment_settings(self, mock_env_prepare, mock_src):
        """Test that prepare() works with default empty environment config."""
        config = MCPServerConfig(
            source=FileSystemSource(path="server.py"),
            # environment defaults to empty Environment()
        )

        await config.prepare(skip_source=False)

        # Environment prepare should be called even with empty config
        mock_env_prepare.assert_called_once_with(output_dir=None)
        mock_src.assert_called_once()


class TestEnvironmentPrepare:
    """Test the Environment.prepare() method."""

    @patch("shutil.which")
    async def test_prepare_no_uv_installed(self, mock_which, tmp_path):
        """Test that prepare() raises error when uv is not installed."""
        mock_which.return_value = None

        env = UVEnvironment(python="3.10")

        with pytest.raises(RuntimeError, match="uv is not installed"):
            await env.prepare(tmp_path / "test-env")

    @patch("subprocess.run")
    @patch("shutil.which")
    async def test_prepare_no_settings(self, mock_which, mock_run, tmp_path):
        """Test that prepare() does nothing when no settings are configured."""
        mock_which.return_value = "/usr/bin/uv"

        env = UVEnvironment()  # No settings

        await env.prepare(tmp_path / "test-env")

        # Should not run any commands
        mock_run.assert_not_called()

    @patch("subprocess.run")
    @patch("shutil.which")
    async def test_prepare_with_python(self, mock_which, mock_run, tmp_path):
        """Test that prepare() runs uv with python version."""
        mock_which.return_value = "/usr/bin/uv"
        mock_run.return_value = MagicMock(
            returncode=0, stdout="Environment cached", stderr=""
        )

        env = UVEnvironment(python="3.10")

        await env.prepare(tmp_path / "test-env")

        # Should run multiple uv commands for initializing the project
        assert mock_run.call_count > 0

        # Check the first call should be uv init
        first_call_args = mock_run.call_args_list[0][0][0]
        assert first_call_args[0] == "uv"
        assert "init" in first_call_args

    @patch("subprocess.run")
    @patch("shutil.which")
    async def test_prepare_with_dependencies(self, mock_which, mock_run, tmp_path):
        """Test that prepare() includes dependencies."""
        mock_which.return_value = "/usr/bin/uv"
        mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")

        env = UVEnvironment(dependencies=["numpy", "pandas"])

        await env.prepare(tmp_path / "test-env")

        # Should run multiple uv commands, one of which should be uv add
        assert mock_run.call_count > 0

        # Find the add command call
        add_call = None
        for call_args, _ in mock_run.call_args_list:
            args = call_args[0]
            if "add" in args:
                add_call = args
                break

        assert add_call is not None, "Should have called uv add"
        assert "numpy" in add_call
        assert "pandas" in add_call
        assert "fastmcp" in add_call  # Always added

    @patch("subprocess.run")
    @patch("shutil.which")
    async def test_prepare_command_fails(self, mock_which, mock_run, tmp_path):
        """Test that prepare() raises error when uv command fails."""
        mock_which.return_value = "/usr/bin/uv"
        mock_run.side_effect = subprocess.CalledProcessError(
            1, ["uv"], stderr="Package not found"
        )

        env = UVEnvironment(python="3.10")

        with pytest.raises(RuntimeError, match="Failed to initialize project"):
            await env.prepare(tmp_path / "test-env")


class TestProjectPrepareCommand:
    """Test the CLI project prepare command."""

    @patch("fastmcp.utilities.mcp_server_config.MCPServerConfig.from_file")
    @patch("fastmcp.utilities.mcp_server_config.MCPServerConfig.find_config")
    async def test_project_prepare_auto_detect(self, mock_find, mock_from_file):
        """Test project prepare with auto-detected config."""
        from fastmcp.cli.cli import prepare

        # Setup mocks
        mock_find.return_value = Path("fastmcp.json")
        mock_config = AsyncMock()
        mock_from_file.return_value = mock_config

        # Run command with output_dir
        with patch("sys.exit"):
            with patch("fastmcp.cli.cli.console.print") as mock_print:
                await prepare(config_path=None, output_dir="./test-env")

        # Should find and load config
        mock_find.assert_called_once()
        mock_from_file.assert_called_once_with(Path("fastmcp.json"))

        # Should call prepare with output_dir
        mock_config.prepare.assert_called_once_with(
            skip_source=False,
            output_dir=Path("./test-env"),
        )

        # Should print success message
        mock_print.assert_called()
        success_call = mock_print.call_args_list[-1][0][0]
        assert "Project prepared successfully" in success_call

    @patch("pathlib.Path.exists")
    @patch("fastmcp.utilities.mcp_server_config.MCPServerConfig.from_file")
    async def test_project_prepare_explicit_path(self, mock_from_file, mock_exists):
        """Test project prepare with explicit config path."""
        from fastmcp.cli.cli import prepare

        # Setup mocks
        mock_exists.return_value = True
        mock_config = AsyncMock()
        mock_from_file.return_value = mock_config

        # Run command with explicit path
        with patch("fastmcp.cli.cli.console.print"):
            await prepare(config_path="myconfig.json", output_dir="./test-env")

        # Should load specified config
        mock_from_file.assert_called_once_with(Path("myconfig.json"))

        # Should call prepare
        mock_config.prepare.assert_called_once_with(
            skip_source=False,
            output_dir=Path("./test-env"),
        )

    @patch("fastmcp.utilities.mcp_server_config.MCPServerConfig.find_config")
    async def test_project_prepare_no_config_found(self, mock_find):
        """Test project prepare when no config is found."""
        from fastmcp.cli.cli import prepare

        # Setup mocks
        mock_find.return_value = None

        # Run command without output_dir - should exit with error for missing output_dir
        with pytest.raises(SystemExit) as exc_info:
            with patch("fastmcp.cli.cli.logger.error") as mock_error:
                await prepare(config_path=None, output_dir=None)

        assert exc_info.value.code == 1
        mock_error.assert_called()
        error_msg = mock_error.call_args[0][0]
        assert "--output-dir parameter is required" in error_msg

    @patch("pathlib.Path.exists")
    async def test_project_prepare_config_not_exists(self, mock_exists):
        """Test project prepare when specified config doesn't exist."""
        from fastmcp.cli.cli import prepare

        # Setup mocks
        mock_exists.return_value = False

        # Run command without output_dir - should exit with error for missing output_dir
        with pytest.raises(SystemExit) as exc_info:
            with patch("fastmcp.cli.cli.logger.error") as mock_error:
                await prepare(config_path="missing.json", output_dir=None)

        assert exc_info.value.code == 1
        mock_error.assert_called()
        error_msg = mock_error.call_args[0][0]
        assert "--output-dir parameter is required" in error_msg

    @patch("pathlib.Path.exists")
    @patch("fastmcp.utilities.mcp_server_config.MCPServerConfig.from_file")
    async def test_project_prepare_failure(self, mock_from_file, mock_exists):
        """Test project prepare when prepare() fails."""
        from fastmcp.cli.cli import prepare

        # Setup mocks
        mock_exists.return_value = True
        mock_config = AsyncMock()
        mock_config.prepare.side_effect = RuntimeError("Preparation failed")
        mock_from_file.return_value = mock_config

        # Run command - should exit with error
        with pytest.raises(SystemExit) as exc_info:
            with patch("fastmcp.cli.cli.console.print") as mock_print:
                await prepare(config_path="config.json", output_dir="./test-env")

        assert exc_info.value.code == 1
        # Should print error message
        error_call = mock_print.call_args_list[-1][0][0]
        assert "Failed to prepare project" in error_call
