import sys
import re
import platform
import subprocess
import json
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from wetlands.environment_manager import EnvironmentManager
from wetlands.internal_environment import InternalEnvironment
from wetlands.external_environment import ExternalEnvironment
from wetlands._internal.dependency_manager import Dependencies
from wetlands._internal.command_generator import Commands

# --- Fixtures ---

conda_list_json = """
[
    {
        "base_url": "https://repo.anaconda.com/pkgs/main",
        "build_number": 1,
        "build_string": "h18a0788_1",
        "channel": "pkgs/main",
        "dist_name": "zlib-1.2.13-h18a0788_1",
        "name": "zlib",
        "platform": "osx-arm64",
        "version": "1.2.13"
    },
    {
        "base_url": "https://repo.anaconda.com/pkgs/main",
        "build_number": 0,
        "build_string": "py312h1a4646a_0",
        "channel": "pkgs/main",
        "dist_name": "zstandard-0.22.0-py312h1a4646a_0",
        "name": "zstandard",
        "platform": "osx-arm64",
        "version": "0.22.0"
    },
    {
        "base_url": "https://repo.anaconda.com/pkgs/main",
        "build_number": 2,
        "build_string": "hd90d995_2",
        "channel": "pkgs/main",
        "dist_name": "zstd-1.5.5-hd90d995_2",
        "name": "zstd",
        "platform": "osx-arm64",
        "version": "1.5.5"
    }
]
    """.splitlines()


@pytest.fixture
def mock_command_executor(monkeypatch):
    """Mocks the CommandExecutor methods."""
    mock_execute = MagicMock(spec=subprocess.Popen)
    mock_execute_output = MagicMock(return_value=["output line 1", "output line 2"])

    # Patch the instance methods after EnvironmentManager is initialized
    # We'll apply these mocks within the main fixture
    mocks = {
        "executeCommands": mock_execute,
        "executeCommandsAndGetOutput": mock_execute_output,
    }
    return mocks


@pytest.fixture
def environment_manager_fixture(tmp_path_factory, mock_command_executor, monkeypatch):
    """Provides an EnvironmentManager instance with mocked CommandExecutor."""
    dummy_micromamba_path = tmp_path_factory.mktemp("conda_root")
    main_env_path = dummy_micromamba_path / "envs" / "main_test_env"

    # Mock installConda to prevent downloads
    monkeypatch.setattr(EnvironmentManager, "installConda", MagicMock())

    manager = EnvironmentManager(condaPath=dummy_micromamba_path, usePixi=False, mainCondaEnvironmentPath=main_env_path)

    # Apply the mocks to the specific instance's commandExecutor
    monkeypatch.setattr(manager.commandExecutor, "executeCommands", mock_command_executor["executeCommands"])
    monkeypatch.setattr(
        manager.commandExecutor, "executeCommandsAndGetOutput", mock_command_executor["executeCommandsAndGetOutput"]
    )

    # Mock environmentExists to simplify create tests
    # We can override this in specific tests if needed
    monkeypatch.setattr(manager, "environmentExists", MagicMock(return_value=False))

    return manager, mock_command_executor["executeCommandsAndGetOutput"], mock_command_executor["executeCommands"]


@pytest.fixture
def environment_manager_pixi_fixture(tmp_path_factory, mock_command_executor, monkeypatch):
    """Provides an EnvironmentManager instance with mocked CommandExecutor."""
    dummy_pixi_path = tmp_path_factory.mktemp("pixi_root")

    # Mock installConda to prevent downloads
    monkeypatch.setattr(EnvironmentManager, "installConda", MagicMock())

    manager = EnvironmentManager(condaPath=dummy_pixi_path, usePixi=True)

    # Apply the mocks to the specific instance's commandExecutor
    monkeypatch.setattr(manager.commandExecutor, "executeCommands", mock_command_executor["executeCommands"])
    monkeypatch.setattr(
        manager.commandExecutor, "executeCommandsAndGetOutput", mock_command_executor["executeCommandsAndGetOutput"]
    )

    # Mock environmentExists to simplify create tests
    # We can override this in specific tests if needed
    monkeypatch.setattr(manager, "environmentExists", MagicMock(return_value=False))

    return manager, mock_command_executor["executeCommandsAndGetOutput"], mock_command_executor["executeCommands"]


# --- Test Functions ---

# ---- _dependenciesAreInstalled Tests ----


def test_environment_manager_conda_path(tmp_path_factory):
    """Check that an exception is raised if the provided CondaPath contains the name of another conda manager."""

    dummy_conda_path = tmp_path_factory.mktemp("path_containing_pixi_and_micromamba").resolve()

    with pytest.raises(Exception):
        manager = EnvironmentManager(condaPath=dummy_conda_path, usePixi=False)

    with pytest.raises(Exception):
        manager = EnvironmentManager(condaPath=dummy_conda_path, usePixi=True)

    manager = EnvironmentManager(condaPath=dummy_conda_path, usePixi=True, acceptAllCondaPaths=True)
    assert manager is not None


def test_dependencies_are_installed_python_mismatch(environment_manager_fixture):
    manager, mock_execute_output, _ = environment_manager_fixture
    # Ensure the version string format causes a mismatch
    different_py_version = "99.99"
    assert not sys.version.startswith(different_py_version)

    dependencies: Dependencies = {"python": f"={different_py_version}"}  # Exact match required by logic

    installed = manager._dependenciesAreInstalled(dependencies)

    assert not installed
    mock_execute_output.assert_not_called()  # Should return False before checking packages


def test_dependencies_are_installed_empty_deps(environment_manager_fixture):
    manager, mock_execute_output, _ = environment_manager_fixture
    dependencies: Dependencies = {}  # Python version check passes by default

    # Mock sys.version temporarily if needed, or assume test runner Python version is okay
    installed = manager._dependenciesAreInstalled(dependencies)

    assert installed is True  # Empty deps means nothing to fail
    mock_execute_output.assert_not_called()  # No packages to check


def test_dependencies_are_installed_conda_only_installed(environment_manager_fixture):
    manager, mock_execute_output, _ = environment_manager_fixture
    manager.mainEnvironment.name = str(Path("some/valid/path"))  # Ensure mainEnv path is not None
    dependencies: Dependencies = {"conda": ["conda-forge::zlib==1.2.13", "zstandard"]}
    # Mock output for 'conda list'
    mock_execute_output.return_value = conda_list_json

    installed = manager._dependenciesAreInstalled(dependencies)

    assert installed is True
    # Check if conda list command was executed within the main env context
    assert mock_execute_output.call_count >= 1
    called_args, called_kwargs = mock_execute_output.call_args
    command_list = called_args[0]
    assert any(f"activate {manager.mainEnvironment.name}" in cmd for cmd in command_list)
    assert any("freeze --all" in cmd for cmd in command_list)


def test_dependencies_are_installed_conda_only_not_installed(environment_manager_fixture):
    manager, mock_execute_output, _ = environment_manager_fixture
    manager.mainEnvironment.path = str(Path("some/valid/path"))
    dependencies: Dependencies = {"conda": ["package1", "missing_package"]}
    mock_execute_output.return_value = conda_list_json

    installed = manager._dependenciesAreInstalled(dependencies)

    assert installed is False


def test_dependencies_are_installed_pip_only_installed(environment_manager_fixture):
    manager, mock_execute_output, _ = environment_manager_fixture
    manager.mainEnvironment.name = str(Path("some/valid/path"))
    dependencies: Dependencies = {"pip": ["package1==1.0", "package2"]}
    # Mock output for 'pip freeze'
    pip_freeze_output = """
package1==1.0
package2==2.5
otherpackage==3.0
    """.splitlines()

    # Mock outputs for both commands, called sequentially
    mock_execute_output.side_effect = [
        conda_list_json,
        pip_freeze_output,
    ]

    installed = manager._dependenciesAreInstalled(dependencies)

    assert installed is True
    # Check if pip freeze command was executed
    assert mock_execute_output.call_count >= 1
    called_args, called_kwargs = mock_execute_output.call_args
    command_list = called_args[0]
    assert any(f"activate {manager.mainEnvironment.name}" in cmd for cmd in command_list)
    assert any("pip freeze --all" in cmd for cmd in command_list)


def test_dependencies_are_installed_pip_only_not_installed(environment_manager_fixture):
    manager, mock_execute_output, _ = environment_manager_fixture
    manager.mainEnvironment.name = str(Path("some/valid/path"))
    dependencies: Dependencies = {"pip": ["package1==1.0", "missing_package==3.3"]}
    mock_execute_output.return_value = "[]"

    installed = manager._dependenciesAreInstalled(dependencies)

    assert installed is False


def test_dependencies_are_installed_conda_and_pip_installed(environment_manager_fixture):
    manager, mock_execute_output, _ = environment_manager_fixture
    manager.mainEnvironment.name = Path("some/valid/path")
    dependencies: Dependencies = {"conda": ["zlib"], "pip": ["p_package==2"]}
    # Mock outputs for both commands, called sequentially
    mock_execute_output.side_effect = [
        conda_list_json,
        ["p_package==2.0"],  # pip freeze output
    ]

    installed = manager._dependenciesAreInstalled(dependencies)

    assert installed is True
    assert mock_execute_output.call_count >= 1
    # Check first call (conda list)
    call1_args, _ = mock_execute_output.call_args_list[0]
    assert any("list --json" in cmd for cmd in call1_args[0])
    # Check second call (pip freeze)
    call2_args, _ = mock_execute_output.call_args_list[1]
    assert any("pip freeze --all" in cmd for cmd in call2_args[0])


def test_dependencies_are_installed_conda_ok_pip_missing(environment_manager_fixture):
    manager, mock_execute_output, _ = environment_manager_fixture
    manager.mainEnvironment.path = Path("some/valid/path")
    dependencies: Dependencies = {"conda": ["conda-forge::zlib==1.2.13"], "pip": ["p_package==2", "missing_pip==3"]}
    mock_execute_output.side_effect = [
        conda_list_json,
        ["p_package==2.0"],  # pip freeze output (missing one)
    ]

    installed = manager._dependenciesAreInstalled(dependencies)

    assert installed is False
    assert mock_execute_output.call_count >= 1


def test_dependencies_are_installed_no_main_env_conda_fails(environment_manager_fixture):
    manager, mock_execute_output, _ = environment_manager_fixture
    manager.mainEnvironment.name = None
    dependencies: Dependencies = {"conda": ["some_package"]}

    installed = manager._dependenciesAreInstalled(dependencies)

    assert installed is False
    mock_execute_output.assert_not_called()  # Should fail before calling conda list


def test_dependencies_are_installed_no_main_env_pip_uses_metadata(environment_manager_fixture, monkeypatch):
    manager, mock_execute_output, _ = environment_manager_fixture
    manager.mainEnvironment.name = None
    dependencies: Dependencies = {"pip": ["pytest"]}  # Assume pytest is installed in test runner env

    # We don't need to mock distributions if pytest is actually installed
    # If not, we would mock importlib.metadata.distributions
    # mock_dist = MagicMock()
    # mock_dist.metadata = {'Name': 'pytest'}
    # mock_dist.version = '1.2.3'
    # monkeypatch.setattr('importlib.metadata.distributions', MagicMock(return_value=[mock_dist]))

    installed = manager._dependenciesAreInstalled(dependencies)

    # This depends on whether 'pytest' is ACTUALLY available via metadata in the test env
    # Let's assume it is for this test case. If not, the mock above is needed.
    import importlib.metadata

    try:
        importlib.metadata.version("pytest")
        assert installed is True
    except importlib.metadata.PackageNotFoundError:
        assert installed is False  # Or assert False if you know it won't be found

    mock_execute_output.assert_not_called()  # Should use metadata, not run pip freeze


# ---- create Tests ----


@pytest.mark.parametrize("force_external", [True, False])
def test_create_environment_already_exists(environment_manager_fixture, monkeypatch, force_external):
    manager, mock_execute_output, _ = environment_manager_fixture
    env_name = "existing-env"

    # Mock environmentExists to return True for this specific name
    mock_exists = MagicMock(return_value=True)
    monkeypatch.setattr(manager, "environmentExists", mock_exists)

    # Add to environments dict to simulate it was *really* created before
    manager.environments[env_name] = ExternalEnvironment(env_name, manager)

    env = manager.create(env_name, dependencies={}, forceExternal=force_external)

    assert isinstance(env, ExternalEnvironment)
    assert env.name == env_name
    mock_exists.assert_called_once_with(env_name)
    mock_execute_output.assert_not_called()  # No creation/installation commands needed
    assert env is manager.environments[env_name]  # Should return existing instance


def test_create_dependencies_met_use_main_environment(environment_manager_fixture, monkeypatch):
    manager, mock_execute_output, _ = environment_manager_fixture
    env_name = "new-env-dont-create"
    dependencies: Dependencies = {"pip": ["numpy==1.2.3"]}

    # Mock _dependenciesAreInstalled to return True
    monkeypatch.setattr(manager, "_dependenciesAreInstalled", MagicMock(return_value=True))
    # Mock environmentExists to return False (it doesn't exist yet)
    monkeypatch.setattr(manager, "environmentExists", MagicMock(return_value=False))

    env = manager.create(env_name, dependencies=dependencies, forceExternal=False)

    assert env is manager.mainEnvironment  # Should return the main environment instance
    assert isinstance(env, InternalEnvironment)
    manager._dependenciesAreInstalled.assert_called_once_with(dependencies)
    manager.environmentExists.assert_called_once_with(env_name)
    mock_execute_output.assert_not_called()  # No commands should be run


def test_create_dependencies_met_force_external(environment_manager_fixture, monkeypatch):
    manager, mock_execute_output, _ = environment_manager_fixture
    env_name = "forced-external-env"
    dependencies: Dependencies = {"pip": ["numpy==1.2.3"]}

    # Mock _dependenciesAreInstalled to return True, but forceExternal=True overrides it
    monkeypatch.setattr(manager, "_dependenciesAreInstalled", MagicMock(return_value=True))
    monkeypatch.setattr(manager, "environmentExists", MagicMock(return_value=False))

    env = manager.create(env_name, dependencies=dependencies, forceExternal=True)

    assert isinstance(env, ExternalEnvironment)
    assert env.name == env_name
    assert env is manager.environments[env_name]
    manager.environmentExists.assert_called_once_with(env_name)
    mock_execute_output.assert_called()  # Creation commands should be run

    # Check for key commands
    called_args, _ = mock_execute_output.call_args
    command_list = called_args[0]
    assert any(f"create -n {env_name}" in cmd for cmd in command_list)
    # Check install commands are present (assuming numpy leads to some install command)
    assert any("install" in cmd for cmd in command_list if "create" not in cmd)


def test_create_dependencies_not_met_create_external(environment_manager_fixture, monkeypatch):
    manager, mock_execute_output, _ = environment_manager_fixture
    env_name = "new-external-env"
    dependencies: Dependencies = {"conda": ["requests"], "pip": ["pandas"]}

    # Mock _dependenciesAreInstalled to return False
    monkeypatch.setattr(manager, "_dependenciesAreInstalled", MagicMock(return_value=False))
    monkeypatch.setattr(manager, "environmentExists", MagicMock(return_value=False))

    env = manager.create(env_name, dependencies=dependencies, forceExternal=False)

    assert isinstance(env, ExternalEnvironment)
    assert env.name == env_name
    assert env is manager.environments[env_name]
    manager._dependenciesAreInstalled.assert_called_once_with(dependencies)
    manager.environmentExists.assert_called_once_with(env_name)
    mock_execute_output.assert_called()  # Creation commands should be run

    # Check for key commands
    called_args, _ = mock_execute_output.call_args
    command_list = called_args[0]
    current_py_version = platform.python_version()
    assert any(f"create -n {env_name} python={current_py_version} -y" in cmd for cmd in command_list)
    assert any(f"install" in cmd for cmd in command_list if "micromamba" in cmd)  # Check for install commands
    assert any("requests" in cmd for cmd in command_list if "install" in cmd)  # Check dep is mentioned
    assert any("pandas" in cmd for cmd in command_list if "pip" in cmd and "install" in cmd)


def test_create_with_python_version(environment_manager_fixture, monkeypatch):
    manager, mock_execute_output, _ = environment_manager_fixture
    env_name = "py-versioned-env"
    py_version = "3.10.5"
    dependencies: Dependencies = {"python": f"={py_version}", "pip": ["toolz"]}  # Use exact match format

    monkeypatch.setattr(manager, "_dependenciesAreInstalled", MagicMock(return_value=False))
    monkeypatch.setattr(manager, "environmentExists", MagicMock(return_value=False))

    env = manager.create(env_name, dependencies=dependencies)

    assert isinstance(env, ExternalEnvironment)
    mock_execute_output.assert_called()
    called_args, _ = mock_execute_output.call_args
    command_list = called_args[0]
    # Check python version is in create command
    assert any(f"create -n {env_name} python={py_version} -y" in cmd for cmd in command_list)
    # Check install command for toolz
    assert any("toolz" in cmd for cmd in command_list if "pip" in cmd and "install" in cmd)


def test_create_with_additional_commands(environment_manager_fixture, monkeypatch):
    manager, mock_execute_output, _ = environment_manager_fixture
    env_name = "env-with-extras"
    dependencies: Dependencies = {"pip": ["tiny-package"]}
    additional_commands: Commands = {
        "all": ["echo 'hello world'"],
        "linux": ["specific command"],  # e.g., 'linux', 'darwin', 'windows'
    }

    monkeypatch.setattr(manager, "_dependenciesAreInstalled", MagicMock(return_value=False))
    monkeypatch.setattr(manager, "environmentExists", MagicMock(return_value=False))

    monkeypatch.setattr(platform, "system", MagicMock(return_value="Linux"))

    manager.create(env_name, dependencies=dependencies, additionalInstallCommands=additional_commands)

    mock_execute_output.assert_called()
    called_args, _ = mock_execute_output.call_args
    command_list = called_args[0]

    # Check create and install commands are present
    assert any(f"create -n {env_name}" in cmd for cmd in command_list)
    assert any("tiny-package" in cmd for cmd in command_list if "pip" in cmd and "install" in cmd)

    # Check additional commands are present
    assert "echo 'hello world'" in command_list
    assert "specific command" in command_list


def test_create_invalid_python_version_raises(environment_manager_fixture, monkeypatch):
    manager, _, _ = environment_manager_fixture
    env_name = "invalid-py-env"
    dependencies: Dependencies = {"python": "=3.8"}  # Below 3.9 limit

    monkeypatch.setattr(manager, "_dependenciesAreInstalled", MagicMock(return_value=False))
    monkeypatch.setattr(manager, "environmentExists", MagicMock(return_value=False))

    with pytest.raises(Exception, match="Python version must be greater than 3.8"):
        manager.create(env_name, dependencies=dependencies)


# ---- install Tests ----


def test_install_in_existing_env(environment_manager_fixture):
    manager, mock_execute_output, _ = environment_manager_fixture
    env_name = "target-env"
    dependencies: Dependencies = {"conda": ["new_dep==1.0"]}

    manager.install(env_name, dependencies)

    mock_execute_output.assert_called_once()
    called_args, _ = mock_execute_output.call_args
    command_list = called_args[0]

    # Check for install commands targeting the environment
    assert any("new_dep==1.0" in cmd for cmd in command_list if "install" in cmd)
    # Check activation commands are present (usually part of install dependencies)
    assert any(
        "micromamba activate" in cmd or ". /path/to/micromamba" in cmd for cmd in command_list
    )  # Check general activation pattern


def test_install_in_main_env(environment_manager_fixture):
    manager, mock_execute_output, _ = environment_manager_fixture
    dependencies: Dependencies = {"pip": ["another_pip_dep"]}

    manager.install(None, dependencies)  # None signifies main/current environment context

    mock_execute_output.assert_called_once()
    called_args, _ = mock_execute_output.call_args
    command_list = called_args[0]

    # Install commands should NOT have "-n env_name"
    assert not any(f"install -n" in cmd for cmd in command_list if "install" in cmd)
    # Check pip install command is present
    assert any("pip install" in cmd and "another_pip_dep" in cmd for cmd in command_list)


def test_install_with_additional_commands(environment_manager_fixture):
    manager, mock_execute_output, _ = environment_manager_fixture
    env_name = "install-env-extras"
    dependencies: Dependencies = {"conda": ["dep1"]}
    additional_commands: Commands = {"all": ["post-install script"]}

    manager.install(env_name, dependencies, additional_commands)

    mock_execute_output.assert_called_once()
    called_args, _ = mock_execute_output.call_args
    command_list = called_args[0]

    # Check install command
    assert any("install" in cmd and "dep1" in cmd for cmd in command_list)
    # Check additional command
    assert "post-install script" in command_list


# ---- executeCommands Tests ----


def test_execute_commands_in_specific_env(environment_manager_fixture):
    manager, _, mock_execute = environment_manager_fixture
    env_name = "exec-env"
    commands_to_run: Commands = {"all": ["python script.py", "echo done"]}
    popen_kwargs = {"cwd": "/some/path"}

    manager.executeCommands(env_name, commands_to_run, popenKwargs=popen_kwargs)

    mock_execute.assert_called_once()
    called_args, called_kwargs = mock_execute.call_args
    command_list = called_args[0]

    # Check activation for the specific environment
    assert any(f"activate {env_name}" in cmd for cmd in command_list)
    # Check user commands are present
    assert "python script.py" in command_list
    assert "echo done" in command_list
    # Check popenKwargs are passed through
    assert called_kwargs.get("popenKwargs") == popen_kwargs


def test_execute_commands_in_main_env(environment_manager_fixture):
    manager, _, mock_execute = environment_manager_fixture
    manager.mainEnvironment.name = str(Path("/path/to/main/env"))  # Give it a path
    commands_to_run: Commands = {"all": ["ls -l"]}

    manager.executeCommands(None, commands_to_run)  # None for main env

    mock_execute.assert_called_once()
    called_args, _ = mock_execute.call_args
    command_list = called_args[0]

    # Check user command
    assert "ls -l" in command_list


# Test with Pixi Environment Manager


def test_create_with_python_version_pixi(environment_manager_pixi_fixture, monkeypatch):
    manager, mock_execute_output, _ = environment_manager_pixi_fixture
    env_name = "py-versioned-env"
    py_version = "3.10.5"
    dependencies: Dependencies = {
        "python": f"={py_version}",
        "pip": ["toolz"],
        "conda": ["dep==1.0"],
    }  # Use exact match format

    monkeypatch.setattr(manager, "_dependenciesAreInstalled", MagicMock(return_value=False))
    monkeypatch.setattr(manager, "environmentExists", MagicMock(return_value=False))

    env = manager.create(env_name, dependencies=dependencies)

    assert isinstance(env, ExternalEnvironment)
    mock_execute_output.assert_called()
    called_args, _ = mock_execute_output.call_args
    command_list = called_args[0]
    pixi_bin = "pixi.exe" if platform.system() == "Windows" else "pixi"
    assert any(f"{pixi_bin} init" in cmd for cmd in command_list)
    # Check python version is in create command
    assert any(re.match(rf"{pixi_bin} add .* python={py_version}", cmd) is not None for cmd in command_list)
    # Check install command for dependencies
    assert any("toolz" in cmd and "--pypi" in cmd for cmd in command_list if f"{pixi_bin} add" in cmd)
    assert any("dep" in cmd for cmd in command_list if f"{pixi_bin} add" in cmd)


# ---- install Tests ----


def test_install_in_existing_env_pixi(environment_manager_pixi_fixture):
    manager, mock_execute_output, _ = environment_manager_pixi_fixture
    env_name = "target-env"
    dependencies: Dependencies = {"conda": ["new_dep==1.0"]}

    manager.install(env_name, dependencies)

    mock_execute_output.assert_called_once()
    called_args, _ = mock_execute_output.call_args
    command_list = called_args[0]
    pixi_bin = "pixi.exe" if platform.system() == "Windows" else "pixi"

    # Check for install commands targeting the environment
    assert any("new_dep==1.0" in cmd for cmd in command_list if f"{pixi_bin} add" in cmd)


# ---- registerEnvironment Tests ----


class TestRegisterEnvironment:
    def test_register_environment_creates_debug_ports_file(self, environment_manager_fixture, tmp_path, monkeypatch):
        """Test that registerEnvironment creates debug_ports.json"""
        manager, _, _ = environment_manager_fixture
        manager.wetlandsInstancePath = tmp_path / "wetlands"

        mock_process = MagicMock()
        mock_process.pid = 12345
        env = ExternalEnvironment("test_env", manager)
        env.process = mock_process

        debug_port = 5678
        executor_path = Path("/path/to/module_executor.py")

        manager.registerEnvironment(env, debug_port, executor_path)

        debug_ports_file = tmp_path / "wetlands" / "debug_ports.json"
        assert debug_ports_file.exists()

        with open(debug_ports_file, "r") as f:
            content = json.load(f)

        assert "test_env" in content
        assert content["test_env"]["debugPort"] == 5678
        assert content["test_env"]["moduleExecutorPath"] == "/path/to/module_executor.py"

    def test_register_environment_appends_to_existing_file(self, environment_manager_fixture, tmp_path, monkeypatch):
        """Test that registerEnvironment appends to existing debug_ports.json"""
        manager, _, _ = environment_manager_fixture
        manager.wetlandsInstancePath = tmp_path / "wetlands"

        # Create existing file with one env
        debug_ports_dir = tmp_path / "wetlands"
        debug_ports_dir.mkdir(exist_ok=True, parents=True)
        debug_ports_file = debug_ports_dir / "debug_ports.json"

        existing_data = {"env1": {"debugPort": 1111, "moduleExecutorPath": "/path/to/exec1"}}
        with open(debug_ports_file, "w") as f:
            json.dump(existing_data, f)

        # Register new environment
        mock_process = MagicMock()
        env = ExternalEnvironment("env2", manager)
        env.process = mock_process

        manager.registerEnvironment(env, 2222, Path("/path/to/exec2"))

        # Verify both envs are in file
        with open(debug_ports_file, "r") as f:
            content = json.load(f)

        assert len(content) == 2
        assert content["env1"]["debugPort"] == 1111
        assert content["env2"]["debugPort"] == 2222

    def test_register_environment_overwrites_existing_env(self, environment_manager_fixture, tmp_path):
        """Test that registerEnvironment overwrites entry for existing environment"""
        manager, _, _ = environment_manager_fixture
        manager.wetlandsInstancePath = tmp_path / "wetlands"

        debug_ports_dir = tmp_path / "wetlands"
        debug_ports_dir.mkdir(exist_ok=True, parents=True)
        debug_ports_file = debug_ports_dir / "debug_ports.json"

        # Create file with old data
        old_data = {"test_env": {"debugPort": 9999, "moduleExecutorPath": "/old/path"}}
        with open(debug_ports_file, "w") as f:
            json.dump(old_data, f)

        # Register with new data
        mock_process = MagicMock()
        env = ExternalEnvironment("test_env", manager)
        env.process = mock_process

        manager.registerEnvironment(env, 5555, Path("/new/path"))

        with open(debug_ports_file, "r") as f:
            content = json.load(f)

        assert content["test_env"]["debugPort"] == 5555
        assert content["test_env"]["moduleExecutorPath"] == "/new/path"

    def test_register_environment_with_no_process(self, environment_manager_fixture):
        """Test that registerEnvironment returns early if process is None"""
        manager, _, _ = environment_manager_fixture

        env = ExternalEnvironment("test_env", manager)
        env.process = None  # No process

        # Should return without error
        manager.registerEnvironment(env, 5678, Path("/path/to/executor"))


# ---- _removeEnvironment Tests ----


class TestRemoveEnvironment:
    def test_remove_environment_existing(self, environment_manager_fixture):
        """Test that _removeEnvironment removes environment from dict"""
        manager, _, _ = environment_manager_fixture
        env_name = "test_env"
        env = ExternalEnvironment(env_name, manager)
        manager.environments[env_name] = env

        assert env_name in manager.environments
        manager._removeEnvironment(env)
        assert env_name not in manager.environments

    def test_remove_environment_non_existing(self, environment_manager_fixture):
        """Test that _removeEnvironment handles non-existing environment gracefully"""
        manager, _, _ = environment_manager_fixture
        env = ExternalEnvironment("non_existing", manager)

        # Should not raise error
        manager._removeEnvironment(env)


# ---- setCondaPath Tests ----


class TestSetCondaPath:
    def test_set_conda_path_updates_settings(self, environment_manager_fixture, monkeypatch, tmp_path):
        """Test that setCondaPath updates the settings manager"""
        manager, _, _ = environment_manager_fixture
        new_path = tmp_path / "new_conda"

        mock_set_path = MagicMock()
        monkeypatch.setattr(manager.settingsManager, "setCondaPath", mock_set_path)

        manager.setCondaPath(new_path, usePixi=True)

        mock_set_path.assert_called_once_with(new_path, True)


# ---- setProxies Tests ----


class TestSetProxies:
    def test_set_proxies_calls_settings_manager(self, environment_manager_fixture, monkeypatch):
        """Test that setProxies delegates to settings manager"""
        manager, _, _ = environment_manager_fixture
        proxies = {"http": "http://proxy.example.com:8080", "https": "https://proxy.example.com:8443"}

        mock_set_proxies = MagicMock()
        monkeypatch.setattr(manager.settingsManager, "setProxies", mock_set_proxies)

        manager.setProxies(proxies)

        mock_set_proxies.assert_called_once_with(proxies)


# ---- _removeChannel Tests ----


class TestRemoveChannel:
    def test_remove_channel_with_channel(self):
        """Test that _removeChannel removes channel prefix"""
        manager = MagicMock()
        manager._removeChannel = EnvironmentManager._removeChannel.__get__(manager)

        result = manager._removeChannel("conda-forge::numpy==1.2.3")
        assert result == "numpy==1.2.3"

    def test_remove_channel_without_channel(self):
        """Test that _removeChannel returns unchanged string if no channel"""
        manager = MagicMock()
        manager._removeChannel = EnvironmentManager._removeChannel.__get__(manager)

        result = manager._removeChannel("numpy==1.2.3")
        assert result == "numpy==1.2.3"


# ---- _addDebugpyInDependencies Tests ----


class TestAddDebugpyInDependencies:
    def test_add_debugpy_in_debug_mode(self, environment_manager_fixture):
        """Test that debugpy is added when debug mode is enabled"""
        manager = EnvironmentManager(
            condaPath=Path("/tmp/test_conda"), usePixi=False, debug=True, acceptAllCondaPaths=True
        )

        with patch.object(manager, "installConda"):
            dependencies: Dependencies = {"pip": ["numpy"]}
            manager._addDebugpyInDependencies(dependencies)

            assert "conda" in dependencies
            assert "debugpy" in dependencies["conda"]

    def test_add_debugpy_not_in_debug_mode(self, environment_manager_fixture):
        """Test that debugpy is not added when debug mode is disabled"""
        manager = EnvironmentManager(
            condaPath=Path("/tmp/test_conda"), usePixi=False, debug=False, acceptAllCondaPaths=True
        )

        with patch.object(manager, "installConda"):
            dependencies: Dependencies = {"pip": ["numpy"]}
            manager._addDebugpyInDependencies(dependencies)

            # Conda deps should not be added if not in debug mode
            if "conda" in dependencies:
                assert "debugpy" not in dependencies.get("conda", [])

    def test_add_debugpy_already_present(self, environment_manager_fixture):
        """Test that debugpy is not added twice if already present"""
        manager = EnvironmentManager(
            condaPath=Path("/tmp/test_conda"), usePixi=False, debug=True, acceptAllCondaPaths=True
        )

        with patch.object(manager, "installConda"):
            dependencies: Dependencies = {"conda": ["debugpy==1.0.0"]}
            manager._addDebugpyInDependencies(dependencies)

            # Check debugpy appears only once
            count = sum(1 for dep in dependencies["conda"] if "debugpy" in str(dep))
            assert count == 1


# ---- getInstalledPackages Tests ----


class TestGetInstalledPackages:
    def test_get_installed_packages_conda(self, environment_manager_fixture, monkeypatch):
        """Test getting installed packages from conda environment"""
        manager, _, _ = environment_manager_fixture
        manager.settingsManager.usePixi = False

        mock_packages = [
            {"name": "numpy", "version": "1.2.3", "kind": "conda"},
            {"name": "pandas", "version": "2.0.0", "kind": "conda"},
        ]
        manager.commandExecutor.executeCommandAndGetJsonOutput = MagicMock(return_value=mock_packages)
        manager.commandExecutor.executeCommandsAndGetOutput = MagicMock(return_value=[])

        result = manager.getInstalledPackages("test_env")

        # Should include conda packages
        assert any(p["name"] == "numpy" for p in result)


# ---- _checkRequirement Tests ----


class TestCheckRequirement:
    def test_check_requirement_conda_installed(self, environment_manager_fixture):
        """Test checking if conda requirement is met"""
        manager, _, _ = environment_manager_fixture
        installed_packages = [
            {"name": "numpy", "version": "1.2.3", "kind": "conda"},
        ]

        result = manager._checkRequirement("numpy==1.2.3", "conda", installed_packages)
        assert result is True

    def test_check_requirement_conda_version_mismatch(self, environment_manager_fixture):
        """Test checking conda requirement with version mismatch"""
        manager, _, _ = environment_manager_fixture
        installed_packages = [
            {"name": "numpy", "version": "1.2.3", "kind": "conda"},
        ]

        result = manager._checkRequirement("numpy==2.0.0", "conda", installed_packages)
        assert result is False

    def test_check_requirement_pip_installed(self, environment_manager_fixture):
        """Test checking if pip requirement is met"""
        manager, _, _ = environment_manager_fixture
        installed_packages = [
            {"name": "requests", "version": "2.28.0", "kind": "pypi"},
        ]

        result = manager._checkRequirement("requests==2.28.0", "pip", installed_packages)
        assert result is True

    def test_check_requirement_removes_channel(self, environment_manager_fixture):
        """Test that channel is removed before checking"""
        manager, _, _ = environment_manager_fixture
        installed_packages = [
            {"name": "numpy", "version": "1.2.3", "kind": "conda"},
        ]

        result = manager._checkRequirement("conda-forge::numpy==1.2.3", "conda", installed_packages)
        assert result is True


class TestExistingEnvironmentAccess:
    """Tests for accessing existing environments via Path objects."""

    def test_environment_exists_with_path_not_found(self, tmp_path):
        """Test that environmentExists() returns False for nonexistent Path."""
        nonexistent = tmp_path / "nonexistent"
        manager = EnvironmentManager(condaPath=tmp_path, usePixi=False)
        assert not manager.environmentExists(nonexistent)

    def test_load_nonexistent_path_raises_error(self, tmp_path_factory):
        """Test that load() raises an error when given a nonexistent Path."""
        tmp = tmp_path_factory.mktemp("conda_root")
        nonexistent = tmp / "nonexistent"

        manager = EnvironmentManager(condaPath=tmp, usePixi=False)

        # Should raise because the environment doesn't exist
        with pytest.raises(Exception, match="was not found"):
            manager.load(name="test_env", environmentPath=nonexistent)


# ---- delete Tests ----


class TestDeleteEnvironment:
    """Tests for deleting environments."""

    def test_delete_nonexistent_environment_micromamba(self, environment_manager_fixture, monkeypatch):
        """Test that delete raises an error when trying to delete a nonexistent environment."""
        manager, mock_execute_output, _ = environment_manager_fixture
        env_name = "nonexistent-env"

        # Mock environmentExists to return False
        monkeypatch.setattr(manager, "environmentExists", MagicMock(return_value=False))

        # Create external environment and call delete on it
        env = ExternalEnvironment(env_name, manager)

        # Should raise exception
        with pytest.raises(Exception, match="does not exist"):
            env.delete()

    def test_delete_existing_environment_micromamba(self, environment_manager_fixture, monkeypatch):
        """Test that delete removes an existing environment (micromamba)."""
        manager, mock_execute_output, _ = environment_manager_fixture
        env_name = "test-env"

        # Mock environmentExists to return True
        monkeypatch.setattr(manager, "environmentExists", MagicMock(return_value=True))

        # Mock send2trash
        mock_send2trash = MagicMock()
        monkeypatch.setattr("wetlands.external_environment.send2trash", mock_send2trash)

        # Mock getEnvironmentPath
        mock_env_path = Path("/path/to/env")
        monkeypatch.setattr(manager.settingsManager, "getEnvironmentPath", MagicMock(return_value=mock_env_path))

        # Create external environment
        env = ExternalEnvironment(env_name, manager)
        manager.environments[env_name] = env

        env.delete()

        # Should call send2trash with environment path
        mock_send2trash.assert_called_once_with(mock_env_path)
        # Should be removed from environments dict
        assert env_name not in manager.environments

    def test_delete_existing_environment_pixi(self, environment_manager_pixi_fixture, monkeypatch):
        """Test that delete removes an existing environment (pixi)."""
        manager, mock_execute_output, _ = environment_manager_pixi_fixture
        env_name = "test-env"

        # Mock environmentExists to return True
        monkeypatch.setattr(manager, "environmentExists", MagicMock(return_value=True))

        # Mock send2trash
        mock_send2trash = MagicMock()
        monkeypatch.setattr("wetlands.external_environment.send2trash", mock_send2trash)

        # Mock getWorkspacePath for Pixi
        mock_workspace_path = Path("/path/to/workspace")
        monkeypatch.setattr(manager.settingsManager, "getWorkspacePath", MagicMock(return_value=mock_workspace_path))

        # Create external environment
        env = ExternalEnvironment(env_name, manager)
        manager.environments[env_name] = env

        env.delete()

        # For pixi, it should call send2trash with workspace path
        mock_send2trash.assert_called_once_with(mock_workspace_path)

    def test_delete_environment_exits_external_environment(self, environment_manager_fixture, monkeypatch):
        """Test that delete properly exits an external environment if it's running."""
        manager, mock_execute_output, _ = environment_manager_fixture
        env_name = "running-env"

        # Mock environmentExists
        monkeypatch.setattr(manager, "environmentExists", MagicMock(return_value=True))

        # Mock send2trash
        mock_send2trash = MagicMock()
        monkeypatch.setattr("wetlands.external_environment.send2trash", mock_send2trash)

        # Mock getEnvironmentPath
        mock_env_path = Path("/path/to/env")
        monkeypatch.setattr(manager.settingsManager, "getEnvironmentPath", MagicMock(return_value=mock_env_path))

        # Create external environment
        env = ExternalEnvironment(env_name, manager)
        manager.environments[env_name] = env

        # Mock launched to return True
        monkeypatch.setattr(env, "launched", MagicMock(return_value=True))

        # Mock _exit
        monkeypatch.setattr(env, "_exit", MagicMock())

        env.delete()

        # Should call _exit on the environment
        env._exit.assert_called_once()
        # Should call send2trash
        mock_send2trash.assert_called_once()


# ---- update Tests ----


class TestUpdateEnvironment:
    """Tests for updating environments."""

    def test_update_nonexistent_environment_raises_error(self, environment_manager_fixture, monkeypatch):
        """Test that update raises an error when trying to update a nonexistent environment."""
        manager, mock_execute_output, _ = environment_manager_fixture
        env_name = "nonexistent-env"
        dependencies: Dependencies = {"pip": ["numpy"]}

        # Mock environmentExists to return False
        monkeypatch.setattr(manager, "environmentExists", MagicMock(return_value=False))

        # Create external environment and call update on it
        env = ExternalEnvironment(env_name, manager)

        # Should raise exception
        with pytest.raises(Exception, match="does not exist"):
            env.update(dependencies)

    def test_update_environment_with_dependencies_dict(self, environment_manager_fixture, monkeypatch):
        """Test that update deletes and recreates an environment with new dependencies."""
        manager, mock_execute_output, _ = environment_manager_fixture
        env_name = "test-env"
        new_dependencies: Dependencies = {"pip": ["numpy==1.0"]}

        # Mock environmentExists to return True
        monkeypatch.setattr(manager, "environmentExists", MagicMock(return_value=True))

        # Create external environment
        env = ExternalEnvironment(env_name, manager)
        manager.environments[env_name] = env

        # Mock delete on the environment
        delete_mock = MagicMock()
        monkeypatch.setattr(env, "delete", delete_mock)

        # Mock manager.create to return a new environment
        new_env = ExternalEnvironment(env_name, manager)
        create_mock = MagicMock(return_value=new_env)
        monkeypatch.setattr(manager, "create", create_mock)

        result = env.update(new_dependencies)

        # Should call delete
        delete_mock.assert_called_once()
        # Should call create with new dependencies
        create_mock.assert_called_once()
        call_args, call_kwargs = create_mock.call_args
        assert call_args[0] == env_name
        assert call_kwargs.get("dependencies") == new_dependencies
        # Should return the created environment
        assert result == new_env

    def test_update_environment_with_config_file(self, environment_manager_fixture, monkeypatch):
        """Test that update works with a config file path."""
        manager, mock_execute_output, _ = environment_manager_fixture
        env_name = "test-env"
        config_path = Path("/path/to/pyproject.toml")
        optional_deps = ["dev"]

        # Mock environmentExists
        monkeypatch.setattr(manager, "environmentExists", MagicMock(return_value=True))

        # Create external environment
        env = ExternalEnvironment(env_name, manager)
        manager.environments[env_name] = env

        # Mock delete on the environment
        delete_mock = MagicMock()
        monkeypatch.setattr(env, "delete", delete_mock)

        # Mock manager.createFromConfig
        new_env = ExternalEnvironment(env_name, manager)
        create_from_config_mock = MagicMock(return_value=new_env)
        monkeypatch.setattr(manager, "createFromConfig", create_from_config_mock)

        env.update(config_path, optionalDependencies=optional_deps)

        # Should call delete
        delete_mock.assert_called_once()
        # Should call createFromConfig with config path and optional deps
        create_from_config_mock.assert_called_once()
        call_args, call_kwargs = create_from_config_mock.call_args
        assert call_args[0] == env_name
        assert call_kwargs.get("configPath") == config_path
        assert call_kwargs.get("optionalDependencies") == optional_deps

    def test_update_with_environment_name_and_config(self, environment_manager_fixture, monkeypatch):
        """Test that update works with config file."""
        manager, mock_execute_output, _ = environment_manager_fixture
        env_name = "test-env"
        config_path = Path("/path/to/pixi.toml")

        # Mock environmentExists
        monkeypatch.setattr(manager, "environmentExists", MagicMock(return_value=True))

        # Create external environment
        env = ExternalEnvironment(env_name, manager)
        manager.environments[env_name] = env

        # Mock delete on the environment
        delete_mock = MagicMock()
        monkeypatch.setattr(env, "delete", delete_mock)

        # Mock manager.createFromConfig
        new_env = ExternalEnvironment(env_name, manager)
        create_from_config_mock = MagicMock(return_value=new_env)
        monkeypatch.setattr(manager, "createFromConfig", create_from_config_mock)

        env.update(config_path)

        # Should call createFromConfig
        create_from_config_mock.assert_called_once()
        call_args, call_kwargs = create_from_config_mock.call_args
        assert call_args[0] == env_name
        assert call_kwargs.get("configPath") == config_path

    def test_update_with_additional_commands(self, environment_manager_fixture, monkeypatch):
        """Test that update passes additional install commands through to create."""
        manager, mock_execute_output, _ = environment_manager_fixture
        env_name = "test-env"
        dependencies: Dependencies = {"pip": ["requests"]}
        additional_commands: Commands = {"mac": ["echo 'test'"]}

        # Mock environmentExists
        monkeypatch.setattr(manager, "environmentExists", MagicMock(return_value=True))

        # Create external environment
        env = ExternalEnvironment(env_name, manager)
        manager.environments[env_name] = env

        # Mock delete on the environment
        delete_mock = MagicMock()
        monkeypatch.setattr(env, "delete", delete_mock)

        # Mock manager.create
        new_env = ExternalEnvironment(env_name, manager)
        create_mock = MagicMock(return_value=new_env)
        monkeypatch.setattr(manager, "create", create_mock)

        env.update(dependencies, additionalInstallCommands=additional_commands)

        # Should pass additional commands to create
        create_mock.assert_called_once()
        call_args = create_mock.call_args
        assert call_args[1].get("additionalInstallCommands") == additional_commands
