"""Step definitions for repositories_remaining_branches_coverage.feature.

Targets the last uncovered lines and branches in repositories.py:
- ToolRepository.add: DuplicateToolError re-raise (L3541-3542),
  DatabaseError wrapping (L3542)
- ToolRepository.get_by_name: success path (L3558)
- ResourceRepository.link_child: parent_type_row absent (branch 2298->2310)
- ResourceRepository.resolve_namespaced_name: ULID fallback (branch 2711->2714)
- ResourceRepository.get_children: populated list (branch 2426->2420)
- ResourceRepository.get_parents: populated list (branch 2459->2453)
- ResourceRepository._get_ancestors + _build_cycle_path (branches 2635->2633, 2661->2659)
- NamespacedProjectRepository.update: success path (branch 2928->2931)
- AutomationProfileRepository.upsert: new insert (branch 4193->4200)
- ValidationAttachmentRepository.attach: project_name scope (branch 3633->3634)
- LifecyclePlanRepository.update: multiple project links (branch 1395->1394)
"""

from __future__ import annotations

from datetime import UTC, datetime
from types import SimpleNamespace
from typing import Any

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

from cleveragents.infrastructure.database.models import (
    Base,
    ResourceLinkModel,
    ResourceModel,
    ResourceTypeModel,
)
from cleveragents.infrastructure.database.repositories import (
    CycleDetectedError,
    DuplicateToolError,
    NamespacedProjectRepository,
    ResourceRepository,
    ToolRepository,
    ValidationAttachmentRepository,
)

# ── helpers ─────────────────────────────────────────────────────────────


def _make_engine_and_factory():
    """Create a fresh in-memory SQLite DB with all tables."""
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)
    factory = sessionmaker(bind=engine)
    return engine, factory


def _now_iso() -> str:
    return datetime.now(tz=UTC).isoformat()


def _make_tool_ns(
    name: str = "local/test-tool", tool_type: str = "tool"
) -> SimpleNamespace:
    """Create a tool domain-like SimpleNamespace suitable for ToolRepository.add."""
    return SimpleNamespace(
        name=name,
        description=f"Tool {name}",
        tool_type=tool_type,
        source="builtin",
        input_schema=None,
        output_schema=None,
        capability=None,
        resource_slots=None,
        lifecycle_json=None,
        code=None,
        mcp_server=None,
        mcp_tool_name=None,
        agent_skill_path=None,
        timeout=300,
        wraps=None,
        transform=None,
        mode=None,
        argument_mapping_json=None,
    )


def _ensure_resource_type_row(factory, type_name: str) -> None:
    """Insert a ResourceTypeModel row directly if absent."""
    session = factory()
    existing = session.query(ResourceTypeModel).filter_by(name=type_name).first()
    if existing:
        return
    now = _now_iso()
    ns = type_name.split("/")[0] if "/" in type_name else "builtin"
    row = ResourceTypeModel(
        name=type_name,
        namespace=ns,
        description="test type",
        resource_kind="physical",
        sandbox_strategy="none",
        user_addable=True,
        handler_ref=None,
        args_schema_json=None,
        allowed_parent_types_json=None,
        allowed_child_types_json=None,
        auto_discover_json=None,
        capabilities_json='{"read": true, "write": true, "sandbox": true, "checkpoint": false}',
        equivalence_json=None,
        source=None,
        created_at=now,
        updated_at=now,
    )
    session.add(row)
    session.commit()


def _create_resource_row(
    factory,
    resource_id: str,
    type_name: str = "test/default-type",
    name: str | None = None,
    namespace: str | None = None,
) -> str:
    """Insert a ResourceModel row directly and return the resource_id."""
    session = factory()
    now = _now_iso()
    row = ResourceModel(
        resource_id=resource_id,
        namespaced_name=name,
        namespace=namespace,
        type_name=type_name,
        resource_kind="physical",
        location=None,
        description="test resource",
        read_only=False,
        auto_discovered=False,
        sandbox_strategy=None,
        content_hash=None,
        properties_json=None,
        metadata_json=None,
        created_at=now,
        updated_at=now,
    )
    session.add(row)
    session.commit()
    return resource_id


def _create_link(factory, parent_id: str, child_id: str) -> None:
    """Insert a ResourceLinkModel row directly."""
    session = factory()
    now = _now_iso()
    session.add(
        ResourceLinkModel(parent_id=parent_id, child_id=child_id, created_at=now)
    )
    session.commit()


# ═══════════════════════════════════════════════════════════════════════
# ToolRepository.add: DuplicateToolError re-raise (lines 3541-3542)
# ═══════════════════════════════════════════════════════════════════════


@given("remaining cov an in-memory database with tool tables")
def step_remaining_cov_tool_db(context: Context) -> None:
    engine, factory = _make_engine_and_factory()
    context.rbc_engine = engine
    context.rbc_session_factory = factory
    context.rbc_tool_repo = ToolRepository(session_factory=factory)
    context.rbc_error = None


@given('remaining cov a tool "{name}" has been added')
def step_remaining_cov_tool_added(context: Context, name: str) -> None:
    context.rbc_tool_repo.add(_make_tool_ns(name))


@when('remaining cov the same tool "{name}" is added again')
def step_remaining_cov_tool_add_dup(context: Context, name: str) -> None:
    # Mock create() to raise DuplicateToolError directly.
    # This is necessary because the real create() is wrapped with
    # @database_retry, which retries DatabaseError subclasses (including
    # DuplicateToolError). By mocking create(), we bypass the retry
    # decorator and directly exercise the ``except DuplicateToolError: raise``
    # path in add() (lines 3540-3541).
    from unittest.mock import patch

    dup_error = DuplicateToolError(name)
    with patch.object(context.rbc_tool_repo, "create", side_effect=dup_error):
        try:
            context.rbc_tool_repo.add(_make_tool_ns(name))
            context.rbc_error = None
        except Exception as exc:
            context.rbc_error = exc


@then("remaining cov a DuplicateToolError should be raised for the duplicate")
def step_remaining_cov_assert_dup_tool(context: Context) -> None:
    assert isinstance(context.rbc_error, DuplicateToolError), (
        f"Expected DuplicateToolError, got {type(context.rbc_error).__name__}: "
        f"{context.rbc_error}"
    )


# ═══════════════════════════════════════════════════════════════════════
# ToolRepository.add: DatabaseError wrapping (line 3542)
# ═══════════════════════════════════════════════════════════════════════


@given(
    "remaining cov a ToolRepository with a session that raises DatabaseError on create"
)
def step_remaining_cov_tool_repo_db_error(context: Context) -> None:
    from cleveragents.core.exceptions import DatabaseError as CoreDatabaseError

    _engine, factory = _make_engine_and_factory()

    # Create a subclass that overrides create() to raise a DatabaseError
    # (not DuplicateToolError) to exercise line 3542-3543
    class _FailingToolRepo(ToolRepository):
        def create(self, tool: Any) -> Any:
            raise CoreDatabaseError("Simulated create failure")

    context.rbc_tool_repo = _FailingToolRepo(session_factory=factory)
    context.rbc_error = None


@when("remaining cov a tool is added and a DatabaseError is expected")
def step_remaining_cov_tool_add_db_error(context: Context) -> None:
    from cleveragents.core.exceptions import DatabaseError as CoreDatabaseError

    try:
        context.rbc_tool_repo.add(_make_tool_ns("local/db-error-tool"))
        context.rbc_error = None
    except CoreDatabaseError as exc:
        context.rbc_error = exc
    except Exception as exc:
        context.rbc_error = exc


@then('remaining cov a DatabaseError mentioning "Failed to add tool" should be raised')
def step_remaining_cov_assert_db_error(context: Context) -> None:
    from cleveragents.core.exceptions import DatabaseError as CoreDatabaseError

    assert context.rbc_error is not None, "Expected a DatabaseError"
    assert isinstance(context.rbc_error, CoreDatabaseError), (
        f"Expected DatabaseError, got {type(context.rbc_error).__name__}: "
        f"{context.rbc_error}"
    )
    assert "Failed to add tool" in str(context.rbc_error), (
        f"Expected 'Failed to add tool' in: {context.rbc_error}"
    )


# ═══════════════════════════════════════════════════════════════════════
# ToolRepository.get_by_name: success returns domain (line 3558)
# ═══════════════════════════════════════════════════════════════════════


@when('remaining cov get_by_name is invoked for "{name}"')
def step_remaining_cov_get_by_name(context: Context, name: str) -> None:
    context.rbc_result = context.rbc_tool_repo.get_by_name(name)


@then('remaining cov get_by_name should return a non-None result with name "{name}"')
def step_remaining_cov_assert_get_by_name(context: Context, name: str) -> None:
    result = context.rbc_result
    assert result is not None, f"Expected non-None result for '{name}'"
    # _to_legacy_domain returns a SimpleNamespace with .name
    actual_name = result.name if hasattr(result, "name") else result.get("name", "")
    assert actual_name == name, f"Expected name '{name}', got '{actual_name}'"


# ═══════════════════════════════════════════════════════════════════════
# ResourceRepository.link_child: parent type row absent (branch 2298→2310)
# ═══════════════════════════════════════════════════════════════════════


@given("remaining cov an in-memory resource database")
def step_remaining_cov_res_db(context: Context) -> None:
    engine, factory = _make_engine_and_factory()
    context.rbc_engine = engine
    context.rbc_factory = factory
    context.rbc_res_repo = ResourceRepository(factory)
    context.rbc_error = None


@given("remaining cov two resources with a type that has no ResourceTypeModel row")
def step_remaining_cov_res_no_type_row(context: Context) -> None:
    # Insert resources directly with a type_name whose ResourceTypeModel
    # does NOT exist. This means parent_type_row will be None, exercising
    # the branch where we skip allowed_children validation.
    session = context.rbc_factory()
    now = _now_iso()
    for rid in ["res-notype-parent", "res-notype-child"]:
        row = ResourceModel(
            resource_id=rid,
            namespaced_name=None,
            namespace=None,
            type_name="ghost-type",
            resource_kind="physical",
            location=None,
            description="test",
            read_only=False,
            auto_discovered=False,
            sandbox_strategy=None,
            content_hash=None,
            properties_json=None,
            metadata_json=None,
            created_at=now,
            updated_at=now,
        )
        session.add(row)
    session.commit()
    context.rbc_link_parent = "res-notype-parent"
    context.rbc_link_child = "res-notype-child"


@when("remaining cov link_child is called for those resources")
def step_remaining_cov_link_child_no_type(context: Context) -> None:
    try:
        context.rbc_res_repo.link_child(context.rbc_link_parent, context.rbc_link_child)
        context.rbc_error = None
    except Exception as exc:
        context.rbc_error = exc


@then("remaining cov the link should be created successfully")
def step_remaining_cov_assert_link_ok(context: Context) -> None:
    assert context.rbc_error is None, f"Unexpected error: {context.rbc_error}"
    session = context.rbc_factory()
    link = (
        session.query(ResourceLinkModel)
        .filter_by(parent_id=context.rbc_link_parent, child_id=context.rbc_link_child)
        .first()
    )
    assert link is not None, "Expected a ResourceLinkModel row"


# ═══════════════════════════════════════════════════════════════════════
# ResourceRepository.resolve_namespaced_name: ULID fallback (branch 2711→2714)
# ═══════════════════════════════════════════════════════════════════════


@given("remaining cov a resource exists with a known ULID but no namespaced name")
def step_remaining_cov_res_ulid_only(context: Context) -> None:
    from ulid import ULID

    rid = str(ULID())
    _ensure_resource_type_row(context.rbc_factory, "test/ulid-type")
    _create_resource_row(
        context.rbc_factory,
        rid,
        type_name="test/ulid-type",
        name=None,
    )
    context.rbc_ulid_resource_id = rid


@when("remaining cov resolve_namespaced_name is called with the ULID")
def step_remaining_cov_resolve_ulid(context: Context) -> None:
    context.rbc_result = context.rbc_res_repo.resolve_namespaced_name(
        context.rbc_ulid_resource_id
    )


@then("remaining cov the resource should be resolved successfully")
def step_remaining_cov_assert_resolved(context: Context) -> None:
    assert context.rbc_result is not None, "Expected resource, got None"
    actual_id = (
        context.rbc_result.resource_id
        if hasattr(context.rbc_result, "resource_id")
        else context.rbc_result.get("resource_id", "")
    )
    assert actual_id == context.rbc_ulid_resource_id, (
        f"Expected ID {context.rbc_ulid_resource_id}, got {actual_id}"
    )


@when('remaining cov resolve_namespaced_name is called with unknown ULID "{ulid}"')
def step_remaining_cov_resolve_unknown(context: Context, ulid: str) -> None:
    context.rbc_result = context.rbc_res_repo.resolve_namespaced_name(ulid)


@then("remaining cov resolve_namespaced_name should return None")
def step_remaining_cov_assert_resolve_none(context: Context) -> None:
    assert context.rbc_result is None, f"Expected None, got {context.rbc_result}"


# ═══════════════════════════════════════════════════════════════════════
# ResourceRepository.get_children: populated list (branch 2426→2420)
# ═══════════════════════════════════════════════════════════════════════


@given("remaining cov a parent resource linked to two child resources")
def step_remaining_cov_parent_with_children(context: Context) -> None:
    from ulid import ULID

    _ensure_resource_type_row(context.rbc_factory, "test/link-type")
    parent_id = str(ULID())
    child1_id = str(ULID())
    child2_id = str(ULID())
    for rid in [parent_id, child1_id, child2_id]:
        _create_resource_row(context.rbc_factory, rid, type_name="test/link-type")
    _create_link(context.rbc_factory, parent_id, child1_id)
    _create_link(context.rbc_factory, parent_id, child2_id)
    context.rbc_parent_id = parent_id


@when("remaining cov get_children is called on the parent resource")
def step_remaining_cov_get_children(context: Context) -> None:
    context.rbc_children = context.rbc_res_repo.get_children(context.rbc_parent_id)


@then("remaining cov {n:d} child resources should be returned")
def step_remaining_cov_assert_children_count(context: Context, n: int) -> None:
    assert len(context.rbc_children) == n, (
        f"Expected {n} children, got {len(context.rbc_children)}"
    )


# ═══════════════════════════════════════════════════════════════════════
# ResourceRepository.get_parents: populated list (branch 2459→2453)
# ═══════════════════════════════════════════════════════════════════════


@given("remaining cov a child resource linked from two parent resources")
def step_remaining_cov_child_with_parents(context: Context) -> None:
    from ulid import ULID

    _ensure_resource_type_row(context.rbc_factory, "test/link-type")
    child_id = str(ULID())
    parent1_id = str(ULID())
    parent2_id = str(ULID())
    for rid in [child_id, parent1_id, parent2_id]:
        _create_resource_row(context.rbc_factory, rid, type_name="test/link-type")
    _create_link(context.rbc_factory, parent1_id, child_id)
    _create_link(context.rbc_factory, parent2_id, child_id)
    context.rbc_child_id = child_id


@when("remaining cov get_parents is called on the child resource")
def step_remaining_cov_get_parents(context: Context) -> None:
    context.rbc_parents = context.rbc_res_repo.get_parents(context.rbc_child_id)


@then("remaining cov {n:d} parent resources should be returned")
def step_remaining_cov_assert_parents_count(context: Context, n: int) -> None:
    assert len(context.rbc_parents) == n, (
        f"Expected {n} parents, got {len(context.rbc_parents)}"
    )


# ═══════════════════════════════════════════════════════════════════════
# ResourceRepository._get_ancestors + _build_cycle_path (branches 2635→2633, 2661→2659)
# ═══════════════════════════════════════════════════════════════════════


@given('remaining cov resources "cyc-A" and "cyc-B" linked as A->B')
def step_remaining_cov_cycle_setup(context: Context) -> None:
    _ensure_resource_type_row(context.rbc_factory, "test/link-type")
    _create_resource_row(context.rbc_factory, "cyc-A", type_name="test/link-type")
    _create_resource_row(context.rbc_factory, "cyc-B", type_name="test/link-type")
    _create_link(context.rbc_factory, "cyc-A", "cyc-B")


@when("remaining cov link_child is called to create B->A forming a cycle")
def step_remaining_cov_link_cycle(context: Context) -> None:
    try:
        context.rbc_res_repo.link_child("cyc-B", "cyc-A")
        context.rbc_error = None
    except Exception as exc:
        context.rbc_error = exc


@then("remaining cov a CycleDetectedError should be raised with a path")
def step_remaining_cov_assert_cycle(context: Context) -> None:
    assert isinstance(context.rbc_error, CycleDetectedError), (
        f"Expected CycleDetectedError, got "
        f"{type(context.rbc_error).__name__}: {context.rbc_error}"
    )
    assert hasattr(context.rbc_error, "path"), "Error should have 'path' attribute"
    assert len(context.rbc_error.path) > 0, "Cycle path should not be empty"


# ═══════════════════════════════════════════════════════════════════════
# NamespacedProjectRepository.update: success path (branch 2928→2931)
# ═══════════════════════════════════════════════════════════════════════


@given("remaining cov an in-memory project database")
def step_remaining_cov_proj_db(context: Context) -> None:
    engine, factory = _make_engine_and_factory()
    context.rbc_engine = engine
    context.rbc_proj_factory = factory
    context.rbc_proj_repo = NamespacedProjectRepository(session_factory=factory)
    context.rbc_error = None


@given('remaining cov a project "{ns_name}" exists')
def step_remaining_cov_proj_exists(context: Context, ns_name: str) -> None:
    from cleveragents.domain.models.core.project import NamespacedProject

    parts = ns_name.split("/", 1)
    namespace = parts[0] if len(parts) == 2 else "local"
    name = parts[1] if len(parts) == 2 else parts[0]

    project = NamespacedProject(
        name=name,
        namespace=namespace,
        description="Original description",
    )
    context.rbc_proj_repo.create(project)
    context.rbc_project = project


@when('remaining cov the project "{ns_name}" is updated with new description')
def step_remaining_cov_proj_update(context: Context, ns_name: str) -> None:
    from cleveragents.domain.models.core.project import NamespacedProject

    parts = ns_name.split("/", 1)
    namespace = parts[0] if len(parts) == 2 else "local"
    name = parts[1] if len(parts) == 2 else parts[0]

    updated = NamespacedProject(
        name=name,
        namespace=namespace,
        description="Updated description via remaining cov test",
    )
    try:
        context.rbc_proj_repo.update(updated)
        context.rbc_error = None
    except Exception as exc:
        context.rbc_error = exc


@then("remaining cov the project update should succeed")
def step_remaining_cov_assert_proj_updated(context: Context) -> None:
    assert context.rbc_error is None, f"Unexpected error: {context.rbc_error}"
    # Verify the update persisted
    result = context.rbc_proj_repo.get(context.rbc_project.namespaced_name)
    assert result is not None, "Project not found after update"
    assert result.description == "Updated description via remaining cov test", (
        f"Expected updated description, got '{result.description}'"
    )


# ═══════════════════════════════════════════════════════════════════════
# AutomationProfileRepository.upsert: new insert (branch 4193→4200)
# ═══════════════════════════════════════════════════════════════════════


@given("remaining cov an in-memory automation profile database")
def step_remaining_cov_ap_db(context: Context) -> None:
    engine, factory = _make_engine_and_factory()
    context.rbc_engine = engine
    context.rbc_ap_factory = factory

    from cleveragents.infrastructure.database.repositories import (
        AutomationProfileRepository,
    )

    context.rbc_ap_repo = AutomationProfileRepository(session_factory=factory)
    context.rbc_error = None


@when('remaining cov a brand new profile "{name}" is upserted')
def step_remaining_cov_ap_new_upsert(context: Context, name: str) -> None:
    from cleveragents.domain.models.core.automation_profile import AutomationProfile

    profile = AutomationProfile(
        name=name,
        description="Brand new profile",
        schema_version="1.0",
    )
    try:
        context.rbc_ap_repo.upsert(profile)
        context.rbc_error = None
    except Exception as exc:
        context.rbc_error = exc


@then('remaining cov the profile "{name}" should be retrievable')
def step_remaining_cov_assert_ap_exists(context: Context, name: str) -> None:
    assert context.rbc_error is None, f"Unexpected error: {context.rbc_error}"
    result = context.rbc_ap_repo.get_by_name(name)
    assert result is not None, f"Profile '{name}' not found after upsert"
    assert result.name == name, f"Expected name '{name}', got '{result.name}'"


# ═══════════════════════════════════════════════════════════════════════
# ValidationAttachmentRepository.attach: project_name scope (branch 3633→3634)
# ═══════════════════════════════════════════════════════════════════════


@given("remaining cov an in-memory database with validation tables")
def step_remaining_cov_val_db(context: Context) -> None:
    engine, factory = _make_engine_and_factory()
    context.rbc_engine = engine
    context.rbc_val_factory = factory
    context.rbc_val_repo = ValidationAttachmentRepository(session_factory=factory)
    context.rbc_error = None


@when('remaining cov a validation is attached using project scope "{proj}"')
def step_remaining_cov_attach_with_project(context: Context, proj: str) -> None:
    try:
        context.rbc_attachment = context.rbc_val_repo.attach(
            validation_name="local/check-proj",
            resource_id="res-proj-1",
            mode="required",
            project_name=proj,
        )
        context.rbc_error = None
    except Exception as exc:
        context.rbc_error = exc


@then('remaining cov the returned attachment has project_name "{proj}"')
def step_remaining_cov_assert_attach_project(context: Context, proj: str) -> None:
    assert context.rbc_error is None, f"Unexpected error: {context.rbc_error}"
    att = context.rbc_attachment
    assert isinstance(att, dict), f"Expected dict, got {type(att)}"
    assert att.get("project_name") == proj, (
        f"Expected project_name '{proj}', got '{att.get('project_name')}'"
    )


@when(
    'remaining cov a validation is attached using project-plan scope "{proj}" "{plan}"'
)
def step_remaining_cov_attach_with_project_and_plan(
    context: Context,
    proj: str,
    plan: str,
) -> None:
    try:
        context.rbc_attachment = context.rbc_val_repo.attach(
            validation_name="local/check-proj-plan",
            resource_id="res-proj-plan-1",
            mode="informational",
            project_name=proj,
            plan_id=plan,
        )
        context.rbc_error = None
    except Exception as exc:
        context.rbc_error = exc


@then('remaining cov the returned attachment has project "{proj}" and plan "{plan}"')
def step_remaining_cov_assert_attach_project_plan(
    context: Context,
    proj: str,
    plan: str,
) -> None:
    assert context.rbc_error is None, f"Unexpected error: {context.rbc_error}"
    att = context.rbc_attachment
    assert isinstance(att, dict), f"Expected dict, got {type(att)}"
    assert att.get("project_name") == proj, (
        f"Expected project_name '{proj}', got '{att.get('project_name')}'"
    )
    assert att.get("plan_id") == plan, (
        f"Expected plan_id '{plan}', got '{att.get('plan_id')}'"
    )


# ═══════════════════════════════════════════════════════════════════════
# LifecyclePlanRepository.update: multiple project links (branch 1395→1394)
# ═══════════════════════════════════════════════════════════════════════


@given("remaining cov an in-memory lifecycle plan database")
def step_remaining_cov_plan_db(context: Context) -> None:
    engine, factory = _make_engine_and_factory()
    session = factory()
    context.rbc_engine = engine
    context.rbc_plan_factory = factory
    context.rbc_plan_session = session
    # Use a single-session factory so flush + commit are visible
    context.rbc_plan_session_factory = lambda: session

    from cleveragents.infrastructure.database.repositories import (
        ActionRepository,
        LifecyclePlanRepository,
    )

    context.rbc_action_repo = ActionRepository(
        session_factory=context.rbc_plan_session_factory,
    )
    context.rbc_plan_repo = LifecyclePlanRepository(
        session_factory=context.rbc_plan_session_factory,
    )
    context.rbc_error = None


@given('remaining cov a plan with action "{action_name}" exists')
def step_remaining_cov_plan_exists(context: Context, action_name: str) -> None:
    from ulid import ULID

    from cleveragents.domain.models.core.action import Action, ActionState
    from cleveragents.domain.models.core.plan import (
        NamespacedName,
        Plan,
        PlanIdentity,
        PlanPhase,
        PlanTimestamps,
        ProcessingState,
    )

    parts = action_name.split("/", 1)
    namespace = parts[0] if len(parts) == 2 else "local"
    short = parts[1] if len(parts) == 2 else parts[0]

    action = Action(
        namespaced_name=NamespacedName(namespace=namespace, name=short),
        description="test action",
        long_description=None,
        definition_of_done="verify it works",
        strategy_actor="local/strategist",
        execution_actor="local/executor",
        estimation_actor=None,
        review_actor=None,
        arguments=[],
        reusable=True,
        read_only=False,
        state=ActionState("available"),
        created_at=datetime.now(),
        updated_at=datetime.now(),
        created_by=None,
        tags=[],
    )
    context.rbc_action_repo.create(action)
    context.rbc_plan_session.commit()

    now = datetime.now()
    plan = Plan(
        identity=PlanIdentity(plan_id=str(ULID()), attempt=1),
        namespaced_name=NamespacedName(namespace="local", name="multi-link-plan"),
        action_name=action_name,
        description="plan for multi-link test",
        definition_of_done="verify links",
        phase=PlanPhase.STRATEGIZE,
        processing_state=ProcessingState.QUEUED,
        strategy_actor="local/strategist",
        execution_actor="local/executor",
        timestamps=PlanTimestamps(created_at=now, updated_at=now),
        error_message=None,
        error_details=None,
        created_by=None,
        tags=[],
        reusable=True,
        read_only=False,
    )
    context.rbc_plan_repo.create(plan)
    context.rbc_plan_session.commit()
    context.rbc_plan = plan


@when("remaining cov the plan is updated with 3 project links")
def step_remaining_cov_plan_update_links(context: Context) -> None:
    from cleveragents.domain.models.core.plan import (
        PlanTimestamps,
        ProjectLink,
    )

    plan = context.rbc_plan
    updated = plan.model_copy(
        update={
            "project_links": [
                ProjectLink(
                    project_name="local/proj-alpha", alias="alpha", read_only=False
                ),
                ProjectLink(
                    project_name="local/proj-beta", alias="beta", read_only=True
                ),
                ProjectLink(
                    project_name="local/proj-gamma", alias=None, read_only=False
                ),
            ],
            "timestamps": PlanTimestamps(
                created_at=plan.timestamps.created_at,
                updated_at=datetime.now(),
            ),
        }
    )
    try:
        context.rbc_plan_repo.update(updated)
        context.rbc_plan_session.commit()
        context.rbc_plan = updated
        context.rbc_error = None
    except Exception as exc:
        context.rbc_error = exc


@then("remaining cov the plan should have 3 project links after retrieval")
def step_remaining_cov_assert_plan_links(context: Context) -> None:
    assert context.rbc_error is None, f"Unexpected error: {context.rbc_error}"
    plan_id = context.rbc_plan.identity.plan_id
    retrieved = context.rbc_plan_repo.get(plan_id)
    assert retrieved is not None, f"Plan '{plan_id}' not found after update"
    assert len(retrieved.project_links) == 3, (
        f"Expected 3 project links, got {len(retrieved.project_links)}"
    )
    link_names = sorted(pl.project_name for pl in retrieved.project_links)
    assert link_names == sorted(
        [
            "local/proj-alpha",
            "local/proj-beta",
            "local/proj-gamma",
        ]
    ), f"Unexpected link names: {link_names}"
