"""Tests for environment manager."""

import sys
import pytest
from pathlib import Path
from unittest.mock import patch, MagicMock

from skop_runner.env_manager import EnvironmentManager


@pytest.fixture
def env_manager(tmp_path):
    """Create environment manager with temp directory."""
    return EnvironmentManager(str(tmp_path))


@pytest.fixture
def env_manager_no_root():
    """Create environment manager without project root."""
    return EnvironmentManager()


class TestEnvironmentDetection:
    """Tests for environment detection."""

    def test_detect_local_venv(self, env_manager, tmp_path):
        """Test detection of local .venv directory."""
        # Create fake venv structure
        venv_dir = tmp_path / ".venv"
        venv_dir.mkdir()
        (venv_dir / "pyvenv.cfg").touch()
        bin_dir = venv_dir / "bin"
        bin_dir.mkdir()
        python = bin_dir / "python"
        python.touch()
        python.chmod(0o755)

        with patch.object(env_manager, "_get_python_version", return_value="3.11.0"):
            envs = env_manager.detect_environments()

        venv_envs = [e for e in envs if e["type"] == "venv"]
        assert len(venv_envs) >= 1
        assert venv_envs[0]["name"] == ".venv"
        assert venv_envs[0]["python_version"] == "3.11.0"

    def test_detect_multiple_venvs(self, env_manager, tmp_path):
        """Test detection of multiple venv directories."""
        for name in [".venv", "venv"]:
            venv_dir = tmp_path / name
            venv_dir.mkdir()
            (venv_dir / "pyvenv.cfg").touch()
            bin_dir = venv_dir / "bin"
            bin_dir.mkdir()
            python = bin_dir / "python"
            python.touch()
            python.chmod(0o755)

        with patch.object(env_manager, "_get_python_version", return_value="3.10.0"):
            envs = env_manager.detect_environments()

        venv_envs = [e for e in envs if e["type"] == "venv"]
        assert len(venv_envs) == 2
        names = {e["name"] for e in venv_envs}
        assert ".venv" in names
        assert "venv" in names

    def test_detect_nested_venv(self, env_manager, tmp_path):
        """Test detection of venvs in subdirectories."""
        # Create nested structure like monorepo
        subproject = tmp_path / "data-science"
        subproject.mkdir()
        venv_dir = subproject / ".venv"
        venv_dir.mkdir()
        (venv_dir / "pyvenv.cfg").touch()
        bin_dir = venv_dir / "bin"
        bin_dir.mkdir()
        python = bin_dir / "python"
        python.touch()
        python.chmod(0o755)

        with patch.object(env_manager, "_get_python_version", return_value="3.11.0"):
            envs = env_manager.detect_environments()

        venv_envs = [e for e in envs if e["type"] == "venv"]
        nested = [e for e in venv_envs if "data-science" in e["name"]]
        assert len(nested) == 1
        assert nested[0]["name"] == "data-science/.venv"

    def test_detect_system_python_or_kernelspec(self, env_manager):
        """Test that system Python or equivalent kernelspec is detected."""
        envs = env_manager.detect_environments()

        # System Python might be deduplicated if a kernelspec already has the same path
        # Either way, we should have at least one Python environment
        python_envs = [
            e for e in envs
            if e.get("language") == "python" or e["type"] in ("system", "kernelspec")
        ]
        assert len(python_envs) >= 1
        # At least one should have the current Python's path
        paths = [e.get("python_path") for e in envs if e.get("python_path")]
        assert sys.executable in paths or any(
            e["type"] == "system" for e in envs
        )

    def test_conda_not_installed(self, env_manager):
        """Test graceful handling when conda is not installed."""
        with patch("subprocess.run", side_effect=FileNotFoundError):
            envs = env_manager.detect_environments()

        # Should still return something (kernelspecs or system python)
        assert len(envs) >= 1

    def test_conda_filters_other_project_envs(self, env_manager, tmp_path):
        """Test that conda envs from other projects are filtered out."""
        import json as json_module

        # Mock conda info response with:
        # - base env (should be included)
        # - global env in envs_dirs (should be included)
        # - env in current project (should be included)
        # - env in another project (should be EXCLUDED)
        mock_conda_info = {
            "conda_prefix": "/opt/anaconda3",
            "envs_dirs": ["/opt/anaconda3/envs"],
            "envs": [
                "/opt/anaconda3",  # base
                "/opt/anaconda3/envs/data-science",  # global env
                str(tmp_path / ".conda-env"),  # in current project
                "/Users/other/projects/other-project/.conda-env",  # other project
            ],
        }

        # Create the local conda env structure
        local_conda = tmp_path / ".conda-env"
        local_conda.mkdir()
        bin_dir = local_conda / "bin"
        bin_dir.mkdir()
        python = bin_dir / "python"
        python.touch()

        def mock_subprocess_run(cmd, **kwargs):
            result = MagicMock()
            if "conda" in cmd:
                result.returncode = 0
                result.stdout = json_module.dumps(mock_conda_info)
            return result

        with patch("subprocess.run", side_effect=mock_subprocess_run):
            with patch.object(env_manager, "_get_python_version", return_value="3.10.0"):
                with patch.object(env_manager, "_check_has_ipykernel", return_value=True):
                    # Mock the python existence check for global envs
                    original_exists = Path.exists
                    def mock_exists(self):
                        path_str = str(self)
                        if "anaconda3" in path_str or str(tmp_path) in path_str:
                            return True
                        return original_exists(self)

                    with patch.object(Path, "exists", mock_exists):
                        envs = env_manager._find_conda_envs()

        # Should have base, data-science, and local .conda-env
        # Should NOT have the other-project env
        env_paths = [e["path"] for e in envs]
        assert "/opt/anaconda3" in env_paths  # base
        assert "/opt/anaconda3/envs/data-science" in env_paths  # global
        assert str(tmp_path / ".conda-env") in env_paths  # local
        assert "/Users/other/projects/other-project/.conda-env" not in env_paths  # filtered

    def test_no_root_still_detects_envs(self, env_manager_no_root):
        """Test environment detection works without project root."""
        envs = env_manager_no_root.detect_environments()

        # Should still find kernelspecs or system Python
        assert len(envs) >= 1


class TestSetProjectRoot:
    """Tests for dynamically setting project root."""

    def test_set_root(self, env_manager_no_root, tmp_path):
        """Test setting project root after initialization."""
        assert env_manager_no_root.project_root is None

        env_manager_no_root.set_project_root(str(tmp_path))

        assert env_manager_no_root.project_root == tmp_path

    def test_set_root_enables_venv_detection(self, env_manager_no_root, tmp_path):
        """Test that setting root enables local venv detection."""
        # Create venv
        venv_dir = tmp_path / ".venv"
        venv_dir.mkdir()
        (venv_dir / "pyvenv.cfg").touch()
        bin_dir = venv_dir / "bin"
        bin_dir.mkdir()
        python = bin_dir / "python"
        python.touch()

        # Initially no venvs (no root)
        envs_before = env_manager_no_root.detect_environments()
        venv_before = [e for e in envs_before if e["type"] == "venv"]
        assert len(venv_before) == 0

        # Set root
        env_manager_no_root.set_project_root(str(tmp_path))

        # Now should find venv
        with patch.object(
            env_manager_no_root, "_get_python_version", return_value="3.11.0"
        ):
            envs_after = env_manager_no_root.detect_environments()

        venv_after = [e for e in envs_after if e["type"] == "venv"]
        assert len(venv_after) >= 1


class TestGetEnvPython:
    """Tests for getting Python executable path."""

    def test_get_venv_python_unix(self, env_manager, tmp_path):
        """Test getting Python path from Unix venv."""
        venv = tmp_path / "testvenv"
        venv.mkdir()
        bin_dir = venv / "bin"
        bin_dir.mkdir()
        python = bin_dir / "python"
        python.touch()

        result = env_manager.get_env_python(str(venv))
        assert result == str(python)

    def test_get_invalid_env(self, env_manager):
        """Test that invalid env path returns None."""
        result = env_manager.get_env_python("/nonexistent/path")
        assert result is None


class TestGetInstalledPackages:
    """Tests for listing installed packages."""

    def test_get_packages_no_args_returns_empty(self, env_manager):
        """Test that calling with no args returns empty list."""
        result = env_manager.get_installed_packages()
        assert result == []

    def test_get_packages_invalid_env_path(self, env_manager):
        """Test that invalid env_path returns empty list."""
        result = env_manager.get_installed_packages(env_path="/nonexistent/path")
        assert result == []

    def test_get_packages_with_env_path(self, env_manager, tmp_path):
        """Test successful package listing via env_path."""
        # Create fake venv
        venv = tmp_path / "testvenv"
        venv.mkdir()
        bin_dir = venv / "bin"
        bin_dir.mkdir()
        python = bin_dir / "python"
        python.touch()

        mock_output = '[{"name": "pip", "version": "23.0"}, {"name": "setuptools", "version": "67.0"}]'

        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0, stdout=mock_output)
            result = env_manager.get_installed_packages(env_path=str(venv))

        assert len(result) == 2
        assert result[0]["name"] == "pip"
        assert result[1]["name"] == "setuptools"

    def test_get_packages_with_python_path(self, env_manager):
        """Test package listing via direct python_path (system Python, kernelspecs, etc.)."""
        mock_output = '[{"name": "numpy", "version": "1.24.0"}]'

        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0, stdout=mock_output)
            result = env_manager.get_installed_packages(python_path="/usr/bin/python3")

        assert len(result) == 1
        assert result[0]["name"] == "numpy"
        # Verify the correct Python was called
        mock_run.assert_called_once()
        call_args = mock_run.call_args[0][0]
        assert call_args[0] == "/usr/bin/python3"

    def test_get_packages_python_path_takes_precedence(self, env_manager, tmp_path):
        """Test that python_path takes precedence over env_path."""
        # Create a venv that would resolve to a different Python
        venv = tmp_path / "testvenv"
        venv.mkdir()
        bin_dir = venv / "bin"
        bin_dir.mkdir()
        venv_python = bin_dir / "python"
        venv_python.touch()

        mock_output = '[{"name": "requests", "version": "2.28.0"}]'

        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0, stdout=mock_output)
            result = env_manager.get_installed_packages(
                env_path=str(venv),
                python_path="/custom/python3",
            )

        # Should use python_path, not env_path
        call_args = mock_run.call_args[0][0]
        assert call_args[0] == "/custom/python3"
        assert len(result) == 1

    def test_get_packages_timeout(self, env_manager):
        """Test handling of pip timeout."""
        import subprocess

        with patch("subprocess.run", side_effect=subprocess.TimeoutExpired("pip", 30)):
            result = env_manager.get_installed_packages(python_path="/usr/bin/python3")

        assert result == []

    def test_get_packages_python_not_found(self, env_manager):
        """Test handling when Python executable doesn't exist."""
        with patch("subprocess.run", side_effect=FileNotFoundError):
            result = env_manager.get_installed_packages(python_path="/nonexistent/python")

        assert result == []


class TestIpykernelDetection:
    """Tests for ipykernel detection and installation."""

    def test_has_ipykernel_in_venv_response(self, env_manager, tmp_path):
        """Test that venv detection includes has_ipykernel field."""
        venv_dir = tmp_path / ".venv"
        venv_dir.mkdir()
        (venv_dir / "pyvenv.cfg").touch()
        bin_dir = venv_dir / "bin"
        bin_dir.mkdir()
        python = bin_dir / "python"
        python.touch()
        python.chmod(0o755)

        with patch.object(env_manager, "_get_python_version", return_value="3.11.0"):
            with patch.object(env_manager, "_check_has_ipykernel", return_value=True):
                envs = env_manager.detect_environments()

        venv_envs = [e for e in envs if e["type"] == "venv"]
        assert len(venv_envs) >= 1
        assert "has_ipykernel" in venv_envs[0]
        assert venv_envs[0]["has_ipykernel"] is True

    def test_has_ipykernel_false_when_missing(self, env_manager, tmp_path):
        """Test that has_ipykernel is False when ipykernel not installed."""
        venv_dir = tmp_path / ".venv"
        venv_dir.mkdir()
        (venv_dir / "pyvenv.cfg").touch()
        bin_dir = venv_dir / "bin"
        bin_dir.mkdir()
        python = bin_dir / "python"
        python.touch()
        python.chmod(0o755)

        with patch.object(env_manager, "_get_python_version", return_value="3.11.0"):
            with patch.object(env_manager, "_check_has_ipykernel", return_value=False):
                envs = env_manager.detect_environments()

        venv_envs = [e for e in envs if e["type"] == "venv"]
        assert len(venv_envs) >= 1
        assert venv_envs[0]["has_ipykernel"] is False

    def test_system_python_has_ipykernel_field(self, env_manager):
        """Test that system Python includes has_ipykernel field."""
        with patch.object(env_manager, "_check_has_ipykernel", return_value=True):
            envs = env_manager.detect_environments()

        system_envs = [e for e in envs if e["type"] == "system"]
        if system_envs:
            assert "has_ipykernel" in system_envs[0]

    @pytest.mark.asyncio
    async def test_install_ipykernel_success(self, env_manager):
        """Test successful ipykernel installation."""
        mock_process = MagicMock()
        mock_process.returncode = 0
        mock_process.communicate = MagicMock(return_value=(b"", b""))

        # Mock Path.exists() and Path.is_file() for validation
        mock_path = MagicMock()
        mock_path.exists.return_value = True
        mock_path.is_file.return_value = True

        # Mock subprocess.run for Python version check
        mock_version_result = MagicMock()
        mock_version_result.returncode = 0
        mock_version_result.stdout = "Python 3.12.0"
        mock_version_result.stderr = ""

        with patch("skop_runner.env_manager.Path", return_value=mock_path):
            with patch("skop_runner.env_manager.subprocess.run", return_value=mock_version_result):
                with patch("asyncio.create_subprocess_exec", return_value=mock_process):
                    with patch("asyncio.wait_for", return_value=(b"", b"")):
                        result = await env_manager.install_ipykernel("/path/to/python")

        assert result["success"] is True
        assert "ipykernel installed" in result["message"]

    @pytest.mark.asyncio
    async def test_install_ipykernel_failure(self, env_manager):
        """Test failed ipykernel installation."""
        mock_process = MagicMock()
        mock_process.returncode = 1
        mock_process.communicate = MagicMock(return_value=(b"", b"pip error"))

        # Mock Path.exists() and Path.is_file() for validation
        mock_path = MagicMock()
        mock_path.exists.return_value = True
        mock_path.is_file.return_value = True

        # Mock subprocess.run for Python version check
        mock_version_result = MagicMock()
        mock_version_result.returncode = 0
        mock_version_result.stdout = "Python 3.12.0"
        mock_version_result.stderr = ""

        with patch("skop_runner.env_manager.Path", return_value=mock_path):
            with patch("skop_runner.env_manager.subprocess.run", return_value=mock_version_result):
                with patch("asyncio.create_subprocess_exec", return_value=mock_process):
                    with patch("asyncio.wait_for", return_value=(b"", b"pip error")):
                        result = await env_manager.install_ipykernel("/path/to/python")

        assert result["success"] is False
        assert "Failed" in result["message"]

    @pytest.mark.asyncio
    async def test_install_ipykernel_timeout(self, env_manager):
        """Test ipykernel installation timeout."""
        import asyncio

        # Mock Path.exists() and Path.is_file() for validation
        mock_path = MagicMock()
        mock_path.exists.return_value = True
        mock_path.is_file.return_value = True

        # Mock subprocess.run for Python version check
        mock_version_result = MagicMock()
        mock_version_result.returncode = 0
        mock_version_result.stdout = "Python 3.12.0"
        mock_version_result.stderr = ""

        with patch("skop_runner.env_manager.Path", return_value=mock_path):
            with patch("skop_runner.env_manager.subprocess.run", return_value=mock_version_result):
                with patch("asyncio.create_subprocess_exec"):
                    with patch("asyncio.wait_for", side_effect=asyncio.TimeoutError):
                        result = await env_manager.install_ipykernel("/path/to/python")

        assert result["success"] is False
        assert "timed out" in result["message"]

    @pytest.mark.asyncio
    async def test_install_ipykernel_invalid_python(self, env_manager):
        """Test ipykernel installation with invalid Python path."""
        result = await env_manager.install_ipykernel("/nonexistent/python")

        assert result["success"] is False
        assert "not found" in result["message"].lower()


class TestFindSystemPython:
    """Tests for _find_system_python method."""

    def test_excludes_venv_python_from_path(self, env_manager, tmp_path):
        """Test that venv Python found via PATH is not returned as system Python."""
        # Create a fake venv structure
        venv_dir = tmp_path / ".venv"
        bin_dir = venv_dir / "bin"
        bin_dir.mkdir(parents=True)
        venv_python = bin_dir / "python3"
        venv_python.touch()
        (venv_dir / "pyvenv.cfg").touch()  # Mark as venv

        # Track original exists/is_file for selective mocking
        original_exists = Path.exists
        original_is_file = Path.is_file

        def mock_exists(self):
            # Return False for explicit system paths, True for tmp_path files
            if str(self) in ["/usr/bin/python3", "/usr/local/bin/python3", "/opt/homebrew/bin/python3"]:
                return False
            return original_exists(self)

        def mock_is_file(self):
            if str(self) in ["/usr/bin/python3", "/usr/local/bin/python3", "/opt/homebrew/bin/python3"]:
                return False
            return original_is_file(self)

        # Mock shutil.which to return the venv Python, and explicit paths to not exist
        with patch.object(Path, "exists", mock_exists):
            with patch.object(Path, "is_file", mock_is_file):
                with patch("shutil.which", return_value=str(venv_python)):
                    result = env_manager._find_system_python()

        # Should not return venv Python since pyvenv.cfg exists
        assert result is None

    def test_excludes_conda_python_from_path(self, env_manager, tmp_path):
        """Test that conda env Python found via PATH is not returned as system Python."""
        # Create a fake conda env structure
        conda_dir = tmp_path / "conda-env"
        bin_dir = conda_dir / "bin"
        bin_dir.mkdir(parents=True)
        conda_python = bin_dir / "python3"
        conda_python.touch()
        (conda_dir / "conda-meta").mkdir()  # Mark as conda env

        original_exists = Path.exists
        original_is_file = Path.is_file

        def mock_exists(self):
            if str(self) in ["/usr/bin/python3", "/usr/local/bin/python3", "/opt/homebrew/bin/python3"]:
                return False
            return original_exists(self)

        def mock_is_file(self):
            if str(self) in ["/usr/bin/python3", "/usr/local/bin/python3", "/opt/homebrew/bin/python3"]:
                return False
            return original_is_file(self)

        # Mock shutil.which to return the conda Python
        with patch.object(Path, "exists", mock_exists):
            with patch.object(Path, "is_file", mock_is_file):
                with patch("shutil.which", return_value=str(conda_python)):
                    result = env_manager._find_system_python()

        # Should not return conda Python since conda-meta exists
        assert result is None

    def test_returns_explicit_system_path_if_exists(self, env_manager):
        """Test that explicit system paths are checked first."""
        def mock_exists(self):
            return str(self) == "/usr/bin/python3"

        def mock_is_file(self):
            return str(self) == "/usr/bin/python3"

        with patch.object(Path, "exists", mock_exists):
            with patch.object(Path, "is_file", mock_is_file):
                result = env_manager._find_system_python()

        assert result is not None
        assert str(result) == "/usr/bin/python3"

    def test_returns_none_when_no_system_python_found(self, env_manager):
        """Test that None is returned when no system Python is found."""
        with patch.object(Path, "exists", return_value=False):
            with patch("shutil.which", return_value=None):
                result = env_manager._find_system_python()

        assert result is None

    def test_returns_path_python_when_not_in_venv(self, env_manager, tmp_path):
        """Test that a PATH Python is returned when not in a venv/conda env."""
        # Create a fake system-like Python (no pyvenv.cfg or conda-meta)
        sys_dir = tmp_path / "usr" / "local" / "bin"
        sys_dir.mkdir(parents=True)
        sys_python = sys_dir / "python3"
        sys_python.touch()

        original_exists = Path.exists
        original_is_file = Path.is_file

        def mock_exists(self):
            if str(self) in ["/usr/bin/python3", "/usr/local/bin/python3", "/opt/homebrew/bin/python3"]:
                return False
            return original_exists(self)

        def mock_is_file(self):
            if str(self) in ["/usr/bin/python3", "/usr/local/bin/python3", "/opt/homebrew/bin/python3"]:
                return False
            return original_is_file(self)

        with patch.object(Path, "exists", mock_exists):
            with patch.object(Path, "is_file", mock_is_file):
                with patch("shutil.which", return_value=str(sys_python)):
                    result = env_manager._find_system_python()

        # Should return the PATH Python since it's not in a venv/conda
        assert result is not None
        assert str(result) == str(sys_python.resolve())


class TestGetAvailableEnvironments:
    """Tests for get_available_environments (VS Code-style detection)."""

    def test_display_name_includes_python_version(self, env_manager, tmp_path):
        """Display names include Python version like VS Code."""
        venv_dir = tmp_path / ".venv"
        venv_dir.mkdir()
        (venv_dir / "pyvenv.cfg").touch()
        bin_dir = venv_dir / "bin"
        bin_dir.mkdir()
        python = bin_dir / "python"
        python.touch()
        python.chmod(0o755)

        with patch.object(env_manager, "_get_python_version", return_value="3.11.5"):
            with patch.object(env_manager, "_check_has_ipykernel", return_value=True):
                envs = env_manager.get_available_environments()

        venv_envs = [e for e in envs if e["type"] == "venv"]
        assert len(venv_envs) >= 1
        # Check display name format: "Python 3.11.5 (.venv)"
        assert "Python 3.11.5" in venv_envs[0]["display_name"]
        assert ".venv" in venv_envs[0]["display_name"]

    def test_excludes_python_kernelspecs(self, env_manager):
        """Python kernelspecs are NOT included (VS Code behavior)."""
        mock_kernelspecs = {
            "kernelspecs": {
                "python3": {
                    "spec": {
                        "display_name": "Python 3",
                        "language": "python",
                        "argv": ["/usr/bin/python3", "-m", "ipykernel_launcher"],
                    }
                },
            }
        }

        envs = env_manager.get_available_environments(kernelspecs=mock_kernelspecs)

        # Should NOT include the python3 kernelspec
        kernelspec_envs = [e for e in envs if e.get("type", "").startswith("kernelspec")]
        python_kernelspecs = [e for e in kernelspec_envs if "python" in e.get("language", "")]
        assert len(python_kernelspecs) == 0

    def test_includes_non_python_kernelspecs(self, env_manager):
        """Non-Python kernelspecs (R, Julia) ARE included."""
        mock_kernelspecs = {
            "kernelspecs": {
                "ir": {
                    "spec": {
                        "display_name": "R",
                        "language": "R",
                        "argv": ["/usr/bin/R"],
                    }
                },
                "julia-1.9": {
                    "spec": {
                        "display_name": "Julia 1.9",
                        "language": "julia",
                        "argv": ["/usr/bin/julia"],
                    }
                },
                "python3": {
                    "spec": {
                        "display_name": "Python 3",
                        "language": "python",
                        "argv": ["/usr/bin/python3"],
                    }
                },
            }
        }

        envs = env_manager.get_available_environments(kernelspecs=mock_kernelspecs)

        non_python = [e for e in envs if e.get("language") and e["language"] != "python"]
        assert len(non_python) == 2
        names = {e["name"] for e in non_python}
        assert "ir" in names
        assert "julia-1.9" in names

    def test_skips_ephemeral_kernelspecs(self, env_manager):
        """skop-* ephemeral kernelspecs are excluded."""
        mock_kernelspecs = {
            "kernelspecs": {
                "skop-abc123": {
                    "spec": {
                        "display_name": "Python (.venv)",
                        "language": "python",
                        "argv": ["/home/user/.venv/bin/python"],
                    }
                },
            }
        }

        envs = env_manager.get_available_environments(kernelspecs=mock_kernelspecs)

        skop_envs = [e for e in envs if e["name"].startswith("skop-")]
        assert len(skop_envs) == 0

    def test_handles_none_kernelspecs(self, env_manager):
        """Works when kernelspecs is None."""
        envs = env_manager.get_available_environments(kernelspecs=None)
        # Should still return detected environments
        assert isinstance(envs, list)

    def test_deduplicates_symlinked_environments(self, env_manager, tmp_path):
        """Environments resolving to same Python are deduplicated."""
        # Create a venv
        venv_dir = tmp_path / ".venv"
        venv_dir.mkdir()
        (venv_dir / "pyvenv.cfg").touch()
        bin_dir = venv_dir / "bin"
        bin_dir.mkdir()
        python = bin_dir / "python"
        python.touch()
        python.chmod(0o755)

        # Create a symlink to the same venv
        symlink_venv = tmp_path / "venv"
        symlink_venv.symlink_to(venv_dir)

        with patch.object(env_manager, "_get_python_version", return_value="3.11.0"):
            with patch.object(env_manager, "_check_has_ipykernel", return_value=True):
                envs = env_manager.get_available_environments()

        # Even though both .venv and venv exist, only one should be in the results
        venv_envs = [e for e in envs if e["type"] == "venv"]
        # The deduplication happens by resolved path
        paths = [e["python_path"] for e in venv_envs]
        resolved_paths = [str(Path(p).resolve()) for p in paths]
        # All resolved paths should be unique
        assert len(resolved_paths) == len(set(resolved_paths))

    def test_kernel_name_is_none_for_python_envs(self, env_manager, tmp_path):
        """Python environments have kernel_name=None (use ephemeral registration)."""
        venv_dir = tmp_path / ".venv"
        venv_dir.mkdir()
        (venv_dir / "pyvenv.cfg").touch()
        bin_dir = venv_dir / "bin"
        bin_dir.mkdir()
        python = bin_dir / "python"
        python.touch()
        python.chmod(0o755)

        with patch.object(env_manager, "_get_python_version", return_value="3.11.0"):
            with patch.object(env_manager, "_check_has_ipykernel", return_value=True):
                envs = env_manager.get_available_environments()

        venv_envs = [e for e in envs if e["type"] == "venv"]
        assert len(venv_envs) >= 1
        assert venv_envs[0]["kernel_name"] is None

    def test_language_field_is_set(self, env_manager, tmp_path):
        """Python environments have language='python'."""
        venv_dir = tmp_path / ".venv"
        venv_dir.mkdir()
        (venv_dir / "pyvenv.cfg").touch()
        bin_dir = venv_dir / "bin"
        bin_dir.mkdir()
        python = bin_dir / "python"
        python.touch()
        python.chmod(0o755)

        with patch.object(env_manager, "_get_python_version", return_value="3.11.0"):
            with patch.object(env_manager, "_check_has_ipykernel", return_value=True):
                envs = env_manager.get_available_environments()

        venv_envs = [e for e in envs if e["type"] == "venv"]
        assert len(venv_envs) >= 1
        assert venv_envs[0]["language"] == "python"


class TestFormatDisplayName:
    """Tests for _format_display_name method."""

    def test_venv_format(self, env_manager):
        """Test venv display name format."""
        result = env_manager._format_display_name("venv", ".venv", "3.11.5")
        assert result == "Python 3.11.5 (.venv)"

    def test_conda_format(self, env_manager):
        """Test conda display name format."""
        result = env_manager._format_display_name("conda", "base (conda)", "3.10.0")
        assert result == "Python 3.10.0 (conda: base)"

        result = env_manager._format_display_name("conda", "myenv (conda)", "3.10.0")
        assert result == "Python 3.10.0 (conda: myenv)"

    def test_pyenv_format(self, env_manager):
        """Test pyenv display name format."""
        result = env_manager._format_display_name("pyenv", "3.12.0", "3.12.0")
        assert result == "Python 3.12.0 (pyenv)"

    def test_pyenv_virtualenv_format(self, env_manager):
        """Test pyenv-virtualenv display name format."""
        result = env_manager._format_display_name("pyenv-virtualenv", "my-project", "3.11.0")
        assert result == "Python 3.11.0 (pyenv: my-project)"

    def test_poetry_format(self, env_manager):
        """Test poetry display name format."""
        result = env_manager._format_display_name("poetry", "poetry", "3.11.0")
        assert result == "Python 3.11.0 (poetry)"

    def test_pipenv_format(self, env_manager):
        """Test pipenv display name format."""
        result = env_manager._format_display_name("pipenv", "pipenv", "3.9.0")
        assert result == "Python 3.9.0 (pipenv)"

    def test_system_format(self, env_manager):
        """Test system display name format."""
        result = env_manager._format_display_name("system", "System Python", "3.11.0")
        assert result == "Python 3.11.0 (system)"

    def test_unknown_version(self, env_manager):
        """Test format with unknown version."""
        result = env_manager._format_display_name("venv", ".venv", None)
        assert result == "Python unknown (.venv)"


class TestPoetryDetection:
    """Tests for Poetry environment detection."""

    def test_detect_poetry_in_project_venv(self, env_manager, tmp_path):
        """Detect Poetry in-project .venv when pyproject.toml exists."""
        # Create pyproject.toml
        pyproject = tmp_path / "pyproject.toml"
        pyproject.write_text("[tool.poetry]\nname = 'test'\n")

        # Create .venv
        venv_dir = tmp_path / ".venv"
        venv_dir.mkdir()
        bin_dir = venv_dir / "bin"
        bin_dir.mkdir()
        python = bin_dir / "python"
        python.touch()
        python.chmod(0o755)

        with patch.object(env_manager, "_get_python_version", return_value="3.11.0"):
            with patch.object(env_manager, "_check_has_ipykernel", return_value=True):
                envs = env_manager._find_poetry_envs()

        assert len(envs) == 1
        assert envs[0]["type"] == "poetry"
        assert envs[0]["name"] == "poetry"

    def test_no_pyproject_toml(self, env_manager, tmp_path):
        """No Poetry env detected without pyproject.toml."""
        envs = env_manager._find_poetry_envs()
        assert len(envs) == 0

    def test_poetry_not_installed(self, env_manager, tmp_path):
        """Graceful handling when poetry not installed."""
        # Create pyproject.toml
        pyproject = tmp_path / "pyproject.toml"
        pyproject.write_text("[tool.poetry]\nname = 'test'\n")

        # No .venv, poetry command will fail
        with patch("subprocess.run", side_effect=FileNotFoundError):
            envs = env_manager._find_poetry_envs()

        assert len(envs) == 0


class TestPipenvDetection:
    """Tests for Pipenv environment detection."""

    def test_no_pipfile(self, env_manager, tmp_path):
        """No Pipenv env detected without Pipfile."""
        envs = env_manager._find_pipenv_envs()
        assert len(envs) == 0

    def test_pipenv_not_installed(self, env_manager, tmp_path):
        """Graceful handling when pipenv not installed."""
        # Create Pipfile
        pipfile = tmp_path / "Pipfile"
        pipfile.write_text("[[source]]\nurl = 'https://pypi.org/simple'\n")

        with patch("subprocess.run", side_effect=FileNotFoundError):
            envs = env_manager._find_pipenv_envs()

        assert len(envs) == 0


class TestVirtualenvwrapperDetection:
    """Tests for virtualenvwrapper environment detection."""

    def test_detect_virtualenvwrapper_envs(self, env_manager, tmp_path, monkeypatch):
        """Detect environments in WORKON_HOME."""
        workon_home = tmp_path / ".virtualenvs"
        workon_home.mkdir()

        # Create a virtualenv
        venv_dir = workon_home / "myproject"
        venv_dir.mkdir()
        bin_dir = venv_dir / "bin"
        bin_dir.mkdir()
        python = bin_dir / "python"
        python.touch()
        python.chmod(0o755)

        monkeypatch.setenv("WORKON_HOME", str(workon_home))

        with patch.object(env_manager, "_get_python_version", return_value="3.10.0"):
            with patch.object(env_manager, "_check_has_ipykernel", return_value=False):
                envs = env_manager._find_virtualenvwrapper_envs()

        assert len(envs) == 1
        assert envs[0]["name"] == "myproject"
        assert envs[0]["type"] == "virtualenvwrapper"

    def test_no_workon_home(self, env_manager, tmp_path, monkeypatch):
        """No envs when WORKON_HOME doesn't exist."""
        nonexistent = tmp_path / "nonexistent"
        monkeypatch.setenv("WORKON_HOME", str(nonexistent))

        envs = env_manager._find_virtualenvwrapper_envs()
        assert len(envs) == 0


class TestPyenvDetection:
    """Tests for pyenv version detection."""

    def test_detect_pyenv_versions(self, env_manager, tmp_path, monkeypatch):
        """Detect Python versions in PYENV_ROOT."""
        pyenv_root = tmp_path / ".pyenv"
        versions_dir = pyenv_root / "versions"
        versions_dir.mkdir(parents=True)

        # Create a Python version
        version_dir = versions_dir / "3.11.0"
        version_dir.mkdir()
        bin_dir = version_dir / "bin"
        bin_dir.mkdir()
        python = bin_dir / "python"
        python.touch()
        python.chmod(0o755)

        monkeypatch.setenv("PYENV_ROOT", str(pyenv_root))

        with patch.object(env_manager, "_get_python_version", return_value="3.11.0"):
            with patch.object(env_manager, "_check_has_ipykernel", return_value=True):
                envs = env_manager._find_pyenv_versions()

        assert len(envs) == 1
        assert envs[0]["name"] == "3.11.0"
        assert envs[0]["type"] == "pyenv"

    def test_no_pyenv_root(self, env_manager, tmp_path, monkeypatch):
        """No envs when PYENV_ROOT doesn't exist."""
        nonexistent = tmp_path / "nonexistent"
        monkeypatch.setenv("PYENV_ROOT", str(nonexistent))

        envs = env_manager._find_pyenv_versions()
        assert len(envs) == 0

    def test_detect_pyenv_virtualenv(self, env_manager, tmp_path, monkeypatch):
        """Detect pyenv-virtualenv environments in {version}/envs/."""
        pyenv_root = tmp_path / ".pyenv"
        versions_dir = pyenv_root / "versions"

        # Create base Python version 3.11.0
        version_dir = versions_dir / "3.11.0"
        version_dir.mkdir(parents=True)
        bin_dir = version_dir / "bin"
        bin_dir.mkdir()
        python = bin_dir / "python"
        python.touch()
        python.chmod(0o755)

        # Create pyenv-virtualenv environment under 3.11.0/envs/my-project
        envs_dir = version_dir / "envs"
        venv_dir = envs_dir / "my-project"
        venv_dir.mkdir(parents=True)
        venv_bin_dir = venv_dir / "bin"
        venv_bin_dir.mkdir()
        venv_python = venv_bin_dir / "python"
        venv_python.touch()
        venv_python.chmod(0o755)

        monkeypatch.setenv("PYENV_ROOT", str(pyenv_root))

        with patch.object(env_manager, "_get_python_version", return_value="3.11.0"):
            with patch.object(env_manager, "_check_has_ipykernel", return_value=True):
                envs = env_manager._find_pyenv_versions()

        # Should detect both: pyenv-virtualenv first, then base Python version
        assert len(envs) == 2

        # First should be pyenv-virtualenv
        venv_env = next((e for e in envs if e["type"] == "pyenv-virtualenv"), None)
        assert venv_env is not None
        assert venv_env["name"] == "my-project"
        assert venv_env["type"] == "pyenv-virtualenv"

        # Second should be base pyenv version
        base_env = next((e for e in envs if e["type"] == "pyenv"), None)
        assert base_env is not None
        assert base_env["name"] == "3.11.0"
        assert base_env["type"] == "pyenv"
