"""Unit tests for SkillService (U6 — skill enable/disable + import/reload).

Covers:
- disable_skill → is_skill_disabled returns True
- enable_skill → is_skill_disabled returns False
- list_disabled_skills → returns correct list
- import_skill with valid YAML → file written, skill loaded
- import_skill with invalid YAML → ValueError
- import_skill with path traversal attempt → ValueError
- reload_skill → unregister + reload
- update_skill_config → YAML updated + reloaded
"""

from __future__ import annotations

from pathlib import Path

import pytest

from agentkit.server.admin.skill_service import SkillService, _validate_skill_name
from agentkit.server.auth.models import init_auth_db
from agentkit.skills.registry import SkillRegistry


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
async def fresh_db(tmp_path: Path) -> Path:
    """A brand-new auth DB on a fresh path (no data)."""
    db_path = tmp_path / "auth.db"
    await init_auth_db(db_path)
    return db_path


@pytest.fixture
def service() -> SkillService:
    return SkillService()


@pytest.fixture
def skills_dir(tmp_path: Path) -> str:
    """A temp skills directory for YAML files."""
    d = tmp_path / "skills"
    d.mkdir()
    return str(d)


@pytest.fixture
def skill_registry() -> SkillRegistry:
    return SkillRegistry()


_VALID_SKILL_YAML = """\
name: test_skill
agent_type: simple_generation
version: "1.0.0"
description: "A test skill for unit testing"
task_mode: llm_generate
execution_mode: direct
max_steps: 1
prompt:
  identity: "Test"
  instructions: "Handle test"
tools: []
"""


# ---------------------------------------------------------------------------
# Enable / disable
# ---------------------------------------------------------------------------


class TestDisableEnable:
    async def test_disable_skill_marks_disabled(self, service: SkillService, fresh_db: Path):
        result = await service.disable_skill(fresh_db, "test_skill", disabled_by="admin-1")
        assert result["skill_name"] == "test_skill"
        assert result["is_disabled"] is True
        assert result["disabled_by"] == "admin-1"
        assert "disabled_at" in result

        assert await service.is_skill_disabled(fresh_db, "test_skill") is True

    async def test_disable_skill_normalizes_name(self, service: SkillService, fresh_db: Path):
        """Skill names are normalized to lowercase before storage."""
        await service.disable_skill(fresh_db, "TestSkill")
        assert await service.is_skill_disabled(fresh_db, "testskill") is True
        assert await service.is_skill_disabled(fresh_db, "TestSkill") is True

    async def test_disable_skill_is_idempotent(self, service: SkillService, fresh_db: Path):
        """Disabling an already-disabled skill updates the row, not duplicates."""
        await service.disable_skill(fresh_db, "skill_a", disabled_by="admin-1")
        await service.disable_skill(fresh_db, "skill_a", disabled_by="admin-2")
        disabled = await service.list_disabled_skills(fresh_db)
        assert disabled == ["skill_a"]

    async def test_enable_skill_removes_disabled_mark(self, service: SkillService, fresh_db: Path):
        await service.disable_skill(fresh_db, "skill_b")
        assert await service.is_skill_disabled(fresh_db, "skill_b") is True

        enabled = await service.enable_skill(fresh_db, "skill_b")
        assert enabled is True
        assert await service.is_skill_disabled(fresh_db, "skill_b") is False

    async def test_enable_skill_returns_false_if_not_disabled(
        self, service: SkillService, fresh_db: Path
    ):
        """Enabling a skill that wasn't disabled returns False (no-op)."""
        enabled = await service.enable_skill(fresh_db, "never_disabled")
        assert enabled is False

    async def test_is_skill_disabled_returns_false_for_unknown(
        self, service: SkillService, fresh_db: Path
    ):
        assert await service.is_skill_disabled(fresh_db, "unknown_skill") is False

    async def test_list_disabled_skills_returns_sorted_list(
        self, service: SkillService, fresh_db: Path
    ):
        await service.disable_skill(fresh_db, "charlie")
        await service.disable_skill(fresh_db, "alpha")
        await service.disable_skill(fresh_db, "bravo")
        result = await service.list_disabled_skills(fresh_db)
        assert result == ["alpha", "bravo", "charlie"]

    async def test_list_disabled_skills_empty_when_none_disabled(
        self, service: SkillService, fresh_db: Path
    ):
        result = await service.list_disabled_skills(fresh_db)
        assert result == []

    async def test_disable_skill_invalid_name_raises(self, service: SkillService, fresh_db: Path):
        with pytest.raises(ValueError, match="Invalid skill name"):
            await service.disable_skill(fresh_db, "Invalid Name With Spaces")

    async def test_disable_skill_uppercase_name_normalizes(
        self, service: SkillService, fresh_db: Path
    ):
        """Uppercase names are normalized to lowercase (regex requires lowercase)."""
        # 'TEST_SKILL' normalizes to 'test_skill' which matches the regex.
        await service.disable_skill(fresh_db, "TEST_SKILL")
        assert await service.is_skill_disabled(fresh_db, "test_skill") is True


# ---------------------------------------------------------------------------
# import_skill
# ---------------------------------------------------------------------------


class TestImportSkill:
    async def test_import_skill_writes_file_and_loads(
        self,
        service: SkillService,
        skills_dir: str,
        skill_registry: SkillRegistry,
    ):
        result = await service.import_skill(
            _VALID_SKILL_YAML,
            skills_dir,
            skill_registry=skill_registry,
        )
        assert result["name"] == "test_skill"
        assert Path(result["path"]).is_file()
        # Skill should be registered in the registry.
        assert skill_registry.has_skill("test_skill")

    async def test_import_skill_without_registry_writes_file_only(
        self,
        service: SkillService,
        skills_dir: str,
    ):
        result = await service.import_skill(_VALID_SKILL_YAML, skills_dir)
        assert result["name"] == "test_skill"
        assert Path(result["path"]).is_file()

    async def test_import_skill_invalid_yaml_raises(
        self,
        service: SkillService,
        skills_dir: str,
    ):
        with pytest.raises(ValueError, match="Invalid YAML"):
            await service.import_skill("not: valid: yaml: [", skills_dir)

    async def test_import_skill_non_mapping_yaml_raises(
        self,
        service: SkillService,
        skills_dir: str,
    ):
        with pytest.raises(ValueError, match="mapping"):
            await service.import_skill("- just\n- a\n- list\n", skills_dir)

    async def test_import_skill_missing_name_field_raises(
        self,
        service: SkillService,
        skills_dir: str,
    ):
        yaml_without_name = "agent_type: simple_generation\n"
        with pytest.raises(ValueError, match="name"):
            await service.import_skill(yaml_without_name, skills_dir)

    async def test_import_skill_invalid_name_raises(
        self,
        service: SkillService,
        skills_dir: str,
    ):
        """YAML with a name that fails the regex raises ValueError."""
        bad_yaml = 'name: "Bad Name With Spaces"\nagent_type: test\n'
        with pytest.raises(ValueError, match="Invalid skill name"):
            await service.import_skill(bad_yaml, skills_dir)

    async def test_import_skill_path_traversal_blocked(
        self,
        service: SkillService,
        skills_dir: str,
    ):
        """A YAML with a name containing path separators is rejected by the regex."""
        # The regex `^[a-z0-9][a-z0-9_-]{0,63}$` rejects '/' and '..',
        # so path traversal via the name field is impossible. We verify
        # this by attempting to import a YAML with a traversal name.
        traversal_yaml = 'name: "../etc/passwd"\nagent_type: test\n'
        with pytest.raises(ValueError, match="Invalid skill name"):
            await service.import_skill(traversal_yaml, skills_dir)


# ---------------------------------------------------------------------------
# reload_skill
# ---------------------------------------------------------------------------


class TestReloadSkill:
    async def test_reload_skill_unregisters_and_reloads(
        self,
        service: SkillService,
        skills_dir: str,
        skill_registry: SkillRegistry,
    ):
        # First import the skill.
        await service.import_skill(
            _VALID_SKILL_YAML,
            skills_dir,
            skill_registry=skill_registry,
        )
        assert skill_registry.has_skill("test_skill")

        # Now reload it.
        result = await service.reload_skill("test_skill", skill_registry, skills_dir)
        assert result["name"] == "test_skill"
        assert result["status"] == "reloaded"
        assert skill_registry.has_skill("test_skill")

    async def test_reload_skill_missing_yaml_raises(
        self,
        service: SkillService,
        skills_dir: str,
        skill_registry: SkillRegistry,
    ):
        with pytest.raises(ValueError, match="not found"):
            await service.reload_skill("nonexistent", skill_registry, skills_dir)

    async def test_reload_skill_invalid_name_raises(
        self,
        service: SkillService,
        skills_dir: str,
        skill_registry: SkillRegistry,
    ):
        with pytest.raises(ValueError, match="Invalid skill name"):
            await service.reload_skill("Bad Name", skill_registry, skills_dir)


# ---------------------------------------------------------------------------
# update_skill_config
# ---------------------------------------------------------------------------


class TestUpdateSkillConfig:
    async def test_update_skill_config_updates_yaml_and_reloads(
        self,
        service: SkillService,
        skills_dir: str,
        skill_registry: SkillRegistry,
    ):
        # First import the skill.
        await service.import_skill(
            _VALID_SKILL_YAML,
            skills_dir,
            skill_registry=skill_registry,
        )

        # Patch the description.
        result = await service.update_skill_config(
            "test_skill",
            {"description": "Updated description"},
            skills_dir,
            skill_registry,
        )
        assert result["name"] == "test_skill"
        assert result["status"] == "updated"

        # Verify the YAML file was updated.
        import yaml

        with open(result["path"], encoding="utf-8") as f:
            data = yaml.safe_load(f)
        assert data["description"] == "Updated description"
        # Original fields should be preserved.
        assert data["name"] == "test_skill"
        assert data["agent_type"] == "simple_generation"

        # Skill should still be registered.
        assert skill_registry.has_skill("test_skill")

    async def test_update_skill_config_missing_yaml_raises(
        self,
        service: SkillService,
        skills_dir: str,
        skill_registry: SkillRegistry,
    ):
        with pytest.raises(ValueError, match="not found"):
            await service.update_skill_config(
                "nonexistent",
                {"description": "x"},
                skills_dir,
                skill_registry,
            )

    async def test_update_skill_config_preserves_name(
        self,
        service: SkillService,
        skills_dir: str,
        skill_registry: SkillRegistry,
    ):
        """Patching the 'name' field is ignored — name is preserved."""
        await service.import_skill(
            _VALID_SKILL_YAML,
            skills_dir,
            skill_registry=skill_registry,
        )

        result = await service.update_skill_config(
            "test_skill",
            {"name": "should_be_ignored", "description": "new"},
            skills_dir,
            skill_registry,
        )
        import yaml

        with open(result["path"], encoding="utf-8") as f:
            data = yaml.safe_load(f)
        # Name should be preserved (not renamed).
        assert data["name"] == "test_skill"
        assert data["description"] == "new"


# ---------------------------------------------------------------------------
# _validate_skill_name helper
# ---------------------------------------------------------------------------


class TestValidateSkillName:
    def test_valid_name_returns_normalized(self):
        assert _validate_skill_name("test_skill") == "test_skill"
        assert _validate_skill_name("TestSkill") == "testskill"
        assert _validate_skill_name("  spaced  ") == "spaced"
        assert _validate_skill_name("a-b_c-1") == "a-b_c-1"

    def test_invalid_name_raises(self):
        with pytest.raises(ValueError):
            _validate_skill_name("Has Spaces")
        with pytest.raises(ValueError):
            _validate_skill_name("")
        with pytest.raises(ValueError):
            _validate_skill_name("-leading-dash")
        with pytest.raises(ValueError):
            _validate_skill_name("../traversal")
        with pytest.raises(ValueError):
            _validate_skill_name("has.dot")
        with pytest.raises(ValueError):
            _validate_skill_name("has/slash")

    def test_non_string_raises(self):
        with pytest.raises(ValueError):
            _validate_skill_name(None)  # type: ignore[arg-type]
        with pytest.raises(ValueError):
            _validate_skill_name(123)  # type: ignore[arg-type]
