"""Step definitions for resource_handlers.feature.

Tests the resource handler protocol, GitCheckoutHandler, FsDirectoryHandler,
handler resolver, and ResourceHandlerService orchestration bridge.
"""

from __future__ import annotations

from datetime import datetime
from unittest.mock import MagicMock, PropertyMock

from behave import given, then, when  # type: ignore[import-untyped]
from behave.runner import Context

from cleveragents.domain.models.core.resource import (
    PhysVirt,
    Resource,
    ResourceCapabilities,
    SandboxStrategy,
)
from cleveragents.domain.models.core.resource_slot import BindingResult
from cleveragents.domain.models.core.resource_type import (
    ResourceKind,
    ResourceTypeSpec,
)
from cleveragents.domain.models.core.resource_type import (
    SandboxStrategy as TypeSandboxStrategy,
)
from cleveragents.infrastructure.sandbox.factory import SandboxFactory
from cleveragents.infrastructure.sandbox.manager import SandboxManager
from cleveragents.infrastructure.sandbox.protocol import (
    SandboxContext,
    SandboxStatus,
)
from cleveragents.resource.handlers.fs_directory import FsDirectoryHandler
from cleveragents.resource.handlers.git_checkout import GitCheckoutHandler
from cleveragents.resource.handlers.protocol import ResourceHandler
from cleveragents.resource.handlers.resolver import (
    HandlerResolutionError,
    clear_handler_cache,
    resolve_handler,
)
from cleveragents.tool.context import BoundResource

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


def _make_resource(
    resource_type_name: str,
    location: str | None = None,
    sandbox_strategy: SandboxStrategy | None = None,
    resource_id: str = "01KJ5C5TPMP8GGX3QC83E2MAQS",
) -> Resource:
    """Create a test Resource domain object."""
    return Resource(
        resource_id=resource_id,
        resource_type_name=resource_type_name,
        classification=PhysVirt.PHYSICAL,
        location=location,
        sandbox_strategy=sandbox_strategy,
        capabilities=ResourceCapabilities(
            readable=True, writable=True, sandboxable=True
        ),
    )


def _make_mock_sandbox_manager() -> tuple[SandboxManager, MagicMock]:
    """Create a SandboxManager with a mock factory that tracks calls."""
    mock_factory = MagicMock(spec=SandboxFactory)

    mock_sandbox = MagicMock()
    mock_sandbox.sandbox_id = "sb-test-001"
    type(mock_sandbox).status = PropertyMock(return_value=SandboxStatus.CREATED)
    mock_sandbox.context = SandboxContext(
        sandbox_id="sb-test-001",
        sandbox_path="/tmp/sandbox/sb-test-001",
        original_path="/tmp/original",
        resource_id="res-test",
        plan_id="plan-test",
        created_at=datetime.now(),
    )
    mock_sandbox.create.return_value = mock_sandbox.context

    mock_factory.create_sandbox.return_value = mock_sandbox

    manager = SandboxManager(factory=mock_factory, cleanup_on_exit=False)
    return manager, mock_factory


def _make_type_spec(
    name: str = "git-checkout",
    sandbox_strategy: str = "git_worktree",
    handler: str
    | None = "cleveragents.resource.handlers.git_checkout:GitCheckoutHandler",
) -> ResourceTypeSpec:
    """Create a minimal ResourceTypeSpec for testing."""
    return ResourceTypeSpec(
        name=name,
        description=f"Test {name} type",
        resource_kind=ResourceKind.PHYSICAL,
        sandbox_strategy=TypeSandboxStrategy(sandbox_strategy),
        user_addable=True,
        built_in=True,
        handler=handler,
    )


# ---------------------------------------------------------------------------
# Protocol conformance
# ---------------------------------------------------------------------------


@then("GitCheckoutHandler should satisfy the ResourceHandler protocol")
def step_git_handler_protocol(context: Context) -> None:
    handler = GitCheckoutHandler()
    assert isinstance(handler, ResourceHandler), (
        "GitCheckoutHandler does not satisfy ResourceHandler protocol"
    )


@then("FsDirectoryHandler should satisfy the ResourceHandler protocol")
def step_fs_handler_protocol(context: Context) -> None:
    handler = FsDirectoryHandler()
    assert isinstance(handler, ResourceHandler), (
        "FsDirectoryHandler does not satisfy ResourceHandler protocol"
    )


# ---------------------------------------------------------------------------
# GitCheckoutHandler
# ---------------------------------------------------------------------------


@given('a git-checkout resource with location "{location}"')
def step_git_resource(context: Context, location: str) -> None:
    context.handler_resource = _make_resource("git-checkout", location=location)


@given('a git-checkout resource at "{location}" using strategy "{strategy}"')
def step_git_resource_with_strategy(
    context: Context, location: str, strategy: str
) -> None:
    context.handler_resource = _make_resource(
        "git-checkout",
        location=location,
        sandbox_strategy=SandboxStrategy(strategy),
    )


@given("a git-checkout resource without location")
def step_git_resource_no_location(context: Context) -> None:
    context.handler_resource = _make_resource("git-checkout", location=None)


@given("a sandbox manager with a mock factory")
def step_mock_sandbox_manager(context: Context) -> None:
    context.handler_sandbox_manager, context.handler_mock_factory = (
        _make_mock_sandbox_manager()
    )


@when('I resolve the resource with GitCheckoutHandler for plan "{plan_id}"')
def step_resolve_git(context: Context, plan_id: str) -> None:
    handler = GitCheckoutHandler()
    context.handler_bound = handler.resolve(
        resource=context.handler_resource,
        plan_id=plan_id,
        slot_name="repo",
        sandbox_manager=context.handler_sandbox_manager,
    )


@when("I try to resolve the resource with GitCheckoutHandler")
def step_try_resolve_git(context: Context) -> None:
    handler = GitCheckoutHandler()
    try:
        handler.resolve(
            resource=context.handler_resource,
            plan_id="PLAN_ERR",
            slot_name="repo",
            sandbox_manager=context.handler_sandbox_manager,
        )
        context.handler_error = None
    except ValueError as exc:
        context.handler_error = exc


# ---------------------------------------------------------------------------
# FsDirectoryHandler
# ---------------------------------------------------------------------------


@given('an fs-directory resource with location "{location}"')
def step_fs_resource(context: Context, location: str) -> None:
    context.handler_resource = _make_resource("fs-directory", location=location)


@given("an fs-directory resource without location")
def step_fs_resource_no_location(context: Context) -> None:
    context.handler_resource = _make_resource("fs-directory", location=None)


@when('I resolve the resource with FsDirectoryHandler for plan "{plan_id}"')
def step_resolve_fs(context: Context, plan_id: str) -> None:
    handler = FsDirectoryHandler()
    context.handler_bound = handler.resolve(
        resource=context.handler_resource,
        plan_id=plan_id,
        slot_name="workdir",
        sandbox_manager=context.handler_sandbox_manager,
    )


@when("I try to resolve the resource with FsDirectoryHandler")
def step_try_resolve_fs(context: Context) -> None:
    handler = FsDirectoryHandler()
    try:
        handler.resolve(
            resource=context.handler_resource,
            plan_id="PLAN_ERR",
            slot_name="workdir",
            sandbox_manager=context.handler_sandbox_manager,
        )
        context.handler_error = None
    except ValueError as exc:
        context.handler_error = exc


# ---------------------------------------------------------------------------
# Shared handler assertions
# ---------------------------------------------------------------------------


@then('the bound resource should have slot name "{name}"')
def step_bound_slot(context: Context, name: str) -> None:
    assert context.handler_bound.slot_name == name


@then('the bound resource should have resource type "{rtype}"')
def step_bound_type(context: Context, rtype: str) -> None:
    assert context.handler_bound.resource_type == rtype


@then("the bound resource sandbox path should not be empty")
def step_bound_sandbox_not_empty(context: Context) -> None:
    assert context.handler_bound.sandbox_path, (
        f"sandbox_path is empty: {context.handler_bound.sandbox_path!r}"
    )


@then('the sandbox was created with strategy "{strategy}"')
def step_sandbox_strategy(context: Context, strategy: str) -> None:
    call_args = context.handler_mock_factory.create_sandbox.call_args
    assert call_args is not None, "create_sandbox was not called"
    assert call_args.kwargs.get("sandbox_strategy") == strategy or (
        len(call_args.args) >= 3 and call_args.args[2] == strategy
    ), f"Expected strategy {strategy}, got {call_args}"


@then("a handler ValueError should be raised")
def step_handler_value_error(context: Context) -> None:
    assert context.handler_error is not None
    assert isinstance(context.handler_error, ValueError)


@then('the handler error should mention "{text}"')
def step_handler_error_text(context: Context, text: str) -> None:
    assert text.lower() in str(context.handler_error).lower(), (
        f"Expected '{text}' in error: {context.handler_error}"
    )


# ---------------------------------------------------------------------------
# Handler Resolver
# ---------------------------------------------------------------------------


@when('I resolve handler "{ref}"')
def step_resolve_handler(context: Context, ref: str) -> None:
    clear_handler_cache()
    context.handler_resolved = resolve_handler(ref)
    context.handler_ref_used = ref


@when("I resolve the same handler reference again")
def step_resolve_handler_again(context: Context) -> None:
    context.handler_resolved_second = resolve_handler(context.handler_ref_used)


@then("the resolved handler should be a GitCheckoutHandler instance")
def step_resolved_is_git(context: Context) -> None:
    assert isinstance(context.handler_resolved, GitCheckoutHandler)


@then("the resolved handler should be a FsDirectoryHandler instance")
def step_resolved_is_fs(context: Context) -> None:
    assert isinstance(context.handler_resolved, FsDirectoryHandler)


@then("both resolved handlers should be the same object")
def step_same_object(context: Context) -> None:
    assert context.handler_resolved is context.handler_resolved_second


@when("I try to resolve an empty handler reference")
def step_try_resolve_empty_handler(context: Context) -> None:
    clear_handler_cache()
    try:
        resolve_handler("")
        context.handler_resolution_error = None
    except HandlerResolutionError as exc:
        context.handler_resolution_error = exc


@when('I try to resolve handler "{ref}"')
def step_try_resolve_handler(context: Context, ref: str) -> None:
    clear_handler_cache()
    try:
        resolve_handler(ref)
        context.handler_resolution_error = None
    except HandlerResolutionError as exc:
        context.handler_resolution_error = exc


@then("a HandlerResolutionError should be raised")
def step_resolution_error(context: Context) -> None:
    assert context.handler_resolution_error is not None
    assert isinstance(context.handler_resolution_error, HandlerResolutionError)


@then('the resolution error should mention "{text}"')
def step_resolution_error_text(context: Context, text: str) -> None:
    assert text.lower() in str(context.handler_resolution_error).lower(), (
        f"Expected '{text}' in error: {context.handler_resolution_error}"
    )


# ---------------------------------------------------------------------------
# ResourceHandlerService
# ---------------------------------------------------------------------------


@given("a resource handler service with mock lookups")
def step_handler_service(context: Context) -> None:
    from cleveragents.application.services.resource_handler_service import (
        ResourceHandlerService,
    )

    manager, _mock_factory = _make_mock_sandbox_manager()
    resource = _make_resource("git-checkout", location="/tmp/test-repo")
    type_spec = _make_type_spec()

    context.handler_svc = ResourceHandlerService(
        sandbox_manager=manager,
        resource_lookup=lambda _name_or_id: resource,
        type_lookup=lambda _name: type_spec,
    )
    context.handler_svc_manager = manager


@given("a resource handler service with mock lookups and no handler string")
def step_handler_service_no_handler(context: Context) -> None:
    from cleveragents.application.services.resource_handler_service import (
        ResourceHandlerService,
    )

    manager, _mock_factory = _make_mock_sandbox_manager()
    resource = _make_resource("fs-directory", location="/tmp/test-dir")
    type_spec = _make_type_spec(
        name="fs-directory",
        sandbox_strategy="copy_on_write",
        handler=None,
    )

    context.handler_svc = ResourceHandlerService(
        sandbox_manager=manager,
        resource_lookup=lambda _name_or_id: resource,
        type_lookup=lambda _name: type_spec,
    )


@given('a binding result for slot "{slot}" with resource id "{res_id}"')
def step_binding_result(context: Context, slot: str, res_id: str) -> None:
    if not hasattr(context, "handler_bindings"):
        context.handler_bindings = []
    context.handler_binding = BindingResult(
        slot_name=slot,
        resource_id=res_id,
        resource_name=f"test-{res_id}",
        binding_mode="contextual",
    )
    context.handler_bindings.append(context.handler_binding)


@given('a deferred binding result for slot "{slot}"')
def step_deferred_binding(context: Context, slot: str) -> None:
    if not hasattr(context, "handler_bindings"):
        context.handler_bindings = []
    context.handler_deferred_binding = BindingResult(
        slot_name=slot,
        resource_id=None,
        binding_mode="parameter",
        deferred=True,
    )
    context.handler_bindings.append(context.handler_deferred_binding)


@when('I resolve the binding via resource handler service for plan "{plan_id}"')
def step_svc_resolve_binding(context: Context, plan_id: str) -> None:
    context.handler_svc_bound = context.handler_svc.resolve_binding(
        binding=context.handler_binding,
        plan_id=plan_id,
    )


@when('I resolve all bindings via resource handler service for plan "{plan_id}"')
def step_svc_resolve_all(context: Context, plan_id: str) -> None:
    context.handler_svc_all = context.handler_svc.resolve_bindings(
        bindings=context.handler_bindings,
        plan_id=plan_id,
    )


@when("I try to resolve the single deferred binding via resource handler service")
def step_svc_try_resolve_deferred(context: Context) -> None:
    try:
        context.handler_svc.resolve_binding(
            binding=context.handler_deferred_binding,
            plan_id="PLAN_ERR",
        )
        context.handler_error = None
    except ValueError as exc:
        context.handler_error = exc


@then("the handler service should return a BoundResource")
def step_svc_is_bound(context: Context) -> None:
    assert isinstance(context.handler_svc_bound, BoundResource)


@then('the handler service BoundResource slot should be "{slot}"')
def step_svc_bound_slot(context: Context, slot: str) -> None:
    assert context.handler_svc_bound.slot_name == slot


@then("the handler service BoundResource sandbox path should not be empty")
def step_svc_bound_path(context: Context) -> None:
    assert context.handler_svc_bound.sandbox_path, (
        f"sandbox_path is empty: {context.handler_svc_bound.sandbox_path!r}"
    )


@then("the handler service should return {count:d} bound resource")
def step_svc_count(context: Context, count: int) -> None:
    assert len(context.handler_svc_all) == count


@then('the handler service should have resolved slot "{slot}"')
def step_svc_has_slot(context: Context, slot: str) -> None:
    assert slot in context.handler_svc_all
