"""Tests for launcher.wrapper module."""

from __future__ import annotations

import json
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest


class TestFindClaudeBinary:
    """Test binary discovery logic."""

    def test_finds_via_claude_bin_env(self, tmp_path: Path):
        """Should return path from CLAUDE_BIN env var when file exists."""
        binary = tmp_path / "claude"
        binary.touch()

        with patch.dict("os.environ", {"CLAUDE_BIN": str(binary)}):
            from launcher.wrapper import find_claude_binary

            result = find_claude_binary()

        assert result == str(binary)

    def test_ignores_nonexistent_claude_bin(self):
        """Should skip CLAUDE_BIN if the file does not exist."""
        with patch.dict("os.environ", {"CLAUDE_BIN": "/nonexistent/claude"}):
            with patch("shutil.which", return_value="/usr/local/bin/claude"):
                from launcher.wrapper import find_claude_binary

                result = find_claude_binary()

        assert result == "/usr/local/bin/claude"

    def test_finds_via_shutil_which(self):
        """Should find claude on PATH via shutil.which."""
        with patch.dict("os.environ", {}, clear=False):
            import os

            os.environ.pop("CLAUDE_BIN", None)
            with patch("shutil.which", return_value="/usr/bin/claude"):
                from launcher.wrapper import find_claude_binary

                result = find_claude_binary()

        assert result == "/usr/bin/claude"

    def test_finds_in_well_known_paths(self, tmp_path: Path):
        """Should check well-known installation locations."""
        with patch.dict("os.environ", {}, clear=False):
            import os

            os.environ.pop("CLAUDE_BIN", None)
            with patch("shutil.which", return_value=None):
                with patch("launcher.wrapper.Path.home", return_value=tmp_path):
                    # Create a well-known location
                    npm_bin = tmp_path / ".npm-global" / "bin" / "claude"
                    npm_bin.parent.mkdir(parents=True)
                    npm_bin.touch()

                    from launcher.wrapper import find_claude_binary

                    result = find_claude_binary()

        assert result == str(npm_bin)

    def test_raises_when_not_found(self, tmp_path: Path):
        """Should raise FileNotFoundError when binary not locatable."""
        with patch.dict("os.environ", {}, clear=False):
            import os

            os.environ.pop("CLAUDE_BIN", None)
            with patch("shutil.which", return_value=None):
                # Point home to a temp dir so well-known paths don't resolve
                with patch("launcher.wrapper.Path.home", return_value=tmp_path):
                    # Also ensure /usr/local/bin/claude and /opt/homebrew/bin/claude don't match
                    with patch.object(Path, "is_file", return_value=False):
                        import pytest

                        from launcher.wrapper import find_claude_binary

                        with pytest.raises(FileNotFoundError, match="Could not locate"):
                            find_claude_binary()


class TestBuildEnv:
    """Test environment variable construction."""

    def test_includes_claude_plugin_dir(self, tmp_path: Path):
        """Should set CLAUDE_PLUGIN_DIR in the environment."""
        home = tmp_path / ".tribunal"
        home.mkdir()
        (home / "config.json").write_text(json.dumps({"env": {}, "default_args": []}))

        with patch.dict("os.environ", {"SKILLFIELD_HOME": str(home)}):
            from launcher.wrapper import build_env

            env = build_env()

        assert "CLAUDE_PLUGIN_DIR" in env

    def test_includes_tribunal_home(self, tmp_path: Path):
        """Should set SKILLFIELD_HOME in the environment."""
        home = tmp_path / ".tribunal"
        home.mkdir()
        (home / "config.json").write_text(json.dumps({"env": {}, "default_args": []}))

        with patch.dict("os.environ", {"SKILLFIELD_HOME": str(home)}):
            from launcher.wrapper import build_env

            env = build_env()

        assert env["SKILLFIELD_HOME"] == str(home)

    def test_merges_user_env_overrides(self, tmp_path: Path):
        """Should merge env overrides from user config."""
        home = tmp_path / ".tribunal"
        home.mkdir()
        (home / "config.json").write_text(json.dumps({"env": {"CUSTOM_VAR": "hello"}, "default_args": []}))

        with patch.dict("os.environ", {"SKILLFIELD_HOME": str(home)}):
            from launcher.wrapper import build_env

            env = build_env()

        assert env["CUSTOM_VAR"] == "hello"

    def test_sets_sf_session_id_when_missing(self, tmp_path: Path):
        """Should set SF_SESSION_ID from PID when not already set."""
        home = tmp_path / ".tribunal"
        home.mkdir()
        (home / "config.json").write_text(json.dumps({"env": {}, "default_args": []}))

        env_without_sf = {"SKILLFIELD_HOME": str(home)}
        with patch.dict("os.environ", env_without_sf, clear=False):
            import os

            os.environ.pop("SF_SESSION_ID", None)
            from launcher.wrapper import build_env

            env = build_env()

        assert "SF_SESSION_ID" in env
        assert env["SF_SESSION_ID"].isdigit()

    def test_preserves_existing_sf_session_id(self, tmp_path: Path):
        """Should not overwrite SF_SESSION_ID if already set."""
        home = tmp_path / ".tribunal"
        home.mkdir()
        (home / "config.json").write_text(json.dumps({"env": {}, "default_args": []}))

        with patch.dict("os.environ", {"SKILLFIELD_HOME": str(home), "SF_SESSION_ID": "custom-123"}):
            from launcher.wrapper import build_env

            env = build_env()

        assert env["SF_SESSION_ID"] == "custom-123"


class TestLaunchClaude:
    """Test the Claude launch process."""

    def test_calls_execvpe_on_unix(self, tmp_path: Path):
        """Should exec into claude on Unix platforms."""
        home = tmp_path / ".tribunal"
        home.mkdir()
        (home / "config.json").write_text(json.dumps({"env": {}, "default_args": []}))

        with patch.dict("os.environ", {"SKILLFIELD_HOME": str(home)}):
            with patch("launcher.wrapper.verify_access", return_value=(True, "ok")):
                with patch("launcher.wrapper.find_claude_binary", return_value="/usr/bin/claude"):
                    with patch("launcher.wrapper.sys.platform", "darwin"):
                        with patch("launcher.wrapper.os.execvpe") as mock_exec:
                            with patch("launcher.wrapper.SKILLFIELD_PLUGIN_DIR", tmp_path / ".claude" / "tribunal"):
                                from launcher.wrapper import launch_claude

                                launch_claude(["--verbose"])

        mock_exec.assert_called_once()
        call_args = mock_exec.call_args
        assert call_args[0][0] == "/usr/bin/claude"
        assert "--verbose" in call_args[0][1]

    def test_appends_default_args_from_config(self, tmp_path: Path):
        """Should include default_args from config.json."""
        home = tmp_path / ".tribunal"
        home.mkdir()
        (home / "config.json").write_text(json.dumps({"env": {}, "default_args": ["--output-format", "json"]}))

        with patch.dict("os.environ", {"SKILLFIELD_HOME": str(home)}):
            with patch("launcher.wrapper.verify_access", return_value=(True, "ok")):
                with patch("launcher.wrapper.find_claude_binary", return_value="/usr/bin/claude"):
                    with patch("launcher.wrapper.sys.platform", "darwin"):
                        with patch("launcher.wrapper.os.execvpe") as mock_exec:
                            with patch("launcher.wrapper.SKILLFIELD_PLUGIN_DIR", tmp_path / ".claude" / "tribunal"):
                                from launcher.wrapper import launch_claude

                                launch_claude()

        cmd = mock_exec.call_args[0][1]
        assert "--output-format" in cmd
        assert "json" in cmd

    def test_includes_model_from_config(self, tmp_path: Path):
        """Should include --model from config.json."""
        home = tmp_path / ".tribunal"
        home.mkdir()
        (home / "config.json").write_text(json.dumps({"env": {}, "default_args": [], "model": "opus"}))

        with patch.dict("os.environ", {"SKILLFIELD_HOME": str(home)}):
            with patch("launcher.wrapper.verify_access", return_value=(True, "ok")):
                with patch("launcher.wrapper.find_claude_binary", return_value="/usr/bin/claude"):
                    with patch("launcher.wrapper.sys.platform", "darwin"):
                        with patch("launcher.wrapper.os.execvpe") as mock_exec:
                            with patch("launcher.wrapper.SKILLFIELD_PLUGIN_DIR", tmp_path / ".claude" / "tribunal"):
                                from launcher.wrapper import launch_claude

                                launch_claude()

        cmd = mock_exec.call_args[0][1]
        assert "--model" in cmd
        assert "opus" in cmd

    def test_cli_model_overrides_config_model(self, tmp_path: Path):
        """Should use CLI model when both CLI and config specify model."""
        home = tmp_path / ".tribunal"
        home.mkdir()
        (home / "config.json").write_text(json.dumps({"env": {}, "default_args": [], "model": "haiku"}))

        with patch.dict("os.environ", {"SKILLFIELD_HOME": str(home)}):
            with patch("launcher.wrapper.verify_access", return_value=(True, "ok")):
                with patch("launcher.wrapper.find_claude_binary", return_value="/usr/bin/claude"):
                    with patch("launcher.wrapper.sys.platform", "darwin"):
                        with patch("launcher.wrapper.os.execvpe") as mock_exec:
                            with patch("launcher.wrapper.SKILLFIELD_PLUGIN_DIR", tmp_path / ".claude" / "tribunal"):
                                from launcher.wrapper import launch_claude

                                launch_claude(model="sonnet")

        cmd = mock_exec.call_args[0][1]
        assert "--model" in cmd
        assert "sonnet" in cmd
        assert "haiku" not in cmd

    def test_no_model_arg_when_model_none(self, tmp_path: Path):
        """Should not include --model when model is None."""
        home = tmp_path / ".tribunal"
        home.mkdir()
        (home / "config.json").write_text(json.dumps({"env": {}, "default_args": [], "model": None}))

        with patch.dict("os.environ", {"SKILLFIELD_HOME": str(home)}):
            with patch("launcher.wrapper.verify_access", return_value=(True, "ok")):
                with patch("launcher.wrapper.find_claude_binary", return_value="/usr/bin/claude"):
                    with patch("launcher.wrapper.sys.platform", "darwin"):
                        with patch("launcher.wrapper.os.execvpe") as mock_exec:
                            with patch("launcher.wrapper.SKILLFIELD_PLUGIN_DIR", tmp_path / ".claude" / "tribunal"):
                                from launcher.wrapper import launch_claude

                                launch_claude()

        cmd = mock_exec.call_args[0][1]
        assert "--model" not in cmd

    def test_model_appears_before_default_args(self, tmp_path: Path):
        """Should insert --model before default_args in command."""
        home = tmp_path / ".tribunal"
        home.mkdir()
        (home / "config.json").write_text(json.dumps({"env": {}, "default_args": ["--verbose"], "model": "opus"}))

        with patch.dict("os.environ", {"SKILLFIELD_HOME": str(home)}):
            with patch("launcher.wrapper.verify_access", return_value=(True, "ok")):
                with patch("launcher.wrapper.find_claude_binary", return_value="/usr/bin/claude"):
                    with patch("launcher.wrapper.sys.platform", "darwin"):
                        with patch("launcher.wrapper.os.execvpe") as mock_exec:
                            with patch("launcher.wrapper.SKILLFIELD_PLUGIN_DIR", tmp_path / ".claude" / "tribunal"):
                                from launcher.wrapper import launch_claude

                                launch_claude()

        cmd = mock_exec.call_args[0][1]
        model_idx = cmd.index("--model")
        verbose_idx = cmd.index("--verbose")
        assert model_idx < verbose_idx

    def test_empty_string_model_treated_as_unset(self, tmp_path: Path):
        """Should not include --model when model is empty string."""
        home = tmp_path / ".tribunal"
        home.mkdir()
        (home / "config.json").write_text(json.dumps({"env": {}, "default_args": [], "model": ""}))

        with patch.dict("os.environ", {"SKILLFIELD_HOME": str(home)}):
            with patch("launcher.wrapper.verify_access", return_value=(True, "ok")):
                with patch("launcher.wrapper.find_claude_binary", return_value="/usr/bin/claude"):
                    with patch("launcher.wrapper.sys.platform", "darwin"):
                        with patch("launcher.wrapper.os.execvpe") as mock_exec:
                            with patch("launcher.wrapper.SKILLFIELD_PLUGIN_DIR", tmp_path / ".claude" / "tribunal"):
                                from launcher.wrapper import launch_claude

                                launch_claude()

        cmd = mock_exec.call_args[0][1]
        assert "--model" not in cmd


class TestValidateProviderConfig:
    """Tests for validate_provider_config pre-launch validation."""

    def _custom_provider_cfg(self, key: str = "sk-test-key") -> dict:
        return {
            "env": {"CUSTOM_API_KEY": key},
            "providers": {
                "custom-provider": {
                    "base_url": "https://api.custom-provider.example.com/v1",
                    "api_key_env": "CUSTOM_API_KEY",
                    "models": ["custom-provider-model-v1"],
                }
            },
        }

    def test_provider_model_with_key_present_is_valid(self):
        """Should return True when provider model has its API key configured."""
        from launcher.wrapper import validate_provider_config

        ok, msg = validate_provider_config("custom-provider-model-v1", self._custom_provider_cfg("sk-real-key"))
        assert ok is True
        assert msg == ""

    def test_provider_model_with_missing_key_returns_error(self):
        """Should return False with an actionable message when provider key is empty."""
        from unittest.mock import patch as _patch

        from launcher.wrapper import validate_provider_config

        with _patch.dict("os.environ", {"CUSTOM_API_KEY": ""}, clear=False):
            ok, msg = validate_provider_config("custom-provider-model-v1", self._custom_provider_cfg(""))
        assert ok is False
        assert "CUSTOM_API_KEY" in msg
        assert "custom-provider" in msg

    def test_provider_model_with_key_only_in_env_is_valid(self):
        """Should resolve API key from process environment when not in config.env."""
        from launcher.wrapper import validate_provider_config

        cfg = {
            "env": {},  # key not here
            "providers": {
                "custom-provider": {
                    "api_key_env": "CUSTOM_API_KEY",
                    "models": ["custom-provider-model-v1"],
                }
            },
        }
        with patch.dict("os.environ", {"CUSTOM_API_KEY": "env-key"}):
            ok, msg = validate_provider_config("custom-provider-model-v1", cfg)

        assert ok is True

    def test_standard_anthropic_model_always_valid(self):
        """Standard Anthropic models should pass validation regardless of providers config."""
        from launcher.wrapper import validate_provider_config

        cfg = self._custom_provider_cfg("")  # empty key, but model is not in providers
        for model in ("opus", "sonnet", "haiku", "claude-opus-4-5"):
            ok, msg = validate_provider_config(model, cfg)
            assert ok is True, f"Expected {model} to be valid"

    def test_no_providers_in_config_always_valid(self):
        """Should return True when no providers are configured."""
        from launcher.wrapper import validate_provider_config

        cfg = {"env": {}, "providers": {}}
        ok, _ = validate_provider_config("any-model", cfg)
        assert ok is True

    def test_provider_without_api_key_env_is_valid(self):
        """Provider models without api_key_env requirement always pass."""
        from launcher.wrapper import validate_provider_config

        cfg = {
            "env": {},
            "providers": {
                "local": {
                    "base_url": "http://localhost:8080",
                    "models": ["local-llama"],
                    # no api_key_env
                }
            },
        }
        ok, _ = validate_provider_config("local-llama", cfg)
        assert ok is True

    def test_error_message_includes_fix_hint(self):
        """Error message should tell user how to fix the problem."""
        from unittest.mock import patch as _patch

        from launcher.wrapper import validate_provider_config

        with _patch.dict("os.environ", {"CUSTOM_API_KEY": ""}, clear=False):
            ok, msg = validate_provider_config("custom-provider-model-v1", self._custom_provider_cfg(""))
        assert ok is False
        assert "config.json" in msg or "config" in msg.lower()

    def test_launch_exits_when_provider_api_key_missing(self, tmp_path: Path, capsys):
        """launch_claude should exit 1 with a clear message when provider API key is missing."""
        home = tmp_path / ".tribunal"
        home.mkdir()
        cfg = {
            "env": {"CUSTOM_API_KEY": ""},  # empty key
            "default_args": [],
            "model": "custom-provider-model-v1",
            "providers": {
                "custom-provider": {
                    "base_url": "https://api.custom-provider.example.com/v1",
                    "api_key_env": "CUSTOM_API_KEY",
                    "models": ["custom-provider-model-v1"],
                }
            },
        }
        (home / "config.json").write_text(json.dumps(cfg))

        with patch.dict("os.environ", {"SKILLFIELD_HOME": str(home), "CUSTOM_API_KEY": ""}):
            with patch("launcher.wrapper.verify_access", return_value=(True, "ok")):
                with patch("launcher.wrapper.find_claude_binary", return_value="/usr/bin/claude"):
                    with pytest.raises(SystemExit) as exc_info:
                        from launcher.wrapper import launch_claude

                        launch_claude()

        assert exc_info.value.code == 1
        captured = capsys.readouterr()
        assert "CUSTOM_API_KEY" in captured.err


class TestResolveProviderEnv:
    """Test provider-aware model routing."""

    def test_matching_model_returns_base_url_and_api_key(self):
        """Should return ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY when model matches a provider."""
        from launcher.wrapper import resolve_provider_env

        cfg = {
            "providers": {
                "custom-provider": {
                    "base_url": "https://api.custom-provider.example.com/v1",
                    "api_key_env": "CUSTOM_API_KEY",
                    "models": ["custom-provider-model-v1"],
                }
            },
            "env": {"CUSTOM_API_KEY": "sk-test-key"},
        }
        result = resolve_provider_env("custom-provider-model-v1", cfg)

        assert result["ANTHROPIC_BASE_URL"] == "https://api.custom-provider.example.com/v1"
        assert result["ANTHROPIC_API_KEY"] == "sk-test-key"

    def test_standard_model_returns_empty(self):
        """Should return empty dict for standard Anthropic models."""
        from launcher.wrapper import resolve_provider_env

        cfg = {
            "providers": {
                "custom-provider": {
                    "base_url": "https://api.custom-provider.example.com/v1",
                    "api_key_env": "CUSTOM_API_KEY",
                    "models": ["custom-provider-model-v1"],
                }
            },
            "env": {"CUSTOM_API_KEY": "sk-test-key"},
        }
        assert resolve_provider_env("opus", cfg) == {}
        assert resolve_provider_env("sonnet", cfg) == {}

    def test_missing_api_key_env_returns_only_base_url(self):
        """Should return only base_url when api_key_env is not configured."""
        from launcher.wrapper import resolve_provider_env

        cfg = {
            "providers": {
                "custom": {
                    "base_url": "https://custom.api/v1",
                    "models": ["custom-model"],
                }
            },
            "env": {},
        }
        result = resolve_provider_env("custom-model", cfg)

        assert result == {"ANTHROPIC_BASE_URL": "https://custom.api/v1"}

    def test_empty_providers_returns_empty(self):
        """Should return empty dict when no providers configured."""
        from launcher.wrapper import resolve_provider_env

        cfg = {"providers": {}, "env": {}}
        assert resolve_provider_env("any-model", cfg) == {}

    def test_api_key_resolved_from_process_env(self):
        """Should fall back to process env when key not in config env."""
        from launcher.wrapper import resolve_provider_env

        cfg = {
            "providers": {
                "external": {
                    "base_url": "https://ext.api/v1",
                    "api_key_env": "EXT_API_KEY",
                    "models": ["ext-model"],
                }
            },
            "env": {},
        }
        with patch.dict("os.environ", {"EXT_API_KEY": "env-key-123"}):
            result = resolve_provider_env("ext-model", cfg)

        assert result["ANTHROPIC_API_KEY"] == "env-key-123"

    def test_missing_providers_key_returns_empty(self):
        """Should return empty dict when providers key is absent from config."""
        from launcher.wrapper import resolve_provider_env

        cfg = {"env": {}}
        assert resolve_provider_env("any-model", cfg) == {}

    def test_models_as_string_does_not_substring_match(self):
        """Should not match if models is a string (misconfigured) - prevents substring false positives."""
        from launcher.wrapper import resolve_provider_env

        # If models is a string "claude-opus-model", "opus" is a substring - must not match.
        cfg = {
            "providers": {
                "bad-config": {
                    "base_url": "https://bad.api/v1",
                    "models": "claude-opus-model",  # string instead of list
                }
            },
            "env": {},
        }
        assert resolve_provider_env("opus", cfg) == {}
        assert resolve_provider_env("claude", cfg) == {}


class TestLaunchClaudeProviderRouting:
    """Test that launch_claude injects provider env vars for third-party models."""

    def _make_config(self, tmp_path: Path, model: str) -> Path:
        home = tmp_path / ".tribunal"
        home.mkdir()
        cfg = {
            "env": {"CUSTOM_API_KEY": "sk-test-key"},
            "default_args": [],
            "model": model,
            "providers": {
                "custom-provider": {
                    "base_url": "https://api.custom-provider.example.com/v1",
                    "api_key_env": "CUSTOM_API_KEY",
                    "models": ["custom-provider-model-v1"],
                }
            },
        }
        (home / "config.json").write_text(json.dumps(cfg))
        return home

    def test_custom_provider_model_injects_anthropic_base_url(self, tmp_path: Path):
        """Should set ANTHROPIC_BASE_URL when using a custom provider model."""
        home = self._make_config(tmp_path, "custom-provider-model-v1")

        with patch.dict("os.environ", {"SKILLFIELD_HOME": str(home)}):
            with patch("launcher.wrapper.verify_access", return_value=(True, "ok")):
                with patch("launcher.wrapper.find_claude_binary", return_value="/usr/bin/claude"):
                    with patch("launcher.wrapper.sys.platform", "darwin"):
                        with patch("launcher.wrapper.os.execvpe") as mock_exec:
                            with patch("launcher.wrapper.SKILLFIELD_PLUGIN_DIR", tmp_path / ".claude" / "tribunal"):
                                from launcher.wrapper import launch_claude

                                launch_claude()

        env = mock_exec.call_args[0][2]
        assert env["ANTHROPIC_BASE_URL"] == "https://api.custom-provider.example.com/v1"
        assert env["ANTHROPIC_API_KEY"] == "sk-test-key"

    def test_opus_model_does_not_inject_provider_env(self, tmp_path: Path):
        """Should NOT set ANTHROPIC_BASE_URL when using a native Anthropic model."""
        home = self._make_config(tmp_path, "custom-provider-model-v1")

        with patch.dict("os.environ", {"SKILLFIELD_HOME": str(home)}):
            with patch("launcher.wrapper.verify_access", return_value=(True, "ok")):
                with patch("launcher.wrapper.find_claude_binary", return_value="/usr/bin/claude"):
                    with patch("launcher.wrapper.sys.platform", "darwin"):
                        with patch("launcher.wrapper.os.execvpe") as mock_exec:
                            with patch("launcher.wrapper.SKILLFIELD_PLUGIN_DIR", tmp_path / ".claude" / "tribunal"):
                                from launcher.wrapper import launch_claude

                                launch_claude(model="opus")

        env = mock_exec.call_args[0][2]
        assert "ANTHROPIC_BASE_URL" not in env


class TestLaunchClaudeExitCases:
    """Tests for launch_claude failure paths."""

    def test_exits_when_access_denied(self, tmp_path: Path, capsys):
        """Should exit with code 1 and print a message when access is denied."""
        home = tmp_path / ".tribunal"
        home.mkdir()

        with patch.dict("os.environ", {"SKILLFIELD_HOME": str(home)}):
            with patch("launcher.wrapper.verify_access", return_value=(False, "Access denied: unknown email")):
                with pytest.raises(SystemExit) as exc_info:
                    from launcher.wrapper import launch_claude

                    launch_claude()

        assert exc_info.value.code == 1
        captured = capsys.readouterr()
        assert "Access denied" in captured.err

    def test_exits_when_claude_binary_not_found(self, tmp_path: Path, capsys):
        """Should exit with code 1 when the claude binary cannot be located."""
        home = tmp_path / ".tribunal"
        home.mkdir()

        with patch.dict("os.environ", {"SKILLFIELD_HOME": str(home)}):
            with patch("launcher.wrapper.verify_access", return_value=(True, "ok")):
                with patch("launcher.wrapper.find_claude_binary", side_effect=FileNotFoundError("not found")):
                    with pytest.raises(SystemExit) as exc_info:
                        from launcher.wrapper import launch_claude

                        launch_claude()

        assert exc_info.value.code == 1
        captured = capsys.readouterr()
        assert "not found" in captured.err


class TestBuildEnvKeyValidation:
    """Security: env keys from config.json must be valid POSIX identifiers."""

    def test_valid_key_is_set(self, tmp_path):
        """Valid identifier keys from config env are injected into the environment."""
        home = tmp_path / ".tribunal"
        home.mkdir()
        (home / "config.json").write_text(json.dumps({"env": {"MY_KEY": "value"}, "default_args": []}))

        with patch.dict("os.environ", {"SKILLFIELD_HOME": str(home)}):
            from launcher.wrapper import build_env

            env = build_env()

        assert env["MY_KEY"] == "value"

    def test_invalid_key_with_special_chars_is_rejected(self, tmp_path):
        """Keys with shell-unsafe characters are silently dropped."""
        home = tmp_path / ".tribunal"
        home.mkdir()
        (home / "config.json").write_text(
            json.dumps({"env": {"GOOD_KEY": "ok", "BAD KEY": "injected", "": "empty"}, "default_args": []})
        )

        with patch.dict("os.environ", {"SKILLFIELD_HOME": str(home)}):
            from launcher.wrapper import build_env

            env = build_env()

        assert env["GOOD_KEY"] == "ok"
        assert "BAD KEY" not in env
        assert "" not in env

    def test_key_starting_with_digit_is_rejected(self, tmp_path):
        """Keys starting with a digit are rejected (not valid POSIX identifiers)."""
        home = tmp_path / ".tribunal"
        home.mkdir()
        (home / "config.json").write_text(json.dumps({"env": {"1INVALID": "bad"}, "default_args": []}))

        with patch.dict("os.environ", {"SKILLFIELD_HOME": str(home)}):
            from launcher.wrapper import build_env

            env = build_env()

        assert "1INVALID" not in env


class TestCheckPluginVersion:
    """Tests for _check_plugin_version()."""

    def test_no_plugin_json_is_silent(self, tmp_path, capsys):
        """Should not warn when plugin.json doesn't exist."""
        with patch("launcher.wrapper.SKILLFIELD_PLUGIN_DIR", tmp_path):
            from launcher.wrapper import _check_plugin_version
            _check_plugin_version()
        assert capsys.readouterr().err == ""

    def test_warns_when_launcher_older_than_min_version(self, tmp_path, capsys):
        """Should warn when running launcher is older than plugin's minLauncherVersion."""
        import json
        plugin_json = tmp_path / "plugin.json"
        plugin_json.write_text(json.dumps({"minLauncherVersion": "999.0.0"}))
        with patch("launcher.wrapper.SKILLFIELD_PLUGIN_DIR", tmp_path):
            from launcher.wrapper import _check_plugin_version
            _check_plugin_version()
        assert "upgrade" in capsys.readouterr().err.lower() or "999.0.0" in capsys.readouterr().err

    def test_silent_when_launcher_meets_min_version(self, tmp_path, capsys):
        """Should not warn when launcher version satisfies minimum."""
        import json
        plugin_json = tmp_path / "plugin.json"
        plugin_json.write_text(json.dumps({"minLauncherVersion": "0.0.1"}))
        with patch("launcher.wrapper.SKILLFIELD_PLUGIN_DIR", tmp_path):
            from launcher.wrapper import _check_plugin_version
            _check_plugin_version()
        assert capsys.readouterr().err == ""
