"""Step definitions for Tool Registry persistence coverage.

Covers tools, tool_resource_bindings, validation_attachments tables
and the ToolRegistryService integration layer.
"""

from __future__ import annotations

from datetime import datetime

from behave import given, then, when
from behave.runner import Context
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker

from cleveragents.application.services.tool_registry_service import (
    ToolRegistryService,
)
from cleveragents.infrastructure.database.models import Base
from cleveragents.infrastructure.database.repositories import (
    DuplicateToolError,
    ToolInUseError,
    ToolRegistryRepository,
    ValidationAttachmentRepository,
)

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


def _make_tool_dict(
    name: str = "core/test-tool",
    source: str = "builtin",
    tool_type: str = "tool",
    description: str | None = None,
    code: str | None = None,
    mcp_server: str | None = None,
    mcp_tool_name: str | None = None,
    mode: str | None = None,
    resource_bindings: list[dict[str, object]] | None = None,
) -> dict[str, object]:
    """Create a minimal valid tool dict for testing."""
    now_iso = datetime.now().isoformat()
    result: dict[str, object] = {
        "name": name,
        "description": description or f"Test tool {name}",
        "tool_type": tool_type,
        "source": source,
        "timeout": 300,
        "created_at": now_iso,
        "updated_at": now_iso,
        "resource_bindings": resource_bindings or [],
    }
    if code is not None:
        result["code"] = code
    if mcp_server is not None:
        result["mcp_server"] = mcp_server
    if mcp_tool_name is not None:
        result["mcp_tool_name"] = mcp_tool_name
    if mode is not None:
        result["mode"] = mode
    return result


def _make_binding(slot_name: str = "repo") -> dict[str, object]:
    """Create a resource binding dict."""
    return {
        "slot_name": slot_name,
        "resource_type": "git-checkout",
        "access_mode": "read_only",
        "binding_mode": "contextual",
        "required": True,
    }


def _get_session_factory(context: Context) -> sessionmaker[Session]:
    return context.tool_session_factory  # type: ignore[no-any-return]


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


@given("a clean in-memory database with the tool registry schema")
def step_clean_tool_db(context: Context) -> None:
    """Set up an in-memory SQLite database with all tables."""
    engine = create_engine("sqlite:///:memory:", echo=False)
    Base.metadata.create_all(engine)
    factory: sessionmaker[Session] = sessionmaker(bind=engine, expire_on_commit=False)
    context.tool_engine = engine
    context.tool_session_factory = factory
    # Keep a persistent session for attachment repo (needs same connection)
    context.tool_session = factory()


@given("a tool registry repository backed by the database")
def step_tool_repo(context: Context) -> None:
    """Create a ToolRegistryRepository."""
    context.tool_repo = ToolRegistryRepository(
        session_factory=lambda: context.tool_session,
    )


@given("a validation attachment repository backed by the database")
def step_attachment_repo(context: Context) -> None:
    """Create a ValidationAttachmentRepository."""
    context.attachment_repo = ValidationAttachmentRepository(
        session_factory=lambda: context.tool_session,
    )


# ---------------------------------------------------------------------------
# Given: tool dicts
# ---------------------------------------------------------------------------


@given('a valid tool dict named "{name}" with source "{source}"')
def step_valid_tool_dict(context: Context, name: str, source: str) -> None:
    """Build a tool dict."""
    context.tool_dict = _make_tool_dict(name=name, source=source)


@given('the tool dict includes inline code "{code}"')
def step_tool_dict_code(context: Context, code: str) -> None:
    context.tool_dict["code"] = code


@given('the tool dict includes mcp_server "{server}" and mcp_tool_name "{tool_name}"')
def step_tool_dict_mcp(context: Context, server: str, tool_name: str) -> None:
    context.tool_dict["mcp_server"] = server
    context.tool_dict["mcp_tool_name"] = tool_name


@given('the tool dict has tool_type "{tool_type}" and mode "{mode}"')
def step_tool_dict_type_mode(context: Context, tool_type: str, mode: str) -> None:
    context.tool_dict["tool_type"] = tool_type
    context.tool_dict["mode"] = mode


@given('the tool dict includes a resource binding for slot "{slot_name}"')
def step_tool_dict_binding(context: Context, slot_name: str) -> None:
    bindings = list(context.tool_dict.get("resource_bindings", []))  # type: ignore[union-attr]
    bindings.append(_make_binding(slot_name))
    context.tool_dict["resource_bindings"] = bindings


@given("the tool has already been registered once in the tool registry")
def step_tool_already_registered(context: Context) -> None:
    context.tool_repo.create(context.tool_dict)
    context.tool_session.commit()


@given("the tool has been registered through the tool registry repository")
def step_tool_registered(context: Context) -> None:
    context.tool_repo.create(context.tool_dict)
    context.tool_session.commit()


@given("the following tools have been registered:")
def step_register_multiple_tools(context: Context) -> None:
    for row in context.table:
        tool_dict = _make_tool_dict(
            name=row["name"],
            source=row["source"],
            tool_type=row["tool_type"],
        )
        context.tool_repo.create(tool_dict)
    context.tool_session.commit()


@given('an unscoped validation attachment for "{val_name}" on resource "{res_id}"')
def step_attachment_exists(context: Context, val_name: str, res_id: str) -> None:
    att = context.attachment_repo.attach(
        validation_name=val_name,
        resource_id=res_id,
        mode="required",
    )
    context.tool_session.commit()
    context.last_attachment = att


@given(
    'a project-scoped validation attachment for "{val_name}" on resource "{res_id}" in project "{project}"'
)
def step_attachment_in_project(
    context: Context, val_name: str, res_id: str, project: str
) -> None:
    att = context.attachment_repo.attach(
        validation_name=val_name,
        resource_id=res_id,
        mode="required",
        project_name=project,
    )
    context.tool_session.commit()
    context.last_attachment = att


@given(
    'a plan-scoped validation attachment for "{val_name}" on resource "{res_id}" in plan "{plan_id}"'
)
def step_attachment_with_plan(
    context: Context, val_name: str, res_id: str, plan_id: str
) -> None:
    att = context.attachment_repo.attach(
        validation_name=val_name,
        resource_id=res_id,
        mode="required",
        plan_id=plan_id,
    )
    context.tool_session.commit()
    context.last_attachment = att


# ---------------------------------------------------------------------------
# When: registration
# ---------------------------------------------------------------------------


@when("the tool is registered through the tool registry repository")
def step_register_tool(context: Context) -> None:
    context.last_error = None
    try:
        context.tool_repo.create(context.tool_dict)
        context.tool_session.commit()
    except Exception as exc:
        context.last_error = exc


@when('a second tool with the same registry name "{name}" is registered')
def step_register_duplicate_tool(context: Context, name: str) -> None:
    dup = _make_tool_dict(name=name, source="builtin")
    context.last_error = None
    try:
        context.tool_repo.create(dup)
        context.tool_session.commit()
    except Exception as exc:
        context.last_error = exc


# ---------------------------------------------------------------------------
# When: retrieval
# ---------------------------------------------------------------------------


@when('the tool is looked up by registry name "{name}"')
def step_lookup_tool(context: Context, name: str) -> None:
    context.returned_tool = context.tool_repo.get_by_name(name)


# ---------------------------------------------------------------------------
# When: listing
# ---------------------------------------------------------------------------


@when("all tools are listed from the tool registry")
def step_list_all_tools(context: Context) -> None:
    context.returned_tools = context.tool_repo.list_all()


@when('tools in the "{ns}" namespace are listed from the registry')
def step_list_tools_ns(context: Context, ns: str) -> None:
    context.returned_tools = context.tool_repo.list_all(namespace=ns)


@when('tools of type "{tool_type}" are listed from the registry')
def step_list_tools_type(context: Context, tool_type: str) -> None:
    context.returned_tools = context.tool_repo.list_all(tool_type=tool_type)


@when('tools with source "{source}" are listed from the registry')
def step_list_tools_source(context: Context, source: str) -> None:
    context.returned_tools = context.tool_repo.list_all(source=source)


# ---------------------------------------------------------------------------
# When: update
# ---------------------------------------------------------------------------


@when('the tool description is updated to "{desc}"')
def step_update_tool_desc(context: Context, desc: str) -> None:
    context.tool_dict["description"] = desc
    context.tool_repo.update(context.tool_dict)
    context.tool_session.commit()


@when('the tool is updated with a new resource binding for slot "{slot_name}"')
def step_update_tool_binding(context: Context, slot_name: str) -> None:
    context.tool_dict["resource_bindings"] = [_make_binding(slot_name)]
    context.tool_repo.update(context.tool_dict)
    context.tool_session.commit()


# ---------------------------------------------------------------------------
# When: deletion
# ---------------------------------------------------------------------------


@when('the tool "{name}" is removed from the registry')
def step_remove_tool(context: Context, name: str) -> None:
    context.last_error = None
    try:
        context.removal_result = context.tool_repo.delete(name)
    except Exception as exc:
        context.last_error = exc


# ---------------------------------------------------------------------------
# When: attachments
# ---------------------------------------------------------------------------


@when('the validation "{val_name}" is attached to resource "{res_id}"')
def step_attach_validation(context: Context, val_name: str, res_id: str) -> None:
    context.last_attachment = context.attachment_repo.attach(
        validation_name=val_name,
        resource_id=res_id,
        mode="required",
    )
    context.tool_session.commit()


@when("the validation attachment is detached by its identifier")
def step_detach_by_id(context: Context) -> None:
    att_id = context.last_attachment["attachment_id"]
    context.detach_result = context.attachment_repo.detach(att_id)
    context.tool_session.commit()


@when('the validation attachment "{att_id}" is detached')
def step_detach_nonexistent(context: Context, att_id: str) -> None:
    context.detach_result = context.attachment_repo.detach(att_id)


@when('all attachments for resource "{res_id}" are listed')
def step_list_attachments(context: Context, res_id: str) -> None:
    context.returned_attachments = context.attachment_repo.list_for_resource(
        resource_id=res_id
    )


@when(
    'project-filtered attachments for resource "{res_id}" in project "{project}" are listed'
)
def step_list_attachments_project(context: Context, res_id: str, project: str) -> None:
    context.returned_attachments = context.attachment_repo.list_for_resource(
        resource_id=res_id,
        project_name=project,
    )


@when(
    'plan-filtered attachments for resource "{res_id}" with plan "{plan_id}" are listed'
)
def step_list_attachments_plan(context: Context, res_id: str, plan_id: str) -> None:
    context.returned_attachments = context.attachment_repo.list_for_resource(
        resource_id=res_id,
        plan_id=plan_id,
    )


@when("the attachment is retrieved by its stored identifier")
def step_get_attachment_by_id(context: Context) -> None:
    att_id = context.last_attachment["attachment_id"]
    context.returned_attachment = context.attachment_repo.get_by_id(att_id)


# ---------------------------------------------------------------------------
# When: service layer
# ---------------------------------------------------------------------------


@given("a tool registry service backed by the repositories")
def step_tool_service(context: Context) -> None:
    context.tool_service = ToolRegistryService(
        tool_repo=context.tool_repo,
        attachment_repo=context.attachment_repo,
    )


@when("the tool is registered through the service")
def step_service_register(context: Context) -> None:
    context.tool_service.register_tool(context.tool_dict)
    context.tool_session.commit()


@given("the tool has been registered through the service")
def step_service_given_registered(context: Context) -> None:
    context.tool_service.register_tool(context.tool_dict)
    context.tool_session.commit()


@when('the tool is retrieved from the service by name "{name}"')
def step_service_get(context: Context, name: str) -> None:
    context.service_returned_tool = context.tool_service.get_tool(name)


@when('the service attaches validation "{val_name}" to resource "{res_id}"')
def step_service_attach(context: Context, val_name: str, res_id: str) -> None:
    context.service_attachment = context.tool_service.attach_validation(
        validation_name=val_name,
        resource_id=res_id,
    )
    context.tool_session.commit()


@when("the service detaches the validation attachment")
def step_service_detach(context: Context) -> None:
    att_id = context.service_attachment["attachment_id"]
    context.service_detach_result = context.tool_service.detach_validation(att_id)
    context.tool_session.commit()


@given('the service has attached validation "{val_name}" to resource "{res_id}"')
def step_service_given_attached(context: Context, val_name: str, res_id: str) -> None:
    context.service_attachment = context.tool_service.attach_validation(
        validation_name=val_name,
        resource_id=res_id,
    )
    context.tool_session.commit()


@when('the service lists validations for resource "{res_id}"')
def step_service_list_validations(context: Context, res_id: str) -> None:
    context.service_returned_attachments = (
        context.tool_service.list_validations_for_resource(
            resource_id=res_id,
        )
    )


# ---------------------------------------------------------------------------
# Then assertions
# ---------------------------------------------------------------------------


@then("the tool registry repository should not raise an error")
def step_no_tool_error(context: Context) -> None:
    assert context.last_error is None, f"Unexpected error: {context.last_error}"


@then('the persisted tool should have the registry name "{name}"')
def step_tool_name(context: Context, name: str) -> None:
    tool = context.tool_repo.get_by_name(name)
    assert tool is not None, f"Tool {name} not found"
    assert tool["name"] == name


@then('the persisted tool should have source "{source}"')
def step_tool_source(context: Context, source: str) -> None:
    name = context.tool_dict["name"]
    tool = context.tool_repo.get_by_name(name)
    assert tool is not None
    assert tool["source"] == source


@then('the persisted tool should have tool_type "{tool_type}"')
def step_tool_type(context: Context, tool_type: str) -> None:
    name = context.tool_dict["name"]
    tool = context.tool_repo.get_by_name(name)
    assert tool is not None
    assert tool["tool_type"] == tool_type


@then("the persisted tool should have {count:d} resource binding")
def step_tool_binding_count(context: Context, count: int) -> None:
    name = context.tool_dict["name"]
    tool = context.tool_repo.get_by_name(name)
    assert tool is not None
    assert len(tool.get("resource_bindings", [])) == count


@then('a DuplicateToolError should be raised mentioning "{name}"')
def step_dup_tool_error(context: Context, name: str) -> None:
    assert isinstance(context.last_error, DuplicateToolError), (
        f"Expected DuplicateToolError, got {type(context.last_error)}"
    )
    assert name in str(context.last_error)


@then('the returned tool should have the registry name "{name}"')
def step_returned_tool_name(context: Context, name: str) -> None:
    assert context.returned_tool is not None
    assert context.returned_tool["name"] == name


@then("no tool should be returned from the registry")
def step_no_tool_returned(context: Context) -> None:
    assert context.returned_tool is None


@then("{count:d} tools should be returned from the registry")
def step_tool_count(context: Context, count: int) -> None:
    assert len(context.returned_tools) == count


@then('the returned tool names from the registry should include "{name}"')
def step_tool_names_include(context: Context, name: str) -> None:
    names = [t["name"] for t in context.returned_tools]
    assert name in names


@then('the persisted tool should have description "{desc}"')
def step_tool_desc(context: Context, desc: str) -> None:
    name = context.tool_dict["name"]
    tool = context.tool_repo.get_by_name(name)
    assert tool is not None
    assert tool["description"] == desc


@then('the persisted tool binding slot should be "{slot_name}"')
def step_tool_binding_slot(context: Context, slot_name: str) -> None:
    name = context.tool_dict["name"]
    tool = context.tool_repo.get_by_name(name)
    assert tool is not None
    bindings = tool.get("resource_bindings", [])
    assert len(bindings) > 0
    assert bindings[0]["slot_name"] == slot_name


@then("the removal should return true from the registry")
def step_removal_true(context: Context) -> None:
    assert context.last_error is None
    assert context.removal_result is True


@then("the removal should return false from the registry")
def step_removal_false(context: Context) -> None:
    assert context.last_error is None
    assert context.removal_result is False


@then("the tool should no longer exist in the registry")
def step_tool_not_found(context: Context) -> None:
    name = context.tool_dict["name"]
    tool = context.tool_repo.get_by_name(name)
    assert tool is None


@then('a ToolInUseError should be raised mentioning "{name}"')
def step_tool_in_use_error(context: Context, name: str) -> None:
    assert isinstance(context.last_error, ToolInUseError), (
        f"Expected ToolInUseError, got {type(context.last_error)}"
    )
    assert name in str(context.last_error)


@then("the attachment should have a valid ULID identifier")
def step_attachment_ulid(context: Context) -> None:
    att_id = context.last_attachment["attachment_id"]
    assert len(att_id) == 26


@then('the attachment should reference resource "{res_id}"')
def step_attachment_resource(context: Context, res_id: str) -> None:
    assert context.last_attachment["resource_id"] == res_id


@then("the detachment should return true")
def step_detach_true(context: Context) -> None:
    assert context.detach_result is True


@then("the non-existent detachment should return false")
def step_detach_false(context: Context) -> None:
    assert context.detach_result is False


@then("{count:d} attachment should be returned")
def step_attachment_count(context: Context, count: int) -> None:
    assert len(context.returned_attachments) == count


@then('the retrieved attachment should reference validation "{val_name}"')
def step_retrieved_att_val(context: Context, val_name: str) -> None:
    assert context.returned_attachment is not None
    assert context.returned_attachment["validation_name"] == val_name


@then('the service-returned tool should have the registry name "{name}"')
def step_service_tool_name(context: Context, name: str) -> None:
    assert context.service_returned_tool is not None
    assert context.service_returned_tool["name"] == name


@then("the service detachment should return true")
def step_service_detach_result(context: Context) -> None:
    assert context.service_detach_result is True


@then("the service should return {count:d} validation attachment")
def step_service_attachment_count(context: Context, count: int) -> None:
    assert len(context.service_returned_attachments) == count
