"""Step definitions for ResourceHandlerService full coverage tests.

All step patterns are prefixed with 'rhs' to avoid collisions with other
step definition files in the same features/steps directory.
"""

from __future__ import annotations

import contextlib
from unittest.mock import MagicMock, patch

from behave import given, then, when

from cleveragents.application.services.resource_handler_service import (
    ResourceHandlerService,
)
from cleveragents.core.exceptions import NotFoundError
from cleveragents.domain.models.core.resource_slot import BindingResult
from cleveragents.resource.handlers.resolver import HandlerResolutionError
from cleveragents.tool.context import BoundResource

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


def _make_resource_mock(
    resource_id,
    resource_type_name,
    location=None,
    sandbox_strategy=None,
):
    """Create a mock Resource with the given attributes."""
    resource = MagicMock()
    resource.resource_id = resource_id
    resource.resource_type_name = resource_type_name
    resource.location = location
    # sandbox_strategy=None means no override; "none" is a valid strategy string
    resource.sandbox_strategy = sandbox_strategy
    return resource


def _make_type_spec_mock(
    name,
    handler=None,
    sandbox_strategy="copy_on_write",
):
    """Create a mock ResourceTypeSpec with the given attributes."""
    spec = MagicMock()
    spec.name = name
    spec.handler = handler
    spec.sandbox_strategy = sandbox_strategy
    return spec


def _make_binding_obj(
    slot_name,
    resource_id=None,
    deferred=False,
):
    """Create a BindingResult with the given attributes."""
    return BindingResult(
        slot_name=slot_name,
        resource_id=resource_id,
        binding_mode="parameter" if deferred else "contextual",
        deferred=deferred,
    )


def _make_sandbox_mock(sandbox_path):
    """Create a mock Sandbox with a context carrying sandbox_path."""
    sandbox = MagicMock()
    if sandbox_path is not None:
        sandbox.context = MagicMock()
        sandbox.context.sandbox_path = sandbox_path
    else:
        sandbox.context = None
    return sandbox


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


@given("a mock sandbox manager for rhs")
def step_rhs_mock_sandbox_manager(context):
    context.rhs_sandbox_manager = MagicMock()
    context.rhs_sandbox_call_args = None


@given("a mock resource lookup for rhs")
def step_rhs_mock_resource_lookup(context):
    context.rhs_resources = {}
    context.rhs_resource_lookup_error = {}

    def _lookup(name_or_id):
        if name_or_id in context.rhs_resource_lookup_error:
            raise context.rhs_resource_lookup_error[name_or_id]
        return context.rhs_resources[name_or_id]

    context.rhs_resource_lookup = _lookup


@given("a mock type lookup for rhs")
def step_rhs_mock_type_lookup(context):
    context.rhs_type_specs = {}

    def _lookup(name):
        return context.rhs_type_specs[name]

    context.rhs_type_lookup = _lookup


@given("a ResourceHandlerService under test")
def step_rhs_service_instance(context):
    context.rhs_service = ResourceHandlerService(
        sandbox_manager=context.rhs_sandbox_manager,
        resource_lookup=context.rhs_resource_lookup,
        type_lookup=context.rhs_type_lookup,
    )
    context.rhs_result = None
    context.rhs_error = None
    context.rhs_resolve_handler_patcher = None


# ---------------------------------------------------------------------------
# Binding setup
# ---------------------------------------------------------------------------


@given('an rhs binding that is deferred with slot name "{slot}"')
def step_rhs_deferred_binding(context, slot):
    context.rhs_binding = _make_binding_obj(slot_name=slot, deferred=True)


@given('an rhs binding with no resource_id and slot name "{slot}"')
def step_rhs_binding_no_resource_id(context, slot):
    context.rhs_binding = _make_binding_obj(
        slot_name=slot, resource_id=None, deferred=False
    )


@given('an rhs non-deferred binding with slot "{slot}" and resource_id "{rid}"')
def step_rhs_non_deferred_binding(context, slot, rid):
    context.rhs_binding = _make_binding_obj(
        slot_name=slot, resource_id=rid, deferred=False
    )


# ---------------------------------------------------------------------------
# Resource setup — single step for location + strategy
# ---------------------------------------------------------------------------


@given(
    'rhs resource "{rid}" typed "{rtype}" located at "{loc}" with strategy "{strat}"'
)
def step_rhs_resource_with_loc_and_strat(context, rid, rtype, loc, strat):
    # Convert the string "none" to Python None for sandbox_strategy
    strategy = None if strat == "none" else strat
    context.rhs_resources[rid] = _make_resource_mock(
        resource_id=rid,
        resource_type_name=rtype,
        location=loc,
        sandbox_strategy=strategy,
    )


@given('rhs resource "{rid}" typed "{rtype}" with no location')
def step_rhs_resource_no_location(context, rid, rtype):
    context.rhs_resources[rid] = _make_resource_mock(
        resource_id=rid,
        resource_type_name=rtype,
        location=None,
        sandbox_strategy=None,
    )


# ---------------------------------------------------------------------------
# Type spec setup
# ---------------------------------------------------------------------------


@given('an rhs type spec "{name}" with no handler and sandbox strategy "{strat}"')
def step_rhs_type_spec_no_handler(context, name, strat):
    context.rhs_type_specs[name] = _make_type_spec_mock(
        name=name, handler=None, sandbox_strategy=strat
    )


@given(
    'an rhs type spec "{name}" with handler "{handler}" and sandbox strategy "{strat}"'
)
def step_rhs_type_spec_with_handler(context, name, handler, strat):
    context.rhs_type_specs[name] = _make_type_spec_mock(
        name=name, handler=handler, sandbox_strategy=strat
    )


# ---------------------------------------------------------------------------
# Sandbox manager setup
# ---------------------------------------------------------------------------


@given('the rhs sandbox manager returns a sandbox with path "{path}"')
def step_rhs_sandbox_returns_path(context, path):
    sandbox = _make_sandbox_mock(sandbox_path=path)

    def _capture_call(**kwargs):
        context.rhs_sandbox_call_args = kwargs
        return sandbox

    context.rhs_sandbox_manager.get_or_create_sandbox = MagicMock(
        side_effect=_capture_call
    )


@given("the rhs sandbox manager returns a sandbox with no context")
def step_rhs_sandbox_returns_no_context(context):
    sandbox = _make_sandbox_mock(sandbox_path=None)
    context.rhs_sandbox_manager.get_or_create_sandbox = MagicMock(return_value=sandbox)


# ---------------------------------------------------------------------------
# Handler mock setup
# ---------------------------------------------------------------------------


@given("the rhs resolve_handler function returns a mock handler")
def step_rhs_resolve_handler_returns_mock(context):
    context.rhs_mock_handler = MagicMock()
    context.rhs_resolve_handler_patcher = patch(
        "cleveragents.application.services.resource_handler_service.resolve_handler",
        return_value=context.rhs_mock_handler,
    )
    context.rhs_resolve_handler_patcher.start()


@given('the rhs mock handler resolves to a BoundResource with sandbox_path "{path}"')
def step_rhs_mock_handler_returns_bound(context, path):
    def _resolve(**kwargs):
        return BoundResource(
            slot_name=kwargs["slot_name"],
            resource_id=kwargs["resource"].resource_id,
            resource_type=kwargs["resource"].resource_type_name,
            sandbox_path=path,
            access=kwargs.get("access", "read_only"),
        )

    context.rhs_mock_handler.resolve = MagicMock(side_effect=_resolve)


@given("the rhs resolve_handler function raises HandlerResolutionError")
def step_rhs_resolve_handler_raises(context):
    context.rhs_resolve_handler_patcher = patch(
        "cleveragents.application.services.resource_handler_service.resolve_handler",
        side_effect=HandlerResolutionError("handler not found"),
    )
    context.rhs_resolve_handler_patcher.start()


# ---------------------------------------------------------------------------
# Error setup for resource lookup
# ---------------------------------------------------------------------------


@given('the rhs resource lookup raises NotFoundError for "{rid}"')
def step_rhs_resource_lookup_raises(context, rid):
    context.rhs_resource_lookup_error[rid] = NotFoundError(
        message=f"Resource '{rid}' not found"
    )


# ---------------------------------------------------------------------------
# Binding list setup
# ---------------------------------------------------------------------------


@given("an rhs binding list with:")
def step_rhs_binding_list(context):
    context.rhs_bindings = []
    for row in context.table:
        slot_name = row["slot_name"]
        resource_id = row["resource_id"] if row["resource_id"] else None
        deferred = row["deferred"].lower() == "true"
        context.rhs_bindings.append(
            _make_binding_obj(
                slot_name=slot_name, resource_id=resource_id, deferred=deferred
            )
        )


# ---------------------------------------------------------------------------
# Cleanup helper
# ---------------------------------------------------------------------------


def _rhs_cleanup_patcher(context):
    """Stop the resolve_handler patcher if active."""
    patcher = getattr(context, "rhs_resolve_handler_patcher", None)
    if patcher is not None:
        with contextlib.suppress(RuntimeError):
            patcher.stop()
        context.rhs_resolve_handler_patcher = None


# ---------------------------------------------------------------------------
# When: resolve_binding
# ---------------------------------------------------------------------------


@when("I resolve the rhs single binding")
def step_rhs_resolve_single_binding_default(context):
    try:
        context.rhs_result = context.rhs_service.resolve_binding(
            binding=context.rhs_binding,
            plan_id="default-plan",
            access="read_only",
        )
    except Exception as exc:
        context.rhs_error = exc
    finally:
        _rhs_cleanup_patcher(context)


@when('I resolve the rhs single binding with plan_id "{pid}" and access "{access}"')
def step_rhs_resolve_single_binding(context, pid, access):
    try:
        context.rhs_result = context.rhs_service.resolve_binding(
            binding=context.rhs_binding,
            plan_id=pid,
            access=access,
        )
    except Exception as exc:
        context.rhs_error = exc
    finally:
        _rhs_cleanup_patcher(context)


# ---------------------------------------------------------------------------
# When: resolve_bindings
# ---------------------------------------------------------------------------


@when('I resolve all rhs bindings with plan_id "{pid}" and access "{access}"')
def step_rhs_resolve_all_bindings(context, pid, access):
    try:
        context.rhs_result = context.rhs_service.resolve_bindings(
            bindings=context.rhs_bindings,
            plan_id=pid,
            access=access,
        )
    except Exception as exc:
        context.rhs_error = exc
    finally:
        _rhs_cleanup_patcher(context)


@when("I resolve all rhs bindings expecting an error")
def step_rhs_resolve_all_bindings_error(context):
    try:
        context.rhs_result = context.rhs_service.resolve_bindings(
            bindings=context.rhs_bindings,
            plan_id="error-plan",
            access="read_only",
        )
    except Exception as exc:
        context.rhs_error = exc
    finally:
        _rhs_cleanup_patcher(context)


# ---------------------------------------------------------------------------
# When: resolve_resource
# ---------------------------------------------------------------------------


@when(
    'I resolve the rhs resource directly with plan_id "{pid}" slot "{slot}" and access "{access}"'
)
def step_rhs_resolve_resource_directly(context, pid, slot, access):
    # Use the most recently added resource
    rid = list(context.rhs_resources.keys())[-1]
    resource = context.rhs_resources[rid]
    try:
        context.rhs_result = context.rhs_service.resolve_resource(
            resource=resource,
            plan_id=pid,
            slot_name=slot,
            access=access,
        )
    except Exception as exc:
        context.rhs_error = exc
    finally:
        _rhs_cleanup_patcher(context)


# ---------------------------------------------------------------------------
# Then: BoundResource assertions
# ---------------------------------------------------------------------------


@then('the rhs result is a BoundResource with slot "{slot}" and sandbox_path "{path}"')
def step_rhs_assert_bound_resource(context, slot, path):
    assert context.rhs_error is None, f"Unexpected error: {context.rhs_error}"
    assert isinstance(context.rhs_result, BoundResource), (
        f"Expected BoundResource, got {type(context.rhs_result)}"
    )
    assert context.rhs_result.slot_name == slot, (
        f"Expected slot_name '{slot}', got '{context.rhs_result.slot_name}'"
    )
    assert context.rhs_result.sandbox_path == path, (
        f"Expected sandbox_path '{path}', got '{context.rhs_result.sandbox_path}'"
    )


@then('the rhs BoundResource has resource_id "{rid}" and access "{access}"')
def step_rhs_assert_bound_resource_details(context, rid, access):
    assert context.rhs_result.resource_id == rid, (
        f"Expected resource_id '{rid}', got '{context.rhs_result.resource_id}'"
    )
    assert context.rhs_result.access == access, (
        f"Expected access '{access}', got '{context.rhs_result.access}'"
    )


# ---------------------------------------------------------------------------
# Then: error assertions
# ---------------------------------------------------------------------------


@then('an rhs ValueError is raised with message containing "{fragment}"')
def step_rhs_assert_value_error(context, fragment):
    assert context.rhs_error is not None, (
        "Expected a ValueError but no error was raised"
    )
    assert isinstance(context.rhs_error, ValueError), (
        f"Expected ValueError, got {type(context.rhs_error).__name__}: {context.rhs_error}"
    )
    assert fragment in str(context.rhs_error), (
        f"Expected '{fragment}' in error message, got: {context.rhs_error}"
    )


@then("an rhs NotFoundError is raised")
def step_rhs_assert_not_found_error(context):
    assert context.rhs_error is not None, (
        "Expected a NotFoundError but no error was raised"
    )
    assert isinstance(context.rhs_error, NotFoundError), (
        f"Expected NotFoundError, got {type(context.rhs_error).__name__}: {context.rhs_error}"
    )


@then('an rhs RuntimeError is raised with message containing "{fragment}"')
def step_rhs_assert_runtime_error(context, fragment):
    assert context.rhs_error is not None, (
        "Expected a RuntimeError but no error was raised"
    )
    assert isinstance(context.rhs_error, RuntimeError), (
        f"Expected RuntimeError, got {type(context.rhs_error).__name__}: {context.rhs_error}"
    )
    assert fragment in str(context.rhs_error), (
        f"Expected '{fragment}' in error message, got: {context.rhs_error}"
    )


# ---------------------------------------------------------------------------
# Then: dict result assertions
# ---------------------------------------------------------------------------


@then('the rhs result dict has keys "{key1}" and "{key2}"')
def step_rhs_assert_dict_keys(context, key1, key2):
    assert context.rhs_error is None, f"Unexpected error: {context.rhs_error}"
    assert isinstance(context.rhs_result, dict), (
        f"Expected dict, got {type(context.rhs_result)}"
    )
    assert key1 in context.rhs_result, f"Key '{key1}' not found in result dict"
    assert key2 in context.rhs_result, f"Key '{key2}' not found in result dict"


@then('the rhs result dict does not contain key "{key}"')
def step_rhs_assert_dict_missing_key(context, key):
    assert key not in context.rhs_result, (
        f"Key '{key}' should not be in result dict but was found"
    )


@then("the rhs result dict is empty")
def step_rhs_assert_dict_empty(context):
    assert context.rhs_error is None, f"Unexpected error: {context.rhs_error}"
    assert isinstance(context.rhs_result, dict), (
        f"Expected dict, got {type(context.rhs_result)}"
    )
    assert len(context.rhs_result) == 0, (
        f"Expected empty dict, got {len(context.rhs_result)} entries"
    )


# ---------------------------------------------------------------------------
# Then: sandbox manager call assertions
# ---------------------------------------------------------------------------


@then('the rhs sandbox manager was called with strategy "{strategy}"')
def step_rhs_assert_sandbox_strategy(context, strategy):
    assert context.rhs_error is None, f"Unexpected error: {context.rhs_error}"
    assert context.rhs_sandbox_call_args is not None, (
        "sandbox_manager.get_or_create_sandbox was not called"
    )
    actual = context.rhs_sandbox_call_args.get("sandbox_strategy")
    assert actual == strategy, f"Expected sandbox_strategy '{strategy}', got '{actual}'"
