import asyncio
import io
import zipfile
from types import SimpleNamespace

import pytest

from beaver.engine.providers.base import LLMProvider, LLMResponse
from beaver.interfaces.web.app import _create_skill_upload_draft, _rewrite_uploaded_skill_draft_with_llm
from beaver.services.skillhub_service import SkillHubService
from beaver.skills.authoring.format import is_canonical_skill_body
from beaver.skills.catalog.utils import extract_required_tool_names
from beaver.skills.drafts import DraftService
from beaver.skills.specs import SkillSpecStore
from beaver.tools.mcp.wrapper import MCPToolWrapper


class RewriteProvider(LLMProvider):
    def __init__(self) -> None:
        super().__init__()
        self.messages = []

    async def chat(self, messages, tools=None, model=None, max_tokens=None, temperature=0.7, thinking_enabled=None):
        self.messages = messages
        return LLMResponse(
            content="""{
              "frontmatter": {
                "name": "skill",
                "description": "Use when uploaded skill guidance needs QA formatting.",
                "tools": ["read_file"]
              },
              "content": "# Skill\\n\\n## Overview\\n\\nLLM rewritten overview.\\n\\n## When to Use\\n\\n- Use when testing upload rewrite.\\n\\n## Required Tools\\n\\n- `read_file`\\n\\n## Workflow\\n\\n- Follow the rewritten workflow.\\n\\n## Validation\\n\\n- Verify the result.\\n\\n## Boundaries\\n\\n- Stay in scope.\\n\\n## Anti-Patterns\\n\\n- Do not skip rewrite validation.\\n",
              "change_reason": "normalized upload"
            }""",
            model=model,
        )

    def get_default_model(self):
        return "rewrite-model"


class FakeSkillHubService(SkillHubService):
    async def _get_json(self, path, *, params=None):
        if path == "/skills":
            return {
                "data": {
                    "items": [
                        {
                            "slug": "multi-search-engine",
                            "displayName": "multi-search-engine",
                            "summary": "search",
                            "namespace": "global",
                            "downloadCount": 1,
                            "starCount": 0,
                            "publishedVersion": {"version": "20260413.065325"},
                        }
                    ],
                    "total": 1,
                    "page": 0,
                    "size": 12,
                }
            }
        if path == "/skills/global/multi-search-engine":
            return {
                "data": {
                    "slug": "multi-search-engine",
                    "displayName": "multi-search-engine",
                    "summary": "search",
                    "namespace": "global",
                    "downloadCount": 1,
                    "starCount": 0,
                    "publishedVersion": {"version": "20260413.065325"},
                }
            }
        if path == "/skills/global/multi-search-engine/versions/20260413.065325":
            return {"data": {"version": "20260413.065325"}}
        if path == "/skills/global/multi-search-engine/versions/20260413.065325/files":
            return {"data": [{"filePath": "SKILL.md", "fileSize": 93}, {"filePath": "references/a.txt", "fileSize": 2}]}
        raise AssertionError(path)

    async def _get_text(self, path, *, params):
        if params["path"] == "SKILL.md":
            return "---\nname: multi-search-engine\ndescription: Multi search\ntools:\n  - web_search\n---\nUse search.\n"
        return "ok"


def test_skillhub_search_detail_do_not_install_until_post_install(tmp_path):
    store = SkillSpecStore(tmp_path)
    service = FakeSkillHubService(store)

    search = asyncio.run(service.search(q="multi-search-engine"))
    detail = asyncio.run(service.detail("global", "multi-search-engine"))
    assert search["items"][0]["installed"] is False
    assert detail["installed"] is False
    assert store.get_skill_spec("multi-search-engine") is None

    install = asyncio.run(service.install("global", "multi-search-engine"))
    assert install["ok"] is True
    assert store.get_skill_spec("multi-search-engine") is not None
    assert (tmp_path / "skills" / "multi-search-engine" / "versions" / install["version"] / "references" / "a.txt").read_text() == "ok"


def test_upload_skill_zip_rejects_path_traversal(tmp_path):
    store = SkillSpecStore(tmp_path)
    loaded = SimpleNamespace(skill_spec_store=store, draft_service=DraftService(store))
    buffer = io.BytesIO()
    with zipfile.ZipFile(buffer, "w") as archive:
        archive.writestr("skill/SKILL.md", "---\nname: skill\n---\nBody\n")
        archive.writestr("skill/../evil.txt", "x")

    with pytest.raises(ValueError, match="Unsafe archive entry"):
        _create_skill_upload_draft(loaded, "skill.zip", buffer.getvalue())


def test_upload_skill_zip_keeps_supporting_files_on_draft(tmp_path):
    store = SkillSpecStore(tmp_path)
    loaded = SimpleNamespace(skill_spec_store=store, draft_service=DraftService(store))
    buffer = io.BytesIO()
    with zipfile.ZipFile(buffer, "w") as archive:
        archive.writestr("skill/SKILL.md", "---\nname: skill\n---\nBody\n")
        archive.writestr("skill/references/a.txt", "context")

    draft = _create_skill_upload_draft(loaded, "skill.zip", buffer.getvalue())
    upload_dir = draft["evidence_refs"][0]["supporting_upload_dir"]
    assert (tmp_path / "skills" / "skill" / "draft_uploads" / draft["draft_id"] / "references" / "a.txt").read_text() == "context"
    assert upload_dir.endswith(draft["draft_id"])


def test_upload_skill_zip_canonicalizes_uploaded_skill_body(tmp_path):
    store = SkillSpecStore(tmp_path)
    loaded = SimpleNamespace(skill_spec_store=store, draft_service=DraftService(store))
    buffer = io.BytesIO()
    with zipfile.ZipFile(buffer, "w") as archive:
        archive.writestr(
            "skill/SKILL.md",
            "---\nname: skill\ndescription: raw upload\ntools:\n  - read_file\n---\nBody without our format.\n",
        )

    draft = _create_skill_upload_draft(loaded, "skill.zip", buffer.getvalue())

    assert draft["proposed_frontmatter"]["name"] == "skill"
    assert draft["proposed_frontmatter"]["tools"] == ["read_file"]
    assert is_canonical_skill_body(draft["proposed_content"])


def test_upload_skill_zip_infers_weather_web_tools_from_content(tmp_path):
    store = SkillSpecStore(tmp_path)
    loaded = SimpleNamespace(skill_spec_store=store, draft_service=DraftService(store))
    buffer = io.BytesIO()
    with zipfile.ZipFile(buffer, "w") as archive:
        archive.writestr(
            "weather_search/skills.md",
            "---\nname: weather-search\ndescription: weather lookup\n---\nLook up current weather and forecast for a city online.\n",
        )

    draft = _create_skill_upload_draft(loaded, "weather_search.zip", buffer.getvalue())

    assert draft["proposed_frontmatter"]["tools"] == ["web_fetch", "web_search"]
    assert extract_required_tool_names(draft["proposed_content"]) == ["web_fetch", "web_search"]
    assert is_canonical_skill_body(draft["proposed_content"])


def test_upload_skill_llm_rewrite_updates_draft(tmp_path):
    store = SkillSpecStore(tmp_path)
    draft_service = DraftService(store)
    draft = draft_service.create_new_skill_draft(
        skill_name="skill",
        proposed_content="# Skill\n\n## Overview\n\nFallback.",
        proposed_frontmatter={"name": "skill", "description": "fallback", "tools": ["read_file"]},
        created_by="test",
        reason="upload",
    )
    provider = RewriteProvider()
    agent_service = SimpleNamespace(
        _make_provider_bundle_for_task=lambda _loaded, _kwargs: SimpleNamespace(
            main_provider=provider,
            main_runtime=SimpleNamespace(model="rewrite-model"),
        )
    )
    loaded = SimpleNamespace(skill_spec_store=store, draft_service=draft_service)

    asyncio.run(_rewrite_uploaded_skill_draft_with_llm(agent_service, loaded, draft, filename="skill.zip"))
    rewritten = draft_service.get_draft("skill", draft.draft_id)

    assert rewritten is not None
    assert "LLM rewritten overview" in rewritten.proposed_content
    assert is_canonical_skill_body(rewritten.proposed_content)
    assert "Canonical Beaver SKILL.md format" in provider.messages[1]["content"]
    assert "Available runtime tool names" in provider.messages[1]["content"]


def test_upload_skill_zip_accepts_nested_single_skill_directory(tmp_path):
    store = SkillSpecStore(tmp_path)
    loaded = SimpleNamespace(skill_spec_store=store, draft_service=DraftService(store))
    buffer = io.BytesIO()
    with zipfile.ZipFile(buffer, "w") as archive:
        archive.writestr(
            "plugin/skills/nested-skill/SKILL.md",
            "---\nname: nested-skill\ndescription: nested\n---\nBody\n",
        )
        archive.writestr("plugin/skills/nested-skill/references/a.txt", "context")
        archive.writestr("plugin/README.md", "ignore package file")

    draft = _create_skill_upload_draft(loaded, "plugin.zip", buffer.getvalue())

    assert draft["skill_name"] == "nested-skill"
    upload_dir = draft["evidence_refs"][0]["supporting_upload_dir"]
    assert (tmp_path / "skills" / "nested-skill" / "draft_uploads" / draft["draft_id"] / "references" / "a.txt").read_text() == "context"
    assert "README.md" not in draft["evidence_refs"][0]["files"]


def test_upload_skill_zip_accepts_common_skill_markdown_name_aliases(tmp_path):
    store = SkillSpecStore(tmp_path)
    loaded = SimpleNamespace(skill_spec_store=store, draft_service=DraftService(store))
    buffer = io.BytesIO()
    with zipfile.ZipFile(buffer, "w") as archive:
        archive.writestr(
            "weather_search/skills.md",
            "---\nname: weather-search\ndescription: weather lookup\n---\nBody\n",
        )

    draft = _create_skill_upload_draft(loaded, "weather_search.zip", buffer.getvalue())

    assert draft["skill_name"] == "weather-search"
    assert draft["proposed_frontmatter"]["name"] == "weather-search"
    assert is_canonical_skill_body(draft["proposed_content"])


def test_mcp_wrapper_metadata_preserves_server_id_with_underscores():
    tool_def = SimpleNamespace(name="auth_status", description="Auth", inputSchema={"type": "object", "properties": {}})

    async def call_tool(_name, _arguments):
        return SimpleNamespace(content=[], structuredContent={"ok": True})

    wrapper = MCPToolWrapper(
        "outlook_mcp",
        tool_def,
        call_tool,
        kind="online",
        category="outlook",
        display_name="Outlook",
    )

    assert wrapper.spec.name == "mcp_outlook_mcp_auth_status"
    assert wrapper.spec.metadata["server_id"] == "outlook_mcp"
    assert wrapper.spec.metadata["original_tool_name"] == "auth_status"
