"""SKILL.md 静态安全 lint —— 防 ClawHub 扫描 regress。

每次 push / PR 跑。违反规则 → test gate 挂 → release / deploy-staging block。

规则（基于 2026-04-23 v0.6.0 VT Code insights 误判复盘）：
1. SKILL.md 必须有 `## 安全` / `## Security` 段
2. 如果有 `<user_input>` 样例占位，必须在安全段声明 "DSL / 白名单 / 不经 shell"
3. 如果提敏感数据（手机号 / 身份证 / cert_id），必须声明 "hash" 或 "HMAC"
4. 如果提 vault / credential，必须声明 "服务端 / 不装 / skill user"

SOP 对齐：brain/sop/services/publish.md 发布前 review 清单。
"""

from __future__ import annotations

import re
from pathlib import Path

import pytest

SKILL_MD = Path(__file__).resolve().parent.parent / "SKILL.md"


@pytest.fixture(scope="module")
def skill_text() -> str:
    assert SKILL_MD.exists(), f"SKILL.md 不存在: {SKILL_MD}"
    return SKILL_MD.read_text(encoding="utf-8")


@pytest.fixture(scope="module")
def security_section(skill_text: str) -> str:
    """抽 `## 安全` 或 `## Security` 段到下一个 `##` 之前的内容。"""
    m = re.search(r"(?:^|\n)##\s+(安全|Security)\s*\n([\s\S]+?)(?=\n##\s|\Z)", skill_text)
    if not m:
        return ""
    return m.group(2)


# ─── Rule 1: 必须有「安全」段 ────────────────────────────


def test_has_security_section(security_section: str):
    """SKILL.md 必须有 `## 安全` 段（给扫描器 AI 读到安全 context）。"""
    assert security_section, (
        "SKILL.md 缺 `## 安全` / `## Security` 段。"
        "参考 v0.6.1 SKILL.md 的范式，加一段显式披露安全机制，"
        "否则 ClawHub VT Code insights 可能从语法 / 关键字推断风险标 suspicious。"
        "见 brain/sop/services/publish.md 清单。"
    )


# ─── Rule 2: 用户输入占位 → 必须声明非 shell ───────────


def test_user_input_placeholders_have_dsl_disclaimer(skill_text: str, security_section: str):
    """样例里有 `<占位>`（如 `<where>` / `<姓名>`）→ 安全段必须声明 DSL / 白名单 / 非 shell。"""
    # 匹配 SKILL.md 里任何 `<xxx>` 形式（命令样例中的用户输入占位）
    placeholders = re.findall(r"<[a-zA-Z_一-龥][a-zA-Z_一-龥 ]*>", skill_text)
    # 过滤掉 HTML tag / URL / 明显不是占位的（如 <br>, <a href=...>）
    placeholders = [
        p for p in placeholders if not p.lower().startswith(("<a ", "<br", "<p>", "<li"))
    ]

    if not placeholders:
        return  # 无占位 = 无需 disclaimer

    keywords = ["DSL", "白名单", "不经 shell", "shell", "参数化", "JSON body"]
    assert any(k.lower() in security_section.lower() for k in keywords), (
        f"SKILL.md 有用户输入占位 {placeholders[:3]}... 但安全段里没声明 'DSL / 白名单 / 不经 shell / 参数化'。"
        "扫描器 AI 可能推断 shell injection 风险。参考 v0.6.1 SKILL.md `## 安全` 第一条。"
    )


# ─── Rule 3: 敏感数据 → 必须声明 hash ─────────────────


def test_sensitive_data_has_hash_disclaimer(skill_text: str, security_section: str):
    """提手机号 / 身份证 / cert_id → 安全段必须说 hash / HMAC。"""
    sensitive = ["手机号", "身份证", "cert_id", "身份"]
    mentions = [w for w in sensitive if w in skill_text]
    if not mentions:
        return

    keywords = ["HMAC", "hash", "哈希", "不落明文", "不存明文", "永不存", "不落库"]
    assert any(k in security_section for k in keywords), (
        f"SKILL.md 提到敏感数据 {mentions} 但安全段没声明 hash 存储。"
        "参考 v0.6.1 SKILL.md `## 安全` 手机号 / 身份证条目。"
    )


# ─── Rule 4: Vault / credentials → 必须说边界 ─────────


def test_vault_or_credentials_has_boundary_disclaimer(skill_text: str, security_section: str):
    """提 vault 或 credential → 安全段必须声明 "服务端" 或 "不装" 边界。"""
    triggers = ["vault", "Vault", "credential", "凭证"]
    mentions = [w for w in triggers if w in skill_text]
    if not mentions:
        return

    keywords = ["服务端", "skill user 不装", "不装 vault", "不依赖", "本地 session"]
    assert any(k in security_section for k in keywords), (
        f"SKILL.md 提到凭证相关 {mentions} 但安全段没说明边界（服务端 / skill user 不装）。"
        "参考 v0.6.1 SKILL.md `## 安全` 凭证条目。"
    )


# ─── Rule 5: 网络域名 → 必须显式白名单 ───────────────


def test_network_domain_whitelisted(skill_text: str, security_section: str):
    """SKILL.md 如果提 API endpoint / 服务端 URL → 安全段应声明只访问白名单域名。"""
    triggers = ["endpoint", "xq.agentaily.com", "XIANGQIN_ENDPOINT"]
    mentions = [w for w in triggers if w in skill_text]
    if not mentions:
        return

    keywords = ["只访问", "白名单", "xq.agentaily.com", "XIANGQIN_ENDPOINT", "不向其他域"]
    assert sum(1 for k in keywords if k in security_section) >= 2, (
        "SKILL.md 提 endpoint 但安全段没清晰声明域名白名单。"
        "参考 v0.6.1 SKILL.md `## 安全` 网络条目（两个以上关键字）。"
    )


# ─── Rule 6: pip install / webhook / auto-upgrade → 必须声明供应链 + consent + opt-in ──
# 基于 2026-04-23 v0.6.3 ClawHub LLM (gpt-5-mini) 把 skill 判 suspicious 的复盘：
# LLM guidance 列了 4 条未披露风险：pip install 供应链 / auto-upgrade consent / webhook opt-in。
# 补这条 lint 防将来再提到这些关键字却忘了显式声明 → CI 挂 → 避免发出去又被 ClawHub flagged。


def test_pip_install_and_webhook_have_disclaimers(skill_text: str, security_section: str):
    """SKILL.md 提 pip install / auto-upgrade / webhook / agent_gateway_url → 安全段必须显式声明边界。"""
    # 触发器：出现任何供应链 / webhook 关键字
    supply_triggers = ["pip install", "pip install -U", "acong-tech-xiangqin"]
    webhook_triggers = ["agent_gateway_url", "agent_hooks_token", "webhook"]

    has_supply = any(t in skill_text for t in supply_triggers)
    has_webhook = any(t in skill_text for t in webhook_triggers)

    if has_supply:
        # pip 供应链 → 必须提 pin 版本 / MIT / 公开包 / consent / venv 之一
        supply_keywords = [
            "pin",
            "==",
            "MIT",
            "公开",
            "PyPI",
            "consent",
            "显式",
            "venv",
            "container",
            "隔离",
        ]
        assert sum(1 for k in supply_keywords if k in security_section) >= 2, (
            "SKILL.md 提 pip install 但安全段未声明供应链边界（pin 版本 / MIT / consent / 隔离运行）。"
            "参考 v0.6.4 SKILL.md `## 安全` pip 供应链 + auto-upgrade 两条。"
        )

    if has_webhook:
        # webhook → 必须提 opt-in / 用户提供 URL / 不预置第三方
        webhook_keywords = [
            "opt-in",
            "可选",
            "用户",
            "不配",
            "HMAC",
            "签名",
            "metadata",
            "不含明文",
        ]
        assert sum(1 for k in webhook_keywords if k in security_section) >= 2, (
            "SKILL.md 提 webhook / agent_gateway_url 但安全段未声明 opt-in 边界。"
            "参考 v0.6.4 SKILL.md `## 安全` Webhook 条目。"
        )
