# -*- coding: utf-8 -*-
"""PathSecurityService 路径安全服务测试（Phase 6 Tier 1-5 AP）。

借鉴 zhikuncode-main PathSecurityService.java：
- 5 重校验：符号链接、路径穿越、工作目录边界、受保护路径、Windows 绕过
- RiskLevel 3 级分类（LOW/MEDIUM/HIGH）
- needs_confirmation 标志位
- check_dangerous_removal 命令级拦截
"""
import os
import sys
from pathlib import Path

import pytest

from abyss.security.path_security import (
    PathSecurityService,
    PathCheckResult,
    RiskLevel,
    get_default_service,
)


@pytest.fixture
def service(tmp_path) -> PathSecurityService:
    """返回独立 PathSecurityService 实例，工作目录设为 tmp_path。"""
    return PathSecurityService(working_dir=str(tmp_path))


@pytest.fixture
def make_file(tmp_path: Path):
    def _make(name: str, content: str = "hello") -> str:
        p = tmp_path / name
        p.write_text(content, encoding="utf-8")
        return str(p)
    return _make


class TestPathCheckResult:
    """PathCheckResult 工厂方法测试。"""

    def test_allowed_factory(self):
        r = PathCheckResult.allowed("/tmp/foo", risk=RiskLevel.LOW)
        assert r.allowed is True
        assert r.risk_level == RiskLevel.LOW
        assert r.needs_confirmation is False

    def test_denied_factory(self):
        r = PathCheckResult.denied("forbidden", path="/etc/shadow")
        assert r.allowed is False
        assert r.reason == "forbidden"
        assert r.risk_level == RiskLevel.HIGH

    def test_confirmation_required_factory(self):
        r = PathCheckResult.confirmation_required("symlink", path="/tmp/link")
        assert r.allowed is False
        assert r.needs_confirmation is True
        assert r.risk_level == RiskLevel.HIGH


class TestBasicPathCheck:
    """基本路径校验测试。"""

    def test_safe_path_in_working_dir_allowed(self, service, make_file):
        path = make_file("a.txt", "content")
        result = service.check_read(path)
        assert result.allowed is True
        assert result.risk_level == RiskLevel.LOW

    def test_safe_write_in_working_dir_allowed(self, service, tmp_path):
        path = str(tmp_path / "new.txt")
        result = service.check_write(path)
        assert result.allowed is True

    def test_none_path_denied(self, service):
        result = service.check_read(None)
        assert result.allowed is False
        assert "None" in result.reason or "none" in result.reason.lower()

    def test_empty_path_denied(self, service):
        result = service.check_read("")
        assert result.allowed is False


class TestPathTraversal:
    """路径穿越检测测试。"""

    def test_dotdot_traversal_blocked(self, service, tmp_path):
        path = str(tmp_path / ".." / ".." / "etc" / "passwd")
        result = service.check_read(path)
        assert result.allowed is False
        assert "traversal" in result.reason.lower() or "outside" in result.reason.lower()

    def test_dotdot_in_middle_blocked(self, service, tmp_path):
        path = str(tmp_path / "subdir" / ".." / ".." / "secret")
        result = service.check_read(path)
        assert result.allowed is False


class TestWorkspaceBoundary:
    """工作目录边界检测测试。"""

    def test_absolute_path_outside_working_dir_blocked(self, service, tmp_path):
        # /etc/passwd 在 Unix 上是绝对外部路径
        if sys.platform == "win32":
            path = "C:\\Windows\\System32\\drivers\\etc\\hosts"
        else:
            path = "/etc/passwd"
        result = service.check_read(path)
        assert result.allowed is False
        assert "outside" in result.reason.lower() or "boundary" in result.reason.lower()

    def test_tmp_outside_working_dir_can_be_confirmed(self, service):
        # /tmp 在 Unix 通常在 workspace 之外
        if sys.platform == "win32":
            pytest.skip("Windows uses different temp logic")
        result = service.check_read("/tmp/somefile.txt")
        # 不允许或需确认
        assert result.allowed is False or result.needs_confirmation is True


class TestProtectedPaths:
    """受保护路径黑名单测试。"""

    @pytest.mark.parametrize("protected", [".git", ".env", ".ssh", ".gnupg", ".aws"])
    def test_protected_directories_blocked_for_write(self, service, tmp_path, protected):
        path = str(tmp_path / protected / "config")
        result = service.check_write(path)
        assert result.allowed is False
        # 中文消息含 "受保护路径禁止写入" 或命中受保护路径名
        assert "受保护" in result.reason or protected in result.reason

    def test_protected_path_allowed_for_read_with_confirmation(self, service, tmp_path):
        # 受保护路径读取通常需要确认（不直接拒绝）
        path = str(tmp_path / ".env")
        Path(path).write_text("KEY=value", encoding="utf-8")
        result = service.check_read(path)
        # 实现可选：直接拒绝 or 需确认
        assert result.allowed is False or result.needs_confirmation is True


class TestSymlinkEscape:
    """符号链接逃逸检测测试。"""

    def test_symlink_to_sensitive_file_needs_confirmation(self, service, tmp_path):
        """符号链接指向工作目录外敏感文件应需确认或拒绝。"""
        # 创建指向工作目录外的符号链接
        if sys.platform == "win32":
            # Windows 需要管理员权限创建符号链接，跳过
            pytest.skip("Windows symlink creation requires admin")
        target = "/etc/passwd"
        link = tmp_path / "escape_link"
        try:
            os.symlink(target, str(link))
        except (OSError, PermissionError):
            pytest.skip("cannot create symlink")

        result = service.check_read(str(link))
        # 应被拒绝或需确认
        assert result.allowed is False or result.needs_confirmation is True

    def test_symlink_inside_workspace_allowed(self, service, tmp_path):
        """指向工作目录内文件的符号链接应允许。"""
        if sys.platform == "win32":
            pytest.skip("Windows symlink creation requires admin")
        target = tmp_path / "real.txt"
        target.write_text("content", encoding="utf-8")
        link = tmp_path / "link.txt"
        try:
            os.symlink(str(target), str(link))
        except (OSError, PermissionError):
            pytest.skip("cannot create symlink")

        result = service.check_read(str(link))
        assert result.allowed is True


class TestWindowsBypass:
    """Windows 路径绕过检测测试。"""

    @pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific")
    def test_ntfs_ads_denied(self, service, tmp_path):
        """NTFS ADS（file.txt:stream）应被拒绝。"""
        path = str(tmp_path / "file.txt:hidden_stream")
        result = service.check_write(path)
        assert result.allowed is False

    @pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific")
    def test_dos_device_name_denied(self, service, tmp_path):
        """DOS 设备名（CON/PRN/NUL）应被拒绝。"""
        for name in ["CON", "PRN", "NUL", "AUX"]:
            path = str(tmp_path / name)
            result = service.check_write(path)
            assert result.allowed is False, f"{name} should be blocked"

    @pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific")
    def test_8_3_short_filename_denied(self, service, tmp_path):
        """8.3 短文件名（FILENA~1.TXT）应被拒绝。"""
        path = str(tmp_path / "FILENA~1.TXT")
        result = service.check_write(path)
        assert result.allowed is False

    @pytest.mark.skipif(sys.platform == "win32", reason="Unix-specific")
    def test_unix_device_path_denied(self, service):
        """Unix 设备文件路径应被拒绝。"""
        for dev in ["/dev/zero", "/dev/null", "/dev/random"]:
            result = service.check_write(dev)
            assert result.allowed is False, f"{dev} should be blocked"


class TestDangerousRemoval:
    """危险删除命令检测测试。"""

    @pytest.mark.parametrize("cmd", [
        "rm -rf /",
        "rm -rf ~",
        "rm -rf /*",
        "rm -rf /home",
        "sudo rm -rf /",
        "rm -rf /etc",
    ])
    def test_dangerous_removal_blocked(self, service, cmd):
        result = service.check_dangerous_removal(cmd)
        assert result is not None, f"should detect: {cmd}"

    @pytest.mark.parametrize("cmd", [
        "rm ./test.log",
        "rm -rf ./build",
        "del file.txt",
        "Remove-Item ./test.txt",
        "ls",
        "echo hello",
    ])
    def test_safe_removal_allowed(self, service, cmd):
        result = service.check_dangerous_removal(cmd)
        assert result is None, f"should not block: {cmd}"

    def test_wildcard_removal_blocked(self, service):
        """rm * 等通配符应被拦截。"""
        # 仅测试根目录通配符
        result = service.check_dangerous_removal("rm -rf /*")
        assert result is not None


class TestDefaultService:
    """模块级单例测试。"""

    def test_get_default_service_returns_same_instance(self):
        s1 = get_default_service()
        s2 = get_default_service()
        assert s1 is s2

    def test_default_service_can_check_path(self, tmp_path):
        svc = get_default_service()
        # 设置工作目录为临时目录
        svc.set_working_dir(str(tmp_path))
        path = str(tmp_path / "test.txt")
        Path(path).write_text("content", encoding="utf-8")
        result = svc.check_read(path)
        assert result.allowed is True
        # 清理：避免污染后续测试的全局单例
        svc.set_working_dir(None)


class TestBackwardCompatibility:
    """向后兼容测试：find_protected_name 仍可用。"""

    def test_find_protected_name_still_works(self):
        from abyss.tools.base import find_protected_name
        assert find_protected_name("/path/.env") == ".env"
        assert find_protected_name("/normal/path") is None
