"""
Tests for Bearer P0 security alert fixes (PR #1934).

Verifies:
- Shell injection fix: run_command(shell=True) removed from pre_prompt.py
- GPU detection uses list-based subprocess.run (no shell=True)
- open_file_location uses PathValidator for path validation
"""

import importlib.util
import subprocess
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch


def _load_pre_prompt_module():
    """Load the pre_prompt module from the cookiecutter-docker hooks directory."""
    module_path = (
        Path(__file__).resolve().parents[2]
        / "cookiecutter-docker"
        / "hooks"
        / "pre_prompt.py"
    )
    spec = importlib.util.spec_from_file_location(
        "pre_prompt", str(module_path)
    )
    module = importlib.util.module_from_spec(spec)
    # Mock cookiecutter.prompt since it may not be installed in test env
    sys.modules.setdefault("cookiecutter", MagicMock())
    sys.modules.setdefault("cookiecutter.prompt", MagicMock())
    spec.loader.exec_module(module)
    return module


class TestRunCommandRemoved:
    """Verify that the insecure run_command function has been removed."""

    def test_run_command_not_defined_in_module(self):
        """run_command(shell=True) must not exist in pre_prompt.py."""
        module = _load_pre_prompt_module()
        import inspect

        module_functions = {
            name
            for name, obj in inspect.getmembers(module, inspect.isfunction)
            if obj.__module__ == module.__name__
        }
        assert "run_command" not in module_functions, (
            "run_command should be removed from pre_prompt.py "
            "to fix shell injection"
        )

    def test_no_shell_true_in_source(self):
        """The pre_prompt.py source must not contain shell=True."""
        module_path = (
            Path(__file__).resolve().parents[2]
            / "cookiecutter-docker"
            / "hooks"
            / "pre_prompt.py"
        )
        source = module_path.read_text()
        assert "shell=True" not in source, (
            "pre_prompt.py must not use shell=True in any subprocess call"
        )


class TestCheckGpuLinux:
    """Verify check_gpu_linux uses secure subprocess invocation."""

    def test_calls_subprocess_run_with_list_args(self):
        """check_gpu_linux must call subprocess.run with a list, not a string."""
        module = _load_pre_prompt_module()
        context = {}

        with patch.object(subprocess, "run") as mock_run:
            mock_run.return_value = MagicMock(
                stdout=(
                    "00:02.0 VGA compatible controller: NVIDIA Corporation"
                ),
                returncode=0,
            )
            module.check_gpu_linux(context)

            mock_run.assert_called_once()
            call_args = mock_run.call_args
            cmd_arg = (
                call_args[0][0] if call_args[0] else call_args[1].get("args")
            )
            assert isinstance(cmd_arg, list), (
                f"subprocess.run must be called with a list, "
                f"got {type(cmd_arg)}"
            )
            assert cmd_arg == ["lspci"]

    def test_does_not_use_shell_true(self):
        """check_gpu_linux must not pass shell=True."""
        module = _load_pre_prompt_module()
        context = {}

        with patch.object(subprocess, "run") as mock_run:
            mock_run.return_value = MagicMock(stdout="", returncode=0)
            module.check_gpu_linux(context)

            call_kwargs = mock_run.call_args[1] if mock_run.call_args[1] else {}
            assert call_kwargs.get("shell") is not True, (
                "check_gpu_linux must not use shell=True"
            )

    def test_detects_nvidia_gpu(self):
        """check_gpu_linux correctly detects NVIDIA GPU from lspci output."""
        module = _load_pre_prompt_module()
        context = {}

        nvidia_output = (
            "00:02.0 VGA compatible controller: "
            "NVIDIA Corporation GeForce RTX 3080"
        )
        with patch.object(subprocess, "run") as mock_run:
            mock_run.return_value = MagicMock(
                stdout=nvidia_output,
                returncode=0,
            )
            module.check_gpu_linux(context)

            assert context["_nvidia_gpu"] is True
            assert context["_amd_gpu"] is False
            assert context["enable_gpu"] is True

    def test_detects_amd_gpu(self):
        """check_gpu_linux correctly detects AMD GPU from lspci output."""
        module = _load_pre_prompt_module()
        context = {}

        amd_output = "06:00.0 VGA compatible controller: AMD/ATI Radeon RX 7900"
        with patch.object(subprocess, "run") as mock_run:
            mock_run.return_value = MagicMock(
                stdout=amd_output,
                returncode=0,
            )
            module.check_gpu_linux(context)

            assert context["_amd_gpu"] is True
            assert context["_nvidia_gpu"] is False
            assert context["enable_gpu"] is True

    def test_no_gpu_detected(self):
        """check_gpu_linux handles no GPU detection."""
        module = _load_pre_prompt_module()
        context = {}

        intel_output = (
            "00:02.0 VGA compatible controller: Intel Corporation UHD Graphics"
        )
        with patch.object(subprocess, "run") as mock_run:
            mock_run.return_value = MagicMock(
                stdout=intel_output,
                returncode=0,
            )
            module.check_gpu_linux(context)

            assert context["_nvidia_gpu"] is False
            assert context["_amd_gpu"] is False
            assert context["enable_gpu"] is False

    def test_filters_vga_lines_in_python(self):
        """check_gpu_linux filters VGA lines in Python, not via shell pipe."""
        module = _load_pre_prompt_module()
        context = {}

        lines = [
            "00:00.0 Host bridge: Intel Corporation",
            "00:02.0 VGA compatible controller: NVIDIA Corporation",
            "00:1f.0 ISA bridge: Intel Corporation",
        ]
        lspci_output = chr(10).join(lines)
        with patch.object(subprocess, "run") as mock_run:
            mock_run.return_value = MagicMock(
                stdout=lspci_output,
                returncode=0,
            )
            module.check_gpu_linux(context)

            # subprocess.run called once with ["lspci"], not piped to grep
            mock_run.assert_called_once()
            assert context["_nvidia_gpu"] is True


class TestCheckGpuWindows:
    """Verify check_gpu_windows uses secure subprocess invocation."""

    def test_calls_subprocess_run_with_list_args(self):
        """check_gpu_windows must use subprocess.run with a list."""
        module = _load_pre_prompt_module()
        context = {}

        wmic_output = "Name" + chr(10) + "NVIDIA GeForce RTX 3080"
        with patch.object(subprocess, "run") as mock_run:
            mock_run.return_value = MagicMock(
                stdout=wmic_output,
                returncode=0,
            )
            module.check_gpu_windows(context)

            mock_run.assert_called_once()
            call_args = mock_run.call_args
            cmd_arg = (
                call_args[0][0] if call_args[0] else call_args[1].get("args")
            )
            assert isinstance(cmd_arg, list), (
                f"subprocess.run must be called with a list, "
                f"got {type(cmd_arg)}"
            )
            assert cmd_arg == [
                "wmic",
                "path",
                "win32_VideoController",
                "get",
                "name",
            ]

    def test_does_not_use_shell_true(self):
        """check_gpu_windows must not pass shell=True."""
        module = _load_pre_prompt_module()
        context = {}

        with patch.object(subprocess, "run") as mock_run:
            mock_run.return_value = MagicMock(stdout="Name", returncode=0)
            module.check_gpu_windows(context)

            call_kwargs = mock_run.call_args[1] if mock_run.call_args[1] else {}
            assert call_kwargs.get("shell") is not True, (
                "check_gpu_windows must not use shell=True"
            )

    def test_detects_nvidia_gpu(self):
        """check_gpu_windows correctly detects NVIDIA GPU."""
        module = _load_pre_prompt_module()
        context = {}

        wmic_output = "Name" + chr(10) + "NVIDIA GeForce RTX 3080"
        with patch.object(subprocess, "run") as mock_run:
            mock_run.return_value = MagicMock(
                stdout=wmic_output,
                returncode=0,
            )
            module.check_gpu_windows(context)

            assert context["_nvidia_gpu"] is True
            assert context["_amd_gpu"] is False
            assert context["enable_gpu"] is True

    def test_detects_amd_gpu(self):
        """check_gpu_windows correctly detects AMD GPU."""
        module = _load_pre_prompt_module()
        context = {}

        wmic_output = "Name" + chr(10) + "AMD Radeon RX 7900 XTX"
        with patch.object(subprocess, "run") as mock_run:
            mock_run.return_value = MagicMock(
                stdout=wmic_output,
                returncode=0,
            )
            module.check_gpu_windows(context)

            assert context["_amd_gpu"] is True
            assert context["_nvidia_gpu"] is False
            assert context["enable_gpu"] is True

    def test_detects_radeon_gpu(self):
        """check_gpu_windows detects Radeon-branded AMD GPUs."""
        module = _load_pre_prompt_module()
        context = {}

        wmic_output = "Name" + chr(10) + "Radeon RX 580"
        with patch.object(subprocess, "run") as mock_run:
            mock_run.return_value = MagicMock(
                stdout=wmic_output,
                returncode=0,
            )
            module.check_gpu_windows(context)

            assert context["_amd_gpu"] is True
            assert context["_nvidia_gpu"] is False
            assert context["enable_gpu"] is True

    def test_no_gpu_detected(self):
        """check_gpu_windows handles no GPU detection."""
        module = _load_pre_prompt_module()
        context = {}

        wmic_output = "Name" + chr(10) + "Microsoft Basic Display Adapter"
        with patch.object(subprocess, "run") as mock_run:
            mock_run.return_value = MagicMock(
                stdout=wmic_output,
                returncode=0,
            )
            module.check_gpu_windows(context)

            assert context["_nvidia_gpu"] is False
            assert context["_amd_gpu"] is False
            assert context["enable_gpu"] is False


class TestOpenFileLocationPathValidation:
    """Verify open_file_location uses PathValidator for defense-in-depth."""

    @patch("local_deep_research.research_library.utils.PathValidator")
    @patch("local_deep_research.research_library.utils.subprocess")
    def test_calls_path_validator_before_opening(
        self, mock_subprocess, mock_validator_cls
    ):
        """open_file_location must validate path via PathValidator."""
        from local_deep_research.research_library.utils import (
            open_file_location,
        )

        mock_validated_path = MagicMock()
        mock_validated_path.parent = Path("/safe/directory")
        mock_validator_cls.validate_local_filesystem_path.return_value = (
            mock_validated_path
        )
        mock_subprocess.run.return_value = MagicMock(returncode=0)

        with patch(
            "local_deep_research.research_library.utils.sys"
        ) as mock_sys:
            mock_sys.platform = "linux"
            result = open_file_location("/safe/directory/file.txt")

        mock_validator_cls.validate_local_filesystem_path.assert_called_once_with(
            "/safe/directory/file.txt"
        )
        assert result is True

    @patch("local_deep_research.research_library.utils.PathValidator")
    @patch("local_deep_research.research_library.utils.subprocess")
    def test_uses_validated_path_parent_for_folder(
        self, mock_subprocess, mock_validator_cls
    ):
        """open_file_location must use validated path parent, not raw input."""
        from local_deep_research.research_library.utils import (
            open_file_location,
        )

        mock_validated_path = MagicMock()
        mock_validated_path.parent = Path("/validated/parent")
        mock_validator_cls.validate_local_filesystem_path.return_value = (
            mock_validated_path
        )
        mock_subprocess.run.return_value = MagicMock(returncode=0)

        with patch(
            "local_deep_research.research_library.utils.sys"
        ) as mock_sys:
            mock_sys.platform = "linux"
            open_file_location("/some/raw/path/file.txt")

        mock_subprocess.run.assert_called_once()
        call_args = mock_subprocess.run.call_args[0][0]
        assert call_args == ["xdg-open", str(Path("/validated/parent"))]

    @patch("local_deep_research.research_library.utils.PathValidator")
    def test_returns_false_on_path_validation_failure(self, mock_validator_cls):
        """open_file_location returns False if PathValidator rejects path."""
        from local_deep_research.research_library.utils import (
            open_file_location,
        )

        mock_validator_cls.validate_local_filesystem_path.side_effect = (
            ValueError("Cannot access system directories")
        )

        result = open_file_location("/etc/shadow")

        assert result is False
        mock_validator_cls.validate_local_filesystem_path.assert_called_once_with(
            "/etc/shadow"
        )

    @patch("local_deep_research.research_library.utils.PathValidator")
    def test_blocks_path_traversal_via_validator(self, mock_validator_cls):
        """open_file_location blocks path traversal via PathValidator."""
        from local_deep_research.research_library.utils import (
            open_file_location,
        )

        mock_validator_cls.validate_local_filesystem_path.side_effect = (
            ValueError("Path traversal patterns not allowed")
        )

        result = open_file_location("../../etc/passwd")

        assert result is False
        mock_validator_cls.validate_local_filesystem_path.assert_called_once_with(
            "../../etc/passwd"
        )
