"""
Tests for Skills Wizard.

Tests SkillWizard interactive creation and template generation.
"""

from __future__ import annotations

import tempfile
from pathlib import Path
from unittest.mock import AsyncMock

import pytest

from merlya.skills.loader import SkillLoader
from merlya.skills.models import SkillConfig
from merlya.skills.registry import reset_registry
from merlya.skills.wizard import (
    COMMON_PATTERNS,
    DEFAULT_TOOLS,
    SkillWizard,
    generate_skill_template,
)


class TestSkillWizardInit:
    """Tests for SkillWizard initialization."""

    def test_init_with_callbacks(self):
        """Test initialization with all callbacks."""
        prompt_cb = AsyncMock()
        select_cb = AsyncMock()
        confirm_cb = AsyncMock()

        wizard = SkillWizard(
            prompt_callback=prompt_cb,
            select_callback=select_cb,
            confirm_callback=confirm_cb,
        )

        assert wizard.prompt == prompt_cb
        assert wizard.select == select_cb
        assert wizard.confirm == confirm_cb

    def test_init_creates_default_loader(self):
        """Test that a default loader is created."""
        wizard = SkillWizard()

        assert wizard.loader is not None
        assert isinstance(wizard.loader, SkillLoader)

    def test_init_with_custom_loader(self):
        """Test initialization with custom loader."""
        custom_loader = SkillLoader()
        wizard = SkillWizard(loader=custom_loader)

        assert wizard.loader is custom_loader


class TestSkillWizardPromptName:
    """Tests for _prompt_name method."""

    @pytest.fixture(autouse=True)
    def reset(self):
        """Reset registry before each test."""
        reset_registry()
        yield
        reset_registry()

    @pytest.mark.asyncio
    async def test_prompt_name_valid(self):
        """Test prompting for a valid name."""
        prompt_cb = AsyncMock(return_value="my_skill")
        wizard = SkillWizard(prompt_callback=prompt_cb)

        name = await wizard._prompt_name()

        assert name == "my_skill"

    @pytest.mark.asyncio
    async def test_prompt_name_normalizes(self):
        """Test that name is normalized."""
        prompt_cb = AsyncMock(return_value="  My Skill  ")
        wizard = SkillWizard(prompt_callback=prompt_cb)

        name = await wizard._prompt_name()

        assert name == "my_skill"

    @pytest.mark.asyncio
    async def test_prompt_name_empty_returns_none(self):
        """Test that empty name returns None."""
        prompt_cb = AsyncMock(return_value="")
        wizard = SkillWizard(prompt_callback=prompt_cb)

        name = await wizard._prompt_name()

        assert name is None

    @pytest.mark.asyncio
    async def test_prompt_name_none_returns_none(self):
        """Test that None input returns None."""
        prompt_cb = AsyncMock(return_value=None)
        wizard = SkillWizard(prompt_callback=prompt_cb)

        name = await wizard._prompt_name()

        assert name is None

    @pytest.mark.asyncio
    async def test_prompt_name_no_callback(self):
        """Test without prompt callback."""
        wizard = SkillWizard()

        name = await wizard._prompt_name()

        assert name is None


class TestSkillWizardPromptDescription:
    """Tests for _prompt_description method."""

    @pytest.mark.asyncio
    async def test_prompt_description_valid(self):
        """Test prompting for description."""
        prompt_cb = AsyncMock(return_value="My skill description")
        wizard = SkillWizard(prompt_callback=prompt_cb)

        desc = await wizard._prompt_description()

        assert desc == "My skill description"

    @pytest.mark.asyncio
    async def test_prompt_description_strips(self):
        """Test that description is stripped."""
        prompt_cb = AsyncMock(return_value="  description with spaces  ")
        wizard = SkillWizard(prompt_callback=prompt_cb)

        desc = await wizard._prompt_description()

        assert desc == "description with spaces"

    @pytest.mark.asyncio
    async def test_prompt_description_empty(self):
        """Test empty description."""
        prompt_cb = AsyncMock(return_value="")
        wizard = SkillWizard(prompt_callback=prompt_cb)

        desc = await wizard._prompt_description()

        assert desc == ""

    @pytest.mark.asyncio
    async def test_prompt_description_no_callback(self):
        """Test without callback."""
        wizard = SkillWizard()

        desc = await wizard._prompt_description()

        assert desc == ""


class TestSkillWizardPromptPatterns:
    """Tests for _prompt_patterns method.

    Note: _prompt_patterns() now returns empty list as patterns are
    generated by LLM in create_skill(). These tests verify the new behavior.
    """

    @pytest.mark.asyncio
    async def test_prompt_patterns_returns_empty(self):
        """Test that _prompt_patterns returns empty (patterns are LLM-generated)."""
        select_cb = AsyncMock(return_value="diagnostic")
        wizard = SkillWizard(select_callback=select_cb)

        patterns = await wizard._prompt_patterns()

        # _prompt_patterns now returns empty - patterns are generated by LLM
        assert patterns == []

    @pytest.mark.asyncio
    async def test_prompt_patterns_empty_with_callbacks(self):
        """Test _prompt_patterns returns empty even with callbacks."""
        select_cb = AsyncMock(return_value="custom")
        prompt_cb = AsyncMock(return_value="pattern1.*, pattern2.*")
        wizard = SkillWizard(prompt_callback=prompt_cb, select_callback=select_cb)

        patterns = await wizard._prompt_patterns()

        # Patterns are now generated by LLM, not from user input
        assert patterns == []

    @pytest.mark.asyncio
    async def test_prompt_patterns_empty_without_callbacks(self):
        """Test _prompt_patterns returns empty without callbacks."""
        wizard = SkillWizard()

        patterns = await wizard._prompt_patterns()

        # No longer returns catch-all [r".*"] - returns empty
        assert patterns == []


class TestSkillWizardPromptTools:
    """Tests for _prompt_tools method."""

    @pytest.mark.asyncio
    async def test_prompt_tools_all(self):
        """Test selecting all tools."""
        select_cb = AsyncMock(return_value="all")
        wizard = SkillWizard(select_callback=select_cb)

        tools = await wizard._prompt_tools()

        assert tools == []  # Empty means all allowed

    @pytest.mark.asyncio
    async def test_prompt_tools_single(self):
        """Test selecting a single tool."""
        select_cb = AsyncMock(return_value="ssh_execute")
        wizard = SkillWizard(select_callback=select_cb)

        tools = await wizard._prompt_tools()

        assert tools == ["ssh_execute"]

    @pytest.mark.asyncio
    async def test_prompt_tools_from_prompt(self):
        """Test tools from prompt input."""
        prompt_cb = AsyncMock(return_value="ssh_execute, read_file, write_file")
        wizard = SkillWizard(prompt_callback=prompt_cb)

        tools = await wizard._prompt_tools()

        assert len(tools) == 3
        assert "ssh_execute" in tools


class TestSkillWizardPromptLimits:
    """Tests for _prompt_limits method."""

    @pytest.mark.asyncio
    async def test_prompt_limits_defaults(self):
        """Test default limits."""
        prompt_cb = AsyncMock(side_effect=["", ""])
        wizard = SkillWizard(prompt_callback=prompt_cb)

        max_hosts, timeout = await wizard._prompt_limits()

        assert max_hosts == 5
        assert timeout == 120

    @pytest.mark.asyncio
    async def test_prompt_limits_custom(self):
        """Test custom limits."""
        prompt_cb = AsyncMock(side_effect=["10", "60"])
        wizard = SkillWizard(prompt_callback=prompt_cb)

        max_hosts, timeout = await wizard._prompt_limits()

        assert max_hosts == 10
        assert timeout == 60

    @pytest.mark.asyncio
    async def test_prompt_limits_clamps_max_hosts(self):
        """Test that max_hosts is clamped."""
        prompt_cb = AsyncMock(side_effect=["200", "120"])
        wizard = SkillWizard(prompt_callback=prompt_cb)

        max_hosts, _timeout = await wizard._prompt_limits()

        assert max_hosts == 100  # Clamped to max

    @pytest.mark.asyncio
    async def test_prompt_limits_clamps_timeout(self):
        """Test that timeout is clamped."""
        prompt_cb = AsyncMock(side_effect=["5", "5"])
        wizard = SkillWizard(prompt_callback=prompt_cb)

        _max_hosts, timeout = await wizard._prompt_limits()

        assert timeout == 30  # Clamped to min (30s per SkillConfig model)

    @pytest.mark.asyncio
    async def test_prompt_limits_handles_invalid_input(self):
        """Test handling of invalid input."""
        prompt_cb = AsyncMock(side_effect=["abc", "xyz"])
        wizard = SkillWizard(prompt_callback=prompt_cb)

        max_hosts, timeout = await wizard._prompt_limits()

        # Should use defaults on invalid input
        assert max_hosts == 5
        assert timeout == 120


class TestSkillWizardPromptConfirmations:
    """Tests for _prompt_confirmations method."""

    @pytest.mark.asyncio
    async def test_prompt_confirmations_default(self):
        """Test default confirmations."""
        prompt_cb = AsyncMock(return_value="restart,kill,delete,stop")
        wizard = SkillWizard(prompt_callback=prompt_cb)

        ops = await wizard._prompt_confirmations()

        assert "restart" in ops
        assert "kill" in ops
        assert "delete" in ops
        assert "stop" in ops

    @pytest.mark.asyncio
    async def test_prompt_confirmations_custom(self):
        """Test custom confirmations."""
        prompt_cb = AsyncMock(return_value="reboot, shutdown")
        wizard = SkillWizard(prompt_callback=prompt_cb)

        ops = await wizard._prompt_confirmations()

        assert ops == ["reboot", "shutdown"]

    @pytest.mark.asyncio
    async def test_prompt_confirmations_no_callback(self):
        """Test without callback."""
        wizard = SkillWizard()

        ops = await wizard._prompt_confirmations()

        # Should return defaults
        assert "restart" in ops


class TestSkillWizardPromptSystemPrompt:
    """Tests for _prompt_system_prompt method."""

    @pytest.mark.asyncio
    async def test_prompt_system_prompt_declined(self):
        """Test declining custom system prompt."""
        confirm_cb = AsyncMock(return_value=False)
        wizard = SkillWizard(confirm_callback=confirm_cb)

        prompt = await wizard._prompt_system_prompt("test", "Test skill")

        assert prompt is None

    @pytest.mark.asyncio
    async def test_prompt_system_prompt_custom(self):
        """Test custom system prompt."""
        confirm_cb = AsyncMock(return_value=True)
        prompt_cb = AsyncMock(return_value="You are an expert in X")
        wizard = SkillWizard(prompt_callback=prompt_cb, confirm_callback=confirm_cb)

        prompt = await wizard._prompt_system_prompt("test", "Test skill")

        assert prompt == "You are an expert in X"

    @pytest.mark.asyncio
    async def test_prompt_system_prompt_default_generated(self):
        """Test default system prompt generation."""
        confirm_cb = AsyncMock(return_value=True)
        prompt_cb = AsyncMock(return_value="")
        wizard = SkillWizard(prompt_callback=prompt_cb, confirm_callback=confirm_cb)

        prompt = await wizard._prompt_system_prompt("disk_audit", "Check disk usage")

        assert "disk_audit" in prompt
        assert "Check disk usage" in prompt


class TestSkillWizardCreateSkill:
    """Tests for create_skill method."""

    @pytest.fixture(autouse=True)
    def reset(self):
        """Reset registry before each test."""
        reset_registry()
        yield
        reset_registry()

    @pytest.mark.asyncio
    async def test_create_skill_no_prompt_callback(self):
        """Test that create_skill returns None without prompt callback."""
        wizard = SkillWizard()

        skill = await wizard.create_skill()

        assert skill is None

    @pytest.mark.asyncio
    async def test_create_skill_cancelled_at_name(self):
        """Test cancellation at name prompt."""
        prompt_cb = AsyncMock(return_value=None)
        wizard = SkillWizard(prompt_callback=prompt_cb)

        skill = await wizard.create_skill()

        assert skill is None

    @pytest.mark.asyncio
    async def test_create_skill_full_flow(self):
        """Test full skill creation flow.

        Note: Patterns are now generated by LLM, not prompted.
        The flow is: name → description → (LLM patterns) → advanced options → confirm.
        """
        with tempfile.TemporaryDirectory() as tmpdir:
            # Mock all callbacks - order matches new wizard flow
            prompt_responses = iter(
                [
                    "test_skill",  # name
                    "Test description",  # description
                    # (patterns generated by LLM, no prompt)
                    "ssh_execute",  # tools (when advanced=True)
                    "5",  # max_hosts
                    "120",  # timeout
                    "restart,kill",  # confirmations
                ]
            )
            prompt_cb = AsyncMock(side_effect=lambda _msg, _default: next(prompt_responses))
            # First confirm: advanced options, Second confirm: create skill
            confirm_cb = AsyncMock(side_effect=[True, True])

            loader = SkillLoader(user_dir=Path(tmpdir))
            wizard = SkillWizard(
                prompt_callback=prompt_cb,
                confirm_callback=confirm_cb,
                loader=loader,
            )

            # Mock LLM pattern generation to avoid external calls
            wizard._generate_patterns_with_llm = AsyncMock(return_value=["test.*"])

            skill = await wizard.create_skill()

            assert skill is not None
            assert skill.name == "test_skill"
            assert skill.description == "Test description"

    @pytest.mark.asyncio
    async def test_create_skill_cancelled_at_confirm(self):
        """Test cancellation at final confirmation.

        Note: Patterns are now generated by LLM, not prompted.
        """
        with tempfile.TemporaryDirectory() as tmpdir:
            prompt_responses = iter(
                [
                    "cancel_skill",  # name
                    "Description",  # description
                    # (patterns generated by LLM, no prompt)
                    "ssh_execute",  # tools
                    "5",  # max_hosts
                    "120",  # timeout
                    "restart",  # confirmations
                ]
            )
            prompt_cb = AsyncMock(side_effect=lambda _msg, _default: next(prompt_responses))
            confirm_cb = AsyncMock(side_effect=[True, False])  # Accept advanced, deny creation

            loader = SkillLoader(user_dir=Path(tmpdir))
            wizard = SkillWizard(
                prompt_callback=prompt_cb,
                confirm_callback=confirm_cb,
                loader=loader,
            )

            # Mock LLM pattern generation to avoid external calls
            wizard._generate_patterns_with_llm = AsyncMock(return_value=["cancel.*"])

            skill = await wizard.create_skill()

            assert skill is None


class TestSkillWizardEditSkill:
    """Tests for edit_skill method."""

    @pytest.fixture(autouse=True)
    def reset(self):
        """Reset registry before each test."""
        reset_registry()
        yield
        reset_registry()

    @pytest.mark.asyncio
    async def test_edit_skill_not_found(self):
        """Test editing nonexistent skill."""
        wizard = SkillWizard()

        skill = await wizard.edit_skill("nonexistent")

        assert skill is None

    @pytest.mark.asyncio
    async def test_edit_builtin_skill(self):
        """Test editing builtin skill is rejected."""
        from merlya.skills.registry import get_registry

        # Register a builtin skill
        builtin_skill = SkillConfig(name="builtin_test", builtin=True)
        get_registry().register(builtin_skill)

        wizard = SkillWizard()

        skill = await wizard.edit_skill("builtin_test")

        assert skill is None


class TestSkillWizardDuplicateSkill:
    """Tests for duplicate_skill method."""

    @pytest.fixture(autouse=True)
    def reset(self):
        """Reset registry before each test."""
        reset_registry()
        yield
        reset_registry()

    @pytest.mark.asyncio
    async def test_duplicate_skill_not_found(self):
        """Test duplicating nonexistent skill."""
        wizard = SkillWizard()

        skill = await wizard.duplicate_skill("nonexistent", "new_name")

        assert skill is None

    @pytest.mark.asyncio
    async def test_duplicate_skill_success(self):
        """Test successful skill duplication."""
        from merlya.skills.registry import get_registry

        with tempfile.TemporaryDirectory() as tmpdir:
            # Register original skill
            original = SkillConfig(
                name="original_skill",
                description="Original description",
                max_hosts=10,
                timeout_seconds=60,
                tools_allowed=["ssh_execute"],
                tags=["test"],
            )
            get_registry().register(original)

            loader = SkillLoader(user_dir=Path(tmpdir))
            wizard = SkillWizard(loader=loader)

            new_skill = await wizard.duplicate_skill("original_skill", "copied_skill")

            assert new_skill is not None
            assert new_skill.name == "copied_skill"
            assert "Copy of" in new_skill.description
            assert new_skill.max_hosts == original.max_hosts
            assert new_skill.tools_allowed == original.tools_allowed


class TestGenerateSkillTemplate:
    """Tests for generate_skill_template function."""

    def test_basic_template(self):
        """Test basic template generation."""
        template = generate_skill_template("my_skill")

        assert "name: my_skill" in template
        assert 'version: "1.0"' in template
        assert "max_hosts: 5" in template
        assert "timeout_seconds: 120" in template

    def test_template_with_description(self):
        """Test template with custom description."""
        template = generate_skill_template("audit_skill", "Audit infrastructure")

        assert "name: audit_skill" in template
        assert "Audit infrastructure" in template

    def test_template_default_description(self):
        """Test template with default description."""
        template = generate_skill_template("unnamed")

        assert "Custom skill" in template

    def test_template_has_tools_section(self):
        """Test template has tools section."""
        template = generate_skill_template("test")

        assert "tools_allowed:" in template
        assert "ssh_execute" in template
        assert "read_file" in template

    def test_template_has_confirmations(self):
        """Test template has confirmations section."""
        template = generate_skill_template("test")

        assert "require_confirmation_for:" in template
        assert "restart" in template
        assert "kill" in template
        assert "delete" in template

    def test_template_has_intent_patterns(self):
        """Test template has intent patterns."""
        template = generate_skill_template("disk_check")

        assert "intent_patterns:" in template
        # Template converts underscores to .* : disk_check -> disk.*check
        assert "disk.*check" in template

    def test_template_has_system_prompt_comment(self):
        """Test template has commented system prompt."""
        template = generate_skill_template("test")

        assert "# system_prompt:" in template or "system_prompt:" in template

    def test_template_has_tags(self):
        """Test template has tags section."""
        template = generate_skill_template("test")

        assert "tags:" in template
        assert "custom" in template


class TestCommonPatterns:
    """Tests for COMMON_PATTERNS constant."""

    def test_diagnostic_patterns_exist(self):
        """Test diagnostic patterns exist."""
        assert "diagnostic" in COMMON_PATTERNS
        patterns = COMMON_PATTERNS["diagnostic"]
        assert any("diagnos" in p for p in patterns)

    def test_disk_patterns_exist(self):
        """Test disk patterns exist."""
        assert "disk" in COMMON_PATTERNS
        patterns = COMMON_PATTERNS["disk"]
        assert any("disk" in p for p in patterns)

    def test_logs_patterns_exist(self):
        """Test logs patterns exist."""
        assert "logs" in COMMON_PATTERNS
        patterns = COMMON_PATTERNS["logs"]
        assert any("log" in p for p in patterns)

    def test_services_patterns_exist(self):
        """Test services patterns exist."""
        assert "services" in COMMON_PATTERNS

    def test_network_patterns_exist(self):
        """Test network patterns exist."""
        assert "network" in COMMON_PATTERNS


class TestDefaultTools:
    """Tests for DEFAULT_TOOLS constant."""

    def test_ssh_execute_in_defaults(self):
        """Test ssh_execute is in default tools."""
        assert "ssh_execute" in DEFAULT_TOOLS

    def test_read_file_in_defaults(self):
        """Test read_file is in default tools."""
        assert "read_file" in DEFAULT_TOOLS

    def test_default_tools_not_empty(self):
        """Test default tools list is not empty."""
        assert len(DEFAULT_TOOLS) > 0
