"""
Tests for get_available_environments() - VS Code-style environment detection.

Tests the environment-first approach where:
- Python environments are detected and shown with version in display name
- Python kernelspecs are EXCLUDED (they duplicate detected environments)
- Non-Python kernelspecs (R, Julia) ARE included
- Python environments use ephemeral registration (kernel_name=None)
"""

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

from skop_runner.env_manager import EnvironmentManager


class TestGetAvailableEnvironments:
    """Tests for EnvironmentManager.get_available_environments()"""

    def test_no_kernelspecs_returns_local_envs(self, tmp_path):
        """When no kernelspecs provided, returns local envs with kernel_name=None."""
        # Create a mock venv
        venv_path = tmp_path / ".venv"
        venv_path.mkdir()
        (venv_path / "bin").mkdir()
        (venv_path / "bin" / "python").touch()
        (venv_path / "pyvenv.cfg").touch()

        env_mgr = EnvironmentManager(project_root=str(tmp_path))

        # Mock detect_environments to return predictable results
        with patch.object(env_mgr, 'detect_environments') as mock_detect:
            mock_detect.return_value = [
                {
                    "name": ".venv",
                    "display_name": ".venv",
                    "type": "venv",
                    "path": str(venv_path),
                    "python_path": str(venv_path / "bin" / "python"),
                    "python_version": "3.11.0",
                    "is_active": False,
                    "has_ipykernel": True,
                }
            ]

            result = env_mgr.get_available_environments(kernelspecs=None)

        assert len(result) == 1
        # Python envs use ephemeral registration, so kernel_name is None
        assert result[0]["kernel_name"] is None
        assert result[0]["name"] == ".venv"
        # Display name should include Python version
        assert "Python 3.11.0" in result[0]["display_name"]
        assert result[0]["language"] == "python"

    def test_empty_kernelspecs_returns_local_envs(self, tmp_path):
        """When kernelspecs is empty dict, returns local envs."""
        env_mgr = EnvironmentManager(project_root=str(tmp_path))

        with patch.object(env_mgr, 'detect_environments') as mock_detect:
            mock_detect.return_value = [
                {
                    "name": "System Python",
                    "display_name": "System Python",
                    "type": "system",
                    "path": "/usr/bin",
                    "python_path": "/usr/bin/python3",
                    "python_version": "3.10.0",
                    "is_active": True,
                    "has_ipykernel": True,
                }
            ]

            result = env_mgr.get_available_environments(kernelspecs={})

        assert len(result) == 1
        assert result[0]["kernel_name"] is None
        assert "Python 3.10.0" in result[0]["display_name"]

    def test_python_kernelspecs_excluded(self, tmp_path):
        """Python kernelspecs are excluded (they duplicate detected environments)."""
        venv_path = tmp_path / ".venv"
        venv_path.mkdir()
        (venv_path / "bin").mkdir()
        python_path = venv_path / "bin" / "python"
        python_path.touch()

        env_mgr = EnvironmentManager(project_root=str(tmp_path))

        # Kernelspec with Python language - should be EXCLUDED
        kernelspecs = {
            "kernelspecs": {
                "python3": {
                    "name": "python3",
                    "spec": {
                        "display_name": "Python 3",
                        "language": "python",
                        "argv": [str(python_path), "-m", "ipykernel_launcher", "-f", "{connection_file}"],
                    }
                }
            }
        }

        with patch.object(env_mgr, 'detect_environments') as mock_detect:
            mock_detect.return_value = [
                {
                    "name": ".venv",
                    "display_name": ".venv",
                    "type": "venv",
                    "path": str(venv_path),
                    "python_path": str(python_path),
                    "python_version": "3.11.0",
                    "is_active": False,
                    "has_ipykernel": True,
                }
            ]

            result = env_mgr.get_available_environments(kernelspecs=kernelspecs)

        # Only the detected venv, not the Python kernelspec
        assert len(result) == 1
        assert result[0]["name"] == ".venv"
        assert result[0]["type"] == "venv"

    def test_non_python_kernelspecs_included(self, tmp_path):
        """Non-Python kernelspecs (Julia, R) ARE included."""
        env_mgr = EnvironmentManager(project_root=str(tmp_path))

        kernelspecs = {
            "kernelspecs": {
                "julia-1.9": {
                    "name": "julia-1.9",
                    "spec": {
                        "display_name": "Julia 1.9",
                        "language": "julia",
                        "argv": ["/usr/bin/julia", "--kernel", "{connection_file}"],
                    }
                },
                "ir": {
                    "name": "ir",
                    "spec": {
                        "display_name": "R",
                        "language": "R",
                        "argv": ["/usr/bin/R", "--slave", "-e", "IRkernel::main()"],
                    }
                },
                "python3": {
                    "name": "python3",
                    "spec": {
                        "display_name": "Python 3",
                        "language": "python",
                        "argv": ["/usr/bin/python3", "-m", "ipykernel_launcher"],
                    }
                }
            }
        }

        with patch.object(env_mgr, 'detect_environments') as mock_detect:
            mock_detect.return_value = []

            result = env_mgr.get_available_environments(kernelspecs=kernelspecs)

        # Should have Julia and R, but NOT Python
        assert len(result) == 2

        julia_spec = next((e for e in result if e["name"] == "julia-1.9"), None)
        assert julia_spec is not None
        assert julia_spec["display_name"] == "Julia 1.9"
        assert julia_spec["type"] == "kernelspec:julia"
        assert julia_spec["kernel_name"] == "julia-1.9"
        assert julia_spec["language"] == "julia"

        r_spec = next((e for e in result if e["name"] == "ir"), None)
        assert r_spec is not None
        assert r_spec["display_name"] == "R"
        assert r_spec["type"] == "kernelspec:r"
        assert r_spec["kernel_name"] == "ir"

        # Python kernelspec should NOT be in results
        python_spec = next((e for e in result if e["name"] == "python3"), None)
        assert python_spec is None

    def test_skop_ephemeral_specs_excluded(self, tmp_path):
        """Ephemeral specs (skop-*) are excluded from results."""
        env_mgr = EnvironmentManager(project_root=str(tmp_path))

        kernelspecs = {
            "kernelspecs": {
                "julia-1.9": {
                    "name": "julia-1.9",
                    "spec": {
                        "display_name": "Julia 1.9",
                        "language": "julia",
                        "argv": ["/usr/bin/julia"],
                    }
                },
                "skop-abc123": {
                    "name": "skop-abc123",
                    "spec": {
                        "display_name": "Skop: my-venv",
                        "language": "python",
                        "argv": ["/home/user/.venv/bin/python", "-m", "ipykernel_launcher"],
                    }
                }
            }
        }

        with patch.object(env_mgr, 'detect_environments') as mock_detect:
            mock_detect.return_value = []

            result = env_mgr.get_available_environments(kernelspecs=kernelspecs)

        # Only Julia, not skop-* (and not Python kernelspecs either)
        assert len(result) == 1
        assert result[0]["name"] == "julia-1.9"

    def test_local_env_has_null_kernel_name(self, tmp_path):
        """Local Python envs have kernel_name=None (use ephemeral registration)."""
        venv_path = tmp_path / "my-venv"
        venv_path.mkdir()
        (venv_path / "bin").mkdir()
        python_path = venv_path / "bin" / "python"
        python_path.touch()

        env_mgr = EnvironmentManager(project_root=str(tmp_path))

        with patch.object(env_mgr, 'detect_environments') as mock_detect:
            mock_detect.return_value = [
                {
                    "name": "my-venv",
                    "display_name": "my-venv",
                    "type": "venv",
                    "path": str(venv_path),
                    "python_path": str(python_path),
                    "python_version": "3.11.0",
                    "is_active": False,
                    "has_ipykernel": False,
                }
            ]

            result = env_mgr.get_available_environments(kernelspecs=None)

        assert len(result) == 1
        local_env = result[0]
        assert local_env["kernel_name"] is None

    def test_multiple_local_envs(self, tmp_path):
        """Multiple local envs all get kernel_name=None."""
        venv1 = tmp_path / "venv1"
        venv1.mkdir()
        (venv1 / "bin").mkdir()
        python1 = venv1 / "bin" / "python"
        python1.touch()

        venv2 = tmp_path / "venv2"
        venv2.mkdir()
        (venv2 / "bin").mkdir()
        python2 = venv2 / "bin" / "python"
        python2.touch()

        env_mgr = EnvironmentManager(project_root=str(tmp_path))

        with patch.object(env_mgr, 'detect_environments') as mock_detect:
            mock_detect.return_value = [
                {
                    "name": "venv1",
                    "display_name": "venv1",
                    "type": "venv",
                    "path": str(venv1),
                    "python_path": str(python1),
                    "python_version": "3.11.0",
                    "is_active": False,
                    "has_ipykernel": True,
                },
                {
                    "name": "venv2",
                    "display_name": "venv2",
                    "type": "venv",
                    "path": str(venv2),
                    "python_path": str(python2),
                    "python_version": "3.12.0",
                    "is_active": False,
                    "has_ipykernel": True,
                }
            ]

            result = env_mgr.get_available_environments(kernelspecs=None)

        assert len(result) == 2

        env1 = next((e for e in result if e["name"] == "venv1"), None)
        env2 = next((e for e in result if e["name"] == "venv2"), None)

        # Both should have kernel_name=None (ephemeral registration)
        assert env1["kernel_name"] is None
        assert env2["kernel_name"] is None
        # Both should have language set
        assert env1["language"] == "python"
        assert env2["language"] == "python"

    def test_force_refresh_passed_to_detect_environments(self, tmp_path):
        """force_refresh parameter is passed through to detect_environments."""
        env_mgr = EnvironmentManager(project_root=str(tmp_path))

        with patch.object(env_mgr, 'detect_environments') as mock_detect:
            mock_detect.return_value = []

            env_mgr.get_available_environments(kernelspecs=None, force_refresh=True)
            mock_detect.assert_called_once_with(force_refresh=True)

            mock_detect.reset_mock()
            env_mgr.get_available_environments(kernelspecs=None, force_refresh=False)
            mock_detect.assert_called_once_with(force_refresh=False)


class TestMessageHandlerKernelListAvailable:
    """Tests for MessageHandler._handle_kernel_list_available()"""

    @pytest.mark.asyncio
    async def test_without_jupyter_server(self, tmp_path):
        """When Jupyter Server is not available, returns local envs only."""
        from skop_runner.message_handler import MessageHandler
        from skop_runner.mock_backend import MockKernelBackend
        from skop_runner.file_manager import FileManager

        file_mgr = FileManager()
        file_mgr.set_root(str(tmp_path))
        backend = MockKernelBackend()  # No get_kernelspecs capability

        handler = MessageHandler(backend, file_mgr)

        # Mock detect_environments
        with patch.object(handler.env_manager, 'detect_environments') as mock_detect:
            mock_detect.return_value = [
                {
                    "name": "System Python",
                    "display_name": "System Python",
                    "type": "system",
                    "path": "/usr/bin",
                    "python_path": "/usr/bin/python3",
                    "python_version": "3.10.0",
                    "is_active": True,
                    "has_ipykernel": True,
                }
            ]

            result = await handler._handle_kernel_list_available({})

        assert "environments" in result
        assert len(result["environments"]) == 1
        assert result["environments"][0]["name"] == "System Python"
        # Python envs use ephemeral registration
        assert result["environments"][0]["kernel_name"] is None
        assert result["environments"][0]["language"] == "python"

    @pytest.mark.asyncio
    async def test_with_jupyter_server_non_python_kernels(self, tmp_path):
        """When Jupyter Server has non-Python kernels, they are included."""
        from skop_runner.message_handler import MessageHandler
        from skop_runner.file_manager import FileManager

        file_mgr = FileManager()
        file_mgr.set_root(str(tmp_path))

        # Create a mock backend that exposes get_kernelspecs (optional capability)
        mock_backend = MagicMock()
        mock_backend.get_kernelspecs = AsyncMock(return_value={
            "kernelspecs": {
                "julia-1.9": {
                    "name": "julia-1.9",
                    "spec": {
                        "display_name": "Julia 1.9",
                        "language": "julia",
                        "argv": ["/usr/bin/julia"],
                    }
                },
                "python3": {
                    "name": "python3",
                    "spec": {
                        "display_name": "Python 3",
                        "language": "python",
                        "argv": ["/usr/bin/python3", "-m", "ipykernel_launcher"],
                    }
                }
            }
        })

        handler = MessageHandler(mock_backend, file_mgr)

        with patch.object(handler.env_manager, 'detect_environments') as mock_detect:
            mock_detect.return_value = []

            result = await handler._handle_kernel_list_available({})

        assert "environments" in result
        # Only Julia should be present (Python kernelspec excluded)
        assert len(result["environments"]) == 1
        assert result["environments"][0]["name"] == "julia-1.9"
        assert result["environments"][0]["kernel_name"] == "julia-1.9"

    @pytest.mark.asyncio
    async def test_api_error_falls_back_to_local_envs(self, tmp_path):
        """When API call fails, falls back to local environments only."""
        from skop_runner.message_handler import MessageHandler
        from skop_runner.file_manager import FileManager

        file_mgr = FileManager()
        file_mgr.set_root(str(tmp_path))

        # Mock backend whose get_kernelspecs raises an error
        mock_backend = MagicMock()
        mock_backend.get_kernelspecs = AsyncMock(side_effect=Exception("Connection refused"))

        handler = MessageHandler(mock_backend, file_mgr)

        with patch.object(handler.env_manager, 'detect_environments') as mock_detect:
            mock_detect.return_value = [
                {
                    "name": "my-venv",
                    "display_name": "my-venv",
                    "type": "venv",
                    "path": "/path/to/venv",
                    "python_path": "/path/to/venv/bin/python",
                    "python_version": "3.11.0",
                    "is_active": False,
                    "has_ipykernel": True,
                }
            ]

            # Should not raise, should fall back gracefully
            result = await handler._handle_kernel_list_available({})

        assert "environments" in result
        assert len(result["environments"]) == 1
        assert result["environments"][0]["name"] == "my-venv"
        # Python envs use ephemeral registration
        assert result["environments"][0]["kernel_name"] is None

    @pytest.mark.asyncio
    async def test_refresh_parameter_forces_cache_refresh(self, tmp_path):
        """refresh=True forces cache refresh in environment detection."""
        from skop_runner.message_handler import MessageHandler
        from skop_runner.mock_backend import MockKernelBackend
        from skop_runner.file_manager import FileManager

        file_mgr = FileManager()
        file_mgr.set_root(str(tmp_path))
        backend = MockKernelBackend()

        handler = MessageHandler(backend, file_mgr)

        with patch.object(handler.env_manager, 'get_available_environments') as mock_get:
            mock_get.return_value = []

            await handler._handle_kernel_list_available({"refresh": True})
            mock_get.assert_called_once_with(kernelspecs=None, force_refresh=True)

            mock_get.reset_mock()
            await handler._handle_kernel_list_available({"refresh": False})
            mock_get.assert_called_once_with(kernelspecs=None, force_refresh=False)
