"""セキュリティ関連のテスト (パストラバーサル防止等)."""

import json
from pathlib import Path

from skill_search import SkillSearch
from skill_search.discovery import discover_resources


def _create_skill(tmp_path: Path, name: str, description: str, content: str) -> Path:
    """テスト用スキルを作成するヘルパー."""
    skill_dir = tmp_path / name
    skill_dir.mkdir(parents=True, exist_ok=True)
    (skill_dir / "SKILL.md").write_text(
        f"""---
name: {name}
description: {description}
---

{content}
""",
        encoding="utf-8",
    )
    return skill_dir


class TestDiscoveryPathTraversal:
    """discover_resources のパストラバーサル防止テスト."""

    def test_symlink_outside_skill_dir_is_excluded(self, tmp_path: Path):
        """スキルディレクトリ外へのシンボリックリンクは除外される."""
        # スキルディレクトリと外部ファイルを作成
        skill_dir = tmp_path / "my-skill"
        skill_dir.mkdir()
        (skill_dir / "SKILL.md").write_text("---\nname: x\n---\n# X", encoding="utf-8")

        # 外部の機密ファイルを作成
        secret_file = tmp_path / "secret.txt"
        secret_file.write_text("TOP SECRET DATA", encoding="utf-8")

        # スキルディレクトリ内にシンボリックリンクを作成
        link_path = skill_dir / "stolen.txt"
        try:
            link_path.symlink_to(secret_file)
        except OSError:
            # symlink 作成不可の環境ではスキップ
            return

        resources = discover_resources(skill_dir)
        resource_names = {r.name for r in resources}

        # シンボリックリンク経由のリソースは含まれないこと
        assert "stolen.txt" not in resource_names

    def test_dotdot_symlink_is_excluded(self, tmp_path: Path):
        """.. を含むシンボリックリンクは除外される."""
        skill_dir = tmp_path / "my-skill"
        docs_dir = skill_dir / "docs"
        docs_dir.mkdir(parents=True)
        (skill_dir / "SKILL.md").write_text("---\nname: x\n---\n# X", encoding="utf-8")

        # 外部ファイル
        secret = tmp_path / "etc" / "passwd"
        secret.parent.mkdir(parents=True, exist_ok=True)
        secret.write_text("root:x:0:0", encoding="utf-8")

        # docs/ から ../../etc/passwd へ相対シンボリックリンク
        link = docs_dir / "passwd.txt"
        try:
            link.symlink_to(secret)
        except OSError:
            return

        resources = discover_resources(skill_dir)
        resource_paths = {r.path for r in resources}

        assert str(secret.resolve()) not in resource_paths

    def test_normal_resources_are_included(self, tmp_path: Path):
        """正常なリソースは含まれる."""
        skill_dir = tmp_path / "my-skill"
        skill_dir.mkdir()
        (skill_dir / "SKILL.md").write_text("---\nname: x\n---\n# X", encoding="utf-8")
        (skill_dir / "README.md").write_text("# Docs", encoding="utf-8")
        (skill_dir / "data.json").write_text("{}", encoding="utf-8")

        resources = discover_resources(skill_dir)
        names = {r.name for r in resources}
        assert "README.md" in names
        assert "data.json" in names


class TestReadResourceTraversal:
    """read_resource ツールのパストラバーサル防止テスト."""

    def test_dotdot_resource_name_is_rejected(self, tmp_path: Path):
        """resource_name に '..' が含まれる場合は拒否."""
        skill_dir = _create_skill(tmp_path, "test-skill", "Test", "# Test")
        (skill_dir / "doc.md").write_text("safe doc", encoding="utf-8")

        ss = SkillSearch(skills_dirs=[tmp_path])

        # パストラバーサル攻撃の試行
        result = ss.call_tool(
            "read_resource",
            {"skill_name": "test-skill", "resource_name": "../../etc/passwd"},
        )
        data = json.loads(result)
        assert "error" in data
        assert "traversal" in data["error"].lower()

    def test_backslash_dotdot_is_rejected(self, tmp_path: Path):
        """Windows スタイルの '..' も拒否."""
        _create_skill(tmp_path, "test-skill", "Test", "# Test")
        ss = SkillSearch(skills_dirs=[tmp_path])

        result = ss.call_tool(
            "read_resource",
            {"skill_name": "test-skill", "resource_name": "..\\..\\etc\\passwd"},
        )
        data = json.loads(result)
        assert "error" in data

    def test_legitimate_resource_is_allowed(self, tmp_path: Path):
        """正当なリソースへのアクセスは許可."""
        skill_dir = _create_skill(tmp_path, "test-skill", "Test", "# Test")
        docs = skill_dir / "docs"
        docs.mkdir()
        (docs / "api.md").write_text("# API\n\nGET /users", encoding="utf-8")

        ss = SkillSearch(skills_dirs=[tmp_path])

        result = ss.call_tool(
            "read_resource",
            {"skill_name": "test-skill", "resource_name": "docs/api.md"},
        )
        data = json.loads(result)
        assert "content" in data
        assert "GET /users" in data["content"]

    def test_nonexistent_skill_is_rejected(self, tmp_path: Path):
        """存在しないスキルへのアクセスはエラー."""
        _create_skill(tmp_path, "exists", "Exists", "# Exists")
        ss = SkillSearch(skills_dirs=[tmp_path])

        result = ss.call_tool(
            "read_resource",
            {"skill_name": "evil-skill", "resource_name": "file.md"},
        )
        data = json.loads(result)
        assert "error" in data


class TestExtensionFilter:
    """ファイル拡張子フィルタのテスト."""

    def test_binary_files_excluded(self, tmp_path: Path):
        """バイナリファイルはリソースに含まれない."""
        skill_dir = tmp_path / "my-skill"
        skill_dir.mkdir()
        (skill_dir / "SKILL.md").write_text("---\nname: x\n---\n# X", encoding="utf-8")
        (skill_dir / "malware.exe").write_text("bad", encoding="utf-8")
        (skill_dir / "script.sh").write_text("#!/bin/bash", encoding="utf-8")
        (skill_dir / "image.png").write_text("PNG", encoding="utf-8")

        resources = discover_resources(skill_dir)
        names = {r.name for r in resources}

        assert "malware.exe" not in names
        assert "script.sh" not in names
        assert "image.png" not in names

    def test_allowed_extensions(self, tmp_path: Path):
        """許可された拡張子のみリソースに含まれる."""
        skill_dir = tmp_path / "my-skill"
        skill_dir.mkdir()
        (skill_dir / "SKILL.md").write_text("---\nname: x\n---\n# X", encoding="utf-8")

        allowed = {
            "doc.md": "md content",
            "data.json": "{}",
            "config.yaml": "key: val",
            "config.yml": "key: val",
            "table.csv": "a,b,c",
            "schema.xml": "<root/>",
            "notes.txt": "text",
        }
        for name, content in allowed.items():
            (skill_dir / name).write_text(content, encoding="utf-8")

        resources = discover_resources(skill_dir)
        names = {r.name for r in resources}

        for expected in allowed:
            assert expected in names
