"""Step definitions for tool_registry_service_uncovered_branches.feature.

Exercises the fallback delegation branches in ToolRegistryService that are
not covered by the primary test suite:

- register_tool: create not callable → add fallback → direct create fallback
- remove_tool:   delete not callable → remove fallback → direct delete fallback
- update_tool:   tool_config as Tool instance, dict with string name, dict with dict tool
- list_tools:    with namespace/type/source filters
- attach_validation: invalid mode raises ValidationError

All step names are prefixed with "trs branch" to avoid collisions with the
existing ``tool_registry_service_coverage_steps.py``.
"""

from __future__ import annotations

from typing import Any

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

from cleveragents.application.services.tool_registry_service import (
    ToolRegistryService,
)
from cleveragents.core.exceptions import ValidationError
from cleveragents.domain.models.core.tool import Tool, ToolSource, ToolType

# ---------------------------------------------------------------------------
# Mock repos
# ---------------------------------------------------------------------------


class _NoCreateRepo:
    """Tool repo where ``create`` is a non-callable attribute, but ``add`` works.

    ``getattr(repo, "create", None)`` returns a truthy, non-callable value,
    so ``callable(create_fn)`` is False → the service falls to the ``add`` path.
    """

    def __init__(self) -> None:
        self.create: str = "not-a-function"  # non-callable attribute
        self.add_called = False
        self._last_added: Any = None

    def add(self, tool: Any) -> Any:
        self.add_called = True
        self._last_added = tool
        return tool

    # Required by other service methods
    def get_by_name(self, name: str) -> Any:
        return None

    def update(self, tool: Any) -> Any:
        return tool

    def delete(self, name: str) -> bool:
        return True

    def list_all(
        self,
        namespace: str | None = None,
        tool_type: str | None = None,
        source: str | None = None,
    ) -> list[Any]:
        return []


class _NeitherCreateNorAddRepo:
    """Tool repo where both ``create`` and ``add`` are non-callable strings.

    Forces the service to fall through to ``self._tool_repo.create(tool)``
    (line 57), which will raise ``TypeError`` because ``create`` is a string.
    """

    def __init__(self) -> None:
        self.create: str = "not-a-function"  # type: ignore[assignment]
        self.add: str = "also-not-a-function"  # type: ignore[assignment]

    def get_by_name(self, name: str) -> Any:
        return None

    def update(self, tool: Any) -> Any:
        return tool

    def delete(self, name: str) -> bool:
        return True

    def list_all(
        self,
        namespace: str | None = None,
        tool_type: str | None = None,
        source: str | None = None,
    ) -> list[Any]:
        return []


class _NoDeleteRepo:
    """Tool repo where ``delete`` is a non-callable attribute, but ``remove`` works."""

    def __init__(self) -> None:
        self.delete: str = "not-a-function"  # type: ignore[assignment]
        self.remove_called = False

    def remove(self, name: str) -> bool:
        self.remove_called = True
        return True

    def create(self, tool: Any) -> Any:
        return tool

    def get_by_name(self, name: str) -> Any:
        return None

    def update(self, tool: Any) -> Any:
        return tool

    def list_all(
        self,
        namespace: str | None = None,
        tool_type: str | None = None,
        source: str | None = None,
    ) -> list[Any]:
        return []


class _NeitherDeleteNorRemoveRepo:
    """Tool repo where both ``delete`` and ``remove`` are non-callable strings."""

    def __init__(self) -> None:
        self.delete: str = "not-a-function"  # type: ignore[assignment]
        self.remove: str = "also-not-a-function"  # type: ignore[assignment]

    def create(self, tool: Any) -> Any:
        return tool

    def get_by_name(self, name: str) -> Any:
        return None

    def update(self, tool: Any) -> Any:
        return tool

    def list_all(
        self,
        namespace: str | None = None,
        tool_type: str | None = None,
        source: str | None = None,
    ) -> list[Any]:
        return []


class _StandardToolRepo:
    """Normal tool repo with all methods callable — used for update/list/attach tests."""

    def __init__(self) -> None:
        self._last_updated: Any = None
        self._list_all_kwargs: dict[str, Any] = {}

    def create(self, tool: Any) -> Any:
        return tool

    def get_by_name(self, name: str) -> Any:
        return {"name": name}

    def update(self, tool: Any) -> Any:
        self._last_updated = tool
        return tool

    def delete(self, name: str) -> bool:
        return True

    def list_all(
        self,
        namespace: str | None = None,
        tool_type: str | None = None,
        source: str | None = None,
    ) -> list[Any]:
        self._list_all_kwargs = {
            "namespace": namespace,
            "tool_type": tool_type,
            "source": source,
        }
        return [{"name": "local/listed-tool"}]


class _MockAttachmentRepo:
    """Minimal attachment repo mock."""

    def attach(self, **kwargs: Any) -> dict[str, str]:
        return {"attachment_id": "branch-test-attachment-001"}

    def detach(self, attachment_id: str) -> bool:
        return True

    def list_for_resource(
        self,
        resource_id: str,
        project_name: str | None = None,
        plan_id: str | None = None,
    ) -> list[Any]:
        return []


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

_SAMPLE_TOOL = {
    "name": "local/branch-test-tool",
    "tool_type": "tool",
    "source": "builtin",
}


def _make_tool_instance() -> Tool:
    """Create a real Tool domain object for tests."""
    return Tool(
        name="local/branch-tool",
        description="A tool for branch tests",
        source=ToolSource.BUILTIN,
        tool_type=ToolType.TOOL,
    )


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


@given("a trs branch mock-based tool registry service")
def step_trs_branch_background(context: Context) -> None:
    """Set up default context variables — repos are assigned per-scenario."""
    context.trs_b_tool_repo: Any = None
    context.trs_b_attachment_repo = _MockAttachmentRepo()
    context.trs_b_service: ToolRegistryService | None = None
    context.trs_b_error: Exception | None = None
    context.trs_b_result: Any = None


# ---------------------------------------------------------------------------
# Given: repo variants
# ---------------------------------------------------------------------------


@given("a trs branch tool repo where create is not callable but add is callable")
def step_trs_branch_no_create_repo(context: Context) -> None:
    context.trs_b_tool_repo = _NoCreateRepo()
    context.trs_b_service = ToolRegistryService(
        tool_repo=context.trs_b_tool_repo,
        attachment_repo=context.trs_b_attachment_repo,
    )


@given("a trs branch tool repo where neither create nor add is callable")
def step_trs_branch_neither_create_nor_add(context: Context) -> None:
    context.trs_b_tool_repo = _NeitherCreateNorAddRepo()
    context.trs_b_service = ToolRegistryService(
        tool_repo=context.trs_b_tool_repo,
        attachment_repo=context.trs_b_attachment_repo,
    )


@given("a trs branch tool repo where delete is not callable but remove is callable")
def step_trs_branch_no_delete_repo(context: Context) -> None:
    context.trs_b_tool_repo = _NoDeleteRepo()
    context.trs_b_service = ToolRegistryService(
        tool_repo=context.trs_b_tool_repo,
        attachment_repo=context.trs_b_attachment_repo,
    )


@given("a trs branch tool repo where neither delete nor remove is callable")
def step_trs_branch_neither_delete_nor_remove(context: Context) -> None:
    context.trs_b_tool_repo = _NeitherDeleteNorRemoveRepo()
    context.trs_b_service = ToolRegistryService(
        tool_repo=context.trs_b_tool_repo,
        attachment_repo=context.trs_b_attachment_repo,
    )


@given("a trs branch standard tool repo")
def step_trs_branch_standard_repo(context: Context) -> None:
    context.trs_b_tool_repo = _StandardToolRepo()
    context.trs_b_service = ToolRegistryService(
        tool_repo=context.trs_b_tool_repo,
        attachment_repo=context.trs_b_attachment_repo,
    )


# ---------------------------------------------------------------------------
# When: register_tool
# ---------------------------------------------------------------------------


@when("I trs branch register a tool")
def step_trs_branch_register(context: Context) -> None:
    try:
        context.trs_b_result = context.trs_b_service.register_tool(_SAMPLE_TOOL)
        context.trs_b_error = None
    except Exception as exc:
        context.trs_b_error = exc


@when("I trs branch register a tool expecting an error")
def step_trs_branch_register_error(context: Context) -> None:
    try:
        context.trs_b_result = context.trs_b_service.register_tool(_SAMPLE_TOOL)
        context.trs_b_error = None
    except Exception as exc:
        context.trs_b_error = exc


# ---------------------------------------------------------------------------
# When: remove_tool
# ---------------------------------------------------------------------------


@when('I trs branch remove tool "{name}"')
def step_trs_branch_remove(context: Context, name: str) -> None:
    try:
        context.trs_b_result = context.trs_b_service.remove_tool(name)
        context.trs_b_error = None
    except Exception as exc:
        context.trs_b_error = exc


@when('I trs branch remove tool "{name}" expecting an error')
def step_trs_branch_remove_error(context: Context, name: str) -> None:
    try:
        context.trs_b_result = context.trs_b_service.remove_tool(name)
        context.trs_b_error = None
    except Exception as exc:
        context.trs_b_error = exc


# ---------------------------------------------------------------------------
# When: update_tool with tool_config
# ---------------------------------------------------------------------------


@when("I trs branch update tool with a Tool instance as tool_config")
def step_trs_branch_update_tool_instance(context: Context) -> None:
    tool_instance = _make_tool_instance()
    context.trs_b_tool_instance = tool_instance
    try:
        context.trs_b_result = context.trs_b_service.update_tool(
            tool="local/branch-tool",
            tool_config=tool_instance,
        )
        context.trs_b_error = None
    except Exception as exc:
        context.trs_b_error = exc


@when("I trs branch update tool with a string name and dict tool_config")
def step_trs_branch_update_string_dict(context: Context) -> None:
    try:
        context.trs_b_result = context.trs_b_service.update_tool(
            tool="local/updated-tool",
            tool_config={"description": "updated desc", "timeout": 60},
        )
        context.trs_b_error = None
    except Exception as exc:
        context.trs_b_error = exc


@when("I trs branch update tool with a dict tool and dict tool_config")
def step_trs_branch_update_dict_dict(context: Context) -> None:
    tool_dict = {"name": "local/dict-tool", "description": "orig"}
    try:
        context.trs_b_result = context.trs_b_service.update_tool(
            tool=tool_dict,
            tool_config={"description": "overridden"},
        )
        context.trs_b_error = None
    except Exception as exc:
        context.trs_b_error = exc


# ---------------------------------------------------------------------------
# When: list_tools with filters
# ---------------------------------------------------------------------------


@when('I trs branch list tools with namespace "{ns}" type "{tt}" source "{src}"')
def step_trs_branch_list_tools(context: Context, ns: str, tt: str, src: str) -> None:
    try:
        context.trs_b_result = context.trs_b_service.list_tools(
            namespace=ns, tool_type=tt, source=src
        )
        context.trs_b_error = None
    except Exception as exc:
        context.trs_b_error = exc


# ---------------------------------------------------------------------------
# When: attach_validation with invalid mode
# ---------------------------------------------------------------------------


@when('I trs branch attach validation with mode "{mode}"')
def step_trs_branch_attach_invalid_mode(context: Context, mode: str) -> None:
    try:
        context.trs_b_result = context.trs_b_service.attach_validation(
            validation_name="local/some-validation",
            resource_id="res-001",
        )
        context.trs_b_error = None
    except Exception as exc:
        context.trs_b_error = exc


# ---------------------------------------------------------------------------
# Then: register_tool assertions
# ---------------------------------------------------------------------------


@then("the trs branch result should equal the registered tool")
def step_trs_branch_result_equals_tool(context: Context) -> None:
    assert context.trs_b_error is None, (
        f"Expected no error, got {type(context.trs_b_error).__name__}: "
        f"{context.trs_b_error}"
    )
    assert context.trs_b_result == _SAMPLE_TOOL


@then("the trs branch add method should have been called")
def step_trs_branch_add_called(context: Context) -> None:
    assert hasattr(context.trs_b_tool_repo, "add_called"), (
        "Repo does not track add_called"
    )
    assert context.trs_b_tool_repo.add_called is True, "add() was not called"


@then("a trs branch TypeError should be raised")
def step_trs_branch_type_error(context: Context) -> None:
    assert context.trs_b_error is not None, "Expected TypeError but no error was raised"
    assert isinstance(context.trs_b_error, TypeError), (
        f"Expected TypeError, got {type(context.trs_b_error).__name__}: "
        f"{context.trs_b_error}"
    )


# ---------------------------------------------------------------------------
# Then: remove_tool assertions
# ---------------------------------------------------------------------------


@then("the trs branch removal result should be True")
def step_trs_branch_removal_true(context: Context) -> None:
    assert context.trs_b_error is None, (
        f"Expected no error, got {type(context.trs_b_error).__name__}: "
        f"{context.trs_b_error}"
    )
    assert context.trs_b_result is True, f"Expected True, got {context.trs_b_result}"


@then("the trs branch remove method should have been called")
def step_trs_branch_remove_called(context: Context) -> None:
    assert hasattr(context.trs_b_tool_repo, "remove_called"), (
        "Repo does not track remove_called"
    )
    assert context.trs_b_tool_repo.remove_called is True, "remove() was not called"


# ---------------------------------------------------------------------------
# Then: update_tool assertions
# ---------------------------------------------------------------------------


@then("the trs branch updated result should be the Tool instance")
def step_trs_branch_updated_is_tool(context: Context) -> None:
    assert context.trs_b_error is None, (
        f"Expected no error, got {type(context.trs_b_error).__name__}: "
        f"{context.trs_b_error}"
    )
    assert isinstance(context.trs_b_result, Tool), (
        f"Expected Tool instance, got {type(context.trs_b_result).__name__}"
    )
    assert context.trs_b_result is context.trs_b_tool_instance, (
        "Expected the exact Tool instance passed as tool_config"
    )


@then("the trs branch updated result should contain the merged name and config")
def step_trs_branch_updated_merged(context: Context) -> None:
    assert context.trs_b_error is None, (
        f"Expected no error, got {type(context.trs_b_error).__name__}: "
        f"{context.trs_b_error}"
    )
    result = context.trs_b_result
    assert isinstance(result, dict), f"Expected dict, got {type(result).__name__}"
    assert result["name"] == "local/updated-tool", (
        f"Expected name 'local/updated-tool', got '{result.get('name')}'"
    )
    assert result["description"] == "updated desc", (
        f"Expected description 'updated desc', got '{result.get('description')}'"
    )
    assert result["timeout"] == 60, f"Expected timeout 60, got {result.get('timeout')}"


@then("the trs branch updated result should be the original tool dict")
def step_trs_branch_updated_original(context: Context) -> None:
    assert context.trs_b_error is None, (
        f"Expected no error, got {type(context.trs_b_error).__name__}: "
        f"{context.trs_b_error}"
    )
    result = context.trs_b_result
    assert isinstance(result, dict), f"Expected dict, got {type(result).__name__}"
    # When tool is not a string, the original tool dict is used directly
    assert result["name"] == "local/dict-tool", (
        f"Expected name 'local/dict-tool', got '{result.get('name')}'"
    )


# ---------------------------------------------------------------------------
# Then: list_tools assertions
# ---------------------------------------------------------------------------


@then("the trs branch list result should come from the repo")
def step_trs_branch_list_result(context: Context) -> None:
    assert context.trs_b_error is None, (
        f"Expected no error, got {type(context.trs_b_error).__name__}: "
        f"{context.trs_b_error}"
    )
    assert isinstance(context.trs_b_result, list), (
        f"Expected list, got {type(context.trs_b_result).__name__}"
    )
    assert len(context.trs_b_result) == 1
    assert context.trs_b_result[0]["name"] == "local/listed-tool"
    # Verify filters were passed through
    kwargs = context.trs_b_tool_repo._list_all_kwargs
    assert kwargs["namespace"] == "local"
    assert kwargs["tool_type"] == "tool"
    assert kwargs["source"] == "builtin"


# ---------------------------------------------------------------------------
# Then: attach_validation assertions
# ---------------------------------------------------------------------------


@then('a trs branch ValidationError should be raised with message containing "{text}"')
def step_trs_branch_validation_error(context: Context, text: str) -> None:
    assert context.trs_b_error is not None, (
        "Expected ValidationError but no error was raised"
    )
    assert isinstance(context.trs_b_error, ValidationError), (
        f"Expected ValidationError, got {type(context.trs_b_error).__name__}: "
        f"{context.trs_b_error}"
    )
    assert text in str(context.trs_b_error), (
        f"Expected '{text}' in error message, got '{context.trs_b_error}'"
    )
