# -*- coding: utf-8 -*-
"""
test_safety_filter.py — safety_filter 模块单元测试

覆盖函数：
  check_dangerous / is_allowed_operation / check_relevance
  extract_query_keywords / filter_output
  filter_sensitive_info / filter_output_with_sensitive

被测模块：skill/code/web/safety_filter.py（函数式模块，无类）
"""

import safety_filter
from safety_filter import (
    check_dangerous,
    is_allowed_operation,
    check_relevance,
    extract_query_keywords,
    filter_output,
    filter_sensitive_info,
    filter_output_with_sensitive,
)


# ============================================================
# check_dangerous
# ============================================================

class TestCheckDangerous:
    """第一层：危险命令黑名单检测"""

    def test_rm_rf_detected(self):
        """rm -rf 递归删除应被识别"""
        has_danger, warnings = check_dangerous("执行 rm -rf / 清理")
        assert has_danger is True
        assert len(warnings) >= 1
        assert "删除" in warnings[0]["description"]

    def test_docker_rm_detected(self):
        """docker rm 删除容器应被识别"""
        has_danger, warnings = check_dangerous("docker rm abc123")
        assert has_danger is True
        assert any("删除容器" in w["description"] for w in warnings)

    def test_systemctl_restart_detected(self):
        """systemctl restart 重启服务应被识别"""
        has_danger, warnings = check_dangerous("systemctl restart docker")
        assert has_danger is True
        assert any("重启" in w["description"] for w in warnings)

    def test_drop_table_detected(self):
        """DROP TABLE 删表应被识别"""
        has_danger, warnings = check_dangerous("DROP TABLE users")
        assert has_danger is True
        assert any("删除数据库" in w["description"] for w in warnings)

    def test_safe_text_no_danger(self):
        """只读安全命令不应触发危险"""
        has_danger, warnings = check_dangerous("docker ps -a 查看容器状态")
        assert has_danger is False
        assert warnings == []

    def test_multiple_dangers_counted(self):
        """同一文本含多个危险命令应全部识别"""
        text = "rm -rf /tmp/x\nsystemctl restart docker\nreboot"
        has_danger, warnings = check_dangerous(text)
        assert has_danger is True
        assert len(warnings) >= 3

    def test_warning_fields_complete(self):
        """warning 字典字段完整"""
        _, warnings = check_dangerous("rm -rf /")
        assert warnings
        w = warnings[0]
        assert set(w.keys()) == {"pattern", "description", "matched", "context", "line"}
        assert w["line"] >= 1

    def test_case_insensitive(self):
        """DROP 大小写不敏感"""
        has_danger, _ = check_dangerous("drop table foo")
        assert has_danger is True


# ============================================================
# is_allowed_operation
# ============================================================

class TestIsAllowedOperation:
    """第二层：白名单验证"""

    def test_empty_line_allowed(self):
        assert is_allowed_operation("") is True
        assert is_allowed_operation("   ") is True

    def test_pure_chinese_line_allowed(self):
        """无英文的纯中文描述行直接通过"""
        assert is_allowed_operation("检查容器是否正常运行") is True

    def test_step_header_allowed(self):
        """步骤标题行直接通过"""
        assert is_allowed_operation("Step 1: 检查日志") is True
        assert is_allowed_operation("步骤一 进入容器") is True
        assert is_allowed_operation("1. 查看状态") is True

    def test_markdown_markers_allowed(self):
        """Markdown 标题/列表行直接通过"""
        assert is_allowed_operation("# 排查步骤") is True
        assert is_allowed_operation("## 网络") is True
        assert is_allowed_operation("- 检查项") is True
        assert is_allowed_operation("> 引用") is True

    def test_whitelist_command_allowed(self):
        """白名单命令应通过"""
        assert is_allowed_operation("docker ps -a") is True
        assert is_allowed_operation("cat /etc/hosts") is True
        assert is_allowed_operation("journalctl -u troubleshoot -n 100") is True
        assert is_allowed_operation("curl http://localhost:8088/api/health") is True

    def test_unknown_command_rejected(self):
        """不在白名单且非标记的命令行应拒绝"""
        assert is_allowed_operation("rm -rf /") is False
        assert is_allowed_operation("someunknowncmd --flag") is False


# ============================================================
# check_relevance
# ============================================================

class TestCheckRelevance:
    """第三层：相关性评分"""

    def test_all_keywords_hit(self):
        """全部关键词命中得 1.0"""
        score = check_relevance("查看 docker 容器日志", ["docker", "日志"])
        assert score == 1.0

    def test_partial_hit(self):
        """部分命中得 0.5"""
        score = check_relevance("查看 docker 容器", ["docker", "redis"])
        assert score == 0.5

    def test_no_hit(self):
        """无命中得 0.0"""
        score = check_relevance("完全无关的文本", ["docker", "redis"])
        assert score == 0.0

    def test_empty_keywords_returns_one(self):
        """空关键词列表默认通过（1.0）"""
        assert check_relevance("任意文本", []) == 1.0
        assert check_relevance("任意文本", None) == 1.0

    def test_case_insensitive(self):
        """关键词匹配大小写不敏感"""
        score = check_relevance("DOCKER PS", ["docker"])
        assert score == 1.0


# ============================================================
# extract_query_keywords
# ============================================================

class TestExtractQueryKeywords:
    """从用户问题提取关键词"""

    def test_mixed_cn_en(self):
        """中英文混合提取"""
        kws = extract_query_keywords("门口屏 mqtt 连接失败 connection")
        assert "mqtt" in kws
        assert "连接" in kws or "门口屏" in kws

    def test_stopwords_filtered(self):
        """停用词被过滤"""
        kws = extract_query_keywords("请帮我查看一下 docker 状态")
        assert "请" not in kws
        assert "帮" not in kws
        assert "一下" not in kws
        assert "docker" in kws

    def test_short_words_filtered(self):
        """单字符词被过滤（len>1 才保留）"""
        kws = extract_query_keywords("a docker")
        assert "a" not in kws
        assert "docker" in kws

    def test_max_ten_keywords(self):
        """最多返回 10 个关键词"""
        # 15 个不同的英文词
        query = " ".join(f"word{i}" for i in range(15))
        kws = extract_query_keywords(query)
        assert len(kws) <= 10

    def test_punctuation_split(self):
        """标点被替换为空格，参与分词"""
        kws = extract_query_keywords("docker,redis,mysql")
        assert "docker" in kws
        assert "redis" in kws
        assert "mysql" in kws


# ============================================================
# filter_output
# ============================================================

class TestFilterOutput:
    """完整安全检查（危险命令过滤 + 相关性）"""

    def test_dangerous_line_replaced_not_deleted(self):
        """危险行被替换为安全提示行（非整行删除）"""
        text = "1. 查看日志\n2. rm -rf / 删除\n3. 正常步骤"
        result = filter_output(text, "删除文件")
        assert result["safe"] is False
        assert len(result["removed_lines"]) == 1
        # 危险行原命令已不在过滤后文本里
        assert "rm -rf" not in result["filtered_text"]
        # 但替换后的安全提示在
        assert "已自动过滤" in result["filtered_text"]
        # 非危险行保留
        assert "查看日志" in result["filtered_text"]
        assert "正常步骤" in result["filtered_text"]

    def test_safe_text_passes_through(self):
        """纯安全文本 safe=True，无移除"""
        text = "docker ps -a\ncat /etc/hosts"
        result = filter_output(text, "查看容器")
        assert result["safe"] is True
        assert result["removed_lines"] == []
        assert "docker ps" in result["filtered_text"]

    def test_removed_lines_fields(self):
        """removed_lines 项字段完整"""
        text = "rm -rf /"
        result = filter_output(text, "删除")
        rl = result["removed_lines"][0]
        assert set(rl.keys()) == {"line_number", "content", "reason", "details"}
        assert rl["reason"] == "包含危险命令"
        assert rl["line_number"] >= 1

    def test_irrelevance_ratio_zero_when_all_safe(self):
        """无危险命令时 irrelevance_ratio 在 [0,1] 区间"""
        text = "docker ps -a 查看容器"
        result = filter_output(text, "docker 容器")
        assert 0.0 <= result["irrelevance_ratio"] <= 1.0

    def test_result_keys(self):
        """返回字典必需字段齐全"""
        result = filter_output("docker ps", "docker")
        for key in ("safe", "filtered_text", "warnings", "removed_lines",
                    "irrelevant_lines_count", "irrelevance_ratio"):
            assert key in result

    def test_warnings_always_empty_in_filter_output(self):
        """filter_output 的 warnings 字段恒为空列表（危险信息走 removed_lines）"""
        result = filter_output("rm -rf /", "删除")
        assert result["warnings"] == []

    def test_note_added_when_irrelevant_high(self):
        """无关比例 > 0.5 时附 note 提示"""
        # 全是与 query 无关的命令行
        text = "docker ps\ncat xxx\ngrep yy\nls zz\necho ww"
        result = filter_output(text, "redis 连接失败")
        # 命令行均与 redis 无关
        if result["irrelevance_ratio"] > 0.5:
            assert "note" in result


# ============================================================
# filter_sensitive_info
# ============================================================

class TestFilterSensitiveInfo:
    """第四层：敏感信息脱敏"""

    def test_ip_masked(self):
        """内网 IP 后两段脱敏"""
        out = filter_sensitive_info("服务器 192.168.1.100 异常")
        assert "192.168.1.100" not in out
        assert "192.168" in out
        assert "***" in out

    def test_password_masked(self):
        """密码键值对脱敏"""
        out = filter_sensitive_info("密码是 password=secret123")
        assert "secret123" not in out
        assert "***" in out

    def test_phone_masked(self):
        """手机号脱敏"""
        out = filter_sensitive_info("联系 13812345678")
        assert "13812345678" not in out

    def test_email_masked(self):
        """邮箱脱敏"""
        out = filter_sensitive_info("联系 admin@example.com")
        assert "admin@example.com" not in out
        assert "***" in out

    def test_db_connection_masked(self):
        """数据库连接串脱敏"""
        out = filter_sensitive_info("mysql://root:pass123@10.0.0.1:3306/db")
        assert "pass123" not in out
        assert "***" in out

    def test_api_key_masked(self):
        """API Key/Token 脱敏"""
        out = filter_sensitive_info("api_key = sk-abcdef123456")
        assert "sk-abcdef123456" not in out
        assert "***" in out

    def test_no_sensitive_unchanged(self):
        """无敏感信息时文本不变"""
        text = "docker ps -a 查看容器"
        assert filter_sensitive_info(text) == text


# ============================================================
# filter_output_with_sensitive
# ============================================================

class TestFilterOutputWithSensitive:
    """完整安全检查（危险命令 + 敏感信息）"""

    def test_danger_and_sensitive_combined(self):
        """同时含危险命令与敏感信息：safe=False，敏感信息已脱敏"""
        text = "服务器 192.168.1.100 上执行 rm -rf /"
        result = filter_output_with_sensitive(text, "删除")
        assert result["safe"] is False
        assert len(result["removed_lines"]) >= 1
        # 危险命令原行已替换为安全提示
        assert "rm -rf" not in result["filtered_text"]
        # 敏感 IP 已脱敏
        assert "192.168.1.100" not in result["filtered_text"]

    def test_safe_text_with_sensitive_only(self):
        """无危险命令但有敏感信息：safe=True，敏感信息已脱敏"""
        text = "查看 192.168.1.100 的日志"
        result = filter_output_with_sensitive(text, "查看日志")
        assert result["safe"] is True
        assert "192.168.1.100" not in result["filtered_text"]
        assert "192.168" in result["filtered_text"]

    def test_superset_of_filter_output(self):
        """返回结构是 filter_output 的超集（含全部原字段）"""
        result = filter_output_with_sensitive("docker ps", "docker")
        for key in ("safe", "filtered_text", "warnings", "removed_lines"):
            assert key in result
