# -*- coding: utf-8 -*-
"""
ShellExecTool 单元测试。

重点验证：当用户通过 `Start-Process -NoNewWindow -PassThru` 启动后台进程时，
工具不能卡死等待管道 EOF。后台进程会继承 PowerShell 的 stdout 管道，
即使 PowerShell 退出，管道仍被孙进程持有 → subprocess.run 永远读不到 EOF。

复现 bug：subprocess.run(capture_output=True) 在内部调用 communicate()，
communicate() 阻塞等待 stdout/stderr 管道 EOF。Start-Process -NoNewWindow
让孙进程继承管道写端，导致 EOF 永不到来。
"""
import subprocess
import sys
import time
import os

import pytest

from abyss.tools.shell_exec import ShellExecTool


def _run_tool_in_subprocess(queue, command: str, timeout: int):
    """在子进程中执行 ShellExecTool，避免主测试进程被卡死。

    通过 multiprocessing.Queue 把耗时和结果传回主进程。
    若子进程超时未返回，主进程会 terminate 它。
    """
    # 子进程内重新 import，避免 fork 前的状态污染
    from abyss.tools.shell_exec import ShellExecTool as _Tool
    tool = _Tool()
    start = time.monotonic()
    try:
        result = tool.execute(command=command, timeout=timeout)
        queue.put((time.monotonic() - start, result))
    except Exception as exc:
        queue.put((time.monotonic() - start, {"success": False, "error": str(exc)}))


def _cleanup_ping():
    """清理可能残留的 ping.exe 进程（仅在 Windows 测试中可能被启动）。"""
    if sys.platform == "win32":
        subprocess.run(
            "taskkill /F /IM ping.exe",
            shell=True,
            capture_output=True,
        )


@pytest.mark.skipif(sys.platform != "win32", reason="Start-Process 是 Windows 特性")
def test_start_process_does_not_block_when_spawning_background_process():
    """回归测试：Start-Process -NoNewWindow -PassThru 不能让工具卡死。

    场景：用户启动后台服务（如 python -m http.server），期望工具立即返回
    Start-Process 的 PassThru 进程对象。

    Bug 行为：subprocess.run 永远等不到管道 EOF → 工具卡死。
    期望行为：PowerShell 退出后，工具在数秒内返回。
    """
    # 用 ping -t 作为后台进程：永远运行、容易清理、不依赖 python 路径
    # -NoNewWindow 让 ping 继承 PowerShell 的 stdout 管道 → 触发 bug
    command = 'Start-Process -NoNewWindow -PassThru ping -ArgumentList \'-t\',\'127.0.0.1\''

    # multiprocessing 上下文：若工具卡死，主进程可以 terminate 子进程
    import multiprocessing
    ctx = multiprocessing.get_context("spawn")
    queue = ctx.Queue()
    proc = ctx.Process(
        target=_run_tool_in_subprocess,
        args=(queue, command, 5),
    )
    proc.start()
    # 给工具 15 秒返回（5 秒 timeout + 10 秒余量，足够正常情况）
    proc.join(timeout=15)

    try:
        if proc.is_alive():
            # 工具卡死了 — 这就是 bug
            proc.terminate()
            proc.join(timeout=5)
            pytest.fail(
                "ShellExecTool 卡死：Start-Process -NoNewWindow 后工具未在 15s 内返回"
            )
        elapsed, result = queue.get(timeout=2)
    finally:
        _cleanup_ping()

    # 期望：工具在合理时间内返回（远小于 15s）
    assert elapsed < 10, (
        f"工具耗时 {elapsed:.1f}s 过长 — Start-Process 应立即返回，"
        f"实际结果: {result}"
    )
    # Start-Process 应当成功（returncode 0）
    assert result.get("success") is True, f"命令应成功，实际: {result}"
    # PassThru 的进程对象应当被捕获到 stdout 中（验证 read1() 不被
    # TextIOWrapper 预读阻塞 — 孙进程持有管道时 read() 会卡在填缓冲区）
    stdout = result.get("stdout", "")
    assert "PING" in stdout, (
        f"PassThru 进程对象应出现在 stdout 中，实际 stdout: {stdout!r}"
    )


@pytest.mark.skipif(sys.platform != "win32", reason="Start-Process 是 Windows 特性")
def test_shell_exec_normal_command_still_returns_output():
    """回归保护：修复后台进程 bug 不能破坏普通命令的输出捕获。

    普通命令（无 Start-Process）应当照常返回 stdout/stderr。
    """
    tool = ShellExecTool()
    result = tool.execute(command="Write-Output 'hello-abyss'", timeout=10)
    assert result.get("success") is True
    assert "hello-abyss" in result.get("stdout", "")


# ========== 路径安全测试（防止 shell_exec 绕过 file_read 限制）==========

class TestShellExecPathSecurity:
    """验证 shell_exec 检测读取命令的目标路径，防止绕过 PathSecurityService。

    复现 bug：file_read 拒绝读取系统文件后，AI 改用 Get-Content 命令绕过限制。
    修复：在 shell_exec.execute 中调用 _check_read_paths 检测 cat/type/Get-Content 等
    命令的目标路径，越界路径直接拒绝。
    """

    def test_get_content_system_file_rejected(self):
        """Get-Content 读取系统 hosts 应被拒绝。"""
        tool = ShellExecTool()
        result = tool.execute(
            command='Get-Content "C:\\Windows\\System32\\drivers\\etc\\hosts"',
            timeout=5,
        )
        assert result["success"] is False
        assert "shell_exec 读取路径被拒绝" in result["error"]
        assert "hosts" in result["error"]

    def test_cat_etc_passwd_rejected(self):
        """cat 读取 /etc/passwd 应被拒绝。"""
        tool = ShellExecTool()
        result = tool.execute(command="cat /etc/passwd", timeout=5)
        assert result["success"] is False
        assert "shell_exec 读取路径被拒绝" in result["error"]

    def test_type_system_file_rejected(self):
        """type 读取系统文件应被拒绝。"""
        tool = ShellExecTool()
        result = tool.execute(
            command='type "C:\\Windows\\win.ini"',
            timeout=5,
        )
        assert result["success"] is False
        assert "shell_exec 读取路径被拒绝" in result["error"]

    def test_get_item_system_file_rejected(self):
        """Get-Item 读取系统文件应被拒绝。"""
        tool = ShellExecTool()
        result = tool.execute(
            command='Get-Item "C:\\Windows\\System32\\drivers\\etc\\hosts"',
            timeout=5,
        )
        assert result["success"] is False
        assert "shell_exec 读取路径被拒绝" in result["error"]

    def test_get_content_workspace_file_allowed(self, tmp_path):
        """Get-Content 读取工作目录内文件应放行。"""
        # 创建测试文件
        f = tmp_path / "test.txt"
        f.write_text("hello\n", encoding="utf-8")

        # 设置 PathSecurityService 工作目录为 tmp_path
        from abyss.security.path_security import get_default_service
        old_wd = get_default_service()._working_dir
        get_default_service().set_working_dir(str(tmp_path))
        try:
            tool = ShellExecTool()
            result = tool.execute(
                command=f'Get-Content "{f.name}"',
                timeout=5,
            )
            # 可能成功或失败（取决于 PowerShell），但不应被路径校验拒绝
            err = result.get("error", "") or ""
            assert "shell_exec 读取路径被拒绝" not in err
        finally:
            get_default_service().set_working_dir(old_wd)

    def test_non_read_command_not_affected(self):
        """非读取命令（如 Get-Date / ls）不应被路径校验影响。"""
        tool = ShellExecTool()
        result = tool.execute(command="Get-Date", timeout=5)
        # Get-Date 应成功执行
        assert result.get("success") is True

    def test_pipe_input_not_blocked(self):
        """管道输入的 Get-Content 不应被误判（无显式路径参数）。"""
        tool = ShellExecTool()
        # 这里没有显式路径参数，不应被 _check_read_paths 拒绝
        # 即使命令本身可能失败，也不应因路径校验失败
        result = tool.execute(
            command='Write-Output "hello" | Get-Content',
            timeout=5,
        )
        err = result.get("error", "") or ""
        assert "shell_exec 读取路径被拒绝" not in err

    def test_powershell_variable_not_blocked(self):
        """PowerShell 变量参数（$variable）不应被路径校验影响。"""
        tool = ShellExecTool()
        result = tool.execute(
            command="$p = 'test.txt'; Get-Content $p",
            timeout=5,
        )
        err = result.get("error", "") or ""
        assert "shell_exec 读取路径被拒绝" not in err
