"""Step definitions for Skill context and registry tests."""

from __future__ import annotations

from pathlib import Path
from typing import Any
from unittest.mock import MagicMock

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

from cleveragents.domain.models.core.skill import (
    Skill,
    SkillResolver,
)
from cleveragents.skills.context import SkillContext, SkillExecutionError
from cleveragents.skills.protocol import (
    SkillDefinition,
    SkillMetadata,
)
from cleveragents.skills.registry import SkillRegistry

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


def _make_skill_definition(name: str, **overrides: Any) -> SkillDefinition:
    """Create a SkillDefinition with sensible defaults."""
    defaults: dict[str, Any] = {
        "name": name,
        "description": f"Test skill {name}",
    }
    defaults.update(overrides)
    skill = Skill(**defaults)
    resolver = SkillResolver()
    resolved = resolver.resolve_tools(skill, {})
    metadata = SkillMetadata.from_skill(skill, resolved=resolved)
    return SkillDefinition(
        skill=skill,
        resolved_tools=resolved,
        metadata=metadata,
    )


# ---------------------------------------------------------------------------
# SkillContext Creation
# ---------------------------------------------------------------------------


@when(
    'I create a skill_context with plan "{plan_id}" project "{project_id}" and sandbox "{sandbox}"'
)
def skill_context_create(
    context: Context, plan_id: str, project_id: str, sandbox: str
) -> None:
    """Create a SkillContext with plan/project/sandbox info."""
    context.skill_context = SkillContext(
        plan_id=plan_id,
        project_id=project_id,
        sandbox_path=Path(sandbox),
    )
    context.skill_context_error = None


@when('I create a read_only skill_context with plan "{plan_id}" project "{project_id}"')
def skill_context_create_readonly(
    context: Context, plan_id: str, project_id: str
) -> None:
    """Create a read-only SkillContext."""
    context.skill_context = SkillContext(
        plan_id=plan_id,
        project_id=project_id,
        sandbox_path=Path("/tmp/readonly-sandbox"),
        read_only=True,
    )
    context.skill_context_error = None


@when('I create a skill_context with metadata key "{key}" value "{value}"')
def skill_context_create_with_metadata(context: Context, key: str, value: str) -> None:
    """Create a SkillContext with custom metadata."""
    context.skill_context = SkillContext(
        plan_id="plan-meta",
        project_id="proj-meta",
        sandbox_path=Path("/tmp/meta-sandbox"),
        metadata={key: value},
    )


@then("the skill_context should be created")
def skill_context_check_created(context: Context) -> None:
    """Verify context was created."""
    assert context.skill_context is not None


@then('the skill_context plan_id should be "{expected}"')
def skill_context_check_plan_id(context: Context, expected: str) -> None:
    """Check context plan_id."""
    assert context.skill_context.plan_id == expected


@then('the skill_context project_id should be "{expected}"')
def skill_context_check_project_id(context: Context, expected: str) -> None:
    """Check context project_id."""
    assert context.skill_context.project_id == expected


@then('the skill_context sandbox_path should be "{expected}"')
def skill_context_check_sandbox(context: Context, expected: str) -> None:
    """Check context sandbox_path."""
    assert str(context.skill_context.get_sandbox_path()) == expected


@then("the skill_context should not be read_only")
def skill_context_check_not_readonly(context: Context) -> None:
    """Check context is not read-only."""
    assert context.skill_context.is_read_only() is False


@then("the skill_context should be read_only")
def skill_context_check_readonly(context: Context) -> None:
    """Check context is read-only."""
    assert context.skill_context.is_read_only() is True


# ---------------------------------------------------------------------------
# Plan Metadata
# ---------------------------------------------------------------------------


@then('the skill_context plan metadata should contain key "{key}"')
def skill_context_check_metadata_key(context: Context, key: str) -> None:
    """Check plan metadata contains a key."""
    meta = context.skill_context.get_plan_metadata()
    assert key in meta, f"Expected '{key}' in {list(meta.keys())}"


@then('the skill_context plan metadata key "{key}" should be "{expected}"')
def skill_context_check_metadata_value(
    context: Context, key: str, expected: str
) -> None:
    """Check plan metadata value."""
    meta = context.skill_context.get_plan_metadata()
    actual = meta[key]
    assert str(actual) == expected, f"Expected '{expected}', got '{actual}'"


# ---------------------------------------------------------------------------
# Resource Resolution
# ---------------------------------------------------------------------------


@when('I create a skill_context with resource "{name}" bound to "{value}"')
def skill_context_create_with_resource(context: Context, name: str, value: str) -> None:
    """Create a SkillContext with a resource binding."""
    context.skill_context = SkillContext(
        plan_id="plan-res",
        project_id="proj-res",
        sandbox_path=Path("/tmp/res-sandbox"),
        resource_bindings={name: value},
    )


@when('I resolve resource "{name}" from the skill_context')
def skill_context_resolve_resource(context: Context, name: str) -> None:
    """Resolve a resource from the context."""
    context.resolved_resource = context.skill_context.resolve_resource(name)


@then('the resolved resource should be "{expected}"')
def skill_context_check_resolved_resource(context: Context, expected: str) -> None:
    """Check resolved resource value."""
    assert context.resolved_resource == expected


@when("I create a skill_context with no resources")
def skill_context_create_no_resources(context: Context) -> None:
    """Create a SkillContext with empty resource bindings."""
    context.skill_context = SkillContext(
        plan_id="plan-empty",
        project_id="proj-empty",
        sandbox_path=Path("/tmp/empty-sandbox"),
    )


@when('I try to resolve resource "{name}" from the skill_context')
def skill_context_try_resolve_resource(context: Context, name: str) -> None:
    """Try to resolve a missing resource."""
    context.skill_context_error = None
    try:
        context.skill_context.resolve_resource(name)
    except SkillExecutionError as e:
        context.skill_context_error = e


@then("a skill_context resolution error should be raised")
def skill_context_check_resolution_error(context: Context) -> None:
    """Verify resolution error was raised."""
    assert context.skill_context_error is not None
    assert context.skill_context_error.error_type.value == "resolution_failure"


# ---------------------------------------------------------------------------
# Change Tracking
# ---------------------------------------------------------------------------


@when('I register a tool invocation for "{tool_name}" with duration {duration:g}')
def skill_context_register_invocation(
    context: Context, tool_name: str, duration: float
) -> None:
    """Register a tool invocation."""
    context.skill_context.register_tool_invocation(
        tool_name=tool_name,
        input_data={"arg": "test"},
        output_data={"result": "ok"},
        duration_ms=duration,
    )


@then("the skill_context change tracker should have {count:d} records")
def skill_context_check_tracker_count(context: Context, count: int) -> None:
    """Check change tracker record count."""
    actual = len(context.skill_context.change_tracker)
    assert actual == count, f"Expected {count} records, got {actual}"


@then('the last change tracker record tool_name should be "{expected}"')
def skill_context_check_last_tool(context: Context, expected: str) -> None:
    """Check last tracker record tool_name."""
    record = context.skill_context.change_tracker[-1]
    assert record["tool_name"] == expected


@then("the last change tracker record duration_ms should be {expected:g}")
def skill_context_check_last_duration(context: Context, expected: float) -> None:
    """Check last tracker record duration."""
    record = context.skill_context.change_tracker[-1]
    assert record["duration_ms"] == expected


# ---------------------------------------------------------------------------
# Write Guard
# ---------------------------------------------------------------------------


@when('I try to enforce write guard for tool "{tool_name}"')
def skill_context_try_write_guard(context: Context, tool_name: str) -> None:
    """Try to enforce write guard on a read-only context."""
    context.skill_context_error = None
    try:
        context.skill_context.enforce_write_guard(tool_name)
    except SkillExecutionError as e:
        context.skill_context_error = e


@then("a skill_context permission denied error should be raised")
def skill_context_check_permission_error(context: Context) -> None:
    """Verify permission denied error was raised."""
    assert context.skill_context_error is not None
    assert context.skill_context_error.error_type.value == "permission_denied"


@then('the skill_context permission error tool_name should be "{expected}"')
def skill_context_check_permission_tool(context: Context, expected: str) -> None:
    """Check permission error tool_name."""
    assert context.skill_context_error.tool_name == expected


@when('I enforce write guard for tool "{tool_name}" in writable context')
def skill_context_write_guard_writable(context: Context, tool_name: str) -> None:
    """Enforce write guard in a writable context (should not raise)."""
    context.skill_context_error = None
    try:
        context.skill_context.enforce_write_guard(tool_name)
    except SkillExecutionError as e:
        context.skill_context_error = e


@then("no skill_context error should be raised")
def skill_context_check_no_error(context: Context) -> None:
    """Verify no error was raised."""
    assert context.skill_context_error is None


# ---------------------------------------------------------------------------
# SkillRegistry
# ---------------------------------------------------------------------------


@when("I create a skill_registry")
def skill_registry_create(context: Context) -> None:
    """Create a SkillRegistry."""
    context.skill_registry = SkillRegistry()
    context.skill_context_error = None


@when('I register a skill "{name}" in the registry')
def skill_registry_register(context: Context, name: str) -> None:
    """Register a skill in the registry."""
    defn = _make_skill_definition(name)
    context.skill_registry.register(defn)


@when('I register a skill "{name}" with tool refs in the registry')
def skill_registry_register_with_refs(context: Context, name: str) -> None:
    """Register a skill with tool refs in the registry."""
    defn = _make_skill_definition(name, tool_refs=["local/tool-a", "local/tool-b"])
    context.skill_registry.register(defn)


@when('I get skill "{name}" from the registry')
def skill_registry_get(context: Context, name: str) -> None:
    """Get a skill from the registry."""
    context.registry_skill = context.skill_registry.get(name)


@when('I try to register a duplicate skill "{name}"')
def skill_registry_try_register_dup(context: Context, name: str) -> None:
    """Try to register a duplicate skill."""
    context.skill_context_error = None
    try:
        defn = _make_skill_definition(name)
        context.skill_registry.register(defn)
    except SkillExecutionError as e:
        context.skill_context_error = e


@when('I try to get skill "{name}" from the registry')
def skill_registry_try_get(context: Context, name: str) -> None:
    """Try to get a missing skill."""
    context.skill_context_error = None
    try:
        context.skill_registry.get(name)
    except SkillExecutionError as e:
        context.skill_context_error = e


@when("I list all skills from the registry")
def skill_registry_list(context: Context) -> None:
    """List all skills from the registry."""
    context.registry_skill_list = context.skill_registry.list_all()


@when('I unregister skill "{name}" from the registry')
def skill_registry_unregister(context: Context, name: str) -> None:
    """Unregister a skill from the registry."""
    context.skill_registry.unregister(name)


@when('I try to unregister skill "{name}" from the registry')
def skill_registry_try_unregister(context: Context, name: str) -> None:
    """Try to unregister a missing skill."""
    context.skill_context_error = None
    try:
        context.skill_registry.unregister(name)
    except SkillExecutionError as e:
        context.skill_context_error = e


@when('I resolve tools for "{name}" from the registry')
def skill_registry_resolve_tools(context: Context, name: str) -> None:
    """Resolve tools for a skill from the registry."""
    context.resolved_tools = context.skill_registry.resolve_tools(name)


@then('the registry skill name should be "{expected}"')
def skill_registry_check_name(context: Context, expected: str) -> None:
    """Check registry skill name."""
    assert context.registry_skill.skill.name == expected


@then("a skill_context validation error should be raised")
def skill_context_check_validation_error(context: Context) -> None:
    """Verify validation error was raised."""
    assert context.skill_context_error is not None
    assert context.skill_context_error.error_type.value == "validation_error"


@then("a skill_context not found error should be raised")
def skill_context_check_not_found_error(context: Context) -> None:
    """Verify not found error was raised."""
    assert context.skill_context_error is not None
    assert context.skill_context_error.error_type.value == "skill_not_found"


@then("the registry should have {count:d} skills")
def skill_registry_check_count(context: Context, count: int) -> None:
    """Check registry skill count."""
    actual = len(context.registry_skill_list)
    assert actual == count, f"Expected {count} skills, got {actual}"


@then('the registry skill list should contain "{name}"')
def skill_registry_check_list_contains(context: Context, name: str) -> None:
    """Check registry list contains a skill."""
    names = [m.name for m in context.registry_skill_list]
    assert name in names, f"Expected '{name}' in {names}"


@then("the resolved tools should have {count:d} entries")
def skill_registry_check_resolved_count(context: Context, count: int) -> None:
    """Check resolved tools count."""
    actual = len(context.resolved_tools)
    assert actual == count, f"Expected {count} entries, got {actual}"


@then('the resolved tools should contain "{name}"')
def skill_registry_check_resolved_contains(context: Context, name: str) -> None:
    """Check resolved tools contain a tool name."""
    names = [e.name for e in context.resolved_tools]
    assert name in names, f"Expected '{name}' in {names}"


# ---------------------------------------------------------------------------
# SkillRegistry Validation
# ---------------------------------------------------------------------------


@when("I create a skill_registry with a mock tool registry")
def skill_registry_create_with_mock(context: Context) -> None:
    """Create a SkillRegistry with a mock tool registry."""
    mock_tool_reg = MagicMock()
    mock_tool_reg.get_tool.return_value = None
    context.skill_registry = SkillRegistry(tool_registry=mock_tool_reg)
    context.skill_context_error = None


@when("I validate a skill with missing tool refs")
def skill_registry_validate_missing_refs(context: Context) -> None:
    """Validate a skill with tool refs that don't exist."""
    defn = _make_skill_definition(
        "local/validate-missing",
        tool_refs=["local/nonexistent-tool"],
    )
    context.validation_errors = context.skill_registry.validate_skill(defn)


@when("I validate a skill with missing includes")
def skill_registry_validate_missing_includes(context: Context) -> None:
    """Validate a skill that includes a non-registered skill."""
    from cleveragents.domain.models.core.skill import SkillInclude

    skill = Skill(
        name="local/validate-includes",
        description="Skill with missing includes",
        includes=[SkillInclude(name="local/not-registered")],
    )
    # Don't resolve tools through the resolver since the included
    # skill doesn't exist — just build metadata from the skill alone
    metadata = SkillMetadata(
        name=skill.name,
        description=skill.description,
    )
    defn = SkillDefinition(
        skill=skill,
        resolved_tools=[],
        metadata=metadata,
    )
    context.validation_errors = context.skill_registry.validate_skill(defn)


@when("I validate a skill with no issues")
def skill_registry_validate_no_issues(context: Context) -> None:
    """Validate a skill with no issues."""
    defn = _make_skill_definition("local/valid-skill")
    context.validation_errors = context.skill_registry.validate_skill(defn)


@then('the validation errors should contain "{expected}"')
def skill_registry_check_validation_errors(context: Context, expected: str) -> None:
    """Check validation errors contain a message."""
    assert any(expected in e for e in context.validation_errors), (
        f"Expected error containing '{expected}' in {context.validation_errors}"
    )


@then("the validation errors should be empty")
def skill_registry_check_validation_empty(context: Context) -> None:
    """Verify no validation errors."""
    assert len(context.validation_errors) == 0, (
        f"Expected empty errors, got {context.validation_errors}"
    )
