# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
# SPDX-License-Identifier: AGPL-3.0
"""Tests for SkillProcessor None data handling.

Verifies that SkillProcessor raises a clear ValueError when
skill data is None, instead of falling through to the generic
'Unsupported data type' error.
"""

import shutil
import zipfile
from unittest.mock import AsyncMock, MagicMock

import pytest

from openviking.server.identity import RequestContext, Role
from openviking.utils.skill_processor import SkillProcessor
from openviking_cli.exceptions import InvalidArgumentError
from openviking_cli.session.user_id import UserIdentifier


class TestParseSkillNoneData:
    """SkillProcessor._parse_skill should reject None with a clear message."""

    def test_parse_skill_none_raises_value_error(self):
        """None data should raise ValueError with explicit message."""
        processor = SkillProcessor(vikingdb=None)
        with pytest.raises(ValueError, match="Skill data cannot be None"):
            processor._parse_skill(None)

    def test_parse_skill_none_not_unsupported_type(self):
        """None should NOT produce the generic 'Unsupported data type' message."""
        processor = SkillProcessor(vikingdb=None)
        with pytest.raises(ValueError) as exc_info:
            processor._parse_skill(None)
        assert "Unsupported data type" not in str(exc_info.value)

    def test_parse_skill_valid_dict_passes(self):
        """A valid dict should not raise."""
        processor = SkillProcessor(vikingdb=None)
        skill_dict, aux_files, base_path, cleanup_path = processor._parse_skill(
            {"name": "test-skill", "description": "A test skill"}
        )
        assert skill_dict["name"] == "test-skill"
        assert aux_files == []
        assert base_path is None
        assert cleanup_path is None

    def test_parse_skill_normalizes_hyphenated_allowed_tools(self):
        processor = SkillProcessor(vikingdb=None)
        skill_dict, _, _, _ = processor._parse_skill(
            {
                "name": "test-skill",
                "description": "A test skill",
                "allowed-tools": ["Read", "Write"],
                "tags": ["test"],
            }
        )

        assert skill_dict["allowed_tools"] == ["Read", "Write"]
        assert "allowed-tools" not in skill_dict
        assert skill_dict["tags"] == ["test"]

    def test_parse_skill_zip_returns_cleanup_path(self, tmp_path):
        processor = SkillProcessor(vikingdb=None)
        archive = tmp_path / "skill.zip"
        with zipfile.ZipFile(archive, "w") as zipf:
            zipf.writestr(
                "zip-skill/SKILL.md",
                "---\nname: zip-skill\ndescription: Zip skill\n---\n\n# Zip Skill\n",
            )
            zipf.writestr("zip-skill/helper.txt", "helper")

        skill_dict, aux_files, base_path, cleanup_path = processor._parse_skill(archive)

        assert skill_dict["name"] == "zip-skill"
        assert cleanup_path is not None
        assert cleanup_path.exists()
        assert base_path is not None
        assert base_path.exists()
        assert len(aux_files) == 1
        shutil.rmtree(cleanup_path, ignore_errors=True)

    def test_parse_skill_accepts_single_file_with_non_standard_name(self, tmp_path):
        processor = SkillProcessor(vikingdb=None)
        skill_file = tmp_path / "custom-name.md"
        skill_file.write_text(
            "---\nname: custom-name\ndescription: Custom filename skill\n---\n\n# Skill\n",
            encoding="utf-8",
        )

        skill_dict, aux_files, base_path, cleanup_path = processor._parse_skill(skill_file)

        assert skill_dict["name"] == "custom-name"
        assert skill_dict["source_path"].endswith("custom-name.md")
        assert aux_files == []
        assert base_path is None
        assert cleanup_path is None

    def test_parse_skill_prefers_source_path_hint_for_single_file(self, tmp_path):
        processor = SkillProcessor(vikingdb=None)
        uploaded_path = tmp_path / "upload_123.md"
        uploaded_path.write_text(
            "---\nname: hinted-skill\ndescription: Hint source path\n---\n\n# Skill\n",
            encoding="utf-8",
        )

        skill_dict, _, _, _ = processor._parse_skill(
            uploaded_path,
            source_path_hint="hinted-skill.md",
        )

        assert skill_dict["source_path"] == "hinted-skill.md"

    @pytest.mark.parametrize("skill_dict", [{}, {"description": "missing name"}])
    def test_validate_skill_dict_requires_name_field(self, skill_dict):
        """Dict skill data should fail fast when required metadata is missing."""
        processor = SkillProcessor(vikingdb=None)
        with pytest.raises(InvalidArgumentError, match="Skill must have 'name' field"):
            processor._validate_skill_dict(skill_dict)

    @pytest.mark.parametrize("skill_dict", [{"name": ""}, {"name": "   "}, {"name": 123}])
    def test_validate_skill_dict_requires_non_empty_name_string(self, skill_dict):
        """Dict skill data should reject empty or non-string skill names."""
        processor = SkillProcessor(vikingdb=None)
        with pytest.raises(InvalidArgumentError, match="Skill 'name' must be a non-empty string"):
            processor._validate_skill_dict(skill_dict)

    def test_parse_skill_unsupported_type_still_raises(self):
        """Non-None unsupported types should still raise with type info."""
        processor = SkillProcessor(vikingdb=None)
        with pytest.raises(ValueError, match="Unsupported data type"):
            processor._parse_skill(12345)

    def test_parse_skill_long_raw_content_raises_oserror(self):
        """Long raw SKILL.md content should still surface path probing errors."""
        processor = SkillProcessor(vikingdb=None)
        long_description = "telemetry " * 80
        raw_skill = (
            "---\n"
            "name: telemetry-demo-skill\n"
            f"description: {long_description}\n"
            "tags:\n"
            "  - telemetry\n"
            "---\n\n"
            "# Telemetry Demo Skill\n\n"
            "Use this skill to validate telemetry ingestion.\n"
        )

        with pytest.raises(OSError, match="File name too long"):
            processor._parse_skill(raw_skill)


@pytest.mark.asyncio
async def test_process_skill_preserves_hyphenated_allowed_tools_in_meta(monkeypatch):
    config = MagicMock()
    config.vlm.get_completion_async = AsyncMock(return_value="overview")
    monkeypatch.setattr(
        "openviking.utils.skill_processor.get_openviking_config",
        lambda: config,
    )

    vikingdb = MagicMock()
    vikingdb.enqueue_embedding_msg = AsyncMock(return_value=False)
    viking_fs = MagicMock()
    viking_fs.write_context = AsyncMock()

    processor = SkillProcessor(vikingdb=vikingdb)
    result = await processor.process_skill(
        data={
            "name": "dict-skill",
            "description": "Skill from dict",
            "content": "# Dict Skill",
            "allowed-tools": ["Read"],
            "tags": ["dict"],
        },
        viking_fs=viking_fs,
        ctx=RequestContext(user=UserIdentifier.the_default_user(), role=Role.ROOT),
        allow_local_path_resolution=False,
    )

    assert result["name"] == "dict-skill"
    written_content = viking_fs.write_context.await_args.kwargs["content"]
    assert "allowed-tools:" in written_content
    assert "- Read" in written_content
    assert "tags:" in written_content
    embedding_msg = vikingdb.enqueue_embedding_msg.await_args.args[0]
    assert embedding_msg.context_data["meta"]["allowed_tools"] == ["Read"]
    assert embedding_msg.context_data["meta"]["tags"] == ["dict"]
