"""Step definitions for SkillRegistry coverage boost.

Targets uncovered lines in src/cleveragents/skills/registry.py:
  - validate_plan tool-registry validation loop (get() call)
  - validate_skill tool-registry validation loop (get() call)
  - refresh exception handler for ValueError/RuntimeError
  - refresh skip for inline/mcp/agent_skill entries
"""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock, patch

from behave import given, then, when
from behave.runner import Context

from cleveragents.domain.models.core.skill import (
    Skill,
    SkillAgentSource,
    SkillInclude,
    SkillInlineTool,
    SkillMcpSource,
)
from cleveragents.domain.models.core.tool import ToolSource
from cleveragents.skills.protocol import SkillDefinition, SkillMetadata
from cleveragents.skills.refresh import SkillRefreshResult
from cleveragents.skills.registry import SkillRegistry

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_skill(
    name: str,
    description: str = "test skill",
    tool_refs: list[str] | None = None,
    includes: list[SkillInclude] | None = None,
    anonymous_tools: list[SkillInlineTool] | None = None,
    mcp_servers: list[SkillMcpSource] | None = None,
    agent_skills: list[SkillAgentSource] | None = None,
) -> Skill:
    return Skill(
        name=name,
        description=description,
        tool_refs=tool_refs or [],
        includes=includes or [],
        anonymous_tools=anonymous_tools or [],
        mcp_servers=mcp_servers or [],
        agent_skills=agent_skills or [],
    )


def _make_definition(skill: Skill, resolve: bool = True) -> SkillDefinition:
    if resolve:
        metadata = SkillMetadata.from_skill(skill)
    else:
        # Build metadata without resolving (avoids ValueError for missing includes)
        metadata = SkillMetadata(
            name=skill.name,
            description=skill.description,
        )
    return SkillDefinition(skill=skill, metadata=metadata)


def _register(context: Context, skill: Skill, resolve: bool = True) -> None:
    defn = _make_definition(skill, resolve=resolve)
    context.skill_reg.register(defn)


# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------


@given("a fresh SkillRegistry instance")
def step_fresh_registry(context: Context) -> None:
    context.skill_reg = SkillRegistry()
    context.tool_registry_mock = None
    context.validation_errors = None
    context.refresh_result = None
    context.resolver_patch_runtime_error = False


# ---------------------------------------------------------------------------
# Given: tool registry mock
# ---------------------------------------------------------------------------


@given("the registry has a tool registry mock configured")
def step_tool_registry_mock(context: Context) -> None:
    mock = MagicMock()
    mock.get.return_value = None
    context.tool_registry_mock = mock
    context.skill_reg = SkillRegistry(tool_registry=mock)


# ---------------------------------------------------------------------------
# Given: register skills (unique step text per variant)
# ---------------------------------------------------------------------------


@given('a registered skill "{name}" having tool ref "{ref}"')
def step_skill_with_tool_ref(context: Context, name: str, ref: str) -> None:
    _register(context, _make_skill(name=name, tool_refs=[ref]))


@given('a registered skill "{name}" having only an inline tool')
def step_skill_with_inline(context: Context, name: str) -> None:
    inline = SkillInlineTool(
        description="An inline test tool",
        source=ToolSource.CUSTOM,
        timeout=300,
    )
    _register(context, _make_skill(name=name, anonymous_tools=[inline]))


@given('a registered skill "{name}" having only MCP tools')
def step_skill_with_mcp_only(context: Context, name: str) -> None:
    mcp_source = SkillMcpSource(server="myserver", tools=["read"])
    _register(context, _make_skill(name=name, mcp_servers=[mcp_source]))


@given('a registered skill "{name}" having only agent skill sources')
def step_skill_with_agent_only(context: Context, name: str) -> None:
    agent = SkillAgentSource(path="/skills/my-agent")
    _register(context, _make_skill(name=name, agent_skills=[agent]))


@given('a registered skill "{name}" including "{other}"')
def step_skill_with_include(context: Context, name: str, other: str) -> None:
    skill = _make_skill(name=name, includes=[SkillInclude(name=other)])
    _register(context, skill, resolve=False)


@given('a registered skill "{name}" that will cause RuntimeError on resolve')
def step_skill_with_runtime_error(context: Context, name: str) -> None:
    _register(context, _make_skill(name=name))
    context.resolver_patch_runtime_error = True


@given('a registered skill "{name}" having inline tool plus tool ref "{ref}"')
def step_skill_inline_and_ref(context: Context, name: str, ref: str) -> None:
    inline = SkillInlineTool(
        description="Inline lint helper",
        source=ToolSource.CUSTOM,
        timeout=300,
    )
    _register(
        context, _make_skill(name=name, tool_refs=[ref], anonymous_tools=[inline])
    )


@given('a registered skill "{name}" having MCP tools plus tool ref "{ref}"')
def step_skill_mcp_and_ref(context: Context, name: str, ref: str) -> None:
    mcp_source = SkillMcpSource(server="myserver", tools=["read"])
    _register(
        context, _make_skill(name=name, tool_refs=[ref], mcp_servers=[mcp_source])
    )


@given('a registered skill "{name}" having agent skill plus tool ref "{ref}"')
def step_skill_agent_and_ref(context: Context, name: str, ref: str) -> None:
    agent = SkillAgentSource(path="/skills/my-agent")
    _register(context, _make_skill(name=name, tool_refs=[ref], agent_skills=[agent]))


# ---------------------------------------------------------------------------
# Given: tool registry mock responses
# ---------------------------------------------------------------------------


@given('the tool registry mock returns None for tool "{tool_name}"')
def step_mock_returns_none(context: Context, tool_name: str) -> None:
    mock: MagicMock = context.tool_registry_mock
    mock.get.return_value = None


@given('the tool registry mock returns a tool for "{tool_name}"')
def step_mock_returns_tool(context: Context, tool_name: str) -> None:
    mock: MagicMock = context.tool_registry_mock
    tool_obj = MagicMock()
    tool_obj.name = tool_name

    def _get(name: str) -> Any:
        if name == tool_name:
            return tool_obj
        return None

    mock.get.side_effect = _get


# ---------------------------------------------------------------------------
# When: validate_plan
# ---------------------------------------------------------------------------


@when('I validate a plan referencing skill "{skill_name}"')
def step_validate_plan(context: Context, skill_name: str) -> None:
    plan: dict[str, Any] = {"skills": [skill_name]}
    context.validation_errors = context.skill_reg.validate_plan(plan)


# ---------------------------------------------------------------------------
# When: refresh
# ---------------------------------------------------------------------------


@when('I refresh the coverage boost skill "{name}"')
def step_refresh_skill_coverage(context: Context, name: str) -> None:
    if context.resolver_patch_runtime_error:
        with patch(
            "cleveragents.skills.registry.SkillResolver.resolve_tools",
            side_effect=RuntimeError("resolver broke"),
        ):
            context.refresh_result = context.skill_reg.refresh(name)
    else:
        context.refresh_result = context.skill_reg.refresh(name)


# ---------------------------------------------------------------------------
# Then: validation_errors assertions
# ---------------------------------------------------------------------------


@then('the skills validation errors should contain "{message}"')
def step_errors_contain(context: Context, message: str) -> None:
    errors: list[str] = context.validation_errors
    assert errors is not None, "No validation errors captured"
    matching = [e for e in errors if message in e]
    assert matching, f"Expected error containing '{message}', got: {errors}"


# REMOVED: Duplicate @then step - already defined in skill_context_steps.py


@then("the tool registry mock get should not have been called")
def step_mock_not_called(context: Context) -> None:
    mock: MagicMock = context.tool_registry_mock
    mock.get.assert_not_called()


# ---------------------------------------------------------------------------
# Then: refresh result assertions
# ---------------------------------------------------------------------------


@then('the refresh result should have "{name}" in failed')
def step_refresh_failed(context: Context, name: str) -> None:
    result: SkillRefreshResult = context.refresh_result
    assert result is not None, "No refresh result captured"
    assert name in result.failed, (
        f"Expected '{name}' in failed, got failed={result.failed}"
    )


@then("the refresh result should have zero refreshed")
def step_refresh_zero_refreshed(context: Context) -> None:
    result: SkillRefreshResult = context.refresh_result
    assert result is not None, "No refresh result captured"
    assert len(result.refreshed) == 0, f"Expected 0 refreshed, got {result.refreshed}"


@then('the refresh failed message for "{name}" should contain "{text}"')
def step_refresh_failed_message(context: Context, name: str, text: str) -> None:
    result: SkillRefreshResult = context.refresh_result
    assert name in result.failed, f"'{name}' not in failed: {result.failed}"
    assert text in result.failed[name], (
        f"Expected '{text}' in message, got: {result.failed[name]}"
    )


@then('the refresh result should have "{name}" in refreshed')
def step_refresh_refreshed(context: Context, name: str) -> None:
    result: SkillRefreshResult = context.refresh_result
    assert result is not None, "No refresh result captured"
    assert name in result.refreshed, (
        f"Expected '{name}' in refreshed, got refreshed={result.refreshed}, "
        f"failed={result.failed}"
    )


# ---------------------------------------------------------------------------
# When: validate_skill
# ---------------------------------------------------------------------------


@when('I validate a skill definition with tool ref "{ref}"')
def step_validate_skill_with_ref(context: Context, ref: str) -> None:
    skill = _make_skill(name="local/test-validate", tool_refs=[ref])
    defn = _make_definition(skill)
    context.validation_errors = context.skill_reg.validate_skill(defn)


@when('I validate a skill definition that includes unregistered skill "{include_name}"')
def step_validate_skill_unregistered_include(
    context: Context, include_name: str
) -> None:
    skill = _make_skill(
        name="local/test-include",
        includes=[SkillInclude(name=include_name)],
    )
    defn = _make_definition(skill, resolve=False)
    context.validation_errors = context.skill_reg.validate_skill(defn)
