"""Tests for SkillsResource."""

from __future__ import annotations

from pathlib import Path
from typing import Any
from unittest.mock import AsyncMock
from uuid import UUID, uuid4

import pytest

from mcpgateway_sdk.models.skill import AttachResult, Skill, SkillCatalogEntry
from mcpgateway_sdk.resources.skills import SkillsResource


@pytest.fixture
def http() -> AsyncMock:
    """Mock HTTPClient."""
    return AsyncMock()


@pytest.fixture
def skills(http: AsyncMock) -> SkillsResource:
    """SkillsResource with mocked HTTP."""
    return SkillsResource(http)


def _skill_data(**overrides: Any) -> dict[str, Any]:
    data: dict[str, Any] = {
        "id": str(uuid4()),
        "name": "doc-reader",
        "description": "Reads documents",
        "source_type": "catalog",
        "source_url": "https://example.com/skill",
        "mcp_server_id": str(uuid4()),
        "mcp_server_name": "my-server",
        "skill_metadata": {"version": "1.0"},
        "package_size_bytes": 2048,
        "portal_visible": True,
        "portal_category": "readers",
        "portal_tags": ["docs", "reader"],
        "skill_md_content": "# Doc Reader\nReads docs.",
        "created_at": "2026-01-01T00:00:00Z",
        "updated_at": "2026-01-01T00:00:00Z",
    }
    data.update(overrides)
    return data


def _catalog_entry_data(**overrides: Any) -> dict[str, Any]:
    data: dict[str, Any] = {
        "name": "pdf-reader",
        "description": "Reads PDF files",
        "source": "builtin",
        "source_url": "https://example.com/pdf-reader",
        "author": "MCPGateway",
        "version": "1.0.0",
        "tags": ["pdf", "reader"],
        "license": "MIT",
        "files": ["skill.md", "requirements.txt"],
        "metadata": {},
    }
    data.update(overrides)
    return data


def _attach_result_data() -> dict[str, Any]:
    return {
        "attachment_id": str(uuid4()),
        "tools_registered": ["read_document", "summarize_document"],
        "server_id": str(uuid4()),
        "skill_id": str(uuid4()),
    }


# ---------------------------------------------------------------------------
# _endpoints dict
# ---------------------------------------------------------------------------


class TestEndpointsDict:
    """Tests for _endpoints class variable."""

    def test_endpoints_has_list(self) -> None:
        assert SkillsResource._endpoints["list"] == ("GET", "/api/v1/skills")

    def test_endpoints_has_get(self) -> None:
        assert SkillsResource._endpoints["get"] == ("GET", "/api/v1/skills/{skill_id}")

    def test_endpoints_has_create(self) -> None:
        assert SkillsResource._endpoints["create"] == ("POST", "/api/v1/skills")

    def test_endpoints_has_update(self) -> None:
        assert SkillsResource._endpoints["update"] == (
            "PATCH",
            "/api/v1/skills/{skill_id}",
        )

    def test_endpoints_has_delete(self) -> None:
        assert SkillsResource._endpoints["delete"] == (
            "DELETE",
            "/api/v1/skills/{skill_id}",
        )

    def test_endpoints_has_upload(self) -> None:
        assert SkillsResource._endpoints["upload"] == ("POST", "/api/v1/skills/upload")

    def test_endpoints_has_templates(self) -> None:
        assert SkillsResource._endpoints["templates"] == (
            "GET",
            "/api/v1/skills/templates",
        )

    def test_endpoints_has_catalog(self) -> None:
        assert SkillsResource._endpoints["catalog"] == (
            "GET",
            "/api/v1/skills/catalog/{source}",
        )

    def test_endpoints_has_catalog_detail(self) -> None:
        assert SkillsResource._endpoints["catalog_detail"] == (
            "GET",
            "/api/v1/skills/catalog/{source}/{skill_name}",
        )

    def test_endpoints_has_import_from_catalog(self) -> None:
        assert SkillsResource._endpoints["import_from_catalog"] == (
            "POST",
            "/api/v1/skills/import",
        )

    def test_endpoints_has_download_package(self) -> None:
        assert SkillsResource._endpoints["download_package"] == (
            "GET",
            "/api/v1/skills/{skill_id}/package",
        )

    def test_endpoints_has_attach(self) -> None:
        assert SkillsResource._endpoints["attach"] == (
            "POST",
            "/api/v1/servers/{server_id}/skills/{skill_id}/attach",
        )

    def test_endpoints_has_detach(self) -> None:
        assert SkillsResource._endpoints["detach"] == (
            "DELETE",
            "/api/v1/servers/{server_id}/skills/{skill_id}/detach",
        )

    def test_endpoints_has_exactly_thirteen_entries(self) -> None:
        assert len(SkillsResource._endpoints) == 13


# ---------------------------------------------------------------------------
# list
# ---------------------------------------------------------------------------


class TestList:
    """Tests for SkillsResource.list."""

    async def test_sends_get_with_default_params(
        self, skills: SkillsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {"items": []}

        # Act
        await skills.list()

        # Assert
        http.request.assert_awaited_once_with(
            "GET",
            "/api/v1/skills",
            params={"offset": 0, "limit": 100},
        )

    async def test_sends_get_with_all_filters(
        self, skills: SkillsResource, http: AsyncMock
    ) -> None:
        # Arrange
        server_id = uuid4()
        http.request.return_value = {"items": []}

        # Act
        await skills.list(
            search="reader",
            source_type="catalog",
            server_id=server_id,
            offset=10,
            limit=25,
        )

        # Assert
        http.request.assert_awaited_once_with(
            "GET",
            "/api/v1/skills",
            params={
                "search": "reader",
                "source_type": "catalog",
                "mcp_server_id": str(server_id),
                "offset": 10,
                "limit": 25,
            },
        )

    async def test_returns_list_of_skills(self, skills: SkillsResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = {
            "items": [_skill_data(name="skill-a"), _skill_data(name="skill-b")]
        }

        # Act
        result = await skills.list()

        # Assert
        assert len(result) == 2
        assert all(isinstance(s, Skill) for s in result)
        assert result[0].name == "skill-a"
        assert result[1].name == "skill-b"

    async def test_empty_list_returns_empty(self, skills: SkillsResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = {"items": []}

        # Act
        result = await skills.list()

        # Assert
        assert result == []


# ---------------------------------------------------------------------------
# get
# ---------------------------------------------------------------------------


class TestGet:
    """Tests for SkillsResource.get."""

    async def test_sends_get_with_skill_id(self, skills: SkillsResource, http: AsyncMock) -> None:
        # Arrange
        skill_id = uuid4()
        http.request.return_value = _skill_data()

        # Act
        await skills.get(skill_id)

        # Assert
        http.request.assert_awaited_once_with("GET", f"/api/v1/skills/{skill_id}")

    async def test_accepts_string_skill_id(self, skills: SkillsResource, http: AsyncMock) -> None:
        # Arrange
        skill_id = str(uuid4())
        http.request.return_value = _skill_data()

        # Act
        await skills.get(skill_id)

        # Assert
        http.request.assert_awaited_once_with("GET", f"/api/v1/skills/{skill_id}")

    async def test_returns_skill_model(self, skills: SkillsResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = _skill_data(name="my-skill")

        # Act
        result = await skills.get(uuid4())

        # Assert
        assert isinstance(result, Skill)
        assert result.name == "my-skill"


# ---------------------------------------------------------------------------
# create
# ---------------------------------------------------------------------------


class TestCreate:
    """Tests for SkillsResource.create."""

    async def test_sends_post_with_required_fields(
        self, skills: SkillsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = _skill_data()

        # Act
        await skills.create(name="test-skill", skill_md_content="# Test")

        # Assert
        call_kwargs = http.request.call_args
        assert call_kwargs.args == ("POST", "/api/v1/skills")
        body = call_kwargs.kwargs["json"]
        assert body["name"] == "test-skill"
        assert body["skill_md_content"] == "# Test"

    async def test_sends_all_optional_fields(self, skills: SkillsResource, http: AsyncMock) -> None:
        # Arrange
        server_id = uuid4()
        http.request.return_value = _skill_data()

        # Act
        await skills.create(
            name="full-skill",
            skill_md_content="# Full",
            description="A fully configured skill",
            server_id=server_id,
            skill_metadata={"version": "2.0"},
        )

        # Assert
        body = http.request.call_args.kwargs["json"]
        assert body["name"] == "full-skill"
        assert body["skill_md_content"] == "# Full"
        assert body["description"] == "A fully configured skill"
        assert body["mcp_server_id"] == str(server_id)
        assert body["skill_metadata"] == {"version": "2.0"}

    async def test_returns_skill_model(self, skills: SkillsResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = _skill_data(name="created-skill")

        # Act
        result = await skills.create(name="created-skill", skill_md_content="# New")

        # Assert
        assert isinstance(result, Skill)
        assert result.name == "created-skill"


# ---------------------------------------------------------------------------
# update
# ---------------------------------------------------------------------------


class TestUpdate:
    """Tests for SkillsResource.update."""

    async def test_sends_patch_with_only_provided_fields(
        self, skills: SkillsResource, http: AsyncMock
    ) -> None:
        # Arrange
        skill_id = uuid4()
        http.request.return_value = _skill_data()

        # Act
        await skills.update(skill_id, name="new-name")

        # Assert
        call_kwargs = http.request.call_args
        assert call_kwargs.args == ("PATCH", f"/api/v1/skills/{skill_id}")
        body = call_kwargs.kwargs["json"]
        assert body == {"name": "new-name"}

    async def test_sends_all_optional_fields(self, skills: SkillsResource, http: AsyncMock) -> None:
        # Arrange
        skill_id = uuid4()
        server_id = uuid4()
        http.request.return_value = _skill_data()

        # Act
        await skills.update(
            skill_id,
            name="updated",
            description="Updated desc",
            skill_md_content="# Updated",
            server_id=server_id,
            skill_metadata={"v": "3"},
            portal_visible=True,
            portal_category="tools",
            portal_tags=["a", "b"],
        )

        # Assert
        body = http.request.call_args.kwargs["json"]
        assert body["name"] == "updated"
        assert body["description"] == "Updated desc"
        assert body["skill_md_content"] == "# Updated"
        assert body["mcp_server_id"] == str(server_id)
        assert body["skill_metadata"] == {"v": "3"}
        assert body["portal_visible"] is True
        assert body["portal_category"] == "tools"
        assert body["portal_tags"] == ["a", "b"]

    async def test_clear_server_link_sends_flag_and_null(
        self, skills: SkillsResource, http: AsyncMock
    ) -> None:
        # Arrange
        skill_id = uuid4()
        http.request.return_value = _skill_data()

        # Act
        await skills.update(skill_id, clear_server_link=True)

        # Assert
        body = http.request.call_args.kwargs["json"]
        assert body == {"clear_server_link": True, "mcp_server_id": None}

    async def test_does_not_include_none_fields(
        self, skills: SkillsResource, http: AsyncMock
    ) -> None:
        # Arrange
        skill_id = uuid4()
        http.request.return_value = _skill_data()

        # Act
        await skills.update(skill_id, description="only this")

        # Assert
        body = http.request.call_args.kwargs["json"]
        assert "name" not in body
        assert "skill_md_content" not in body
        assert "server_id" not in body
        assert body == {"description": "only this"}

    async def test_returns_skill_model(self, skills: SkillsResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = _skill_data(name="updated-skill")

        # Act
        result = await skills.update(uuid4(), name="updated-skill")

        # Assert
        assert isinstance(result, Skill)
        assert result.name == "updated-skill"

    async def test_raises_when_server_id_and_clear_server_link(
        self, skills: SkillsResource, http: AsyncMock
    ) -> None:
        # Act + Assert
        with pytest.raises(ValueError, match="Cannot pass both server_id and clear_server_link"):
            await skills.update(uuid4(), server_id=uuid4(), clear_server_link=True)

        # Verify no HTTP call was made
        http.request.assert_not_awaited()


# ---------------------------------------------------------------------------
# delete
# ---------------------------------------------------------------------------


class TestDelete:
    """Tests for SkillsResource.delete."""

    async def test_sends_delete_with_skill_id(
        self, skills: SkillsResource, http: AsyncMock
    ) -> None:
        # Arrange
        skill_id = uuid4()
        http.request.return_value = None

        # Act
        await skills.delete(skill_id)

        # Assert
        http.request.assert_awaited_once_with("DELETE", f"/api/v1/skills/{skill_id}")

    async def test_returns_none(self, skills: SkillsResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = None

        # Act
        result = await skills.delete(uuid4())

        # Assert
        assert result is None


# ---------------------------------------------------------------------------
# upload
# ---------------------------------------------------------------------------


class TestUpload:
    """Tests for SkillsResource.upload."""

    async def test_upload_with_content_and_filename(
        self, skills: SkillsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = _skill_data()

        # Act
        await skills.upload(content=b"# My Skill", filename="skill.md")

        # Assert
        call_kwargs = http.request.call_args
        assert call_kwargs.args == ("POST", "/api/v1/skills/upload")
        files = call_kwargs.kwargs["files"]
        assert files == {"file": ("skill.md", b"# My Skill", "application/octet-stream")}
        # No data when no server_id
        assert "data" not in call_kwargs.kwargs or call_kwargs.kwargs.get("data") is None

    async def test_upload_with_file_path(
        self, skills: SkillsResource, http: AsyncMock, tmp_path: Path
    ) -> None:
        # Arrange
        file = tmp_path / "my-skill.md"
        file.write_bytes(b"# Skill from file")
        http.request.return_value = _skill_data()

        # Act
        await skills.upload(file_path=file)

        # Assert
        call_kwargs = http.request.call_args
        files = call_kwargs.kwargs["files"]
        assert files == {"file": ("my-skill.md", b"# Skill from file", "application/octet-stream")}

    async def test_upload_with_server_id(self, skills: SkillsResource, http: AsyncMock) -> None:
        # Arrange
        server_id = uuid4()
        http.request.return_value = _skill_data()

        # Act
        await skills.upload(content=b"# Skill", filename="skill.md", server_id=server_id)

        # Assert
        call_kwargs = http.request.call_args
        data = call_kwargs.kwargs["data"]
        assert data == {"mcp_server_id": str(server_id)}

    async def test_upload_raises_without_file_or_content(
        self, skills: SkillsResource, http: AsyncMock
    ) -> None:
        # Act + Assert
        with pytest.raises(ValueError, match="file_path.*content.*filename"):
            await skills.upload()

    async def test_upload_raises_with_content_but_no_filename(
        self, skills: SkillsResource, http: AsyncMock
    ) -> None:
        # Act + Assert
        with pytest.raises(ValueError, match="filename.*required"):
            await skills.upload(content=b"data")

    async def test_returns_skill_model(self, skills: SkillsResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = _skill_data(name="uploaded")

        # Act
        result = await skills.upload(content=b"# Upload", filename="skill.md")

        # Assert
        assert isinstance(result, Skill)
        assert result.name == "uploaded"


# ---------------------------------------------------------------------------
# templates
# ---------------------------------------------------------------------------


class TestTemplates:
    """Tests for SkillsResource.templates."""

    async def test_sends_get_request(self, skills: SkillsResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = {"items": []}

        # Act
        await skills.templates()

        # Assert
        http.request.assert_awaited_once_with("GET", "/api/v1/skills/templates")

    async def test_returns_list_of_dicts(self, skills: SkillsResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = {
            "items": [
                {"name": "basic", "description": "Basic template"},
                {"name": "advanced", "description": "Advanced template"},
            ]
        }

        # Act
        result = await skills.templates()

        # Assert
        assert len(result) == 2
        assert result[0]["name"] == "basic"
        assert result[1]["name"] == "advanced"

    async def test_empty_templates(self, skills: SkillsResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = {"items": []}

        # Act
        result = await skills.templates()

        # Assert
        assert result == []


# ---------------------------------------------------------------------------
# catalog
# ---------------------------------------------------------------------------


class TestCatalog:
    """Tests for SkillsResource.catalog."""

    async def test_sends_get_with_source(self, skills: SkillsResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = {"items": []}

        # Act
        await skills.catalog("builtin")

        # Assert
        http.request.assert_awaited_once_with("GET", "/api/v1/skills/catalog/builtin", params={})

    async def test_sends_get_with_search_param(
        self, skills: SkillsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {"items": []}

        # Act
        await skills.catalog("github", search="pdf")

        # Assert
        http.request.assert_awaited_once_with(
            "GET", "/api/v1/skills/catalog/github", params={"search": "pdf"}
        )

    async def test_url_encodes_source_in_path(
        self, skills: SkillsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {"items": []}

        # Act
        await skills.catalog("https://github.com/anthropics/skills")

        # Assert — URL source must be percent-encoded in the path segment
        http.request.assert_awaited_once_with(
            "GET",
            "/api/v1/skills/catalog/https%3A%2F%2Fgithub.com%2Fanthropics%2Fskills",
            params={},
        )

    async def test_returns_list_of_catalog_entries(
        self, skills: SkillsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {
            "items": [_catalog_entry_data(name="skill-a"), _catalog_entry_data(name="skill-b")]
        }

        # Act
        result = await skills.catalog("builtin")

        # Assert
        assert len(result) == 2
        assert all(isinstance(e, SkillCatalogEntry) for e in result)
        assert result[0].name == "skill-a"


# ---------------------------------------------------------------------------
# catalog_detail
# ---------------------------------------------------------------------------


class TestCatalogDetail:
    """Tests for SkillsResource.catalog_detail."""

    async def test_sends_get_with_source_and_name(
        self, skills: SkillsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = _catalog_entry_data()

        # Act
        await skills.catalog_detail("builtin", "pdf-reader")

        # Assert
        http.request.assert_awaited_once_with("GET", "/api/v1/skills/catalog/builtin/pdf-reader")

    async def test_url_encodes_source_in_path(
        self, skills: SkillsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = _catalog_entry_data()

        # Act
        await skills.catalog_detail("https://github.com/org/repo", "my-skill")

        # Assert
        http.request.assert_awaited_once_with(
            "GET",
            "/api/v1/skills/catalog/https%3A%2F%2Fgithub.com%2Forg%2Frepo/my-skill",
        )

    async def test_returns_catalog_entry_model(
        self, skills: SkillsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = _catalog_entry_data(name="pdf-reader")

        # Act
        result = await skills.catalog_detail("builtin", "pdf-reader")

        # Assert
        assert isinstance(result, SkillCatalogEntry)
        assert result.name == "pdf-reader"
        assert result.source == "builtin"


# ---------------------------------------------------------------------------
# import_from_catalog
# ---------------------------------------------------------------------------


class TestImportFromCatalog:
    """Tests for SkillsResource.import_from_catalog."""

    async def test_sends_post_with_required_fields(
        self, skills: SkillsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = _skill_data()

        # Act
        await skills.import_from_catalog("builtin", "pdf-reader")

        # Assert
        call_kwargs = http.request.call_args
        assert call_kwargs.args == ("POST", "/api/v1/skills/import")
        body = call_kwargs.kwargs["json"]
        assert body["source"] == "builtin"
        assert body["skill_name"] == "pdf-reader"

    async def test_sends_optional_server_id(self, skills: SkillsResource, http: AsyncMock) -> None:
        # Arrange
        server_id = uuid4()
        http.request.return_value = _skill_data()

        # Act
        await skills.import_from_catalog("builtin", "pdf-reader", server_id=server_id)

        # Assert
        body = http.request.call_args.kwargs["json"]
        assert body["mcp_server_id"] == str(server_id)

    async def test_sends_optional_github_repo(
        self, skills: SkillsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = _skill_data()

        # Act
        await skills.import_from_catalog("github", "my-skill", github_repo="org/repo")

        # Assert
        body = http.request.call_args.kwargs["json"]
        assert body["github_repo"] == "org/repo"

    async def test_returns_skill_model(self, skills: SkillsResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = _skill_data(name="imported-skill")

        # Act
        result = await skills.import_from_catalog("builtin", "pdf-reader")

        # Assert
        assert isinstance(result, Skill)
        assert result.name == "imported-skill"


# ---------------------------------------------------------------------------
# download_package
# ---------------------------------------------------------------------------


class TestDownloadPackage:
    """Tests for SkillsResource.download_package."""

    async def test_sends_get_with_skill_id(self, skills: SkillsResource, http: AsyncMock) -> None:
        # Arrange
        skill_id = uuid4()
        http.request_raw.return_value = b"PKzip-content"

        # Act
        await skills.download_package(skill_id)

        # Assert
        http.request_raw.assert_awaited_once_with("GET", f"/api/v1/skills/{skill_id}/package")

    async def test_returns_bytes(self, skills: SkillsResource, http: AsyncMock) -> None:
        # Arrange
        http.request_raw.return_value = b"\x50\x4b\x03\x04"

        # Act
        result = await skills.download_package(uuid4())

        # Assert
        assert isinstance(result, bytes)
        assert result == b"\x50\x4b\x03\x04"


# ---------------------------------------------------------------------------
# attach
# ---------------------------------------------------------------------------


class TestAttach:
    """Tests for SkillsResource.attach."""

    async def test_sends_post_with_server_and_skill_ids(
        self, skills: SkillsResource, http: AsyncMock
    ) -> None:
        # Arrange
        skill_id = uuid4()
        server_id = uuid4()
        http.request.return_value = _attach_result_data()

        # Act
        await skills.attach(skill_id, server_id)

        # Assert
        http.request.assert_awaited_once_with(
            "POST",
            f"/api/v1/servers/{server_id}/skills/{skill_id}/attach",
        )

    async def test_returns_attach_result_model(
        self, skills: SkillsResource, http: AsyncMock
    ) -> None:
        # Arrange
        data = _attach_result_data()
        http.request.return_value = data

        # Act
        result = await skills.attach(uuid4(), uuid4())

        # Assert
        assert isinstance(result, AttachResult)
        assert result.tools_registered == ["read_document", "summarize_document"]


# ---------------------------------------------------------------------------
# detach
# ---------------------------------------------------------------------------


class TestDetach:
    """Tests for SkillsResource.detach."""

    async def test_sends_delete_with_server_and_skill_ids(
        self, skills: SkillsResource, http: AsyncMock
    ) -> None:
        # Arrange
        skill_id = uuid4()
        server_id = uuid4()
        http.request.return_value = None

        # Act
        await skills.detach(skill_id, server_id)

        # Assert
        http.request.assert_awaited_once_with(
            "DELETE",
            f"/api/v1/servers/{server_id}/skills/{skill_id}/detach",
        )

    async def test_returns_none(self, skills: SkillsResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = None

        # Act
        result = await skills.detach(uuid4(), uuid4())

        # Assert
        assert result is None
