"""Step definitions for Safety Profile Enforcement tests.

Tests that ToolRuntime._enforce_capabilities correctly enforces safety
profile constraints: unsafe tool gating, skill category allow-lists,
checkpoint requirements, sandbox requirements, human approval,
cost limits, and retry limits from the safety profile.
"""

from __future__ import annotations

from typing import Any

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

from cleveragents.domain.models.core.safety_profile import SafetyProfile
from cleveragents.domain.models.core.tool import (
    Tool,
    ToolCapability,
    ToolSource,
)
from cleveragents.infrastructure.events.reactive import ReactiveEventBus
from cleveragents.tool.context import ToolExecutionContext
from cleveragents.tool.lifecycle import (
    ToolAccessDeniedError,
    ToolCheckpointRequiredError,
    ToolCostLimitExceededError,
    ToolDescriptor,
    ToolHumanApprovalRequiredError,
    ToolResult,
    ToolRetryLimitExceededError,
    ToolRuntime,
    ToolSafetyViolationError,
    ToolSandboxRequiredError,
)

# ---------------------------------------------------------------------------
# Helpers -- Stub ToolInstance
# ---------------------------------------------------------------------------


class _StubToolInstance:
    """Minimal ToolInstance for testing enforcement without real execution."""

    def __init__(self, descriptor: ToolDescriptor) -> None:
        self._descriptor = descriptor

    def discover(self) -> ToolDescriptor:
        return self._descriptor

    def activate(self, ctx: ToolExecutionContext) -> None:
        pass

    def execute(self, params: dict[str, Any], ctx: ToolExecutionContext) -> ToolResult:
        return ToolResult(success=True, data={"ok": True})

    def deactivate(self, ctx: ToolExecutionContext) -> None:
        pass


def _make_tool(
    name: str,
    *,
    writes: bool = False,
    read_only: bool = False,
    unsafe: bool = False,
    checkpointable: bool = False,
) -> Tool:
    """Create a Tool domain model with given capability flags."""
    return Tool(
        name=name,
        description=f"Test tool {name}",
        source=ToolSource.CUSTOM,
        code="pass",
        capability=ToolCapability(
            read_only=read_only,
            writes=writes,
            unsafe=unsafe,
            checkpointable=checkpointable,
        ),
    )


# ---------------------------------------------------------------------------
# Background: register tools
# ---------------------------------------------------------------------------


@given('a registered tool "test/writer" that writes and is safe')
def step_register_writer(context: Context) -> None:
    """Register a tool that writes but is not unsafe."""
    context.enforcement_runtime = ToolRuntime(event_bus=ReactiveEventBus())
    tool = _make_tool("test/writer", writes=True, unsafe=False)
    desc = ToolDescriptor(
        name=tool.name,
        description=tool.description,
        capability=tool.capability,
    )
    instance = _StubToolInstance(desc)
    context.enforcement_runtime.register_tool(tool, instance)


@given('a registered tool "test/unsafe-tool" that is unsafe')
def step_register_unsafe(context: Context) -> None:
    """Register a tool marked as unsafe."""
    runtime: ToolRuntime = context.enforcement_runtime
    tool = _make_tool("test/unsafe-tool", writes=False, unsafe=True)
    desc = ToolDescriptor(
        name=tool.name,
        description=tool.description,
        capability=tool.capability,
    )
    instance = _StubToolInstance(desc)
    runtime.register_tool(tool, instance)


@given('a registered tool "test/reader" that is read-only and safe')
def step_register_reader(context: Context) -> None:
    """Register a read-only safe tool."""
    runtime: ToolRuntime = context.enforcement_runtime
    tool = _make_tool("test/reader", read_only=True, unsafe=False)
    desc = ToolDescriptor(
        name=tool.name,
        description=tool.description,
        capability=tool.capability,
    )
    instance = _StubToolInstance(desc)
    runtime.register_tool(tool, instance)


# ---------------------------------------------------------------------------
# Safety profile construction
# ---------------------------------------------------------------------------


@given("a safety profile with allow_unsafe_tools {val}")
def step_safety_allow_unsafe(context: Context, val: str) -> None:
    """Create a safety profile with specific allow_unsafe_tools value."""
    context.enforcement_safety = SafetyProfile(
        allow_unsafe_tools=val.lower() == "true",
        require_checkpoints=False,
        require_sandbox=False,
    )


@given('a safety profile with allowed_skill_categories "{cats}"')
def step_safety_categories(context: Context, cats: str) -> None:
    """Create a safety profile with specific allowed skill categories."""
    cat_list = [c.strip() for c in cats.split(",") if c.strip()]
    context.enforcement_safety = SafetyProfile(
        allowed_skill_categories=cat_list,
        require_checkpoints=False,
        require_sandbox=False,
    )


@given("a safety profile with empty allowed_skill_categories")
def step_safety_empty_categories(context: Context) -> None:
    """Create a safety profile with empty allowed skill categories."""
    context.enforcement_safety = SafetyProfile(
        allowed_skill_categories=[],
        require_checkpoints=False,
        require_sandbox=False,
    )


@given("a safety profile with require_checkpoints {val}")
def step_safety_require_checkpoints(context: Context, val: str) -> None:
    """Create a safety profile with specific require_checkpoints value."""
    context.enforcement_safety = SafetyProfile(
        require_checkpoints=val.lower() == "true",
        require_sandbox=False,
    )


@given('a combined safety profile blocking unsafe tools with categories "{cats}"')
def step_safety_combined(context: Context, cats: str) -> None:
    """Create a safety profile blocking unsafe tools with category constraints."""
    cat_list = [c.strip() for c in cats.split(",") if c.strip()]
    context.enforcement_safety = SafetyProfile(
        allow_unsafe_tools=False,
        allowed_skill_categories=cat_list,
        require_checkpoints=False,
        require_sandbox=False,
    )


# ---------------------------------------------------------------------------
# Tool execution context construction
# ---------------------------------------------------------------------------


@given("a tool execution context with the safety profile")
def step_ctx_with_safety(context: Context) -> None:
    """Create a ToolExecutionContext with the current safety profile."""
    context.enforcement_ctx = ToolExecutionContext(
        plan_id="test-plan-001",
        safety_profile=context.enforcement_safety,
    )


@given("a tool execution context with the safety profile and require_checkpoints {val}")
def step_ctx_with_safety_and_cp(context: Context, val: str) -> None:
    """Create a context with safety profile and explicit require_checkpoints."""
    context.enforcement_ctx = ToolExecutionContext(
        plan_id="test-plan-001",
        require_checkpoints=val.lower() == "true",
        safety_profile=context.enforcement_safety,
    )


@given("a tool execution context without a safety profile")
def step_ctx_without_safety(context: Context) -> None:
    """Create a ToolExecutionContext without a safety profile."""
    context.enforcement_ctx = ToolExecutionContext(
        plan_id="test-plan-001",
    )


@given("a tool execution context that is read-only without a safety profile")
def step_ctx_readonly(context: Context) -> None:
    """Create a read-only ToolExecutionContext without a safety profile."""
    context.enforcement_ctx = ToolExecutionContext(
        plan_id="test-plan-001",
        plan_read_only=True,
    )


@given('the tool skill category is "{category}"')
def step_set_tool_category(context: Context, category: str) -> None:
    """Set the tool_skill_category metadata on the execution context."""
    context.enforcement_ctx.metadata["tool_skill_category"] = category


# ---------------------------------------------------------------------------
# Sandbox requirement context construction
# ---------------------------------------------------------------------------


@given("a safety profile with require_sandbox {val}")
def step_safety_require_sandbox(context: Context, val: str) -> None:
    """Create a safety profile with specific require_sandbox value."""
    context.enforcement_safety = SafetyProfile(
        require_sandbox=val.lower() == "true",
        require_checkpoints=False,
    )


@given("a tool execution context with the safety profile and no sandbox_id")
def step_ctx_with_safety_no_sandbox(context: Context) -> None:
    """Create a context with safety profile and no sandbox_id."""
    context.enforcement_ctx = ToolExecutionContext(
        plan_id="test-plan-001",
        safety_profile=context.enforcement_safety,
        sandbox_id=None,
    )


@given('a tool execution context with the safety profile and sandbox_id "{sandbox_id}"')
def step_ctx_with_safety_and_sandbox(context: Context, sandbox_id: str) -> None:
    """Create a context with safety profile and a sandbox_id."""
    context.enforcement_ctx = ToolExecutionContext(
        plan_id="test-plan-001",
        safety_profile=context.enforcement_safety,
        sandbox_id=sandbox_id,
    )


# ---------------------------------------------------------------------------
# Human approval context construction
# ---------------------------------------------------------------------------


@given("a safety profile with require_human_approval {val}")
def step_safety_require_human_approval(context: Context, val: str) -> None:
    """Create a safety profile with specific require_human_approval value."""
    context.enforcement_safety = SafetyProfile(
        require_human_approval=val.lower() == "true",
        require_sandbox=False,
        require_checkpoints=False,
    )


@given("a tool execution context with the safety profile and no approval")
def step_ctx_with_safety_no_approval(context: Context) -> None:
    """Create a context with safety profile but no human approval."""
    context.enforcement_ctx = ToolExecutionContext(
        plan_id="test-plan-001",
        safety_profile=context.enforcement_safety,
    )


@given("a tool execution context with the safety profile and human approval granted")
def step_ctx_with_safety_and_approval(context: Context) -> None:
    """Create a context with safety profile and human approval granted."""
    context.enforcement_ctx = ToolExecutionContext(
        plan_id="test-plan-001",
        safety_profile=context.enforcement_safety,
        metadata={"human_approved": True},
    )


# ---------------------------------------------------------------------------
# Cost limit context construction
# ---------------------------------------------------------------------------


@given("a safety profile with max_cost_per_plan {val:g}")
def step_safety_max_cost_per_plan(context: Context, val: float) -> None:
    """Create a safety profile with max_cost_per_plan."""
    context.enforcement_safety = SafetyProfile(
        max_cost_per_plan=val,
        require_sandbox=False,
        require_checkpoints=False,
    )


@given("a tool execution context with the safety profile and accumulated_cost {val:g}")
def step_ctx_with_safety_and_cost(context: Context, val: float) -> None:
    """Create a context with safety profile and accumulated cost."""
    context.enforcement_ctx = ToolExecutionContext(
        plan_id="test-plan-001",
        safety_profile=context.enforcement_safety,
        accumulated_cost=val,
    )


@given("a safety profile with max_total_cost {val:g}")
def step_safety_max_total_cost(context: Context, val: float) -> None:
    """Create a safety profile with max_total_cost."""
    context.enforcement_safety = SafetyProfile(
        max_total_cost=val,
        require_sandbox=False,
        require_checkpoints=False,
    )


@given(
    "a tool execution context with the safety profile"
    " and total_accumulated_cost {val:g}"
)
def step_ctx_with_safety_and_total_cost(context: Context, val: float) -> None:
    """Create a context with safety profile and total accumulated cost."""
    context.enforcement_ctx = ToolExecutionContext(
        plan_id="test-plan-001",
        safety_profile=context.enforcement_safety,
        total_accumulated_cost=val,
    )


# ---------------------------------------------------------------------------
# Retry limit context construction
# ---------------------------------------------------------------------------


@given("a safety profile with max_retries_per_step {val:d}")
def step_safety_max_retries(context: Context, val: int) -> None:
    """Create a safety profile with max_retries_per_step."""
    context.enforcement_safety = SafetyProfile(
        max_retries_per_step=val,
        require_sandbox=False,
        require_checkpoints=False,
    )


@given("a tool execution context with the safety profile and step_retry_count {val:d}")
def step_ctx_with_safety_and_retries(context: Context, val: int) -> None:
    """Create a context with safety profile and step retry count."""
    context.enforcement_ctx = ToolExecutionContext(
        plan_id="test-plan-001",
        safety_profile=context.enforcement_safety,
        step_retry_count=val,
    )


# ---------------------------------------------------------------------------
# Missing metadata context construction
# ---------------------------------------------------------------------------


@given(
    "a tool execution context with the safety profile and no skill category metadata"
)
def step_ctx_with_safety_no_category(context: Context) -> None:
    """Create a context with safety profile but no tool_skill_category."""
    context.enforcement_ctx = ToolExecutionContext(
        plan_id="test-plan-001",
        safety_profile=context.enforcement_safety,
    )


# ---------------------------------------------------------------------------
# Execution steps
# ---------------------------------------------------------------------------


@when('I enforce safety and try to run tool "{tool_name}"')
def step_try_execute(context: Context, tool_name: str) -> None:
    """Try executing a tool through the safety-enforcing runtime, capturing errors."""
    runtime: ToolRuntime = context.enforcement_runtime
    ctx: ToolExecutionContext = context.enforcement_ctx
    context.enforcement_error = None
    context.enforcement_result = None
    try:
        result = runtime.execute(tool_name, {}, ctx)
        context.enforcement_result = result
    except (
        ToolSafetyViolationError,
        ToolAccessDeniedError,
        ToolCheckpointRequiredError,
        ToolSandboxRequiredError,
        ToolHumanApprovalRequiredError,
        ToolCostLimitExceededError,
        ToolRetryLimitExceededError,
    ) as exc:
        context.enforcement_error = exc


@when('I enforce safety and run tool "{tool_name}"')
def step_execute(context: Context, tool_name: str) -> None:
    """Execute a tool through the safety-enforcing runtime (expected to succeed)."""
    runtime: ToolRuntime = context.enforcement_runtime
    ctx: ToolExecutionContext = context.enforcement_ctx
    context.enforcement_result = runtime.execute(tool_name, {}, ctx)
    context.enforcement_error = None


# ---------------------------------------------------------------------------
# Assertions
# ---------------------------------------------------------------------------


@then("a ToolSafetyViolationError should be raised")
def step_check_safety_violation(context: Context) -> None:
    """Verify a ToolSafetyViolationError was raised."""
    assert context.enforcement_error is not None, (
        "Expected ToolSafetyViolationError but no error was raised"
    )
    assert isinstance(context.enforcement_error, ToolSafetyViolationError), (
        f"Expected ToolSafetyViolationError, "
        f"got {type(context.enforcement_error).__name__}: "
        f"{context.enforcement_error}"
    )


@then("a safety ToolCheckpointRequiredError should be raised")
def step_check_checkpoint_required(context: Context) -> None:
    """Verify a ToolCheckpointRequiredError was raised."""
    assert context.enforcement_error is not None, (
        "Expected ToolCheckpointRequiredError but no error was raised"
    )
    assert isinstance(context.enforcement_error, ToolCheckpointRequiredError), (
        f"Expected ToolCheckpointRequiredError, "
        f"got {type(context.enforcement_error).__name__}: "
        f"{context.enforcement_error}"
    )


@then("a safety ToolAccessDeniedError should be raised")
def step_check_access_denied(context: Context) -> None:
    """Verify a ToolAccessDeniedError was raised."""
    assert context.enforcement_error is not None, (
        "Expected ToolAccessDeniedError but no error was raised"
    )
    assert isinstance(context.enforcement_error, ToolAccessDeniedError), (
        f"Expected ToolAccessDeniedError, "
        f"got {type(context.enforcement_error).__name__}: "
        f"{context.enforcement_error}"
    )


@then('the safety violation error should mention "{text}"')
def step_check_violation_text(context: Context, text: str) -> None:
    """Check that the safety violation error message contains the expected text."""
    error_str = str(context.enforcement_error)
    assert text in error_str, f"Expected error to mention '{text}', got: {error_str}"


@then("a ToolSandboxRequiredError should be raised")
def step_check_sandbox_required(context: Context) -> None:
    """Verify a ToolSandboxRequiredError was raised."""
    assert context.enforcement_error is not None, (
        "Expected ToolSandboxRequiredError but no error was raised"
    )
    assert isinstance(context.enforcement_error, ToolSandboxRequiredError), (
        f"Expected ToolSandboxRequiredError, "
        f"got {type(context.enforcement_error).__name__}: "
        f"{context.enforcement_error}"
    )


@then('the sandbox error should mention "{text}"')
def step_check_sandbox_error_text(context: Context, text: str) -> None:
    """Check that the sandbox error message contains the expected text."""
    error_str = str(context.enforcement_error)
    assert text in error_str, f"Expected error to mention '{text}', got: {error_str}"


@then("a ToolHumanApprovalRequiredError should be raised")
def step_check_human_approval_required(context: Context) -> None:
    """Verify a ToolHumanApprovalRequiredError was raised."""
    assert context.enforcement_error is not None, (
        "Expected ToolHumanApprovalRequiredError but no error was raised"
    )
    assert isinstance(context.enforcement_error, ToolHumanApprovalRequiredError), (
        f"Expected ToolHumanApprovalRequiredError, "
        f"got {type(context.enforcement_error).__name__}: "
        f"{context.enforcement_error}"
    )


@then('the approval error should mention "{text}"')
def step_check_approval_error_text(context: Context, text: str) -> None:
    """Check that the approval error message contains the expected text."""
    error_str = str(context.enforcement_error)
    assert text in error_str, f"Expected error to mention '{text}', got: {error_str}"


@then("a ToolCostLimitExceededError should be raised")
def step_check_cost_limit_exceeded(context: Context) -> None:
    """Verify a ToolCostLimitExceededError was raised."""
    assert context.enforcement_error is not None, (
        "Expected ToolCostLimitExceededError but no error was raised"
    )
    assert isinstance(context.enforcement_error, ToolCostLimitExceededError), (
        f"Expected ToolCostLimitExceededError, "
        f"got {type(context.enforcement_error).__name__}: "
        f"{context.enforcement_error}"
    )


@then('the cost error should mention "{text}"')
def step_check_cost_error_text(context: Context, text: str) -> None:
    """Check that the cost error message contains the expected text."""
    error_str = str(context.enforcement_error)
    assert text in error_str, f"Expected error to mention '{text}', got: {error_str}"


@then("a ToolRetryLimitExceededError should be raised")
def step_check_retry_limit_exceeded(context: Context) -> None:
    """Verify a ToolRetryLimitExceededError was raised."""
    assert context.enforcement_error is not None, (
        "Expected ToolRetryLimitExceededError but no error was raised"
    )
    assert isinstance(context.enforcement_error, ToolRetryLimitExceededError), (
        f"Expected ToolRetryLimitExceededError, "
        f"got {type(context.enforcement_error).__name__}: "
        f"{context.enforcement_error}"
    )


@then('the retry error should mention "{text}"')
def step_check_retry_error_text(context: Context, text: str) -> None:
    """Check that the retry error message contains the expected text."""
    error_str = str(context.enforcement_error)
    assert text in error_str, f"Expected error to mention '{text}', got: {error_str}"


@then("the tool execution should succeed")
def step_check_execution_success(context: Context) -> None:
    """Verify the tool execution succeeded."""
    assert context.enforcement_result is not None, "Expected a tool result but got None"
    assert context.enforcement_result.success is True, (
        f"Expected success=True, got: {context.enforcement_result}"
    )
